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

# Send Users Back to an Unfinished Offer

> Fetch a claim's offer link from your backend with one HMAC-signed request, to email or message a user who started an offer but did not finish it.

A user taps **Claim**, lands on the brand's site, and leaves before finishing. Your backend can fetch the link that user opened and send it back to them later, for example in a "finish this later" email. This needs no app update: it works for any claim your app already made, from its transaction id.

The link carries the user's transaction id, so when they finish, the original claim gets the credit.

> [!WARNING]
>
> Whoever opens the link completes the offer **as that user**. Send it only to the user who claimed, and keep it out of logs, analytics and any third-party tool.

## What you need

* The claim's **transaction id**: `claim.transactionId` from the placement result in your app. Send it to your server when the user claims. It is null when Encore could not record the claim, and then there is nothing to look up.
* If your app calls `identify()`, the **user id** you pass to it. It's optional: leave it out when your app never calls `identify()` and the SDK makes the id itself, and for claims made before `identify()`, which keep the pre-identify owner.
* Your project's **publishable key** and its paired **secret key**, from the [dashboard](https://dashboard.encorekit.com). Call this endpoint only from your server. It never accepts a publishable key on its own.

## Request

```http theme={null}
GET https://api.encorekit.com/encore/publisher/sdk/v1/transactions/{transactionId}/claim-url?userId=user-123   # userId is optional
```

| Parameter | In | Required | Description |
| - | - | - | - |
| `transactionId` | path | Yes | The claim's transaction id (a UUID). |
| `userId` | query | No | Your user id for the user who made the claim. When you send it, a claim that belongs to another user returns `404`. Leave it out when the SDK makes the id, or for a claim made before `identify()`: that claim keeps its pre-identify owner, so sending the identified id returns `404`. The claim always has to be in your project. |

> [!NOTE]
>
> Leaving out `userId` skips the check that the claim is that user's. The claim still has to be in your project, but making sure the link reaches the right person is then up to you: send it only to the user you stored the transaction id with.

### Authentication

The request is HMAC-signed. Send these headers:

| Header | Value |
| - | - |
| `X-API-Key` | Your publishable key (`pk_live_*` or `pk_test_*`). |
| `X-Signature` | Hex HMAC-SHA256 of the base string, keyed with the secret key that pairs with your publishable key. |
| `X-Timestamp` | Unix time in seconds. It is part of what you sign, and a skew over 5 minutes is rejected. |
| `X-Platform` | Required when your project has more than one app: the platform of any app in it (`ios`, `android`, `web` or `api`). Whichever you name, the lookup covers claims from every app in the project. |

The base string is `timestamp.METHOD.path.query`. Two details cause most signature failures:

* **Sign the path without `/encore`.** You call `/encore/publisher/sdk/v1/...`, but Encore's gateway strips `/encore` before the API checks the signature, so sign `/publisher/sdk/v1/transactions/{transactionId}/claim-url`.
* **Sign the query string, not a body.** With `userId`, that's `userId=` followed by the URI-encoded user id. Without it, the query is empty and the base string ends in a dot: `timestamp.GET.path.`

```javascript theme={null}
// Your server (Node). Never call this from the app.
import crypto from 'node:crypto';

async function fetchClaimUrl(transactionId, userId /* optional */) {
  const method = 'GET';
  const path = `/publisher/sdk/v1/transactions/${transactionId}/claim-url`; // signed: no /encore
  const query = userId ? `userId=${encodeURIComponent(userId)}` : '';
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const signature = crypto
    .createHmac('sha256', process.env.ENCORE_SECRET_KEY)
    .update(`${timestamp}.${method}.${path}.${query}`)
    .digest('hex');

  const res = await fetch(`https://api.encorekit.com/encore${path}${query ? `?${query}` : ''}`, {
    method,
    headers: {
      'X-API-Key': process.env.ENCORE_PUBLISHABLE_KEY,
      'X-Signature': signature,
      'X-Timestamp': timestamp,
      'X-Platform': 'ios', // only when the project has more than one app
    },
  });
  if (res.status === 404) return null; // not this user's claim, or not in your project
  if (!res.ok) throw new Error(`claim-url lookup failed: ${res.status}`);

  const body = await res.json();
  return body.claimUrl; // null when there is no link to send; body.reason says why
}
```

## Response

```typescript theme={null}
interface ClaimUrlResponse {
  claimUrl: string | null;   // the link to send, or null
  linkType: string;          // "advertiser" today
  expiresAt: string | null;  // ISO 8601 when Encore sets an expiry; null for "advertiser" links
  reason: string | null;     // why claimUrl is null; null when there is a link
  campaignId: string;
  status: string;            // "open", "completed" or "closed"
  isTestRequest: boolean;    // the claim was made with a test key or a debug build
}
```

Send the link when `claimUrl` is not null. Read it as the only signal: any `linkType` value means it is a link you can send. Encore may add values to `linkType`, `reason` and `status`, so treat a `reason` you do not recognize as "no link", and a `status` you do not recognize as closed.

`advertiser` links are the brand's own link, and Encore sets no expiry on them. They keep crediting the claim for as long as the brand's network attributes it, often around 30 days. Send it within days of the claim, not weeks later.

### When there is no link

| `reason` | Meaning |
| - | - |
| `completed` | The user already finished this offer. |
| `revoked` | The claim's entitlement was revoked. |
| `closed` | The claim can no longer be credited. |
| `campaign_inactive` | The offer has ended or is paused. |
| `not_per_user` | This offer's link credits your app, not a particular user (for example Amazon offers), so there is no per-user link to send. |
| `no_destination` | The offer has no link configured. |
| `demo` | A demo offer, which test keys see. It has no real brand link. |

### Test keys

A test key (`pk_test_*` with its secret) reads only claims made in a development environment: with a test key, or from a debug build. It gets `404` for every live user's claim. Claims made from a debug build on a live key also read with `isTestRequest: true`, and their links are real, so test your email flow with your own account.

## Status codes

| Code | Meaning |
| - | - |
| `200` | Found. Check `claimUrl`. It is null when `reason` explains why there is no link. |
| `400` | `X-API-Key`, `X-Signature` or `X-Timestamp` is missing or malformed (a non-hex signature, a non-numeric timestamp), the transaction id is not a UUID, or `userId` is empty or over 256 characters. |
| `401` | The signature does not verify, `X-Timestamp` is skewed over 5 minutes, the key does not resolve, or `X-Platform` is missing, not a supported value, or names a platform no app in a multi-app project uses. |
| `404` | No such claim in your project, `userId` is not the user who made it, or the user's data was erased. |
| `429` | Over 600 lookups per minute for your project, or Encore could not check the limit. Wait for `Retry-After` and retry. |
| `5xx` | Encore-side failure. Retry with backoff. |

## Related

* `placement()` result, where `claim.transactionId` comes from in your app: [iOS](https://docs.encorekit.com/publishers/ios/sdk-reference/presentation-result.md), [Android](https://docs.encorekit.com/publishers/android/sdk-reference/presentation-result.md), [React Native](https://docs.encorekit.com/publishers/react-native/sdk-reference/presentation-result.md), [Flutter](https://docs.encorekit.com/publishers/flutter/sdk-reference/presentation-result.md).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.