# Calls

> Place an outbound AI voice call with a script and the answers you want back, read what happened, and hang it up — the three endpoints that drive the phone from your own systems.

Source: https://docs.amomic.in/api/calls

---

```endpoint
POST /api/v1/calls
```

Your agent picks up the phone and calls someone. The request returns in well under a second; the conversation takes minutes. Everything about using this endpoint follows from that gap.

## When you use this

A call is the right tool when the answer needs a person's attention *now* and a message would sit unread — a technician visit that needs confirming today, an AMC that lapses this week, a high-value order that stalled at checkout, a lead that filled a form ninety seconds ago. Your CRM or workflow engine already knows the moment; this endpoint is the trigger.

The whole surface is three endpoints:

| Method | Path | Purpose |
|---|---|---|
| `POST` | `/api/v1/calls` | Place a call. Returns while the phone is still ringing. |
| `GET` | `/api/v1/calls/{call_id}` | Read the current state and, once it has been read, what came of it (`result`). |
| `POST` | `/api/v1/calls/{call_id}/hangup` | End a call that is ringing or in progress. |

All three require the `calls:write` scope, including the read: there is no separate read scope for calls. See [Scopes](/api/scopes).

There is no list endpoint. You cannot enumerate calls over the API; you track the `call_id` values you created, or you consume the `call.started`, `call.ended` and `call.outcome` [webhooks](/webhooks/events).

::: warning 201 means ringing, not connected
This is the single most important thing on the page. A `201` tells you the call was placed and the destination is being rung. It does not mean anyone answered, and it does not mean the conversation went well. `duration_seconds`, `outcome`, `started_at`, and `ended_at` are all `null` in the create response, by definition — the call has not happened yet.
:::

```mock
{"component": "CallSequenceDiagram", "props": {"caption": "Your request returns at the third step. Everything the customer actually experiences happens afterwards, and reaches you through GET /calls/{id} or the call.ended webhook."}}
```

## Parameters

```params
[
  {"name": "to", "type": "string", "required": true, "description": "The number to call. 6–20 characters, digits with an optional leading <code>+</code> — pattern <code>^\\+?\\d+$</code>. Send it in international form. See <a href=\"#how-to-is-read\">How <code>to</code> is read</a>."},
  {"name": "agent_id", "type": "string", "required": false, "default": "null", "description": "Maximum 64 characters. An <em>assertion</em>, not a selector — see below. Omit it and the agent deployed to the caller-ID number answers."},
  {"name": "from_number", "type": "string", "required": false, "default": "null", "description": "Maximum 20 characters. The caller-ID number the customer sees, which must be an active number in this workspace. Only the digits are compared, so <code>+918047261935</code> and <code>918047261935</code> are the same number. Omit it and the workspace's first active number is used."},
  {"name": "purpose", "type": "string", "required": false, "default": "null", "description": "Maximum 500 characters. Why you are calling, in plain language. Reaches the agent's opening line and system prompt."},
  {"name": "script", "type": "string", "required": false, "default": "null", "description": "Maximum 4,000 characters. The flow the call should follow. Covered in order and in the agent's own words, never read out. See <a href=\"/api/call-outcomes\">Call outcomes</a>."},
  {"name": "details", "type": "object", "required": false, "default": "{}", "description": "Up to 20 <code>\"Label\": value</code> pairs about the person, each value a string, number or boolean: <code>{\"Name\": \"Rohit\", \"Appliance\": \"Split AC, 1.5 ton\"}</code>. Given to the agent as information, never as instructions. When present, the agent confirms it is talking to the right person before using any of them. Labels are cut at 80 characters and values at 300; <code>true</code> and <code>false</code> become yes and no; empty pairs are dropped; lines past 3,000 characters in total are left out whole."},
  {"name": "capture", "type": "object", "required": false, "default": "null", "description": "What to find out on the call: a goal, up to 12 fields, and a success rule. The agent asks for them during the call, and the answers come back in <code>result</code>. See <a href=\"/api/call-outcomes\">Call outcomes</a>."},
  {"name": "ring_seconds", "type": "integer", "required": false, "default": 30, "description": "How long to let the destination ring before giving up. Minimum 10, maximum 90."},
  {"name": "metadata", "type": "object", "required": false, "default": "{}", "description": "Free-form key/value object stored on the call record. Carry your own lead id or job reference here. Not spoken and not returned by the API, but sent back to you in the <code>call.outcome</code> webhook. Avoid the keys <code>campaign_id</code>, <code>campaign_name</code> and <code>callback_id</code>: see <a href=\"#reserved-metadata-keys\">Reserved metadata keys</a>."}
]
```

