Skip to content

Best practices

Production readiness

What to check before you point real callers at an assistant: the failure paths, the endpoint contracts, and the signals worth watching in week one.

An assistant that demos well and an assistant that survives a Monday morning differ in almost nothing you can hear. They differ in what happens when something goes wrong: when nobody picks up the transfer, when your endpoint is slow, when a webhook is dropped, when a caller goes silent and never comes back.

Those paths never run during a demo, which is exactly why they are the ones to check before real traffic arrives.

The call path#

The first requirement is that a caller reaches an assistant at all, and that a call cannot get stuck.

The number is owned and linked. A number with no assistant has nothing to answer it. Confirm assistantId is set on the phone number you are about to publish, and that everything you send is E.164: +15555550123, never (555) 555-0123.

Failover is set, and the kill switch is off. failoverNumber is where calls go if the assistant cannot take them; it can be set on the number, on the assistant, or both. failoverMode: true is a different thing entirely. It forwards every call straight to that number without the assistant ever answering. Set the number, leave the mode off, and know the flag exists so you can reach for it deliberately during an incident rather than discovering it left on afterwards.

Calls cannot run forever. callTimeLimitSeconds (10 to 43200) caps duration. Without it, a call that fails to end quietly stays open. Pick a limit generously above your longest real conversation.

Silence ends cleanly. A caller who puts the phone down without hanging up holds an open line indefinitely. idleMessages are spoken after idleTimeoutSeconds (up to 60) of quiet, at most idleMessageMaxSpokenCount times (up to 10), after which the call ends. Write them as an escalating sequence ending in a graceful close, not as the same line three times.

The greeting is right. firstMessage is spoken exactly as written, before the assistant has heard the caller, so it is also the line that sets the language of the first exchange on a multilingual line. Say the business name in it; callers who are not sure they dialled the right place hang up.

Escalation actually escalates#

The most damaging production failure is not a wrong answer. It is a caller who asked for a person and got dead air.

Match the transfer mode to the destination. A cold transfer leaves OnCore the moment it connects: if the line rings out, that is your phone system's behavior and OnCore cannot recover it. That is fine for an always-staffed queue or an IVR. For a named person, a small team, or anything out of hours, use warm-return, so a caller whose hand-off is not answered comes back to the assistant instead of into silence.

Write the return message. warmReturnMessage (up to 2000 characters) is what the caller hears at that moment. Write it as an opening rather than an apology. The caller is still on the line and the assistant can still take a message or offer a callback.

Tune the dial timeout. transferDialTimeout (1 to 60 seconds) is how long a transfer attempt is given to connect. Too short and you never reach anyone; too long and the caller waits through a full ring cycle before anything else happens.

Delay the whisper if the destination talks first. warmWhisperDelaySeconds (0 to 30) pauses the briefing so it does not land on top of an automated greeting. Two to three seconds usually clears one.

Act on the misses. supervisor_answered: false on the end-of-call report means a caller wanted a person and did not reach one. Route it somewhere a human sees it before you go live. That single signal is the most actionable thing the platform tells you.

Check the trigger, not just the destination. A transfer only happens if a transfer reason matches the situation, and matching is driven by how you described it. A perfectly configured destination behind a vague trigger never fires.

Your webhook endpoint#

Most integrations lose data here, and they lose it silently.

Verify every delivery. Voice, messaging and tool events carry your client server secret in the x-sadie-core-secret header. Compare it against the value you hold, and reject mismatches. WhatsApp onboarding events are signed instead, with an HMAC over the raw request body; parse after verifying, never before, or the bytes will not match. Verifying webhooks has both.

Respond 2xx first, process after. Voice, messaging and tool deliveries are sent once. A non-2xx, a timeout, or a crash in your handler means the event is gone, not retried. Acknowledge immediately and do the work asynchronously. This is the single most common way a production integration loses call results.

The two models behave differently

The shared-secret deliveries are at-most-once, no retries. The HMAC-signed WhatsApp deliveries are at-least-once with up to 5 retries, and all retries reuse the same X-OnCore-Delivery id, so that path needs deduplication, and the other path needs speed. Building both endpoints the same way gets one of them wrong.

Know which URL is live. An assistant's serverUrl override wins over your tenant-level server URL. If you set an override once and are now watching the fallback, you will see nothing and conclude the platform is broken. Also note that PATCHing notifications appends URLs rather than replacing them. Check the list after every update, or it grows.

