Skip to main content

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 to graph.facebook.com as-is.

When to call us

Two triggers, both gated by a max-ads-per-thread check on your side:
  1. 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.
  2. 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.
Counting prior offers is simple: every Encore offer link points at the 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.com links 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 the X-API-Key header. That’s the only auth header required.
X-API-Key: pk_live_...
Use pk_test_... against the sandbox project, pk_live_... against production. Same shape as our iOS / Android / Web SDKs.

Request

POST https://api.encorekit.com/encore/publisher/sdk/v1/offers/message
Content-Type: application/json
{
  "clientId": "client-42",
  "clientName": "Tony's Pizza",
  "messagingPlatform": "whatsapp",
  "recipient": "15014005001",
  "inboundMessageId": "wamid.HBgLMTUwMTQwMDUwMDEVAgARGBI5RDcwQ0E5RkM3REIwQTcyAA==",
  "chatId": "8b3f7c10-9d4e-4d2a-91a1-3f0e8a1c0b5d",
  "attributes": {
    "countryCode": "US",
    "language": "en",
    "platform": "ios"
  }
}

Try it with curl

Drop in your publishable key and run. inboundTimestamp is omitted — optional field, the request works without it.
curl --location --request POST 'https://api.encorekit.com/encore/publisher/sdk/v1/offers/message' \
  --header 'X-API-Key: YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
    "clientId": "client-42",
    "clientName": "Tony'"'"'s Pizza",
    "messagingPlatform": "whatsapp",
    "recipient": "15014005001",
    "inboundMessageId": "wamid.HBgLMTUwMTQwMDUwMDEVAgARGBI5RDcwQ0E5RkM3REIwQTcyAA==",
    "chatId": "8b3f7c10-9d4e-4d2a-91a1-3f0e8a1c0b5d",
    "attributes": {
      "countryCode": "US",
      "language": "en",
      "platform": "ios"
    }
  }'
FieldTypeRequiredNotes
clientIdstringStable 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.
clientNamestringDisplay 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.
recipientstringThe 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.
inboundMessageIdstringThe 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.
chatIdstringConversation / 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.
inboundTimestampintegeroptionalUnix 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.
userIdstringoptionalHashed end-user identifier (you choose the hash; we don’t dictate). Used for frequency capping in future iterations. Not part of the dedup hash.
requestIdUUIDoptionalA/B-test seed. Omit to let us generate one.
attributes.countryCodeISO-3166 alpha-2optionalUsed for geo-targeting. Defaults to IP-based lookup.
attributes.city, regionstringoptionalAdditional geo signals.
attributes.languagestringoptionalDrives locale on the creative copy.
attributes.platform"android" | "ios" | "web"optionalThe end user’s device platform if known. Falls back to your app’s default.
attributes.gender"male" | "female" | "other"optionalAccepted for forward compatibility; not used for targeting yet.
attributes.customRecord<string, string>optionalPass-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 fieldWhatsApp Cloud APIInstagram (Messenger Platform)Facebook (Messenger Platform)
inboundMessageIdentry[].changes[].value.messages[].id (wamid.HBgL...)entry[].messaging[].message.midentry[].messaging[].message.mid
inboundTimestamp (optional)entry[].changes[].value.messages[].timestamp — already in unix secondsentry[].messaging[].timestamp — unix milliseconds; convert with Math.floor(ts / 1000)entry[].messaging[].timestamp — unix milliseconds; convert with Math.floor(ts / 1000)
recipientmessages[].from (E.164, e.g. "15014005001")messaging[].sender.id (PSID)messaging[].sender.id (PSID)
chatIdentry[].changes[].value.metadata.phone_number_id or your own thread keymessaging[].thread.id if present, else messaging[].sender.idmessaging[].thread.id if present, else messaging[].sender.id
messagingPlatform"whatsapp""instagram""facebook"
clientIdIntegrator-defined (your operator’s stable ID — restaurant ID, account ID, etc.). Same value across platforms for the same operator.
clientNameIntegrator-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

