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.
The error shape
Section titled “The error shape”{ "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. Themessageis for people and may change. detailsis present only when there is something to add.request_ididentifies the request. Send anx-request-idheader (8 to 64 characters ofA-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.
Errors on the Admin API
Section titled “Errors on the Admin API”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.
Rate limits
Section titled “Rate limits”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.
Questions
Section titled “Questions”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.
Which errors should I retry?
Section titled “Which errors should I retry?”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.