# Create an agent (always as a draft)

Source: https://docs.usetone.ai/reference/agents/create

> A new agent is always a draft, whatever you send: a draft can be dialled outbound but will NOT answer inbound calls, so publishing is the deliberate step that puts it on a phone line.

`POST /v1/agents`
A new agent is always a draft, whatever you send: a draft can be dialled outbound but will NOT answer inbound calls, so publishing is the deliberate step that puts it on a phone line. The `(ttsModel, ttsVoice)` pair is validated here — a voice belonging to a different model version is rejected now rather than failing mid-call.

Requires the `write` scope.

## Request body

| Field | Type | | Meaning |
|---|---|---|---|
| `callSettings` | object | optional | How the call RUNS rather than what it says: opening line, background ambience, what to do when the caller goes quiet, voicemail handling, the maximum call length and the farewell. A COMPLETE object; omitting the key leaves stored settings untouched. Custom lines are translated into the agent spoken languages automatically on save. |
| `callSettings.background` | object | optional | Ambient background audio for the call. |
| `callSettings.background.sound` | string | optional | An ambience bed played under the whole call, which makes a bot sound less like it is in a vacuum. Tone's own loops, not the voice vendor's. Open set — handle an unrecognised value rather than throwing. Today: `none`, `office`, `call_center`, `city_traffic`. |
| `callSettings.background.volume` | number | optional | How loud the bed sits under the speech, 0 to 1. Past about 0.4 it starts competing with the agent on a phone earpiece. |
| `callSettings.hangupLine` | object | optional | What the agent says as it ends the call on its own — after the nudge ladder runs out, or when it decides the task is done. |
| `callSettings.hangupLine.i18n` | object | optional | Read-only. Tone's translations of `text` into the agent's other languages, keyed by language code. Accepted and IGNORED on write so you can PATCH an object straight back from a GET; translations are always recomputed server-side, so a client can never inject one. |
| `callSettings.hangupLine.text` | string | null | optional | The line, in the agent's first language — Tone translates it into the others. Send `null` or an empty string to restore Tone's built-in line for this slot: that is the only way to spell "go back to the default", and it is why clearing the field does not make the agent say nothing. |
| `callSettings.maxCallSeconds` | integer | null | optional | A hard ceiling on call length, in seconds, after which Tone hangs up. `null` means no agent-level cap. This can only ever LOWER the platform ceiling, never raise it. Worth setting on any agent that bills per minute. |
| `callSettings.nudges` | object | optional | What the agent does when the caller goes quiet. |
| `callSettings.nudges.enabled` | boolean | optional | Whether the agent prompts a silent caller at all. Turning this off leaves the call in silence until the caller speaks or the cap is reached. |
| `callSettings.nudges.hangupAfter` | boolean | optional | Whether to end the call once the last rung goes unanswered. Requires `enabled` — hanging up after nothing would mean never, which is not what the name promises, so the pairing is enforced. |
| `callSettings.nudges.steps` | object[] | optional | The ladder, in order, up to 5 rungs. Each rung's wait is measured from the previous one. At least one rung is required while `enabled` is true, because nudges on with no rungs is a switch that says the feature is on and never fires. |
| `callSettings.nudges.steps[].afterSeconds` | integer | required | 🔴 Seconds of CONTINUED silence since the previous rung — a gap, not an offset from the start of the call. Two rungs of 15 fire at 15s and 30s of quiet. Minimum 3s, so a ladder cannot machine-gun the caller. |
| `callSettings.nudges.steps[].line` | object | optional | What the agent says on this rung. Leave `text` null to use Tone's built-in localized nudge. |
| `callSettings.nudges.steps[].line.i18n` | object | optional | Read-only. Tone's translations of `text` into the agent's other languages, keyed by language code. Accepted and IGNORED on write so you can PATCH an object straight back from a GET; translations are always recomputed server-side, so a client can never inject one. |
| `callSettings.nudges.steps[].line.text` | string | null | optional | The line, in the agent's first language — Tone translates it into the others. Send `null` or an empty string to restore Tone's built-in line for this slot: that is the only way to spell "go back to the default", and it is why clearing the field does not make the agent say nothing. |
| `callSettings.openingLine` | object | optional | What the agent says first, after Tone's fixed AI-and-recording disclosure. Unlike the other lines here it is substituted with the call's variables, so `{{customer_name}}` works. Leave `text` null and an outbound agent falls back to "How can I help you today?", which is the wrong opening for a call the customer did not place — set it. |
| `callSettings.openingLine.i18n` | object | optional | Read-only. Tone's translations of `text` into the agent's other languages, keyed by language code. Accepted and IGNORED on write so you can PATCH an object straight back from a GET; translations are always recomputed server-side, so a client can never inject one. |
| `callSettings.openingLine.text` | string | null | optional | The line, in the agent's first language — Tone translates it into the others. Send `null` or an empty string to restore Tone's built-in line for this slot: that is the only way to spell "go back to the default", and it is why clearing the field does not make the agent say nothing. |
| `callSettings.voicemail` | object | optional | What happens when an answering machine picks up. |
| `callSettings.voicemail.enabled` | boolean | optional | Whether to leave `message` when an answering machine picks up. 🔴 Detection itself runs either way — an undetected mailbox is billed as an answered call and counted as a successful contact, so turning this off means "hang up without speaking", never "do not check". |
| `callSettings.voicemail.message` | object | optional | What to leave on the machine. Left only when `enabled` is true; the call ends either way. |
| `callSettings.voicemail.message.i18n` | object | optional | Read-only. Tone's translations of `text` into the agent's other languages, keyed by language code. Accepted and IGNORED on write so you can PATCH an object straight back from a GET; translations are always recomputed server-side, so a client can never inject one. |
| `callSettings.voicemail.message.text` | string | null | optional | The line, in the agent's first language — Tone translates it into the others. Send `null` or an empty string to restore Tone's built-in line for this slot: that is the only way to spell "go back to the default", and it is why clearing the field does not make the agent say nothing. |
| `callSettings.voicemail.windowSeconds` | integer | optional | How long after the call connects detection stays armed. Past this window a long monologue is a talkative caller, not a machine, so the check stands down. |
| `evaluationCriteria` | object[] | optional | Questions a post-call judge answers about each call, returned as `evaluations` with a verdict and a rationale per criterion. A COMPLETE list, replacing what is stored. |
| `evaluationCriteria[].id` | string | required | 🔴 The storage key for this criterion's verdicts, and IMMUTABLE once calls have been graded against it. Renaming it orphans every historical verdict — rename `name` instead, which exists for exactly that. |
| `evaluationCriteria[].name` | string | required | The human label for this criterion, shown wherever verdicts are reported. Safe to rename at any time. |
| `evaluationCriteria[].prompt` | string | required | The question the post-call judge answers about the transcript, handed to it verbatim. Write it so that "yes" is unambiguous. |
| `inputVariables` | object[] | optional | Placeholders the prompt can reference and a call can fill, each with a default used when a call supplies nothing. A COMPLETE list, replacing what is stored; an empty array clears it. |
| `inputVariables[].default` | string | optional | What to substitute when a call supplies no value. 🔴 Give every variable a literal fallback such as "unknown" — an empty default renders the prompt with a blank where a fact should be, and a model handed a blank invents one. |
| `inputVariables[].description` | string | optional | What this variable holds. For your own reference and the dashboard editor — it is not sent to the model. |
| `inputVariables[].name` | string | required | The placeholder name, lowercase snake_case. Reference it in the system prompt or a spoken line as `{{name}}`, and supply its value per call. |
| `knowledgeBaseIds` | string[] | optional | Which knowledge bases this agent may answer from — the COMPLETE desired set, not a delta. Omitting the key leaves attachments untouched; sending an empty array detaches every base. A published version pins the SET OF IDS, not their contents, so re-indexing a base changes what past versions answer with. |
| `llmModel` | string | optional | Which language model the agent thinks on. The legal values come from GET /v1/catalog/voice and change without a deploy — treat this as an open set. |
| `name` | string | required | Your name for the agent. Shown in the dashboard and on every call it runs. |
| `outputVariables` | object[] | optional | Fields to extract from the transcript after each call, returned as `outputs` on the call record. Each field description IS the extraction instruction, so write it as you would tell a person what to look for. A COMPLETE list, replacing what is stored. |
| `outputVariables[].description` | string | required | The extraction instruction, handed verbatim to the model that reads the transcript after the call. Required, because a bare field name extracts garbage — say what counts as an answer and what to do when the call never covered it. |
| `outputVariables[].enumValues` | string[] | optional | The allowed answers, 2-20 of them. Required when `type` is `enum` and rejected otherwise — the pairing is enforced, because values on a non-enum type would silently do nothing. |
| `outputVariables[].name` | string | required | The key this value is stored under on the finished call, lowercase snake_case. Read it back from `outputs` on `GET /v1/calls/{id}`. |
| `outputVariables[].type` | string | required | What kind of value to extract. `enum` clamps the answer to `enumValues`, which is what makes an outcome countable rather than free text. Open set — handle an unrecognised value rather than throwing. Today: `string`, `number`, `boolean`, `enum`. |
| `purpose` | string | optional | 🔴 The regulatory sender classification for every call this agent makes. Not cosmetic: it decides which calling window applies and whether consent is required. `promotional` and `collections` have hard time windows; `service` and `transactional` have none. Defaults to the strictest, `promotional`, and there is deliberately no per-call override. Open set — handle an unrecognised value rather than throwing. Today: `promotional`, `service`, `transactional`, `collections`. |
| `systemPrompt` | string | required | What the agent is trying to do, in your own words. Reference an input variable with double braces, for example {{{{customer_name}}}}. Keep it task-shaped: the platform already supplies conversational behaviour such as interruption handling and language matching. |
| `tools` | object[] | optional | HTTP tools the agent may call mid-conversation, up to 10. Credentials are referenced from your stored secrets, never written inline. A COMPLETE list, replacing what is stored. |
| `tools[].connectionId` | string | optional | Read-only provenance. Which integration connection installed this tool — used to answer "which agents use this connection" exactly, where matching on `preset` cannot (two connections to the same vendor are indistinguishable). Never reaches the running call. |
| `tools[].description` | string | required | 🔴 WHEN the agent should call this tool — the single most important field on it. A vague line is the biggest cause of a tool misfiring, so at least 20 characters are required. Describe the trigger condition, not the implementation. |
| `tools[].http` | object | required | The request Tone makes when the agent calls this tool. |
| `tools[].http.auth` | object | optional | How Tone authenticates to your endpoint. Every credential is referenced by stored-secret id rather than typed in here, which is what keeps it out of every read of the agent. |
| `tools[].http.auth.header` | string | optional | Which header carries an `api_key` credential. Defaults to `x-api-key`. Applies only to `api_key`, and only when the credential goes in a header. |
| `tools[].http.auth.in` | string | optional | Where the credential goes. Defaults to `header`. 🔴 A `body` credential on a GET or DELETE is refused — those send no body, so it would be dropped and the endpoint would answer 401, which reads exactly like a revoked key. |
| `tools[].http.auth.name` | string | optional | The query key or dotted body path the credential lands in. Required when `in` is not `header`. |
| `tools[].http.auth.secondary` | object | optional | A second credential, for APIs that authenticate with a PAIR such as an access key plus a secret key. It carries no `type` because a pair is never two schemes — it is only ever "put this value in this place". |
| `tools[].http.auth.secondary.in` | string | optional | Where the second credential goes. Defaults to `header`, and every placement rule the primary has applies here too. |
| `tools[].http.auth.secondary.name` | string | required | The header name, query key or dotted body path for the second credential. Must not collide with the primary's slot — one would silently overwrite the other. |
| `tools[].http.auth.secondary.secretId` | string | required | The second stored secret. Must differ from the primary — the same id twice sends one value under two names, and the rejection that follows says nothing about which half was wrong. |
| `tools[].http.auth.secretId` | string | required | The stored secret holding the credential value. 🔴 A REFERENCE, never the credential itself — the value is decrypted only when a call runs, so it never rides a dashboard read of the agent. Create one in the dashboard under Secrets. |
| `tools[].http.auth.type` | string | required | The scheme. `bearer` and `basic` are header schemes by definition and cannot be placed elsewhere; only `api_key` may move. |
| `tools[].http.bodyFormat` | string | optional | How the body is encoded. `json` by default; `form` sends `application/x-www-form-urlencoded`, which is FLAT — a dotted parameter path cannot be encoded into it and is refused. |
| `tools[].http.bodyRoot` | string | optional | Whether the request body is a JSON object (default) or an array. 🔴 With `array`, every parameter path must start with an index such as `0.email` — a named key set on an array is dropped silently when the body is serialised, and your API then reports a missing required field. |
| `tools[].http.headers` | object | optional | Static headers sent with every call, up to 16. 🔴 Credentials do not go here — `authorization` is refused outright, as are platform-owned headers and anything `x-tone-*`. Use `auth`, which keeps the secret out of every agent read. |
| `tools[].http.method` | string | required | The HTTP method. GET and DELETE send no body, so parameters default to the query string. |
| `tools[].http.timeoutMs` | integer | optional | How long to wait for your endpoint before giving up, in milliseconds (1000-30000). Remember the caller is waiting in silence — keep it short enough that the agent can say something before it becomes an awkward pause. |
| `tools[].http.url` | string | required | The endpoint Tone calls. Checked at save time — a URL pointing at private address space, loopback or a cloud metadata endpoint is refused here rather than discovered mid-call, and the check re-runs against the resolved address at execution. Must be `https` if the tool sends credentials. |
| `tools[].mock` | object | optional | Return a fixed response instead of calling the endpoint, so the conversation can be tested before the integration is live. |
| `tools[].mock.enabled` | boolean | required | While true, Tone returns `response` instead of calling your endpoint. Use it to rehearse the conversation before the API exists. |
| `tools[].mock.response` | string | required | The canned reply handed to the model in place of a real response. Shape it like the real thing — the model reads it the same way. |
| `tools[].name` | string | required | What the model calls this tool, lowercase snake_case. Must be unique on the agent and cannot shadow a built-in name such as `end_call` or `query_knowledge`. |
| `tools[].parameters` | object[] | optional | What the agent collects before calling, up to 16. Names must be unique within the tool. |
| `tools[].parameters[].description` | string | optional | What this parameter holds, in the model's terms. Required unless the parameter is pinned with `value` — a parameter the model fills with no description collects garbage. |
| `tools[].parameters[].enum` | string[] | optional | Clamp the model's answer to this list, 2-24 values. String parameters only, and mutually exclusive with `value`. |
| `tools[].parameters[].in` | string | optional | Where the value goes on the wire. Defaults to the method's natural place — query for GET and DELETE, body for the rest. Use `path` to substitute a `{placeholder}` in the URL. |
| `tools[].parameters[].name` | string | required | The parameter name the model fills in, lowercase snake_case. Also the wire name unless `path` overrides it. |
| `tools[].parameters[].path` | string | optional | The wire name, when it differs from `name`: a query key, a path placeholder, or a dotted location inside the body such as `attendee.email`. Dots nest only inside a JSON body — on a query key they would be sent literally, so that combination is rejected. |
| `tools[].parameters[].required` | boolean | optional | Whether the model must supply this before the tool can run. |
| `tools[].parameters[].type` | string | required | What kind of value the model must produce. |
| `tools[].parameters[].value` | string | optional | Pin the parameter to a fixed value, injected at call time and INVISIBLE to the model. This is how a tenant id or an account key reaches your API without the model being able to see or change it. |
| `tools[].preset` | string | optional | Read-only provenance. Names the integration preset that installed this tool, if any. Round-trips unchanged so renaming a preset-installed tool does not strip it. |
| `tools[].speech` | object | optional | What the agent says out loud while it waits for your endpoint, keyed by language code (`hi-IN`, `en-IN`). Without it the caller hears silence for the length of `timeoutMs`, which on a phone call reads as a dropped line. Up to 16 languages. |
| `voice` | object | optional | The speech stack: which model transcribes, which speaks, in which voice, and which languages the agent handles. 🔴 The (ttsModel, ttsVoice) pair is a constraint, not two independent choices — a voice belongs to one model version and the wrong pairing is rejected at write time rather than becoming a failure mid-call. Read the valid combinations from GET /v1/catalog/voice. |
| `voice.languages` | string[] | optional | Every language this agent is allowed to speak, most-preferred first. Also the clamp on auto-detection: the transcriber understands about twice as many languages as the speaking model can speak, so a detection outside this list falls back to the first entry instead of producing a reply the voice cannot say. An empty list means "no opinion" — adopt whatever is detected. |
| `voice.sttLanguage` | string | optional | The language to expect from the caller, or `auto` to detect it per utterance. Auto-detection is clamped to `languages`, so a detection outside that list falls back to the first entry rather than opening a socket the speaking model would reject. |
| `voice.sttMode` | string | optional | How the transcriber treats the caller's language. `transcribe` writes what was said in the language it was said in; `translate` renders it into English. Values come from the catalog. |
| `voice.sttModel` | string | optional | Which model transcribes the caller. Read the legal values from `GET /v1/catalog/voice` — they change without a deploy. |
| `voice.ttsModel` | string | optional | Which model speaks. 🔴 Half of a constraint: a voice belongs to one model version, so changing this can invalidate the `ttsVoice` already on the agent — send both together when you switch. |
| `voice.ttsOptions` | object | optional | Model-specific voice tuning, as a map of option name to number. Which options exist depends on the speaking model — v3 dropped `pitch` and `loudness` and added `temperature`. `GET /v1/catalog/voice` lists each model's options with their ranges; an option the chosen model does not take is rejected. |
| `voice.ttsVoice` | string | optional | Which voice speaks. 🔴 Valid only for its own `ttsModel` — `simran` is a v3 voice and is rejected on v2, `anushka` is the reverse. The pair is checked at write time so it cannot fail mid-call. |

