> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zelto.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Chatbot & text conversations

> POST chatbot, SMS, WhatsApp, and web-chat conversations into Zelto over the same ingest endpoint as voice calls — no audio required.

Zelto ingests text conversations — a chatbot, messaging agent, or any
text-based assistant (web chat, SMS, WhatsApp, in-app) — over the **same**
endpoint as voice calls: `POST https://ingest.zelto.ai/webhooks/calls`. A text
conversation is just a call with a transcript and no audio. Once it lands, it
runs through the full pipeline — transcription is skipped (there's already a
transcript), and everything downstream (summaries, [findings](/docs/findings),
reviews, scorecards) works exactly as it does for voice.

The mechanics — minting a key, idempotency, error handling — are shared with
[Custom & other providers](/docs/integrations/api-call-upload). This page covers
what's specific to text.

## Mint a key

Text upload uses the same API key as every other custom upload. Follow
[Custom & other providers → Mint a key](/docs/integrations/api-call-upload#mint-a-key)
to create one under **Settings → Integrations → API Call Upload**, then expose
it as `ZELTO_API_KEY`.

## Upload a chat conversation

Send the messages as `transcript.turns` — `role: "user"` for the person,
`role: "assistant"` for your agent — and leave the audio and telephony fields
out. Only `call.externalId` plus an agent reference are required.

```bash theme={null}
curl -X POST https://ingest.zelto.ai/webhooks/calls \
  -H "Authorization: Bearer $ZELTO_API_KEY" \
  -H "Content-Type: application/json" \
  -H "X-Zelto-Provider: zelto" \
  -d @chat.json
```

```json title="chat.json" theme={null}
{
  "agent": { "externalId": "support-chatbot", "name": "Support Chatbot" },
  "call": {
    "externalId": "chat_01HXYZ",
    "startedAt": "2026-05-29T15:00:00Z",
    "endedAt": "2026-05-29T15:04:12Z"
  },
  "transcript": {
    "turns": [
      { "role": "user", "content": "Hi, I need to reschedule my appointment." },
      { "role": "assistant", "content": "Happy to help — what day works for you?" }
    ]
  },
  "metadata": { "channel": "web-chat" }
}
```

A successful upload returns HTTP `200` with `{ "received": true }`.

**Send:**

* `agent.externalId` (Zelto finds or creates the agent) or `agentId` (an
  existing agent's UUID). Use a stable id per agent, not per conversation.
* `call.externalId` — a stable, unique id for the conversation (the dedup key).
* `transcript.turns[]` — the messages, in order, as `user` / `assistant` turns.
* `call.startedAt` / `call.endedAt` — the conversation's start and end
  timestamps (ISO 8601). Optional, but they let you sort and window by time.
* `metadata.channel` — record the channel (`web-chat`, `sms`, `whatsapp`,
  `in-app`) so you can filter on it later. There's no dedicated channel field.

**Omit:**

* `recordingUrl` and `recordingUploadId` — there's no audio to re-host.
* Per-turn `startSeconds` / `endSeconds` and `words[]` — those are for
  word-synchronized audio playback.
* `durationSeconds` and `customer.number` — voice/telephony fields with no text
  equivalent.

<Note>
  A delivery with **neither a transcript nor a recording** is treated as an
  empty dial: Zelto acks it with `200` but creates no conversation, since
  there's nothing to analyze. A text conversation must carry at least one
  `transcript.turns` entry.
</Note>

### Tool calls

If your chatbot invokes tools mid-conversation (check availability, look up an
order), forward each as a `tool` turn carrying a structured `toolCall` — the
same shape voice agents use. Zelto shows it inline in the transcript and feeds
it into analysis. See
[Tool calls](/docs/integrations/api-call-upload#tool-calls) for the field
reference and an example.

### Idempotent re-uploads

`call.externalId` is the dedup key. Re-POSTing the same id **updates** the
conversation in place and never double-charges, so retries and later enrichment
re-sends are both safe. See
[Idempotent re-uploads](/docs/integrations/api-call-upload#idempotent-re-uploads).

## Conversations display as "calls"

Text conversations ingest and analyze fully, but Zelto's model and UI are
voice-first: an ingested conversation is labeled a "call" throughout, and its
name defaults to `Call <externalId>` unless you give the agent a friendlier
`name`. There's no first-class channel or modality field yet — the channel you
put in `metadata` is stored and queryable, but the interface won't badge the
conversation as chat. Everything analytical still works; only the labeling is
voice-flavored.

## Related

* [Custom & other providers](/docs/integrations/api-call-upload) — the shared
  upload contract: keys, errors, and native Vapi/Retell payloads.
* [Conversations](/docs/conversations) — where uploaded conversations land.
* [API reference](/api-reference/agents/list-agents) — REST conventions and the
  full request shape, with a playground to test a payload.
* [MCP](/docs/mcp) — the same API key gives connected editors read/write access.
