Hosted SIM API

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.

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

Texts

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

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:

  1. the Call's or Message's StatusCallback;
  2. 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.