Skip to content

Documentation

Build with Engage

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

Channels

A channel is a way a workspace talks to customers: a WhatsApp number it can send from, or an Instagram account it can answer on. Until a WhatsApp number is connected, nothing else in the product can do anything.

Owners only

Connecting, rotating and disconnecting a channel need the owner role, and so does setting the questions on an Instagram profile; a staff member gets 403. Listing channels and reading a profile are open to everyone in the workspace.

Connect a number you already own

POST /v1/channels

{
  "phoneNumberId": "…",
  "wabaId": "…",
  "accessToken": "…"
}
FieldTypeNotes
phoneNumberIdrequiredstringMeta's id for the number, from the WhatsApp Manager. Not the phone number itself.
wabaIdrequiredstringThe WhatsApp Business Account the number belongs to.
accessTokenrequiredstringA system user token with messaging permission. Stored encrypted and never returned.

Meta asks for a payment method, and that is not a charge

Meta requires a card on file on the WhatsApp Business Platform before a number can send, the way a bank wants your details before it will settle payments. Having one on file is not the same as being billed: your first 1,000 replies each month are free, and a shop that stays under that pays Meta nothing.

Since 1 October 2026 the card does more than settle bills. Without one, Meta delivers replies inside the free thousand and then stops delivering them for the rest of the month. Engage shows whether a number has a payment method on Settings › Channels for exactly this reason.

Meta bills the business directly for anything chargeable. It does not pass through Engage, so there is no markup to ask about.

The token is encrypted and unrecoverable

Credentials are stored encrypted and no endpoint ever returns one. If you lose the token, generate a new one in Meta and rotate — it cannot be read back out.

Meta shows a system user token once. Put it somewhere safe before you leave that page.

Embedded Signup

POST /v1/channels/whatsapp/embedded-signup

The other route: a business connects their own number through Meta's flow without ever handling a token by hand. It requires Tech Provider status on the Meta app; without it, use the manual route above.

POST /v1/channels/whatsapp/embedded-signup

{
  "code": "…",
  "wabaId": "…",
  "phoneNumberId": "…",
  "coexistence": true
}

Keeping the WhatsApp Business app

Most businesses already run on the WhatsApp Business app and will not give it up to gain an API - and Meta refuses a number registered to it outright. Coexistence lets the same number serve both: they keep answering on their phone, and Engage sees and sends on the same threads.

Send coexistence: true when the business chose that route in Meta's dialog. It changes what happens on this side: the number is already registered to the app, so Engage skips registration, and it asks Meta for the contacts and conversation history the app holds.

What a business gives up

WhatsApp Web and Desktop stop working for that number, and all companion devices are unlinked. Throughput is fixed at 20 messages a second. Broadcast lists become read-only, and disappearing messages, view-once and live location are switched off in one-to-one chats. The app must be on version 2.24.17 or newer.

Replies the owner types on their phone arrive as ordinary messages in the thread, marked as sent by a person. The AI will not answer a question somebody has already answered, whoever answered it and from wherever.

History arrives afterwards, in chunks, covering up to 180 days. Each message keeps its own date, so the thread reads in the order it happened. Nothing in it triggers a workflow, an AI reply or a reply window - a question from six months ago is not waiting for an answer. Media older than 14 days is not included by Meta and shows as an attachment that was not saved.

Connect an Instagram account by signing in

The route a shop owner can actually finish. They sign in to Instagram, approve, and come back connected — where the token route below asks them to generate a sixty-day credential in a developer dashboard, which most will not do and should not have to.

GET /v1/channels/onboarding carries instagramAuthorizeUrl. Send the business there; Instagram returns them to the registered redirect with a code on the URL, and that code is posted back:

curl -X POST https://api.engentra.io/v1/channels/instagram/business-login   -H "X-API-Key: $ENGAGE_API_KEY"   -H "Content-Type: application/json"   -d '{ "code": "AQB…" }'

Always take the sign-in URL from /v1/channels/onboardingrather than building your own — it carries the permissions this app is approved for, and a hand-built one will be refused.

The response is the same as the token route: the channel, and the username it connected.

A code is single-use and lives for seconds

Exchange it as soon as it arrives, once. A retry with the same code fails for a different reason than the first attempt did, which sends whoever is debugging it to the wrong place. If it has expired, start the sign-in again — that always works.

If instagramAuthorizeUrl comes back null, signing in is not available and the token route below is the way to connect.

