Skip to content

API reference/phone

Receive delivery status webhooks from an SMS provider

POST/api/v1/phone/feedback/{provider}/{feedbackKey}

authentication
Feedback key in the URL
billing
Free.

The webhook target for an SMS provider's delivery status callbacks, the phone counterpart of the email bounce webhook. Providers cannot send bearer tokens, so the account's feedback key rides in the URL (the same key as for email, generated or rotated on the dashboard); the provider name picks the payload adapter. Twilio and MessageBird post form fields, Vonage and Sinch JSON; both are read. Only definitive statuses become outcomes; queued, sent, buffered, accepted and unknown are ignored.

Provider Path Mapping
twilio /api/v1/phone/feedback/twilio/{key} Message status callback: delivered → delivered · undelivered and failed → undelivered. To is the number; the callback carries no timestamp, so the outcome is dated on receipt.
vonage /api/v1/phone/feedback/vonage/{key} SMS API receipts (msisdn, status, message-timestamp) and Messages API status webhooks (to, status, timestamp): delivered → delivered · expired, failed, rejected and undeliverable → undelivered.
messagebird /api/v1/phone/feedback/messagebird/{key} Status reports (recipient, status, statusDatetime), configured as POST: delivered → delivered · delivery_failed and expired → undelivered.
sinch /api/v1/phone/feedback/sinch/{key} Per-recipient reports (recipient, status, at) and batch reports (statuses[].recipients): Delivered → delivered · Failed, Expired, Rejected and Aborted → undelivered.

A failed receipt's error code — Twilio ErrorCode, Vonage err-code (SMS API) or error.title (Messages API), MessageBird statusErrorCode (or, without one, statusReason), Sinch code — is read into the report's failure_class from each provider's published code list: permanent (Vonage 3 and 4, Messages API 1190, MessageBird 1, Sinch 150 and 151), transient (such as Twilio 30003, 30004, 30005 and 30008, Vonage 1, 2, 7 and 8), not_sms_capable (Twilio 30006, Vonage 14, MessageBird 10, 11 and 21, Sinch 152) and sender_side (such as Twilio 30007, 30022, 30032 and 30034, Vonage 6, 10, 11 and 39, MessageBird 104 and 110, Sinch 402 and 412). A code that says the recipient opted out — Twilio 21610, Vonage 9 and 1240, MessageBird 103 or the status reason opted out — is not recorded as a failure at all but as an opted_out outcome, which puts the number on your suppression list with reason opted_out and never scores it; Sinch publishes no opt-out code. The tables are a best-effort reading of each provider's documentation; a code none of them lists leaves the failure unclassified, which counts exactly as every failure did before classes existed.

Throttled at 120 requests per minute per IP. Outcomes recorded here behave exactly like ones reported through POST /api/v1/phone/feedback: they feed the account's own later lookups and its suppression list, and are never billed. A receipt dated in the future is recorded as now.

Parameters

name in type description
providerrequiredpathstringWhich provider's payload format to expect. One of: twilio, vonage, messagebird, sinch.
feedbackKeyrequiredpathstringThe account's feedback key, which starts with `fb_`.

Request body

The provider's native callback payload, as JSON or form fields.

Example request

curl -X POST https://spaw.co/api/v1/phone/feedback/twilio/fb_… \
  -H "Content-Type: application/json" \
  -d '{
  "MessageSid": "SM0f2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d",
  "To": "+447911012345",
  "MessageStatus": "undelivered",
  "ErrorCode": "30003"
}'
const response = await fetch('https://spaw.co/api/v1/phone/feedback/twilio/fb_…', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    "MessageSid": "SM0f2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d",
    "To": "+447911012345",
    "MessageStatus": "undelivered",
    "ErrorCode": "30003"
  }),
});
const result = await response.json();
import requests

response = requests.post(
    'https://spaw.co/api/v1/phone/feedback/twilio/fb_…',
    json={
        'MessageSid': 'SM0f2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d',
        'To': '+447911012345',
        'MessageStatus': 'undelivered',
        'ErrorCode': '30003'
    },
)
result = response.json()
$ch = curl_init('https://spaw.co/api/v1/phone/feedback/twilio/fb_…');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST => 'POST',
    CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
    CURLOPT_POSTFIELDS => json_encode([
        'MessageSid' => 'SM0f2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d',
        'To' => '+447911012345',
        'MessageStatus' => 'undelivered',
        'ErrorCode' => '30003'
    ]),
]);
$result = json_decode(curl_exec($ch), true);

This endpoint has no console on its page. It is a URL your provider posts reports to, authenticated by the feedback key in the path. Nothing calls it from your own code, so there is nothing here to run.

Responses

200The outcomes the payload carried were recorded.

{
    "success": true,
    "data": {
        "recorded": 1,
        "skipped": 0
    }
}

401The feedback key does not exist or was rotated.

{
    "success": false,
    "error": {
        "code": "INVALID_FEEDBACK_KEY",
        "message": "That feedback key does not exist or was rotated.",
        "request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
    }
}

404The provider path is not one of twilio, vonage, messagebird, or sinch.

{
    "success": false,
    "error": {
        "code": "UNKNOWN_FEEDBACK_PROVIDER",
        "message": "Supported providers: twilio, vonage, messagebird, sinch.",
        "request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
    }
}

429Over 5 requests per second for the key. Retry after the limit resets.

{
    "success": false,
    "error": {
        "code": "RATE_LIMITED",
        "message": "Too many requests. Retry after the limit resets.",
        "request_id": "req_01m1kgdm4xngzmbmff68g94w0c"
    }
}

Error codes

Failures answer { success: false, error: { code, message, request_id } }. Each code has its own page.

markdown version·openapi.json