ThunderPhone 2.0 is live.Self-serve, from 2¢/min.Read the announcement

Developer cookbook

Place outbound calls (API)

Trigger an AI-driven outbound call from your own code — survey, follow-up, or confirmation flows.

Outbound calling lets you hand a destination number and an agent configuration to ThunderPhone and have the AI place the call on your behalf. Typical use cases:

  • Appointment confirmations
  • Survey callbacks
  • "Second-try" follow-ups after a missed call
  • Dispatch-style notifications

Prerequisites

  1. Choose an outbound-enabled number

    Use a number you own through a VoIP connection, or request permission to use your ThunderPhone numbers for outbound calls from the Phone Numbers page. ThunderPhone numbers start inbound-only. ThunderPhone reviews each request manually; once approved, those numbers can place calls up to the approved daily limit (resetting at 00:00 UTC) and run campaigns, and you can ask for a higher limit. Customer VoIP numbers have no daily limit, so we recommend them for outbound calling. See Bring your own numbers.

  2. Create an agent

    An outbound-flavoured prompt tends to start with the agent identifying itself and its purpose — "Hi, this is Acme calling to confirm your appointment for tomorrow at 3pm…" Set outbound_speak_order to agent_first (the default).

  3. Keep a positive balance

    Outbound calls return 402 Payment Required if balance is ≤ $0.00. Top up or enable auto-reload under Organization → Billing Settings in the dashboard. Your sk_live_ key can check the balance with GET /v1/billing but can't top up or change auto-reload.

Place a call with a saved agent

The simplest path — reference an agent by id:

curl -X POST https://api.thunderphone.com/v1/call \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from_number": "+15551234567",
    "to_number":   "+14155552671",
    "agent_id":    12
  }'

Response:

{ "call_id": 987654321, "status": "initiated" }

Place a call with inline config

If you want a one-off prompt that isn't worth saving as an agent, pass config instead. The shape matches the response schema of the call.incoming webhook:

curl -X POST https://api.thunderphone.com/v1/call \
  -H "Authorization: Bearer sk_live_YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "from_number": "+15551234567",
    "to_number":   "+14155552671",
    "config": {
      "prompt":  "You are confirming Jane Doe appointment for 3pm tomorrow…",
      "voice":   "john",
      "product": "spark"
    }
  }'

Follow the call

In parallel, subscribe to the telephony.complete webhook — the fastest way to know a call finished. If you can't accept inbound webhooks, poll GET /v1/calls/{call_id} every couple of seconds; the record includes end_reason, duration_seconds, and the recording URL once the call ends.

If your agent is set up to extract structured data after the call, that data is delivered separately after finalization completes.

Pass per-call variables to a saved agent

Saved agents can use placeholders like {{name}} or {{name|there}} in prompt text. When you place an outbound call with agent_id, pass variables with the values for that call:

{
  "from_number": "+[REDACTED_PHONE]",
  "to_number": "+[REDACTED_PHONE]",
  "agent_id": 12,
  "variables": {
    "name": "Jordan"
  }
}

Use string values keyed by placeholder name. These variables apply to saved agents; this guide's inline config example does not use them.

Failure modes worth handling

ErrorFix
402 Payment RequiredTop up the balance or enable auto-reload
400 with code: destination_not_allowedThunderPhone does not call that number: invalid numbers, emergency numbers, premium-rate numbers, short codes, or sanctioned countries. From ThunderPhone numbers, only ordinary US and Canadian numbers (no premium-rate). reason says which; see blocked destinations
409 with retryable: false for a ThunderPhone numberFollow the code: request outbound calling on the Phone Numbers page, wait for a pending review, or use another eligible number
429 with code: platform_outbound_daily_limit_reachedThe organization used today's outbound calls from ThunderPhone numbers. Wait for resets_at (00:00 UTC), request a higher limit on the Phone Numbers page, or call from a number on your own VoIP connection
429 with code: concurrent_call_limit_reachedThe organization has as many calls in progress as its concurrent-call limit (20 by default, counting inbound, outbound, web and test calls). Retry after the Retry-After header's seconds or when a call ends; email support@thunderphone.com to raise the limit
429 with code: same_number_too_soonYou called the same to_number less than 10 seconds ago. Retry after the Retry-After header's seconds
409 with retryable: false for a customer VoIP numberCorrect the state named by code; for verification failures, run POST /v1/phone-numbers/{id}/verify-voip
409 idempotency conflictReuse the key only with the original normalized request, or send a new key
404 from_number is not registered to this organizationConfirm the from_number matches an active phone number in your organization. A number that is already provisioning, failed, releasing, or released also returns this response
500 with only a detail fieldNo outbound trunk is configured. Contact support; this is not a number-state problem you can correct
502 Bad GatewayTransient SIP / LiveKit failure; safe to retry

See the outbound calls reference for the complete stable eligibility-code list and both 409 response shapes. If an active number becomes ineligible after the request is accepted but before the call is placed, the final check returns the non-retryable 409 payload.

Controlling hold time

Outbound calls that run long because the callee is slow to respond (IVR trees, queues) can be capped with max_hold_seconds:

{
  "from_number": "+15551234567",
  "to_number":   "+14155552671",
  "agent_id":    12,
  "max_hold_seconds": 120
}

The agent hangs up if no human audio has been received in the last N seconds. Default is 900 seconds (15 minutes).


Next steps