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

# On-Demand Calls

> Place outbound campaign calls one at a time from your own systems, over the API

Most campaign calling starts with a CSV: you upload a [contact list](/campaigns/contact-lists), launch a run, and the platform works through it. **On-demand calls** are the other way in — your own system asks SquawkVoice to call one person, right now or within the next 48 hours, and the campaign supplies the agent, the caller ID, the schedule and the compliance rules.

Use it when the trigger lives in your systems rather than in a list: a web form submission that deserves an immediate callback, a CRM stage change, an abandoned checkout, a support ticket that needs a voice follow-up.

<Info>
  On-demand calling is for **voice** campaigns. On a chat or email campaign the tab reads "On-demand calls are for voice campaigns".
</Info>

## Before You Start

<Steps>
  <Step title="Create an API key">
    Go to **Manage → Advanced → API Keys** and create a key. The key is shown once — copy it then. See [API Keys](/manage/api-keys).
  </Step>

  <Step title="Set up a voice campaign">
    The campaign needs an AI agent assigned and an outbound number pool holding at least one active number, both on its **Setup** tab. See [Campaign Setup](/campaigns/campaign-setup).
  </Step>

  <Step title="Copy the campaign ID">
    Open **Studio → Manage → Campaigns → your campaign → On-Demand Calls**. The campaign ID is at the top with a copy button — that ID goes in the request path.
  </Step>
</Steps>

<Warning>
  A campaign missing its agent or its number pool **still accepts your call and returns success**. The contact then sits at **pending** indefinitely, and nothing in the Studio explains why. Check the campaign's **Setup** tab before you point an integration at it.
</Warning>

You do **not** need to launch the campaign. A draft campaign accepts and dials on-demand calls — which also means on-demand calling bypasses the launch checklist, including its compliance attestation step. A campaign you deliberately **stopped**, or one that failed, refuses on-demand calls until you relaunch it.

## The On-Demand Calls Tab

Every campaign has an **On-Demand Calls** tab, at **Studio → Manage → Campaigns → your campaign**. It holds:

* The **campaign ID**, with a copy button.
* **API reference** — a drawer with your API base URL as its own copyable field, plus ready-to-run **curl** and **JavaScript** snippets for all three endpoints, and a **Manage API Keys** button.
* A status card for the campaign's on-demand run, once your first call has landed. Before that the tab reads "No on-demand calls yet" — the run is created for you by the first successful request, so there is nothing to set up.

Clicking the status card opens **On-Demand activity**: the same contact list as a normal run detail, with a **Cancel** action on any contact that has not been dialed yet.

<Note>
  Copy the base URL from the **API reference** drawer rather than assuming it — it differs between environments, and the drawer always shows the correct one for the Studio you are signed in to.
</Note>

## Place a Call

```bash theme={null}
curl -X POST '{baseUrl}/api/v1/campaigns/{campaignId}/contacts/on-demand' \
  -H 'X-API-Key: <your API key>' \
  -H 'Content-Type: application/json' \
  -d '{
    "phone": "+15551234567",
    "postcode": "94107"
  }'
```

Returns **202 Accepted**.

### Request fields

| Field                     | Required                       | Notes                                                                                                                                                                                                                 |
| ------------------------- | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `phone`                   | Yes                            | International format, e.g. `+15551234567`.                                                                                                                                                                            |
| `postcode`                | Yes                            | A string or a number. **Send it as a string if it has a leading zero** — JSON drops the zero before SquawkVoice ever sees it, and the postcode is what resolves the contact's local timezone for legal calling hours. |
| `country`                 | No                             | ISO 3166-1 alpha-2, default `US`. Used together with `postcode` to resolve the timezone.                                                                                                                              |
| `scheduled_at`            | No                             | ISO-8601. Omit for an immediate call. Must resolve to a future instant **within 48 hours**.                                                                                                                           |
| `scheduled_at_timezone`   | No                             | IANA name, e.g. `America/Chicago`. Only used to interpret a `scheduled_at` that carries no offset.                                                                                                                    |
| `has_pewc`                | No                             | Defaults to `false`. Prior express written consent.                                                                                                                                                                   |
| `pewc_date`               | Only when `has_pewc` is `true` | `2026-01-04`, `1/4/2026`, `01/04/2026` and `Jan 4 2026` are all accepted.                                                                                                                                             |
| `is_voice_call_opted_out` | No                             | Defaults to `false`.                                                                                                                                                                                                  |
| `custom_fields`           | No                             | Keys must already exist in the campaign's custom-field configuration. A campaign with no custom fields configured rejects any `custom_fields` at all.                                                                 |

How `scheduled_at` is interpreted:

* With an offset or a `Z`, it is a self-contained instant and is used as-is.
* Without one, it is read in `scheduled_at_timezone` if you supplied that.
* Without either, it is read in the timezone derived from the `postcode`.

### Response

```json theme={null}
{
  "success": true,
  "data": {
    "contact_id": "…",
    "request_id": "…",
    "run_id": "…",
    "run_status": "running",
    "dialing_status": "pending",
    "scheduled_at": null,
    "retry_after": null,
    "scrub_status": "pending",
    "is_on_demand": true,
    "dispatched_scrub": true
  }
}
```

`dialing_status` is `pending` for an immediate call and `callback_requested` for a scheduled one. **Keep the `request_id`** — it is the reference you can cancel by, and the only identifier you hold if you did not store `contact_id`.

