docs

Docs › Get started

Test mode

Every account ships with parallel hs_live_… and hs_test_… keys. Test keys run a simulated carrier: numbers are free and instant, compliance approves immediately, and the whole event pipeline — webhooks included — behaves exactly like production.

What the simulator does

  • Numbers — search and purchase return instantly and cost nothing; the inventory is synthetic but shaped like the real thing.
  • Messages — sends are accepted and delivery receipts arrive moments later as real webhook events.
  • Voice — inbound calls, answers, hangups, and voicemails can be simulated so your call-handling UI is testable without dialing a phone.
  • Compliance — brands vet and campaigns approve immediately, with the same status_changed events production emits over days.

Magic numbers

Rehearse the failures you hope never happen:

ValueBehavior
+15005550001Send accepted, then delivery fails — message.failed webhook with carrier_rejected
+15005550002Recipient replies STOP after the first delivery — rehearse opt-out handling
+15005550003Dialed party never answers (click-to-call and ring targets)
+15005550004Not portable — port-in checks fail with a reason
+15005550005Port-in goes to action_needed after submission
+15005550007Gathers time out — the party never presses anything
+15005550008Media streams fail to start — call.stream.failed
+15005550009The carrier rejects the send itself; after retries the message ends failed with send_failed
+16054551234A real blocked exchange: calls return destination_not_supported — the high-cost (access-stimulation) block applies in test mode too
E911 ZIP 00000Emergency-address registration is rejected
Any other numberDelivers instantly with a receipt; dialed parties answer within seconds

A failure drill

curl
# Same code as production — only the key differs.
curl https://api.handset.dev/v1/messages \
  -H "Authorization: Bearer $HANDSET_TEST_KEY" \
  -d '{"from": "num_…", "to": "+15005550001", "body": "This one is doomed."}'

# moments later, at your webhook endpoint:
→ message.failed  "failure_reason": "carrier_rejected"
Mode isolation is strict: a test key can never see live traffic, and vice versa — separate ledgers end to end.