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

# Offers API — Feed of Offers You Render

> Fetch a batch of ranked offers for a user in one REST call, then render them in your own UI and cycle through them. Each offer carries its own link plus context fields, and its impression is confirmed separately when you actually display it.

## Overview

If you already have your own app or UI and just want Encore to **pick and rank offers for you**, call the Offers API. You pass us a client identifier, the end-user identifier, and three targeting attributes. We return a **ranked feed of offers** — each one a branded short `clickUrl` plus `title`, `description`, `imageUrl`, `advertiserName`, `advertiserLogo`, `additionalImages`, the campaign `perk`, and an `impressionUid` — and you render and cycle through them however you like in your own surface.

The integration is one REST call to fetch the feed; you control display order and how you cycle through the offers. We don't draw any UI for you: you lay out each offer from these fields and wire the tap to its `clickUrl`. Because you reveal offers one at a time, **the fetch is no longer the impression** — you confirm each offer's impression separately when it's actually displayed (see [The impression model](#the-impression-model)).

<Note>
  **Two flows, pick the right one:**

  * **`/offers/feed` (this page)** — *you render your own UI.* Encore picks and ranks a batch of offers and hands you the raw fields; you build the card/banner/screen yourself and cycle through them.
  * **[`/offers/message`](/publishers/automated-messaging/integration)** — *you compose a message.* A closely related offer payload, but tailored for appending a brand-safe offer to an outbound automated message on WhatsApp, Messenger, Instagram, SMS, etc.

  Both use the same publishable-key auth and return the same core offer fields. The payloads are nearly identical — `/offers/feed` returns an array and additionally includes `advertiserLogo`, `additionalImages`, and a per-offer `impressionUid` (handy when you're rendering your own branded UI and confirming impressions yourself), which `/offers/message` omits. Choose `/offers/feed` when you own the rendering surface; choose `/offers/message` when you're stitching the offer into a text conversation.
</Note>

***

## How it works

```mermaid theme={null}
sequenceDiagram
    participant User as End user
    participant You as Your app / UI
    participant Encore

    User->>You: opens a screen / placement
    You->>Encore: POST /offers/feed
    Encore->>Encore: rank eligible offers
    Encore-->>You: { offers: [ { impressionUid, clickUrl, ... }, ... ] }
    You->>User: render the first offer in your own UI
    You->>Encore: POST /offers/impressions/{impressionUid}/delivered
    User->>You: taps the offer
    You->>User: open clickUrl (link.encorekit.com short link)
    Note over Encore: 302 → advertiser + click/claim registered
    User->>You: cycles to the next offer
    You->>Encore: POST /offers/impressions/{nextImpressionUid}/delivered
```

The model is deliberately simple:

1. **Fetch the feed.** Call `/offers/feed` once to get a ranked batch of offers. You decide the display order and cycle through them at your own pace.
2. **Impression = the offer is displayed.** Fetching the feed is **not** the impression. When you actually show an offer, confirm its impression with a separate `delivered` call using that offer's `impressionUid` (see below).
3. **Click / claim = open `clickUrl`.** When the user taps an offer in your UI, open its branded short `clickUrl`. It 302-redirects to the advertiser with attribution and registers the click/claim.

***

## Authentication

Pass the publishable key Encore provides you in the `X-API-Key` header. That's the only auth header required.

```bash theme={null}
X-API-Key: pk_live_...
```

Use `pk_test_...` against the sandbox project, `pk_live_...` against production. Same key shape as our iOS / Android / Web SDKs and the `/offers/message` endpoint.

<Note>
  The **test** key (`pk_test_...`) returns offers for development but does **no impression or conversion tracking** — `delivered` calls and clicks against a test key are accepted but not counted. Use `pk_live_...` for anything that should show up in your dashboard.
</Note>

***

## Request

The request is minimal — a client identifier, the end-user identifier, three targeting attributes, and an optional `limit`.

```http theme={null}
POST https://api.encorekit.com/encore/publisher/sdk/v1/offers/feed
Content-Type: application/json
```

```json theme={null}
{
  "clientId": "client-42",
  "userId": "user-8b3f7c10",
  "limit": 10,
  "attributes": {
    "countryCode": "US",
    "language": "en",
    "platform": "ios"
  }
}
```

### Try it with curl

Drop in your publishable key and run.

```bash theme={null}
curl --location --request POST 'https://api.encorekit.com/encore/publisher/sdk/v1/offers/feed' \
  --header 'X-API-Key: YOUR_API_KEY' \
  --header 'Content-Type: application/json' \
  --data '{
    "clientId": "client-42",
    "userId": "user-8b3f7c10",
    "limit": 10,
    "attributes": {
      "countryCode": "US",
      "language": "en",
      "platform": "ios"
    }
  }'
```

| Field                    | Type                              | Required | Notes                                                                                                                                                                                                                      |
| ------------------------ | --------------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `clientId`               | string                            | ✓        | Stable per-operator identifier — the business, account, or operator the offers are rendered on behalf of. Carried through as an analytics dimension so impressions, clicks, and conversions can be broken down per client. |
| `userId`                 | string                            | ✓        | End-user identifier on your surface. Seeds bandit ranking and is the userId on the transaction created at click time, plus the userId dimension on the click event.                                                        |
| `limit`                  | integer                           | —        | How many offers to return; you control display order and cycle through them. Range `1`–`25`, default `10`. We may return fewer if fewer eligible offers exist.                                                             |
| `attributes.countryCode` | ISO-3166 alpha-2                  | ✓        | Two uppercase letters (e.g. `"US"`, `"GB"`, `"CA"`). Drives geo-targeting and the country dimension on analytics.                                                                                                          |
| `attributes.language`    | string                            | ✓        | BCP-47 language tag (e.g. `"en"`, `"es"`). Drives creative locale resolution.                                                                                                                                              |
| `attributes.platform`    | `"android"` \| `"ios"` \| `"web"` | ✓        | The **end user's** device platform — drives creative compatibility filtering.                                                                                                                                              |

***

## Response

The response returns an **array of ranked offers** under `offers`. Each offer carries a branded short `clickUrl`, an `impressionUid`, plus `title`, `description`, `imageUrl`, `advertiserName`, `advertiserLogo`, `additionalImages`, and `perk`. There is no UI payload to forward; you render each offer yourself from these fields (see [Rendering offers](#rendering-offers)).

```json theme={null}
{
  "success": true,
  "offers": [
    {
      "impressionUid": "550e8400-e29b-41d4-a716-446655440000",
      "clickUrl": "https://link.encorekit.com/Xa3kPq7T",
      "title": "Get 50% Off",
      "description": "Limited time offer",
      "imageUrl": "https://cdn.example.com/banner.png",
      "advertiserName": "Acme",
      "advertiserLogo": "https://cdn.example.com/acme-logo.png",
      "additionalImages": ["https://cdn.example.com/img2.png"],
      "perk": "3 months free"
    }
  ]
}
```

The response carries **no internal identifiers** beyond each offer's `impressionUid` (`offers[].id`, `campaignId`, `creativeId`, and `requestId` are not returned — they're still computed server-side for analytics).

| Field                       | Type             | Notes                                                                                                                                                                                                                                  |
| --------------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `success`                   | `true`           | Always `true` on a 200. Non-success outcomes come back as 4xx/5xx; see [Status codes](#status-codes).                                                                                                                                  |
| `offers`                    | array            | A ranked list of offers, best first. **Empty array** when no eligible offers were found for this user (geo-targeted out, no active campaigns, all creatives filtered by platform). See below.                                          |
| `offers[].impressionUid`    | UUID             | Per-offer impression handle. Use it to confirm the impression when you display this offer — `POST /offers/impressions/{impressionUid}/delivered` (see [The impression model](#the-impression-model)). Not end-user-visible.            |
| `offers[].clickUrl`         | URL              | Branded short link on the fixed host `link.encorekit.com`. Open it when the user taps this offer — it 302-redirects to the advertiser with attribution and registers the click/claim. End-user-visible. **Always wire the tap to it.** |
| `offers[].title`            | string           | Headline of the offer. Use as the lead line in your UI.                                                                                                                                                                                |
| `offers[].description`      | string \| `null` | Short supporting line about the offer (e.g. `"Limited time offer"`). May be `null`.                                                                                                                                                    |
| `offers[].imageUrl`         | URL              | Wide banner creative (\~2.4:1). Render it as the offer's hero image (see [Image specs](#image-specs)).                                                                                                                                 |
| `offers[].advertiserName`   | string           | The advertiser / brand name (e.g. `"Acme"`).                                                                                                                                                                                           |
| `offers[].advertiserLogo`   | URL \| `null`    | The advertiser's logo image URL (roughly square), or `null` when the advertiser has no logo set. Render it as the brand mark if present.                                                                                               |
| `offers[].additionalImages` | array of URL     | Extra image URLs for this offer; ratio not guaranteed. **May be empty.** Render ratio-agnostic.                                                                                                                                        |
| `offers[].perk`             | string \| `null` | The campaign value proposition (e.g. `"3 months free"`), or `null`. May also be **absent** from the object. Use it to describe the deal.                                                                                               |

### When there are no eligible offers

```json theme={null}
{
  "success": true,
  "offers": []
}
```

Never a 404. If `offers` is an empty array, render nothing for the Encore placement — fall back to your own UI (for example, show your own default placement or progression UI in this case). Common reasons: geo-targeted out, the client has no eligible campaigns, all eligible creatives were filtered by the recipient's platform.

***

## The impression model

<Warning>
  **Fetching the feed is no longer the impression.** Earlier single-offer integrations treated the `/offers/best` call itself as the impression. That is **not** how `/offers/feed` works. Because you fetch a batch and reveal offers one at a time, each offer's impression is confirmed **separately**, when you actually display it.
</Warning>

When an offer becomes visible to the user, confirm its impression with a publishable-key call using that offer's `impressionUid`:

```http theme={null}
POST https://api.encorekit.com/encore/publisher/sdk/v1/offers/impressions/{impressionUid}/delivered
X-API-Key: pk_live_...
```

```bash theme={null}
curl --location --request POST \
  'https://api.encorekit.com/encore/publisher/sdk/v1/offers/impressions/550e8400-e29b-41d4-a716-446655440000/delivered' \
  --header 'X-API-Key: YOUR_API_KEY'
```

* **One `delivered` call per offer, when it's actually shown.** As you cycle through the feed, fire a `delivered` call for each offer at the moment it's revealed — not when you fetch the batch, and not for offers the user never sees.
* **The dashboard's impression count comes from these `delivered` calls.** If you don't confirm, the offer shows zero impressions even though it was fetched.
* **Clicks = open `clickUrl`.** When the user taps an offer, open its `clickUrl`; the 302 redirect registers the click/claim.
* **Completions = conversion.** A completion (conversion) is registered downstream when the user converts at the advertiser.
* A `delivered` call against a **test** key (`pk_test_...`) is accepted but not tracked.

***

## Rendering offers

You build the offer UI yourself from the returned fields — there's no Encore-specific component or template to embed. A typical card uses `imageUrl` as the visual, `title` as the headline, `perk` / `description` as supporting copy, and `advertiserName` (with the optional `advertiserLogo`) as the brand attribution. You decide the order and cycle through the `offers` array at your own pace.

The hard requirements: **confirm each offer's impression** (`delivered`) when you show it, and **wire the tap to its `clickUrl`.** Open `clickUrl` (in-app browser, custom tab, or `window.open`) when the user taps the offer so the click/claim attributes correctly.

```text theme={null}
┌─────────────────────────────────────┐
│  [imageUrl  ~2.4:1 wide banner]      │
│                                      │
│  Get 50% Off                title    │
│  3 months free                 perk  │
│  Limited time offer       description │
│                                      │
│  [logo] by Acme       advertiserName  │
│  ─────────────────────────────────   │
│  [ Claim ] ──────────────▶ clickUrl  │
└─────────────────────────────────────┘
```

### Image specs

Your UI designer should design the slots to these ratios:

| Field                | Aspect ratio                               | Nullable           | How to render                                                                                                        |
| -------------------- | ------------------------------------------ | ------------------ | -------------------------------------------------------------------------------------------------------------------- |
| `advertiserLogo`     | \~**1:1 (square)** — not strictly enforced | Yes                | Render in a 1:1 frame with `object-fit: contain`, and handle `null` (hide the mark / fall back to `advertiserName`). |
| `imageUrl`           | **\~2.4:1 wide banner** (NOT 16:9)         | No                 | Design the slot for \~2.4:1, or use `object-fit: cover`. See the warning below.                                      |
| `additionalImages[]` | Not guaranteed                             | Array may be empty | Render ratio-agnostic (`object-fit: contain` in a flexible frame). Don't assume any fixed ratio.                     |

<Warning>
  `imageUrl` is a **wide \~2.4:1 banner**, not 16:9. A 16:9 slot will crop or letterbox it — design the slot for \~2.4:1 (or use `object-fit: cover`).
</Warning>

<Note>
  `clickUrl` is a branded short link on the fixed host `link.encorekit.com`. It's a plain URL, so it works from any platform — open it in an in-app browser, a Custom Tab / `SFSafariViewController`, or a new web tab. No interactive template is required.
</Note>

### Share to a friend

Because `clickUrl` is a plain, branded short link, you can also let a user **share the offer with a friend** — drop `clickUrl` into a share sheet / message. Whoever opens it gets the same attributed redirect.

***

## Status codes

| Code          | Meaning                                                                                                                                 |
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `200`         | Success. `offers` may be an empty array if no eligible offers.                                                                          |
| `400`         | Malformed request (missing required field, invalid `attributes.countryCode`, `limit` out of range, etc.). Don't retry; fix the request. |
| `401`         | Missing or invalid `X-API-Key`.                                                                                                         |
| `409`         | The client has exceeded their trial limit (rare).                                                                                       |
| `429`         | Rate-limited. Back off + retry.                                                                                                         |
| `503`         | Server temporarily over capacity (request queue full, dependency degraded). Retry with backoff.                                         |
| `5xx` (other) | Encore-side failure — render nothing and proceed without offers.                                                                        |

### Retry guidance

* **Retries are not deduplicated server-side.** Every `/offers/feed` call generates fresh short links and analytics rows, so only fetch a feed once per placement you intend to render.
* Use exponential backoff with jitter (e.g. 200ms → 1s → 5s, max 3 retries) for transient failures (429 / 503 / network).
* After exhausting retries, render nothing — never degrade the end-user experience because of an Encore outage.

<Note>
  Treat **any** non-200 response as a soft fallback: skip the offer placement entirely. The end-user experience should never degrade because of an Encore outage.
</Note>

***

## End-to-end example

A complete flow, from screen open to attributed click:

```text theme={null}
1. The end user opens a screen in your app that has an Encore offer
   placement.

2. Your app POSTs to Encore once to fetch the feed:

   POST /encore/publisher/sdk/v1/offers/feed
   X-API-Key: pk_live_abc123...
   {
     "clientId": "client-42",
     "userId": "user-8b3f7c10",
     "limit": 10,
     "attributes": { "countryCode": "US", "language": "en", "platform": "ios" }
   }

3. Encore ranks the eligible offers and responds with an array (see Response):

   {
     "success": true,
     "offers": [
       {
         "impressionUid": "550e8400-e29b-41d4-a716-446655440000",
         "clickUrl": "https://link.encorekit.com/Xa3kPq7T",
         "title": "Get 50% Off",
         "advertiserName": "Acme",
         "perk": "3 months free",
         ...
       },
       ...
     ]
   }

4. Your app renders the first offer in your own UI from those fields
   (imageUrl, title, perk, advertiserName) and wires the tap target
   to that offer's clickUrl.

5. The moment the offer is actually displayed, your app confirms the
   impression:

   POST /encore/publisher/sdk/v1/offers/impressions/550e8400-.../delivered
   X-API-Key: pk_live_abc123...

6. The end user taps the offer. Your app opens
   https://link.encorekit.com/Xa3kPq7T in an in-app browser / Custom Tab.
   The short link 302-redirects to the advertiser, the click/claim is
   registered, and the attribution chain closes.

7. Your app cycles to the next offer in the array and repeats steps 4–6,
   firing a delivered call for each offer as it's shown.
```
