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
| Status | Meaning |
|---|---|
400 | Validation error — a field is missing, malformed, or has an unknown value. |
401 | Authentication failed — missing or invalid API key. |
402 | Insufficient credits — the tenant balance is exhausted. |
403 | Feature not enabled — the tenant is not entitled to the module or tool you called. |
404 | Not found — the resource does not exist or is not visible to this tenant. |
409 | Duplicate — the resource already exists (for example, a conflicting idempotent create). |
429 | Rate 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:
| Code | Typical status | Meaning |
|---|---|---|
insufficient_credits | 402 | The tenant has no credits left to create a session or run a tool. |
feature_not_enabled | 403 | The tenant's plan/config doesn't include this module or standalone tool. |
not_found | 404 | The requested resource does not exist for this tenant. |
invalid_status | 400 | An update-status call passed a value that is not a valid session status. |
type is immutable | 400 | A Lists API PATCH tried to change a list's type after creation. |
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" }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."