Errors and Testing

If you only test the happy path, telephony will punish you.

Common error families

Expect these kinds of failures:

  • invalid or revoked API key
  • validation errors on line or call creation
  • insufficient credits
  • consent missing for outbound calls
  • webhook destination failures
  • missing resources
  • rate limiting

Error response shape

Every error response follows the same envelope. The code field is the stable, machine-readable identifier you should branch on. The message field is human-readable and may change over time.

1{
2 "error": {
3 "code": "validation_error",
4 "message": "Request validation failed.",
5 "details": [
6 {
7 "field": "name",
8 "message": "Required"
9 }
10 ]
11 }
12}

Error codes

Every error response includes a stable code field. Branch on the code, not the message.

CodeHTTPWhen it happens
validation_error422Missing or invalid request fields
email_taken409Email already registered (signup flows)
invalid_credentials401Wrong email or password
invalid_api_key401Key is malformed, expired, or revoked
unauthorized401No auth header provided
not_found404Resource does not exist
forbidden403Blocked by an internal auth or policy check
call_not_found404Call ID does not match any call
consent_required403Outbound call to a number without consent
number_opted_out403Destination number has opted out
call_in_progress409Line already has an active call
insufficient_credits402Balance below the one-minute voice reserve (e.g. 13¢ Zone A webhook, 26¢ Zone A hosted; multiplied by zone for international — see voice-zones guide)
line_no_webhook422Line has no webhook_url configured
rate_limited429Per-endpoint rate limit exceeded
conversation_window_expired40324-hour SMS reply window has closed
internal_error500Unexpected server error
Treat 4xx errors as permanent for the current request and 5xx errors as transient. Retry 5xx with backoff, but do not retry 4xx without fixing the request first.

Error response examples

Concrete shapes for the codes you are most likely to hit in production.

insufficient_credits
1{
2 "error": {
3 "code": "insufficient_credits",
4 "message": "Insufficient balance to place call."
5 }
6}
consent_required
1{
2 "error": {
3 "code": "consent_required",
4 "message": "Outbound calls require consent for this destination."
5 }
6}
conversation_window_expired
1{
2 "error": {
3 "code": "conversation_window_expired",
4 "message": "The 24-hour SMS conversation window has closed."
5 }
6}
call_in_progress
1{
2 "error": {
3 "code": "call_in_progress",
4 "message": "Line already has an active call."
5 }
6}
rate_limited
1{
2 "error": {
3 "code": "rate_limited",
4 "message": "Too many requests. Retry after the window resets."
5 }
6}

Rate limits

Rate limits vary per endpoint. Most use a 1 hour rolling window; SMS is the only endpoint with a 1 minute window.

EndpointLimitWindow
POST /api/v1/lines101 hour
PATCH /api/v1/lines/{id}601 hour
DELETE /api/v1/lines/{id}101 hour
POST /api/v1/calls601 hour
POST /api/v1/calls/conversation601 hour
POST /api/v1/messages101 minute
POST /api/v1/consent1001 hour
POST /api/v1/disclosures201 hour
POST /api/v1/webhooks/test101 hour
Read endpoints (conversations, settings, usage)601 hour

When you exceed a limit, the API returns 429 Too Many Requests with the rate_limited error code.

SDK error handling

Both SDKs surface a typed error class so you can branch on code without parsing response bodies.

1import { Saperly, SaperlyError } from '@saperly/sdk';
2
3try {
4 const call = await saperly.calls.create({ lineId: 'LINE_ID', toNumber: '+1...' });
5} catch (err) {
6 if (err instanceof SaperlyError) {
7 console.error(err.code, err.message); // e.g. "insufficient_credits"
8 }
9}

What to test before shipping

Inbound call

  • call connects
  • caller hears the expected first response
  • transcript is readable
  • call record is queryable after completion

Outbound call

  • blocked without consent
  • succeeds with consent
  • failure state is visible if destination is unreachable

SMS

  • inbound event arrives
  • reply succeeds
  • conversation history looks correct

Webhooks

  • your endpoint handles retries safely
  • non-200 responses are visible in webhook delivery logs
  • test endpoint can hit your webhook before production traffic does

Load testing

Use sk_test_ keys for load testing. Test keys hit the same infrastructure but are isolated from production billing. Rate limits still apply — if you hit 429 rate_limited, back off and retry.

Recommended approach:

  1. Create 2-3 test lines
  2. Simulate inbound webhook events against your handler
  3. Place outbound calls to your own test numbers
  4. Monitor webhook delivery success rate and latency
  5. Check billing transactions to verify correct charging

Useful debugging endpoints

  • GET /api/v1/webhooks/deliveries
  • GET /api/v1/webhooks/stats
  • POST /api/v1/webhooks/test
  • GET /api/v1/calls
  • GET /api/v1/compliance/audit

Advice

Break your webhook on purpose once in staging.

Then look at delivery logs, retries, and the user experience. Better to learn that path deliberately than from your first customer.