API WhatsApp

Conversations

Read the shared WhatsApp inbox over the StartMessaging API: list conversations, check the 24-hour window, read message history, and assign or resolve chats.

Overview

A conversation is the thread between your workspace and one customer’s WhatsApp number: the same threads your team works in the dashboard’s shared inbox. The API lets your own systems read them, check whether the 24-hour window is open, read the message history, and assign, resolve or reopen a conversation.

Method & pathScopeWhat it does
GET /api/v1/conversationsconversations:readList conversations, with filters
GET /api/v1/conversations/{id}conversations:readGet one conversation
GET /api/v1/conversations/{id}/messagesconversations:readThe conversation’s messages, newest first
PATCH /api/v1/conversations/{id}conversations:writeAssign, unassign, resolve or reopen

Plans. Reading conversations and their messages works on every plan, Developer included: a Developer workspace reads the conversations its own sends start. Changing one (PATCH) is the shared inbox’s job and needs the team inbox on your plan (Team, Business, Enterprise and the free trial). Without it, PATCH answers 403 PLAN_FEATURE_REQUIRED with details.feature: "agent_inbox".

An API key sees the whole inbox, whoever a conversation is assigned to. Conversations are created by inbound messages and by sends; there is no endpoint to create one directly.

The conversation object

{
  "id": "3f6c2a1e-8b4d-4c7a-9e21-5d0b7f9a1c33",
  "contactId": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
  "contactPhone": "+919812345678",
  "contactWaUserId": "IN.13491208655302741918",
  "contactName": "Aarti Sharma",
  "status": "open",
  "assignedToUserId": "5b1d9e2c-7a3f-4c8e-b6d1-2f4a8c9e0b7d",
  "unreadCount": 2,
  "lastMessageAt": "2026-10-02T10:15:00.000Z",
  "lastInboundAt": "2026-10-02T10:15:00.000Z",
  "freeEntryPointExpiresAt": null,
  "windowOpen": true,
  "createdAt": "2026-09-28T08:00:00.000Z",
  "updatedAt": "2026-10-02T10:15:00.000Z"
}
FieldDescription
contactIdThe contact, or null when it has none (an old conversation, or one whose contact was deleted; it is linked again when that number writes in or is messaged).
contactPhoneThe customer’s WhatsApp number in E.164, or null for someone with a WhatsApp username whom Meta names only by their user id (contactWaUserId), until they share their number by talking to you.
contactWaUserIdMeta’s business-scoped user id for the customer, once Meta has sent one, or null. Stable for your business; it changes only if they change their number.
contactNameThe name WhatsApp or your team recorded, or null.
statusopen or resolved. A new message from the customer reopens a resolved conversation automatically.
assignedToUserIdThe member handling it, or null while it waits in the unassigned queue.
unreadCountCustomer messages nobody on your team has opened yet.
lastMessageAtLatest message in either direction.
lastInboundAtWhen the customer last wrote, or null if they never have.
freeEntryPointExpiresAtWhen Meta’s free entry point window ends, or null when there is none. It opens when the customer came from a Click-to-WhatsApp ad (or a Facebook Page button) and you replied in time; until it ends, Meta charges nothing for messages in this conversation, templates included. It does not open the 24-hour window: outside that, you still need an approved template.
windowOpenWhether WhatsApp’s 24-hour customer service window is open right now, computed when you read it. While open you may send any message; once closed, only an approved template.
updatedAtLast change (a message, an assignment, a status change). What updatedSince filters on.

List conversations

GET /api/v1/conversations returns conversations with the newest activity first, in the { "items": [...], "nextCursor": "..." } list shape (limit 1–100, default 25).

Query parameterDescription
statusopen or resolved.
assignedToUserIdA member’s user id, or unassigned for the queue nobody has picked up.
contactPhoneThe customer’s number in E.164 (URL-encode + as %2B; a missing + is tolerated). Anything that is not a phone number is 400 MOBILE_INVALID. A conversation with a username-only customer has no number to filter on: match it by contactWaUserId in your own records.
updatedSinceIncremental sync: only conversations changed at or after this time, oldest change first. A date, or a date-time with a timezone (2026-10-01T10:00:00Z).
curl "https://waba-api.startmessaging.com/api/v1/conversations?status=open&assignedToUserId=unassigned" \
  -H "Authorization: Bearer sm_live_your_api_key"
