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

# Splitting Your Funnel by Placement

> Label the surface each offer is shown on, and your dashboard breaks impressions, claims, and conversions out per surface.

If you show offers in more than one place (a cancellation flow, a home feed, a post-purchase screen), you almost certainly want to know which one is working. Send an optional `placementId` and Encore's dashboard breaks your funnel out by surface, with no other change on your side.

Without it, every offer you serve lands in a single bucket labelled `(unlabeled)`. That is not an error state; it is simply what "you did not tell us where this was" looks like.

## Sending it

`placementId` is an optional top-level string on all three offer endpoints:

<CodeGroup>
  ```bash POST /offers/message theme={null}
  curl -X POST https://api.encorekit.com/encore/publisher/sdk/v1/offers/message \
    -H "X-API-Key: YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "clientId": "client-42",
      "chatId": "8b3f7c10-9d4e-4d2a-91a1-3f0e8a1c0b5d",
      "attributes": { "countryCode": "US", "language": "en" },
      "placementId": "post_order_confirmation"
    }'
  ```

  ```bash POST /offers/feed theme={null}
  curl -X POST https://api.encorekit.com/encore/publisher/sdk/v1/offers/feed \
    -H "X-API-Key: YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "clientId": "client-42",
      "userId": "user-1234",
      "limit": 10,
      "attributes": { "countryCode": "US", "language": "en" },
      "placementId": "home_feed"
    }'
  ```

  ```bash POST /offers/catalog theme={null}
  curl -X POST https://api.encorekit.com/encore/publisher/sdk/v1/offers/catalog \
    -H "X-API-Key: YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "userId": "user-1234",
      "countryCode": "US",
      "placementId": "cancel_flow"
    }'
  ```
</CodeGroup>

It is your label, in your vocabulary: Encore never interprets it, only groups by it.

<Note>
  **One placement per request, not per offer.** On `/offers/feed` and `/offers/catalog` the whole response belongs to the surface you named, because the surface is where you rendered the list. If you genuinely render two surfaces, make two calls.
</Note>

## What it applies to

You send the label once, at serve time. Encore carries it through the rest of the funnel for you:

| Stage             | Carries your placement? | How                                                               |
| ----------------- | ----------------------- | ----------------------------------------------------------------- |
| **Impression**    | Yes                     | Stamped when the offer is served                                  |
| **Click / claim** | Yes                     | Carried on the link, so the tap is attributed to the same surface |
| **Conversion**    | Yes                     | Inherited from the claim it belongs to                            |

That last row is the one that matters most and the one you get for free: a conversion is attributed to the surface whose **claim** produced it, so your conversion rate per placement is arithmetically consistent with the impressions and claims above it.

<Warning>
  **You do not need to (and cannot) send a placement on `POST /offers/impressions/{impressionUid}/delivered`.** That call confirms an impression that was already served, and it inherits the placement from the serve. A second, independently supplied label there could contradict the first.
</Warning>

## How your label is stored

Encore normalises the value once, when it arrives:

| Rule                                   | Input               | Stored as           |
| -------------------------------------- | ------------------- | ------------------- |
| Surrounding whitespace is trimmed      | `"  cancel_flow  "` | `cancel_flow`       |
| Blank is treated as absent             | `""` or `"   "`     | *(unlabeled)*       |
| Longer than 64 characters is truncated | 200-character label | first 64 characters |

Two consequences worth knowing:

* **Trimming means `" cancel_flow "` and `"cancel_flow"` are the same placement**, rather than two rows that look like a duplicate. Trimming also happens *before* the 64-character cut, so padding never eats into the label itself.
* **An over-long label is truncated, never rejected.** A cosmetic mistake in a label will never fail an offer request or take your integration down.

Case is **not** folded: `Cancel_Flow` and `cancel_flow` are two placements. Pick one convention and keep to it.

## Choosing labels

Name the **surface**, not the campaign or the moment:

<CodeGroup>
  ```text Good theme={null}
  cancel_flow
  home_feed
  post_order_confirmation
  settings_upgrade
  ```

  ```text Avoid theme={null}
  offer_1                     // which surface is this?
  2026_summer_promo           // that is a campaign, not a placement
  user_8b3f7c10               // per-user: unbounded, and unreadable in a report
  ```
</CodeGroup>

The last one is the trap worth stating outright: **a label that varies per user or per request gives you one row per impression** and a breakdown you cannot read. Keep the set small, stable, and human-readable: you are naming places in your product, and there are only so many of those.

## Seeing the result

Open **Analytics** in the [publisher portal](https://app.encorekit.com). The placements table lists each label you have sent with its impressions, claims, and conversions. Traffic served before you started sending a placement stays in `(unlabeled)`: there is no backfill, because nothing recorded which surface those older impressions ran on.

<Note>
  New labels appear as soon as traffic arrives under them. There is no configuration step and nothing to register in advance: send a new `placementId` and it shows up.
</Note>
