Skip to content

Documentation

Build with Engage

Connect customer data, business events, and communication channels to build automated journeys with workflows and AI.

Conversations

The WhatsApp thread with one customer. Read it, reply in it, or open one with somebody who has never written to you.

A user token, not an API key

These endpoints are not on the API key allowlist. Reading what customers said and replying to them is something a signed-in person does, not something a key in a server does — so they need an access token from POST /v1/auth/login.

List conversations

GET /v1/conversations

Newest activity first. Each conversation carries the state that decides what you may send next — see the 24-hour window. That is the field to render in any list you build: an operator needs to know whether they can type freely before they start typing.

Each row also carries customerName, customerPhone and lastMessage — a one-line preview, its sender and when it was sent — so a list can be drawn without a request per row.

Get one

GET /v1/conversations/{conversationId}

The same shape as a row of the list. Use it for a screen showing a single conversation — opened from a link, say — rather than searching pages of the list for it. A conversation in another workspace returns 404, exactly like one that does not exist.

channel on every conversation says which channel the thread is on — whatsapp or instagram — and it decides what you can do when the window closes. On WhatsApp a closed thread reopens with an approved template. On Instagram there is no template and no way to message first at all, so a closed thread waits for the customer. A screen that reads only withinReplyWindow will offer a template button on a thread that refuses it.

Read the messages

GET /v1/conversations/{conversationId}/messages

Messages in both directions. The sender distinguishes customer, human, ai and system — kept apart because somebody reading the thread later needs to know who actually said a thing, and because an AI reply that turns out to be wrong is a different conversation with the customer than a colleague's mistake.

Outbound messages carry a status: queued → sent → delivered → read, or failed. Meta does not guarantee webhook ordering, so the status only ever moves forward — a late delivered arriving after read is discarded rather than losing the stronger fact.

Reply

inside the window

POST /v1/conversations/{conversationId}/reply

{ "text": "Yes, we have it in stock." }

outside it

POST /v1/conversations/{conversationId}/reply-template

{
  "name": "order_ready",
  "language": "en_US",
  "variables": { "1": "Ada", "2": "50kg rice" }
}

name here, templateName when starting one

Replying with a template takes name. Starting a conversation, below, takes templateName. The two endpoints disagree, and sending the wrong one is a 400 saying a template name is required — which reads as though you sent nothing.

Both refuse rather than fail silently. Outside the window a plain reply comes back 400 with a message naming the reason, and a customer who has unsubscribed is unreachable through either — the check runs on every send, so there is no way around it.

A refusal is not an error in your code

“This conversation is outside its 24-hour reply window” and “this customer has unsubscribed” are answers, not faults. Show them to the operator as a reason with a way forward — offer the template picker — rather than as a red failure they cannot act on.

Send a file

curl -X POST https://api.engentra.io/v1/conversations/{id}/reply-media   -H "X-API-Key: $ENGAGE_API_KEY"   -H "Content-Type: application/json"   -d '{ "mediaId": "...", "caption": "Here it is in blue" }'

Upload the file once with POST /v1/media and send its id here. Files are kept, so the same price list goes to the twentieth customer without being uploaded a twentieth time — and a send refused for a closed window has not consumed an upload to find that out.

Meta fetches the file from a short-lived signed link rather than us uploading it to them, so this needs media storage configured; without it the call is refused rather than failing at Meta with something unreadable.

The caption behaves differently per channel

On WhatsApp it travels with the file. Instagram's attachment has no caption field, so it follows as a second message — two notifications rather than words silently dropped. Instagram also takes no documents at all, which is refused here rather than arriving as something the customer cannot open.

Show products from the catalog

curl -X POST https://api.engentra.io/v1/conversations/{id}/products   -H "X-API-Key: $ENGAGE_API_KEY"   -H "Content-Type: application/json"   -d '{ "productIds": ["...", "..."] }'

A customer asks what you have and the useful answer is pictures with prices. Up to ten products, and the response says what actually happened:

response

{
  "messages": [ { "id": "...", "messageType": "image", "content": "Blue ankara dress - NGN 18000.00" } ],
  "products": 1
}

On Instagram it is one message: Meta's generic template, a scrollable row of cards, with a View button on any product that has a URL. On WhatsApp it is one message per product — the carousel equivalent there needs a Meta commerce catalog linked to the business, which Engage does not assume. So messages comes back longer than one, and three notifications is a different thing to do to somebody than one. The dashboard says so before you send.

Products with no photo still go, as a line of text. Leaving one out of the answer without saying so would be worse.

Start one

The outbound-first path: messaging somebody who has not written to you today, and may never have written to you at all. It must be a template, because there is no open window to reply inside.

POST /v1/conversations

{
  "channelId": "…",
  "to": "+2348012345678",
  "name": "Ada Obi",
  "templateName": "order_ready",
  "language": "en_US",
  "variables": { "1": "Ada" },
  "optInSource": "checkout form, 12 Sept"
}
FieldTypeNotes
torequiredstringE.164. Enrols the customer if they are new.
templateNamerequiredstringMust already be approved and synced — see Templates.
optInSourcestringWhere this person agreed to be messaged. Recorded against the consent, and the thing you will want when somebody asks why they received this.

Handing over

POST /v1/conversations/{conversationId}/take-over
POST /v1/conversations/{conversationId}/hand-back

“I'm handling this.” Taking over marks the conversation as yours (assignedTo) and sets aiEnabled to false: automated messages to that customer pause, and a workflow's send is refused with the reason in its run history - the run carries on without it. Handing back turns automation on again. Each writes a note into the thread saying who did it. Any member can do either; taking over a conversation a teammate holds moves it to you.

Next

The 24-hour window →

The rule that decides whether a reply or a template is allowed.