There is no `channel` field. Voice is the only dialable channel, and it is resolved for you.

### How `to` is read

`to` is screened, and the number that passes the screen is the number dialled.

- **With a leading `+`**, it is read as an international number.
- **Without one**, it is first tried as a number in your workspace's country. If it is not a valid number there, it is read as international digits. In an India workspace, `9876543210` becomes `+919876543210`, and `919876543210` is read as `+91 98765 43210`.
- A number that cannot be read either way is refused with `400` and the message `That destination number isn't usable.` One shorter than 8 digits is refused with `That destination number is too short.` Neither carries a `details.reason`.
- Calls go only to countries enabled for outbound calling, and some number ranges (satellite and premium-rate ranges among them) are blocked. See the failures below.

The response's `to` is the number as dialled, as `+` and digits. Store that, not what you sent.

### Reserved metadata keys

Three keys in `metadata` mean something to the call record. `campaign_id` (with `campaign_name`) labels the call as a campaign call in the console, and `callback_id` labels it as a follow-up. A call labelled either way is no longer treated as one your integration placed, so its `call.outcome` webhook does **not** carry your `metadata`. Use other names, such as `lead_id` or `job_id`.

### The agent comes from the number, not from `agent_id`

This trips up everyone once, so it is worth being blunt about.

A phone number in your workspace has exactly one agent deployed to it. When you place a call, the caller-ID number is resolved first — from `from_number` if you supplied one, otherwise the workspace's first active number — and **the agent deployed to that number is the agent that speaks**. `agent_id` does not choose anything.

What `agent_id` does is assert your expectation. If you pass one and it does not match the agent actually deployed to that number, the call is refused with `412 failed_precondition` and this `details` object:

```json title="error.details on a 412"
{
  "reason": "agent_number_mismatch",
  "deployed_agent_id": "bot_01JQRS4T8XZ0AB1CD2EF3GH4"
}
```

That is the point of the field. If your system cares which agent talks to a customer — and for anything money-adjacent it should — pass both `from_number` and `agent_id` together. Then a deployment change made in the console on a Tuesday afternoon turns into a loud 412 in your logs instead of the wrong agent quietly having the wrong conversation. `details.deployed_agent_id` tells you what is actually there.

To route to a different agent, call from a different number. One number, one agent.

### What `purpose` does

`purpose` is free text, up to 500 characters, and it reaches the agent as the reason for the call. It is added to the agent's instructions for the whole conversation, and it is stored on the call record.

Write it as an instruction to a colleague, not as a script:

```json
{ "purpose": "Confirm Asha's AC service visit on Thursday 15 October between 11am and 1pm, and offer another slot if it does not suit." }
```

For the steps of the call, use `script`; for what you want to know by the end of it, use `capture`. The three work together: `purpose` is why, `script` is how, `capture` is what to bring back. [Call outcomes](/api/call-outcomes) covers `script`, `details` and `capture` in full.

Two things it is not. It is not the greeting — the opening line is a fixed template that identifies your business, discloses that the caller is a virtual assistant, and asks whether now is a good time, in that order. And it is not the agent's knowledge; the agent still answers from the knowledge base bound to it. `purpose` sets the errand, nothing else.

::: note The inbound greeting is deliberately not used
Whatever greeting your number uses for inbound calls is discarded on an outbound call. "Thank you for calling" is wrong when you did the calling. [Outbound calls](/channels/outbound-calls) covers the opening in full, including the Hindi and Hinglish variants.
:::

### `ring_seconds`

Bounded to 10–90, default 30. Below 10 the destination often has not finished routing to the handset; above 90 you are holding one of your 10 concurrent call slots for someone who is not going to answer. A call that rings out never reaches the agent and finalises with a zero duration and an outcome such as `no_answer` or `busy`.