### Example request

```json
{
  "callSettings": {
    "background": {
      "sound": "none",
      "volume": 0.3
    },
    "hangupLine": {
      "text": "Thanks for your time. Goodbye."
    },
    "maxCallSeconds": 600,
    "nudges": {
      "enabled": true,
      "hangupAfter": true,
      "steps": [
        {
          "afterSeconds": 15,
          "line": {
            "text": null
          }
        }
      ]
    },
    "openingLine": {
      "text": "Hi {{customer_name}}, I’m calling from Acme about your order."
    },
    "voicemail": {
      "enabled": true,
      "windowSeconds": 30
    }
  },
  "evaluationCriteria": [
    {
      "id": "confirmed_order",
      "name": "Confirmed the order",
      "prompt": "Did the customer explicitly confirm they still want the order?"
    }
  ],
  "inputVariables": [
    {
      "default": "there",
      "description": "The customer’s first name, as it appears on the order.",
      "name": "customer_name"
    }
  ],
  "knowledgeBaseIds": [
    "9f1c2d84-4e3a-4f6c-b902-7d1e8a33c451"
  ],
  "llmModel": "gemini-2.5-flash",
  "name": "Order confirmation",
  "outputVariables": [
    {
      "description": "Which day the customer agreed to take delivery. Leave empty if they did not commit to one.",
      "enumValues": [
        "monday",
        "tuesday",
        "wednesday"
      ],
      "name": "delivery_day",
      "type": "enum"
    }
  ],
  "purpose": "transactional",
  "systemPrompt": "You call customers to confirm a cash-on-delivery order. Confirm the order, ask which day suits them for delivery, then thank them and end the call. Be brief and polite.",
  "tools": [
    {
      "connectionId": "b4e6d8a1-9c3f-4b2e-8d7a-1f0e9c8b7a6d",
      "description": "Call this once the customer has agreed to a specific delivery date and time window.",
      "http": {
        "auth": {
          "header": "x-api-key",
          "in": "header",
          "name": "api_key",
          "secondary": {
            "in": "header",
            "name": "x-acme-secret",
            "secretId": "7c2b8e5d-1a3f-4e6b-8c9d-0e1f2a3b4c5d"
          },
          "secretId": "3f1a7f6e-2b4c-4d1e-9a8b-5c6d7e8f9a0b",
          "type": "bearer"
        },
        "bodyFormat": "json",
        "bodyRoot": "object",
        "headers": {
          "x-acme-source": "tone"
        },
        "method": "POST",
        "timeoutMs": 5000,
        "url": "https://api.acme.example/v1/deliveries"
      },
      "mock": {
        "enabled": true,
        "response": "{\"status\":\"booked\",\"slot\":\"2026-09-02T10:00:00+05:30\"}"
      },
      "name": "book_delivery_slot",
      "parameters": [
        {
          "description": "The agreed date as YYYY-MM-DD.",
          "name": "delivery_date",
          "required": true,
          "type": "string"
        }
      ],
      "preset": "cal.com/book",
      "speech": {
        "en-IN": "Let me check that for you.",
        "hi-IN": "मैं अभी देखता हूँ।"
      }
    }
  ],
  "voice": {
    "languages": [
      "hi-IN",
      "en-IN"
    ],
    "sttLanguage": "auto",
    "sttMode": "transcribe",
    "sttModel": "saaras:v3",
    "ttsModel": "bulbul:v3",
    "ttsOptions": {
      "pace": 1
    },
    "ttsVoice": "simran"
  }
}
```