const params = new URLSearchParams({ status: "open", assignedToUserId: "unassigned" });
const res = await fetch(`https://waba-api.startmessaging.com/api/v1/conversations?${params}`, {
  headers: { Authorization: "Bearer sm_live_your_api_key" },
});
const { data } = await res.json();
for (const c of data.items) console.log(c.contactPhone ?? c.contactWaUserId, c.windowOpen);
import requests

res = requests.get(
    "https://waba-api.startmessaging.com/api/v1/conversations",
    headers={"Authorization": "Bearer sm_live_your_api_key"},
    params={"status": "open", "assignedToUserId": "unassigned"},
)
for c in res.json()["data"]["items"]:
    print(c["contactPhone"] or c["contactWaUserId"], c["windowOpen"])

The updatedSince recipe is the same as for contacts: keep updatedSince unchanged while paging, upsert by id, store the updatedAt of the last row you processed, and start the next sync a few minutes before it. A conversation that changes while you page moves to the end of the walk, so it is not lost. See Pagination & Idempotency.

Get a conversation

curl "https://waba-api.startmessaging.com/api/v1/conversations/3f6c2a1e-8b4d-4c7a-9e21-5d0b7f9a1c33" \
  -H "Authorization: Bearer sm_live_your_api_key"

Check windowOpen before sending anything other than a template. An unknown id, or one from another workspace, is 404 CONVERSATION_NOT_FOUND.

List a conversation’s messages

GET /api/v1/conversations/{id}/messages returns the messages newest first, paged with limit and cursor.

{
  "id": "c4e1b2a3-9d8f-4e7a-b6c5-0a1b2c3d4e5f",
  "conversationId": "3f6c2a1e-8b4d-4c7a-9e21-5d0b7f9a1c33",
  "direction": "inbound",
  "type": "interactive",
  "status": "delivered",
  "text": "Yes, confirm order",
  "templateName": null,
  "media": null,
  "interactive": {
    "type": "button_reply",
    "body": null,
    "options": [],
    "reply": { "id": "cod_confirm", "title": "Yes, confirm order" }
  },
  "failureCode": null,
  "failureReason": null,
  "pricing": null,
  "senderId": null,
  "apiKeyId": null,
  "clientReference": null,
  "reply": { "kind": "button", "id": "cod_confirm", "title": "Yes, confirm order" },
  "context": { "messageId": "7d1e2f3a-4b5c-4d6e-8f9a-0b1c2d3e4f5a", "clientReference": "order-1042" },
  "form": null,
  "timestamp": "2026-10-02T10:15:00.000Z"
}
FieldDescription
directioninbound (from the customer) or outbound (from you).
typetext, template, image, video, audio, document, sticker or interactive. The same value GET /api/v1/messages/{id} and the message.received webhook give. Kinds the API does not model (location, contact card, reaction) are text with a bracketed placeholder such as [location].
statusqueued, sent, delivered, read or failed for messages you sent; inbound messages are delivered.
textThe body, a template’s filled-in text, a file’s caption or the tapped option’s label. null for an uncaptioned file.
mediatype, mimeType, filename, caption. Metadata only: received media is served only to signed-in dashboard users, so the API never returns a media URL.
interactiveButtons or a list you sent (type: button or list, body, options), or the customer picking one (button_reply or list_reply, with reply).
failureCode, failureReasonWhatsApp’s error code and explanation when status is failed.
pricingFor a message you sent, how Meta classified it once a receipt says so: category (such as marketing, utility, authentication, service) and type (regular when charged, free_customer_service, free_entry_point). Meta bills your WhatsApp Business Account directly; this is a record, not a StartMessaging charge. null for inbound messages and until the first receipt.
senderIdThe member who sent it from the inbox; null for inbound, automations, campaigns and API sends.
apiKeyIdThe API key that sent it, for API sends.
clientReferenceYour own reference, when the message was sent through the API with one.
replyWhat the customer tapped: kind (button, list or quick_reply), id and title. For a reply button or list row you sent, branch on id (the id you gave it). For a template quick-reply button you sent through the API, the API cannot set the id: match title, the button text as approved, and find your message with context.clientReference.
contextSet when the customer replied to a specific message (tapping a button on it counts): messageId (our id, or null when it is not this workspace’s message) and that message’s clientReference.
formA submitted WhatsApp Form: kind, flowToken, flowId, flowName, fields (each with key, label and value) and raw. null otherwise. See Webhooks.

