Skip to main content
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

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:
{
  "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"
}
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.

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:0020: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.
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.

Track progress

Poll

curl https://voice.growdental.ai/api/partner/v1/call-requests/c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f \
  -H "Authorization: Bearer $GROWDENTAL_API_KEY"
{
  "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.

Quotas and backpressure

Quotas are enforced before anything is queued — a rejected request dials no one:
Statuserror codeMeaningWhat to do
429rate_limitedBulk bucket exceeded (60 req/min)Honor Retry-After
429daily_cap_exceededYour daily call-request contact cap is reached (no Retry-After header)Resume tomorrow, or talk to us about limits
409queue_fullThe practice has too many pending partner contactsBack off; retry after existing contacts drain
409insufficient_minute_balanceThe practice’s calling-minute balance can’t absorb the batchContact the practice / us — do not retry blindly
409idempotency_conflictKey reused with a different payload, or original still in flightUse 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.