Connect an Instagram account with a token

curl -X POST https://api.engentra.io/v1/channels/instagram \
  -H "X-API-Key: $ENGAGE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "accessToken": "IGAA…" }'

One field, because there is only one thing to send. The account id is read back from the token by the API rather than taken from the caller — which is what stops a credential being filed under an account it does not belong to. The response carries the channel and the username it reached, so you can show the business which account they actually connected.

FieldTypeNotes
accessTokenrequiredstringA long-lived Instagram user access token for a Professional account (Business or Creator) with messaging permissions. Proved against Instagram before anything is stored.

Nothing is subscribed per account here. Unlike a WhatsApp Business Account, the webhook subscription belongs to the Meta app and is configured once for every business at a time, so there is no per-connection call to make.

Instagram cannot be messaged first

A conversation on Instagram has to be started by the person. There is no template to reopen a closed thread, and the reply window is 24 hours from their last message — after that, nothing can be sent until they write again.

So campaigns, workflow sends and follow-ups are WhatsApp. Instagram is the inbox half: DMs, story replies and story mentions arrive, and your team or the AI answers them. A workflow step that tries to send on Instagram is refused rather than queued.

Someone who messages you on both channels is two customers until you say otherwise. Merging them is on the customer's page, and Customers covers what moves and what does not.

Connected, but no messages arriving?

GET /v1/channels/{channelId}/subscription shows what Meta will actually send for a channel. An empty list means nothing will ever be delivered for it.

Rotating the credential fixes it — use Replace token in Settings › Channels, or PUT /v1/channels/{channelId}/credentials.

An Instagram token expires; a WhatsApp one does not

A long-lived Instagram token lasts sixty days. Engage renews it automatically with ten days to spare and stores the new expiry on the channel as credentialExpiresAt, so in the ordinary case there is nothing to do.

It is worth knowing what failure looks like, because it is quiet. Webhooks are authenticated by signature rather than by the token, so when one dies messages keep arriving and only sending breaks — a business receiving customers it cannot answer, with the channel still showing Connected. Settings › Channels says so once a token is inside the renewal window, and Instagram cannot renew one that has already expired: that needs a new token from Meta and PUT /credentials.

Comments, answered privately

“How much?” under a photo, forty times. It is what a social-commerce business spends its morning on, and the only Instagram door that starts in public — the comment is visible to everybody and the answer should not be, because a price under a post is a price every competitor can read and every other commenter will then expect.

GET  /v1/instagram/comments
POST /v1/instagram/comments/{id}/reply   { "text": "It is N18,000 - shall I hold one?" }

The list is what nobody has answered, newest first. Comments past seven days are still in it with withinReplyWindow false, marked rather than hidden: somebody looking at last week needs the difference between “not done” and “cannot be done”, and a row that vanished at the seven-day mark would look like a bug. Both calls are open to every member — answering comments is the work, and making it an owner’s job would mean the person doing it cannot.

The reply is addressed to the comment, not to a person, because until it is sent there is no messageable id for somebody who has only commented. So the reply is what earns one: it creates the customer and the thread, dated from when they commented, and everything after it is an ordinary Instagram conversation inside the ordinary 24 hours. It also raises comment.replied.

One private reply per comment, ever

Not one at a time — one. Meta refuses the second with an error that does not say so, so Engage refuses it first with 409 and words you can show somebody. After that, the rest of the conversation happens in the thread it opened.

Comments are stored whether or not anybody answers them, because the unanswered ones are the backlog and that list is most of the value. This needs Meta's instagram_manage_comments permission, which is a separate App Review from messaging.

Questions to offer first

The only opening move Instagram allows. You cannot message first, so every conversation starts with the customer — and the one thing you can do about that is hand them the question. Up to four, shown above an empty thread, set under Settings › Channels or here:

curl -X PUT https://api.engentra.io/v1/channels/{channelId}/profile   -H "X-API-Key: $ENGAGE_API_KEY"   -H "Content-Type: application/json"   -d '{
    "iceBreakers": [
      { "question": "What are your prices?", "payload": "PRICES" },
      { "question": "Do you deliver?", "payload": "DELIVERY" }
    ],
    "menuLinks": [ { "label": "Shop", "url": "https://adafabrics.example" } ]
  }'

