doc/api/protected_branches.md
{{< details >}}
{{< /details >}}
Use this API to manage protected branches.
GitLab Premium and GitLab Ultimate support more granular protections for pushing to branches. Administrators can grant permission to modify and push to protected branches only to deploy keys, instead of specific users.
The ProtectedRefAccess.allowed_access_levels method defines the following access levels used across
push, merge, and unprotect configurations.
0: No access - Valid for push and merge access levels only. Not valid for unprotect access levels.30: Developer40: Maintainer60: Administrator - Valid for GitLab Self-Managed only.In addition to role-based access levels, you can assign access by:
user_id): Valid for push, merge, and unprotect access levels.group_id): Valid for push, merge, and unprotect access levels. The group must have
the Developer, Maintainer, or Owner role for the project.deploy_key_id): Valid for push access levels only.For more information, see the protect repository branches examples.
[!note] To avoid permanently locking protection settings for a branch, ensure at least one user or group retains unprotect permissions for the branch at all times. For more information, see control who can unprotect branches.
{{< history >}}
{{< /history >}}
Get a list of protected branches from a project as they are defined in the UI. If a wildcard is set, it is returned instead of the exact name of the branches that match that wildcard.
GET /projects/:id/protected_branches
Supported attributes:
| Attribute | Type | Required | Description |
|---|---|---|---|
id | integer or string | Yes | ID or URL-encoded path of the project. |
search | string | No | Name or part of the name of protected branches to search for. |
If successful, returns 200 OK and the
following response attributes:
| Attribute | Type | Description |
|---|---|---|
allow_force_push | boolean | If true, force push is allowed on this branch. |
code_owner_approval_required | boolean | If true, code owner approval is required for pushes to this branch. |
id | integer | ID of the protected branch. |
inherited | boolean | If true, protection settings are inherited from parent group. Premium and Ultimate only. |
merge_access_levels | array | Array of merge access level configurations. |
merge_access_levels[].access_level | integer | Access level for merging. |
merge_access_levels[].access_level_description | string | Human-readable description of the access level. |
merge_access_levels[].group_id | integer | ID of the group with merge access. Premium and Ultimate only. |
merge_access_levels[].id | integer | ID of the merge access level configuration. |
merge_access_levels[].user_id | integer | ID of the user with merge access. Premium and Ultimate only. |
name | string | Name of the protected branch. |
push_access_levels | array | Array of push access level configurations. |
push_access_levels[].access_level | integer | Access level for pushing. |
push_access_levels[].access_level_description | string | Human-readable description of the access level. |
push_access_levels[].deploy_key_id | integer | ID of the deploy key with push access. |
push_access_levels[].group_id | integer | ID of the group with push access. Premium and Ultimate only. |
push_access_levels[].id | integer | ID of the push access level configuration. |
push_access_levels[].user_id | integer | ID of the user with push access. Premium and Ultimate only. |
In the following example request, the project ID is 5.
curl --header "PRIVATE-TOKEN: <your_access_token>" \
--url "https://gitlab.example.com/api/v4/projects/5/protected_branches"
The following example response includes:
100 and 101.push_access_levels with IDs 1001, 1002, and 1003.merge_access_levels with IDs 2001 and 2002.[
{
"id": 100,
"name": "main",
"push_access_levels": [
{
"id": 1001,
"access_level": 40,
"access_level_description": "Maintainers"
},
{
"id": 1002,
"access_level": 40,
"access_level_description": "Deploy key",
"deploy_key_id": 1
}
],
"merge_access_levels": [
{
"id": 2001,
"access_level": 40,
"access_level_description": "Maintainers"
}
],
"allow_force_push":false,
"code_owner_approval_required": false
},
{
"id": 101,
"name": "release/*",
"push_access_levels": [
{
"id": 1003,
"access_level": 40,
"access_level_description": "Maintainers"
}
],
"merge_access_levels": [
{
"id": 2002,
"access_level": 40,
"access_level_description": "Maintainers"
}
],
"allow_force_push":false,
"code_owner_approval_required": false
}
]
Users on GitLab Premium or Ultimate also see
the user_id, group_id, and inherited parameters. If the inherited parameter
exists, the setting was inherited from the project's group.
The following example response includes:
100.push_access_levels with IDs 1001 and 1002.merge_access_levels with ID 2001.[
{
"id": 101,
"name": "main",
"push_access_levels": [
{
"id": 1001,
"access_level": 40,
"user_id": null,
"group_id": null,
"access_level_description": "Maintainers"
},
{
"id": 1002,
"access_level": 40,
"access_level_description": "Deploy key",
"deploy_key_id": 1,
"user_id": null,
"group_id": null
}
],
"merge_access_levels": [
{
"id": 2001,
"access_level": null,
"user_id": null,
"group_id": 1234,
"access_level_description": "Example Merge Group"
}
],
"allow_force_push":false,
"code_owner_approval_required": false,
"inherited": true
}
]
Retrieves a specified protected branch or wildcard protected branch.
GET /projects/:id/protected_branches/:name
Supported attributes:
| Attribute | Type | Required | Description |
|---|---|---|---|
id | integer or string | Yes | ID or URL-encoded path of the project. |
name | string | Yes | Name of the branch or wildcard. |
If successful, returns 200 OK and the
following response attributes:
| Attribute | Type | Description |
|---|---|---|
allow_force_push | boolean | If true, force push is allowed on this branch. |
code_owner_approval_required | boolean | If true, code owner approval is required for pushes to this branch. |
id | integer | ID of the protected branch. |
merge_access_levels | array | Array of merge access level configurations. |
merge_access_levels[].access_level | integer | Access level for merging. |
merge_access_levels[].access_level_description | string | Human-readable description of the access level. |
merge_access_levels[].group_id | integer | ID of the group with merge access. Premium and Ultimate only. |
merge_access_levels[].id | integer | ID of the merge access level configuration. |
merge_access_levels[].user_id | integer | ID of the user with merge access. Premium and Ultimate only. |
name | string | Name of the protected branch. |
push_access_levels | array | Array of push access level configurations. |
push_access_levels[].access_level | integer | Access level for pushing. |
push_access_levels[].access_level_description | string | Human-readable description of the access level. |
push_access_levels[].group_id | integer | ID of the group with push access. Premium and Ultimate only. |
push_access_levels[].id | integer | ID of the push access level configuration. |
push_access_levels[].user_id | integer | ID of the user with push access. Premium and Ultimate only. |
In the following example request, the project ID is 5 and branch name is main:
curl --header "PRIVATE-TOKEN: <your_access_token>" \
--url "https://gitlab.example.com/api/v4/projects/5/protected_branches/main"
Example response:
{
"id": 101,
"name": "main",
"push_access_levels": [
{
"id": 1001,
"access_level": 40,
"access_level_description": "Maintainers"
}
],
"merge_access_levels": [
{
"id": 2001,
"access_level": 40,
"access_level_description": "Maintainers"
}
],
"allow_force_push":false,
"code_owner_approval_required": false
}
Users on GitLab Premium or Ultimate also see
the user_id and group_id parameters.
Example response:
{
"id": 101,
"name": "main",
"push_access_levels": [
{
"id": 1001,
"access_level": 40,
"user_id": null,
"group_id": null,
"access_level_description": "Maintainers"
}
],
"merge_access_levels": [
{
"id": 2001,
"access_level": null,
"user_id": null,
"group_id": 1234,
"access_level_description": "Example Merge Group"
}
],
"allow_force_push":false,
"code_owner_approval_required": false
}
{{< history >}}
deploy_key_id configuration introduced in GitLab 17.5.deploy_key_id configuration moved from GitLab Premium to GitLab Free in GitLab 18.10.{{< /history >}}
Protect a single repository branch or several project repository branches using a wildcard protected branch.
POST /projects/:id/protected_branches
Supported attributes:
| Attribute | Type | Required | Description |
|---|---|---|---|
id | integer or string | Yes | ID or URL-encoded path of the project. |
name | string | Yes | Name of the branch or wildcard. |
allow_force_push | boolean | No | If true, members who can push to this branch can also force push. Default is false. |
allowed_to_merge | array | No | Array of merge access levels, with each described by a hash of the form {user_id: integer}, {group_id: integer}, or {access_level: integer}. Premium and Ultimate only. |
allowed_to_push | array | No | Array of push access levels, with each described by a hash of the form {user_id: integer}, {group_id: integer}, {deploy_key_id: integer}, or {access_level: integer}. user_id, group_id, and access_level are Premium and Ultimate only. |
allowed_to_unprotect | array | No | Array of unprotect access levels, with each described by a hash of the form {user_id: integer}, {group_id: integer}, or {access_level: integer}. Access level No access is not available for this field. Premium and Ultimate only. |
code_owner_approval_required | boolean | No | If true, prevents pushes to this branch if it matches an item in the CODEOWNERS file. Default is false. Premium and Ultimate only. |
merge_access_level | integer | No | Access levels allowed to merge. Default is 40 (Maintainer role). |
push_access_level | integer | No | Access levels allowed to push. Default is 40 (Maintainer role). |
unprotect_access_level | integer | No | Access levels allowed to unprotect. Default is 40 (Maintainer role). 0 (No access) is not valid. |
When you configure access levels:
allowed_to_push and allowed_to_merge.id in the allowed_to_push, allowed_to_merge, or allowed_to_unprotect arrays.
The id field identifies an existing access level record and is only valid when you
update a protected branch. If you include an id that does not
match an existing record, the API returns 404 Not Found.This behavior differs from the UI, which automatically clears other role selections
when you select No one (access_level: 0).
If successful, returns 201 Created and the
following response attributes:
| Attribute | Type | Description |
|---|---|---|
allow_force_push | boolean | If true, force push is allowed on this branch. |
code_owner_approval_required | boolean | If true, code owner approval is required for pushes to this branch. |
id | integer | ID of the protected branch. |
merge_access_levels | array | Array of merge access level configurations. |
merge_access_levels[].access_level | integer | Access level for merging. |
merge_access_levels[].access_level_description | string | Human-readable description of the access level. |
merge_access_levels[].group_id | integer | ID of the group with merge access. Premium and Ultimate only. |
merge_access_levels[].id | integer | ID of the merge access level configuration. |
merge_access_levels[].user_id | integer | ID of the user with merge access. Premium and Ultimate only. |
name | string | Name of the protected branch. |
push_access_levels | array | Array of push access level configurations. |
push_access_levels[].access_level | integer | Access level for pushing. |
push_access_levels[].access_level_description | string | Human-readable description of the access level. |
push_access_levels[].deploy_key_id | integer | ID of the deploy key with push access. |
push_access_levels[].group_id | integer | ID of the group with push access. Premium and Ultimate only. |
push_access_levels[].id | integer | ID of the push access level configuration. |
push_access_levels[].user_id | integer | ID of the user with push access. Premium and Ultimate only. |
unprotect_access_levels | array | Array of unprotect access level configurations. |
unprotect_access_levels[].access_level | integer | Access level for unprotecting. |
unprotect_access_levels[].access_level_description | string | Human-readable description of the access level. |
unprotect_access_levels[].group_id | integer | ID of the group with unprotect access. Premium and Ultimate only. |
unprotect_access_levels[].id | integer | ID of the unprotect access level configuration. |
unprotect_access_levels[].user_id | integer | ID of the user with unprotect access. Premium and Ultimate only. |
In the following example request, the project ID is 5 and branch name is *-stable.
curl --request POST \
--header "PRIVATE-TOKEN: <your_access_token>" \
--url "https://gitlab.example.com/api/v4/projects/5/protected_branches?name=*-stable&push_access_level=30&merge_access_level=30&unprotect_access_level=40"
The example response includes:
101.push_access_levels with ID 1001.merge_access_levels with ID 2001.unprotect_access_levels with ID 3001.{
"id": 101,
"name": "*-stable",
"push_access_levels": [
{
"id": 1001,
"access_level": 30,
"access_level_description": "Developers + Maintainers"
}
],
"merge_access_levels": [
{
"id": 2001,
"access_level": 30,
"access_level_description": "Developers + Maintainers"
}
],
"unprotect_access_levels": [
{
"id": 3001,
"access_level": 40,
"access_level_description": "Maintainers"
}
],
"allow_force_push":false,
"code_owner_approval_required": false
}
Users on GitLab Premium or Ultimate also see
the user_id and group_id parameters:
The following example response includes:
101.push_access_levels with ID 1001.merge_access_levels with ID 2001.unprotect_access_levels with ID 3001.{
"id": 1,
"name": "*-stable",
"push_access_levels": [
{
"id": 1001,
"access_level": 30,
"user_id": null,
"group_id": null,
"access_level_description": "Developers + Maintainers"
}
],
"merge_access_levels": [
{
"id": 2001,
"access_level": 30,
"user_id": null,
"group_id": null,
"access_level_description": "Developers + Maintainers"
}
],
"unprotect_access_levels": [
{
"id": 3001,
"access_level": 40,
"user_id": null,
"group_id": null,
"access_level_description": "Maintainers"
}
],
"allow_force_push":false,
"code_owner_approval_required": false
}
{{< details >}}
{{< /details >}}
Elements in the allowed_to_push / allowed_to_merge / allowed_to_unprotect array should take the
form {user_id: integer}, {group_id: integer}, or {access_level: integer}.
Each user must have access to the project and each group must have this project shared.
These access levels allow more granular control over protected branch access.
For more information, see configure group permissions.
The following example request creates a protected branch with user push access and group merge access.
The user_id is 2 and the group_id is 3.
curl --request POST \
--header "PRIVATE-TOKEN: <your_access_token>" \
--url "https://gitlab.example.com/api/v4/projects/5/protected_branches?name=*-stable&allowed_to_push%5B%5D%5Buser_id%5D=2&allowed_to_merge%5B%5D%5Bgroup_id%5D=3"
The following example response includes:
101.push_access_levels with ID 1001.merge_access_levels with ID 2001.unprotect_access_levels with ID 3001.{
"id": 101,
"name": "*-stable",
"push_access_levels": [
{
"id": 1001,
"access_level": null,
"user_id": 2,
"group_id": null,
"access_level_description": "Administrator"
}
],
"merge_access_levels": [
{
"id": 2001,
"access_level": null,
"user_id": null,
"group_id": 3,
"access_level_description": "Example Merge Group"
}
],
"unprotect_access_levels": [
{
"id": 3001,
"access_level": 40,
"user_id": null,
"group_id": null,
"access_level_description": "Maintainers"
}
],
"allow_force_push":false,
"code_owner_approval_required": false
}
{{< history >}}
{{< /history >}}
Elements in the allowed_to_push array should take the form {user_id: integer}, {group_id: integer},
{deploy_key_id: integer}, or {access_level: integer}.
The deploy key must be enabled for your project and it must have write access to your project repository.
For other requirements, see allow deploy keys to push to a protected branch.
Example request:
curl --request POST \
--header "PRIVATE-TOKEN: <your_access_token>" \
--url "https://gitlab.example.com/api/v4/projects/5/protected_branches?name=*-stable&allowed_to_push[][deploy_key_id]=1"
The following example response includes:
101.push_access_levels with ID 1001.merge_access_levels with ID 2001.unprotect_access_levels with ID 3001.{
"id": 101,
"name": "*-stable",
"push_access_levels": [
{
"id": 1001,
"access_level": null,
"user_id": null,
"group_id": null,
"deploy_key_id": 1,
"access_level_description": "Deploy"
}
],
"merge_access_levels": [
{
"id": 2001,
"access_level": 40,
"user_id": null,
"group_id": null,
"access_level_description": "Maintainers"
}
],
"unprotect_access_levels": [
{
"id": 3001,
"access_level": 40,
"user_id": null,
"group_id": null,
"access_level_description": "Maintainers"
}
],
"allow_force_push":false,
"code_owner_approval_required": false
}
{{< details >}}
{{< /details >}}
{{< history >}}
{{< /history >}}
Example request:
curl --request POST \
--header "PRIVATE-TOKEN: <your_access_token>" \
--header "Content-Type: application/json" \
--data '{
"name": "main",
"allowed_to_push": [
{"access_level": 30}
],
"allowed_to_merge": [
{"access_level": 30},
{"access_level": 40}
]
}' \
--url "https://gitlab.example.com/api/v4/projects/5/protected_branches"
The following example response includes:
105.push_access_levels with ID 1001.merge_access_levels with IDs 2001 and 2002.unprotect_access_levels with ID 3001.{
"id": 105,
"name": "main",
"push_access_levels": [
{
"id": 1001,
"access_level": 30,
"access_level_description": "Developers + Maintainers",
"user_id": null,
"group_id": null
}
],
"merge_access_levels": [
{
"id": 2001,
"access_level": 30,
"access_level_description": "Developers + Maintainers",
"user_id": null,
"group_id": null
},
{
"id": 2002,
"access_level": 40,
"access_level_description": "Maintainers",
"user_id": null,
"group_id": null
}
],
"unprotect_access_levels": [
{
"id": 3001,
"access_level": 40,
"access_level_description": "Maintainers",
"user_id": null,
"group_id": null
}
],
"allow_force_push":false,
"code_owner_approval_required": false
}
{{< details >}}
{{< /details >}}
To create a protected branch where only a specific group can unprotect the branch:
curl --request POST \
--header "PRIVATE-TOKEN: <your_access_token>" \
--header "Content-Type: application/json" \
--data '{
"name": "production",
"allowed_to_unprotect": [
{"group_id": 789}
]
}' \
--url "https://gitlab.example.com/api/v4/projects/5/protected_branches"
To allow multiple types of users to unprotect a branch:
curl --request POST \
--header "PRIVATE-TOKEN: <your_access_token>" \
--header "Content-Type: application/json" \
--data '{
"name": "main",
"allowed_to_unprotect": [
{"user_id": 123},
{"group_id": 456},
{"access_level": 40}
]
}' \
--url "https://gitlab.example.com/api/v4/projects/5/protected_branches"
This configuration allows these users to unprotect the branch:
123.456.Unprotect the given protected branch or wildcard protected branch.
DELETE /projects/:id/protected_branches/:name
Supported attributes:
| Attribute | Type | Required | Description |
|---|---|---|---|
id | integer or string | Yes | ID or URL-encoded path of the project. |
name | string | Yes | Name of the branch. |
If successful, returns 204 No Content.
In the following example request, the project ID is 5 and branch name is *-stable:
curl --request DELETE \
--header "PRIVATE-TOKEN: <your_access_token>" \
--url "https://gitlab.example.com/api/v4/projects/5/protected_branches/*-stable"
{{< history >}}
deploy_key_id configuration introduced in GitLab 17.5.{{< /history >}}
Update a protected branch.
PATCH /projects/:id/protected_branches/:name
Supported attributes:
| Attribute | Type | Required | Description |
|---|---|---|---|
id | integer or string | Yes | ID or URL-encoded path of the project. |
name | string | Yes | Name of the branch or wildcard. |
allow_force_push | boolean | No | If true, members who can push to this branch can also force push. |
allowed_to_merge | array | No | Array of merge access levels, with each described by a hash of the form {user_id: integer}, {group_id: integer}, or {access_level: integer}. Premium and Ultimate only. |
allowed_to_push | array | No | Array of push access levels, with each described by a hash of the form {user_id: integer}, {group_id: integer}, {deploy_key_id: integer}, or {access_level: integer}. user_id, group_id, and access_level are Premium and Ultimate only. |
allowed_to_unprotect | array | No | Array of unprotect access levels, with each described by a hash of the form {user_id: integer}, {group_id: integer}, {access_level: integer}, or {id: integer, _destroy: true} to destroy an existing access level. Access level No access is not available for this field. Premium and Ultimate only. |
code_owner_approval_required | boolean | No | If true, prevents pushes to this branch if it matches an item in the CODEOWNERS file. Premium and Ultimate only. |
For information about how access levels interact when you set multiple values, see protect repository branches.
If successful, returns 200 OK and the
following response attributes:
| Attribute | Type | Description |
|---|---|---|
allow_force_push | boolean | If true, force push is allowed on this branch. |
code_owner_approval_required | boolean | If true, code owner approval is required for pushes to this branch. |
id | integer | ID of the protected branch. |
merge_access_levels | array | Array of merge access level configurations. |
merge_access_levels[].access_level | integer | Access level for merging. |
merge_access_levels[].access_level_description | string | Human-readable description of the access level. |
merge_access_levels[].group_id | integer | ID of the group with merge access. Premium and Ultimate only. |
merge_access_levels[].id | integer | ID of the merge access level configuration. |
merge_access_levels[].user_id | integer | ID of the user with merge access. Premium and Ultimate only. |
name | string | Name of the protected branch. |
push_access_levels | array | Array of push access level configurations. |
push_access_levels[].access_level | integer | Access level for pushing. |
push_access_levels[].access_level_description | string | Human-readable description of the access level. |
push_access_levels[].deploy_key_id | integer | ID of the deploy key with push access. |
push_access_levels[].group_id | integer | ID of the group with push access. Premium and Ultimate only. |
push_access_levels[].id | integer | ID of the push access level configuration. |
push_access_levels[].user_id | integer | ID of the user with push access. Premium and Ultimate only. |
unprotect_access_levels | array | Array of unprotect access level configurations. |
unprotect_access_levels[].access_level | integer | Access level for unprotecting. |
unprotect_access_levels[].access_level_description | string | Human-readable description of the access level. |
unprotect_access_levels[].group_id | integer | ID of the group with unprotect access. Premium and Ultimate only. |
unprotect_access_levels[].id | integer | ID of the unprotect access level configuration. |
unprotect_access_levels[].user_id | integer | ID of the user with unprotect access. Premium and Ultimate only. |
Example request:
curl --request PATCH \
--header "PRIVATE-TOKEN: <your_access_token>" \
--url "https://gitlab.example.com/api/v4/projects/5/protected_branches/feature-branch?allow_force_push=true&code_owner_approval_required=true"
Elements in the allowed_to_push, allowed_to_merge, and allowed_to_unprotect arrays should
be one of user_id, group_id, or access_level, and take the form {user_id: integer}, {group_id: integer} or
{access_level: integer}.
allowed_to_push includes an extra element, deploy_key_id, that takes the form {deploy_key_id: integer}.
To update:
user_id: Ensure the updated user has access to the project. Include the access level
record's id in the hash.group_id: Ensure the updated group has this project shared.
Include the access level record's id in the hash.deploy_key_id: Ensure the deploy key is enabled for your project and has write access
to your project repository.To update any other field on an existing access level record, include the record's id in the hash.
To delete, you must pass _destroy set to true. See the following examples.
push_access_level recordcurl --header 'Content-Type: application/json' --request PATCH \
--data '{"allowed_to_push": [{"access_level": 40}]}' \
--header "PRIVATE-TOKEN: <your_access_token>" \
--url "https://gitlab.example.com/api/v4/projects/22034114/protected_branches/main"
Example response:
{
"name": "main",
"push_access_levels": [
{
"id": 12,
"access_level": 40,
"access_level_description": "Maintainers",
"user_id": null,
"group_id": null
}
]
}
push_access_level recordcurl --header 'Content-Type: application/json' --request PATCH \
--data '{"allowed_to_push": [{"id": 12, "access_level": 0}]}' \
--header "PRIVATE-TOKEN: <your_access_token>" \
--url "https://gitlab.example.com/api/v4/projects/22034114/protected_branches/main"
Example response:
{
"name": "main",
"push_access_levels": [
{
"id": 12,
"access_level": 0,
"access_level_description": "No One",
"user_id": null,
"group_id": null
}
]
}
push_access_level recordcurl --header 'Content-Type: application/json' --request PATCH \
--data '{"allowed_to_push": [{"id": 12, "_destroy": true}]}' \
--header "PRIVATE-TOKEN: <your_access_token>" \
--url "https://gitlab.example.com/api/v4/projects/22034114/protected_branches/main"
Example response:
{
"name": "main",
"push_access_levels": []
}
unprotect_access_level recordPrerequisites:
allowed_to_unprotect configuration.user_id must be a project member.group_id must have access to the project.To modify who can unprotect an existing protected branch, include the id of the existing access
level record. For example:
curl --request PATCH \
--header "PRIVATE-TOKEN: <your_access_token>" \
--header "Content-Type: application/json" \
--data '{
"allowed_to_unprotect": [
{"id": 17486, "user_id": 3791}
]
}' \
--url "https://gitlab.example.com/api/v4/projects/5/protected_branches/main"
To remove specific access levels, use _destroy: true.