Recipes← all recipesView on GitHub

Reject callers on a blocklist before answering

Voicecall blocking and screening (blocklist)

Read the caller's number when SignalWire fetches your call document, and decline a listed number before the call is answered while everyone else is connected.

swmlwebhooksscreening

The claim

Call screening does not need a feature. SignalWire fetches your document with the inbound call webhook, and that request carries the caller’s number, so your handler is the place where the decision lives. The list is yours, kept wherever you keep things, and the platform never sees it.

Why it holds

Two facts carry the claim.

  • The vendored REST spec documents the inbound call webhook payload. Its call object requires from, “the number/URI that initiated this call”, and that is what the handler reads.
  • The bundled SWML schema’s hangup verb takes an optional reason whose values are exactly hangup, busy and decline. A document that starts with hangup and never says answer refuses the call rather than picking it up and dropping it.

Numbers are compared by digits, so +1 (555) 555-0101 on the list still catches 15555550101 on the wire. An absent caller id is not on any list and is connected.

How it works

def is_blocked(caller):
    d = digits(caller)
    return bool(d) and d in BLOCKED

def document(caller):
    service = SWMLService(name="screen", route="/swml")
    if is_blocked(caller):
        service.add_verb("hangup", {"reason": REASON})
    else:
        service.add_verb("answer", {})
        service.add_verb("connect", {"to": DESTINATION})
    return json.loads(service.render_document())

What a listed caller’s request gets back:

{"version": "1.0.0", "sections": {"main": [{"hangup": {"reason": "decline"}}]}}

decline tells the network the call was refused. busy makes the line look engaged instead, which a persistent dialler reads as a dead end. Both are in the schema; REJECT_REASON picks one.

The route sits behind the basic auth you put in the webhook URL, as every document-serving recipe here does. Nobody but SignalWire reads your routing.

Limitations

The verifier proves the documents, not what a blocked caller hears. What decline and busy sound like on the far end depends on the caller’s carrier.

call.from is what the network delivered. A caller who withholds their number arrives with none, and this recipe connects them; screening anonymous callers is a policy you add, not a default.

What to change first

Add "+14155550123" to BLOCKLIST and run the verifier. The allowed caller’s assertion fails on a hangup, which is the point: the list is the whole decision, and it is yours.