Service accounts
- Create a service account for a whole group
- Create a service account for one repository
- Find the service accounts in a group or repository
- Look up one service account
- Give a service account its credentials
- Change a service account's name or access level
- Give a service account a picture
- Remove a service account's picture
- Retire a service account
Overview
A service account is an identity nobody logs into — a deploy bot, a release script, a nightly job — that belongs to a group or a single repository instead of to a person. It carries its own access level, authenticates with tokens minted for it, and appears in history under its own name, so your automation keeps working when the engineer who set it up changes team.
Give one the smallest access level that does its job, point your automation at its token, and revoke that token when the automation retires. Deleting the account takes every token issued for it with it, which is what makes a service account a safer home for a long-lived credential than somebody's personal token.
Quick start
Give automation its own identity
Start at Service accounts in the side navigation.
Choose New service account.
Name it after the job it does, and pick the lowest level that job needs.
Choose Create service account, then give it a token so the job can authenticate as it.
Before you start
- Owner on the group to create a group account, or Maintainer on the repository to create a repository one. Creating in a group is the higher bar because a group account reaches every repository underneath it.
- The same standing as creating it, to rename, re-level, re-picture, delete or mint a token for an account— Owner on the group for a group account, Maintainer on the repository for a repository one — and a level at least the account's own. You cannot steer an account that holds more access than you do: a token carries everything its account can do, so being able to mint one for an Owner-level account would be a way to act as Owner. Levels are inherited downwards, so a level you hold on any group above the account's scope counts; one held on an unrelated branch of the tree does not.
- Reporter on the scope to read one.Listing and reading follow the scope's visibility: a public or internal group answers any signed-in person, a private one answers only its members. None of these calls answer an anonymous caller at all.
- A signed-in person.A set of calls is closed to a service-account caller whatever scopes its token carries, because each one records a person: creating another service account, minting or listing access tokens, creating a group or a repository, starring one, opening, closing or merging a merge request, and asking for a limit increase. They answer
400 BAD_REQUESTsaying the caller is not supported for the operation, rather than403. A provisioning script running as a bot has to stop one step short of minting new identities, and a bot that needs a merge request opened needs a person to open it. - A token with the right scope, if you are calling the API or the CLI rather than clicking:
service_account:readto list and read,service_account:createto create,service_account:updateto rename, re-level or change the picture,service_account:deleteto delete. Scopes are fixed when a token is minted and cannot be added afterwards. token:manageas well, if the same script also mints the account's credentials. That is a separate call on a separate scope, described in Access tokens.- The group or repository already exists, and you have picked which one. An account's scope is fixed at creation and cannot be moved.
How it works
Where an account lives, and how far it reaches
Every service account is owned by exactly one scope, and its access level applies to that scope and everything nested inside it.
acme ← an account here works in every group and repository below└── platform ├── api-gateway ← an account here works in this repository only └── billingA group account with Developer level can push to every repository in that group's subtree, including ones created after the account was. A repository account reaches that one repository and nothing else. There is no way to add a service account to a second scope: if a bot needs to work in two unrelated places, either create one account in each, or create a single account on the nearest group that contains both.
That reach is also why a service account does not appear in a scope's member list. Its access level is part of the account itself rather than a membership you could remove separately, so it lives and dies with the account and shows up under Service accounts rather than under Members.
The name you choose and the identity we generate
You choose one thing: the display name, between 1 and 100 characters, and it is the only part meant for humans. The username and the email address are generated for you at creation and never change — not when you rename the account, not when the group is renamed. A group account's username reads sa_group_9f2c1a04_7b3d81: the kind of scope, the first eight characters of that scope's id, and six random ones. A repository account's reads sa_repository_… instead. The email is that username at @noreply.gitmoap. Nothing is delivered to that address; it exists so the account can be an author, an assignee and a commit identity like anyone else.
Nothing about creation is idempotent. Two calls with the same name produce two separate accounts with two separate identities, so a provisioning script that runs twice leaves you with a duplicate bot rather than the one you meant.
You cannot create an account more powerful than you are
The level you assign is compared against your own level on the scope, and anything higher is refused. The same ceiling applies when you raise an existing account's level later, so there is no two-step route around it.
No account is an Owner, whoever creates it: Maintainer is the highest level a service account can hold. Owner is the level that changes who else has access, and that stays with people.
| Your level on the scope | Levels you can assign |
|---|---|
| Owner | Guest, Reporter, Developer, Maintainer |
| Maintainer | Guest, Reporter, Developer, Maintainer |
Who can see one, and what is recorded
Names, levels and creation details are readable by anyone who can read the scope — on a public or internal group or repository, that is every signed-in person. Tokens are never part of that: no listing, detail view or export contains a secret, and a token's plaintext is shown exactly once, at the moment it is minted.
Reading a single service account is recorded in the audit log, as is every create, rename, level change, picture change and delete. Listing is not. If you need to prove who gave a bot Maintainer and when, that is the record to read.
How to
Create a service account for a whole group
Pick the group whose subtree the account should work in, give it a name a human will recognise in an activity list, and give it a level. Start lower than feels comfortable — Reporter reads, Developer pushes to unprotected branches and opens merge requests, Maintainer pushes to protected branches and changes settings. Raising the level later is one call; discovering a bot had Owner for a year is not recoverable.
Open the group, choose Settings, then Service accounts, then New service account. Give it a display name and pick an access level — the picker offers only the levels at or below your own. Create adds it to the list and shows its generated username, which is the name the account acts under everywhere else.
gitmoap sa create-group \ --group-id <id> \ --access-level guest|reporter|developer|maintainer|owner \ --name <name>curl -X POST https://api.gitmoap.example/groups/{group_id}/service-accounts \ -H "Authorization: Bearer $GITMOAP_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "access_level": "<one of developer | guest | maintainer | owner | reporter>", "name": "<string>" }'Creating the account does not give it any credentials. It cannot do anything at all until you mint it a token.
Create a service account for one repository
Use this when the automation only ever touches one repository — a deploy job, a docs publisher, a bot that labels merge requests. The account has no standing anywhere else in the tree, so a leaked token cannot be pointed at a sibling repository.
Open the repository, choose Settings, then Service accounts, then New service account. Name it, pick a level, and Create. The list on this page shows only this repository's accounts — accounts created on a group above it work here too, and are managed from that group.
gitmoap sa create-repo \ --repo-id <id> \ --access-level guest|reporter|developer|maintainer|owner \ --name <name>curl -X POST https://api.gitmoap.example/repositories/{repo_id}/service-accounts \ -H "Authorization: Bearer $GITMOAP_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "access_level": "<one of developer | guest | maintainer | owner | reporter>", "name": "<string>" }'Find the service accounts in a group or repository
Accounts come back newest first, with their level, their generated username and who created them. A group's listing shows the accounts owned by that group; it does not include the accounts owned by its child groups and repositories, which are listed on those.
Open the group, choose Settings, then Service accounts. Each row shows the display name, the generated username, the access level and who created it. Long lists page as you scroll.
gitmoap sa list-group --group-id <id>curl -X GET https://api.gitmoap.example/groups/{group_id}/service-accounts \ -H "Authorization: Bearer $GITMOAP_TOKEN"Open the repository, choose Settings, then Service accounts for the same list, scoped to this repository.
gitmoap sa list-repo --repo-id <id>curl -X GET https://api.gitmoap.example/repositories/{repo_id}/service-accounts \ -H "Authorization: Bearer $GITMOAP_TOKEN"Look up one service account
Useful when a comment, a work item or a commit turns out to be the work of sa_repository_… and you want to know what it is, who created it and what it can do.
Choose the account in the Service accounts list, or click the author name anywhere it appears in history. The detail panel shows the display name, the generated username and email, the owning scope, the access level, and who created it and when.
gitmoap sa get --sa-id <id>curl -X GET https://api.gitmoap.example/service-accounts/{sa_id} \ -H "Authorization: Bearer $GITMOAP_TOKEN"Give a service account its credentials
A service account authenticates with tokens minted for it, which begin gms_. Minting one needs token:manage on your own token as well as the standing to manage the account — Owner on the group for a group account, Maintainer on the repository for a repository one, and a level at least the account's own — and the plaintext is shown once and never again.
gitmoap access-tokens create-sa-token \ --sa-id <service-account-id> \ --name "deploy from CI" \ --scopes '["git:read","git:write"]' \ --expires-in-days 90Replace <service-account-id> with the id from gitmoap sa list-group --group-id <your-group> --output json. A token cannot raise the account's access level: one carrying git:write still pushes as a Reporter if that is what the account is. If you bind the token to a group or repository, that scope has to be one you can see yourself. And minting, like creating, has to be done as a person — a service account's own token cannot mint tokens, for itself or for anything else. The full treatment, including expiry, resource binding and revocation, is in Access tokens.
Change a service account's name or access level
Renaming is cosmetic and safe. Changing the level takes effect on the account's very next request, including requests already in flight from a running job, and applies to every token the account holds — you cannot give one token more reach than another by editing the account.
At least one field has to change; an empty update is refused rather than treated as a no-op.
Open the account from the Service accounts list and choose Edit. Change the display name, the access level, or both, then Save. The level picker again offers only the levels at or below your own.
gitmoap sa update --sa-id <id>curl -X PATCH https://api.gitmoap.example/service-accounts/{sa_id} \ -H "Authorization: Bearer $GITMOAP_TOKEN"The scope is not editable here, and there is no route that makes it editable. Moving a bot from one group to another means creating a new account in the destination, minting it a token, switching your automation over, then deleting the old one.
Give a service account a picture
A picture makes a bot's activity obvious at a glance in a busy discussion. Uploading happens in two steps: ask for an upload address, then send the image bytes to it yourself.
You have to declare the exact byte size up front. That number is bound into the upload address, so an upload that sends a different number of bytes is rejected by the storage service rather than silently truncated, and the address itself stops working after five minutes. image/png, image/jpeg, image/gif and image/webp are accepted; anything else is refused before an address is issued.
Open the account from the Service accounts list, choose Edit, then click the picture and pick a file. The browser handles the upload for you and the new picture appears once it finishes.
gitmoap sa upload-avatar \ --sa-id <id> \ --content-length <n> \ --content-type <content type>curl -X PUT https://api.gitmoap.example/service-accounts/{sa_id}/avatar \ -H "Authorization: Bearer $GITMOAP_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "content_length": "<int64>", "content_type": "<string>" }'An account can instead carry the address of a picture you host elsewhere, set with the avatar field when you create or update it. When that is set it wins over anything uploaded here.
Remove a service account's picture
Removing the picture deletes the stored image, and no copy is kept — keep the original file somewhere if you might want it back. The account keeps working exactly as before and falls back to the default image derived from its generated address.
Open the account, choose Edit, then Remove picture. The change is immediate.
gitmoap sa delete-avatar --sa-id <id>curl -X DELETE https://api.gitmoap.example/service-accounts/{sa_id}/avatar \ -H "Authorization: Bearer $GITMOAP_TOKEN"Retire a service account
Warning
Deleting a service account is immediate and cannot be undone. Every token ever minted for it stops working in the same instant, so any job still using one starts failing its next request with 401 UNAUTHORIZED. The identity is gone and cannot be recreated: a new account with the same display name gets a new username and a new email, and none of the old activity attaches to it. Commits it made are git objects and are untouched, but they now name an identity nothing resolves.
Before you delete, find out what is still using it. The account's own contribution history shows what it has been doing and how recently, and its token list shows how many credentials exist and when each was last used. A bot with a token used in the last hour is a bot something depends on.
An account that has left records behind often cannot be deleted at all. While it is still named as the creator of something the platform keeps — a comment it wrote, a work item it filed, a release it cut, a pipeline run it started — the delete is refused with 409 CONFLICT rather than performed, so the record is never quietly detached from whoever made it. Revoking its tokens is the way to retire a bot that has done work.
If you only need to stop it temporarily, revoke its tokens instead and keep the account. Revoking is reversible in the sense that matters — you mint a new token when you want the bot back — and it leaves the identity, its history and its level intact.
Open the account from the Service accounts list and choose Delete. The confirmation names the account; Delete is immediate.
gitmoap sa delete --sa-id <id>curl -X DELETE https://api.gitmoap.example/service-accounts/{sa_id} \ -H "Authorization: Bearer $GITMOAP_TOKEN"Deleting the owning group or repository deletes its service accounts too, with the same consequences and no separate confirmation.
Reference
9 endpoints
POST/groups/{group_id}/service-accountsservice_account:create
Create a service account scoped to a group
Token scope
service_account:create
Counts against
service_accounts_per_scope
Send
Path
- group_iduuidGroup ID
Body
- access_leveldeveloperguestmaintainerownerreporter
- avatarobjectoptional
- namestring
Refuses
400Invalid request401Unauthenticated — no credential, or an invalid one403Forbidden404Group not found409The request conflicts with the resource's current state; `reason` says how429Service-account limit reached for this group
Receive
- access_leveldeveloperguestmaintainerownerreporter
- avatarobjectoptional
- created_atstring
created_byobject4 fields
- avatar_urlobjectoptional
- emailstring
- idstring
- usernamestring
- emailstring
- namestring
- scope_idstring
- scope_typegrouprepository
- service_account_iduuid
- usernamestring
POST/repositories/{repo_id}/service-accountsservice_account:create
Create a service account scoped to a repository
Token scope
service_account:create
Counts against
service_accounts_per_scope
Send
Path
- repo_iduuidRepository ID
Body
- access_leveldeveloperguestmaintainerownerreporter
- avatarobjectoptional
- namestring
Refuses
400Invalid request401Unauthenticated — no credential, or an invalid one403Forbidden404Repository not found409The request conflicts with the resource's current state; `reason` says how429Service-account limit reached for this repository
Receive
The same shape create_group_sa returns — 10 fields. Go to it
Show it here
- access_leveldeveloperguestmaintainerownerreporter
- avatarobjectoptional
- created_atstring
created_byobject4 fields
- avatar_urlobjectoptional
- emailstring
- idstring
- usernamestring
- emailstring
- namestring
- scope_idstring
- scope_typegrouprepository
- service_account_iduuid
- usernamestring
GET/groups/{group_id}/service-accountsservice_account:read
List service accounts in a group
Token scope
service_account:read
Send
Path
- group_iduuidGroup ID
Query
- limitint32optionalPage size (1-100)
- next_tokenstringoptionalPagination cursor
Refuses
400Bad request — a path parameter is not a well-formed value of its type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Not found — or found and private, which is masked as absent
Receive
items[]object13 fields
- access_leveldeveloperguestmaintainerownerreporter
- avatarobjectoptional
- created_atstring
created_byobject4 fields
- avatar_urlobjectoptional
- emailstring
- idstring
- usernamestring
- emailstring
- namestring
- scope_idstring
- scope_typegrouprepository
- service_account_iduuid
- usernamestring
- next_tokenobjectoptional
GET/repositories/{repo_id}/service-accountsservice_account:read
List service accounts in a repository
Token scope
service_account:read
Send
Path
- repo_iduuidRepository ID
Query
- limitint32optionalPage size (1-100)
- next_tokenstringoptionalPagination cursor
Refuses
400Bad request — a path parameter is not a well-formed value of its type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Not found — or found and private, which is masked as absent
Receive
The same shape list_group_sas returns — 2 fields. Go to it
Show it here
items[]object13 fields
- access_leveldeveloperguestmaintainerownerreporter
- avatarobjectoptional
- created_atstring
created_byobject4 fields
- avatar_urlobjectoptional
- emailstring
- idstring
- usernamestring
- emailstring
- namestring
- scope_idstring
- scope_typegrouprepository
- service_account_iduuid
- usernamestring
- next_tokenobjectoptional
GET/service-accounts/{sa_id}service_account:read
Get a service account by ID
Token scope
service_account:read
Send
Path
- sa_iduuidService account ID
Refuses
400Bad request — a path parameter is not a well-formed value of its type401Unauthenticated — no credential, or an invalid one403Forbidden — the caller's access level or token scope is insufficient404Service account not found
Receive
The same shape create_group_sa returns — 10 fields. Go to it
Show it here
- access_leveldeveloperguestmaintainerownerreporter
- avatarobjectoptional
- created_atstring
created_byobject4 fields
- avatar_urlobjectoptional
- emailstring
- idstring
- usernamestring
- emailstring
- namestring
- scope_idstring
- scope_typegrouprepository
- service_account_iduuid
- usernamestring
PATCH/service-accounts/{sa_id}service_account:update
Update a service account
Token scope
service_account:update
Send
Path
- sa_iduuidService account ID
Body
- access_levelobjectoptional
- avatarobjectoptional
- nameobjectoptional
Refuses
400Invalid request401Unauthenticated — no credential, or an invalid one403Forbidden404Service account not found409The request conflicts with the resource's current state; `reason` says how
Receive
The same shape create_group_sa returns — 10 fields. Go to it
Show it here
- access_leveldeveloperguestmaintainerownerreporter
- avatarobjectoptional
- created_atstring
created_byobject4 fields
- avatar_urlobjectoptional
- emailstring
- idstring
- usernamestring
- emailstring
- namestring
- scope_idstring
- scope_typegrouprepository
- service_account_iduuid
- usernamestring
PUT/service-accounts/{sa_id}/avatarservice_account:update
Get a URL to upload a service account's avatar to
Token scope
service_account:update
Counts against
avatar_bytes
Send
Path
- sa_iduuidService account ID
Body
- content_lengthint64Exact byte size of the upload. Bound into the presigned PUT, so the upload must send exactly this many bytes; rejected up front if over the avatar cap.
- content_typestring
Refuses
400Invalid request401Unauthenticated — no credential, or an invalid one403Forbidden404Service account not found409The request conflicts with the resource's current state; `reason` says how501Avatar uploads are not available on this deployment
Receive
- avatar_urlstring
- upload_urlstring
DELETE/service-accounts/{sa_id}/avatarservice_account:update
Delete a service account avatar
Token scope
service_account:update
Send
Path
- sa_iduuidService account ID
Refuses
400Bad request — a path parameter is not a well-formed value of its type401Unauthenticated — no credential, or an invalid one403Forbidden404Service account not found409The request conflicts with the resource's current state; `reason` says how
Receive
No body.
DELETE/service-accounts/{sa_id}service_account:delete
Delete a service account
Token scope
service_account:delete
Send
Path
- sa_iduuidService account ID
Refuses
400Bad request — a path parameter is not a well-formed value of its type401Unauthenticated — no credential, or an invalid one403Forbidden404Service account not found409Something still references this service account
Receive
No body.
Limits
Service accounts per scope — 10 on the Free plan. The cap is counted per scope, not per tree: a group can hold 10 of its own while each repository inside it holds its own 10, and accounts belonging to a child scope never count against the parent's allowance.
| Plan | Service accounts per group or repository |
|---|---|
| Free | 10 |
| Pro | 50 |
| Team | 200 |
| Enterprise | 1 000 |
At the cap, a create call is refused with 429 TOO_MANY_REQUESTS and nothing is created — no half-made account, no orphaned identity, nothing to clean up. Delete an account you no longer need, move the automation to a group account that serves several repositories, or raise the ceiling: a higher plan lifts every scope's allowance, and an Owner can ask for a higher limit on one namespace. To know how close a scope is before a script finds out for you, count the accounts in its own listing.
Pictures — 5 MiB per upload. You declare the exact size when you ask for an upload address; 0 bytes or anything over 5 MiB is refused with 400 BAD_REQUEST before an address is issued, and the address that comes back accepts exactly that many bytes and expires after five minutes.
One more ceiling worth knowing, because it is the one a busy bot meets: a single account holds at most 100 tokens. Revoke the ones you have stopped using rather than accumulating them — Access tokens covers listing and revoking.
Troubleshooting
400 BAD_REQUESTmentioning sa_id, group_id or repo_id
- Cause
- The id in the URL is not a well-formed id — often a path like
acme/platformpasted where an account id belongs - What to do
- Use the id from the listing. The CLI accepts a group or repository path for
--group-idand--repo-id, but an account is only ever addressed by its id
400 BAD_REQUESTmentioning name
- Cause
- The display name is empty or longer than 100 characters
- What to do
- Shorten it. Any characters are fine, including spaces and emoji
400 BAD_REQUESTmentioning access_level
- Cause
- The level is not one of
guest,reporter,developer,maintainer— or it isowner, which no account can hold - What to do
- Send one of the four, lowercase.
admin,writeandreadare not levels here
400 BAD_REQUEST— At least one field must be provided
- Cause
- An update was sent with nothing in it
- What to do
- Include the name, the level or the picture address. An empty update is refused rather than accepted as a no-op
400 BAD_REQUEST— Service account / pipeline run callers are not supported for this operation
- Cause
- A service account's token tried to create another service account, or to mint an access token
- What to do
- Run the call as a person. This holds even when the token carries
service_account:createortoken:manage, so provisioning scripts have to stop one step short of minting identities and credentials
400 BAD_REQUESTon a picture upload, mentioning content type
- Cause
- The file is not
image/png,image/jpeg,image/giforimage/webp - What to do
- Convert it. The type is checked before an upload address is issued, so nothing was uploaded
400 BAD_REQUESTon a picture upload, mentioning size
- Cause
- The declared size is
0or above the 5 MiB cap - What to do
- Resize the image and declare its real byte count. Declaring a smaller number than you intend to send does not help — the size is bound into the upload address
401 UNAUTHORIZED
- Cause
- No credential, an expired one, or a revoked one. A bot that worked yesterday and fails everywhere today usually has an expired token — or its account was deleted, which revokes every token it held
- What to do
- Mint a fresh token. If minting is refused with
404, the account itself is gone and needs recreating
403 FORBIDDENon creating an account in a group
- Cause
- Creating a group service account needs Owner, one level above what a Maintainer holds
- What to do
- Ask an Owner of the group to create it, or to raise your level. A repository account needs only Maintainer, so a repository-scoped account may be the better fit. Renaming, re-levelling and deleting an account that already exists need only Maintainer, group or repository alike
403 FORBIDDEN— Cannot assign an access level higher than your own
- Cause
- You asked for a level above your own on that scope
- What to do
- Ask for a level at or below yours, or have an Owner create the account. Raising it afterwards hits the same ceiling
403 INSUFFICIENT_SCOPE
- Cause
- The token is valid but was minted without the scope this call needs
- What to do
- Mint a new token carrying
service_account:read,service_account:create,service_account:updateorservice_account:delete, whichever the call needs. Scopes are chosen at mint time and cannot be added to an existing token. Both picture calls needservice_account:update, not a scope of their own
403 FORBIDDEN— Access token is not authorized for this resource
- Cause
- The token is bound to a different group or repository than the one you are reaching
- What to do
- Use a token bound to this scope, or an unbound one. A binding cannot be widened after minting
404 NOT_FOUNDon a group or repository you can see in the web app
- Cause
- The scope is private and your token's identity is not a member of it
- What to do
- Check you are a member. A private scope answers
404rather than403to a non-member so its existence stays private
404 NOT_FOUNDon an account that existed this morning
- Cause
- It was deleted, or the group or repository that owned it was
- What to do
- Check the audit log for the delete. Recreating gives you a new identity and a new username; the old one cannot be restored
409 CONFLICTon delete
- Cause
- The account is still named as the creator of something kept — a comment, a work item, a release, a pipeline run
- What to do
- Revoke its tokens and leave the account in place; that stops the bot without detaching its work. Delete only an account that has created nothing
429 TOO_MANY_REQUESTSon create
- Cause
- The scope is at its service-account cap
- What to do
- Delete an account you no longer need, or raise the ceiling. Nothing was created by the refused call
The bot can clone but every push is rejected
- Cause
- The account's level is below Developer, or the branch is protected and the level is below Maintainer
- What to do
- Raise the account's level, or push to an unprotected branch. A token scope does not substitute for a level —
git:writeon a Reporter account still cannot push
A group account cannot reach a repository in a different group
- Cause
- An account's reach is its own scope and everything nested inside it, and nothing else
- What to do
- Create an account on the nearest group containing both, or a second account in the other place
The account is missing from the group's member list
- Cause
- Service accounts are not members. Their level is part of the account rather than a membership row
- What to do
- Look under Service accounts in the group or repository settings
Running the provisioning script twice left two identical bots
- Cause
- Creation is not idempotent — every call mints a new account, even with the same name
- What to do
- List the scope's accounts before creating, and delete the duplicate. Match on the account id you stored, never on the display name
The upload address returns an error from storage rather than from Gitmoap
- Cause
- The address expired after five minutes, or the upload sent a different number of bytes than was declared
- What to do
- Ask for a fresh address and send exactly the declared byte count
Examples
Provision a deploy bot for a group, end to end
Creates a Developer-level account for everything under acme/platform, mints it a 90-day token that can push, and prints the token once.
SA_ID=$(gitmoap sa create-group \ --group-id acme/platform \ --name "Deploy bot" \ --access-level developer \ --output json | jq -r '.service_account_id') gitmoap access-tokens create-sa-token \ --sa-id "$SA_ID" \ --name "deploy from CI" \ --scopes '["git:read","git:write"]' \ --expires-in-days 90 \ --output json | jq -r '.token'Put that last value straight into your CI secret store — it is not retrievable afterwards. Your job then authenticates git with it:
export GITMOAP_TOKEN="<the token>"git clone gitmoap://acme/platform/api-gatewayAudit what a scope's bots can do
Lists a group's accounts newest first with just the fields worth reviewing, which makes an over-privileged account easy to spot.
gitmoap sa list-group --group-id acme/platform --limit 100 --output json \ | jq -r '.items[] | [.access_level, .name, .username, .created_at] | @tsv'Anything at maintainer deserves a reason. Lower one with gitmoap sa update --sa-id <id> --access-level developer, which takes effect on that account's next request.
FAQ
Can a service account be a member of a second group?
No. Its reach is its owning scope and everything nested inside it, and there is no way to add it anywhere else. Create an account on the nearest common parent, or one account per place.
Can I move an account to another group?
No — the scope is fixed at creation. Create a new account in the destination, mint it a token, switch the automation over, then delete the old account.
Do service accounts expire?
The account does not. Its tokens do, if you gave them an expiry, and that is where the expiry belongs: a bot that has to keep working needs a rotation you plan, not an account that vanishes.
How do I pause a bot without losing it?
Revoke its tokens. The account, its history and its level stay exactly as they are, and minting a new token brings it back. Deleting is the irreversible option, and it is refused outright once the account is named as the creator of anything the platform keeps.
Can I read a token back?
No. The plaintext is shown once when it is minted and is never recoverable — not by you, not by an Owner, not by us. Mint a new one and revoke the old.
Does a service account count against my member limit?
No. It counts against the service-account limit for its scope, and members are counted separately.
Why does the username look nothing like the name I chose?
The username and email are generated so they are unique and stable no matter what anyone renames later. The display name is the human-facing half, and you can change it whenever you like.
Who shows as the author of a bot's commits?
Whoever the commit itself names. A commit carries the author name and email git recorded when it was written, and pushing it with the account's token does not rewrite that — so a bot that pushes somebody else's work leaves that person as the author. Set your job's git identity to the account's generated email if you want its own commits credited to it. Everything the account does through the API — the comments it leaves, the work items it files, the releases it cuts, the pipeline runs it starts — is attributed to it directly, and it has its own contribution history, so that activity never quietly merges into the record of the person who created it.
Related
- Access tokensminting, binding, expiring and revoking the credentials a service account actually uses
- Groupsthe nesting that decides how far a group account reaches, and how a level held above a scope carries down into it
- Repositoriesrepository members, visibility, and what each access level lets a principal do
- Audit logwho created a bot, who raised its level, and when
- Pipelineswhen a job needs to act inside Gitmoap, and whether it needs its own identity at all
- Billing and plansupgrading, and asking for a higher limit on one namespace