Recipes← all recipesView on GitHub

Relay calls and texts through a proxy number

Voicephone number masking (call and text by proxy)

Pair two people on one number of yours so calls and texts between them are relayed with your number showing, and neither ever sees the other's.

swmlwebhooksmasking

The claim

Number masking is two lookups and two verbs. Your handler keeps a session that says, on proxy P, participant A reaches B and B reaches A. When either of them calls or texts P, the inbound webhook carries from and to, and the session gives the other party. The document then relays the call or the text with P as the visible number.

Why it holds

Three facts carry the claim.

  • The inbound call webhook’s call and the inbound message webhook’s message both require from and to, which is all the lookup needs.
  • The bundled schema describes connect.from as “the caller ID to use when dialing the number”, so the called party’s phone shows the proxy.
  • reply is the Messaging SWML method that sends a message. Its to defaults to the sender and its from to the number that received the text, so naming both relays the text with the proxy on it.

A stranger who calls the proxy hears that the number is not active and the call ends. A stranger who texts it gets an empty document: nothing sent, nothing kept. Pairing two numbers is a decision about who can reach whom, so /pair is behind a key the server holds and your systems present as X-Proxy-Key.

How it works

def call_document(caller, proxy):
    service = SWMLService(name="proxy-call", route="/call")
    other = other_party(proxy, caller)
    if other:
        service.add_verb("connect", {"to": other, "from": proxy})
    else:
        service.add_verb("answer", {})
        service.add_verb("play", {"url": f"say:{NOT_ACTIVE}"})
        service.add_verb("hangup", {})
    return json.loads(service.render_document())

def message_document(sender, proxy, body):
    other = other_party(proxy, sender)
    steps = []
    if other:
        steps.append({"reply": {"to": other, "from": proxy, "body": body or ""}})
    return {"version": "1.0.0", "sections": {"main": steps}}

What the buyer’s call to the proxy gets back:

{"version": "1.0.0",
 "sections": {"main": [{"connect": {"to": "+13105550199", "from": "+15550001111"}}]}}

The session store is a file keyed by proxy and participant, with both directions written at once. In your app it is a table with an expiry, because a masked pairing should end when the transaction does. The webhooks and the pairing are different requests, which is why the store is not a dictionary.

Both webhook routes sit behind the basic auth you put in the handler URLs, as every document-serving recipe here does.

Limitations

A participant is in one pairing at a time on a given proxy. Pairing A with C removes B’s route back to A, or B could keep reaching A while A’s replies went to C. Two pairings for one person need two proxy numbers.

The verifier proves the documents, not the display. Whether a phone shows the proxy number depends on the carriers between the platform and that phone.

Sessions here never expire. A real pairing has a lifetime, and the table that replaces the file should carry one.

Presenting a number you own as the caller ID for a relayed call is the normal case here; the proxy is yours. Relaying a text from a 10DLC number is A2P traffic and needs a registered campaign.

What to change first

Change "from": proxy to "from": caller in call_document and run the verifier. The document validates and the assertion fails, which is the point: that one field is the difference between a relay and a leak.