Skip to content

Errors

Every failure from the Sealcord API has one shape and a stable code. Match on the code and decide what your app or backend does next. This page lists the codes you can meet on the activation routes, the purchase lookup and the Admin API.

{
"error": {
"code": "seat_limit_reached",
"message": "Every seat of this licence is in use.",
"details": { "limit": 3, "active": 3 },
"request_id": "8f0d6c1e-2a55-4b8e-9d61-0c1f3f6a7b90"
}
}
  • Match on code. The message is for people and may change.
  • details is present only when there is something to add.
  • request_id identifies the request. Send an x-request-id header (8 to 64 characters of A-Z a-z 0-9 . _ -) to choose your own: the answer echoes it. Quote it when you write to Sealcord about a failure.

A validation failure lists each field at fault:

{
"error": {
"code": "validation_failed",
"message": "The request is not valid.",
"details": {
"issues": [{ "path": "machine_id", "message": "1 to 128 of A-Z a-z 0-9 . _ : -" }]
},
"request_id": "…"
}
}

Request bodies are JSON and at most 64 KB. Every response carries Cache-Control: no-store.

Errors on the activation and purchase routes

Section titled “Errors on the activation and purchase routes”

These are the answers your app gets from POST /v1/activations, /v1/activations/status and /v1/activations/deactivate, and from the customer purchase lookup.

Status Code What it means What to do
400 validation_failed The body does not match the schema Fix the fields in details.issues. It is a bug in your app
403 licence_revoked Activation of a revoked licence Treat the licence as revoked
403 licence_expired Activation of an expired licence Treat the licence as revoked
404 licence_not_found A genuine key that Sealcord does not know Not a revocation: keep the offline result
404 purchase_not_found The customer lookup found nothing. The answer is the same for every mismatch Ask the customer to check the email and order id
409 seat_limit_reached Every seat of the licence is taken. details.limit and details.active Show the limit and offer to free a seat
413 payload_too_large The body is over 64 KB Send a smaller body
422 licence_key_invalid Not a key Sealcord issues, an edited one, or one signed with a retired key Your offline check should have refused it. Offer to get the customer’s key again
422 licence_wrong_product The key’s prefix is one product’s and its payload another’s Tell the customer the key is for another product
429 rate_limited Too many requests Back off for the seconds in the Retry-After-public header (Retry-After-lookup for the lookup)
500 internal_error Something unexpected on Sealcord’s side Keep the offline result and retry later
503 product_unavailable The product cannot sign right now Keep the offline result and retry later

A network error, a timeout or any 5xx must never lock a customer out: keep the offline result and ask again later.

Admin API calls send Authorization: Bearer sc_<prefix>_<secret>. These are the answers your backend can get.

Status Code What it means What to do
400 validation_failed The body, path or query does not match the schema. A status-change reason that starts with polar: is reserved for payment providers Fix the field in details.issues. Retrying the same body fails again
401 unauthorized The key is missing, wrong, revoked or expired Alert someone. Do not retry
402 plan_limit_reached Your plan has no room: active licences, products, API keys, payment integrations, webhook endpoints or team places. details names the plan and the limit Upgrade the plan or free room. Nothing was written, and existing licences keep working
403 forbidden The call is not allowed for this caller, for example an assistant adding a webhook endpoint, or a member changing their own role Use the route as its page describes
403 insufficient_scope The key lacks a scope. details.missing lists them. A team change with a key always answers this: no key holds members:write Create a key with the scope
403 insufficient_role Team routes: the member’s role does not allow it. details.role names that role Ask a member with a higher role
404 not_found An unknown route, or an activation id that is not yours Check the address and the id
404 licence_not_found An unknown licence id, or another organization’s Check the id. After a conflict on issue, see safe retries
404 product_not_found An unknown product id, or another organization’s Check the product id
404 public_key_not_found Retiring a public key the product does not accept Check the product’s accepted keys
404 integration_not_found An unknown payment integration, or another organization’s; when saving a mapping, also a disconnected one List integrations and use a connected id
404 mapping_not_found Deleting a mapping the integration does not have List the mappings
404 webhook_endpoint_not_found An unknown webhook endpoint, or another organization’s List endpoints
404 webhook_delivery_not_found A delivery id that is not one of that endpoint’s List the endpoint’s deliveries
404 member_not_found Team routes: a member the organization does not have List members
404 invitation_not_found Team routes: an invitation that does not exist List invitations
409 conflict An id or prefix is taken, a licence id is taken, an external reference is taken on the product (details.licence_id), or you retire a product’s current signing key Read the licence by its id before deciding it was not issued
409 licence_anonymised Revoking, expiring or reinstating an anonymised licence Nothing to do: an anonymised licence never comes back
409 product_reserved A product id or key prefix is reserved or was used before. details.field names which Choose another id or prefix
409 invitation_not_pending Team routes: the invitation was already accepted or revoked List invitations
413 payload_too_large The body is over 64 KB Send a smaller body
422 polar_token_invalid Saving a Polar mapping: Polar refuses the integration’s token. details.reason Reconnect the integration in the Console with a new token
429 rate_limited Over the admin rate limit, or past a daily cap such as 5 new products a day Wait the seconds in Retry-After-admin (a daily cap sends none), then retry with the same body and id
500 internal_error Something unexpected on Sealcord’s side Retry later with the same licence id, after checking the licence
502 polar_unavailable Polar did not answer, or answered with an error, while a mapping was saved Retry later
503 product_unavailable The product cannot sign right now. Nothing was written Retry later with the same id, and report it
503 signing_keys_not_configured, integrations_not_configured, webhooks_not_configured A Sealcord feature is not available right now Retry later, and write to Sealcord if it lasts
503 service_unavailable A dependency is down while creating a product or inviting a member Retry later

On a timeout, a 5xx or any answer you are unsure of while issuing, read the licence by the id you chose before you decide it was not issued. The issuing guide explains why.

Limits count requests per minute, for each route on its own. A response within the limit carries X-RateLimit-Limit-<name>, X-RateLimit-Remaining-<name> and X-RateLimit-Reset-<name>. A 429 carries Retry-After-<name> instead, in seconds. <name> is the limit that applies:

<name> Applies to Limit
public The three activation routes 30 a minute per client IP address on each route
lookup The customer purchase lookup 5 a minute per client IP address
admin The Admin API 300 a minute per API key on each route (per member for an assistant; per client IP address for a missing or refused key)

Check a licence at most once a day per install and when the customer enters a key, and you stay far below the activation limit.

Should my app show the error message to the customer?

Section titled “Should my app show the error message to the customer?”

Show your own text for the codes you handle, such as seat_limit_reached. The message is for developers and may change.

Is a licence_not_found answer a revocation?

Section titled “Is a licence_not_found answer a revocation?”

No. It means the key is genuine but Sealcord has no such licence. Keep the result of your offline check.

Retry 429 after the seconds in Retry-After-<name>, and 5xx or a timeout later, with backoff. Do not retry 400, 401, 403 or 422 with the same request: it fails again.