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

# outbound calls

> dial a number and put an agent on the line.

`POST /v1/calls` dials a phone number and connects whoever answers to an
agent — the dashboard's **test on phone**, for your code. the bot is started
and the greeting synthesised while the phone rings, so the callee hears it the
moment they pick up.

## request

<ParamField body="agentId" type="string" required>
  the agent — its UUID or `ua_…` handle. it must be **deployed** and use a
  voice that can take calls (`muga` or `mulberry`). a draft answers
  `409 agent_not_deployed`: a call never runs a configuration you did not
  release.
</ParamField>

<ParamField body="toNumber" type="string" required>
  the number to dial, in E.164: `+` and 7–15 digits. spaces are tolerated.
</ParamField>

<ParamField body="fromNumber" type="string">
  the caller id, as one of your own numbers: a [SIP trunk](/sip-trunks)'s
  number, or a [rented number](/phone-numbers) that is active and connected
  to this agent.
</ParamField>

<ParamField body="fromTrunkId" type="string">
  dial from this SIP trunk, by id. wins over `fromNumber`.
</ParamField>

<ParamField body="callSettings" type="object">
  [call behaviour](/manage-agents#call-behaviour) for this one call. omit to
  use the agent's stored settings.
</ParamField>

```bash curl theme={null}
curl -X POST https://silk-api.rumik.ai/v1/calls \
  -H "Authorization: Bearer rk_live_•••••••••" \
  -H "Content-Type: application/json" \
  -d '{ "agentId": "ua_fe3277d8", "toNumber": "+14155551234" }'
```

## response

returns `202 Accepted` as soon as the carrier takes the dial.

<ResponseField name="callId" type="string">
  the call's id — what `GET /v1/calls/{callId}` and the dashboard's
  **conversations** show. store it to correlate the call with your own records.
</ResponseField>

<ResponseField name="status" type="string">
  always `calling` here. the call's real state is on `GET /v1/calls/{callId}`.
</ResponseField>

<ResponseField name="agentId" type="string">
  the agent's UUID.
</ResponseField>

<ResponseField name="toNumber" type="string">
  the number dialed, normalised.
</ResponseField>

<ResponseField name="fromNumber" type="string | null">
  the caller id the callee sees. `null` when the platform's shared number was
  used.
</ResponseField>

```json theme={null}
{
  "callId": "01a05b4d-b1ad-7be0-8d29-04b2fd7e7dca",
  "status": "calling",
  "agentId": "019f6a2e-7c1d-7b3a-9e4f-1a2b3c4d5e6f",
  "toNumber": "+14155551234",
  "fromNumber": null
}
```

## which number the call comes from

| you send                        | the callee sees                                                                      |
| ------------------------------- | ------------------------------------------------------------------------------------ |
| nothing                         | the agent's connected rented number if it has one, else the platform's shared number |
| `fromNumber` of a rented number | that number — it must be `active` and connected to this agent                        |
| `fromNumber` of a SIP trunk     | that number, dialed through your carrier                                             |
| `fromTrunkId`                   | that trunk's number                                                                  |

a rented number dials out only for the agent it is connected to. connect it
first with [`POST /v1/phone-numbers/{id}/agent`](/phone-numbers#connect-it-to-an-agent);
otherwise you get `409 number_not_connected`. the dashboard does that step for
you; the api keeps it explicit so an inbound route is never moved as a side
effect of placing a call.

## following the call

poll `GET /v1/calls/{callId}` — or list them all with `GET /v1/calls`.

```bash curl theme={null}
curl https://silk-api.rumik.ai/v1/calls/01a05b4d-b1ad-7be0-8d29-04b2fd7e7dca \
  -H "Authorization: Bearer rk_live_•••••••••"
```

```json theme={null}
{
  "id": "01a05b4d-b1ad-7be0-8d29-04b2fd7e7dca",
  "agentSlug": "ua_fe3277d8",
  "status": "completed",
  "channel": "phone",
  "systemInstruction": "…",
  "greeting": "…",
  "transcript": [
    { "role": "assistant", "content": "Hi, this is acme support. How can I help?", "offsetSeconds": 0.4 },
    { "role": "user", "content": "I need to change my delivery address.", "offsetSeconds": 3.1 }
  ],
  "recordingUrl": "https://storage.googleapis.com/…?X-Goog-Signature=…",
  "startedAt": "2026-09-11T10:20:03Z",
  "endedAt": "2026-09-11T10:21:40Z",
  "durationSeconds": 97,
  "durationMs": 97210,
  "endReason": "customer_ended",
  "errorMessage": null,
  "summary": null,
  "createdAt": "2026-09-11T10:20:00Z"
}
```

<ResponseField name="status" type="string">
  `starting` (dialing, bot launching) → `running` (answered) → `completed`.
  `failed` if it never connected or errored; `expired` if no end signal ever
  arrived (never charged).
</ResponseField>

<ResponseField name="channel" type="string">
  `phone` for calls placed here and for inbound calls on your numbers; `web`
  for [web calls](/web-call); `realtime` for the [realtime socket](/realtime-agent).
</ResponseField>

<ResponseField name="transcript" type="object[] | null">
  every turn, once the call has ended. `offsetSeconds` is where the turn starts
  in the recording.
</ResponseField>

<ResponseField name="recordingUrl" type="string | null">
  a signed URL valid for a few minutes. fetch it when you need it; do not
  store it.
</ResponseField>

<ResponseField name="endReason" type="string | null">
  `customer_ended`, `never_connected`, … — why the call ended.
</ResponseField>

## call history

`GET /v1/calls` lists every billed call on the account, newest first, in the
same shape. page with `limit` (1–100, default 25) and `before` = the last `id`
you have; `nextCursor` is `null` on the last page. each call names the agent
that ran it in `agentSlug`, so filter on that for one agent's history.

## billing and limits

a phone call is billed like a [web call](/voice-agents#billing) — per second
on pay-as-you-go, on a slot on a concurrency plan — plus the carrier's
per-minute cost when it goes out over a rented number. it takes one concurrent
slot for its whole duration ([limits](/agent-limits)), and each account has a
daily cap on outbound calls (`429 agent_phone_daily_limit`).

## errors

<AccordionGroup>
  <Accordion title="404 agent_not_found · number_not_found · sip_trunk_not_found">
    no such agent, or `fromNumber` / `fromTrunkId` is not one of your numbers.
  </Accordion>

  <Accordion title="409 agent_not_deployed">
    the agent was saved but never deployed, so no version is live to run the
    call. deploy it with
    [`POST /v1/agents/{agent_ref}/deploy`](/manage-agents#deploy); nothing is
    started or billed until you do.
  </Accordion>

  <Accordion title="409 number_not_connected · number_not_active">
    the rented number in `fromNumber` is not connected to this agent, or its
    rental is not active.
  </Accordion>

  <Accordion title="422 agent_phone_model_unsupported">
    the agent's voice engine cannot take phone calls. switch it to `muga` or
    `mulberry`.
  </Accordion>

  <Accordion title="402 insufficient_balance">
    the account cannot fund a usable call. top up, or enable auto top-up.
  </Accordion>

  <Accordion title="429 concurrency_limit_exceeded · agent_phone_daily_limit">
    every concurrent slot is busy, or today's outbound cap is reached.
  </Accordion>

  <Accordion title="502 agent_session_failed">
    the carrier could not place the call. nothing was charged; retry.
  </Accordion>
</AccordionGroup>

<Tip>
  for a **browser** call use [`POST /v1/webcall`](/web-call); for your own
  audio pipeline, the [realtime socket](/realtime-agent). all three land in the
  same call history.
</Tip>

next: [phone numbers](/phone-numbers).
