Conversations
One thread per phone per keyword - list conversations with unread counts and the conversation-affinity window, read a thread, and mark it read.
A conversation is the thread between one phone and one of your keywords on one short
code. It is created the first time either side messages the other, and its
conversation_id is the same ID you get in every
message.inbound webhook and
send response, so you can key your own session state on it.
Each conversation also shows the affinity window: while affinity_active is true,
plain replies from that phone on a shared code (no keyword) are delivered to you. The
window runs for 72 hours (by default) from the last message your keyword sent to the phone.
If another keyword on the same code texts that phone more recently, plain replies go to it
instead, even though your window has not expired.
All conversation endpoints need service_id or keyword (plus code if you hold the
keyword on two codes). Scope: two_way or query.
List conversations
GET https://sms.esmsafrica.io/v1/two-way/conversationsMost recent activity first, cursor paginated.
| Parameter | Type | Description |
|---|---|---|
service_id / keyword | integer / string | Required: which keyword. |
code | string | With keyword, when you hold it on two codes. |
unread_only | boolean | Only conversations with unread inbound messages. |
cursor | string | next_cursor from the previous page. |
limit | integer | 1 to 200, default 50. |
Response
{
"data": [
{
"conversation_id": 981,
"service_id": 42,
"keyword": "CLINIC",
"code": "3450",
"phone": "+2348012345678",
"unread": 2,
"last_inbound_at": "2026-10-12T09:32:10+00:00",
"last_sent_at": "2026-10-12T09:30:40+00:00",
"last_read_at": null,
"affinity_active": true,
"affinity_expires_at": "2026-10-15T09:30:40+00:00",
"opted_out": false,
"last_message": {
"id": "a3c5e8f0-6b1d-4c2e-9f7a-1d2e3f4a5b6c",
"direction": "inbound",
"text": "2",
"created_at": "2026-10-12T09:32:10+00:00"
}
}
],
"has_more": false,
"next_cursor": null,
"total_unread": 7
}| Field | Description |
|---|---|
unread | Inbound messages since you last marked it read. |
last_inbound_at / last_sent_at / last_read_at | Last message from the phone, last message you sent, last time you marked it read. |
affinity_active | true while plain replies from this phone route to you (unless another keyword on the code texted it more recently). |
affinity_expires_at | When the window closes: last_sent_at plus the window. null if you never sent to this phone. |
opted_out | The phone opted out of your keyword or the whole code. Sends to it are refused. |
last_message | The latest message in either direction. |
total_unread | Unread messages across all conversations for this keyword. |
Examples
curl "https://sms.esmsafrica.io/v1/two-way/conversations?keyword=CLINIC&unread_only=true&limit=50" \
-H "Authorization: Bearer $ESMS_API_KEY"Get a conversation
GET https://sms.esmsafrica.io/v1/two-way/conversations/{phone}Inbound and outbound messages with one phone for a keyword, oldest first. Write the
phone in international format; the leading + is optional (2348012345678), or encode
it as %2B.
| Parameter | Type | Description |
|---|---|---|
service_id / keyword | integer / string | Required: which keyword. |
code | string | With keyword, when you hold it on two codes. |
limit | integer | 1 to 500, default 100 (the most recent messages). |
before | string | next_before from the previous response, to page further back. |
Response
{
"conversation": {
"conversation_id": 981,
"service_id": 42,
"keyword": "CLINIC",
"code": "3450",
"phone": "+2348012345678",
"unread": 2,
"last_inbound_at": "2026-10-12T09:32:10+00:00",
"last_sent_at": "2026-10-12T09:30:40+00:00",
"last_read_at": null,
"affinity_active": true,
"affinity_expires_at": "2026-10-15T09:30:40+00:00",
"opted_out": false,
"last_message": { "id": "a3c5e8f0-6b1d-4c2e-9f7a-1d2e3f4a5b6c", "direction": "inbound", "text": "2", "created_at": "2026-10-12T09:32:10+00:00" }
},
"messages": [
{
"id": "b7e1c2d4-0f3a-4c5b-8d6e-7f8091a2b3c4",
"service_id": 42,
"keyword": "CLINIC",
"code": "3450",
"direction": "inbound",
"from": "+2348012345678",
"to": "3450",
"phone": "+2348012345678",
"text": "CLINIC",
"body_without_keyword": "",
"matched_by": "keyword",
"status": "received",
"billed": true,
"cost": 0.0068,
"segments": 1,
"conversation_id": 981,
"created_at": "2026-10-12T09:30:00+00:00",
"auto_reply": false
},
{
"id": "6f1d2c3b-4a59-4e7d-8c6b-5a4f3e2d1c0b",
"service_id": 42,
"keyword": "CLINIC",
"code": "3450",
"direction": "outbound",
"from": "3450",
"to": "+2348012345678",
"phone": "+2348012345678",
"text": "CLINIC: Welcome! Reply 1 Emergency, 2 Maternity, 3 Pharmacy, 4 Talk to a nurse.",
"body_without_keyword": null,
"matched_by": null,
"status": "delivered",
"billed": true,
"cost": 0.0068,
"segments": 1,
"conversation_id": 981,
"created_at": "2026-10-12T09:30:40+00:00",
"auto_reply": false
}
],
"has_more": false,
"next_before": null
}Messages have the same fields as List messages,
plus auto_reply (true when your keyword's auto-reply sent it). Returns
404 not_found when you have never exchanged a message with that phone on this keyword.
Examples
curl "https://sms.esmsafrica.io/v1/two-way/conversations/2348012345678?keyword=CLINIC&limit=100" \
-H "Authorization: Bearer $ESMS_API_KEY"Mark a conversation read
POST https://sms.esmsafrica.io/v1/two-way/conversations/{phone}/readSets unread to 0. It does not change the order of your conversation list. Pass
service_id or keyword (and code if needed) as query parameters.
{ "phone": "+2348012345678", "unread": 0 }curl -X POST "https://sms.esmsafrica.io/v1/two-way/conversations/2348012345678/read?keyword=CLINIC" \
-H "Authorization: Bearer $ESMS_API_KEY"Service-scoped paths
The same three operations are available under a service ID, which is what the dashboard
uses. They take the same parameters (except service_id / keyword / code) and return
the same shapes.
| Method | Path |
|---|---|
GET | /v1/two-way/services/{service_id}/conversations |
GET | /v1/two-way/services/{service_id}/conversations/{phone} |
POST | /v1/two-way/services/{service_id}/conversations/{phone}/read |
curl "https://sms.esmsafrica.io/v1/two-way/services/42/conversations?unread_only=true" \
-H "Authorization: Bearer $ESMS_API_KEY"Related
Messages
POST /v1/two-way/messages sends FROM your keyword's short code and opens the conversation window; GET /v1/two-way/messages lists inbound and outbound messages with cursor pagination.
Webhooks
The message.inbound, two_way.opt_out and two_way.opt_in events - every field, headers, HMAC-SHA256 signature verification, full handlers in five languages, the 1m/5m/30m/2h/8h retry schedule, test events and the delivery log.