Version

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:read could enumerate and revoke every other token its owner held, including the ones running their automation. Each route now demands its matching token:* ability as well. If you have a token that reads analytics and also manages tokens, add token:view-any / token:delete to 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

List the caller's tokens

GET
curl https://api.subscriby.net/v1/tokens \  -H "Authorization: Bearer $SUBSCRIBY_TOKEN"

Paginated, newest first. per_page accepts 1–100 and defaults to 25.

GET
/v1/tokens

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
AuthorizationBearer <token>

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

page?integer

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.

Range1 <= value
Default1
per_page?integer

Rows per page, 1 to 100. A higher value clamps to the cap silently. Defaults to 25.

Range1 <= value <= 100
Default25
sort_by?string

The column to order by. Defaults to created_at; a column the endpoint does not offer falls back to the default rather than failing.

Default"created_at"
sort_direction?string

asc or desc. Defaults to desc.

Default"desc"

Value in

  • "asc"
  • "desc"

Responses

200OK

The caller's tokens, newest first.

401Unauthorized

The request carries no bearer token, or one that is revoked, malformed, or minted for another environment (an sbt_test_ token on production).

403Forbidden

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 requests

The token has spent its 300 requests a minute or 10,000 an hour; Retry-After says when the next one is accepted.

Mint a token

POST
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:create is 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."      ]    }  }}
POST
/v1/tokens

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
AuthorizationBearer <token>

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

Idempotency-Key*string

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.

Formatuuid

Request body

JSONWhat the request carries

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

201Created

The token resource plus the plaintext, as a 201.

400Bad request

Every write needs an Idempotency-Key header. Send a fresh UUID per distinct operation.

401Unauthorized

The request carries no bearer token, or one that is revoked, malformed, or minted for another environment (an sbt_test_ token on production).

403Forbidden

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.

409Conflict

The key was already used in the last 24 hours with a different request body.

422Validation failed

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 early

The first request with this key is still running; retry in a few seconds and the original response is replayed.

429Too many requests

The token has spent its 300 requests a minute or 10,000 an hour; Retry-After says when the next one is accepted.

Get a token

GET
curl https://api.subscriby.net/v1/tokens/$TOKEN_ID \  -H "Authorization: Bearer $SUBSCRIBY_TOKEN"

One of the caller's tokens, without its value.

  • id is an integer serialised as a string, the one identifier in the platform that is not a UUID. See Identifiers.
  • abilities is the gate strings only. Scope tuples (scope:team:…, scope:project:…) are split out into scopes for readability, so abilities_count counts 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.
GET
/v1/tokens/{token}

Requires ability

The token must hold this ability, or the call is refused with 403.

Authorization

bearerToken
AuthorizationBearer <token>

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

token*integer

One of the caller's tokens, resolved by the route binder.

Responses

200OK

The token resource.

401Unauthorized

The request carries no bearer token, or one that is revoked, malformed, or minted for another environment (an sbt_test_ token on production).

403Forbidden

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 found

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 requests

The token has spent its 300 requests a minute or 10,000 an hour; Retry-After says when the next one is accepted.

Revoke a token

DELETE
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.

DELETE
/v1/tokens/{token}

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
AuthorizationBearer <token>

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

token*integer

One of the caller's tokens, resolved by the route binder.

Header Parameters

Idempotency-Key*string

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.

Formatuuid

Responses

204No content

No content

400Bad request

Every write needs an Idempotency-Key header. Send a fresh UUID per distinct operation.

401Unauthorized

The request carries no bearer token, or one that is revoked, malformed, or minted for another environment (an sbt_test_ token on production).

403Forbidden

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 found

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.

409Conflict

The key was already used in the last 24 hours with a different request body.

422Validation failed

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 early

The first request with this key is still running; retry in a few seconds and the original response is replayed.

429Too many requests

The token has spent its 300 requests a minute or 10,000 an hour; Retry-After says when the next one is accepted.

How is this guide?

Version

On this page

Subscriby is a product
designed by you — for you.
No boardroom full of executives deciding what we ships next. Our roadmap always shaped by you with your feedback.

Share feedback or a request