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

# Attribute Subscriptions to Offers

> Link your subscription events back to the Encore offers a user saw: identify() with your stable customer id before showing offers, then forward each subscription event server-side to POST /webhooks/encore with that same id as app_account_id.

For Encore to measure whether its offers drive your subscriptions, each subscription event
must join back to the offer exposure Encore recorded for the same person. The whole
integration reduces to one rule: **one stable customer id everywhere**. Concretely, that
is two steps:

1. **Identify with your stable customer id before presenting offers.**
   `Encore.identify(customerId)` keys every exposure and claim to that id.
2. **Forward each subscription event server-side** to Encore's webhook with
   `user.app_account_id` set to that **same id**, byte-identical, plus the matching
   event verb.

Encore joins your events to offer exposures by `app_account_id`. That is the whole job:
no id plumbing through your checkout, no other client work.

## Using RevenueCat?

If RevenueCat manages your subscriptions, you are most likely done already. Encore
natively ingests RevenueCat webhooks, so you never build the server-side forwarding in
step 2 yourself. The whole recipe is two alignment moves:

1. **Identify with your RevenueCat app user id.** Call `Encore.identify()` with the same
   id you pass to RevenueCat as `app_user_id` (equivalently, use one stable customer id
   for both). This is step 1 below with one extra constraint: the two ids must match.
2. **Point RevenueCat's webhook forwarding at Encore.** Add the per-app receiver URL from
   your Encore dashboard's webhook settings as a RevenueCat webhook target. See
   [RevenueCat's webhook guide](https://www.revenuecat.com/docs/integrations/webhooks) for
   how to add a webhook in their dashboard. Many
   publishers already have this configured; if so, just confirm the URL.

That is the entire integration: no payload construction, no signing, no event mapping.
Encore links RevenueCat's subscription events to your offer exposures server-side using
the shared id. If this is you, you are done; the manual steps below are for everyone else.

## Step 1: identify before you show offers

Call [`identify()`](/publishers/web/sdk-reference/identify) with your own stable customer
id, before any offer is presented:

```javascript theme={null}
import Encore from '@encorekit/web-sdk';

Encore.identify('cus_8f4kq2'); // your stable customer id, e.g. your database user id
```

The id you pass becomes the **app account id** that keys every exposure and claim Encore
records; it is the value
[`getAppAccountId()`](/publishers/web/sdk-reference/webhook-ingestion#getappaccountid)
returns. Without `identify()`, the SDK falls back to an auto-generated anonymous id, and
your server has no way to reproduce that value later, so your subscription events can
never link back. On web this is critical: identify as soon as you know who the user is,
and always before [`show()`](/publishers/web/sdk-reference/show).

## Step 2: forward subscription events with the same id

Whenever a subscription event happens in your billing system (subscribe, renew, cancel,
and so on), your server POSTs it to `https://api.encorekit.com/encore/webhooks/encore`
with:

* `user.app_account_id`: the **same customer id** from step 1, byte-identical
* `subscription.original_transaction_id`: your biller's subscription id
* `event_type`: the matching Encore verb, for example `did_subscribe` or `did_renew`

The full contract (HMAC auth headers, the `buildEncoreHeaders` signing helper, the payload
schema, the verb list, and responses) lives in the
[Webhook Ingestion reference](/publishers/web/sdk-reference/webhook-ingestion). This guide
only composes it:

```javascript theme={null}
import { buildEncoreHeaders } from './encore-signing'; // the reference's signing helper

const ENCORE_URL = 'https://api.encorekit.com/encore/webhooks/encore';

async function forwardToEncore({ verb, eventId, occurredAt, subscriptionId, customerId }) {
  const rawBody = JSON.stringify({
    event_id: eventId,                    // unique per event; Encore dedups on it
    event_type: verb,                     // e.g. 'did_subscribe'; full list in the reference
    occurred_at: occurredAt,              // RFC3339 UTC with a trailing Z
    user: { app_account_id: customerId }, // the SAME id you passed to identify()
    subscription: { original_transaction_id: subscriptionId, store: 'stripe' },
  });

  const headers = buildEncoreHeaders(rawBody, {
    publishableKey: 'pk_live_your_publishable_key',
    secretKey: process.env.ENCORE_SECRET_KEY,
  });

  await fetch(ENCORE_URL, { method: 'POST', headers, body: rawBody }); // send the same bytes
}

// In your billing webhook handler (Stripe shown; any biller works the same):
app.post('/webhooks/billing', async (req, res) => {
  const event = req.body;

  if (event.type === 'customer.subscription.created') {
    // Resolve YOUR customer id from your own records (the subscription-to-customer
    // mapping in your database), never from anything the browser sent.
    const customerId = await lookupCustomerId(event.data.object.customer);

    await forwardToEncore({
      verb: 'did_subscribe',
      eventId: event.id,
      occurredAt: new Date(event.created * 1000).toISOString(),
      subscriptionId: event.data.object.id,
      customerId,
    });
  }

  res.sendStatus(200);
});
```

## Using Another Billing Provider?

If your subscriptions run through Stripe, Paddle, or your own custom billing rather than
RevenueCat, the two manual steps above are your path. Identify with your stable customer
id (step 1), then forward each subscription event server-side to Encore's webhook with
that same id (step 2). That is the canonical path for any biller Encore does not ingest
natively; there is no other client work.

## Gotchas

* **Byte-identical id.** `user.app_account_id` must equal the `identify()` id exactly;
  any prefixing, casing, or trimming difference breaks the join silently.
* **Identify before show, not after.** Exposures recorded before `identify()` are keyed
  to the anonymous id and are not retro-linked; a late identify leaves them unattributed.
* **Resolve the id server-side.** Read the customer id from your own auth session or
  subscription records, never from the client request body: a spoofed id attributes
  someone else's subscription.
* **Every event carries the id.** `user.app_account_id` is required on every event, not
  just the first; an event without it is acknowledged but dropped. See
  [how the join works](/publishers/web/sdk-reference/webhook-ingestion#how-the-join-works).
* **Use the canonical verbs.** An unrecognized `event_type` is accepted and stored but
  cannot be projected into measurement; map your biller's events to the
  [verb list](/publishers/web/sdk-reference/webhook-ingestion#field-reference).

## Related

* [Webhook Ingestion reference](/publishers/web/sdk-reference/webhook-ingestion): the full contract this guide forwards to.
* [`identify()`](/publishers/web/sdk-reference/identify): the identity model, including `getAppAccountId()`.
* [Reduce Paywall Churn](/publishers/web/guides/intercept-paywall-abandonment): an offer flow whose outcomes this attribution measures.