## Example

Calling a customer the moment your service desk books their AC service visit, to confirm the slot.

::: codegroup

```bash cURL
curl -X POST https://services.amomic.in/api/v1/calls \
  -H "Authorization: Bearer $AMOMIC_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: job-VH-4821-confirm-call" \
  -d '{
    "to": "+919876543210",
    "from_number": "+918047261935",
    "agent_id": "bot_01JQRS4T8XZ0AB1CD2EF3GH4",
    "purpose": "Confirm the AC service visit for job VH-4821 on Thursday 15 October between 11am and 1pm, and offer another slot if it does not suit.",
    "details": { "Name": "Asha Menon", "Appliance": "Split AC, 1.5 ton", "Job": "VH-4821" },
    "ring_seconds": 40,
    "metadata": { "job_id": "VH-4821" }
  }'
```

```javascript Node.js
const BASE = "https://services.amomic.in/api/v1";

async function callToConfirmVisit(job) {
  const response = await fetch(`${BASE}/calls`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.AMOMIC_API_KEY}`,
      "Content-Type": "application/json",
      "Idempotency-Key": `job-${job.id}-confirm-call`,
    },
    body: JSON.stringify({
      to: `+${job.customerPhone.replace(/\D/g, "")}`,
      from_number: process.env.AMOMIC_CALLER_ID,
      agent_id: process.env.AMOMIC_SERVICE_AGENT_ID,
      purpose:
        `Confirm the AC service visit for job ${job.id} on ${job.slotLabel}, ` +
        `and offer another slot if it does not suit.`,
      details: { Name: job.customerName, Appliance: job.appliance, Job: job.id },
      ring_seconds: 40,
      metadata: { job_id: job.id },
    }),
  });

  const payload = await response.json();
  if (!response.ok) {
    const { code, message, details } = payload.error;
    // 403 and 412 are configuration problems: alert, do not retry.
    throw new Error(`${code}/${details.reason ?? "-"}: ${message}`);
  }

  // payload.status is "ringing". The conversation has not happened yet.
  await db.jobs.update(job.id, { amomicCallId: payload.call_id });
  return payload.call_id;
}
```

```python Python
import os
import re

import requests

BASE = "https://services.amomic.in/api/v1"


def call_to_confirm_visit(job) -> str:
    response = requests.post(
        f"{BASE}/calls",
        headers={
            "Authorization": f"Bearer {os.environ['AMOMIC_API_KEY']}",
            "Idempotency-Key": f"job-{job.id}-confirm-call",
        },
        json={
            "to": "+" + re.sub(r"\D", "", job.customer_phone),
            "from_number": os.environ["AMOMIC_CALLER_ID"],
            "agent_id": os.environ["AMOMIC_SERVICE_AGENT_ID"],
            "purpose": (
                f"Confirm the AC service visit for job {job.id} on {job.slot_label}, "
                "and offer another slot if it does not suit."
            ),
            "details": {"Name": job.customer_name, "Appliance": job.appliance, "Job": job.id},
            "ring_seconds": 40,
            "metadata": {"job_id": job.id},
        },
        timeout=30,
    )
    if not response.ok:
        error = response.json()["error"]
        raise RuntimeError(
            f"{error['code']}/{error['details'].get('reason', '-')}: {error['message']}"
        )

    call = response.json()  # call["status"] == "ringing"
    db.jobs.update(job.id, amomic_call_id=call["call_id"])
    return call["call_id"]
