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

# offer_completed webhook

> Payload contract for the offer_completed webhook: the HTTPS POST Encore sends to your server when a completion is verified.

An outbound webhook is an endpoint **you** implement: this page is the contract of the request Encore sends when one of your users' offer completions is verified, the response Encore expects back, and the delivery semantics.

<Note>
  For wiring it up (dashboard setup, a runnable signature-verification handler, idempotency, and reconciliation), see the [Receive Completion Events guide](/publishers/offers-api/guides/receive-completion-events). For the *inbound* direction (you forward your own billing's subscription events into Encore), see [Webhook Ingestion](/publishers/web/sdk-reference/webhook-ingestion): an unrelated flow with different headers and signing.
</Note>

## The request Encore sends

One event type, `POST`ed once to the URL you configure, at the moment a transaction is marked **verified** (driven by the advertiser's conversion postback, not by the user tapping or claiming).

```http theme={null}
POST /webhooks/encore HTTP/1.1
Content-Type: application/json
X-Webhook-Signature: 5a4f2e8c1b3d9e7f6a0c8b2d4e6f1a3c5b7d9e0f...
X-Webhook-Timestamp: 1772044200
```

### Headers

| Header                | Value                                                                                                                               |
| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
| `Content-Type`        | Always `application/json`.                                                                                                          |
| `X-Webhook-Signature` | Hex-encoded HMAC-SHA256 over the [signature base string](#signature), keyed by your app's signing secret (`whsec_...`).             |
| `X-Webhook-Timestamp` | Unix time in **seconds** at the moment Encore signed. This value goes into the signature base string; use it for a freshness check. |

### Body

```json theme={null}
{
  "event": "offer_completed",
  "timestamp": "2026-02-25T18:30:00.000Z",
  "transactionId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "userId": "usr_9f8e7d6c-5b4a-3210-fedc-ba0987654321",
  "campaignName": "Audible Premium Plus",
  "payout": 10
}
```

| Field           | Type             | Description                                                                                                                                                                                                                                                                                                                                       |
| --------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `event`         | string           | Always `"offer_completed"`, currently the only event type Encore sends. Still switch on it, so a future event type doesn't fall through your handler.                                                                                                                                                                                             |
| `timestamp`     | ISO 8601 string  | UTC, milliseconds, trailing `Z`: when Encore generated this delivery. A *different format* from the `X-Webhook-Timestamp` header (Unix seconds); the header is the one you sign and check for freshness.                                                                                                                                          |
| `transactionId` | string (UUID)    | The Encore transaction that completed. **Treat this as your idempotency key.**                                                                                                                                                                                                                                                                    |
| `userId`        | string           | The end-user identifier **as your app supplied it, verbatim**: the value you passed to `identify()` or as `userId` on an Encore API call. Encore stores it opaquely and echoes it back unchanged, so it's the key you look the user up by. If you never supplied an id, this is the SDK's anonymous per-device id.                                |
| `campaignName`  | string           | Human-readable name of the campaign the user completed. A display/logging value, not a stable identifier; campaign names can change.                                                                                                                                                                                                              |
| `payout`        | number \| `null` | The payout amount associated with this completion, when one was reported. **`null` is normal and common**: several completion paths carry no amount at delivery time, so code for `null` rather than treating it as an error. No currency field is delivered. Treat your dashboard analytics, not this field, as the source of truth for revenue. |

<Note>
  **That's the whole body.** There is no app id, campaign id, creative id, or event id, and no nesting: the payload is flat, exactly the six fields above. A handler written against a nested `req.body.data` won't work; read the fields off the top level. The `usr_...` shape above is only a placeholder from the test-delivery sample; Encore mints no user ids and adds no prefix.
</Note>

## Signature

The signature base string is the timestamp header and the **raw request body**, joined by a single dot:

```
<X-Webhook-Timestamp>.<raw_body>
```

HMAC-SHA256 that string with your `whsec_...` signing secret and hex-encode it. The result must equal `X-Webhook-Signature`. Verify against the exact bytes Encore sent, not a re-serialization; a runnable handler is in the [guide](/publishers/offers-api/guides/receive-completion-events#verify-the-signature).

## The response Encore expects

Return any `2xx` within **5 seconds**. Anything else fails the delivery:

| Behaviour                       | What happens                                                                                                   |
| ------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| **Retries**                     | **None.** A delivery is attempted exactly once.                                                                |
| **Timeout**                     | 5 seconds. A slower response is abandoned and the event is dropped.                                            |
| **Non-2xx response**            | Logged on Encore's side and dropped. Not retried, not queued, not replayable from the dashboard.               |
| **Redirects**                   | Deliveries are sent with redirects disabled; a `301`/`302` fails the delivery outright. Publish the final URL. |
| **Webhook disabled or deleted** | Nothing is sent, and nothing is backfilled when you re-enable it.                                              |
| **Ordering**                    | Not guaranteed.                                                                                                |
| **Event id**                    | None delivered. Use `transactionId` for deduplication.                                                         |

## URL requirements

| Requirement                  | Why                                                                                                                                                                                     |
| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **HTTPS**                    | Plain `http://` URLs are rejected. Payloads carry user identifiers.                                                                                                                     |
| **Publicly resolvable host** | An SSRF guard resolves the hostname before every delivery and refuses `localhost` plus private, loopback, and link-local addresses, including a public hostname that *resolves* to one. |
| **No redirects**             | Deliveries are sent with redirects disabled.                                                                                                                                            |

## Related

* [Receive Completion Events guide](/publishers/offers-api/guides/receive-completion-events): setup, verification handler, idempotency, reconciliation, local testing.
* [API Reference overview](/publishers/offers-api/overview#the-claim-model): why a claim is not a conversion, and where completion fits.
* [Webhook Ingestion](/publishers/web/sdk-reference/webhook-ingestion): the inbound direction, for forwarding your own subscription events into Encore.
