Testing your integration
Test mode lets you exercise DialStack's entire event-driven surface without placing a real phone call. Authenticate with a test secret key (sk_test_...) and every call you create runs through a simulated lifecycle: each leg advances on a timer, the same events a real call fires are delivered in the same order, and a complete call record is available when it ends.
Nothing is actually dialed — the simulator only fabricates the source of the events. Everything downstream is your real production path: webhooks arrive over the same signed, retried, ordered delivery your live traffic uses, and the SSE feed at /v1/events carries the same call.* events it does for live calls. So a green test run means your integration is ready for production.
The goal is coverage parity: every webhook event DialStack emits in production can be triggered in sandbox. Most events happen on their own as a call plays out (the lifecycle, recordings, and voicemail); a few are triggered by dialing a specific test number; and standalone events like fax and queue notifications are fired on demand.
What you need
Three things: a sandbox account, a test key, and a phone number to originate from.
The number must be a sandbox number — not one of your real numbers. If the sandbox account doesn't have one yet, order one (sandbox numbers order instantly). Just like in live mode, a call needs a number, so your test drives the same "order a number, then place a call" path your production code will. See Test phone numbers for how the simulator uses it.
Point your requests at the same base URL as production and pass your test key:
Authorization: Bearer sk_test_YOUR_SECRET_KEY
DialStack-Account: acct_YOUR_SANDBOX_ACCOUNT
Every simulated event carries the sandbox account_id, so you can always tell test traffic apart from live.
Receiving webhooks locally
Simulated events are delivered to your platform's configured webhook URL — the same URL your live events use. During development that URL has to reach your laptop, so you need a publicly reachable address that forwards to your local server. Any HTTP tunnel gives you one; point your sandbox webhook URL at the address it hands back.
For example, with cloudflared:
cloudflared tunnel --url http://localhost:3000
# → https://random-name.trycloudflare.com (set this as your sandbox webhook URL)
ngrok and other tunnels work the same way. And if you only need the call.* lifecycle, you can skip webhooks entirely — the SSE feed at /v1/events needs no tunnel at all, because you connect out to it.
Outbound calls
POST /v1/calls doubles as your traffic generator. In test mode it starts a simulated outbound call that runs both legs on timers and delivers the resulting events to your webhook. Your production code path runs unmodified: it dials whatever number the user clicked, and in a test you dial a magic number to script the outcome.
curl -X POST https://api.dialstack.ai/v1/calls \
-H "Authorization: Bearer sk_test_YOUR_SECRET_KEY" \
-H "DialStack-Account: acct_YOUR_SANDBOX_ACCOUNT" \
-d '{"user": "user_01...", "dial_string": "+15005550100"}'
The response is 202 Accepted with no body, exactly as in production. The call id arrives in the call.initiated webhook. For the answered scenario above, call.initiated, call.ringing, call.answered, and call.end arrive in order, followed a few seconds later by recording.available, recording.transcription.complete, and recording.summary.complete.
During the call:
GET /v1/calls/{id}returns200with a sparse live projection while the call is in flight —statusisnulland fields only known once the call ends (such as duration and the recording) are absent, filling in when the terminal record lands aftercall.end. This matches production, so polling logic you build against sandbox behaves identically against live traffic.
Scenario magic numbers
The outcome of a simulated call is scripted by the number you dial, following the same convention as Stripe's test cards and Twilio's test numbers. Encoding the scenario in the number instead of an API flag means your integration test drives your real code path, with no test-only branches.
| Number dialed | What it simulates |
|---|---|
+1 500 555 0100 | Answered after 5s, ~30s of talk time, recording produced |
+1 500 555 0101 | Rings for 20s, no answer |
+1 500 555 0102 | Busy |
+1 500 555 0103 | The far end's answering machine picks up — recorded as a normal answered call (why) |
+1 500 555 0104 | Answered, then transferred mid-call (call.transfer) |
933 | Answered, and additionally emits call.emergency |
| any other number | Default: answered, short call, recording produced |
The 500 555 01xx range is not dialable on the real network, so a test number that leaks into your production config can never ring a real person. Numbers are matched on their digits, so +15005550100, 15005550100, and 500-555-0100 all resolve to the same scenario. Dialing 933 (the standard emergency self-test number) emits call.emergency so you can exercise your emergency-call handling; no real emergency call is placed. 911 is intentionally not a sandbox trigger.
A note on voicemail
On an outbound call there is no reliable signal for whether a person or an answering machine picked up — a machine answering looks exactly like a human answering. So 0103 (and the voicemail outcome in a custom scenario) is recorded as an ordinary answered, completed call. Your integration won't see a distinct "went to voicemail" status on an outbound call, because production can't produce one either.
A true voicemail deposit — a voicemail call status plus the voicemail.new / voicemail.transcription.complete events — only happens on an inbound call that lands in one of your own users' mailboxes, which is something DialStack can see. Trigger that with the inbound endpoint or a custom scenario.
Inbound calls and screen pop
No number you dial can trigger an inbound call, so inbound simulations use a dedicated test-only endpoint, POST /v1/test_helpers/calls. It drives call.incoming and call.ringing so you can test screen pop. Set from_number to a known contact's number to exercise your caller-ID matching. The endpoint returns 200 with the started call's id and scenario, so a test harness can assert on the response directly.
curl -X POST https://api.dialstack.ai/v1/test_helpers/calls \
-H "Authorization: Bearer sk_test_YOUR_SECRET_KEY" \
-H "DialStack-Account: acct_YOUR_SANDBOX_ACCOUNT" \
-d '{"user": "user_01...", "from_number": "+15551234567"}'
user is the target user for the inbound call, and it's currently required: the simulator delivers the call directly to that user rather than running your account's inbound routing (routing simulation isn't supported yet). This endpoint is sandbox-only; a live key receives a 400 explaining it's test-mode only.
Custom scenarios
Magic numbers cover the common outcomes. For everything else, pass an explicit scenario object to POST /v1/test_helpers/calls to set the outcome and timing directly. This works for both inbound and outbound calls — set direction: "outbound" to simulate a device-originated call (distinct from API-originated click-to-call on POST /v1/calls); for outbound, to_number selects the magic scenario when you don't pass an explicit one.
curl -X POST https://api.dialstack.ai/v1/test_helpers/calls \
-H "Authorization: Bearer sk_test_YOUR_SECRET_KEY" \
-H "DialStack-Account: acct_YOUR_SANDBOX_ACCOUNT" \
-d '{
"user": "user_01...",
"from_number": "+15551234567",
"from_name": "Sarah Johnson",
"scenario": { "outcome": "voicemail", "ring_seconds": 15, "voicemail_seconds": 12 }
}'
Scenario fields: outcome (answered, no-answer, busy, voicemail), ring_seconds, talk_seconds, recording, and voicemail_seconds. Any field you omit keeps its default. Duration fields are clamped to 120 seconds each, and an unrecognized outcome returns 400. For inbound, from_number must be in E.164 format.
The voicemail outcome only deposits a voicemail when the call is inbound to one of your users (see A note on voicemail). Such a call drops a voicemail in that user's mailbox and fires voicemail.new followed by voicemail.transcription.complete.
Standalone events (fax, queue, and more)
The call lifecycle, recordings, and voicemail all play out as part of a simulated call. The remaining webhook events belong to separate subsystems — faxes and call queues — plus a couple of one-off notifications that aren't tied to a call's timeline. Sandbox fires these on demand: POST /v1/test_helpers/events sends a single event of the type you name, with a representative sample payload, through the same webhook delivery path. It returns 200 echoing the event it sent.
curl -X POST https://api.dialstack.ai/v1/test_helpers/events \
-H "Authorization: Bearer sk_test_YOUR_SECRET_KEY" \
-H "DialStack-Account: acct_YOUR_SANDBOX_ACCOUNT" \
-d '{"event": "queue.call.answered"}'
You can fire any of these:
- Fax:
fax.delivered,fax.failed,fax.received - Queue:
queue.call.queued,queue.call.dispatched,queue.call.answered,queue.call.abandoned,queue.call.timed_out,queue.call.completed,queue.call.callback_requested,queue.call.callback_attempted,queue.call.callback_failed - Other standalone notifications (not part of any call, fax, or queue flow):
recording.failed,call.mobile_push_wakeup
An unrecognized type returns 400 with the current list of supported types. Like the other test endpoints, this is sandbox-only — a live key receives a 400.
Between the call simulations and this endpoint, every webhook event DialStack emits in production can be triggered in sandbox, so you can exercise your handler for all of them.
Test phone numbers
A simulated call needs an origin number, and the simulator uses your sandbox account's first phone number. If the account has none, the call returns 400 — order a sandbox number first (they order instantly).
That number is a real, listable resource: outbound simulated calls populate the call record's from_number from it, inbound simulations target it, and it appears in GET /v1/phone_numbers like any other number. So a simulated call never references a number that doesn't exist.
Recordings and transcripts
Answered simulated calls produce a sample recording. recording.available fires, and the signed download URL from GET /v1/calls/{id}/recording serves real audio bytes. recording.transcription.complete and recording.summary.complete then follow: GET /v1/calls/{id}/transcript returns a completed transcript with verbatim text, and the call record's summary field (also on GET /v1/calls/{id}) carries the summary. All three are sample content, so you can exercise the full recording → transcript → summary path end to end.
What to expect
With nothing more than a test key, you can trigger and receive every webhook event DialStack emits in production — no live telephony involved — and then fetch the resulting call record, recording, and transcript.
Click-to-call specifically: a POST /v1/calls to a sandbox user runs the full two-leg call to completion on its own and produces a call record attributed to that user, with a recording.
Related
- Webhook Events for the full event catalog and payload shapes.
- Activity Logging for the receive, dedupe, and write pattern.
- Screen Pop for handling inbound calls.
- Click to Call for the outbound flow.