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

# Triggering calls

> Create your first call request: contacts, purpose, context, calling windows, idempotency, and quotas.

A **call request** queues outbound voice-AI calls to 1–50 contacts at a granted practice. You supply who to call and why; the platform handles the rest — practice-local calling hours, retries, concurrency caps, and outcome classification.

Requires the `call-requests:write` scope.

## Create a call request

```bash theme={null}
curl -X POST https://voice.growdental.ai/api/partner/v1/practices/9a8b7c6d-5e4f-4a3b-8c9d-0e1f2a3b4c5d/call-requests \
  -H "Authorization: Bearer $GROWDENTAL_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 018f3c9e-5f7a-7c3e-b1ce-0a1b2c3d4e5f" \
  -d '{
    "purpose": "recall_reminder",
    "context": "Spring hygiene recall. Patients are due for a cleaning; offer morning or afternoon slots.",
    "contacts": [
      {
        "first_name": "Maria",
        "last_name": "Lopez",
        "phone": "+18015550142",
        "external_ref": "your-patient-4821"
      },
      {
        "first_name": "James",
        "phone": "+13855550178",
        "external_ref": "your-patient-5177"
      }
    ],
    "window": {
      "daily_start": "10:00",
      "daily_end": "16:00",
      "days": [1, 2, 3, 4, 5]
    }
  }'
```

A `201` response returns the call request with every contact `pending`:

```json theme={null}
{
  "id": "c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f",
  "status": "in_progress",
  "practice_id": "9a8b7c6d-5e4f-4a3b-8c9d-0e1f2a3b4c5d",
  "purpose": "recall_reminder",
  "context": "Spring hygiene recall. Patients are due for a cleaning; offer morning or afternoon slots.",
  "window": {
    "start": null,
    "end": null,
    "daily_start": "10:00",
    "daily_end": "16:00",
    "days": [1, 2, 3, 4, 5]
  },
  "counts": { "total": 2, "pending": 2, "calling": 0, "completed": 0, "failed": 0, "skipped": 0 },
  "contacts": [
    {
      "phone": "+18015550142",
      "name": "Maria Lopez",
      "external_ref": "your-patient-4821",
      "status": "pending",
      "outcome": null,
      "call_id": null
    },
    {
      "phone": "+13855550178",
      "name": "James",
      "external_ref": "your-patient-5177",
      "status": "pending",
      "outcome": null,
      "call_id": null
    }
  ],
  "created_at": "2026-07-04T16:58:00Z",
  "updated_at": "2026-07-04T16:58:00Z"
}
```

<Warning>
  **Always send an `Idempotency-Key`.** If your POST times out and you retry
  without one, patients can be dialed twice. With the key, a retry replays the
  original response instead of creating a second batch; reusing a key with a
  *different* payload is rejected with `409 idempotency_conflict`. Keys are
  replayable for 24 hours.
</Warning>

### The fields that matter

* **`purpose`** — why these contacts are being called (`recall_reminder`, `treatment_follow_up`, `appointment_confirmation`, `reactivation`, ...). Passed to the voice agent (as the `callPurpose` dynamic variable) so it can frame the conversation.
* **`context`** — free-text context (up to 5,000 characters) applied to every contact in the request, passed to the agent as the `callContext` dynamic variable. Purpose and context are the **only** way to influence call content; prompts and agent internals are not exposed.
* **`window`** — optional constraints, interpreted in the **practice's timezone**. Calls always stay inside the practice's configured calling hours; your window can only narrow them. Omit it to use the defaults (`09:00`–`20:00`, Monday–Friday). `start`/`end` are absolute ISO 8601 datetimes: dialing never begins before `start`, and contacts still pending when `end` passes are marked `skipped`.
* **`contacts[].external_ref`** — your stable ID for the contact. It is echoed as `external_ref` on contact progress and as `contact_external_ref` in `call.completed` webhook payloads, so you can correlate results without exchanging PHI.
* **`contacts[].phone`** — E.164 format (`+18015550142`). Duplicate numbers (after normalization) are rejected with `400 duplicate_contacts`.

### Consent

By submitting a contact you attest, per your partner agreement, that the contact has given prior express consent to receive this call at that number (TCPA and applicable state law). Do not submit contacts from purchased lists or anyone who has opted out. See the [go-live checklist](/guides/go-live-checklist).

## Track progress

### Poll

```bash theme={null}
curl https://voice.growdental.ai/api/partner/v1/call-requests/c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f \
  -H "Authorization: Bearer $GROWDENTAL_API_KEY"
```

```json theme={null}
{
  "id": "c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f",
  "status": "in_progress",
  "practice_id": "9a8b7c6d-5e4f-4a3b-8c9d-0e1f2a3b4c5d",
  "purpose": "recall_reminder",
  "context": "Spring hygiene recall. Patients are due for a cleaning; offer morning or afternoon slots.",
  "window": {
    "start": null,
    "end": null,
    "daily_start": "10:00",
    "daily_end": "16:00",
    "days": [1, 2, 3, 4, 5]
  },
  "counts": { "total": 2, "pending": 1, "calling": 0, "completed": 1, "failed": 0, "skipped": 0 },
  "contacts": [
    {
      "phone": "+18015550142",
      "name": "Maria Lopez",
      "external_ref": "your-patient-4821",
      "status": "completed",
      "outcome": "appointment_booked",
      "call_id": "6e1f9c2b-7a8d-4e3f-b012-3c4d5e6f7a8b"
    },
    {
      "phone": "+13855550178",
      "name": "James",
      "external_ref": "your-patient-5177",
      "status": "pending",
      "outcome": null,
      "call_id": null
    }
  ],
  "created_at": "2026-07-04T16:58:00Z",
  "updated_at": "2026-07-04T17:32:04Z"
}
```

Each contact moves `pending → calling → completed | failed` (or `skipped` if the window expires or attempts are exhausted), with automatic retries for unanswered attempts. Once a contact has a `call_id`, fetch the call for its summary and (scope permitting) transcript.

`GET /practices/{practiceId}/call-requests` lists your requests for a practice, newest first, with `limit`/`offset` pagination and aggregate `counts` per request (no per-contact detail).

### Or subscribe

Prefer webhooks over polling: `call.completed` fires per contact as calls finish (carrying your `contact_external_ref`), and `call_request.completed` fires once when the whole batch is terminal. See [Receiving call outcomes](/guides/receiving-call-outcomes).

## Quotas and backpressure

Quotas are enforced **before** anything is queued — a rejected request dials no one:

| Status | `error` code                  | Meaning                                                                  | What to do                                              |
| ------ | ----------------------------- | ------------------------------------------------------------------------ | ------------------------------------------------------- |
| 429    | `rate_limited`                | Bulk bucket exceeded (60 req/min)                                        | Honor `Retry-After`                                     |
| 429    | `daily_cap_exceeded`          | Your daily call-request contact cap is reached (no `Retry-After` header) | Resume tomorrow, or talk to us about limits             |
| 409    | `queue_full`                  | The practice has too many pending partner contacts                       | Back off; retry after existing contacts drain           |
| 409    | `insufficient_minute_balance` | The practice's calling-minute balance can't absorb the batch             | Contact the practice / us — do not retry blindly        |
| 409    | `idempotency_conflict`        | Key reused with a different payload, or original still in flight         | Use a fresh key for new work; retry later for in-flight |

Batch sensibly: one call request with 50 contacts beats 50 single-contact requests — it consumes one bulk-bucket slot and gives you a single `call_request.completed` event to reconcile against.
