Mint a key
- Open Settings → Integrations → API Call Upload and click Create API key.
- Give it a descriptive name (e.g.
prod-twilio-uploader). - Copy the key once — Zelto only stores a hash.
Set up with a coding agent
Don’t want to wire this up by hand? Paste the prompt below into the coding agent in your editor — Claude Code, Cursor, Codex, or similar. It points the agent at this guide and the API reference, spells out the full upload contract, and has it send each finished conversation following your own codebase’s conventions. You still supply the key: mint one above and expose it asZELTO_API_KEY — the
prompt tells the agent to read it from the environment, never hardcode
it.
Paste into your coding agent
Upload a conversation
POST the whole conversation — agent, transcript, optional recording, and metadata — in one request to/webhooks/calls. Zelto acknowledges
immediately and processes it into a conversation and transcript in the
background. The API Reference tab covers the REST conventions and has a request
playground for verifying a payload before you wire up production.
The example below is a voice call; a text conversation is the
same shape with the audio fields left out.
call.json
200 with { "received": true }.
Only call.externalId and an agent reference are required; everything
else is optional. Provide either agent.externalId (Zelto finds or
creates the agent for you) or agentId to attach the call to an agent
you already created in the dashboard. Include call.recordingUrl and Zelto
re-hosts the audio so the player keeps working past your source’s recording TTL.
Set version to label the agent build or prompt revision behind the call
(e.g. "v3"); Zelto tracks it as the call’s agent version so you can compare
performance across versions on the agent’s page. It’s optional and works with
either agent reference — send the same string for calls on the same build.
Text and messaging conversations
The same endpoint ingests text conversations — a chatbot or messaging agent (web chat, SMS, WhatsApp, in-app). Send the messages astranscript.turns
(role: "user" for the person, role: "assistant" for your agent) and omit
the audio and telephony fields: recordingUrl, and usually durationSeconds
and customer.number. There’s no recording to re-host and no per-turn audio
timing, so drop startSeconds/endSeconds too. Record the channel in
metadata (there’s no dedicated channel field) if you want to filter on it
later.
chat.json
Handle errors
Errors come back as JSON with anerror string; validation and provider errors
add a details object naming what to fix.
| Status | error | When |
|---|---|---|
401 | Unauthorized | Missing or invalid API key. |
400 | Invalid JSON | The body isn’t valid JSON. |
400 | Unsupported provider | X-Zelto-Provider is set to an unrecognized value. |
400 | Validation failed | A field is missing or the wrong type — details lists each offending field. |
A
200 means Zelto accepted the conversation and is processing it in the
background — not that ingestion has finished. Bad auth and malformed bodies
fail synchronously with a 4xx, but a semantic problem like an agentId that
doesn’t exist in your org still returns 200 and then fails quietly, so it
never appears. Prefer agent.externalId (find-or-create) to avoid that, and
confirm conversations land under Conversations.A delivery that carries neither a transcript nor a recording is treated as
an unconnected dial: Zelto acks it with 200 but intentionally creates no
conversation, since there’s nothing to transcribe or analyze. Send the call
once it has a transcript or recordingUrl (a later
enrichment re-send with one of them ingests
normally).Idempotent re-uploads
call.externalId is the dedup key. POSTing the same id again updates
the existing conversation in place — refreshing the recording, transcript,
duration, or cost — and never double-charges. Send the call once when it
ends, then re-send later if you enrich it (e.g. once the recording
finishes uploading).
Already on Vapi or Retell?
The same endpoint also accepts native Vapi and Retell webhook payloads — set theX-Zelto-Provider header to vapi or retell and POST that
provider’s body unchanged. Omit the header (or send zelto) to use the
canonical shape above.
Verify the first call
After your first POST, open Conversations — the call appears within a few seconds with its transcript. The API Reference tab’s request playground is the fastest way to send a test payload. If a call doesn’t show up, check the response your POST got —{ "received": true } means Zelto accepted
it, while a 4xx returns the reason in the body (an unsupported X-Zelto-Provider
value, a failed field validation, or an invalid key). See
Connect a voice provider.
When to use this vs a native integration
If you already run on Vapi or Retell, use the native integration — webhooks land for free and recordings are fetched for you. This page is for everything else: Bland, Pipecat, a text or messaging agent, in-house stacks, or anything we haven’t shipped a first-class card for yet. Running on LiveKit? It has a dedicated LiveKit page.Related
- Connect a voice provider — pick the right path.
- LiveKit — the same endpoint, with a worker example.
- API reference — REST conventions and the full request shape.
- Conversations — where uploaded calls land.

