Recipes← all recipesView on GitHub

Handle call status callbacks

Voicecall status webhooks

Receive ordered webhooks for initiated, ringing, answered, and completed call states, keyed by call ID.

webhooksobservability

The claim

Storing each callback under its CallSid by SequenceNumber rebuilds an ordered timeline of the call, whatever order the callbacks arrived in. The compat call create takes StatusCallback and StatusCallbackEvent, and the vendored spec describes the events as “Valid values: initiated, ringing, answered, completed, ringing_forwarded, ringing_queued. Defaults to completed”. With a StatusCallback and no event list, you ask for only the completed event. This recipe asks for four of the six; ringing_forwarded and ringing_queued are the two it leaves out. The spec documents the payload it posts. The voice status callback carries CallSid, CallStatus, SequenceNumber, Timestamp, Direction, From, To and a block of audio statistics. CallDuration is “Only present on the completed event”, and the spec does not require it there.

Why it holds

SequenceNumber is “The order in which events occur, starting at 0”, and the spec adds that events “may not appear” in that order at your server. So the handler stores by sequence and never by arrival.

How it works

EVENTS = ["initiated", "ringing", "answered", "completed"]

def place(to):
    return client.compat.calls.create(To=to, From=FROM, Url=CALL_URL,
                                      StatusCallback=STATUS_URL,
                                      StatusCallbackEvent=EVENTS,
                                      StatusCallbackMethod="POST")

def record(payload):
    CALLS.setdefault(payload["CallSid"], {})[int(payload["SequenceNumber"])] = payload

What the platform receives:

POST /api/laml/2010-04-01/Accounts/<project>/Calls
{"To": "+1555XXXXXXX", "From": "+1555YYYYYYY", "Url": "https://<your-host>/cxml/greeting.xml",
 "StatusCallback": "https://<your-host>/status",
 "StatusCallbackEvent": ["initiated", "ringing", "answered", "completed"],
 "StatusCallbackMethod": "POST"}

timeline(call_sid) sorts the stored payloads by sequence and returns the steps, the final status, the parties, and the duration when the completed callback carries CallDuration. GET /calls/<CallSid> serves it from the process that holds the store. The fixture’s third callback uses CallStatus in-progress, which the spec’s enum defines as “The call was answered and is in progress”.

Limitations

The spec calls status callbacks “advisory, best-effort notifications” whose “delivery can be delayed or fail silently”. It says not to gate time-critical actions on receiving one. A timeline is a record, not a trigger.

The store is a dictionary in the process. Swap CALLS for your database before anything depends on it.

What to change first

Swap CALLS for a store of your own keyed by CallSid and SequenceNumber, so a restart or a second worker keeps the same ordering. To see what that key protects, as an exercise and not a change to keep: replace int(payload["SequenceNumber"]) in record with len(CALLS[payload["CallSid"]]) and run the verifier. The step-order assertion fails on the fixture’s shuffled arrival, which is the failure this recipe exists to prevent.