All tutorials
AI Voice Address Capture: Resolve Recorded Addresses with Swiftcomplete
Swiftcomplete's voice address API turns a recording of someone saying their address into a
validated UK postal address. Send the recording to us in one HTTPS request, and the response
holds the address in the same format as the Swiftcomplete Validate API. It's built for
voicemail, recorded calls and voice messages, and all you need is a Swiftcomplete API key.
If the caller is on a live call, our streaming API is usually the better fit: it listens while
the caller speaks, sends live transcripts and needs no recording or upload. Both APIs return
their results in the same format, so one piece of code can handle either.
- Your server sends one recording, holding one spoken address, in a single HTTPS
POST. - We transcribe the recording and identify the address.
- The response holds exactly one result: the validated address,
not_resolvedor an error.
There's no job to poll and no callback: the result is in the response. The response holds
only the result, with no transcripts. To resolve another address, send another request.
All requests require an API key passed in the X-Api-Key header.
Keep your key on your server. Always send recordings from your backend, never from a web
page or mobile app, where your key could be read. If you record the caller in a browser or
app, upload the recording through your server.
Send a WAV file, or raw audio with its format in the query string (step 5). Send the audio at
its original sample rate. There's no need to resample it.
| Audio | encoding | sample_rate_hz | Typical source |
|---|---|---|---|
| 16-bit PCM | pcm_s16le | 48000 | Browser and app microphones |
| 16-bit PCM | pcm_s16le | 16000 | Wideband (HD) voice and many voice AI platforms |
| 16-bit PCM | pcm_s16le | 8000 | Telephone audio decoded to 16-bit PCM |
| G.711 μ-law | mulaw | 8000 | Telephone recordings, common in North America and Japan |
| G.711 A-law | alaw | 8000 | Telephone recordings, common in Europe |
The audio must be mono. pcm_s16le is signed 16-bit little-endian PCM. mulaw and alaw are
G.711, one byte per sample.
- WAV files declare their own format, so there's nothing else to send. Standard and
extensible WAV files both work. - Raw audio has no header, so you must tell us its format with
encodingand
sample_rate_hz(step 5). Without them, the request ends withprotocol_error. - Compressed formats, such as MP3, M4A, Ogg or Opus, aren't accepted. Convert them first.
Each recording can hold up to 60 seconds of audio. It works best when it holds just the
caller saying their address.
To convert a recording, you can use FFmpeg:
ffmpeg -i input.m4a -ac 1 -ar 16000 -c:a pcm_s16le address.wav
If your call recordings are stereo, with the caller on one channel, send only the caller's
channel. For example, -af "pan=mono|c0=c1" in place of -ac 1 keeps the right-hand channel.
Send the recording as the body of a POST to:
https://voice.swiftcomplete.com/v1/address/recording
| Header | Required | Description |
|---|---|---|
| x-api-key | Yes | Your Swiftcomplete API key. |
| Content-Type | No | audio/wav for a WAV file. A body that starts with a WAV header is read as a WAV file whatever its Content-Type. |
| Idempotency-Key | No | Identifies this address across retries, for example a UUID: 1 to 128 printable ASCII characters, without spaces. See <a href="#retries">Retries</a>. |
For example, with curl:
curl https://voice.swiftcomplete.com/v1/address/recording \ -H "x-api-key: $SWIFTCOMPLETE_API_KEY" \ -H "Content-Type: audio/wav" \ -H "Idempotency-Key: $(uuidgen)" \ --data-binary @address.wav
For raw audio, add its format to the query string:
curl "https://voice.swiftcomplete.com/v1/address/recording?encoding=mulaw&sample_rate_hz=8000" \ -H "x-api-key: $SWIFTCOMPLETE_API_KEY" \ -H "Content-Type: application/octet-stream" \ --data-binary @address.ulaw
| Query parameter | Required | Description |
|---|---|---|
| encoding | For raw audio | pcm_s16le, mulaw or alaw, as in step 4. |
| sample_rate_hz | For raw audio | 8000, 16000 or 48000, as in step 4. |
| channels | No | 1, the default. |
For a WAV file, we ignore the query string's format and read the file's own. We ignore
parameters we don't recognise, so a client that sends extra ones keeps working.
The whole recording must arrive within 30 seconds. We recommend a client timeout of 60 seconds
after your upload completes.
Refused before processing
If we can't accept the request, you receive an HTTP status without a result:
| Status | Meaning | What to do |
|---|---|---|
| 401 | The key is missing or malformed, or Swiftcomplete doesn't recognise it. | Check that the key is copied exactly and sent in x-api-key. |
| 403 | Swiftcomplete recognises the key but won't accept it, for example because its free trial has ended or it is restricted to URLs or IP addresses. | Read the description, then check your account or use an unrestricted key. |
| 429 | The key already has four requests or streaming connections open, or the service has too many requests waiting to start. | Retry after the number of seconds in Retry-After. |
| 503 | The service is temporarily unavailable. | Retry with backoff. |
A 401 or 403 for a key in the right format comes with a JSON body from Swiftcomplete
that explains why, the same as the Validate API's:
{ "parameter": "key", "description": "Your credits have expired. Please sign in to your account and purchase credits to continue using the service.", "resolutionExamples": [] }
Show or log the description. Its wording comes from Swiftcomplete and may change, so
don't match on it. A missing or malformed key gets a 401 with no body.
Otherwise, the response body is one of the three results below, as JSON. Read its type,
and use the HTTP status only to tell success from failure.
validation_completed: an address was found
HTTP 200:
{ "type": "validation_completed", "turn_id": "9fb7c1e2-4d0a-4c55-a0f3-2b8e6d1f7a90", "response": { "validation": { "result": "match", "resolution": "premise" }, "address": { "singleLine": "65 Alfred Road, London, W2 5EU, United Kingdom", "multiLine": { "line1": "65 Alfred Road", "primaryLocality": "London", "postalCode": "W2 5EU" } }, "associatedRecords": null } }
response is exactly what the Swiftcomplete Validate API returns for this address, so you
can handle it with the same code.
validation.resultismatch,suggestionornone.validation.resolutionis the level the address was matched to, for examplepremise
orstreet.addressholds the formatted address, both on a single line and split into lines.
turn_id identifies this request. Include it if you contact support.
not_resolved: no address could be identified
HTTP 200:
{"type":"not_resolved","turn_id":"9fb7c1e2-4d0a-4c55-a0f3-2b8e6d1f7a90"}
We couldn't identify the address confidently enough, so we don't return a guess. Ask the
caller again if you can, for example asking them to say the postcode clearly.
error: the request failed
{"type":"error","turn_id":"9fb7c1e2-4d0a-4c55-a0f3-2b8e6d1f7a90","code":"service_busy","retry_after_ms":1000}
| HTTP | code | Meaning | What to do |
|---|---|---|---|
| 400 | invalid_audio | The recording isn't in a supported format (step 4) or holds no audio, or the Content-Type says WAV but the body isn't a WAV file. | Check your recording against step 4. |
| 400 | protocol_error | The request couldn't be read, for example raw audio without encoding and sample_rate_hz, an invalid Idempotency-Key or query parameter, or an upload that took more than 30 seconds. | Fix your request. |
| 403 | api_key_rejected | Swiftcomplete stopped accepting your key during the request, for example because your credits ran out or your free trial ended. The address isn't returned. | Read the description and check your account. Retrying won't help until it's fixed. |
| 413 | utterance_limit_exceeded | The recording holds more than 60 seconds of audio, or the request is too large. | Send only the address. |
| 422 | no_speech | We heard no speech. | Ask the caller to say their address again. |
| 429 | service_busy | The service is at capacity, or a request with the same Idempotency-Key is still in progress. No work was done. | Retry after retry_after_ms milliseconds. The Retry-After header gives the same wait in seconds. |
| 503 or 500 | service_unavailable | A temporary failure on our side. | Retry once (see below). If it keeps happening, contact us with the turn_id and error_ref. |
An api_key_rejected error also has a description from Swiftcomplete, for example:
{"type":"error","turn_id":"9fb7c1e2-4d0a-4c55-a0f3-2b8e6d1f7a90","code":"api_key_rejected","description":"Your free trial has expired. Please sign in to your account and purchase credits to continue using the service."}
A service_unavailable error also has an error_ref, such as "error_ref":"E954", which
helps our support team find the cause. New values may be added, so don't write code that
depends on particular ones.
To retry, send the same recording again.
If a network failure or timeout means you didn't receive the result, retry with the same
Idempotency-Key. For 120 seconds after a result, the same key returns that result straight
away, with its original turn_id, without processing the audio again. A key whose request
ended in an error isn't kept, so a retry with it is processed afresh. If the original request
is still in progress, you receive service_busy. Use a new key for every new recording,
because a reused key returns the earlier result.
| Limit | Value |
|---|---|
| Addresses per request | 1 |
| Audio per recording | 60 seconds |
| Request size | 5.8 MB, enough for 60 seconds of 48 kHz audio |
| Time to upload the recording | 30 seconds |
| Simultaneous requests per key | 4, shared with streaming connections. Contact us if you need more. |
The API resolves UK postal addresses spoken in English, one address per recording.
Both examples send a WAV file, retry with the same Idempotency-Key after a network failure
or a 429, and print the result.
Python
"""Resolve a recorded UK address with Swiftcomplete. Usage: pip install requests export SWIFTCOMPLETE_API_KEY=your-key python resolve_recording.py address.wav """ import os import sys import time import uuid import requests URL = "https://voice.swiftcomplete.com/v1/address/recording" ATTEMPTS = 3 TIMEOUT_S = (10, 90) # To connect, then for the upload and the result. def post(audio, idempotency_key): return requests.post( URL, data=audio, headers={ "x-api-key": os.environ["SWIFTCOMPLETE_API_KEY"], "Content-Type": "audio/wav", "Idempotency-Key": idempotency_key, }, timeout=TIMEOUT_S, ) def describe(response): try: body = response.json() except ValueError: body = {} if body.get("type") == "validation_completed": result = body["response"] address = (result.get("address") or {}).get("singleLine") return f"{result['validation']['result']}: {address}" if body.get("type") == "not_resolved": return "No address identified; ask the caller again" if body.get("type") == "error": detail = "" if "retry_after_ms" in body: detail += f", retry after {body['retry_after_ms']} ms" if "error_ref" in body: detail += f", ref {body['error_ref']}" if "description" in body: detail += f": {body['description']}" return f"Error: {body['code']}{detail} (turn {body['turn_id']})" # Refused before processing, with Swiftcomplete's description when it gave one. description = body.get("description") return f"Refused: HTTP {response.status_code}" + (f": {description}" if description else "") def main(): with open(sys.argv[1], "rb") as file: audio = file.read() idempotency_key = str(uuid.uuid4()) # One key for this address, reused if we retry. for attempt in range(1, ATTEMPTS + 1): try: response = post(audio, idempotency_key) except requests.RequestException as error: print(f"Request failed ({error})") continue if response.status_code == 429 and attempt < ATTEMPTS: time.sleep(float(response.headers.get("Retry-After", 1))) continue print(describe(response)) return sys.exit("No result") if __name__ == "__main__": main()
Node.js
// Resolve a recorded UK address with Swiftcomplete. // // Usage (Node.js 18 or later): // SWIFTCOMPLETE_API_KEY=your-key node resolve-recording.mjs address.wav import { randomUUID } from "node:crypto"; import { readFileSync } from "node:fs"; import { setTimeout as sleep } from "node:timers/promises"; const URL = "https://voice.swiftcomplete.com/v1/address/recording"; const ATTEMPTS = 3; const TIMEOUT_MS = 90000; // For the upload and the result. function post(audio, idempotencyKey) { return fetch(URL, { method: "POST", headers: { "x-api-key": process.env.SWIFTCOMPLETE_API_KEY, "Content-Type": "audio/wav", "Idempotency-Key": idempotencyKey, }, body: audio, signal: AbortSignal.timeout(TIMEOUT_MS), }); } function describe(status, body) { if (body.type === "validation_completed") { const { validation, address } = body.response; return `${validation.result}: ${address?.singleLine}`; } if (body.type === "not_resolved") return "No address identified; ask the caller again"; if (body.type === "error") { const retry = body.retry_after_ms ? `, retry after ${body.retry_after_ms} ms` : ""; const ref = body.error_ref ? `, ref ${body.error_ref}` : ""; const why = body.description ? `: ${body.description}` : ""; return `Error: ${body.code}${retry}${ref}${why} (turn ${body.turn_id})`; } // Refused before processing, with Swiftcomplete's description when it gave one. return `Refused: HTTP ${status}${body.description ? `: ${body.description}` : ""}`; } async function resolve(audio) { const idempotencyKey = randomUUID(); // One key for this address, reused if we retry. for (let attempt = 1; attempt <= ATTEMPTS; attempt++) { let response; try { response = await post(audio, idempotencyKey); } catch (error) { console.error(`Request failed (${error.message})`); continue; } const text = await response.text(); if (response.status === 429 && attempt < ATTEMPTS) { await sleep(Number(response.headers.get("retry-after") ?? 1) * 1000); continue; } let body = {}; try { body = JSON.parse(text); } catch { /* no JSON body */ } return describe(response.status, body); } return null; } const result = await resolve(readFileSync(process.argv[2])); if (result) { console.log(result); } else { console.error("No result"); process.exitCode = 1; }
- Ask for the whole address at once, including the postcode, for example: "Please tell
me your full address, including the postcode." - Record only the caller's answer, on the caller's channel alone if your recordings have
one channel per speaker. - Send the original audio. Don't resample telephone audio up to a higher rate, and don't
compress it on the way. - Read the address back to the caller to confirm it when you can, especially for a
suggestion. - If the address isn't resolved, ask again if you can, for example asking for the
postcode first. - Keep your API key on your server.
401: check that the key is copied exactly and is sent in thex-api-keyheader.403without atype: read thedescription. Check that your account has credits
and that the key has no URL or IP address restrictions.api_key_rejected: your key stopped being accepted during the request. Read the
descriptionand check your account.protocol_errorfor raw audio: check that the query string hasencodingand
sample_rate_hz, or send a WAV file instead.invalid_audio: check that the recording is mono, in a format and sample rate from
step 4, and not compressed. For raw audio, check that the query string matches the audio.utterance_limit_exceeded: trim the recording to the caller saying their address.- Frequent
not_resolvedorno_speech: check that the recording is the caller's voice
at its original sample rate, isn't clipped or very quiet, and contains the full address. - Timeouts: allow 60 seconds after your upload completes.
If you need help, contact us at
support@swiftcomplete.com and include the turn_id, and
the error_ref if you received one.
How your business might benefit from Swiftcomplete
- Voicemail: turn an address left in a voicemail into a validated address.
- Recorded calls: pick up a caller's address after the call, without listening back.
- Voice messages: take addresses that customers send as voice notes in your app or
messaging channel. - Deliveries and bookings: collect a delivery or appointment address recorded over the
phone. - Insurance and claims: record the address of a property or incident accurately.
- Utilities and telecoms: check addresses recorded during sign-up, moves and engineer
bookings.