Be reachable. localhost and private addresses are not reachable from OnCore, and URLs are validated at write time, so unresolvable hosts are rejected outright. Confirm your production URL resolves publicly before launch, not during it.

Have a backfill path. Because deliveries are not retried, plan for a lost one: the call record is always readable from the calls API, so a nightly reconcile against your own records closes the gap without polling.

Handle both shapes on a shared endpoint. Voice events carry call_id; messaging events carry conversation_id plus channel and customer_identifier. Branch on which fields are present rather than assuming one payload.

Your API client#

Branch on error.code, never on error.message. Codes are the contract; messages are written for people and get reworded. The error envelope is identical across the whole API, so one handler covers it.

Retry with backoff and jitter. Treat any 429 as throttling and 5xx as potentially transient, back off exponentially with randomized delay, and honour an explicit cooldown when one is supplied. Backoff is a floor, not a replacement. Rate limits has the shape of the retry loop.

Never blind-retry a POST. There is no idempotency key, so a retried create after a timeout can produce a duplicate assistant, number, or form. Reconcile with a GET before retrying. PATCH, PUT and DELETE are safe to replay.

Reduce the load rather than surviving it. Page with limit up to 500 instead of many small pages, cache the voice, model and transcriber lookups at startup since they rarely change, filter server-side with search, and let webhooks push call results instead of polling for them. The cheapest request is the one you never send.

Tolerate change. Deserialize permissively and ignore fields you do not recognize, treat IDs as opaque, and do not depend on array order unless the endpoint documents one. Assistant settings sub-objects and the lookup catalogues are the parts expected to grow.

Keys and secrets#

  • The API key is a server-side secret. Never ship it to a browser or a mobile app. For browser-facing features, exchange it server-side for a short-lived token. That is exactly what browser calling and embedded web chat do.
  • Rotation is immediate and has no rollback. The previous key stops working the moment you rotate, so schedule it and update your integration in the same change.
  • The client server secret is not an API key. It verifies inbound webhooks and will never authenticate an outbound request. Confusing the two produces a 401 that looks inexplicable. Both live on the API Keys page; see API keys.
  • Store both in your secret manager, never in source control.

recordingEnabled is set per assistant and applies to every call that assistant handles; there is no per-call opt-in. Transcripts, summaries and analysis are produced either way, so turning recording off costs you the audio and nothing else.

Recording-consent law varies by jurisdiction and complying with it (including notifying callers) is yours to handle. Because the switch is per assistant, you can record some lines and not others. Deletions run on your own retention schedule and each cleanup is reported through retention audits, so removals stay auditable.

Rehearse before you publish the number#

Place real calls, not scripted ones#

Browser calling connects you to the same assistant without dialling. Run the paths that matter: the happy path, the transfer, the tool call, the question it cannot answer, and a call where you say nothing at all.

Interrupt it#

Talk over the assistant mid-sentence. Give two answers to one question. Change your mind halfway through a booking. Real callers do all three, and none of it appears in a scripted test.

Confirm the webhook arrived#

Check your handler's log for every one of those calls, not just the first. A verification bug that rejects one payload shape is invisible until that shape occurs.

Read the transcripts back#

Compare what was said against what you intended. Writing prompts for voice covers the turn-level issues (stacked questions, unspoken formatting, unconfirmed details) that only show up in a transcript.

Break something on purpose#

Point a transfer at a number nobody answers and confirm the caller comes back rather than ringing out. Take your tool endpoint offline and hear what the assistant does. These are the paths you are actually shipping.

What to watch in week one#

Configuration is read when a call starts, so a fix applies from the next call, which makes the first week the cheapest time to correct things. Four signals repay daily attention:

SignalWhereWhat it tells you
supervisor_answered: falseEnd-of-call reportA caller asked for a person and did not get one
endedReason and status spreadGET /calls, GET /calls/statusesWhether calls are ending the way you expect
Success rate and average duration per assistantGET /calls/summary-by-assistantA drift in either usually precedes a complaint
Category mixcategory / category_list on the report, analysis on the callWhat callers actually ask about, versus what you built for

Enable csatFeedback if you want satisfaction ratings alongside those, and read a handful of transcripts every day regardless. Aggregates tell you something changed; transcripts tell you what.

Next steps#