Documentation Index
Fetch the complete documentation index at: https://docs.encorekit.com/llms.txt
Use this file to discover all available pages before exploring further.
Overview
If your platform automates outbound messages on WhatsApp, Messenger, or Instagram for your clients, Encore can supply one extra outbound message per conversation: a complimentary, brand-safe offer (Apple Music free trial, Disney+ trial, etc.) sent alongside the response your client already sends. The integration is one REST call; no SDK to install, no per-message webhook to host. You pass us the recipient identifier in the request. We return a fully-composed Graph API body with the recipient already populated — add your Authorization header and POST it tograph.facebook.com as-is.
When to call us
Two triggers, both gated by a max-ads-per-thread check on your side:- First message of a new conversation thread. Always eligible. Call Encore, send the offer message first, then your client’s normal automated reply as a follow-up.
- First message after a long idle gap (a “re-engagement” — the diner went quiet for some hours/days and returned). Also eligible, but only if you haven’t already shown the configured max number of Encore offers in the same thread.
encorekit.com domain. Scan the existing message history in the thread for outbound links to that domain and compare to your per-thread cap (e.g. 3). If you’re below the cap, call us; if at the cap, skip.
Sequence matters: send the offer message before your client’s reply, not after. The offer reads as a brief “while you wait” footer to the conversation rather than a postscript stapled onto the end.
When not to call
- Mid-thread replies — only first-of-thread and post-idle re-engagement triggers should fire an offer.
- The per-thread cap has been reached (counted via
encorekit.comlinks in the message history). - The end user has opted out (out of scope at v1; recipient-level cooldown is a planned feature).
Authentication
Pass your publishable key in theX-API-Key header. That’s the only auth header required.
pk_test_... against the sandbox project, pk_live_... against production. Same shape as our iOS / Android / Web SDKs.
Request
Try it with curl
Drop in your publishable key and run.inboundTimestamp is omitted — optional field, the request works without it.
| Field | Type | Required | Notes |
|---|---|---|---|
clientId | string | ✓ | Stable per-operator identifier — the business, account, or operator your platform is sending on behalf of (e.g. a restaurant ID). Forms half of the server-side dedup key alongside inboundMessageId. |
clientName | string | ✓ | Display name for the operator. Interpolated into the visible offer body — e.g. "{clientName} auto-replier just unlocked a special perk for you". Send the human-readable name ("Tony's Pizza"), not the ID. |
messagingPlatform | "whatsapp" | "instagram" | "facebook" | ✓ | Drives the response payload shape. |
recipient | string | ✓ | The end user’s identifier on the target platform: WhatsApp phone number in E.164 format (e.g. "15014005001"), Messenger PSID, or Instagram IGSID. Embedded in the returned payload so you can POST verbatim. |
inboundMessageId | string | ✓ | The platform-assigned message ID of the inbound message that triggered this call (WhatsApp wamid, Messenger mid, IG mid). Must be retry-stable on your side: a retry of the same inbound carries the same ID. Used server-side to derive a deterministic impression dedup key — see Idempotency. |
chatId | string | ✓ | Conversation / thread identifier on the messaging platform. Used as the messaging-surface userId — drives bandit seeding, the click-time transaction, and the userId dimension on the click event. Not part of the dedup hash. |
inboundTimestamp | integer | optional | Unix timestamp in seconds of the inbound message (from the Meta webhook’s messages[].timestamp field). Recorded for inbound→served latency analytics; omit if you don’t have it cheaply available — the request still works, the metric is just left null. |
userId | string | optional | Hashed end-user identifier (you choose the hash; we don’t dictate). Used for frequency capping in future iterations. Not part of the dedup hash. |
requestId | UUID | optional | A/B-test seed. Omit to let us generate one. |
attributes.countryCode | ISO-3166 alpha-2 | optional | Used for geo-targeting. Defaults to IP-based lookup. |
attributes.city, region | string | optional | Additional geo signals. |
attributes.language | string | optional | Drives locale on the creative copy. |
attributes.platform | "android" | "ios" | "web" | optional | The end user’s device platform if known. Falls back to your app’s default. |
attributes.gender | "male" | "female" | "other" | optional | Accepted for forward compatibility; not used for targeting yet. |
attributes.custom | Record<string, string> | optional | Pass-through metadata. |
Where each field comes from in the platform webhook
Every required field maps to a value Meta puts in the inbound webhook payload — you don’t need to invent or join anything. Per platform:| Encore field | WhatsApp Cloud API | Instagram (Messenger Platform) | Facebook (Messenger Platform) |
|---|---|---|---|
inboundMessageId | entry[].changes[].value.messages[].id (wamid.HBgL...) | entry[].messaging[].message.mid | entry[].messaging[].message.mid |
inboundTimestamp (optional) | entry[].changes[].value.messages[].timestamp — already in unix seconds | entry[].messaging[].timestamp — unix milliseconds; convert with Math.floor(ts / 1000) | entry[].messaging[].timestamp — unix milliseconds; convert with Math.floor(ts / 1000) |
recipient | messages[].from (E.164, e.g. "15014005001") | messaging[].sender.id (PSID) | messaging[].sender.id (PSID) |
chatId | entry[].changes[].value.metadata.phone_number_id or your own thread key | messaging[].thread.id if present, else messaging[].sender.id | messaging[].thread.id if present, else messaging[].sender.id |
messagingPlatform | "whatsapp" | "instagram" | "facebook" |
clientId | Integrator-defined (your operator’s stable ID — restaurant ID, account ID, etc.). Same value across platforms for the same operator. | ||
clientName | Integrator-defined (your operator’s display name — e.g. "Tony's Pizza"). Shown to the end user in the offer body. |
Timestamp unit reminder. If you choose to send
inboundTimestamp, send it in seconds, not milliseconds. WhatsApp’s messages[].timestamp is already seconds; IG/Messenger’s messaging[].timestamp is milliseconds, so Math.floor(meta_ts / 1000) first. Sending milliseconds doesn’t fail the request — it just poisons the inbound→served latency metric. Easiest move: skip the field on IG/Messenger if you don’t want to bother with the conversion.Response
| Field | Type | Notes |
|---|---|---|
success | true | Always true on a 200. Non-success outcomes come back as 4xx/5xx; see Status codes. |
impressionId | 64-char hex string | Server-derived SHA-256 of clientId:inboundMessageId. Stable across retries for the same input. Store this — you reference it on the delivery callback once the offer is sent. |
offer | object | null | null when no eligible offer was found for this user (geo-targeted out, no active campaigns, all creatives filtered by platform). See below. |
offer.id, offer.campaignId, offer.creativeId | UUID | IDs of the selected campaign + creative. Same value for id and campaignId (kept for symmetry with /offers/search). |
offer.clickUrl | URL | The destination URL with attribution params appended. End-user-visible when they tap the CTA. |
offer.requestId | UUID | Echoes the request’s requestId (server-generated if you omitted one). |
message | object | null | The Graph API body, ready to POST. null when offer is null. See Sending the message. |
When there’s no eligible offer
offer and message are both null, skip the promo step entirely and send only your client’s normal automated reply. Common reasons: geo-targeted out, your client has no eligible campaigns, all eligible creatives were filtered by recipient platform.
impressionId is still returned even when no offer was served — the call counts as a no-fill impression on our side. You don’t need to do anything with it in the no-offer case.
Sending the message
message.payload is the entire Graph API body, recipient already populated. Add your access token and POST it.
WhatsApp Cloud API
Messenger / Instagram Graph API
generic template card with an image, title, optional subtitle, and a single web-URL button.
Status codes
| Code | Meaning |
|---|---|
200 | Success. offer and message may be null if no eligible offer. impressionId is always present. |
400 | Malformed request (invalid messagingPlatform, missing required fields, etc.). Don’t retry; fix the request. |
401 | Missing or invalid X-API-Key. |
409 | Your client has exceeded their trial limit (rare). |
429 | Rate-limited. Back off + retry with same inboundMessageId. |
503 | Server temporarily over capacity (request queue full, dependency degraded). Retry with backoff. |
5xx (other) | Encore-side failure — proceed without the promo message, send only your client’s normal reply. |
Retry guidance
- Always reuse the same
inboundMessageIdacross retries. The server deduplicates within a 60-second window usingsha256(clientId, inboundMessageId); a retry returns the sameimpressionIdand the same offer (or same null result). No double-impression risk. - Use exponential backoff with jitter (e.g. 200ms → 1s → 5s, max 3 retries).
- After exhausting retries, drop the call and proceed with your client’s normal reply — never degrade end-user experience because of an Encore outage.
Treat any non-200 response as a soft fallback: skip the promo step and send your client’s configured auto-reply on its own. The end-user experience should never degrade because of an Encore outage.