Build your own inbox
An inbox is a list of cases. A case is one conversation with one contact inside a pipeline.
Caseobject, 24 fields
objectenumrequiredcaseidstringrequiredcontactIdstringrequiredpipelineIdstringrequiredstageIdstring or nullrequiredagentIdstring or nullrequiredThe agent that answers the case: the copilot of a copilot case, otherwise the agent of the current stage.
statusenumrequiredopenclosedassigneeIdstring or nullrequiredchannelenumrequiredWhere the contact talks: whatsapp, email, api (your own system) or in_app. null when the case has no channel to write back.
whatsappapiin_appemailnullphoneNumberIdstring or nullrequiredThe WhatsApp number the contact wrote to, when it is a WhatsApp case.
controlenumrequiredWho answers the contact. human also covers a case the agent escalated and nobody took yet: see agentState.
agenthumanagentStateenumrequiredWhat the agent is doing, to drive a typing indicator: thinking and replying mean it is working; waiting_for_contact means it answered; escalated means it asked for a person; idle is everything else.
idlethinkingreplyingwaiting_for_contactescalatedunreadintegerrequiredMessages of the contact since the last POST /v2/cases/{id}/read.
lastMessageAtstring or null (date-time)requiredlastMessageLastMessage or nullrequiredA preview of the last public message.
LastMessage3 fields
authorenumrequiredcontactagentteamtypestringrequiredtextstring or nullrequiredThe first 140 characters. null when the message is a file.
contactCaseContactoptionalThe contact with its presence. Present only on GET /v2/cases and GET /v2/cases/{id}, when the organization setting presenceEnabled is on, at least one presence signal has arrived, and the key has the contacts:presence-read scope.
CaseContact2 fields
idstringrequiredpresencePresencerequiredPresence2 fields
onlinebooleanrequiredtrue when the last active signal is less than 120 seconds old and no offline signal came after.
lastSeenAtstring or null (date-time)requiredThe last time a signal arrived. While online it is exact; once offline it can be up to 5 minutes behind. null when no signal ever arrived for this contact.
outcomestring or nullrequiredclosedAtstring or null (date-time)requiredfieldsobjectrequiredThe values of the fields the pipeline declares, by key. Sensitive fields are not included.
tagsstring[]requiredThe tags of the contact of the case.
archivedAtstring or null (date-time)requiredWhen the case was archived: out of the board, state untouched. null when it is on the board.
mutedbooleanrequiredtrue while the contact is muted: the agent does not answer them and their open cases are closed. Muting belongs to the person, so every case of the contact shows it.
createdAtstring (date-time)requiredupdatedAtstring (date-time)required
Every case points to a contact, and every message belongs to a case:
Contactobject, 16 fields
objectenumrequiredcontactidstringrequirednamestringrequiredemailstring or nullrequiredphonestring or nullrequiredexternalRefstring or nullrequiredcompanyIdstring or nullrequiredThe company of this contact, or null when it has none.
tagsstring[]requiredfieldsobjectrequiredThe values of the contact fields this contact has, by key. Empty when it has none.
createdAtstring (date-time)requiredupdatedAtstring (date-time)requiredpresencePresenceoptionalWhether the contact is online. Present only when the organization setting presenceEnabled is on, at least one presence signal has arrived, and the key has the contacts:presence-read scope.
Presence2 fields
onlinebooleanrequiredtrue when the last active signal is less than 120 seconds old and no offline signal came after.
lastSeenAtstring or null (date-time)requiredThe last time a signal arrived. While online it is exact; once offline it can be up to 5 minutes behind. null when no signal ever arrived for this contact.
matchobject or nulloptionalOnly when you search with q: the field of the contact that matched and its value, or null when the match was the name, email, phone or externalRef. Fields marked sensitive are never searched or shown.
match2 fields
Only when you search with q: the field of the contact that matched and its value, or null when the match was the name, email, phone or externalRef. Fields marked sensitive are never searched or shown.
fieldstringoptionalvaluestringoptional
caseCountintegeroptionalOnly when you search, filter by pipeline or open cases, or sort: how many cases the contact has.
openCaseCountintegeroptionalSame as caseCount, only the open ones.
lastActivityAtstring (date-time)optionalSame as caseCount: the latest message of its cases, or its creation date.
Messageobject, 18 fields
objectenumrequiredmessageidstringrequiredcaseIdstringrequiredauthorenumrequiredcontactagentteamauthorNamestring or nullrequiredvisibilityenumrequiredpublicinternaltextstringrequiredcontentTypestringrequiredattachmentsMessageAttachment[]requiredMessageAttachment4 fields
typestringrequiredmimeTypestringrequiredurlstringrequiredA signed link. Open it before expiresAt.
expiresAtstring (date-time)required
interactiveInteractive or nulloptionalThe buttons, list, product or catalog the message carried. null on any other message.
InteractiveButtonsMessage6 fields
typestringrequiredbodystringrequiredThe text of the message. Required for buttons, list and product_list.
headerstringoptionalText above the body. Required for product_list.
footerstringoptionalheaderMediaInteractiveMediaHeaderoptionalAn image, video or document above the body. Only for buttons and cta_url. Do not send it together with header.
InteractiveMediaHeader2 fields
typeenumrequiredimagevideodocumenturlstringrequiredA public https link WhatsApp can download.
buttonsInteractiveButton[]requiredOnly for type buttons: 1 to 3 buttons.
InteractiveButton2 fields
idstringrequiredComes back as reply.id when the person taps it. Unique within the message.
titlestringrequired
replyMessageReply or nulloptionalSet when the person tapped a button or a list row: the id and title they tapped. null otherwise.
MessageReply2 fields
idstringrequiredtitlestringrequired
orderMessageOrder or nulloptionalSet when the person sent a cart from the WhatsApp catalog. text carries their note. null otherwise.
MessageOrder2 fields
catalogIdstringrequireditemsMessageOrderItem[]requiredMessageOrderItem4 fields
productIdstringrequiredquantitynumberrequiredpricenumberrequiredUnit price, as the catalog has it.
currencystringrequired
locationMessageLocation or nulloptionalSet when the person shared a location (answering a location_request or on their own). text carries it as a sentence the agent reads. null otherwise.
MessageLocation4 fields
latitudenumberrequiredlongitudenumberrequirednamestringoptionaladdressstringoptional
flowResponseMessageFlowResponse or nulloptionalSet when the person finished a WhatsApp Flow: the fields they filled in. null otherwise.
MessageFlowResponse2 fields
tokenstringoptionalThe token you sent with the Flow, if any.
dataobjectrequiredThe fields the person filled in the last screen of the Flow.
contactMessageContact or nulloptionalSet when the person shared their phone answering a contact_request. A contact card the person forwards on their own is not kept. null otherwise.
MessageContact1 field
phonesMessageSharedPhone[]requiredMessageSharedPhone2 fields
phonestringrequiredwaIdstringoptional
errorMessageError or nulloptionalSet when status is failed: why. null on any other status. Read it to know whether to retry, send a template (outside the 24-hour window) or stop.
MessageError2 fields
codenumber or nullrequiredThe WhatsApp error code when WhatsApp rejected the message. null when the platform stopped it before sending.
messagestringrequiredWhy the message failed.
statusenumrequiredDelivery to the channel of the case.
pendingsentdeliveredreadfailedcreatedAtstring (date-time)required
List what needs attention
Keep the list fresh with the message.created, case.control_changed and case.escalated webhooks instead of polling.
Show what the agent is doing
agentState on the case drives a typing indicator.
agentState | Meaning |
|---|---|
thinking, replying | The agent is working |
waiting_for_contact | The agent answered |
escalated | The agent asked for a person |
idle | Anything else |
control says who answers the contact: agent or human. human also covers a case the agent escalated that nobody took yet.
Let a person answer
Take control, write as the team, and hand the case back.
Writing as team needs cases:send. While the agent is in control the call answers 409 case_agent_in_control, so a contact never gets two answers at once. On WhatsApp, free text more than 24 hours after the contact's last message answers 422 channel_window_closed; send a template instead.
Send buttons, lists and products
On a WhatsApp case, interactive replaces text. The person answers with one tap, and the tap arrives as a message of the contact with reply: { id, title }.
type | Content field | Limits |
|---|---|---|
buttons | buttons | 1 to 3 buttons, titles up to 20 characters |
list | list | up to 10 sections and 10 rows in total, titles up to 24 characters |
product | product | one product of the catalog connected to the number |
product_list | products | up to 30 products in up to 10 sections, with header and body |
catalog | catalog | opens the whole catalog |
cta_url | cta | one button that opens an https link |
flow | flow | a button that opens a WhatsApp Flow |
location_request | none | asks the person to share a location |
contact_request | none | asks the person to share their phone |
call | call | a button that starts a WhatsApp call |
carousel | carousel | 2 to 10 swipeable cards with an image or video |
The catalog is connected by the business in Meta Commerce Manager and WhatsApp Manager, not through the API. Save its id on the number with catalogId: a product, product_list or catalog message answers 422 catalog_not_connected when the number has no catalog or the message names another one. productId is the id of the product in that catalog. The WhatsApp guide has the limits of each format and the commerce settings.
A message that breaks a limit answers 400 invalid_request with param set to the field, for example interactive.buttons. Send interactive or text, never both. Interactive messages follow the same rules as free text: cases:send, a person in control and the 24 hours window. On a case that is not on WhatsApp the answer is 422 interactive_unsupported_channel.
When the person sends a cart from the catalog, it arrives as a message with order: { catalogId, items }, and message.received carries type: "order". A shared location arrives as location, the answer of a Flow as flowResponse and a phone shared for a contact_request as contact; the event carries only their type (location, flow_response, contact) and you read the data in the message.
A message whose status is failed has error: { code, message } with the reason: WhatsApp's code, or null when the platform stopped it before sending.
Leave a private note
A message with "visibility": "internal" is a note only your team sees. It never reaches the contact or the agent and needs only cases:write.
A note signs with the name of the API key. Send authorName to sign it with the person who wrote it:
A message from the contact ("author": "contact") in a stage without an agent is saved when a person has taken control of the case. If nobody has, it answers 422, because nobody would answer. POST /v2/cases with only agentId answers 422 on pipelineId when that agent attends the entry stage of more than one pipeline: send pipelineId or stageId too. case.control_changed carries contactId and externalRef.
Tag, fill and reveal
tags and fields change with PATCH /v2/cases/{id}. Tags replace the whole list and belong to the contact, so the contact and its other cases show the same ones. fields writes the fields the pipeline declares, whoever fills them (the team, the channel, your system or the agent). A key the pipeline does not declare answers 400 invalid_request with param: "fields" and nothing is saved.
A field marked sensitive is written but never read back: the case, the webhooks and the agent do not carry it. To see it, reveal it with a key that has cases:reveal, a scope of its own that cases:read and cases:write do not include.
Every reveal writes who looked, the case and the field into an audit trail before the value is returned. The trail never holds the value. The response is not kept for an idempotent replay and not in the request log, so a retry reveals again and is audited again. A field that is not sensitive, or that the pipeline does not declare, answers 422 field_not_sensitive. It is the same audit trail the dashboard writes to when a person reveals a field.
Pin a note
PATCH /v2/cases/{id}/notes/{noteId} takes pinned. A case has one pinned note, so pinning one unpins the one that was pinned. Send content, pinned or both.
Archive, move and change pipeline
archive takes a case off the board without deleting it or changing its status, and unarchive brings it back. Both are safe to repeat. The case carries archivedAt, and GET /v2/cases?archived=true lists the archived ones.
move takes stageId or pipelineId, never both. With pipelineId the case goes to the entry stage of that pipeline, with the same checks as a move to a stage of another pipeline.
POST /v2/cases/move-batch moves up to 100 cases of one pipeline to a stage of that pipeline. It does not fail as a whole: the ones that can move move, and notMoved gives the reason for each of the others.
reason | Meaning |
|---|---|
not_found | No such case in your organization |
not_in_pipeline | The case is in another pipeline. Use move with pipelineId |
archived, closed | The case cannot move |
already_in_stage | Nothing to do |
required_fields_missing | The stage needs fields the case lacks. missingFields has their keys |
Mute and hand to a person
mute silences the contact of the case: the agent stops answering and every open case of the contact closes. Muting belongs to the contact, so every case of the person shows muted: true until unmute. The cases it closed stay closed.
escalate marks the case as needing a person, with a reason your team reads. It fires case.control_changed with escalated: true, and the case answers control: "human". case.escalated is the event of an agent handing a case over, so it does not fire here. A closed case answers 409.
Read the history of a person
The cases of a person are GET /v2/cases?contactId=ctc_..., and the messages of each one are in GET /v2/cases/{id}/messages. The events you reported for the person are GET /v2/contacts/{id}/events, newest first, with what each one sent or why it did not.
Remind your team
A reminder is a task for a member, due at a time. The assignee sees it in their tasks in the dashboard and gets the notice when it is due, like one a person created there.
GET /v2/cases/{id}/reminders?status=active lists the ones still to do. assigneeId has to be an active member (400 on assigneeId otherwise). PATCH /v2/reminders/{id} edits it, dismiss drops it but keeps it in the list, and DELETE removes it. Creating and editing need cases:write, reading needs cases:read.
Other case actions
move changes the stage or the pipeline, assign sets the assignee (take the mbr_ id from GET /v2/members, which needs members:read), close and reopen change the status, and read clears unread. Attachments are uploaded to /v2/cases/{id}/attachments and sent by passing attachmentIds in a message.