## Response

| Field | Type | | Meaning |
|---|---|---|---|
| `callSettings` | object | required | How a call RUNS — ambience, quiet-caller nudges, voicemail handling, length cap, farewell. |
| `createdAt` | string | required | When the agent was created. RFC 3339, UTC. |
| `evaluationCriteria` | object | required | What the post-call judge grades each call against; produces `calls.evaluations`. |
| `id` | string | required | The agent's unique id. |
| `inputVariables` | object | required | Declared `{{placeholders}}` the prompt can use, with their defaults. Values are supplied per call via `variables`. |
| `knowledgeBaseIds` | string[] | required | Knowledge bases this agent may answer from. |
| `languages` | string[] | required | What the agent SPEAKS, primary first. A detected language outside this list is clamped to it. |
| `llmProvider` | string | required | Who supplies the language model. Open set — read the current options from GET /v1/catalog/voice. |
| `name` | string | required | Your name for the agent. |
| `outputVariables` | object | required | The post-call extraction schema — what shapes `calls.outputs`. |
| `purpose` | string | required | The regulatory classification of calls this agent places. Drives the calling-window rules the compliance gate enforces — it is not cosmetic, and there is deliberately no per-call override. Open set — handle an unrecognised value rather than throwing. Today: `promotional`, `service`, `transactional`, `collections`. |
| `status` | string | required | A `draft` agent can still be dialled outbound; an INBOUND call is refused until it is `live`, because a half-written prompt should not answer a real customer. Open set — handle an unrecognised value rather than throwing. Today: `draft`, `live`. |
| `sttLanguage` | string | null | required | Pinned recognition language, or null to auto-detect. |
| `sttMode` | string | required | How the transcriber is asked to handle language — transcribing faithfully versus translating. Open set. |
| `sttModel` | string | required | Speech-to-text model id. |
| `sttProvider` | string | required | Who supplies speech-to-text. Open set. |
| `systemPrompt` | string | required | The agent’s instructions, verbatim. |
| `tools` | object | required | Custom API tools the agent may call mid-conversation. Credentials appear only as `secretId` references, never inline. |
| `ttsModel` | string | required | Text-to-speech model id, e.g. `bulbul:v3`. |
| `ttsOptions` | object | required | Voice tuning (pace, and model-specific fields). The legal keys and ranges differ per TTS model and are documented on the agent write schemas. |
| `ttsProvider` | string | required | Who supplies text-to-speech. Open set. |
| `ttsVoice` | string | required | Speaker id. NOT portable across model versions — a voice valid on `bulbul:v3` is a 400 on `bulbul:v2`. `GET /v1/catalog/voice` lists the legal pairings. |
| `updatedAt` | string | required | When the agent was last changed. RFC 3339, UTC. |