Assign, resolve or reopen

PATCH /api/v1/conversations/{id} takes two fields. Anything else is 400 VALIDATION_ERROR.

FieldDescription
statusresolved closes the conversation, open reopens it.
assignedToUserIdA member’s user id (an active member who is not a Viewer), or null to return it to the unassigned queue. Otherwise 422 ASSIGNEE_NOT_MEMBER.
curl -X PATCH "https://waba-api.startmessaging.com/api/v1/conversations/3f6c2a1e-8b4d-4c7a-9e21-5d0b7f9a1c33" \
  -H "Authorization: Bearer sm_live_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{ "assignedToUserId": "5b1d9e2c-7a3f-4c8e-b6d1-2f4a8c9e0b7d", "status": "open" }'
await fetch(
  "https://waba-api.startmessaging.com/api/v1/conversations/3f6c2a1e-8b4d-4c7a-9e21-5d0b7f9a1c33",
  {
    method: "PATCH",
    headers: {
      Authorization: "Bearer sm_live_your_api_key",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ status: "resolved" }),
  }
);
requests.patch(
    "https://waba-api.startmessaging.com/api/v1/conversations/3f6c2a1e-8b4d-4c7a-9e21-5d0b7f9a1c33",
    headers={"Authorization": "Bearer sm_live_your_api_key"},
    json={"assignedToUserId": None},  # back to the unassigned queue
)
  • The change is recorded in the conversation’s history as made by your API key. A changed assignment fires conversation.assigned; resolving fires conversation.resolved. Reopening (status: "open") fires no webhook.
  • Setting a value the conversation already has changes nothing and fires no event, so retrying is safe.
  • An API key cannot claim a conversation or mark it read; those stay with agents in the dashboard.
  • A key with conversations:write but not conversations:read gets back only { "id": "..." }.

Conversation webhooks

Subscribe to these events (they need conversations:read). Both carry the conversation and an actor saying who made the change.

EventWhen
conversation.assignedThe assignment changed (assigned, reassigned or unassigned).
conversation.resolvedThe conversation was resolved (its status changed to resolved). Reopening it, by your team, the API or a new customer message, fires no event.
{
  "id": "evt_8a7b6c5d-4e3f-4a2b-9c1d-0e9f8a7b6c5d",
  "type": "conversation.assigned",
  "createdAt": "2026-10-02T10:20:00.000Z",
  "workspaceId": "...",
  "data": {
    "conversation": {
      "id": "3f6c2a1e-8b4d-4c7a-9e21-5d0b7f9a1c33",
      "contactId": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
      "contactPhone": "+919812345678",
      "contactWaUserId": null,
      "status": "open",
      "assignedToUserId": "5b1d9e2c-7a3f-4c8e-b6d1-2f4a8c9e0b7d"
    },
    "actor": { "type": "api_key", "id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d" }
  }
}

actor.type is member (a workspace member, with their user id), api_key (with the key id), staff (StartMessaging support, id may be null) or system (automatic: round-robin assignment or a chatbot flow, id is null). Use it to ignore changes your own integration made.

FAQ

How do I know if I can send a free-form message? Read the conversation and check windowOpen. It is computed at the moment you read it. When it is false, send an approved template with POST /api/v1/messages.

Can I download media a customer sent? No. The API returns media metadata (type, MIME type, file name, caption) only. Received files are available to signed-in agents in the dashboard.

Why do I get 403 PLAN_FEATURE_REQUIRED on PATCH /conversations? Assigning, resolving and reopening need the team inbox, on Team, Business, Enterprise and the free trial. Reading conversations works on every plan, Developer included.

Why is contactPhone null? The customer uses a WhatsApp username and Meta has given us only their user id (contactWaUserId). The number appears once they share it by talking to you.

Will my own PATCH trigger a webhook back to me? Yes, if it changed the assignment or resolved the conversation (reopening fires no event). The event’s actor is { "type": "api_key", "id": "..." }, so you can skip events your own key caused.

Can I create a conversation through the API? Not directly. Send a message with POST /api/v1/messages and the conversation (and contact) is created if needed.