Voicetta
    Voicetta.

    Voicetta API

    Outbound Calls API

    Start a phone call from your own product with POST /v1/calls. Covers the outbound_calls scope, idempotency, call status, errors, rate limits, and following a call to its transcript.

    Outbound Calls API

    POST /v1/calls tells a Voicetta agent to ring a phone number.

    Use it to start a call from your own product: after a form submission, when a lead lands in your CRM, on a schedule, or as a step in a workflow.

    This is the only endpoint that writes. Everything else in the API reads.

    Before your first call

    You need three things.

    1. An API key with permission to place calls. Go to Settings → Developers, create a key, and tick Place outbound calls. Keys that only read cannot dial.
    2. An agent ID. Copy it from Agent in the dashboard, or call GET /v1/assistants.
    3. A phone number enabled for outbound calls. Set this up on the agent's number in the dashboard.

    Existing API keys stay read-only. Being able to read a transcript and being able to ring a phone are separate decisions, so granting the first never grants the second.

    Place a call

    bash
    curl -X POST "https://api.voicetta.com/v1/calls" \ -H "Authorization: Bearer vk_live_your_key_here" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: lead-8842" \ -d '{ "assistant_id": "c4f2a8e1-9b7d-4e35-a612-7f8c3d5b9e04", "to": "+14155559999", "customer_name": "Sarah Mitchell", "call_variables": { "hotel_name": "The Pine Ridge Inn", "check_in_date": "October 14" }, "metadata": { "crm_id": "cust_123", "campaign": "autumn-reactivation" } }'

    You get 201 Created with a call_id:

    json
    { "call": { "call_id": "9f8b1c4e-6d2a-4b7f-8e21-3c5a7d9e0f11", "workspace_id": "1a2b3c4d-5e6f-4071-8293-a4b5c6d7e8f9", "assistant_id": "c4f2a8e1-9b7d-4e35-a612-7f8c3d5b9e04", "assistant_name": "Front desk", "direction": "outbound", "status": "queued", "duration_seconds": 0, "started_at": "2026-09-18T09:14:02Z", "customer": { "name": "Sarah Mitchell", "phone": "+14155559999" }, "call_variables": { "hotel_name": "The Pine Ridge Inn", "check_in_date": "October 14" }, "metadata": { "crm_id": "cust_123", "campaign": "autumn-reactivation" } } }

    The response comes back when the call is accepted, not when it connects. Dialling, ringing and talking all happen afterwards.

    Request fields

    FieldTypeRequiredMeaning
    assistant_idUUIDYesThe agent that will handle the call
    tostringYesNumber to call, E.164, e.g. +14155559999
    fromstringNoNumber to call from, E.164
    customer_namestringNoWho is being called. Available to the prompt as {{participant_name}}
    call_variablesobjectNoValues for this one call, substituted into the prompt
    metadataobjectNoYour own JSON, stored and returned unchanged

    from is optional when the agent has exactly one number enabled for outbound calls, because then there is nothing to choose. With two or more you have to say which, since guessing would place the call from whichever number happened to sort first.

    metadata is never interpreted by Voicetta. Put your CRM id, your campaign name, or whatever you need to match the call back to your own records. It comes back on the call object and on every webhook for that call. At most 8 KB.

    call_variables fills the {{placeholders}} in the agent's prompt. That has its own page: Dynamic Variables.

    Following the call

    One call_id identifies the call for its whole life — while it is queued, while it is talking, and afterwards as the conversation that holds the transcript.

    Poll it:

    bash
    curl -H "Authorization: Bearer vk_live_your_key_here" \ "https://api.voicetta.com/v1/calls/9f8b1c4e-6d2a-4b7f-8e21-3c5a7d9e0f11"

    You get the same object shape throughout, so one handler works from queued to ended. The blocks that cannot exist yet are simply absent: there is no transcript until there has been a conversation.

    Statuses

    StatusMeaning
    queuedAccepted, waiting for an agent to pick it up
    in_progressDialling, ringing or talking
    endedThe call happened and a transcript has been saved
    errorThe call did not happen and never will. See end_reason

    queued and in_progress always resolve. A call that stops making progress is moved to error rather than left waiting forever, so your client never polls indefinitely.

    Or use webhooks instead

    Polling works, but webhooks are less work.

    EventFires
    call.startedThe call begins dialling
    call.completedThe call ended and post-call processing finished

    Both carry the same call_id, so you can correlate them without storing anything extra. Enable them in Settings → Developers.

    call.started carries who is being called, but no transcript, summary, outcome or recording — those are all products of the call having ended, and arrive with call.completed. See the Webhook Reference.

    Retries and duplicate calls

    A call spends money and rings a real person, so a retry after a timeout must not dial twice.

    Send an Idempotency-Key header with a value unique to the call you intend to place:

    text
    Idempotency-Key: lead-8842

    If we have already seen that key in your workspace, you get 201 with the original call and an Idempotent-Replay: true header. No second call is placed.

    The same status and body either way, so a client that ignores the header still behaves correctly, and one that reads it can tell the retry was absorbed.

    Pick something stable and meaningful: an order number, a CRM record id, a job id. Not a fresh UUID per attempt — that defeats the point. Keys are stored hashed, so a key containing customer data does not linger in our tables.

    Errors

    The status code tells you whether to fix the request, wait, or give up.

    StatusMeaningWhat to do
    400Bad destination, missing from, outbound not enabled, or invalid call_variablesFix and resend
    401Missing or invalid API keyCheck the key
    403Key cannot place calls, or that country is not permittedTick the scope, or check restrictions
    404No such agent or number in this workspaceCheck the id
    409Wallet empty, or the agent is at its concurrency limitTop up, or retry later
    429Rate limit exceededBack off and retry
    502The telephony provider refused the callRetry; it is not your request

    A 409 and a 429 are worth retrying. A 400 is not — the same request will fail the same way.

    Validation errors name the field:

    json
    { "detail": [ "call_variables key 'guest-name' must start with a letter and contain only letters, digits and underscores" ] }

    Every problem is reported at once, so you do not need one request per mistake.

    Rate limits

    LimitScope
    200 calls per hourPer workspace
    10 requests per secondBurst protection

    Counted per workspace rather than per caller, because an API key has no user behind it and a serverless client arrives from a new IP every time.

    Country restrictions

    Calls are allowed within the country of the originating number. A 403 with country_not_allowed means the destination is outside it. See Calling Restrictions.

    Test calls

    Calls placed through the API are real and billed like any other call. There is no dry-run mode — use one of your own numbers as to.

    List your agents

    GET /v1/assistants returns everything you need to place a call, so a new integration only has to make one request first.

    bash
    curl -H "Authorization: Bearer vk_live_your_key_here" \ "https://api.voicetta.com/v1/assistants"
    json
    { "assistants": [ { "assistant_id": "c4f2a8e1-9b7d-4e35-a612-7f8c3d5b9e04", "assistant_name": "Front desk", "workspace_id": "1a2b3c4d-5e6f-4071-8293-a4b5c6d7e8f9", "phone_numbers": [ { "phone_number": "+14155551234", "country": "US", "outbound_enabled": true } ], "call_variables": ["hotel_name", "check_in_date"] } ] }

    call_variables here is read from the agent's actual prompt, so it cannot drift from what the agent really says. Variables Voicetta fills in itself are not listed, because passing one would be rejected.

    Reading agents needs only a normal API key. The write scope is for POST /v1/calls.