Get started
Errors
The error envelope, status codes, and error codes the Core API returns.
Every error from the Core API uses one consistent JSON envelope, so a single error handler covers the whole API:
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Validation error",
"details": [
{ "field": "phoneNumber", "message": "phoneNumber must be E.164-formatted (e.g. +15555550123)" }
]
},
"metadata": {
"timestamp": "2026-07-30T12:00:00.000Z",
"version": "1.0",
"path": "/assistants"
}
}| Field | Type | Description |
|---|---|---|
success | boolean | Always false for errors |
error.code | string | Machine-readable error code; branch on this, not on message |
error.message | string | Human-readable explanation |
error.details | array | object | Optional extra context. For validation failures it is an array of { field, message } objects, one per invalid field |
metadata.timestamp | string | ISO 8601 time the error was produced |
metadata.version | string | API version |
metadata.path | string | The request path |
Successful responses use the same envelope with "success": true and a data field instead of error.
Status codes and error codes#
| Status | Code | When |
|---|---|---|
| 400 | VALIDATION_ERROR | The body or query parameters failed validation; see error.details for the offending fields |
| 400 | PARSE | The request body is not valid JSON |
| 401 | UNAUTHORIZED | Missing, malformed, or invalid Authorization header; see Authentication |
| 403 | FORBIDDEN | The key is valid but not allowed to act on this resource (e.g. the tenant does not belong to your organization) |
| 404 | RESOURCE_NOT_FOUND | The route exists but the resource does not (or belongs to another tenant) |
| 404 | NOT_FOUND | The route itself does not exist |
| 409 | CONFLICT | The request conflicts with current state. Some endpoints return more specific 409 codes (e.g. WHATSAPP_ONBOARDING_IN_PROGRESS); always branch on error.code |
| 429 | Endpoint-specific | The request was throttled. WhatsApp onboarding and transcriber testing (BACKLOG_FULL) publish specific 429 contracts; use the returned cooldown field when present and otherwise back off with jitter |
| 500 | INTERNAL_SERVER_ERROR | Something went wrong on OnCore's side. Internals are never leaked; the message is generic |
Codes can be more specific
error.code is the stable contract. Individual endpoints may return endpoint-specific codes on the same status (for example the WhatsApp onboarding endpoints return codes like WHATSAPP_RESEND_NOT_ALLOWED), so match codes rather than status numbers where the docs call them out.
Deliberate exceptions to the envelope#
Two embed-facing endpoints intentionally do not use the envelope for their 404 responses:
POST /webrtc-call/connect: Browser voice calls- the
/web-chat/*routes: Embedded web chat
When a phone number is unknown, belongs to another tenant, or has been deleted, they all return the same fixed body:
{ "error": "not_found" }The body is byte-identical across all three cases on purpose: these endpoints face end-user embeds, and a distinguishable response would let someone probe which phone numbers exist on the platform. Their success responses are also bare JSON (no envelope). See the channel guides for the exact shapes.
The live call monitoring routes are a third exception, for the same reason plus one more. POST /calls/{id}/monitor-session, /takeover, and /end all answer with bare JSON, because a browser's fetch hands the join material straight to a client library, and their 404 is the same byte-identical { "error": "not_found" } for an unknown call, another tenant's call, and a deleted one. All three also refuse with 409 carrying a machine-readable reason the envelope has no field for:
{ "error": "conflict", "reason": "not_live" }Branch on reason; the guide lists the whole set and what each one means. Their 400 is not an exception, because an over-length field is an ordinary validation failure with no reason code to carry, so it arrives in the envelope above. Neither are the two GET routes on that page, /calls/live and /calls/{id}/monitor-sessions, which use the envelope like the rest of the calls API.
Handling errors#
const res = await fetch("https://core-api.heysadie.ai/assistants", {
headers: { Authorization: `ApiKey ${process.env.SADIE_API_KEY}` },
});
const body = await res.json();
if (!body.success) {
switch (body.error.code) {
case "UNAUTHORIZED":
// rotate / fix the key
break;
case "VALIDATION_ERROR":
// body.error.details -> [{ field, message }]
break;
default:
// log body.error.code + body.error.message
}
}Debugging a failure#
When a specific class of request keeps failing, the troubleshooting pages work through the causes in order:
- Authentication failures: 401, 403, and the
X-Tenant-Idrule. - Webhooks not arriving: delivery and signature verification.
- Calls and assistant behavior: connection, prompts, and tools.
Next steps#
- Rate limits: detecting throttling and backing off correctly.
- Authentication failures: the rejections that happen before your request reaches any data.
- Conventions: the response envelope every endpoint follows.