Recipes← all recipesView on GitHub

Transcribe a voicemail and text it to the owner

Voicevoicemail transcription (voicemail to text)

Turn transcription on in a cXML Record, then text the owner the words and the recording link when the transcription callback arrives.

Also called voicemail to text, voicemail to email or SMS, transcribe a recording, visual voicemail

cxmlrecordingwebhooks

The claim

Voicemail is only useful if someone listens to it. Turning on transcription is one attribute on the Record verb. The platform does the rest: it POSTs the words to the URL you name, and your handler forwards them. The callback’s own reference names that use, “forwarding the body via SignalWire SMS”, which is what this does.

Why it holds

The trap is in the payload. It carries TranscriptionSid, TranscriptionText, TranscriptionStatus, TranscriptionUrl, RecordingSid and RecordingUrl, and nothing about the call. There is no From. Your app builds the document per call, so the caller’s number goes into the callback URL. The signature check covers that URL, query string and all.

How it works

def callback_url(caller):
    return f"{PUBLIC_URL}/transcription?from={quote(caller or 'unknown', safe='')}"

def voicemail_document(caller):
    return ('<?xml version="1.0" encoding="UTF-8"?>\n'
            "<Response>\n"
            f"  <Say>{escape(GREETING)}</Say>\n"
            f'  <Record transcribe="true" '
            f"transcribeCallback={quoteattr(callback_url(caller))} "
            f'maxLength="{MAX_SECONDS}" playBeep="true" finishOnKey="#" timeout="5"/>\n'
            "</Response>\n")

The Record reference documents every attribute used here. transcribe is “Identifies whether to produce a text transcription of the recording”, default false. transcribeCallback is “A URL to which SignalWire will make a POST request to once the transcription is complete”. timeout is “The number of seconds of silence that ends a recording”, and finishOnKey is the digit set that ends it early.

TranscriptionStatus is completed or failed. A failed transcription still gets a text, with the recording link and no words. A voicemail nobody knows about is worse than one nobody can read.

Two guards, because the handler spends money. The route refuses a callback SignalWire did not sign, using the check from verify-a-webhook-signature over the full URL including the query. And each TranscriptionSid is texted once, because the callback’s reference calls these “advisory, best-effort notifications” whose “delivery can be delayed”.

Limitations

The verifier proves the document, the callback handling and the message. The quality of a transcription, and how long it takes to arrive, are live questions.

The claim on a transcription is taken before the text is sent and is never given back. A send that reached SignalWire and lost its response looks like one that never arrived, so a lost send is not retried. The marker directory stands in for a unique key in your table.

SWML’s record and record_call carry no transcription parameter in 3.0.1, so this is a cXML recipe. take-a-voicemail is the SWML version of the recording itself.

Record also takes an action URL, which is the recording-completed hook and a different callback from this one. Use it when you need to say something to the caller after the beep stops.

The transcription text is customer speech. It is sent onward as a text message here, so the number in OWNER_NUMBER is the only place it lands.

What to change first

Drop ?from= from callback_url and run the verifier. The document assertion fails, and on a real call the owner would get a text saying “Voicemail from unknown”, because the payload never carried the caller.