### Example response

```json
{
  "data": {
    "callSettings": {
      "background": {
        "sound": "none",
        "volume": 0.3
      },
      "hangupLine": {
        "text": "Thanks for your time. Goodbye."
      },
      "maxCallSeconds": 600,
      "nudges": {
        "enabled": true,
        "hangupAfter": true,
        "steps": [
          {
            "afterSeconds": 15,
            "line": {
              "text": null
            }
          }
        ]
      },
      "openingLine": {
        "text": "Hi {{customer_name}}, I’m calling from Acme about your order."
      },
      "voicemail": {
        "enabled": true,
        "windowSeconds": 30
      }
    },
    "createdAt": "2026-08-25T09:14:22Z",
    "evaluationCriteria": [
      {
        "id": "confirmed_order",
        "name": "Confirmed the order",
        "prompt": "Did the customer explicitly confirm they still want the order?"
      }
    ],
    "id": "9f1c2d84-4e3a-4f6c-b902-7d1e8a33c451",
    "inputVariables": [
      {
        "default": "there",
        "description": "The customer’s first name, as it appears on the order.",
        "name": "customer_name"
      }
    ],
    "knowledgeBaseIds": [
      "9f1c2d84-4e3a-4f6c-b902-7d1e8a33c451"
    ],
    "languages": [
      "hi-IN",
      "en-IN"
    ],
    "llmProvider": "google",
    "name": "Order confirmation",
    "outputVariables": [
      {
        "description": "Which day the customer agreed to take delivery. Leave empty if they did not commit to one.",
        "enumValues": [
          "monday",
          "tuesday",
          "wednesday"
        ],
        "name": "delivery_day",
        "type": "enum"
      }
    ],
    "purpose": "promotional",
    "status": "draft",
    "sttLanguage": "auto",
    "sttMode": "transcribe",
    "sttModel": "saaras:v3",
    "sttProvider": "sarvam",
    "systemPrompt": "You call customers to confirm a cash-on-delivery order…",
    "tools": [
      {
        "connectionId": "b4e6d8a1-9c3f-4b2e-8d7a-1f0e9c8b7a6d",
        "description": "Call this once the customer has agreed to a specific delivery date and time window.",
        "http": {
          "auth": {
            "header": "x-api-key",
            "in": "header",
            "name": "api_key",
            "secondary": {
              "in": "header",
              "name": "x-acme-secret",
              "secretId": "7c2b8e5d-1a3f-4e6b-8c9d-0e1f2a3b4c5d"
            },
            "secretId": "3f1a7f6e-2b4c-4d1e-9a8b-5c6d7e8f9a0b",
            "type": "bearer"
          },
          "bodyFormat": "json",
          "bodyRoot": "object",
          "headers": {
            "x-acme-source": "tone"
          },
          "method": "POST",
          "timeoutMs": 5000,
          "url": "https://api.acme.example/v1/deliveries"
        },
        "mock": {
          "enabled": true,
          "response": "{\"status\":\"booked\",\"slot\":\"2026-09-02T10:00:00+05:30\"}"
        },
        "name": "book_delivery_slot",
        "parameters": [
          {
            "description": "The agreed date as YYYY-MM-DD.",
            "name": "delivery_date",
            "required": true,
            "type": "string"
          }
        ],
        "preset": "cal.com/book",
        "speech": {
          "en-IN": "Let me check that for you.",
          "hi-IN": "मैं अभी देखता हूँ।"
        }
      }
    ],
    "ttsModel": "bulbul:v3",
    "ttsOptions": {
      "pace": 1
    },
    "ttsProvider": "sarvam",
    "ttsVoice": "simran",
    "updatedAt": "2026-08-25T09:14:22Z"
  }
}
```
