Skip to content

Contacts Endpoints

Contacts are the core data object for SMS campaigns. A contact is a phone number with optional metadata (name, email, tags, notes). Contacts belong to an account and can be a member of multiple contact lists simultaneously via a junction table.

Base paths:

  • /api/v1/contacts — individual contacts
  • /api/v1/contact-lists — named lists (groups)

All endpoints require Authorization: Bearer <access_token>.


Data Model Overview

Why many-to-many?

The initial design used a single list_id FK on contacts. In practice, customers frequently need the same contact in multiple campaigns (e.g. "VIP Customers" list + "Newsletter" list). The junction table contact_list_memberships handles this cleanly. The service layer maintains a denormalised total_count on contact_lists rather than running COUNT(*) on every request.


Contact Lists

GET /api/v1/contact-lists

Returns all contact lists for the authenticated account.

Response — 200 OK

json
[
  {
    "id": "<uuid>",
    "name": "VIP Customers",
    "description": "Customers who spend over KES 10,000/month",
    "contactCount": 42,
    "createdAt": "2025-01-01T00:00:00.000Z"
  }
]

POST /api/v1/contact-lists

Creates a new contact list.

Request Body

json
{
  "name": "Newsletter Subscribers",
  "description": "Opted-in newsletter audience"
}
FieldRequiredNotes
nameYes
descriptionNoShown in the UI beside the list name

Response — 201 Created — returns the created list object.


PATCH /api/v1/contact-lists/:id

Updates a contact list's name and/or description.

Request Body

json
{
  "name": "Newsletter Subscribers (updated)",
  "description": "Updated description"
}

Response — 200 OK — returns the updated list.


DELETE /api/v1/contact-lists/:id

Deletes a contact list. The contacts themselves are not deleted — they remain in the account and may still belong to other lists.

Response — 204 No Content


POST /api/v1/contact-lists/:id/contacts

Adds one or more contacts to a list (bulk add). Uses INSERT ... ON CONFLICT DO NOTHING — safe to call with contacts already in the list.

Request Body

json
{
  "contactIds": ["<uuid>", "<uuid>"]
}

Response — 200 OK

json
{
  "added": 2
}

DELETE /api/v1/contact-lists/:id/contacts

Removes one or more contacts from a list. The contacts themselves are not deleted from the account.

Request Body

json
{
  "contactIds": ["<uuid>"]
}

Response — 200 OK

json
{
  "removed": 1
}

Contacts

GET /api/v1/contacts

Returns paginated contacts for the account. Supports filtering and searching.

Query Parameters

ParamTypeNotes
pageintegerDefault: 1
pageSizeintegerDefault: 25, max: 100
searchstringSearches first name, last name, and phone
listIduuidFilter to contacts in a specific list
isOptedOutbooleanFilter by opt-out status
tagsstringComma-separated tag names. Returns contacts that have all specified tags

Response — 200 OK

json
{
  "contacts": [
    {
      "id": "<uuid>",
      "firstName": "John",
      "lastName": "Doe",
      "phone": "+254712345678",
      "email": "john@example.com",
      "tags": ["vip", "nairobi"],
      "notes": "Prefers Swahili",
      "isOptedOut": false,
      "listIds": ["<list-uuid-1>", "<list-uuid-2>"],
      "createdAt": "2025-01-01T00:00:00.000Z",
      "updatedAt": "2025-01-01T00:00:00.000Z"
    }
  ],
  "total": 150,
  "page": 1,
  "pageSize": 25
}

GET /api/v1/contacts/tags

Returns all distinct tag strings used across the account's contacts.

Response — 200 OK

json
["enterprise", "nairobi", "newsletter", "vip"]

GET /api/v1/contacts/:id

Returns a single contact by ID.

Response — 200 OK — same shape as a single item from the list above.

Errors

CodeCause
404 Not FoundContact not found or belongs to a different account

POST /api/v1/contacts

Creates a new contact and optionally adds them to one or more lists.

Request Body

json
{
  "firstName": "John",
  "lastName": "Doe",
  "phone": "+254712345678",
  "email": "john@example.com",
  "tags": ["vip"],
  "notes": "Met at Nairobi conference 2025",
  "listIds": ["<list-uuid>"]
}
FieldRequiredNotes
firstNameNo
lastNameNo
phoneYesMust be a valid international phone number
emailNo
tagsNoArray of strings
notesNo
listIdsNoUUIDs of lists to add the contact to

Response — 201 Created — returns the created contact.

Errors

CodeCause
422 UnprocessablePhone number fails validation

PATCH /api/v1/contacts/:id

Updates a contact. All fields optional. listIds performs a full sync — the contact's list memberships are replaced with the provided array.

