DIDWW
Second voice carrier, next to Twilio. Calls in and out.
A number's provider column picks its trunk
(core/hailhq/core/carrier_routing.py).
# A call from a DIDWW number goes out through LIVEKIT_DIDWW_SIP_OUTBOUND_TRUNK_ID.
curl -X POST "$HAIL_API_URL/calls" -H "Authorization: Bearer $HAIL_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"from":"+351300000000","to":"+14155550100","system_prompt":"Be brief.","recipient_consent":true}'Buying goes through the normal quote flow once DIDWW_API_KEY and
LIVEKIT_DIDWW_SIP_OUTBOUND_TRUNK_ID are both set and DIDWW has enabled
inventory search on the account (§1, step 3). With any
of the three missing, no DIDWW number is quoted (§6).
Not supported on DIDWW: SMS. Inbound calls: see below.
Hail sends no SMS through
DIDWW (CARRIERS["didww"].sms_route is None), so a quote with "sms" in
capabilities never gets a DIDWW offer. DIDWW itself sells SMS-capable
numbers only in US, CA, MX and PR (number groups with the sms_in feature);
Portugal and the rest are voice only on their side too.
1. DIDWW account
-
Sign up at my.didww.com, billed in USD. Hail reads DIDWW's SKU prices as USD cents with no currency conversion or check; a non-USD account will silently mis-price every DIDWW number.
-
Outbound trunks are off by default. Ask
support@didww.comto enable "Outbound Trunks" on the account. -
Inventory search over the API is off by default too. Ask
support@didww.comto enableGET /v3/available_didson the account (and on the sandbox account, if you have one). Check:curl -s -H "Api-Key: $DIDWW_API_KEY" -H "Accept: application/vnd.api+json" \ "https://api.didww.com/v3/available_dids?page[size]=1" # 403 {"errors":[{"detail":"Access for Customer is Denied"}]} → not enabled yet # 200 {"data":[...]} → enabledEvery other call works without it (
balance,countries,did_groups,address_requirements), so the key looks fine while quotes stay empty. -
Numbers are bought through Hail (§5); the DIDWW panel is only needed for the trunk and the API key.
2. DIDWW outbound trunk
Voice → Outbound Trunks → Create (guide):
- Authentication: Credentials & IP-Based.
- Allowed SIP IPs and Allowed RTP IPs: LiveKit Cloud's static ranges
143.223.88.0/21,161.115.160.0/19,153.57.128.0/18(LiveKit static IPs). If the form rejects a range, ask DIDWW support to add it. - CLI settings: add the number to the allowed Caller IDs. On mismatch: Reject call.
- Save. Copy the SIP username and password for step 3.
DIDWW uses SIP digest on INVITE (realm out.didww.com), no REGISTER.
Signaling hosts: fra.eu.out.didww.com (EU), nyc.us.out.didww.com (US),
any.out.didww.com (anycast)
(SIP information).
3. LiveKit outbound trunk
In LiveKit Cloud, Telephony → SIP trunks → Create new trunk → Outbound:
- Address:
fra.eu.out.didww.com - Numbers:
*(any DIDWW number; the Twilio trunk uses the same). Hail picks the trunk by the number'sprovider, so no per-number list is needed. - Username and password: from step 2
Or with the CLI:
cat > didww-trunk.json <<'EOF'
{"trunk": {"name": "didww", "address": "fra.eu.out.didww.com",
"numbers": ["*"], "auth_username": "<username>", "auth_password": "<password>"}}
EOF
lk sip outbound create didww-trunk.jsonCopy the trunk ID (ST_…) into .env as LIVEKIT_DIDWW_SIP_OUTBOUND_TRUNK_ID
and restart api.
4. API key
my.didww.com → API → create a key. Put it in .env:
DIDWW_API_KEY=<key>
DIDWW_ENVIRONMENT=productionDIDWW_ENVIRONMENT=sandbox points every call at sandbox-api.didww.com.
The sandbox is a separate site with its own login and key:
my-sandbox.didww.com → API → DIDWW API 3 →
Create new API Key. No sandbox login? Ask support@didww.com for a sandbox
account tied to your production account. The sandbox needs
available_dids enabled as well (step 3 of §1).
For a local smoke test keep the production values in .env and override on
the command line:
DIDWW_API_KEY=<sandbox key> DIDWW_ENVIRONMENT=sandbox \
uv run --directory api uvicorn hailhq.api.main:app --port 80805. Buying a number
curl -X POST "$HAIL_API_URL/numbers/quotes" -H "Authorization: Bearer $HAIL_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"country_code":"PT","number_type":"national","capabilities":["voice"]}'
# → offers[].provider == "didww", readiness "ready" or "verification_required"
curl -X POST "$HAIL_API_URL/numbers" -H "Authorization: Bearer $HAIL_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"country_code":"PT","number_type":"national","quote_id":"<quote_id>"}'Order of events for a country that needs end-user registration:
readiness: verification_required→ the customer fills/verifications(console wizard). Hail creates the identity, address and proofs at DIDWW and validates them (address_requirement_validations). Papers that pass are approved at once. No person reviews them (create_verification).POST /numbersreserves setup + first month, orders the DID (provisioning_state: pending).- The reconciler files the registration (
address_verifications) once the DID exists and polls it every 15 minutes. DIDWW approves in 1–3 days →active. - Rejected →
failed, the DID is terminated, the monthly fee is refunded, the setup fee is charged (DIDWW billed it and does not refund). - Still pending after 7 days →
failed. DID exists: terminated, monthly fee refunded, setup fee charged. No DID: setup and monthly fee refunded.
If the terminate call fails, the DID id is kept in
provisioning_metadata.unreleased_resource_id and the reconciler retries every
minute until DIDWW accepts it.
Schemas: openapi/openapi.yaml. Code:
providers/voice/didww.py,
providers/verification/didww.py,
number_orders.py, the didww
entry of CARRIERS in
carrier_routing.py.
A call from a didww number with LIVEKIT_DIDWW_SIP_OUTBOUND_TRUNK_ID empty
fails with end_reason = carrier_route_failed before any LiveKit room exists.
The same applies to Twilio numbers when LIVEKIT_TWILIO_SIP_OUTBOUND_TRUNK_ID is empty.
6. No DIDWW offer shows up
Ask for a quote and read unavailable_providers:
curl -s -X POST "$HAIL_API_URL/numbers/quotes" -H "Authorization: Bearer $HAIL_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"country_code":"PT","number_type":"national","capabilities":["voice"]}' \
| python3 -c 'import json,sys; q=json.load(sys.stdin); print(q["unavailable_providers"], [o["provider"] for o in q["offers"]])'| You see | Cause | Fix |
|---|---|---|
["didww"] in unavailable_providers | The DIDWW lookup failed. Almost always available_dids is not enabled (§1, step 3). | Run the curl in §1 step 3. 403 → ask DIDWW support. Other error → see the api log. |
[] and no didww offer | DIDWW_API_KEY or LIVEKIT_DIDWW_SIP_OUTBOUND_TRUNK_ID empty, or "sms" requested. | Set both in .env, restart api. Quote with "capabilities":["voice"]. |
422 we don't offer a national number… | The pair (country, type) is not in costs/didww.json. | Wait for the weekly catalog sync, or run it (docs/operations/number-catalog-sync.md). |
422 for a pair costs/didww.json lists | The row has by_request or receive_only (below). | Nothing to fix: Hail does not sell it. For by_request, support orders it by hand. |
The console's "both jobs" card (calls + SMS) never lists DIDWW; only "calls-only numbers" does.
Numbers Hail cannot sell
The catalog (costs/didww.json) marks two kinds
of DIDWW number that the console and POST /numbers/quotes never offer:
jq -r '.numbers[] | select(.by_request) | "\(.country_code):\(.number_type)"' costs/didww.json
jq -r '.numbers[] | select(.receive_only) | "\(.country_code):\(.number_type)"' costs/didww.jsonby_request: true(40 rows on 2026-10-07): DIDWW picks the number when the order is placed, so there is no number to show and buy. Examples: PT national, DE local, GB local, FR local, ES local. The console shows "set up by our team, not bought here" with an Ask support for a number button that emails hi@hail.so. Twilio or Telnyx sell most of these country and type pairs; only these 8 have no other carrier: AL local, CH toll-free, DE national, DK national, HK national, IE national, MT national, NG local. Fulfilment:docs/operations/didww-by-request.md.receive_only: true(86 rows, mostly toll-free): the number takes calls but cannot place them (DIDWW featurevoice_inwithoutvoice_out).
The weekly catalog sync sets both from DIDWW's number groups
(runbook); the API purchase gate
skips marked rows
(telephony_catalog.py).
Inbound calls
A DID takes calls through the voice IN trunk it is assigned to. Hail assigns the DID when it routes calls to an agent; you create the trunk once.
- Voice → Inbound Trunks → Create: type SIP, host
<project>.sip.livekit.cloud, port 5060, transport TCP. Copy the trunk id into.envasDIDWW_VOICE_IN_TRUNK_ID. SetDIDWW_API_KEY(my.didww.com → API). - Create the LiveKit inbound trunk and dispatch rule
(LiveKit Cloud §4) and set
LIVEKIT_SIP_INBOUND_TRUNK_ID.
PATCH /v1/numbers/{id} with voice_agent_id then assigns the DID to the
trunk (PATCH /v3/dids/{id}, relationship voice_in_trunk) and lists the
number on the LiveKit trunk; null clears both (Agents).
A DID added by hand without a stored DID id is looked up by number.
Reference: DIDWW voice IN trunks.
Telnyx
Second carrier for numbers, calls and SMS, next to Twilio. When both are configured, POST /numbers/quotes compares live inventory, prices and verification requirements from each and ranks the offers; POST /numbers buys one by quoteid. A number never changes carrier: calls from a Telnyx number go out through the Telnyx trunk, SMS through Telnyx messaging.
AWS SES (email)
Outbound and inbound email go through Amazon SES (SESv2 API). You need an AWS account and the SES service enabled in one region. You also need IAM-role credentials (recommended for EC2/ECS/EKS deployments) or a long-lived access key.