Skip to main content

Mint a key

  1. Open Settings → Integrations → API Call Upload and click Create API key.
  2. Give it a descriptive name (e.g. prod-twilio-uploader).
  3. Copy the key once — Zelto only stores a hash.
The card lists active keys with their last-used timestamp. Delete a key any time to revoke access immediately. Minting and revoking need the owner or admin role — a key reads every conversation in the organization, so it isn’t a member-level credential.

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 as ZELTO_API_KEY — the prompt tells the agent to read it from the environment, never hardcode it.
Paste into your coding agent
If your editor has the Zelto MCP server connected, the agent can call its read_docs tool to pull the always-current contract instead of the embedded copy — and the same key works for MCP, the REST API, and this upload flow.

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
A successful upload returns HTTP 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.

Agent groups

Send agent.groups to organize agents that belong together — for example the several agents you run for one customer, or one campaign. Each name is a group (a company, area, or campaign); Zelto finds-or-creates it in your organization and adds the agent to it. An agent can belong to more than one group, and the field is additive — a later upload never removes a group the agent already has, so it’s safe to send on every call. Once an agent is in a group you can filter the whole dashboard by that group from the sidebar, and filter the conversations list by group. You can also manage an agent’s groups by hand on its settings page in the dashboard.

Tool calls

If your agent invokes tools mid-call (function calls — check availability, book an appointment, look up an order), forward each one as a tool turn carrying a structured toolCall. Zelto shows tool calls inline in the transcript, records them as queryable tool-call traces, and feeds the structured call — name, arguments, and result — into its AI analysis (summaries, findings, scorecards, metrics). So you send the tool call once, as structured data; you do not need to also hand-render it into the turn’s content. A toolCall is { name, arguments?, result?, status?, toolCallId?, errorMessage? } — only name is required. arguments and result are any JSON; status is success, error, or pending; toolCallId correlates an invocation with its result if you send them as separate turns. The turn’s content may be empty when a toolCall is present (Zelto labels it with the tool name), and startSeconds still orders it in the timeline.

Text and messaging conversations

The same endpoint ingests text conversations — a chatbot or messaging agent (web chat, SMS, WhatsApp, in-app). Send the messages as transcript.turns and omit the audio and telephony fields; there’s no recording to re-host. See Chatbot & text conversations for the full text-specific contract and a chat.json example.

Handle errors

Errors come back as JSON with an error string; validation and provider errors add a details object naming what to fix.
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 the X-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.