> ## 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.

# Create a call request

> Queues outbound voice-AI calls to 1–50 contacts at a granted practice.
Requires `call-requests:write`. Counts against the **bulk** rate bucket
(60 requests/minute).

Calls are placed by the platform dialer inside the practice's calling
window (or the narrower window you provide), with retries and
concurrency caps applied automatically. Track progress by polling
`GET /call-requests/{callRequestId}` or by subscribing to the
`call_request.completed` webhook event.

**Always send an `Idempotency-Key`** — a network retry without one can
dial patients twice.

**Consent:** by submitting contacts you attest, per your partner
agreement, that each contact has given prior express consent to be
called at the supplied number (TCPA and applicable state law).

Quotas (enforced before anything is dialed):
- Per-partner daily contact cap → `429 daily_cap_exceeded`
- Per-practice pending-contact cap → `409 queue_full`
- Practice minute-balance circuit breaker → `409 insufficient_minute_balance`




## OpenAPI

````yaml /openapi.yaml post /practices/{practiceId}/call-requests
openapi: 3.1.0
info:
  title: GrowDental Partner API
  version: '2026-07-04'
  summary: >-
    Programmatic access to GrowDental's voice-AI calling platform for dental
    practices.
  description: >
    The GrowDental Partner API lets platforms and practice-management systems

    integrate with GrowDental's voice-AI calling for dental practices: list the

    practices you have been granted, read call results, fetch recordings,

    trigger outbound calls, and receive signed webhook events.


    **Status: early access.** This specification is the published v1 contract.

    Where the deployed API and this document disagree during early access, the

    discrepancy is a bug — report it and we will reconcile.


    ### Access


    Access is **invite-only**. There is no self-serve signup: partner apps,

    API keys, and practice grants are issued by the GrowDental team. Contact

    [partners@growdental.ai](mailto:partners@growdental.ai) to get started.


    ### Conventions


    - All request and response bodies are JSON; property names are
    **snake_case**
      (`duration_seconds`, `next_cursor`, `enabled_events`).
    - Errors use one envelope everywhere: `{ "error": "<machine_code>",
    "message": "<human text>" }`.

    - Requests referencing practices your app has **not** been granted return
      `404` (never `403`), so practice identifiers cannot be probed.
    - `GET` endpoints never take request bodies; all filters are query
    parameters.

    - Pagination varies by resource: the calls list uses cursor pagination
      (`limit` + `cursor` in, `data` + `next_cursor` out); the call-requests
      list uses `limit` + `offset` with a `pagination` object; the practices
      list is unpaginated.
    - Every `POST` endpoint honors an `Idempotency-Key` header (24-hour replay
    window).

    - Timestamps are ISO 8601 / RFC 3339 UTC strings.


    ### Rate limits


    Per API key: **600 requests/minute** by default; bulk write endpoints

    (call-request creation) have a separate **60 requests/minute** budget.

    Limited requests receive `429 rate_limited` with a `Retry-After` header

    (seconds).


    ### PHI and the BAA gate


    Scopes that expose protected health information (`transcripts:read`,

    `recordings:read`, `intake:write`) are hard-gated in code: they return

    `403 baa_required` until a Business Associate Agreement is on file for

    your app. Webhook payloads are PHI-minimal by design (IDs, outcomes, and

    links); PHI travels only over authenticated API pulls.
  contact:
    name: GrowDental Partnerships
    email: partners@growdental.ai
    url: https://voice.growdental.ai
servers:
  - url: https://voice.growdental.ai/api/partner/v1
    description: Production
security:
  - bearerAuth: []
tags:
  - name: Practices
    description: Practices your partner app has been granted access to.
  - name: Calls
    description: Completed and in-flight calls for granted practices.
  - name: Recordings
    description: Short-lived, signed access to call recordings (PHI — BAA required).
  - name: Call Requests
    description: Trigger outbound voice-AI calls to one or more contacts.
  - name: Webhook Endpoints
    description: Manage the HTTPS endpoints that receive signed event deliveries.