```

:::

`POST /calls` honours `Idempotency-Key`, and you should always send one. A retry after a timeout replays the stored 201 rather than dialling the customer twice, which, unlike a duplicate message, is a phone ringing in someone's pocket a second time. See [Idempotency](/api/idempotency).

## Response

```json title="201 Created"
{
  "call_id": "call_01JQRS4T8XZ0AB1CD2EF3GH4",
  "status": "ringing",
  "direction": "outbound",
  "to": "+919876543210",
  "from_number": "+918047261935",
  "agent_id": "bot_01JQRS4T8XZ0AB1CD2EF3GH4",
  "duration_seconds": null,
  "outcome": null,
  "summary": null,
  "recording_uri": null,
  "started_at": null,
  "ended_at": null,
  "capture": null,
  "result": { "state": "pending", "disposition": null, "goal_met": null, "success": null, "summary": null, "sentiment": null, "next_step": null, "next_step_reason": null, "fields": {}, "captured": 0, "expected": 0, "missing_required": [], "read_at": null }
}
```

`to` and `from_number` are the numbers actually used, as `+` and digits. `agent_id` echoes what you sent, so it is `null` when you sent none, even though an agent will speak; read the call back to see which. `capture` echoes the capture you sent, with every field key filled in, or `null` when you sent none.

Store `call_id`. It is the only handle you get, and there is no way to look a call up by phone number or by your own metadata.

---

```endpoint
GET /api/v1/calls/{call_id}
```

Read the current state of one call. Scope `calls:write`. No query parameters. Any call in the workspace can be read this way, including inbound calls and campaign calls, as long as you have its `call_id`.

::: codegroup

```bash cURL
curl https://services.amomic.in/api/v1/calls/call_01JQRS4T8XZ0AB1CD2EF3GH4 \
  -H "Authorization: Bearer $AMOMIC_API_KEY"
