> ## 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.

# POST /offers/message

> Endpoint contract for POST /offers/message: select one eligible offer for an outbound automated message.

Selects one eligible offer for a messaging surface and returns its link plus context fields so you can compose and send the message yourself. No message is composed server-side.

```http theme={null}
POST https://api.encorekit.com/encore/publisher/sdk/v1/offers/message
```

<Note>
  For when to call (thread triggers, per-thread caps) and how to compose and send the message, see the [Automated Messaging guide](/publishers/offers-api/guides/automated-messaging). If you render offers in your own app UI rather than a text conversation, use [`POST /offers/feed`](/publishers/offers-api/reference/feed): a nearly identical payload that returns an array and adds `advertiserLogo`, `additionalImages`, and a per-offer `impressionUid`.
</Note>

## Authentication

Pass your publishable key in the `X-API-Key` header (`pk_live_*` for production, `pk_test_*` for development). Multi-app projects also send `X-Platform: api`. Full model: [overview's Authentication section](/publishers/offers-api/overview#authentication).

## Request

### Structure

```typescript theme={null}
interface MessageOfferRequest {
  clientId: string;              // required: stable per-operator identifier
  chatId: string;                // required: conversation/thread identifier
  attributes: {
    countryCode: string;         // required: ISO 3166-1 alpha-2, two uppercase letters
    language: string;            // required: BCP-47 language tag
    platform?: 'android' | 'ios' | 'web';  // recipient's device platform; never 'api'
  };
}
```

### Field Reference

| Field                    | Type   | Required | Description                                                                                                                                                                                                                                              |
| ------------------------ | ------ | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `clientId`               | string | Yes      | Stable per-operator identifier: the business, account, or operator your platform is sending on behalf of (for example a restaurant id). Carried through as an analytics dimension so impressions, clicks, and conversions can be broken down per client. |
| `chatId`                 | string | Yes      | Conversation / thread identifier on the messaging platform. Used as the messaging-surface userId: it drives bandit seeding, the click-time transaction, and the userId dimension on the click event.                                                     |
| `attributes.countryCode` | string | Yes      | ISO 3166-1 alpha-2, two uppercase letters (for example `"US"`, `"GB"`, `"CA"`). Drives geo-targeting and the country dimension on analytics.                                                                                                             |
| `attributes.language`    | string | Yes      | BCP-47 language tag (for example `"en"`, `"es"`). Drives creative locale resolution.                                                                                                                                                                     |
| `attributes.platform`    | string | No       | The **recipient's** device platform: `"android"`, `"ios"`, or `"web"`. Drives creative compatibility filtering. Optional for API-platform apps: omit it to receive any eligible offer untrimmed. Never send `"api"` here.                                |

## Response

### Structure

```typescript theme={null}
interface MessageOfferResponse {
  success: true;
  offer: {
    clickUrl: string;              // branded short link on link.encorekit.com
    title: string;                 // offer headline
    description: string | null;    // short supporting line
    imageUrl: string;              // primary creative image URL
    advertiserName: string;        // advertiser / brand name
    perk?: string | null;          // campaign value proposition
    badgeLabel: 'free_trial' | 'discount' | null;  // constrained promo badge
  } | null;                        // null when no eligible offer was found
}
```

### Field Reference

| Field                  | Type                                     | Description                                                                                                                                                                                                                                    |
| ---------------------- | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `success`              | `true`                                   | Always `true` on a 200. Non-success outcomes come back as 4xx/5xx; see [Status codes](#status-codes).                                                                                                                                          |
| `offer`                | object \| `null`                         | `null` when no eligible offer was found for this user (geo-targeted out, no active campaigns, all creatives filtered by platform). Never a 404: skip the promo step and send only your client's normal reply.                                  |
| `offer.clickUrl`       | URL                                      | Branded short link on the fixed host `link.encorekit.com`. A plain URL: embed it as text in your message; it tracks the click and redirects to the advertiser's landing page. End-user-visible. **Always include it** in the message you send. |
| `offer.title`          | string                                   | Headline of the offer. Use as the lead line when composing your message.                                                                                                                                                                       |
| `offer.description`    | string \| `null`                         | Short supporting line about the offer (for example `"Songs & Podcasts"`). May be `null`.                                                                                                                                                       |
| `offer.imageUrl`       | URL                                      | Creative image. Attach it as a media message if your channel supports media; otherwise omit it.                                                                                                                                                |
| `offer.advertiserName` | string                                   | The advertiser / brand name (for example `"Apple Music"`).                                                                                                                                                                                     |
| `offer.perk`           | string \| `null`                         | The campaign value proposition (for example `"6 months free"`), or `null`. May also be absent from the object.                                                                                                                                 |
| `offer.badgeLabel`     | `"free_trial"` \| `"discount"` \| `null` | Constrained promo badge, distinct from `perk`. Always present (`null` when the campaign has none). Map the enum to your own display text.                                                                                                      |

The response carries **no internal identifiers** (impression, campaign, creative, or request ids) and no server-composed message; you build the text from the fields above.

## Example

```bash theme={null}
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",
    "chatId": "8b3f7c10-9d4e-4d2a-91a1-3f0e8a1c0b5d",
    "attributes": {
      "countryCode": "US",
      "language": "en",
      "platform": "ios"
    }
  }'
```

```json theme={null}
{
  "success": true,
  "offer": {
    "clickUrl": "https://link.encorekit.com/aB3xY7q2",
    "title": "Free Apple Music for 6 months",
    "description": "Songs & Podcasts",
    "imageUrl": "https://storage.googleapis.com/encore-assets-prod/creatives/2c643fc5-91cb-4b0a-ba0a-0536c2c4f871/primary-1778695603093.png",
    "advertiserName": "Apple Music",
    "perk": "6 months free",
    "badgeLabel": "free_trial"
  }
}
```

## Status codes

| Code          | Meaning                                                                                                     |
| ------------- | ----------------------------------------------------------------------------------------------------------- |
| `200`         | Success. `offer` may be `null` if no eligible offer.                                                        |
| `400`         | Malformed request (missing required field, invalid `attributes.countryCode`). 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 and retry.                                                                           |
| `503`         | Server temporarily over capacity. Retry with backoff.                                                       |
| `5xx` (other) | Encore-side failure. Proceed without the promo message.                                                     |

### Retry guidance

* **Idempotency is on your side.** The server does not deduplicate retries; every call generates a fresh short link and analytics row. Call `/offers/message` once per inbound message you've decided to attach an offer to, and de-dupe inbound webhooks on your side before calling.
* Use exponential backoff with jitter (for example 200ms, 1s, 5s, max 3 retries) for transient failures (429 / 503 / network).
* After exhausting retries, drop the call and send only your client's normal reply.

## Related

* [Automated Messaging guide](/publishers/offers-api/guides/automated-messaging): triggers, caps, and message composition.
* [`POST /offers/feed`](/publishers/offers-api/reference/feed): the in-app sibling that returns a ranked array with per-offer impression handles.
* [API Reference overview](/publishers/offers-api/overview): auth and the claim model.