paths:
  /practices/{practiceId}/call-requests:
    post:
      tags:
        - Call Requests
      summary: Create a call request
      description: >
        Queues outbound voice-AI calls to 1–50 contacts at a granted practice.

        Requires `call-requests:write`. Counts against the **bulk** rate bucket

        (60 requests/minute).


        Calls are placed by the platform dialer inside the practice's calling

        window (or the narrower window you provide), with retries and

        concurrency caps applied automatically. Track progress by polling

        `GET /call-requests/{callRequestId}` or by subscribing to the

        `call_request.completed` webhook event.


        **Always send an `Idempotency-Key`** — a network retry without one can

        dial patients twice.


        **Consent:** by submitting contacts you attest, per your partner

        agreement, that each contact has given prior express consent to be

        called at the supplied number (TCPA and applicable state law).


        Quotas (enforced before anything is dialed):

        - Per-partner daily contact cap → `429 daily_cap_exceeded`

        - Per-practice pending-contact cap → `409 queue_full`

        - Practice minute-balance circuit breaker → `409
        insufficient_minute_balance`
      operationId: createCallRequest
      parameters:
        - $ref: '#/components/parameters/PracticeId'
        - $ref: '#/components/parameters/IdempotencyKey'
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateCallRequestBody'
      responses:
        '201':
          description: Call request accepted and queued.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/CallRequestDetail'
        '400':
          description: |
            Validation failed (`validation_error`), the body was not valid
            JSON (`invalid_json`), or the contacts list contains the same
            E.164-normalized phone number twice (`duplicate_contacts`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                validationError:
                  value:
                    error: validation_error
                    message: >-
                      contacts.0.phone: must be a valid E.164-compatible phone
                      number (e.g. +18175551234)
                duplicateContacts:
                  value:
                    error: duplicate_contacts
                    message: >-
                      contacts contains duplicate phone numbers after E.164
                      normalization.
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '404':
          $ref: '#/components/responses/NotFound'
        '409':
          description: |
            Conflict. One of:
            - `idempotency_conflict` — this `Idempotency-Key` was already used
              with a different payload, or the original request is still in
              flight.
            - `queue_full` — the practice already has too many pending
              partner-requested contacts; back off and retry later.
            - `insufficient_minute_balance` — the practice's calling-minute
              balance cannot absorb this request (and auto-purchase is not
              enabled).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                idempotencyConflict:
                  value:
                    error: idempotency_conflict
                    message: >-
                      This Idempotency-Key was already used with a different
                      request payload.
                queueFull:
                  value:
                    error: queue_full
                    message: >-
                      This practice already has 100 pending partner-initiated
                      calls (cap 100). Retry after current calls complete.
                minuteBalance:
                  value:
                    error: insufficient_minute_balance
                    message: >-
                      The practice's calling-minute balance cannot absorb this
                      request (estimated ~30 minutes). Ask the practice to add
                      minutes or enable auto-purchase, then retry.
        '429':
          description: |
            Rate limited (`rate_limited`, bulk bucket — honor `Retry-After`)
            or the per-partner daily contact cap was reached
            (`daily_cap_exceeded` — retry tomorrow; no `Retry-After` header).
          headers:
            Retry-After:
              $ref: '#/components/headers/RetryAfter'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
              examples:
                rateLimited:
                  value:
                    error: rate_limited
                    message: Rate limit exceeded. Retry after 12 seconds.
                dailyCapExceeded:
                  value:
                    error: daily_cap_exceeded
                    message: >-
                      Daily call-request cap reached (200/200 contacts requested
                      today). Retry tomorrow or contact support to raise the
                      cap.
components:
  parameters:
    PracticeId:
      name: practiceId
      in: path
      required: true
      description: Practice ID (must be granted to your app — ungranted IDs return 404).
      schema:
        type: string
        format: uuid
    IdempotencyKey:
      name: Idempotency-Key
      in: header
      required: false
      description: |
        Client-generated unique key (e.g. a UUID) making the POST safely
        retryable for 24 hours: retries with the same key and payload replay
        the original response; the same key with a **different** payload is
        rejected with `409 idempotency_conflict`. Strongly recommended on
        every POST — required in practice for call-request creation, where a
        blind retry can dial patients twice.
      schema:
        type: string
        maxLength: 255
      example: 018f3c9e-5f7a-7c3e-b1ce-0a1b2c3d4e5f
  schemas:
    CreateCallRequestBody:
      type: object
      required:
        - contacts
        - purpose
      properties:
        contacts:
          type: array
          minItems: 1
          maxItems: 50
          items:
            $ref: '#/components/schemas/CallRequestContactInput'
          description: |
            Contacts to dial (1–50). Duplicate phone numbers (after E.164
            normalization) are rejected with `400 duplicate_contacts`.
        purpose:
          type: string
          minLength: 1
          maxLength: 100
          description: >
            Why these contacts are being called — shapes the agent's framing

            of the conversation. Free-form slug; common values:

            `recall_reminder`, `treatment_follow_up`,
            `appointment_confirmation`,

            `reactivation`.
          examples:
            - recall_reminder
        context:
          type: string
          maxLength: 5000
          description: |
            Free-text context for the voice agent, applied to every contact.
            Structured context is the only way partners influence call
            content — prompts and agent internals are not exposed.
        window:
          $ref: '#/components/schemas/CallRequestWindow'
    CallRequestDetail:
      description: A call request with its window, context, and per-contact progress.
      allOf:
        - $ref: '#/components/schemas/CallRequest'
        - type: object
          required:
            - context
            - window
            - contacts
          properties:
            context:
              type:
                - string
                - 'null'
              description: The free-text context supplied at creation.
            window:
              type: object
              required:
                - start
                - end
                - daily_start
                - daily_end
                - days
              properties:
                start:
                  type:
                    - string
                    - 'null'
                  format: date-time
                end:
                  type:
                    - string
                    - 'null'
                  format: date-time
                daily_start:
                  type:
                    - string
                    - 'null'
                  description: Daily calling-window start (HH:MM, practice-local).
                daily_end:
                  type:
                    - string
                    - 'null'
                  description: Daily calling-window end (HH:MM, practice-local).
                days:
                  type:
                    - array
                    - 'null'
                  items:
                    type: integer
                    minimum: 0
                    maximum: 6
                  description: Calling days, `0` = Sunday through `6` = Saturday.
            contacts:
              type: array
              items:
                $ref: '#/components/schemas/CallRequestContact'
    Error:
      type: object
      description: The error envelope used by every non-2xx response.
      required:
        - error
        - message
      properties:
        error:
          type: string
          description: Stable machine-readable error code.
          examples:
            - invalid_api_key
            - insufficient_scope
            - baa_required
            - not_found
            - rate_limited
        message:
          type: string
          description: >-
            Human-readable explanation. Do not parse; codes are stable, messages
            are not.
    CallRequestContactInput:
      type: object
      description: >-
        A contact to call. You attest that this contact has consented to receive
        this call.
      required:
        - phone
      properties:
        phone:
          type: string
          description: Phone number to dial — E.164 or a US number normalizable to E.164.
          examples:
            - '+18015550142'
        first_name:
          type: string
          minLength: 1
          maxLength: 100
          description: Contact first name (used by the voice agent).
        last_name:
          type: string
          minLength: 1
          maxLength: 100
        email:
          type: string
          format: email
          maxLength: 255
        external_ref:
          type: string
          minLength: 1
          maxLength: 255
          description: |
            Your stable identifier for this contact. Echoed back as
            `external_ref` on contact progress and as `contact_external_ref`
            on `call.completed` webhook payloads, so you can correlate results
            to your own records without exchanging PHI.
          examples:
            - your-patient-4821
    CallRequestWindow:
      type: object
      description: |
        Optional scheduling constraints. Calls are always confined to the
        practice's configured calling hours and days; a window can only narrow
        them further. `daily_start`/`daily_end` are practice-local times.
      properties:
        start:
          type: string
          format: date-time
          description: |
            Earliest dialing datetime (must be within 30 days). Dialing begins
            at the first dialer tick at or after this time that falls inside
            the daily window.
        end:
          type: string
          format: date-time
          description: |
            Latest dialing datetime — must be in the future and within 30
            days. Contacts still pending when it passes are marked `skipped`
            and the request completes.
        daily_start:
          type: string
          pattern: ^([01][0-9]|2[0-3]):[0-5][0-9]$
          description: >-
            Earliest practice-local time of day to dial (HH:MM, 24h). Default
            `09:00`.
          examples:
            - '09:00'
        daily_end:
          type: string
          pattern: ^([01][0-9]|2[0-3]):[0-5][0-9]$
          description: >-
            Latest practice-local time of day to dial (HH:MM, 24h). Default
            `20:00`.
          examples:
            - '17:00'
        days:
          type: array
          minItems: 1
          maxItems: 7
          items:
            type: integer
            minimum: 0
            maximum: 6
          description: >-
            Calling days as integers, `0` = Sunday through `6` = Saturday.
            Default `[1,2,3,4,5]`.
    CallRequest:
      type: object
      description: A partner-triggered batch of outbound calls (list representation).
      required:
        - id
        - status
        - practice_id
        - purpose
        - counts
        - created_at
        - updated_at
      properties:
        id:
          type: string
          format: uuid
        status:
          type: string
          enum:
            - queued
            - in_progress
            - completed
            - paused
            - cancelled
          description: |
            `queued` until dialing begins, `in_progress` while contacts are
            being worked, `completed` when every contact is terminal.
        practice_id:
          type: string
          format: uuid
        purpose:
          type:
            - string
            - 'null'
        counts:
          $ref: '#/components/schemas/CallRequestCounts'
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time
    CallRequestContact:
      type: object
      description: Per-contact progress within a call request.
      required:
        - phone
        - name
        - external_ref
        - status
        - outcome
        - call_id
      properties:
        phone:
          type:
            - string
            - 'null'
          description: The E.164 number you supplied.
        name:
          type:
            - string
            - 'null'
          description: The contact's name (first + last), when supplied.
        external_ref:
          type:
            - string
            - 'null'
          description: The `external_ref` you supplied for this contact, or `null`.
        status:
          type: string
          enum:
            - pending
            - calling
            - completed
            - failed
            - skipped
        outcome:
          type:
            - string
            - 'null'
          description: Outcome classification once the contact's call completed.
        call_id:
          type:
            - string
            - 'null'
          format: uuid
          description: The resulting call — fetch it via `GET /calls/{callId}`.
    CallRequestCounts:
      type: object
      description: |
        Aggregate contact-status counts for a call request. Keys beyond the
        listed ones may appear if new contact statuses are introduced.
      required:
        - total
        - pending
        - calling
        - completed
        - failed
        - skipped
      properties:
        total:
          type: integer
          description: Total contacts in the request.
        pending:
          type: integer
          description: Contacts not yet dialed.
        calling:
          type: integer
          description: Contacts currently being dialed.
        completed:
          type: integer
          description: Contacts whose call finished.
        failed:
          type: integer
          description: Contacts that failed after exhausting retries.
        skipped:
          type: integer
          description: Contacts skipped by the dialer.
      additionalProperties:
        type: integer
  responses:
    Unauthorized:
      description: |
        Authentication failed: missing/malformed `Authorization` header
        (`missing_authorization`), unknown key (`invalid_api_key`), or a
        revoked key (`key_revoked`).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            missingAuthorization:
              value:
                error: missing_authorization
                message: >-
                  Missing or malformed Authorization header. Expected:
                  Authorization: Bearer gd_live_...
            invalidApiKey:
              value:
                error: invalid_api_key
                message: The provided API key is not valid.
            keyRevoked:
              value:
                error: key_revoked
                message: This API key has been revoked.
    Forbidden:
      description: |
        The key authenticated but is not allowed: the partner app is suspended
        (`app_suspended`), the key lacks the required scope
        (`insufficient_scope`), or a PHI scope was used without a signed BAA
        (`baa_required`).
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          examples:
            appSuspended:
              value:
                error: app_suspended
                message: This partner app is suspended.
            insufficientScope:
              value:
                error: insufficient_scope
                message: 'This API key does not have the required scope: calls:read'
            baaRequired:
              value:
                error: baa_required
                message: >-
                  The scope recordings:read exposes PHI and requires a signed
                  BAA. Contact support to complete your BAA.
    NotFound:
      description: |
        Resource not found. Also returned — deliberately, instead of `403` —
        when the resource exists but belongs to a practice your app has not
        been granted.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/Error'
          example:
            error: not_found
            message: Practice not found.
  headers:
    RetryAfter:
      description: Seconds to wait before retrying.
      schema:
        type: integer
        minimum: 1
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      description: |
        Partner API key sent as a Bearer token:

        ```
        Authorization: Bearer gd_live_...
        ```

        Keys are issued by the GrowDental team (invite-only), shown **once**
        at creation, and stored server-side only as a SHA-256 hash. Key format:
        `gd_live_` (or `gd_test_`) followed by a 43-character base62 secret
        (~256 bits of entropy). To rotate, request a new key, deploy it, then
        ask us to revoke the old one — both keys work during the overlap.

````