Error Handling
Every non-2xx response from the Not AI Public REST API uses the same JSON envelope and the same enumerated code list. Treat the HTTP status as the coarse signal and the error.code string as the stable identifier you write logic against.
Error envelope
{
"error": {
"code": "INVALID_API_KEY",
"message": "The supplied API key is not valid.",
"details": {
"field": "..."
}
}
}
codeis a stable identifier from the enumerated list below. Codes never change meaning inside a major version.messageis a human-readable sentence. Treat it as opaque. Copy changes are not breaking.detailsis an optional object with field-level context. Validation errors populate it with the offending field name; most other errors omit it.
Health (GET /health) and the OpenAPI document (GET /openapi/v3.json) are the only routes that never return this envelope. They have no error responses defined in v1.
HTTP status codes
| Status | Meaning |
|---|---|
200 |
Success. Body is the response envelope. |
204 |
Success with no body. Used where the operation is intentionally empty. |
400 |
The request was malformed or failed validation. details will name the offending field. |
401 |
The request was unauthenticated. The key was missing (MISSING_API_KEY) or invalid (INVALID_API_KEY — malformed, unknown, or for the wrong region). |
402 |
A plan-tier limit was reached (e.g. registering a webhook subscription past the plan’s cap). Upgrade or free a slot. |
403 |
The key is valid but is not permitted to access the requested resource (for example, a session that belongs to another integration). |
404 |
The requested resource does not exist. |
429 |
A per-minute rate-limit window was exhausted. A Retry-After header is included when the edge can compute one; if absent, back off with exponential delay. See Rate Limits. |
5xx |
The API failed to serve the request. Retryable for idempotent reads. |
Stable error codes
These codes are stamped on the v1 contract. New codes may be added in v1 (additive change); existing codes will never be removed or repurposed.
| Code | Typical status | When you see it |
|---|---|---|
MISSING_API_KEY |
401 | The request carried no Authorization: Bearer and no x-api-key header. |
INVALID_API_KEY |
401 | The presented key was malformed, unknown, revoked, or for the wrong region. Identical envelope for all four cases by design. |
INVALID_REQUEST |
400 | A query string or path parameter failed validation. details names the offending field. |
INVALID_BODY |
400 | The request body was malformed, missing a required field, or otherwise not valid JSON for the target route. |
INVALID_URL |
400 | A webhook subscription URL was rejected by the SSRF / format validator (bad scheme, non-80/443 port, userinfo, private/loopback/link-local host, blocked metadata host, URL longer than 2048 chars). |
INVALID_EVENT_TYPE |
400 | A webhook subscription’s eventType was not one of the documented event-type strings. |
NOT_FOUND |
404 | The path resolved to a resource that does not exist or is not visible to this integration. |
FORBIDDEN |
403 | The key resolved successfully but cannot access the resource. |
PLAN_LIMIT_REACHED |
402 | The integration is at the plan-tier cap for the resource (e.g. webhook subscriptions). Free a slot or upgrade. |
RATE_LIMITED |
429 | A per-minute rate-limit window was exhausted. Authorization: Bearer callers are isolated by key; x-api-key callers share a per-tier edge bucket and can be throttled by unrelated same-tier traffic. Honor Retry-After when present; otherwise back off exponentially. |
UNSUPPORTED_OPERATION |
400 | The endpoint is documented but the specific combination of arguments is not yet supported. Adjust the request. |
SERVICE_UNAVAILABLE |
503 | A downstream dependency is unavailable. Retry with backoff. |
INTERNAL_ERROR |
500 | An unhandled exception was caught by the global error middleware. Retry with backoff and report if it persists. |
Examples
400, bad query string:
{
"error": {
"code": "INVALID_REQUEST",
"message": "pageSize must be between 1 and 100.",
"details": { "field": "pageSize" }
}
}
401, the credential was malformed or unknown:
{
"error": {
"code": "INVALID_API_KEY",
"message": "The supplied API key is not valid."
}
}
401, no credential was presented:
{
"error": {
"code": "MISSING_API_KEY",
"message": "Provide your API key via 'Authorization: Bearer aik_v1_...' or the 'x-api-key' header."
}
}
404, unknown session id:
{
"error": {
"code": "NOT_FOUND",
"message": "No session with that id is visible to this integration."
}
}
500, unhandled server-side fault:
{
"error": {
"code": "INTERNAL_ERROR",
"message": "An unexpected error occurred."
}
}
Retry semantics
The API performs no automatic server-side retries on your behalf. Clients should implement their own retry policy with the following rules:
- 2xx. Never retry.
- 400, 401, 403, 404. Never retry. Fix the request and try again.
- 429. Honor
Retry-Afterwhen the response includes it; otherwise back off with exponential delay and jitter. Do not retry sooner than the floor of either path. - 5xx (500, 502, 503, 504). Retry the request with exponential backoff and jitter.
GETretries are always safe. For mutating requests (POST,PUT,PATCH,DELETE) verify the operation is idempotent at your endpoint before retrying —PUT/DELETEare idempotent by design,POSTcreate endpoints document their idempotency behavior per route.
A reasonable default policy for a server-to-server consumer:
import os
import time
import random
import requests
BASE_URL = "https://stg-api.isnotai.com"
HEADERS = {"Authorization": f"Bearer {os.environ['ISNOTAI_API_KEY']}"}
def get_with_retry(path, params=None, max_attempts=5):
for attempt in range(max_attempts):
response = requests.get(f"{BASE_URL}{path}", headers=HEADERS, params=params, timeout=10)
if response.status_code < 400:
return response.json()
if response.status_code == 429:
retry_after = response.headers.get("Retry-After")
if retry_after and retry_after.isdigit():
time.sleep(int(retry_after))
else:
time.sleep((2 ** attempt) + random.random())
continue
if 500 <= response.status_code < 600:
backoff = (2 ** attempt) + random.random()
time.sleep(backoff)
continue
response.raise_for_status()
raise RuntimeError(f"giving up on {path} after {max_attempts} attempts")
Log the full error envelope (code, message, and details) when a retry policy gives up. That is what Not AI support will need to triage.