Request Body

json
{
  "firstName": "John",
  "phone": "+254722000000",
  "tags": ["vip", "enterprise"],
  "listIds": ["<list-uuid-a>", "<list-uuid-b>"]
}

Response — 200 OK — returns the updated contact.


DELETE /api/v1/contacts/:id

Soft-deletes a contact (moves to trash). Sets deleted_at = NOW(). The contact disappears from active views but can be restored from the trash for 90 days.

Response — 204 No Content


POST /api/v1/contacts/bulk-delete

Soft-deletes multiple contacts at once.

Request Body

json
{
  "ids": ["<uuid>", "<uuid>"]
}

Response — 200 OK

json
{
  "deleted": 2
}

Trash

Soft-deleted contacts live in the trash for 90 days before being permanently removed.

GET /api/v1/contacts/trash

Returns all trashed contacts for the account.

Response — 200 OK — array of contact objects with deletedAt populated.


POST /api/v1/contacts/trash/restore

Restores one or more contacts from trash (clears deleted_at).

Request Body

json
{
  "ids": ["<uuid>"]
}

Response — 200 OK

json
{
  "restored": 1
}

DELETE /api/v1/contacts/trash

Permanently purges one or more trashed contacts. This is irreversible.

Request Body

json
{
  "ids": ["<uuid>"]
}

Response — 200 OK

json
{
  "purged": 1
}

Import & Export

POST /api/v1/contacts/import

Bulk-imports contacts from a CSV or Excel file. The file is parsed server-side and each phone number is validated against the mno_prefixes table synchronously at import time.

Content-Type: multipart/form-data

FieldTypeNotes
fileFile.csv or .xlsx
listIdstring (optional)Add all imported contacts to this list

Expected CSV columns (order flexible, matched by header name):

first_name, last_name, phone, email, tags, notes

Response — 201 Created

json
{
  "imported": 90,
  "valid": 87,
  "invalid": 3,
  "skipped": 2
}
FieldMeaning
importedTotal rows inserted (valid + invalid combined)
validContacts whose phone prefix matched a known operator (status: valid)
invalidContacts whose prefix was unrecognised — stored with status: invalid, invalid_reason: 'unknown_network'
skippedRows rejected entirely — duplicate phone number or unparseable format

POST /api/v1/contacts/bulk

JSON-body bulk import. Used by the frontend CSV import wizard after client-side column mapping and parsing.

Request Body

json
{
  "contacts": [
    {
      "firstName": "Jane",
      "lastName": "Doe",
      "phone": "+254712345678",
      "email": "jane@example.com",
      "tags": ["vip"],
      "notes": "Met at Nairobi conference"
    }
  ],
  "listId": "<optional-list-uuid>"
}

Response — 201 Created

json
{
  "imported": 90,
  "valid": 87,
  "invalid": 3,
  "skipped": 2
}

Same field semantics as POST /contacts/import.


GET /api/v1/contacts/export

Downloads all contacts (or contacts in a specific list) as a CSV file.

Query Parameters

ParamNotes
listIdExport only contacts in this list. Omit for all contacts

Responsetext/csv file download.

first_name,last_name,phone,email,tags,notes
John,Doe,+254712345678,john@example.com,vip;nairobi,Met at conference

GET /api/v1/contacts/export-data

Returns contacts as a JSON array for client-side export formatting. Used by the UI's export wizard (which lets users choose CSV vs. other formats and send by email before downloading).

Query Parameters

ParamNotes
listIdScope to a specific list. Omit for all contacts

Response — 200 OK — array of contact objects (same shape as GET /contacts items).


POST /api/v1/contacts/export-email

Sends a pre-formatted export file to an email address. The client builds the file (base64-encoded) and the API dispatches the email via the configured mail provider.

Request Body

json
{
  "email": "user@example.com",
  "filename": "contacts-export.csv",
  "fileBase64": "<base64-encoded-file-contents>",
  "mimeType": "text/csv",
  "count": 87
}

Response — 200 OK (no body)


Phone Validation

Validation runs in two passes:

1. Client-side format checkgoogle-libphonenumber (parseAndKeepRawInput + isValidNumberForRegion) confirms the number is structurally valid for its region before the form is submitted. Numbers that fail are rejected with an inline error.

2. Server-side MNO lookup — at import or single-contact creation time, the API normalises the phone (strips + / 00 IDD prefix, removes non-digits), then queries the mno_prefixes table using a longest-prefix-match algorithm. Contacts that match a known operator prefix receive status: 'valid'; those that do not are stored with status: 'invalid' and invalid_reason: 'unknown_network'. The lookup covers 53 African countries (151 prefixes).

Internal use only — Sema Link Engineering