Errors
Every error of the API has the same format: an HTTP status, and an error object with a code and a message.
// 409 Conflict
{
"error": {
"code": "key_taken",
"message": "Another license of the product already uses this key"
}
}codeis stable: branch on it in your code.messageexplains the error to a developer, in English. It can change at any time: don’t parse it, and don’t show it to your customers as is.
Each endpoint lists the codes it can return in the API reference.
Validation errors
An invalid path, query parameter or body answers 422, not 400, with the validation_error code and an issues array, giving the path and message of each invalid value:
// 422 Unprocessable Entity
{
"error": {
"code": "validation_error",
"message": "Invalid request",
"issues": [
{ "path": ["email"], "message": "Invalid email address" },
{ "path": ["maxVersion", "lockMinorAt"], "message": "The minor version can't be locked before the major version" }
]
}
}path lists the keys leading to the value, numbers for the positions in an array. Every invalid value is listed, not only the first one.
Fields an endpoint doesn’t know are ignored, not refused: a misspelled optional field doesn’t cause a validation error, it’s left out. See conventions.
Status codes
| Status | Means | Retry? |
|---|---|---|
200, 201, 204 | Success. 201 for creations, 204 when there’s nothing to return. | |
401 | Missing or invalid credentials. | No: fix the credentials. |
402 | Your plan’s limits. | No: upgrade, or reduce your usage. |
403 | The credentials can’t do this. | No. |
404 | Not found, or not reachable with these credentials. | No. |
409 | Conflicts with the current state, like a key already used. | No: change the request, or the state first. |
413, 415 | An uploaded file is too large, or of the wrong type. | No. |
422 | Invalid request. | No: fix the request. |
429 | Too many requests with this API key. | Yes, after the Retry-After seconds. |
500 | Unexpected error on our side, with internal_error. | Yes, with a delay. |
502 | A third party, like Stripe, couldn’t be reached. | Yes, with a delay. |
Common codes
These can be returned by any endpoint.
| Status | Code | When |
|---|---|---|
401 | unauthorized | No API key, token or session. |
401 | invalid_api_key | The API key doesn’t exist, or was revoked. |
401 | invalid_token | The token’s signature is invalid, it expired, or the resource it’s scoped to was deleted. |
402 | plan_limit_exceeded | Your organization stayed over its plan’s limits after the grace period: the API is blocked until you upgrade or reduce your usage. See plans and billing. |
403 | insufficient_role | From the app: your role doesn’t allow this. The message names the permission needed. |
403 | token_not_allowed | The endpoint doesn’t accept tokens, or not of this type. |
403 | insufficient_scope | The token lacks the permission the endpoint needs. |
404 | not_found | The resource doesn’t exist, or the credentials don’t reach it: a live API key asking for a test resource, a token asking for another customer’s license. |
422 | validation_error | Invalid request, with the issues. |
429 | rate_limited | The API key made more than 10 requests in the last second. Wait for the Retry-After header’s seconds. See rate limit. |
500 | internal_error | Unexpected error. |
404 doesn’t tell a resource that doesn’t exist from one you can’t reach, on purpose: nobody learns what exists in an organization they can’t access.
Codes by resource
Products
| Status | Code | When |
|---|---|---|
402 | plan_limit_reached | Creating a product would go over your plan’s products. |
403 | api_key_mode | Creating a test product with a live API key, or the opposite. |
409 | slug_taken | Another product of the organization uses this slug. |
409 | product_in_use | Deleting a live product that still has licenses. |
400 | not_test_product | Resetting a live product: only test products can be reset. |
413 | logo_too_large | The logo is 1 MB or more. |
415 | unsupported_logo_type | The logo isn’t a WebP, PNG, JPEG or SVG image. |
Policies
| Status | Code | When |
|---|---|---|
409 | slug_taken | Another policy of the product uses this slug. |
409 | default_required | Unsetting the default policy: make another one the default instead. |
409 | policy_in_use | Deleting a policy that licenses follow. |
409 | last_policy | Deleting the last policy of a product. |
Customers
| Status | Code | When |
|---|---|---|
409 | email_taken | Another customer of the product uses this email. |
409 | customer_in_use | Deleting a customer who still has licenses. |
409 | test_recipient_not_member | Emailing a customer of a test product who isn’t a member of your organization. See emails in test mode. |
409 | portal_disabled | Emailing a portal link while the product’s portal is disabled. |
Licenses
| Status | Code | When |
|---|---|---|
404 | policy_not_found | The product has no policy with this policyId. |
409 | key_taken | Another license of the product uses this key. |
409 | license_in_use | Deleting a license used on active machines: deactivate them first. |
409 | test_recipient_not_member | Sending keys of a test product to a customer who isn’t a member of your organization. |
Validating a key has its own codes: see handling validation errors.
Releases
| Status | Code | When |
|---|---|---|
409 | version_taken | The product already has a release with this version. |
Stripe integration
| Status | Code | When |
|---|---|---|
422 | invalid_api_key | The Stripe key isn’t a secret or restricted key, or Stripe rejected it. |
422 | key_mode_mismatch | A test mode key for a live product, or the opposite. |
422 | missing_permission | The Stripe key lacks permissions, listed in the message. See permissions. |
409 | webhook_url_unreachable | Stripe can’t reach LicenseManager’s webhook URL. |
409 | not_connected | Mapping a price while Stripe isn’t connected in the product’s mode. |
409 | price_mapped | The price already creates licenses of a product. |
404 | price_not_found | The Stripe account has no price with this id. |
422 | price_unusable | The price can’t create licenses, like a recurring price. The message tells why. |
502 | provider_unavailable | Stripe couldn’t be reached. |
Billing
These endpoints only work from the app.
| Status | Code | When |
|---|---|---|
403 | session_required | Called with an API key or a token. |
403 | forbidden | Your role can’t manage the subscription. |
400 | custom_plan | Your organization is on a custom plan: contact us to change it. |
400 | already_on_plan | Your organization is already on this plan. |
Handling errors in your code
- Branch on
code, never onmessage, and treat unknown codes like their status. - Retry
500and502a few times, waiting longer each time, and network errors too. Retry429after theRetry-Afterseconds. Don’t retry other errors: they’ll fail the same way. - Creating twice: when a request creating a license times out, it may have been created anyway. List the customer’s licenses before retrying, or create it with your own
key, so the retry fails withkey_takenrather than creating a second license.