All tutorials

AI Voice Address Capture: Resolve Recorded Addresses with Swiftcomplete

intermediate
1Overview

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.

2How it works
  1. Your server sends one recording, holding one spoken address, in a single HTTPS POST.
  2. We transcribe the recording and identify the address.
  3. The response holds exactly one result: the validated address, not_resolved or 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.

3Authentication

All requests require an API key passed in the X-Api-Key header.

Get an API key here

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.

4Prepare your recording

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.

Audioencodingsample_rate_hzTypical source
16-bit PCMpcm_s16le48000Browser and app microphones
16-bit PCMpcm_s16le16000Wideband (HD) voice and many voice AI platforms
16-bit PCMpcm_s16le8000Telephone audio decoded to 16-bit PCM
G.711 μ-lawmulaw8000Telephone recordings, common in North America and Japan
G.711 A-lawalaw8000Telephone 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 encoding and
    sample_rate_hz (step 5). Without them, the request ends with protocol_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
Copied

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.

5Send the recording

Send the recording as the body of a POST to:

https://voice.swiftcomplete.com/v1/address/recording
Copied
HeaderRequiredDescription
x-api-keyYesYour Swiftcomplete API key.
Content-TypeNoaudio/wav for a WAV file. A body that starts with a WAV header is read as a WAV file whatever its Content-Type.
Idempotency-KeyNoIdentifies 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
Copied

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
Copied
Query parameterRequiredDescription
encodingFor raw audiopcm_s16le, mulaw or alaw, as in step 4.
sample_rate_hzFor raw audio8000, 16000 or 48000, as in step 4.
channelsNo1, 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.

6Handle the response

Refused before processing

If we can't accept the request, you receive an HTTP status without a result:

StatusMeaningWhat to do
401The key is missing or malformed, or Swiftcomplete doesn't recognise it.Check that the key is copied exactly and sent in x-api-key.
403Swiftcomplete 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.
429The 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.
503The 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": []
}
Copied

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
  }
}
Copied

response is exactly what the Swiftcomplete Validate API returns for this address, so you
can handle it with the same code.

  • validation.result is match, suggestion or none.
  • validation.resolution is the level the address was matched to, for example premise
    or street.
  • address holds 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"}
Copied

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}
Copied
HTTPcodeMeaningWhat to do
400invalid_audioThe 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.
400protocol_errorThe 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.
403api_key_rejectedSwiftcomplete 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.
413utterance_limit_exceededThe recording holds more than 60 seconds of audio, or the request is too large.Send only the address.
422no_speechWe heard no speech.Ask the caller to say their address again.
429service_busyThe 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 500service_unavailableA 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."}
Copied

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.

7Retries

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.

8Limits
LimitValue
Addresses per request1
Audio per recording60 seconds
Request size5.8 MB, enough for 60 seconds of 48 kHz audio
Time to upload the recording30 seconds
Simultaneous requests per key4, shared with streaming connections. Contact us if you need more.
9Coverage

The API resolves UK postal addresses spoken in English, one address per recording.

10Example code

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()
Copied

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;
}
Copied
11Best practices
  • 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.
12Troubleshooting
  • 401: check that the key is copied exactly and is sent in the x-api-key header.
  • 403 without a type: read the description. 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
    description and check your account.
  • protocol_error for raw audio: check that the query string has encoding and
    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_resolved or no_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.