> ## Documentation Index
> Fetch the complete documentation index at: https://yieldxyz.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Receive webhooks

> Create an endpoint and subscription, then build a receiver that verifies, deduplicates, and processes transaction and position events.

Build a receiver for yield.xyz webhook deliveries: create an endpoint and subscription in the dashboard, then verify, deduplicate, and process each delivery. This guide covers the receiving side. See [Webhooks](/documentation/advanced-setup/webhooks) for the event model, delivery statuses, and retry schedule.

## Prerequisites

* The owner, admin, or operator role on the team, to create endpoints and subscriptions. Every role can view them. See [Roles and permissions](/documentation/advanced-setup/roles-permissions).
* An HTTPS URL you control, served by a handler that can read the raw, unparsed request body.

## Create an endpoint and subscription

<Steps>
  <Step title="Create the endpoint">
    In the project page's **Webhooks** section, click **Create endpoint** and fill in:

    * **URL**: your HTTPS receiver.
    * **Secret**: at least 24 characters. You choose this value; it isn't generated for you.
    * **Description** (optional).
  </Step>

  <Step title="Create a subscription">
    On the same endpoint, click **Create subscription**, then expand **Advanced: resources & actions** to pick what it receives:

    * **Resources**: `transaction`, `position`.
    * **Actions**: `status_changed`.

    Combined, a resource and action give you an event type: `transaction.status_changed` or `position.status_changed`. Those are the only two the dashboard offers today; select one or both.
  </Step>

  <Step title="Optionally narrow delivery with filters">
    The subscription dialog also has a `filtersJson` field, a JSON object matched against each event's subject. Populated keys must all match; an omitted key matches everything.

    ```json theme={null}
    { "networks": ["ethereum", "base"], "addresses": ["0xYOUR_WALLET_ADDRESS"] }
    ```

    `filtersJson` accepts `yield_ids`, `addresses`, `balance_types`, and `networks`, each a list of strings. For the two live event types, only `addresses` and `networks` narrow anything: `transaction.status_changed` and `position.status_changed` events carry `address` and `network` on their subject, but never a `yield_id` or `balance_type`. Setting `yield_ids` or `balance_types` filters out every event.
  </Step>
</Steps>

## Signature verification

Every delivery is a signed `POST`. Read the raw request body before any JSON-parsing middleware touches it: the signature covers the exact bytes sent, and re-serializing the parsed body will not reproduce it.

Headers on every delivery:

| Header | Value |
| - | - |
| `X-Yield-Delivery` | This delivery's id |
| `X-Yield-Event` | The resource (`transaction` or `position`) |
| `X-Yield-Action` | The action (`status_changed`) |
| `X-Yield-Timestamp` | Unix seconds when the request was signed |
| `X-Yield-Signature` | `sha256=<hex HMAC>` |

The signature is an HMAC-SHA256 over `{timestamp}.{rawBody}` (the timestamp header value, a literal `.`, then the raw body bytes), keyed with the endpoint's secret, hex-encoded and prefixed `sha256=`. Compute the same value and compare it in constant time:

```typescript theme={null}
import { createHmac, timingSafeEqual } from "node:crypto";

function verifySignature(rawBody: string, timestamp: string, signatureHeader: string, secret: string): boolean {
  const expectedHex = createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
  const expected = `sha256=${expectedHex}`;

  const a = Buffer.from(signatureHeader);
  const b = Buffer.from(expected);
  if (a.length !== b.length) return false;
  return timingSafeEqual(a, b);
}
```

<Warning>
  Reject the request if verification fails, and reject a timestamp that's too old to guard against replay. Never process a delivery before verifying its signature.
</Warning>

## Respond quickly

yield.xyz waits up to 15 seconds for a response; any 2xx status counts as delivered. Acknowledge with a 2xx as soon as you've verified the signature and durably queued the event, then do slower processing (database writes, downstream calls) after responding, not before.

## Deduplicate on the event id

Delivery isn't exactly once: the same event can arrive more than once, for example after a retry that actually succeeded on your end. Each event has a unique `id`; store it and skip an event you've already processed, rather than assuming one delivery per event.

## Payload

```json Payload (excerpt, transaction.status_changed) theme={null}
{
  "id": "evt_...",
  "type": "transaction.status_changed",
  "resource": "transaction",
  "action": "status_changed",
  "created_at": "2026-09-30T12:00:00.000Z",
  "project_id": "...",
  "subject": { "transaction_id": "tx_...", "network": "ethereum", "address": "0x..." },
  "data": {
    "id": "tx_...",
    "network": "ethereum",
    "status": "CONFIRMED",
    "type": "STAKE",
    "hash": "0x...",
    "createdAt": "2026-09-30T11:59:40.000Z",
    "broadcastedAt": "2026-09-30T11:59:45.000Z",
    "error": null,
    "address": "0x..."
  }
}
```

`position.status_changed` follows the same envelope, with a `data` object of `position_id`, `status`, `completedAt`, `integrationId`, `address`, `type`, and (when applicable) `validatorAddress`.

## Retries and out-of-order delivery

Failed deliveries are retried with increasing backoff; see [Webhooks](/documentation/advanced-setup/webhooks) for the schedule. Because of retries, a later delivery for the same subject can arrive after an earlier one you already processed, or two deliveries can arrive out of order. Treat `data` as the current state at delivery time, not as an incremental update, and let a newer event overwrite what an older one wrote rather than assuming strict ordering. If you need to confirm a transaction's latest state independent of webhook timing, re-read it directly with `GET /v1/transactions/{transactionId}` (see [Enter and exit flows](/guides/earn/enter-exit-flows)).

<Note>
  Webhooks report the same status transitions the Yield API exposes; nothing here is guaranteed exactly-once or strictly ordered. Poll as the source of truth when a workflow depends on it, and use webhooks to avoid polling continuously.
</Note>

## Verify

After creating an endpoint and subscription, trigger an action so a matching event fires, then open the endpoint's delivery history in the dashboard: each entry shows its status (pending, sending, delivered, retry scheduled, or dead), attempt count, and the response your receiver returned. A `delivered` entry with a 2xx response confirms your receiver is reachable, signed correctly, and responding within the timeout.

## Next steps

<Columns cols={2}>
  <Card title="Webhooks" icon="webhook" href="/documentation/advanced-setup/webhooks">
    The event model, delivery statuses, and retry schedule.
  </Card>

  <Card title="Enter and exit flows" icon="arrow-right-arrow-left" href="/guides/earn/enter-exit-flows">
    Poll transactions directly instead of, or alongside, webhooks.
  </Card>

  <Card title="Monitoring and alerting" icon="bell" href="/guides/earn/monitoring-alerting">
    Combine webhooks with polling to keep position state current.
  </Card>

  <Card title="Manage team roles" icon="user-shield" href="/guides/enterprise/rbac">
    Who can create and manage webhook endpoints.
  </Card>
</Columns>


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