Skip to main content
Concepts covers API Keys as a way to grant programmatic access to an Organization or a Blob. This page is about the third thing a key can be scoped to — a user — and what that makes possible. Every write in BlobHub is stamped with a user_id — the account that authored it. Look at any Blob, any Revision, or any message posted into a workflow thread, and that field is what tells you who is responsible. Today, that user is always a person, because the only way to authenticate programmatically is an API key, and every API key belongs to whoever minted it. A service account is what changes that.

What a Service Account Is

A service account is a machine identity that is a user, not a special kind of API key. It gets its own id, from the same id space every human user gets. It shows up wherever a user_id shows up — member lists, author fields, thread attribution — because as far as the rest of the platform is concerned, it is one. Contrast that with what happens today. When you mint an API key, you mint it as yourself. Every Blob a CI job creates with that key, every Revision a deploy script commits, every message a bot posts into a thread — all of it records your user_id, because the key never had an identity of its own to give. It carries authority (what the holder is allowed to do) but not identity (who is actually doing it). Three different scripts sharing one key all look, in the history, like one person. Give the work its own service account instead, and a Revision it commits is authored by ci-deploy, not by the engineer who set ci-deploy up. That’s the entire idea. Everything else on this page is either a consequence of it or a rule for keeping it safe.

Ownership vs. Membership

Two different questions govern a service account, and BlobHub keeps them completely separate:
  • Who may administer it — rename it, mint or revoke its keys, retire it — is decided by ownership. Every service account is owned by exactly one user or one organization, decided at creation and fixed from then on.
  • What it can reach — which organizations and Blobs it can read, write, or administer — is decided by membership, exactly the way it’s decided for a human. You add a service account as a member of an organization or a Blob, with a role, the same way you’d add a colleague.
These never overlap. Owning an account grants no reach through it — administering ci-deploy doesn’t hand you its memberships. And being a member of an organization grants a service account no say over its own administration. Granting a service account access to something is the ordinary “add a member” flow. There is no separate mechanism for machine identities — you pick the account the same way you’d pick a person. The dashed line is administration; the solid chain is reach. Both start at the account, and they never touch each other again.

Identity, Not Alias

A service account has:
  • An id — a UUID, assigned the moment it’s created, from the same id space every user gets.
  • A name — free-form text you choose, one to a few words. It does not have to be unique: ci-deploy can exist in ten different organizations at once.
It does not have an alias, and that’s deliberate, not an oversight. Every human user can claim one short, memorable, globally-unique alias, and that namespace is flat, global, and permanently reserved — once claimed, an alias is never freed, even after the user is gone. If service accounts could claim aliases too, every CI pipeline and worker fleet on the platform would be drawing from the same limited, permanent namespace real people use for their handles, and a machine could squat a name a person might otherwise want. So service accounts don’t compete for it at all. Look one up by its id or its name; there is no alias to find.

The Recipe

Here’s the whole lifecycle end to end, using an organization’s own admin-scoped API key throughout, via the X-API-Key header — the same credential your CI pipeline would use. $ORG_API_KEY needs the admin role on org_456.
1

Create the account

owner_target_target_id is an internal index key the API happens to serialize — safe to ignore, along with target_target_id further down. type is the extension point: "service" is the only value today, and a future kind of account will be a new value here, not a new endpoint.
2

Add it as a member

Owning org_456 doesn’t make ci-deploy a member of it — that’s the invariant above, in action. It has to be added like anyone else:
3

Mint its key

Same endpoint you’d use for an Organization or a Blob, targeted at the account instead:
Two fields carry the whole feature. user_id is the account — not whoever’s $ORG_API_KEY called this endpoint — because this key authenticates as that principal. created_by_user_id is provenance: the human on whose authority the key exists. They are never the same field; conflating them is exactly the bug service accounts close, where a key’s user_id was always its creator, so a worker key always looked like whoever set it up. key is returned once, in this response, and never again — store it now.
4

See it write as the account

Anything from here on uses ci-deploy’s own key, not the org key:
That’s ci-deploy looking itself up. Notice there’s no alias key at all — not null, simply never written. (role here is the caller’s own standing on whatever it just read, owner because it’s reading itself — not a stored attribute of the account.) The same resolution happens on every write: a Blob this key creates, a Revision it commits, a message it posts into a thread all carry author_id or user_id equal to user_901ci-deploy, in the product’s own records, not whoever was behind $ORG_API_KEY.

Key Reach

A key minted at the account is the account — there’s no extra scoping step, and nothing to configure beyond the account’s own memberships:
  • Add ci-deploy as a member of three organizations, and the one key from the recipe reaches all three. You don’t mint a separate key per organization it needs to touch.
  • Remove ci-deploy from one of those organizations, and that access is gone immediately, without touching the key at all. The key is still valid; it simply has one less place left to use it.
This is the same rule that governs every human user’s access. A service account’s key just makes it visible from the outside, because a key is the only way the account has to prove who it is.

The Access Perimeter

A service account authenticated by its own key may do product work. It may not change who can access what. That one sentence is the whole capability model, and it is worth holding as a boundary rather than as a list of refusals, because the list will grow over time and the boundary will not. Product work is everything the platform is for: reading and writing Blobs, committing Revisions, running workflows, posting into threads, uploading session objects, listing anything it can reach. The account does all of it under its own user_id, at whatever role its memberships give it, with no special cases anywhere. The access perimeter is the much smaller set of operations that decide who else can get in. Every one of them refuses a service account’s own key with 403: † The only conditional row: PATCH is refused solely when visibility actually changes, and every other field goes through untouched — see Publishing Is the Subtle One below. The refusal never depends on role. An account holding an admin membership passes every access check that membership implies and is still refused here, because admin is standing on a target and this rule asks a different question — what is behind the credential — that no role can answer. Every refusal is the same response:
Reads are never on the perimeter. Listing an organization’s members, its API keys or its credentials is ordinary work for an account that can reach them — seeing the perimeter is not changing it.