```

```javascript Node.js
const response = await fetch(
  `https://services.amomic.in/api/v1/calls/${callId}`,
  { headers: { Authorization: `Bearer ${process.env.AMOMIC_API_KEY}` } },
);
const call = await response.json();
```

```python Python
call = requests.get(
    f"https://services.amomic.in/api/v1/calls/{call_id}",
    headers={"Authorization": f"Bearer {os.environ['AMOMIC_API_KEY']}"},
    timeout=15,
).json()
```

:::

| Field | Type | Notes |
|---|---|---|
| `call_id` | string | `call_…`. The id you were given at create. |
| `status` | string | Where the call is in its lifecycle. Open vocabulary — see below. |
| `direction` | string | `outbound` or `inbound`. Falls back to `outbound` when unset. |
| `to` | string \| null | The number called. For an inbound call, the caller's number. |
| `from_number` | string \| null | Your number on the call: the caller ID on an outbound call, the number dialled on an inbound one. Digits only here, without the `+`. |
| `agent_id` | string \| null | The agent that handled the call. |
| `duration_seconds` | integer \| null | Connected duration. `null` until the call finalises; `0` for a call that was never answered. |
| `outcome` | string \| null | How the line ended. `null` while it is live. |
| `summary` | string \| null | A short summary of the call, once one has been written. `null` until then, and on calls nobody answered. For the reading of the call, use `result.summary`. |
| `recording_uri` | string \| null | Set only when recording is on for that number's deployment. It is a storage reference, not a playable link; play recordings from the console. |
| `started_at` | string \| null | ISO 8601 with offset, for example `2026-10-14T05:50:04.812311+00:00`. |
| `ended_at` | string \| null | Same format. `null` while the call is live. |
| `capture` | object \| null | The capture sent with the call, with keys filled in. `null` when none was sent. |
| `result` | object | What came of the call: `state`, `disposition`, `fields` and more. `state` is `pending` until the call has been read. See [Call outcomes](/api/call-outcomes#reading-the-result). |

```json title="200 OK — after the call finished"
{
  "call_id": "call_01JQRS4T8XZ0AB1CD2EF3GH4",
  "status": "completed",
  "direction": "outbound",
  "to": "+919876543210",
  "from_number": "918047261935",
  "agent_id": "bot_01JQRS4T8XZ0AB1CD2EF3GH4",
  "duration_seconds": 96,
  "outcome": "caller_hangup",
  "summary": "Asha confirmed the AC service visit for Thursday and asked the technician to call from the gate.",
  "recording_uri": null,
  "started_at": "2026-10-14T05:50:04.812311+00:00",
  "ended_at": "2026-10-14T05:51:40.930118+00:00",
  "capture": null,
  "result": {
    "state": "ready",
    "disposition": "completed",
    "goal_met": "yes",
    "success": null,
    "summary": "Asha confirmed the AC service visit for Thursday and asked the technician to call from the gate.",
    "sentiment": "positive",
    "next_step": "none",
    "next_step_reason": null,
    "fields": {},
    "captured": 0,
    "expected": 0,
    "missing_required": [],
    "read_at": "2026-10-14T05:52:21.004512+00:00"
  }
}
```

A `call_id` that does not exist, or belongs to another workspace, returns `404 not_found` with the message `Call not found.` The two cases are answered identically on purpose.

### Do not switch exhaustively on `status`

`status` is typed as a plain string. **No closed enum is declared, so no closed list is published.** New lifecycle states can appear without a version bump, and code that throws on an unrecognised value will eventually throw in production at 3 a.m.

| `status` | What it means |
|---|---|
| `ringing` | Placed; the destination is being rung. |
| `dialing`, `starting`, `in_progress` | The call is being connected or is under way. |
| `human_handoff`, `human_active` | The agent handed the caller to a member of your team, who is about to take the call or is on it. |
| `completed` | The call is over. This includes calls nobody answered. |
| `failed` | The call could not be placed (outcome `originate_error`). |

Give yourself a default branch:

```javascript
switch (call.status) {
  case "completed":
    return "done";
  case "failed":
    return "failed";
  default:
    // ringing, in_progress, a handoff, or a state added later: still live.
    return "live";
}
```

The field that tells you how the line ended is `outcome`, and it is only meaningful once the call has ended. What the conversation achieved is in `result`:

| `outcome` | What it means |
|---|---|
| `caller_hangup` | The person you called ended the call. The usual ending for a normal conversation. |
| `agent_hangup` | The agent decided the conversation was finished and ended it. |
| `idle` | Silence on the line past the idle threshold; the agent said goodbye and hung up. |
| `max_duration` | The call hit the configured time cap. |
| `no_answer` | Rang out, or the destination could not be classified more precisely. |
| `busy` | The destination was busy. |
| `rejected` | The destination actively declined the call. |
| `invalid_number` | The number does not exist on the network. |
| `unreachable` | The destination could not be reached: switched off, out of coverage or out of order. |
| `congestion` | The network refused the call for capacity reasons. |
| `suspended` | The workspace is billing-suspended; the call was answered only to say so. |
| `setup_error` | Media setup failed after the call was answered. |
| `originate_error` | The call could not be placed at all. |

Unanswered calls finalise as `status: "completed"` with `duration_seconds: 0` and one of the network outcomes above. A `completed` status therefore does not mean a conversation happened. Check `duration_seconds` and `outcome` together, or read `result.disposition`.

### Getting the outcome: poll, or subscribe

**Subscribe.** Register a webhook for `call.ended` and you are told the moment the call finalises. Register `call.outcome` as well and you get the reading (disposition, summary and every answer to your capture) as soon as it lands, usually within a minute after that. This is the right answer for anything that needs to react: updating a CRM, reopening a job, scheduling another try. See [Event reference](/webhooks/events) for exactly which fields each event carries, and remember that delivery is at-least-once, so dedupe on the event id.

**Poll.** If you cannot receive webhooks, `GET /calls/{call_id}` works, but poll gently: wait `ring_seconds` before your first read, check every few seconds, and stop as soon as `ended_at` is set. If you want the answers, carry on until `result.state` is no longer `pending`. Every poll spends one of your 120 requests per minute, and a tight loop across a batch of calls will hit the [rate limit](/api/rate-limits) long before the calls finish.

```python
import time

import requests


def wait_for_reading(call_id: str, headers: dict, ring_seconds: int = 30, timeout_s: int = 900):
    time.sleep(ring_seconds)  # nothing to see before this
    deadline = time.monotonic() + timeout_s
    while time.monotonic() < deadline:
        call = requests.get(f"{BASE}/calls/{call_id}", headers=headers, timeout=15).json()
        if call["ended_at"] is not None and call["result"]["state"] != "pending":
            return call
        time.sleep(10)
    raise TimeoutError(f"{call_id} was not read within {timeout_s}s")
```

---

```endpoint
POST /api/v1/calls/{call_id}/hangup
```

End a call. Scope `calls:write`. No request body.

::: codegroup

```bash cURL
curl -X POST https://services.amomic.in/api/v1/calls/call_01JQRS4T8XZ0AB1CD2EF3GH4/hangup \
  -H "Authorization: Bearer $AMOMIC_API_KEY"
