Developers › Errors
API error codes
Every error has the same shape, a stable code to act on, and a request_id to quote to support.
Generated from Suite\ApiKit\ApiError::CODES (common/api-kit). Don't edit by hand: change the list in code and run php common/api-kit/bin/errors-doc.php. This is part of the API standard (api-standard.md) and goes into the developer documentation.
The error body
Every failed call to any Kanishu product API (status 400 or above) returns:
{
"error": {
"code": "validation_failed",
"message": "The email field must be a valid email address.",
"details": { "fields": { "email": ["The email field must be a valid email address."] } },
"request_id": "6f1c2a7e-0d7b-4c1e-9a55-2f0c3d1b8e41"
}
}
code: what went wrong, from the list below. Program against this, not the message. Codes keep their names within a version; new ones may be added.message: a sentence for a person, which may change wording.details: extra facts for that code (always an object, often empty). Examples: the fields forvalidation_failed, the limit and how much is used forlimit_reached, the existing record forduplicate.request_id: quote it to support. It is also in theX-Request-Idheader of every response, success or failure. Send your ownX-Request-Id(up to 64 letters, digits and dashes) to tie our logs to yours; otherwise we generate one.- A
5xxnever carries internal detail: no exception text, SQL, file names or stack traces. 429responses carryRetry-After(seconds).- Kept for v1: a few older endpoints also put extra fields next to
codeinsideerror(for exampleusedandlimit_valueforlimit_reached). Validation failures also keep Laravel's top-levelmessageanderrors. The same facts are indetails; usedetails. - Security: a bad, expired, revoked or switched-off token all return the same
invalid_token, and "not yours" returnsnot_found. Guessing therefore tells an attacker nothing.
Codes
Authentication
| Code | Status | When | What to do |
|---|---|---|---|
unauthenticated |
401 | No token was sent, or the Authorization header is not Bearer <token>. |
Send the token as Authorization: Bearer <token>. |
invalid_token |
401 | The token is unknown, expired, revoked, switched off for inactivity, or its person was disabled. Deliberately one code, so a guessed token can't be told apart from a real one. | Check the token in API Access. Request a new one if it ended. |
bad_signature |
401 | A request signature is missing, wrong or too old (high-risk scopes only). | Sign every call with the token's signing secret (Kanishu-Signature: t=…,n=…,v1=…), a fresh timestamp and a new nonce. message says what was wrong. |
token_in_url |
400 | A token was put in the URL (?token=, ?access_token=, …). Refused, because URLs end up in logs. |
Send it as Authorization: Bearer <token>, and replace that token: it may now be in logs. |
session_expired |
401 | A browser session ended (screens only, not tokens). | Sign in again. |
ip_not_allowed |
403 | The token has an IP allow-list and the call came from another address. | Call from an allowed address, or ask for the list to be changed (a new request). |
Authorisation
| Code | Status | When | What to do |
|---|---|---|---|
invalid_scope |
403 | The token does not have the scope this endpoint needs. | Request access with that scope. |
forbidden |
403 | The token's person may not do this (their role or the record's rules). | Ask an admin, or use a token of someone who may. |
forbidden_organization |
403 | The call named another organisation. A customer token reaches only its own. | Leave out organization_id, or use your own. |
not_supported |
403 | This operation needs a different kind of token (for example a person's own token, not an API client's). | Use the kind of token the endpoint describes. |
Plan
| Code | Status | When | What to do |
|---|---|---|---|
feature_not_included |
402 | The organisation's plan does not include this feature. details.feature names it. |
Upgrade the plan, or don't use this endpoint. |
subscription_read_only |
402 | The subscription has ended and is read-only: reads work, changes are refused until it is renewed. | Renew the subscription in Kanishu Hub; until then, only read. |
subscription_suspended |
402 | The subscription has ended and access is suspended; the message says when the data is deleted. | Renew the subscription in Kanishu Hub. |
limit_reached |
402 | The write would go over a plan limit. details has the limit and how much is used. |
Free some up, or upgrade the plan. |
Usage
| Code | Status | When | What to do |
|---|---|---|---|
rate_limited |
429 | Too many calls in a short time (server protection). | Wait for the seconds in Retry-After, then retry. |
quota_exceeded |
429 | The organisation's daily quota for this product function is used up. details names the function and the quota. |
Wait until it resets at midnight (Retry-After), or upgrade the plan. |
daily_limit_exceeded |
429 | ITAM's v1 name for quota_exceeded: the organisation's daily quota for this function is used up. |
Wait until the quota resets (the Retry-After header), or upgrade the plan. |
Request
| Code | Status | When | What to do |
|---|---|---|---|
bad_request |
400 | The request could not be read (malformed JSON, wrong content type, bad parameters). | Fix the request; message says what is wrong. |
invalid_request |
400 | A parameter is not valid for this endpoint. | Fix the parameter named in message. |
request_failed |
400 | A general refusal with the reason in message (older endpoints; replaced by a specific code where one fits). |
Read message. |
validation_failed |
422 | One or more fields are invalid. details.fields lists each field and its messages. |
Fix the fields listed. |
missing_organization_id |
422 | A product-to-product call did not say which organisation. | Send organization_id. |
not_eligible |
422 | The record is not in a state that allows this (for example a ticket not yet resolved). | Check the record's state first. |
approval_refused |
422 | An approval could not be recorded; message says why. |
Read message. |
incomplete |
422 | Part of a multi-part change was missing, so nothing was changed. | Send the complete change. |
activation_failed |
422 | Hub could not switch on a product for an organisation (Hub to product only). | Hub retries; see its logs. |
idempotency_key_reused |
409 | The same Idempotency-Key was sent with a different request. | Use a new key for a new request. |
idempotency_in_progress |
409 | A request with this Idempotency-Key is still being processed. | Retry after the seconds in Retry-After; you will get the first request's answer. |
invalid_filter |
400 | A filter[…] the list does not offer. details.allowed lists the ones it does. |
Use only the listed filters. |
invalid_sort |
400 | A sort field the list does not offer. details.allowed lists the ones it does. |
Sort only by the listed fields. |
invalid_include |
400 | An include the endpoint does not offer. details.allowed lists the ones it does. |
Ask only for the listed includes. |
invalid_fields |
400 | A fields name the answer does not have. details.allowed lists the ones it does. |
Ask only for the listed fields. |
not_found |
404 | No such record or endpoint for this organisation. Also used where saying "exists but not yours" would leak information. | Check the id and the URL. |
method_not_allowed |
405 | The endpoint exists but not with this HTTP method. | Use the method in the API reference. |
conflict |
409 | The change clashes with the record's current state. | Reload the record and try again. |
duplicate |
409 | A matching record already exists. details has its id. |
Use the existing record. |
exists |
409 | The thing being created already exists. | Use the existing one. |
version_conflict |
412 | The record changed since you read it: the If-Match version is old (412, with details.current_version). Some older screens' endpoints answer 409. |
Read the record again and apply your change to the new version. |
payload_too_large |
413 | The request body or file is too big. | Send less, or split it. |
unsupported_media_type |
415 | The body is not JSON (or the file type is not accepted). | Send Content-Type: application/json. |
Availability
| Code | Status | When | What to do |
|---|---|---|---|
product_maintenance |
503 | The product is in planned maintenance. | Retry later. |
product_disabled |
503 | The product is switched off on the platform. | Contact support. |
platform_tenant_not_configured |
503 | A platform-level setup step is missing. | Contact support. |
unavailable |
503 | The service is briefly unavailable (for example during a deploy). | Retry after a short wait. |
Server
| Code | Status | When | What to do |
|---|---|---|---|
server_error |
500 | Something failed on our side. Never includes internal detail. | Retry once; if it persists, contact support quoting request_id. |
When a response has no code of its own, the code comes from the status: 400 bad_request, 401 unauthenticated, 402 limit_reached, 403 forbidden, 404 not_found, 405 method_not_allowed, 409 conflict, 412 version_conflict, 413 payload_too_large, 415 unsupported_media_type, 422 validation_failed, 429 rate_limited, 500 server_error, 503 unavailable.