Publishing Is the Subtle One

PATCH on an Organization or a Blob is not a perimeter operation. It is refused only when it actually changes visibility, in either direction:
  • Renaming, re-describing, re-tagging, setting a url — always allowed, including in the same request as anything else. These are ordinary product work and stay that way.
  • Sending visibility with the value already stored — allowed. It moves nothing, so there is nothing to refuse. This matters more than it looks: an idempotent client that PATCHes its whole desired state on every run would otherwise start failing forever the moment that state included visibility.
  • Flipping privatepublic, or publicprivate — refused.
Creating a Blob is untouched, whatever visibility you create it with: a new public Blob exposes only content the account itself authored. (Creating an Organization is refused for an unrelated reason — see Restrictions.)

Impersonation Is the Escape Hatch

Nothing on the perimeter is permanently out of reach. It is out of reach of the key. Behind every operation in that table the platform asks one question — is there a human behind this request? — and the account’s own key answers no, always, whoever holds it and whatever role it carries. An acting token answers yes, and names them. Mint one as yourself and, for the next 60 minutes, you hold a token that authenticates as the account and carries you as the actor. It does everything the account’s key does, plus the perimeter operations its access reaches, because the request now has a human behind it — you, on the record, for whatever it does. That is the trade the whole design turns on: the perimeter is not sealed shut, it is made attributable. One operation the token does not carry all the way across is minting an API key at an org or blob target, which is refused while acting with 400 and error: cannot_mint_while_acting. Such a key authenticates as whoever mints it, so one minted here would record the account rather than you, and would then be refused by every operation in the table above for the rest of its life. Mint it as yourself — an account is given reach into an organization or a Blob by membership, never by that target’s own key. Minting the account its own key, at target=user, is untouched — that is the operation acting mode exists for. The hatch is deliberately unreachable from inside. A service account’s key cannot mint the first acting token, and an acting token cannot mint another, so no automation can walk itself across the perimeter — a human starts every chain, every time.

Why the Line Is Drawn Here

This is what makes it safe to hand a key to something you don’t fully control: a worker on a machine you don’t own, a third-party CI runner, a script pulled from somewhere else. If that key leaks, whoever holds it can act as the account within the memberships it already has — and nothing more. They cannot mint a longer-lived replacement, add the account to another organization, stand up a second account to survive your rotating the leaked one, or flip a private Blob public and walk away with a URL that outlives the key. Revoke the key and the access ends completely, in one step, because the leak never had a way to spread past it. (All of this is about the account’s own key. Administering the account — minting it a fresh key, for instance — is still perfectly possible, but only through the paths in the next section, none of which run through the key the account already holds.)

Who May Administer One

Administering a service account — renaming it, minting or revoking its keys, retiring it — always traces back to a human, or to a credential a human has already trusted with broad authority over the account’s owner:
  • The owning user, signed in as themselves.
  • An admin of the owning organization, when the account is owned by an organization rather than a person.
  • An org-scoped API key minted with the admin role, for the owning organization. Every administrative operation on a service account requires admin specifically — a write-scoped key won’t reach any of them.
  • An acting token, for the one account it acts as — and only that one. It carries the human who minted it, so it satisfies the perimeter rule, but it is scoped to the single account it names: it cannot administer a sibling account the same human happens to own.
That third path is easy to miss, and it’s the one automation actually depends on: your CI pipeline’s own admin-scoped org key can administer the service accounts that belong to that same organization — create, rename, key, and retire them — exactly as an admin of that organization could by hand. You don’t need a personal API key with a human’s name on it in the loop just to manage the accounts your pipeline depends on. It’s also the credential the recipe above used throughout. What’s never on this list: the service account’s own key, administering anything — including itself. See The Access Perimeter, above.

Lifecycle

A service account starts active the moment it’s created and stays that way until someone retires it. Retiring one:
  1. Deletes every API key it holds.
  2. Deletes every credential stored on the account itself — third-party provider secrets at target=user/{account}. Credentials stored on an Organization or a Blob are untouched: those belong to the Organization or the Blob and outlive every account that ever read them.
  3. Removes every membership it holds.
  4. Sets its status to retired.
Retirement is terminal. There’s no reactivating a retired account, and nothing to free up or reuse — it never held an alias to begin with. What retirement does not do is delete the record itself: the id stays resolvable, so a Revision ci-deploy authored a year ago still shows ci-deploy, not a dangling reference. Retiring an account ends its future; it cannot rewrite its past.

Restrictions

Three things a service account can never do, by design:
  • Own an organization, or another account. Creating either is rejected outright — with service_account_cannot_own_org or service_account_cannot_own_account where the request gets far enough to be told why, and a plain 403 where it does not. Anything owned by a service account would become unreachable the moment whoever administers that account lost access to it, with no way back in.
  • Change the access perimeter with its own key. Covered above, under The Access Perimeter — no exception for role, no exception for target, and the same answer whether it is granting, revoking, or publishing. Impersonation is the only way across.
  • Sign in. There’s no password and no OAuth identity behind a service account — the only way to act as one is a key minted for it, or an acting token minted by a human.
A service account is deliberately narrow: an identity with exactly the reach its memberships give it, and nothing that lets it grant itself more.