Recipes← all recipesView on GitHub

Route calls by dialed number or time

Voicetime-of-day call routing

Read the dialed number and local time from each inbound webhook, then return the matching call flow.

Also called after-hours call routing, business hours routing, time-of-day routing, call forwarding schedule

The claim

SignalWire fetches SWML with a POST the vendored REST spec documents as the inbound call webhook. Its call object requires call_id, direction, type, from, to and six more fields, and carries to_number for phone calls. The spec describes to as “The number/URI of the destination of this call”. This handler computes a document per request. It keys a table of lines by dialed number and judges the clock in the line’s own time zone with zoneinfo. It builds one of three documents with SWMLService: greet and connect, play the hours and hang up, or not in service.

How it works

def document(to, now):
    service = SWMLService(name="front", route="/swml")
    service.add_verb("answer", {})
    line = LINES.get(to)
    if not line:
        service.add_verb("play", {"url": f"say:{UNKNOWN}"})
    elif is_open(line, now):
        service.add_verb("play", {"url": f"say:{line['greeting']}"})
        service.add_verb("connect", {"to": line["connect"], "timeout": CONNECT_TIMEOUT})
    else:
        service.add_verb("play", {"url": "say:" + CLOSED.format(...)})
    service.add_verb("hangup", {})
    return service.get_document()

@app.post("/swml")
def swml():
    payload = request.get_json(force=True)
    return jsonify(document(dialed(payload["call"]), datetime.now(timezone.utc)))

What the platform sends, and what it gets back for an open line:

POST https://<your-host>/swml
{"call": {"call_id": "...", "type": "phone", "from": "+1555...", "to": "+15550001111",
          "to_number": "+15550001111", ...}, "vars": {}, "envs": {}, "params": {}}

{"version": "1.0.0", "sections": {"main": [
  {"answer": {}}, {"play": {"url": "say:Ridgeline Cycles sales, one moment."}},
  {"connect": {"to": "+15550100001", "timeout": 25}}, {"hangup": {}}]}}

dialed() prefers to_number, which the spec says is present for phone calls, and falls back to to for SIP and WebRTC. The hours live on the line, so two lines in two zones read one clock and get two answers.

Limitations

You prove the documents against a frozen clock. What the caller hears, and whether the destination answers, are the platform’s side of a live call.

The table is a dictionary and the hours are one window a day. Holidays, lunch breaks and per-day schedules are yours to model; the branch point stays the same.

What to change first

Change the workshop’s tz to America/Denver and run the verifier. The 15:59 edge case fails, because 22:59 UTC is 16:59 in Denver and the workshop closes at 16:00. The zone belongs to the line, and the verifier holds each line to its own.