Contacts & lists
A contact is a person you may message and the record of why you may. A list is a group of them, and it is what a campaign sends to.
| Method | Path | Scope |
|---|---|---|
GET | /v1/contacts | contacts:read |
POST | /v1/contacts | contacts:write |
POST | /v1/contacts/import | contacts:write |
GET | /v1/lists | contacts:read |
POST | /v1/lists | contacts:write |
DELETE | /v1/lists/{id} | contacts:write |
Creating one
POST /v1/contacts
{
"phone_number": "+447400123456",
"first_name": "Ada",
"last_name": "Lovelace"
}{
"id": "01j8xk2p7n4qc3m9v0wz5bt6ay",
"object": "contact",
"reference": "K7Q0M1",
"phone_number": "+447400123456",
"country": "GB",
"first_name": "Ada",
"last_name": "Lovelace",
"consent_status": "unknown",
"created_at": "2026-09-17T09:14:22+00:00"
}The reference
Every contact gets a six-character code. It is there to be read aloud: somebody rings up, quotes theirs, and you find them.
The alphabet is Crockford base-32 — digits, then A–Z with I, L, O and U left out. I, L and O go because they are indistinguishable from 1, 1 and 0 in most fonts and over a phone line. U goes so a random code cannot spell anything unfortunate, which matters because these appear in links sent under your brand.
GET /v1/contacts?reference=K7QOM1Lookup is forgiving, and that is the whole point. An O is read as a zero, an I or an L as a one, a U as a V, case is ignored, and spaces and hyphens are stripped. k7q-om1 finds K7Q0M1. Nobody should have to say "that's a letter O, not a zero".
Importing in bulk
POST /v1/contacts/import
{
"contacts": [
{ "phone_number": "+447400123456", "first_name": "Ada" },
{ "phone_number": "+447400123457", "first_name": "Grace" }
],
"list_id": "01j8xk…",
"consent_status": "opted_in",
"consent_source": "Booking form, March 2026"
}{
"object": "import_result",
"imported": 2,
"already_suppressed": 0,
"invalid": [],
"list_size": 2
}Up to 5,000 rows a call. Three fields in the answer are worth reading rather than ignoring:
imported— created or updated. Importing an existing number updates it rather than making a second contact.already_suppressed— how many of those people have opted out. They are imported, and they will not be messaged.invalid— rows that could not be read, each with its index and the value that failed. A partial import tells you exactly what it skipped; it does not fail the whole batch because one row had a typo.
Importing someone again does not un-opt them out. The suppression wins, every time. If it did not, the way to resurrect a list of people who had asked you to stop would be to upload it again.
consent_source is not a formality
Setting consent_status to opted_in requires it, and the sentence is stored against every contact in the file. Marketing needs recorded prior opt-in under PECR; if somebody complains, that line is the answer. The import is also the only moment anybody still knows what it was.
Lists
POST /v1/lists { "name": "Regulars" }
GET /v1/contacts?list_id=01j8xk…A campaign sends to a list, so this is the step between having contacts and being able to message them. Deleting a list does not delete its contacts.
Searching
GET /v1/contacts?search=lovelace
GET /v1/contacts?search=447400
GET /v1/contacts?search=K7Q0M1One box across name, number and reference. A code-shaped term is also resolved forgivingly, so the search box behaves the same way the reference filter does.