payload is a short name of your own, and it is the reason these are worth setting rather than decorative. When a customer taps one it comes back on a conversation.question_tapped event, so a workflow can answer “Do you deliver?” without a person reading it — and it keeps working when you reword the question, which matching on the words would not.

A tap opens the 24-hour reply window exactly as a typed message does. The thread shows the question they chose, not the payload: a thread full of DELIVERY is a thread nobody can answer.

Saving replaces all of them

Meta has no way to change one question, so sending iceBreakers replaces every one. Sending an empty list takes them off your profile, which is how you remove questions you should not have put up.

GET on the same path reads them back from Meta rather than from a copy here — somebody can change them in the Instagram app, and two answers to one question is the disagreement nobody notices until you insist you set something you cannot see. Meta enforces its own limits on length, and its refusal is passed through word for word because it names the real problem.

Where your Instagram customers come from

Since you cannot message first, the one thing you control is where the door is. An ig.me link opens a Direct thread with your account, and a ref on it comes back with the customer's first message:

https://ig.me/m/adafabrics?ref=packaging-qr

Settings › Channels builds these, using the username read from your token so there is nothing to type wrong. Put a different ref on the packaging, the newsletter and the shop poster, and Results shows which of them produced the sales. Ads need no setup — a click-to-Direct ad carries its own id.

What is connected

GET /v1/channels

Active and disconnected channels of every type — numbers and Instagram accounts on one list. type says which, and it is worth reading rather than assuming: providerAccountId is a phone number id on WhatsApp and an Instagram account id on Instagram, displayName is the @username on Instagram and null on WhatsApp, and qualityRating is a WhatsApp idea that is null everywhere else. Never the credential. Deleted connections are excluded.

Quality rating, and the marketing pause

Meta rates every WhatsApp number green, yellow or red from how the last seven days of messages were received - blocks and reports pull it down. A number that stays low gets its messaging limit cut. Engage reads the rating hourly, and at once when Meta's webhook says it changed; it is qualityRating on the channel, with qualityCheckedAt.

While a number is yellow or red, Engage refuses marketing templates from it - workflows, the inbox and the API alike - with the reason in the error and in the workflow run. Order updates, shipping updates and replies inside the 24-hour window carry on. When Meta rates the number green again, marketing resumes by itself. Messages that were due while it was paused are not sent late.

Owners get an email when the rating drops and when it recovers, and every screen of the dashboard says marketing is paused until it does.

Subscribe the webhook field

For the immediate check, subscribe your Meta app's webhook to phone_number_quality_update. Without it the hourly check still catches every change, up to an hour later.

Rotate a credential

PUT /v1/channels/{channelId}/credentials

{
  "accessToken": "…",
  "wabaId": "…"
}

wabaId is optional — send it only if the number moved to a different WhatsApp Business Account. Meta tokens expire and get revoked. Rotating replaces the stored credential in place, so the channel, its conversations and its history all stay attached — reconnecting as a new channel would orphan every thread on it.

The same endpoint rotates an Instagram credential, and sending wabaId with one is simply ignored. The new token is proved against the account the channel already holds: a token for a different Instagram account is refused, because it is a working token and would otherwise leave the channel sending as one account while still receiving as another.

Disconnect

DELETE /v1/channels/{channelId}

Stops sending and receiving. The conversations and messages stay, because they are a record of what happened rather than a property of the connection.

One number, one workspace

A provider account can be live on only one channel at a time. Two workspaces pointed at the same WhatsApp number would each receive half its inbound messages, depending on which channel a lookup happened to find — a failure that would look like messages going missing at random.

Delete a connection

DELETE /v1/channels/{channelId}/connection

Owners can permanently remove a disconnected channel from Settings. Disconnect it first: deleting an active connection returns 409. If it is the default and another account is active, select that account as default before disconnecting. Success returns 204; another workspace's channel returns 404.

This clears the saved credentials and registration PIN, removes the connection from the channel list, and releases its account ID for fresh signup. An inert historical record remains so conversations and messages are not erased. Old conversations cannot send through it, and workflows or campaigns explicitly using the deleted channel must be updated. Adding the account again creates a new channel; it does not revive the old conversations. Use Reconnect instead when you want to retain the existing connection and continue its threads.

Deletion here does not delete the number or account from Meta, revoke Meta permissions, or unsubscribe a shared WhatsApp Business Account. Manage those separately in Meta.

Next

Templates →

The messages you may send outside the 24-hour window.