Skip to content

Conventions

Errors

Errors use HTTP status codes and one JSON shape everywhere, with a stable code to branch on and a request_id to quote when something looks wrong.

The error object

400 Bad Request
{
  "error": {
    "code": "validation_failed",
    "message": "Some fields are invalid.",
    "details": {
      "email": [
        "Must be a valid email address."
      ],
      "first_name": [
        "Required."
      ]
    },
    "request_id": "5f0c2e7a-9b4d-4a1c-8e3f-6d2b9a7c1e05"
  }
}
  • codestring

    Stable and machine-readable — branch on this. The codes are listed below.

  • messagestring

    For people, in English. It may change; do not parse it.

  • detailsobjectNullable

    Extra data for some codes: the invalid fields for validation_failed, retry_after for rate_limited, hub and upgrade_url for hub_not_in_plan.

  • The ID of this request, also in the X-Request-Id header.

HTTP status codes

HTTP status codes
StatusMeaning
200 OKThe request succeeded. Reads, updates, deletes and restores return 200.
201 CreatedA POST created a record. The body holds the new record.
400 Bad Requestvalidation_failed or invalid_json.
401 Unauthorizedunauthorized: no valid API key.
403 Forbiddeninsufficient_scope, forbidden, hub_not_in_plan or workspace_read_only.
404 Not Foundnot_found: unknown, or not visible to the key’s owner.
409 Conflictidempotency_key_reused.
429 Too Many Requestsrate_limited. Wait for Retry-After seconds.
500 Internal Server Errorinternal_error. Retry with backoff.
503 Service UnavailableThe workspace is briefly unavailable, for example during an update. The body may not be JSON. Retry with backoff.

Error codes

  • A query parameter or body field is invalid: a wrong type, a value out of range, an unknown filter or sort field. details maps each field to its messages.

    What to doFix the fields listed in details. Retrying the same request returns the same error.

  • The request body is not valid JSON.

    What to doSend a JSON object with Content-Type: application/json.

  • The Authorization header is missing or malformed, or the key is unknown, revoked or expired.

    What to doSend Authorization: Bearer sk_live_… with an active key.

  • The key is valid but does not carry the scope this endpoint requires.

    What to doUse a key with the scope shown on the endpoint, for example deals:write.

  • The key’s owner is not allowed to do this: their role in the workspace does not permit the action.

    What to doUse a key owned by someone with the permission, or change the owner’s role.

  • The endpoint belongs to a hub the workspace’s plan does not include. details.hub names the hub and details.upgrade_url links to the plan.

    What to doAdd the hub to the plan, then retry.

  • The workspace is read-only, for example after its subscription ended. Reads still work; writes are refused, except creating leads.

    What to doRestore the subscription. Writes succeed again as soon as it is active.

  • The path is unknown, or the record does not exist or is not visible to the key’s owner. The API does not tell these apart.

    What to doCheck the ID, and that the key’s owner can see the record in the app.

  • The Idempotency-Key was used in the last 24 hours with a different request body.

    What to doGenerate a new key for every new request; reuse a key only to retry the same request.

  • The key sent more requests than its limit allows. details.retry_after and the Retry-After header give the seconds to wait.

    What to doWait Retry-After seconds, then retry. Spread bulk work over time.

  • Something failed on our side. The request may or may not have been applied.

    What to doRetry with exponential backoff, with an Idempotency-Key for POST. Send the request_id to support if it persists.

  • The workspace cannot serve the request right now, for example while it starts up or is being updated. Nothing was applied.

    What to doRetry after a short pause with exponential backoff, with the same Idempotency-Key for POST.

New codes may be added; treat an unknown code by its HTTP status.

Validation errors

validation_failed lists every problem at once, so a form or an import can show them all. details maps a field — or a query parameter, such as sort or a filter — to its messages.

Fields the API does not know are ignored rather than refused, so a typo in a field name passes silently. Check the response: it holds what was stored.

Handling errors

Retry what can succeed later — 429, 500 and 503 — with growing delays, and fix the request for anything else. Give POST requests an Idempotency-Key so a retry can never create a record twice.

nextora.mjs
const RETRYABLE = new Set([429, 500, 502, 503, 504]);

async function nextora(path, init = {}, attempt = 1) {
  const response = await fetch(`https://acme.nextora.co/api/v1${path}`, {
    ...init,
    headers: {
      Authorization: `Bearer ${process.env.NEXTORA_API_KEY}`,
      'Content-Type': 'application/json',
      ...init.headers,
    },
  });
  if (response.ok) return response.json();

  if (RETRYABLE.has(response.status) && attempt < 5) {
    const retryAfter = Number(response.headers.get('Retry-After'));
    const delay = retryAfter > 0 ? retryAfter * 1000 : 2 ** attempt * 250 + Math.random() * 250;
    await new Promise(resolve => setTimeout(resolve, delay));
    return nextora(path, init, attempt + 1);
  }

  const body = await response.json().catch(() => null);
  const error = new Error(body?.error?.message || `HTTP ${response.status}`);
  Object.assign(error, { status: response.status, code: body?.error?.code, details: body?.error?.details, requestId: body?.error?.request_id });
  throw error;
}

Request IDs

Every response has an X-Request-Id header, and every error repeats it as request_id. Log it with failures; when you write to [email protected], the request ID lets us find the exact request.