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

# API quickstart

> Add contacts to an agent, start it, receive call results by webhook, and fetch the call and recording with the Callers API.

In this quickstart you'll drive an outbound Voice agent entirely from code: find the agent, register a webhook, add contacts, start the agent, and then read each call's result and recording. It takes about 15 minutes.

<ol />

## Before you begin

* An outbound **Voice** agent in the dashboard with a script, a voice, and a phone number. If you don't have one yet, follow the [dashboard quickstart](https://docs.callers.ai/documentation/get-started/quickstart.md). The agent should be stopped while you set it up.
* A public HTTPS endpoint that can receive `POST` requests. For local testing, a tunneling tool such as ngrok works.
* Node.js 18+ or Python 3.9+ if you want to run the code samples (cURL works too).

**1. Create an API key**

In the dashboard, open **Settings** → **API Keys** and create a key. See [API keys](https://docs.callers.ai/documentation/using-callers/generate-api-key.md) for details.

Store it in an environment variable so the samples below can use it:

```bash theme={"dark"}
export CALLERS_API_KEY="paste-your-key-here"
```

Send the key in the `Authorization` header with no `Bearer` prefix.

**2. Find your agent ID**

`GET /agents` needs your account ID, so first fetch it from `GET /organization`. The first item is the account the key belongs to; any subaccounts follow.

```bash cURL theme={"dark"}
ORG_ID=$(curl -s https://api.callers.ai/organization \
  -H "Authorization: $CALLERS_API_KEY" | jq -r '.data[0].id')

curl "https://api.callers.ai/agents?organizationId=$ORG_ID" \
  -H "Authorization: $CALLERS_API_KEY"
```

```javascript Node.js theme={"dark"}
const API = "https://api.callers.ai";
const headers = { Authorization: process.env.CALLERS_API_KEY };

const org = await fetch(`${API}/organization`, { headers }).then((r) => r.json());
const orgId = org.data[0].id;

const agents = await fetch(`${API}/agents?organizationId=${orgId}`, { headers })
  .then((r) => r.json());

for (const agent of agents.data) {
  console.log(agent.id, agent.name, agent.active ? "active" : "stopped");
}
```

```python Python theme={"dark"}
import os
import requests

API = "https://api.callers.ai"
HEADERS = {"Authorization": os.environ["CALLERS_API_KEY"]}

org_id = requests.get(f"{API}/organization", headers=HEADERS).json()["data"][0]["id"]

agents = requests.get(
    f"{API}/agents", headers=HEADERS, params={"organizationId": org_id}
).json()["data"]

for agent in agents:
    print(agent["id"], agent["name"], "active" if agent["active"] else "stopped")
```

Copy the `id` of your outbound agent:

```bash theme={"dark"}
export AGENT_ID="6650c1f2a1b2c3d4e5f60718"
```

> [!TIP]
>
> You can also copy the agent ID from the agent's URL in the dashboard.

**3. Subscribe a webhook**

Tell Callers where to send results. This example asks for two events: `CALL_FINISHED` (every call, once the call has fully ended) and `POST_PROCESSING_FINISHED:INTERESTED` (the contact reached the agent's goal).

```bash cURL theme={"dark"}
curl -X POST "https://api.callers.ai/agents/$AGENT_ID/webhook/subscribe" \
  -H "Authorization: $CALLERS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Quickstart",
    "url": "https://example.com/callers/webhook",
    "events": ["CALL_FINISHED", "POST_PROCESSING_FINISHED:INTERESTED"]
  }'
```

```javascript Node.js theme={"dark"}
await fetch(`${API}/agents/${process.env.AGENT_ID}/webhook/subscribe`, {
  method: "POST",
  headers: { ...headers, "Content-Type": "application/json" },
  body: JSON.stringify({
    name: "Quickstart",
    url: "https://example.com/callers/webhook",
    events: ["CALL_FINISHED", "POST_PROCESSING_FINISHED:INTERESTED"],
  }),
});
```

```python Python theme={"dark"}
requests.post(
    f"{API}/agents/{os.environ['AGENT_ID']}/webhook/subscribe",
    headers=HEADERS,
    json={
        "name": "Quickstart",
        "url": "https://example.com/callers/webhook",
        "events": ["CALL_FINISHED", "POST_PROCESSING_FINISHED:INTERESTED"],
    },
)
```

The URL must be HTTPS. Subscribing the same URL again merges the new events into the existing webhook instead of adding a duplicate. See [Webhook events](https://docs.callers.ai/documentation/webhook/webhook-events.md) for every event you can subscribe to.

**4. Add contacts**

Add the people to call with the bulk endpoint. Each contact needs a `name`, a phone number in international format, and a `data` object. Keys in `data` become [variables](https://docs.callers.ai/documentation/build-your-agent/agent-variables.md) in your script, so `{{company}}` in the script is filled from `data.company`.

```bash cURL theme={"dark"}
curl -X POST "https://api.callers.ai/agents/$AGENT_ID/contacts/bulk?verbose=true" \
  -H "Authorization: $CALLERS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "contacts": [
      { "name": "Dana Levi", "phoneNumber": "+14155552671", "data": { "company": "Acme" } },
      { "name": "Sam Ortiz", "phoneNumber": "+14155550123", "data": { "company": "Globex" } }
    ]
  }'
```

```javascript Node.js theme={"dark"}
const result = await fetch(
  `${API}/agents/${process.env.AGENT_ID}/contacts/bulk?verbose=true`,
  {
    method: "POST",
    headers: { ...headers, "Content-Type": "application/json" },
    body: JSON.stringify({
      contacts: [
        { name: "Dana Levi", phoneNumber: "+14155552671", data: { company: "Acme" } },
        { name: "Sam Ortiz", phoneNumber: "+14155550123", data: { company: "Globex" } },
      ],
    }),
  }
).then((r) => r.json());

console.log(result.data); // { requested, added, modified, contactDetails }
```

```python Python theme={"dark"}
result = requests.post(
    f"{API}/agents/{os.environ['AGENT_ID']}/contacts/bulk",
    headers=HEADERS,
    params={"verbose": "true"},
    json={
        "contacts": [
            {"name": "Dana Levi", "phoneNumber": "+14155552671", "data": {"company": "Acme"}},
            {"name": "Sam Ortiz", "phoneNumber": "+14155550123", "data": {"company": "Globex"}},
        ]
    },
).json()

print(result["data"])  # requested, added, modified, contactDetails
```

```json Response theme={"dark"}
{
  "status": "success",
  "data": {
    "requested": 2,
    "added": 2,
    "modified": 0,
    "contactDetails": [
      { "contactId": "6650c3a9a1b2c3d4e5f60801", "phoneNumber": "14155552671", "status": "created" },
      { "contactId": "6650c3a9a1b2c3d4e5f60802", "phoneNumber": "14155550123", "status": "created" }
    ]
  }
}
```

If `added + modified` is lower than `requested`, some contacts were skipped (for example, an invalid phone number). See [Manage contacts](https://docs.callers.ai/api-reference/guides/manage-contacts.md).

**5. Start the agent**

```bash cURL theme={"dark"}
curl -X POST "https://api.callers.ai/agents/$AGENT_ID/start" \
  -H "Authorization: $CALLERS_API_KEY"
```

```javascript Node.js theme={"dark"}
await fetch(`${API}/agents/${process.env.AGENT_ID}/start`, { method: "POST", headers });
```

```python Python theme={"dark"}
requests.post(f"{API}/agents/{os.environ['AGENT_ID']}/start", headers=HEADERS)
```

The agent starts calling eligible contacts within its [availability hours](https://docs.callers.ai/documentation/contacts/scheduling-and-retries.md). If you call `start` on an agent that is already active, you get `400` with `Campaign already activated.`, which is safe to ignore.

**6. Receive the webhook**

When a call ends, Callers sends a `POST` with the full call payload to your URL. Respond with any `2xx` status quickly, then process the data.

This minimal receiver also verifies the `X-Signature` header so you only trust requests from Callers. Download the public key from [Validating webhook signatures](https://docs.callers.ai/documentation/webhook/validating-webhook-signatures.md) and save it as `callers-public-key.pem`.

```javascript Node.js (Express) theme={"dark"}
import crypto from "node:crypto";
import fs from "node:fs";
import express from "express";

const publicKey = fs.readFileSync("callers-public-key.pem", "utf8");
const app = express();

// Keep the raw body: the signature is computed over the exact bytes sent.
app.post("/callers/webhook", express.raw({ type: "*/*" }), (req, res) => {
  const signature = req.get("X-Signature") || "";
  const valid = crypto.verify(
    "sha256",
    req.body,
    publicKey,
    Buffer.from(signature, "base64")
  );
  if (!valid) return res.sendStatus(401);

  const call = JSON.parse(req.body.toString("utf8"));
  res.sendStatus(200);

  console.log(call.webhookEvent, call.id, call.status, call.successCriteria?.name);
});

app.listen(3000);
```

```python Python (Flask) theme={"dark"}
import base64
import json

from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.exceptions import InvalidSignature
from flask import Flask, request

with open("callers-public-key.pem", "rb") as f:
    PUBLIC_KEY = serialization.load_pem_public_key(f.read())

app = Flask(__name__)

@app.post("/callers/webhook")
def callers_webhook():
    raw = request.get_data()  # exact bytes sent by Callers
    signature = request.headers.get("X-Signature", "")
    try:
        PUBLIC_KEY.verify(
            base64.b64decode(signature), raw, padding.PKCS1v15(), hashes.SHA256()
        )
    except (InvalidSignature, ValueError):
        return "", 401

    call = json.loads(raw)
    print(call["webhookEvent"], call["id"], call["status"])
    return "", 200
```

Key fields in the payload:

| Field | Example | Meaning |
| - | - | - |
| `webhookEvent` | `CALL_FINISHED` | The event that triggered this delivery. |
| `id` | `6650c4d0…` | Call ID. |
| `campaign.id` | `6650c1f2…` | Agent ID (legacy field name). |
| `contact.id` | `6650c3a9…` | Contact ID. |
| `status` | `INTERESTED` | Call outcome. |
| `successCriteria` | `{ "goalReached": true, "name": "Demo Scheduled" }` | Which success criterion matched. |
| `summary` | `{ "Budget": { "value": "50k" } }` | Answers to your post-call key questions. |
| `transcript` | `[{ "role": "assistant", "content": "…" }]` | The conversation. |
| `recordingUrl` | `https://…` | Recording, when available. |

See [Webhook payload](https://docs.callers.ai/documentation/webhook/webhook-payload.md) for every field.

**7. Fetch the call and recording**

You can re-fetch the same payload at any time, for example to backfill a missed delivery. Use `campaign.id` and `id` from the webhook:

```bash cURL theme={"dark"}
CALL_ID="6650c4d0a1b2c3d4e5f60901"

# Full call payload (same shape as the webhook)
curl "https://api.callers.ai/agents/$AGENT_ID/calls/$CALL_ID/webhook-payload" \
  -H "Authorization: $CALLERS_API_KEY"

# Fresh recording URL
curl "https://api.callers.ai/agents/$AGENT_ID/calls/$CALL_ID/recording" \
  -H "Authorization: $CALLERS_API_KEY"
```

```javascript Node.js theme={"dark"}
const callId = "6650c4d0a1b2c3d4e5f60901";
const base = `${API}/agents/${process.env.AGENT_ID}/calls/${callId}`;

const payload = await fetch(`${base}/webhook-payload`, { headers }).then((r) => r.json());
const recording = await fetch(`${base}/recording`, { headers }).then((r) => r.json());

console.log(payload.data.summary, recording.data); // recording.data is the URL
```

```python Python theme={"dark"}
call_id = "6650c4d0a1b2c3d4e5f60901"
base = f"{API}/agents/{os.environ['AGENT_ID']}/calls/{call_id}"

payload = requests.get(f"{base}/webhook-payload", headers=HEADERS).json()["data"]
recording_url = requests.get(f"{base}/recording", headers=HEADERS).json()["data"]
```

Recording links can expire (signed links are valid for about an hour), so request a fresh URL whenever you need to play or download a recording rather than storing the link.

## Troubleshooting

> **Every request returns 403 Forbidden resource**
>
> The key is missing, mistyped, revoked, or sent with a `Bearer ` prefix. Send the raw key: `Authorization: <key>`. If you send `x-sub-organization-id`, make sure it's a subaccount of the key's account.

> **GET /agents returns 400**
>
> `organizationId` is a required query parameter. Get it from `GET /organization`.

> **Adding contacts returns 'Not allowed to create contacts on incoming campaigns.'**
>
> The agent is an inbound agent. Contacts can only be added to outbound agents.

> **The agent is active but no calls go out**
>
> Check the agent's availability hours and time zone, that it has an outbound phone number, and that the contacts still have attempts left (not already completed or archived). Contacts on the Do Not Call list are skipped.

> **My webhook never fires**
>
> Confirm the URL is public HTTPS and returns `2xx`. Check which events you subscribed to: `CALL_FINISHED` fires for every call, while `POST_PROCESSING_FINISHED:*` events fire only for the matching outcome. Delivery attempts are logged on each call in the dashboard. See [Webhooks overview](https://docs.callers.ai/documentation/webhook/webhook-overview.md#delivery-and-retries).

## Next steps

> **[Launch outbound calls](https://docs.callers.ai/api-reference/guides/launch-outbound-calls.md)**
>
> Production patterns for running outbound agents from your system.

> **[Webhook events](https://docs.callers.ai/documentation/webhook/webhook-events.md)**
>
> Choose the events your integration needs.

> **[Validate signatures](https://docs.callers.ai/documentation/webhook/validating-webhook-signatures.md)**
>
> Verify that webhooks come from Callers.

> **[API introduction](https://docs.callers.ai/api-reference/introduction.md)**
>
> Errors, pagination, and conventions.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.