API Reference
Errors
Every status code the API returns, what it means, and how to handle it.
When a request fails, the HTTP status and statusCode agree, statusDetail.status
is ERROR, result is an empty object, and the reason is in
statusDetail.message:
{
"statusCode": 400,
"statusDetail": { "status": "ERROR", "message": "email must be a valid email" },
"result": {}
}Status codes
| Status | Meaning | What to do |
|---|---|---|
400 Bad Request | The request was malformed — invalid JSON, a bad query parameter, a field that failed validation (wrong type, out-of-range, or missing required), or a rejected business rule (e.g. an inactive plan, a past billing date). | Read statusDetail.message for the offending field or rule and fix the request. |
401 Unauthorized | The API key is missing, unknown, or expired. | Send a valid Authorization: Bearer cnk_… header. See Settings → API Keys. |
403 Forbidden | An explicit organizationId on /portal/token doesn't match the key's organisation. | Send the correct organizationId, or omit it. |
404 Not Found | The record doesn't exist — or belongs to another organisation (the two are masked as one). | Check the id. A foreign id looks identical to a missing one by design. |
409 Conflict | The request clashes with current state — e.g. a duplicate employee id, a usage signal against an archived customer, or a pay link for an invoice that isn't OPEN. | Reconcile with the current state, then retry. |
422 Unprocessable Entity | The body is well-formed but can't be applied to the current state — raised by the usage/units flows when the customer has no active plan, the credit/outcome/unit agent key isn't in your workspace, the model isn't enabled, or the customer is out of allowance. (Field-level validation is 400, not 422.) | Read statusDetail.message; fix the referenced entity or top up allowance. Not retryable unchanged. |
500 Internal Server Error | Something went wrong on our side. | Retry with backoff; if it persists, contact support. |
Why foreign ids are 404, not 403
A key is scoped to one organisation, and referencing an id from another
organisation returns 404 everywhere — the same as an id that doesn't exist.
That's deliberate: it stops a key probing for valid foreign ids by telling
"forbidden" apart from "not found". The only endpoint that distinguishes them is
/portal/token.
Handling errors
Treat any non-2xx status as a failure and read statusDetail.message for the
reason. 4xx codes mean your request needs to change and won't succeed on a
blind retry; 500 is transient and safe to retry.
const res = await fetch(url, init);
const body = await res.json();
if (body.statusDetail.status !== "SUCCESS") {
// body.statusCode → 400 | 401 | 403 | 404 | 409 | 422 | 500
// body.statusDetail.message → human-readable reason
throw new Error(`${body.statusCode}: ${body.statusDetail.message}`);
}Typed errors in the SDK
The official @clocknext/sdk client throws a typed error for
each status, all extending ClocknextError (with .status and .retryable), so
you can branch on the failure mode instead of parsing the message:
import { AuthError, NotFoundError, AllowanceError } from "@clocknext/sdk";
try {
await cnk.customers.get("cus_missing");
} catch (err) {
if (err instanceof NotFoundError) {/* 404 */}
else if (err instanceof AuthError) {/* 401 */}
else if (err instanceof AllowanceError) {/* 422 — out of allowance */}
}Retries
Only retry codes that can succeed unchanged:
- Retry
500(and network/timeout failures) with exponential backoff. - Do not retry
4xx— the request itself is the problem; fix it first.
Be careful retrying non-idempotent writes (a POST that creates a record).
If a POST /customers times out you can't tell whether it landed, so a blind
retry risks a duplicate. Prefer to look the record up before retrying, or use the
@clocknext/sdk client, which only auto-retries replay-safe
calls.