Getting 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 |
| 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.
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
}
}