```

```javascript Node.js
const response = await fetch(
  `https://services.amomic.in/api/v1/calls/${callId}/hangup`,
  { method: "POST", headers: { Authorization: `Bearer ${process.env.AMOMIC_API_KEY}` } },
);
const { ended } = await response.json();
```

```python Python
ended = requests.post(
    f"https://services.amomic.in/api/v1/calls/{call_id}/hangup",
    headers={"Authorization": f"Bearer {os.environ['AMOMIC_API_KEY']}"},
    timeout=15,
).json()["ended"]
```

:::

```json title="200 OK"
{ "call_id": "call_01JQRS4T8XZ0AB1CD2EF3GH4", "ended": true }
```

`ended` is `true` when the call's status was `ringing` or `in_progress` and a hang-up was sent. It is `false` for any other status, and nothing is done. That covers a call that is already over, which is a normal, successful answer, not an error, so hangup is safe to call repeatedly. It also covers a call your team has taken over (`human_handoff`, `human_active`): the API does not cut off a member of your team mid-call.

An unknown `call_id` returns `404 not_found` with `Call not found.`

Use it when your own system learns the call is pointless mid-flight: the customer just rescheduled online, someone else already reached them, or your operator cancelled the job.

## Failures

After the key, scope and request checks, guards run in this order, and the first one that trips is the error you get.

```responses
[
  {"status": 400, "code": "invalid_argument", "reason": "", "description": "Request validation failed, for example a bad `to` pattern, `ring_seconds` outside 10–90, more than 20 `details`, or a capture that does not validate. `details.errors` names the field."},
  {"status": 401, "code": "unauthenticated", "reason": "", "description": "Key missing, malformed, invalid, revoked, expired after a roll, or from the wrong environment. `details.reason` distinguishes all six."},
  {"status": 403, "code": "permission_denied", "reason": "workspace_suspended", "description": "The workspace is suspended for billing. Every API key in it is refused before anything else is checked."},
  {"status": 403, "code": "permission_denied", "reason": "missing_scope", "description": "The key lacks `calls:write`. Required for placing, reading and hanging up."},
  {"status": 409, "code": "conflict", "reason": "idempotency_key_reuse", "description": "The same `Idempotency-Key` was already used with a different request body."},
  {"status": 409, "code": "conflict", "reason": "idempotency_in_progress", "description": "A request with the same `Idempotency-Key` is still running. Wait `details.retry_after_seconds` (2) and retry with the same key."},
  {"status": 412, "code": "failed_precondition", "reason": "unsupported_channel", "description": "Outbound calling is not available on this deployment at all. `details.supported` lists what is."},
  {"status": 412, "code": "failed_precondition", "reason": "voice_not_configured", "description": "Voice calling is not configured on this deployment. Nothing you can fix in the request."},
  {"status": 412, "code": "failed_precondition", "reason": "workspace_suspended", "description": "Calling is paused on this workspace. Normally the 403 above fires first."},
  {"status": 412, "code": "failed_precondition", "reason": "wallet_empty", "description": "The workspace has used every voice minute its plan includes this month and has no prepaid credits left. Someone has to add credits or upgrade in Billing. Inbound calls and calls already in progress are unaffected."},
  {"status": 412, "code": "failed_precondition", "reason": "channel_dormant", "description": "The workspace's plan no longer includes phone numbers. The number is held for a short time so an upgrade can keep it, but it places no calls."},
  {"status": 400, "code": "invalid_argument", "reason": "", "description": "`That destination number isn't usable.` or `That destination number is too short.` See How `to` is read."},
  {"status": 400, "code": "invalid_argument", "reason": "self_dial", "description": "The destination is one of this workspace's own numbers. Calling it would connect the agent to itself."},
  {"status": 403, "code": "permission_denied", "reason": "destination_blocked", "description": "The destination is in a number range blocked for outbound calling, such as satellite or premium-rate numbers."},
  {"status": 403, "code": "permission_denied", "reason": "country_not_allowed", "description": "Outbound calls are not enabled for the destination's country. `details.allowed_country_codes` lists the country codes that are, for example `[\"91\"]`."},
  {"status": 429, "code": "rate_limited", "reason": "call_concurrency", "description": "This workspace already has 10 calls in progress. `details.limit` is 10. Separate from the per-key request limit."},
  {"status": 412, "code": "failed_precondition", "reason": "no_phone_number", "description": "The workspace has no active phone number to call from. Get a number in the console first."},
  {"status": 403, "code": "permission_denied", "reason": "unknown_from_number", "description": "The `from_number` you asked for is not an active number in this workspace."},
  {"status": 412, "code": "failed_precondition", "reason": "no_agent_deployed", "description": "The caller-ID number has no agent deployed to it. A number without a deployment cannot talk."},
  {"status": 403, "code": "permission_denied", "reason": "", "description": "`That number isn't in this workspace.` The number's deployment belongs to another workspace."},
  {"status": 412, "code": "failed_precondition", "reason": "agent_number_mismatch", "description": "Your `agent_id` is not the agent deployed to that number. `details.deployed_agent_id` names the one that is."},
  {"status": 429, "code": "rate_limited", "reason": "daily_call_budget", "description": "The workspace reached its daily outbound calling safety limit. `details` carries `calls` and `reserved_minutes` used so far, and the limits `call_limit` and `minute_limit`."},
  {"status": 429, "code": "rate_limited", "reason": "rate_limited", "description": "120 requests per minute per API key exceeded."},
  {"status": 404, "code": "not_found", "reason": "", "description": "`Call not found.` From `GET /calls/{id}` and hangup, for an unknown id or a call owned by another workspace."},
  {"status": 500, "code": "internal", "reason": "originate_failed", "description": "The call could not be placed. The call record is marked `failed` with outcome `originate_error`. The key is released, so retry with the same `Idempotency-Key`."}
]
```

### Three different 429s

They mean different things and want different responses. None carries a `Retry-After` header, and none is a bug in your request.

- `reason: "rate_limited"`: you are calling the *API* too fast. 120 requests per minute per key. Slow your loop down.
- `reason: "call_concurrency"`: you are calling *people* too fast. A workspace runs at most 10 live calls at once. Your loop is fine; your queue drain rate is not. Wait for calls to end, or spread the batch over time. See [The other 429: concurrent calls](/api/rate-limits#the-other-429-concurrent-calls).
- `reason: "daily_call_budget"`: the workspace has placed as many outbound calls today as its safety limit allows. Each call reserves its maximum length in minutes against the limit when it is placed. Do not retry today; read `details` to see which limit you met.

### The 412 family is configuration, not weather

`voice_not_configured`, `workspace_suspended`, `wallet_empty`, `channel_dormant`, `no_phone_number`, `no_agent_deployed`, `agent_number_mismatch` and `unsupported_channel` all return exactly the same result if you retry in ten seconds. Do not put them behind a backoff. Page someone, or fail the job with the reason attached. The fix is always in the console. The two billing ones are covered in [What happens when you run out](/operate/credits#what-happens-when-you-run-out).

::: danger You own outbound compliance
Amomic-AI places the call you ask for. On calls placed through this endpoint it does not check the destination against any do-not-call or preference registry, does not enforce calling hours, and does not cap calls per recipient. The country allow-list and the daily safety limit protect the platform, not your customers. Those rules apply to you as the caller, so build the checks into the system that decides whom to call, before you reach this endpoint.
:::

## Related

::: cards

::: card Outbound calls, in depth
[How the agent opens the call](/channels/outbound-calls), what the customer hears first, and what changes versus an inbound call.
:::

::: card Trigger a call from your CRM
[The full recipe](/recipes/crm-call) — trigger, idempotency, webhook, and writing the outcome back.
:::

::: card Numbers and KYC
[Getting a number](/channels/phone-numbers) you can call from, and the compliance review that gates it.
:::

::: card Call outcomes
[Design a capture](/api/call-outcomes) and read back what the call learned.
:::

::: card Call events
[`call.started`, `call.ended` and `call.outcome`](/webhooks/events) — the push alternative to polling.
:::

:::
