Axuz Developers

Errors

Real error response format of the Axuz API, HTTP status map, the resource 404 versus the route 404, tracing with X-Trace-Id and when it is safe to retry a call.

Every error response comes with an HTTP status and, almost always, a small JSON body. Decide what to do from the status; use the body text to show to the user or to log, not as a contract.

The format

The canonical format is an object with an error key:

{ "error": "task not found" }

This holds for the main service and for the workspace, calendar, forms, files and billing services. Some errors add fields of their own next to error:

SituationBody
Validation 422 (UnprocessableEntity helper){"error": "name_required"}, optionally with "message" carrying the detail
Personal token 429{"error": "rate_limited", "limit": 60, "count": 61, "retry_after": 12}
Mutation 429 on team routes{"error": "rate_limited", "retry_after": 12}
IP-block 429 on the authentication service{"error": "ip_blocked", "retry_after_seconds": 300}
503 when the limiter is down (fail-closed mode){"error": "rate_limiter_unavailable"}
409 for a custom field that is in use{"error": "field_has_values"}

The OAuth endpoints (/oauth/token, /oauth/revoke, /oauth/introspect) follow the OAuth 2 error format (RFC 6749), with error as a short code such as rate_limited; see Authentication.

The error text is not a stable code

The value of error is one of two things: a slug (board_not_found, name_required, field_has_values) or a short English phrase (task not found, missing team membership, invalid body). Slugs tend to be stable; phrases can be reworded without notice. So:

  • branch on the HTTP status (and at most on slugs the reference names);
  • never do error.includes("not found") to decide control flow;
  • show the user a message of your own, and keep the original error in your log.

In some older handlers of the main service a database failure may still arrive as a 500 with the raw driver text in error. Do not depend on it, do not show it to users and do not parse it: the workspace service already returns only internal error and records the detail on the server.

Status map

StatusMeaningReal error examplesWhat to do
400Malformed request: invalid JSON, invalid id, invalid parameterinvalid body, name required, invalid teamId, invalid before, at least one scope requiredFix the request. Do not resend it unchanged.
401No valid credential: missing, expired or revoked token, or ended sessionunauthorizedRenew or recreate the credential. Do not resend unchanged.
403Authenticated, but not allowedmissing team membership, insufficient team role: requires admin, scope required: me:read, api key not accepted on this routeA role, scope or membership is missing. Retrying will not help.
404The resource (or route) does not exist, or is not visible to youtask not found, team not found, board_not_found, no draftSee the next section.
409Conflicts with the current stateslug already taken, an export is already queued or running for this scope, field_has_valuesResolve the conflict; re-reading the resource is usually the first step.
413Body too largebody too largeShrink the body (or send files through the upload flow).
422Valid JSON, but a business rule is violatedname_required, invalid_rangeFix the data. Do not resend unchanged.
429Request limit reachedrate_limitedWait for Retry-After. See Rate limits.
500Internal failureinternal errorMay be retried with backoff, only if the operation is safe (below).
503Dependency unavailable or limiter in fail-closed moderate_limiter_unavailable, an idempotency-guard-unavailable messageRetry with backoff; honour Retry-After when present.

Two details that often cause confusion. First, the calendar service sometimes writes its message in Portuguese (cursor inválido), because the human-readable text varies per service. Second, a denied authorization is not always a 403: to avoid revealing that resources of other teams exist, several routes answer 404 when the resource exists but belongs to another tenant.

The singular-resource 404 and the route 404

A 404 has two meanings that the client must tell apart.

Contractual 404. Some routes that return one resource per scope answer 404 with a slug when the resource simply has not been created yet. This is by design, not a failure. Two real cases:

  • GET/teams/{teamId}/timelines/{timelineId}/board answers 404 with {"error": "board_not_found"} when the channel has no board yet. The right flow is to offer “create board” and call the POST on the same path, which is idempotent: it returns 201 on creation and 200 with the existing board if there already was one.
  • GET/teams/{teamId}/timelines/{timelineId}/draft answers 404 with {"error": "no draft"} when there is no saved draft for that channel (and parent_post_id). Treat it as “empty”, not as an error.

Route 404. A path the API does not know (a typo, the wrong version, the wrong service) also yields 404. The main and workspace services have no custom 404 handler: the response comes from the router, normally as plain text (404 page not found), with no JSON.

In practice, the client should handle it like this:

const res = await fetch(url, { headers });
if (res.status === 404) {
  const type = res.headers.get('content-type') ?? '';
  if (!type.includes('application/json')) {
    throw new Error(`no such route: ${url}`); // your bug, do not retry
  }
  const { error } = await res.json();
  if (error === 'board_not_found') return null; // singular resource not created yet
  throw new Error(`resource not found: ${error}`);
}

Tracing: X-Trace-Id

The main service returns the X-Trace-Id header on every response. If you send your own X-Trace-Id (up to 128 characters, with no quotes, backslashes or line breaks) it is preserved; otherwise the server generates a UUID. The value shows up in server logs and is what support uses to find the exact line of a failure. Store it with any 5xx you log or report.

curl -sS -i https://api.axuz.net/me/summary \
  -H "Authorization: Bearer $AXUZ_TOKEN" \
  -H "X-Trace-Id: my-job-2026-10-22-0001" | grep -i '^x-trace-id'

The workspace service correlates internal errors by the router’s request identifier, but it is not documented that it returns it in a response header; do not rely on that.

Retrying safely

General rule: retry only when the failure is transient and the operation is idempotent.

  • Always safe to retry: GET, HEAD, PUT and DELETE (the end result is the same; a second DELETE may return 404 because the resource is already gone, and that counts as success), on statuses 429, 500, 502, 503, 504 and on network failure, with exponential backoff and jitter.
  • Never retry blindly: a POST that creates something. If the connection drops after you sent it, you cannot know whether the server processed it. On routes that accept the Idempotency-Key header, send a unique key per operation; a retry returns the same response with Idempotent-Replay: true. The same key with a different body gives 422, and the same key still in flight gives 409. Routes without that support must be reconciled by reading state (for example, listing before recreating).
  • Never retry: 400, 401, 403, 404, 409, 413 and 422. The same request will produce the same response.
import random
import time

RETRY_STATUS = {429, 500, 502, 503, 504}

def backoff(attempt: int, retry_after: float | None) -> float:
    """Wait: honour Retry-After; otherwise exponential with full jitter."""
    if retry_after is not None:
        return retry_after + random.uniform(0, 0.5)
    return random.uniform(0, min(30, 0.5 * 2 ** attempt))

The complete example, reading Retry-After, is in Rate limits.

Updated