Webhooks
We send your server an HTTP request when a call or a text arrives, and when a call's or a message's status changes.
Incoming calls
We POST to your line's VoiceUrl about 1-3 seconds after the first ring, with CallSid, AccountSid, From (or
anonymous), To, CallStatus (ringing), Direction (inbound) and ApiVersion.
- Answer with TwiML within 8 seconds: the carrier's voicemail timer sets that limit. Your dongle holds the call up to 12 seconds for a decision; then it rings on into the SIM's voicemail.
VoiceFallbackUrlruns only when your primary fails fast (an error status or invalid TwiML), never after a timeout, and only if at least 4 seconds of the 12 remain. Its timeout is what remains, minus 1 second.- No answer, an error or invalid TwiML: the call rings on into the SIM's voicemail, and the error is reported (see Errors below).
- An incoming call's status goes to your line's
StatusCallback: only thecompletedevent (a line has no event list to choose from).
Outbound calls
The call's Url is fetched when the call is answered, with CallSid, AccountSid, From, To, CallStatus
(in-progress), Direction (outbound-api) and ApiVersion. It has 10 seconds and isn't retried. After an error
or a timeout, FallbackUrl is asked once (10 seconds); if that fails too, the call ends and the error is reported.
During a call: action URLs and Redirect
Gather,DialandMessageactionURLs get the call's parameters plus the verb's result:Digits(Gather);DialCallStatusandDialCallDuration(Dial);SmsStatus(Message:queued,sentorfailed).Redirectsends the call's parameters by itsmethod.- Each has 15 seconds, isn't retried, and counts toward the call's TwiML limits. A failure ends the call, with the error reported.
Texts
SmsUrlgetsMessageSid,SmsSid,SmsMessageSid,AccountSid,From,To,Body,NumMedia(0),NumSegmentsandApiVersion. Answer with nothing, or with TwiML<Message>to reply from your line.SmsFallbackUrl: within eachSmsUrltry, ifSmsUrlerrors or times out, the same try asksSmsFallbackUrlonce (15 seconds each).- A reply's
<Message>actiongets the reply's Sids and itsSmsStatus. It has 15 seconds, isn't retried, and may answer with nothing or TwiML. A failure is only reported.
Delivery and retries
| Request | Timeout | Tries | If it fails | Success |
|---|---|---|---|---|
VoiceUrl (incoming call) |
8 s | 1 | VoiceFallbackUrl, only after a fast failure; otherwise voicemail |
a 2xx with valid TwiML |
Url (outbound call, at the answer) |
10 s | 1 | FallbackUrl once (10 s); then the call ends |
a 2xx with valid TwiML |
action URLs and Redirect |
15 s | 1 | the call ends | a 2xx with valid TwiML |
SmsUrl |
15 s | up to 4, after 5 s, 30 s and 5 min | SmsFallbackUrl within each try (15 s) |
any 2xx |
StatusCallback (calls, messages) and runtime errors |
15 s | up to 4, after 5 s, 30 s and 5 min | any 2xx |
- A 4xx isn't retried. A 5xx, a timeout or a connection error is.
- Delivery is at least once: dedupe on the Sid. The
I-DWW-Idempotency-Tokenheader is sent too. - Fallback requests carry
ErrorCodeandErrorUrl. - After the last failed
SmsUrltry, the text stays in Messages (statusreceived), no more webhooks are sent for it, and the error is reported.
GET instead of POST
With GET (any *Method), the parameters go in the query string. X-DWW-Signature covers the URL including the
query, and X-DWW-Signature-256 signs an empty body. Error reports are always POST.
Check every request's signature
Every request we send carries X-DWW-Signature: HMAC-SHA1 with your AuthToken over the full URL plus the POST
parameters sorted by name (each name followed by its value), base64-encoded. Compute the same over the request you
received and compare.
import base64, hashlib, hmac
def valid_dww_signature(url, params, signature, auth_token): # signature = the X-DWW-Signature header
data = url + "".join(k + params[k] for k in sorted(params))
mac = hmac.new(auth_token.encode(), data.encode(), hashlib.sha1).digest()
return hmac.compare_digest(base64.b64encode(mac).decode(), signature)
It also carries X-DWW-Signature-256, for replay protection: t=<unix time>,v1=<signature>, where the signature
is HMAC-SHA256 with your AuthToken over t, a ., the full URL, a . and the raw request body, in lowercase hex.
Reject a request whose t is more than 5 minutes from your clock, either way.
const crypto = require("node:crypto");
function validDwwSignature256(url, rawBody, header, authToken, now = Date.now() / 1000) {
const parts = Object.fromEntries(header.split(",").map((p) => {
const i = p.indexOf("=");
return [p.slice(0, i).trim(), p.slice(i + 1).trim()];
}));
const t = Number(parts.t);
if (!Number.isFinite(t) || Math.abs(now - t) > 300) return false; // 5 minutes, either way
const expected = crypto.createHmac("sha256", authToken).update(`${t}.${url}.`).update(rawBody).digest("hex");
const given = String(parts.v1 || "");
return given.length === expected.length && crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected));
}
import hashlib, hmac, time
def valid_dww_signature_256(url, raw_body, header, auth_token, now=None):
parts = dict(p.strip().split("=", 1) for p in header.split(","))
t = int(parts["t"])
if abs((time.time() if now is None else now) - t) > 300: # 5 minutes, either way
return False
expected = hmac.new(auth_token.encode(), f"{t}.{url}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))
Use the raw body exactly as received (bytes), before any parsing.
Which URLs we fetch
https only, on ports 80, 443 or 1024-65535, and never private, loopback or internal addresses. A refused URL is 790001. Your TwiML may be up to 64 KB and audio up to 10 MB.
Webhook source addresses
Every webhook and every fetch of your TwiML or audio comes from 170.64.152.18, over IPv4 only. The same list is
published, with no login, at https://api.dongleswithwings.com/.well-known/dww-egress.json:
{"version": 1, "updated": "2026-11-06", "ipv4": ["170.64.152.18/32"], "ipv6": [], "notice_days": 14, "webhooks": "IPv4 only"}
Any change is announced at least 14 days ahead. Allow-listing the address is optional: the signature is the real check.
Errors while a call or text runs
A bad TwiML document, a webhook timeout or a refused URL is POSTed, with ErrorCode, ErrorUrl and ErrorMessage,
to the first of these that's set:
- the Call's or Message's
StatusCallback; - your line's
StatusCallback.
It's always in the console's log view too. If neither is set, the console log is the only place it appears.