{
  "success": true,
  "impressionId": "d260ce6212a72ca30959e1faf00cc17e5cd4adf425c817111f0f97d8bbe52670",
  "offer": {
    "id": "2c643fc5-91cb-4b0a-ba0a-0536c2c4f871",
    "campaignId": "2c643fc5-91cb-4b0a-ba0a-0536c2c4f871",
    "creativeId": "f7a1be25-2a31-4adf-b0e6-19a3c91a0e10",
    "clickUrl": "https://api.encorekit.com/encore/click/eyJhcHBJZCI6Ii4uLiJ9.abc123def456...",
    "requestId": "11111111-1111-4111-8111-111111111111"
  },
  "message": {
    "platform": "whatsapp",
    "payload": {
      "messaging_product": "whatsapp",
      "recipient_type": "individual",
      "to": "15014005001",
      "type": "interactive",
      "interactive": {
        "type": "cta_url",
        "header": {
          "type": "image",
          "image": {
            "link": "https://storage.googleapis.com/encore-assets-prod/creatives/2c643fc5-91cb-4b0a-ba0a-0536c2c4f871/primary-1778695603093.png"
          }
        },
        "body": {
          "text": "Free Apple Music for 6 months — claim before midnight"
        },
        "action": {
          "name": "cta_url",
          "parameters": {
            "display_text": "Claim",
            "url": "https://api.encorekit.com/encore/click/eyJhcHBJZCI6Ii4uLiJ9.abc123def456..."
          }
        }
      }
    }
  }
}
FieldTypeNotes
successtrueAlways true on a 200. Non-success outcomes come back as 4xx/5xx; see Status codes.
impressionId64-char hex stringServer-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.
offerobject | nullnull 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.creativeIdUUIDIDs of the selected campaign + creative. Same value for id and campaignId (kept for symmetry with /offers/search).
offer.clickUrlURLThe destination URL with attribution params appended. End-user-visible when they tap the CTA.
offer.requestIdUUIDEchoes the request’s requestId (server-generated if you omitted one).
messageobject | nullThe Graph API body, ready to POST. null when offer is null. See Sending the message.

When there’s no eligible offer

{
  "success": true,
  "impressionId": "d260ce6212a72ca30959e1faf00cc17e5cd4adf425c817111f0f97d8bbe52670",
  "offer": null,
  "message": null
}
Never a 404. If 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

curl 'https://graph.facebook.com/v22.0/<WHATSAPP_BUSINESS_PHONE_NUMBER_ID>/messages' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer <ACCESS_TOKEN>' \
  -d '<message.payload from Encore response, verbatim>'
The end user gets an image header, body text, and a “Claim” button.

Messenger / Instagram Graph API

curl 'https://graph.facebook.com/v22.0/me/messages?access_token=<PAGE_ACCESS_TOKEN>' \
  -X POST -H 'Content-Type: application/json' \
  -d '<message.payload from Encore response, verbatim>'
The IG / Messenger payload is a generic template card with an image, title, optional subtitle, and a single web-URL button.

Status codes

CodeMeaning
200Success. offer and message may be null if no eligible offer. impressionId is always present.
400Malformed request (invalid messagingPlatform, missing required fields, etc.). Don’t retry; fix the request.
401Missing or invalid X-API-Key.
409Your client has exceeded their trial limit (rare).
429Rate-limited. Back off + retry with same inboundMessageId.
503Server 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 inboundMessageId across retries. The server deduplicates within a 60-second window using sha256(clientId, inboundMessageId); a retry returns the same impressionId and 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.

End-to-end example

A complete WhatsApp flow, from incoming message to outbound Graph API call:
1. End user +14155551234 texts your client's business number
   +14155556789 on WhatsApp. Meta delivers the webhook to your platform
   with wamid = "wamid.HBgLMTQxNTU1NTEyMzQVAgARGBI5RDcwQ0E5RkM3REIwQTcyAA==".

2. Your platform receives the webhook / accessibility event.

3. Your platform POSTs to Encore (new chat → fresh chatId):

   POST /encore/publisher/sdk/v1/offers/message
   X-API-Key: pk_live_abc123...
   {
     "clientId": "client-42",
     "clientName": "Tony's Pizza",
     "messagingPlatform": "whatsapp",
     "recipient": "14155551234",
     "inboundMessageId": "wamid.HBgLMTQxNTU1NTEyMzQVAgARGBI5RDcwQ0E5RkM3REIwQTcyAA==",
     "chatId": "8b3f7c10-9d4e-4d2a-91a1-3f0e8a1c0b5d",
     "attributes": { "countryCode": "US", "platform": "ios" }
   }

4. Encore responds with impressionId + offer + payload (see Response section):

   {
     "success": true,
     "impressionId": "d260ce62...8bbe52670",
     "offer": { ... },
     "message": { "platform": "whatsapp", "payload": { ... } }
   }

5. Your platform sends the offer to the end user — payload goes through
   unchanged:

   POST graph.facebook.com/v22.0/<BUSINESS_PHONE>/messages
   Authorization: Bearer <token>
   <message.payload from step 4, verbatim>

   End user receives: [image] "Free Apple Music for 6 months — claim before
   midnight"  [Claim button]

6. Your platform then sends your client's configured auto-reply normally:

   "Thanks for your message! Our team will get back to you shortly."

7. End user taps Claim → OS browser opens → advertiser landing page →
   attribution chain closes.