Reference

Errors

HTTP status codes, stable error codes, and the per-surface response body shapes.

Every error carries the appropriate HTTP status. Branch on the HTTP status first, then treat the stable string code as the machine signal. Do not string-match human-readable messages — they can change without notice.

HTTP status codes

StatusMeaning
400Validation error — a field is missing, malformed, or has an unknown value.
401Authentication failed — missing or invalid API key.
402Insufficient credits — the tenant balance is exhausted.
403Feature not enabled — the tenant is not entitled to the module or tool you called.
404Not found — the resource does not exist or is not visible to this tenant.
409Conflict — the resource already exists (for example, a conflicting idempotent create), or it is in a state that forbids the call.
429Rate limited — too many requests; back off and retry. See rate limits.

Note

403 and 404 deliberately look similar from the outside. A resource that belongs to another tenant returns 404, not 403 — the API never confirms that a resource exists for a tenant it doesn't belong to. 403 is reserved for your own tenant hitting a feature it isn't entitled to run (see standalone tools).

Stable error codes

These string codes are stable machine signals. Match on them rather than on message text, since message copy can be reworded at any time:

CodeTypical statusMeaning
insufficient_credits402The tenant has no credits left to create a session or run a tool.
feature_not_enabled403The tenant's plan/config doesn't include this module or standalone tool.
not_found404The requested resource does not exist for this tenant.
invalid_status400An update-status call passed a value that is not a valid session status.
type is immutable400A Lists API PATCH tried to change a list's type after creation.
import_in_progress / already_deleted409A DELETE on an identity-import batch that is still queued or running, or that has already been deleted.

Note

A unified { "error": { "code", "message" } } envelope across all surfaces is planned. Until it ships, the body shape depends on which surface you hit — covered below. Branching on HTTP status keeps your handling portable across the transition.

Per-surface body shapes

The JSON body currently differs by surface.

Success is a bare object or array. Errors are a single detail string carrying the code or message:

{ "detail": "insufficient_credits" }
{ "detail": "not_found" }
{ "detail": "feature_not_enabled" }

A few /v3 endpoints add a second field alongside detailfeatures on a 403, and a human-readable error on the identity-import validation 400s. detail is always the machine signal; treat anything beside it as extra context for a log line, not as the code to branch on.

Handling patterns

Edge cases worth handling explicitly

Example: robust create handling

const res = await fetch(`${BASE}/v3/session/`, {
  method: "POST",
  headers: { "x-api-key": KEY, "content-type": "application/json",
             "idempotency-key": key },
  body: JSON.stringify(body),
});

switch (res.status) {
  case 201: return res.json();                       // ok (or idempotent replay)
  case 402: throw new TemporaryOutage("insufficient_credits");
  case 403: throw new ConfigError((await res.json()).detail);
  case 429: return retryWithBackoff();                // honour the bucket
  case 400: throw new BadRequest(await res.json());   // fix the request
  default:  throw new Error(`obsidian ${res.status}: ${await res.text()}`);
}
import requests

resp = requests.post(
    f"{BASE}/v3/session/",
    headers={"x-api-key": KEY, "Idempotency-Key": key},
    json=body,
    timeout=15,
)

if resp.status_code == 201:
    session = resp.json()
elif resp.status_code == 402:
    raise TemporaryOutage("insufficient_credits")
elif resp.status_code == 403:
    raise ConfigError(resp.json().get("detail"))
elif resp.status_code == 429:
    session = with_backoff(lambda: requests.post(...))
elif resp.status_code == 400:
    raise BadRequest(resp.json())
else:
    resp.raise_for_status()
resp, err := client.Do(req)
if err != nil {
    return nil, err
}
defer resp.Body.Close()

switch resp.StatusCode {
case 201:
    return decodeSession(resp.Body)
case 402:
    return nil, ErrInsufficientCredits
case 403:
    return nil, ErrFeatureNotEnabled
case 429:
    return withBackoff(createSession)
case 400:
    return nil, decodeValidationError(resp.Body)
default:
    return nil, fmt.Errorf("obsidian %d", resp.StatusCode)
}

Warning

Never build a code path that treats an unrecognised status as success. New status codes can be introduced for new failure modes; a default branch that raises (as above) is safer than one that falls through to "ok."

FAQ