## Cancel a Call

A call can be cancelled any time before it is dialed. Either identifier works:

```bash theme={null}
curl -X DELETE '{baseUrl}/api/v1/campaigns/{campaignId}/contacts/on-demand/{contactId}' \
  -H 'X-API-Key: <your API key>'
```

```bash theme={null}
curl -X DELETE '{baseUrl}/api/v1/campaigns/{campaignId}/contacts/on-demand/by-request-id/{requestId}' \
  -H 'X-API-Key: <your API key>'
```

Both return **200** with `already_cancelled` telling you which of the two things happened:

```json theme={null}
{ "success": true, "data": { "contact_id": "…", "dialing_status": "blocked", "already_cancelled": false } }
```

Cancelling is **idempotent** — a repeat call, or a call that arrives after the contact has already been picked up for dialing, is not an error. It comes back as 200 with `already_cancelled: true`. Only a contact still at `pending` or `callback_requested` can actually be cancelled; once dialing has begun there is nothing to stop.

## Responses and Errors

| Status | Meaning                                                                                                                                                                                      |
| ------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `202`  | Contact accepted (place a call).                                                                                                                                                             |
| `200`  | Cancellation processed — check `already_cancelled`.                                                                                                                                          |
| `400`  | Invalid `phone`, `postcode`, PEWC fields or `custom_fields`; or a `scheduled_at` that is unparseable, in the past, or more than 48 hours out.                                                |
| `401`  | Missing or rejected API key. Also what you get for a key that has expired or been revoked.                                                                                                   |
| `403`  | The key's account does not have access to this campaign.                                                                                                                                     |
| `404`  | Campaign not found; or, on a cancel, no on-demand contact matching that id.                                                                                                                  |
| `422`  | The postcode could not be resolved to a timezone, there are no dialing rules covering the resulting jurisdiction, or the campaign is stopped or failed and is not accepting on-demand calls. |

## Things to Design Around

<AccordionGroup>
  <Accordion title="There is no idempotency key">
    A retried `POST` places a **second real call**. Repeat submissions of the same phone number are accepted deliberately — each one is its own attempt with its own `request_id` — so de-duplicate on your side, and store the `request_id` you get back.
  </Accordion>

  <Accordion title="48 hours is a hard ceiling">
    Anything further out is rejected with a `400`. There is no long-horizon scheduler behind this endpoint. To call someone next week, hold the request in your own system until it is inside the window.
  </Accordion>

  <Accordion title="On-demand calls take concurrency first">
    A campaign's on-demand run can be dialing at the same time as a CSV run, and the on-demand run has first claim on the campaign's `max_concurrent_calls` budget. While on-demand traffic is flowing, the CSV run visibly dials fewer calls per tick — and nothing in the Studio explains the slowdown. Raise the campaign's concurrency if both need to run hard at once.
  </Accordion>

  <Accordion title="The on-demand run always uses current settings">
    A CSV run is frozen at launch: it keeps immutable snapshots of the settings, schedule, agent and number pool. The on-demand run is not. It re-reads the campaign's dialing settings, schedule, number pool and agent on **every** request, which is why its cards are labelled "current" rather than "snapshot". Editing the campaign therefore re-points on-demand calling immediately, with no relaunch — convenient, but easy to do by accident.
  </Accordion>

  <Accordion title="There is no status read-back">
    The response is fire-and-forget. There is no key-authenticated way to ask how a call went — outcomes live in the Studio, on the **On-Demand activity** list and in [Call History](/analyze/call-history).
  </Accordion>
</AccordionGroup>

## Compliance Still Applies

Placing a call through the API skips the CSV pool, not the rules. Every compliance gate is evaluated again at dial time, exactly as it is for a CSV run:

* Legal calling hours for the contact's resolved timezone, and TCPA restrictions — see [Scheduling](/campaigns/scheduling).
* Prior express written consent, and voice opt-out.
* DNC and reassigned-number scrubbing, with its per-dial charge — see [Compliance & DNC](/campaigns/compliance-and-dnc).
* Frequency caps.

An insufficient wallet balance pauses the run, the same as for any other campaign calling.

<Warning>
  Because a draft campaign accepts on-demand calls, the launch checklist's attested-disclosure step can be bypassed. Make sure your agent's disclosure is configured before you send live traffic through this endpoint.
</Warning>

## Where On-Demand Activity Shows Up

| Where                                            | On-demand calls appear?                               |
| ------------------------------------------------ | ----------------------------------------------------- |
| **On-Demand Calls** tab → **On-Demand activity** | Yes — the full contact list, with per-contact cancel. |
| [Call History](/analyze/call-history)            | Yes — as ordinary calls.                              |
| **Runs** tab                                     | No — the on-demand run is kept out.                   |
| **Live Monitoring**                              | No.                                                   |
| Campaign list statistics                         | No.                                                   |

This keeps your CSV run reporting comparable across launches. When you need the on-demand picture, go to the tab.

## Where to go next

<Columns cols={2}>
  <Card title="API Keys" icon="key" href="/manage/api-keys">
    Create and revoke the keys this API authenticates with.
  </Card>

  <Card title="Campaign Setup" icon="sliders" href="/campaigns/campaign-setup">
    Assign the agent, number pool and dialing settings on-demand calls will use.
  </Card>
</Columns>
