Tokens API
Personal access tokens are what authorize every REST and MCP call.
Personal access tokens are what authorize every REST and MCP call. They look like sbt_live_<id>_<secret> in production and sbt_test_<id>_<secret> everywhere else, and the plaintext value exists exactly once: in the response that created it.
You can mint one in the dashboard under Settings → API Tokens, or through the mint endpoint below. No token.* webhook events exist: mint and revoke are deliberately silent, because deliveries would let an observer fingerprint credential churn on an account they cannot otherwise see.
Background
Authorization
Ownership is necessary but not sufficient.
Changed in 3.0.0. These routes previously required no ability at all, on the reasoning that owning a token is the permission. It is not: a token scoped to nothing but
dashboard:readcould enumerate and revoke every other token its owner held, including the ones running their automation. Each route now demands its matchingtoken:*ability as well. If you have a token that reads analytics and also manages tokens, addtoken:view-any/token:deleteto it, or better, mint a separate one.
Tokens belonging to another user are invisible: every route returns 404 RESOURCE_NOT_FOUND for an id outside the caller's own set, so the API cannot be used to probe which ids exist.
Endpoints
/v1/tokensList the caller's tokensPOST/v1/tokensMint a tokenGET/v1/tokens/{token}Get a tokenDELETE/v1/tokens/{token}Revoke a tokencurl https://api.subscriby.net/v1/tokens \ -H "Authorization: Bearer $SUBSCRIBY_TOKEN"Paginated, newest first. per_page accepts 1–100 and defaults to 25.
Requires ability
The token must hold this ability, or the call is refused with 403.
MCP tool
Runs the same action from an agent, behind the same ability.
Authorization
bearerToken A personal access token minted on the dashboard under Settings, then Tokens, sent as Authorization: Bearer sbt_live_…. The token carries the abilities each endpoint lists under Requires ability and is frozen to one team.
In: header
Query Parameters
The 1-based page to return. A page past the last answers an empty data array with meta.total still filled, so a loop can stop without guessing.
1 <= value1Rows per page, 1 to 100. A higher value clamps to the cap silently. Defaults to 25.
1 <= value <= 10025The column to order by. Defaults to created_at; a column the endpoint does not offer falls back to the default rather than failing.
"created_at"asc or desc. Defaults to desc.
"desc"Value in
- "asc"
- "desc"
Responses
200OKapplication/json
The caller's tokens, newest first.
401UnauthorizedAUTHENTICATION_REQUIREDapplication/json
The request carries no bearer token, or one that is revoked, malformed, or minted for another environment (an sbt_test_ token on production).
403ForbiddenTOKEN_MISSING_ABILITYapplication/json
The token is valid but does not carry the ability this endpoint requires; error.context.required_ability names the one to grant. An endpoint that also checks who owns a row or which tier the account is on answers FORBIDDEN, TEAM_TIER_REQUIRED or CONNECTOR_TIER_REQUIRED with the same status, and says so in its own description.
429Too many requestsRATE_LIMITEDapplication/json
The token has spent its 300 requests a minute or 10,000 an hour; Retry-After says when the next one is accepted.
curl -X POST https://api.subscriby.net/v1/tokens \ -H "Authorization: Bearer $SUBSCRIBY_TOKEN" \ -H "Idempotency-Key: $(uuidgen)" \ -H "Content-Type: application/json" \ -d '{ "name": "Nightly reconciliation", "team_id": "a83f0d51-4c92-4b7e-8615-2fd9e70a3c86", "abilities": ["project-subscription:view-any", "payment:view-any"] }'token sits alongside data, not inside it, the same shape webhook endpoints use for their one-time signing secret. Store it before you do anything else; there is no endpoint that will hand it back. Minting is deliberately not offered as an MCP tool.
team_id is required here, and only here. Everywhere else in v1 the team comes from the calling token's frozen scope and a team_id in the body is ignored. Minting is the exception: a token is created for a team, and you may belong to several. The minted token gets scope:team:<team_id> automatically. It cannot be widened later.
A token can never mint a stronger token. The abilities you request must be a subset of the abilities the calling token holds, and
token:createis withheld unconditionally from anything minted this way.
Minting used to be dashboard-only precisely so a leaked automation token could not spin up a higher-privilege one. That concern is real, and attenuation is the answer to it rather than a retreat from it: a compromised credential can produce nothing its holder could not already do directly. token:create is withheld on top of the subset rule for a separate reason: inherited, it would leave an unbounded ladder of tokens descending from one leak, with no single revocation that ends the chain. A human in the dashboard is not attenuated. They authenticated with a password and a second factor, not with a bearer string, so they can grant anything in the catalog.
If the calling token carries a deprecated coarse ability, it may still mint the precise abilities that ability covers; the split does not shrink what an existing token can delegate.
Asking for more than you hold returns 422; context.abilities echoes back what you asked for, not what was refused, and the refused ones are named in message:
{ "error": { "code": "VALIDATION_FAILED", "message": "A token cannot grant abilities it does not hold: coupon:delete, project:delete.", "context": { "abilities": ["coupon:delete", "project:delete", "project:view-any"] } }}A retired ability keeps working on tokens that already carry it, but no new token can be given one. This is a field-level failure, so every offending entry in the array is reported at once under fields, and the message names the replacements; an ability that is simply unknown gets a different message pointing at the catalog, because a typo and a retirement need different fixes:
{ "error": { "code": "VALIDATION_FAILED", "message": "webhook-endpoint:manage is retired and cannot be granted to a new token. Use webhook-delivery:retry, webhook-delivery:view-any, webhook-endpoint:create, webhook-endpoint:delete, webhook-endpoint:update, webhook-endpoint:view, webhook-endpoint:view-any instead.", "fields": { "abilities.0": [ "webhook-endpoint:manage is retired and cannot be granted to a new token. Use webhook-delivery:retry, webhook-delivery:view-any, webhook-endpoint:create, webhook-endpoint:delete, webhook-endpoint:update, webhook-endpoint:view, webhook-endpoint:view-any instead." ] } }}Requires ability
The token must hold this ability, or the call is refused with 403.
Idempotent
Send the header on every call; the same key replays the original response for 24 hours.
Authorization
bearerToken A personal access token minted on the dashboard under Settings, then Tokens, sent as Authorization: Bearer sbt_live_…. The token carries the abilities each endpoint lists under Requires ability and is frozen to one team.
In: header
Header Parameters
A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with Idempotent-Replay: true), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409.
uuidRequest body
JSONWhat the request carriesRequiredapplication/json
A token to mint: its label, the abilities it holds and the team it is minted for. The abilities must be a subset of the calling token's, and token:create is never granted this way.
Responses
201Createdapplication/json
The token resource plus the plaintext, as a 201.
400Bad requestIDEMPOTENCY_KEY_MISSINGapplication/json
Every write needs an Idempotency-Key header. Send a fresh UUID per distinct operation.
401UnauthorizedAUTHENTICATION_REQUIREDapplication/json
The request carries no bearer token, or one that is revoked, malformed, or minted for another environment (an sbt_test_ token on production).
403ForbiddenTOKEN_MISSING_ABILITYapplication/json
The token is valid but does not carry the ability this endpoint requires; error.context.required_ability names the one to grant. An endpoint that also checks who owns a row or which tier the account is on answers FORBIDDEN, TEAM_TIER_REQUIRED or CONNECTOR_TIER_REQUIRED with the same status, and says so in its own description.
409ConflictIDEMPOTENCY_KEY_REUSEDapplication/json
The key was already used in the last 24 hours with a different request body.
422Validation failedVALIDATION_FAILEDapplication/json
The payload broke a rule, and error.fields maps each offending key to its messages. A refusal from the domain, such as a plan that cannot go on sale or a member who cannot be removed, uses the same code with error.message saying why and no fields.
On this endpoint: VALIDATION_FAILED: when a requested ability is not held by the calling token, token:create is requested, team_id is not a team the caller owns or belongs to, an ability is retired or unknown, or abilities is empty.
425Too earlyIDEMPOTENCY_REPLAY_IN_PROGRESSapplication/json
The first request with this key is still running; retry in a few seconds and the original response is replayed.
429Too many requestsRATE_LIMITEDapplication/json
The token has spent its 300 requests a minute or 10,000 an hour; Retry-After says when the next one is accepted.
curl https://api.subscriby.net/v1/tokens/$TOKEN_ID \ -H "Authorization: Bearer $SUBSCRIBY_TOKEN"One of the caller's tokens, without its value.
idis an integer serialised as a string, the one identifier in the platform that is not a UUID. See Identifiers.abilitiesis the gate strings only. Scope tuples (scope:team:…,scope:project:…) are split out intoscopesfor readability, soabilities_countcounts real permissions rather than bookkeeping.- The plaintext value is never returned here. Lose it and the only recovery is to revoke the token and mint another.
Requires ability
The token must hold this ability, or the call is refused with 403.
Authorization
bearerToken A personal access token minted on the dashboard under Settings, then Tokens, sent as Authorization: Bearer sbt_live_…. The token carries the abilities each endpoint lists under Requires ability and is frozen to one team.
In: header
Path Parameters
One of the caller's tokens, resolved by the route binder.
Responses
200OKapplication/json
The token resource.
401UnauthorizedAUTHENTICATION_REQUIREDapplication/json
The request carries no bearer token, or one that is revoked, malformed, or minted for another environment (an sbt_test_ token on production).
403ForbiddenTOKEN_MISSING_ABILITYapplication/json
The token is valid but does not carry the ability this endpoint requires; error.context.required_ability names the one to grant. An endpoint that also checks who owns a row or which tier the account is on answers FORBIDDEN, TEAM_TIER_REQUIRED or CONNECTOR_TIER_REQUIRED with the same status, and says so in its own description.
404Not foundRESOURCE_NOT_FOUNDapplication/json
An id in the path names nothing the token can see. TENANT_MISMATCH: the project sits outside the token's scope:project: allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred.
429Too many requestsRATE_LIMITEDapplication/json
The token has spent its 300 requests a minute or 10,000 an hour; Retry-After says when the next one is accepted.
curl -X DELETE https://api.subscriby.net/v1/tokens/$TOKEN_ID \ -H "Authorization: Bearer $SUBSCRIBY_TOKEN" \ -H "Idempotency-Key: $(uuidgen)"Returns 204 No Content. Revocation is immediate: in-flight requests already authenticated will finish, and the next one fails.
Requires ability
The token must hold this ability, or the call is refused with 403.
MCP tool
Runs the same action from an agent, behind the same ability.
Idempotent
Send the header on every call; the same key replays the original response for 24 hours.
Authorization
bearerToken A personal access token minted on the dashboard under Settings, then Tokens, sent as Authorization: Bearer sbt_live_…. The token carries the abilities each endpoint lists under Requires ability and is frozen to one team.
In: header
Path Parameters
One of the caller's tokens, resolved by the route binder.
Header Parameters
A key unique to this operation, such as a fresh UUID. The same key replays the original 2xx response for 24 hours (with Idempotent-Replay: true), so a retry after a timeout never repeats the write; the same key with a different body is refused with 409.
uuidResponses
204No content
No content
400Bad requestIDEMPOTENCY_KEY_MISSINGapplication/json
Every write needs an Idempotency-Key header. Send a fresh UUID per distinct operation.
401UnauthorizedAUTHENTICATION_REQUIREDapplication/json
The request carries no bearer token, or one that is revoked, malformed, or minted for another environment (an sbt_test_ token on production).
403ForbiddenTOKEN_MISSING_ABILITYapplication/json
The token is valid but does not carry the ability this endpoint requires; error.context.required_ability names the one to grant. An endpoint that also checks who owns a row or which tier the account is on answers FORBIDDEN, TEAM_TIER_REQUIRED or CONNECTOR_TIER_REQUIRED with the same status, and says so in its own description.
404Not foundRESOURCE_NOT_FOUNDapplication/json
An id in the path names nothing the token can see. TENANT_MISMATCH: the project sits outside the token's scope:project: allow-list, or the token carries no team scope. Both answer 404 rather than 403 so that existence outside the token's scope cannot be inferred.
On this endpoint: RESOURCE_NOT_FOUND: when no token with that id belongs to the authenticated user.
409ConflictIDEMPOTENCY_KEY_REUSEDapplication/json
The key was already used in the last 24 hours with a different request body.
422Validation failedVALIDATION_FAILEDapplication/json
With token: self-revoke blocked when you tried to revoke the exact row you are authenticating with. Revoke it from the dashboard, or from another token on the same account. This exists so an automation cannot lock itself out mid-run and leave the failure looking like a network fault.
425Too earlyIDEMPOTENCY_REPLAY_IN_PROGRESSapplication/json
The first request with this key is still running; retry in a few seconds and the original response is replayed.
429Too many requestsRATE_LIMITEDapplication/json
The token has spent its 300 requests a minute or 10,000 an hour; Retry-After says when the next one is accepted.
Related
How is this guide?