Contacts and groups
The contact book (everyone across your groups, by phone) and contact groups, with custom fields, opt-out status and history, search and export.
Contacts live in groups (the older "contact lists"; /v1/contact-lists
keeps working). The contact book shows each person once, across all groups,
addressed by E.164 phone (+256772123456; URL-encode the + as %2B if your
client requires it). Custom fields are stored per contact and are available as
merge fields when you send to a group. Keys need send to write and query to
read.
Contact book
| Method | Path | Description |
|---|---|---|
GET | /v1/contacts | Search: search (phone or name), group_id, opted_out (true / false), page, limit (max 200). Each item: phone, name, groups[], fields, opted_out, created_at. |
POST | /v1/contacts | {phone, name?, fields?, group_ids: [...]} - add to groups, or update where it already exists (fields are merged). |
GET | /v1/contacts/{phone} | Fields, groups, opted_out, opt_out details, opt_out_history (STOP / START events) and the 20 most recent messages. |
PATCH | /v1/contacts/{phone} | {phone?, name?, fields?} - updates every row of that number; fields are merged. |
DELETE | /v1/contacts/{phone} | Removes the number from every group (204). |
GET | /v1/contacts/export | All contacts, or group_id=, as format=csv (default) or xlsx: phone, name, group, opted_out, custom fields, added_at. |
A numeric path segment under 10 digits (/v1/contacts/123) addresses a single
group row by id, as the older API did.
Local numbers like 0772... are read in your workspace default country (or
your signup country). Invalid numbers are refused with the reason
(not_a_number, invalid_number, missing_country_code, landline, ...).
Groups
| Method | Path | Description |
|---|---|---|
GET | /v1/groups | List (q filters by name). |
POST | /v1/groups | {name, tags?} - an empty group. |
GET | /v1/groups/{id} | id, name, count, columns, tags. |
PATCH | /v1/groups/{id} | {name?, tags?}. |
DELETE | /v1/groups/{id} | Deletes the group and its rows. |
GET | /v1/groups/{id}/contacts | search, page, limit (max 500); each row has opted_out. |
POST | /v1/groups/{id}/contacts | {contacts: [{phone, name?, fields?}], default_country?} - up to 10,000; returns added and rejected (with reasons, including duplicate). |
DELETE | /v1/groups/{id}/contacts/{phone} | Remove one number from the group. |
For large files use Imports and
POST /v1/imports/{id}/save-to-group.
Opted-out numbers stay in your groups (so you can see their history) but are never messaged: every send and campaign checks the opt-out list, including right before each batch.
Example
curl -X POST https://sms.esmsafrica.io/v1/groups \
-H "Authorization: Bearer $ESMS_API_KEY" -H "Content-Type: application/json" \
-d '{"name": "VIP customers"}'
curl -X POST https://sms.esmsafrica.io/v1/groups/12/contacts \
-H "Authorization: Bearer $ESMS_API_KEY" -H "Content-Type: application/json" \
-d '{"default_country": "KE", "contacts": [{"phone": "0712345678", "name": "Ada", "fields": {"tier": "gold"}}]}'
curl "https://sms.esmsafrica.io/v1/contacts/%2B254712345678" -H "Authorization: Bearer $ESMS_API_KEY"// SDK 1.2.0 or later
const g = await esms.groups.create("VIP customers");
await esms.groups.addContacts(g.id, [{ phone: "0712345678", name: "Ada", fields: { tier: "gold" } }], "KE");
const person = await esms.contacts.get("+254712345678");
await esms.contacts.update("+254712345678", { fields: { tier: "platinum" } });# SDK 1.2.0 or later
g = esms.groups.create("VIP customers")
esms.groups.add_contacts(g["id"], [{"phone": "0712345678", "name": "Ada", "fields": {"tier": "gold"}}], default_country="KE")
person = esms.contacts.get("+254712345678")
esms.contacts.update("+254712345678", fields={"tier": "platinum"})// SDK 1.2.0 or later
$g = $esms->groups->create('VIP customers');
$esms->groups->addContacts($g['id'], [['phone' => '0712345678', 'name' => 'Ada', 'fields' => ['tier' => 'gold']]], 'KE');
$person = $esms->contacts->get('+254712345678');
$esms->contacts->update('+254712345678', ['fields' => ['tier' => 'platinum']]);// SDK 1.2.0 or later
g, _ := client.Groups.Create(ctx, "VIP customers", nil)
gid := int(g["id"].(float64))
client.Groups.AddContacts(ctx, gid, []esms.GroupContact{{Phone: "0712345678", Name: "Ada", Fields: map[string]string{"tier": "gold"}}}, "KE")
person, _ := client.Contacts.Get(ctx, "+254712345678")
client.Contacts.Update(ctx, "+254712345678", esms.JSON{"fields": map[string]string{"tier": "platinum"}})