Prerequisites
- The owner, admin, or operator role on the team, to create endpoints and subscriptions. Every role can view them. See Roles and permissions.
- A publicly reachable HTTPS URL you control, served by a handler that can read the raw, unparsed request body.
Create an endpoint and subscription
1
Create the endpoint
In the project page’s Webhooks section, click Create endpoint and fill in:
- URL: your receiver.
- Secret (min 24 chars): between 24 and 255 characters. You choose this value; it isn’t generated for you, and it isn’t shown again after you save, so store it now.
- Description (optional).
2
Create a subscription
With the endpoint open, click Create subscription, then expand Advanced: resources & actions to pick what it receives:
- Resources:
transaction,position. - Actions:
status_changed.
transaction.status_changed or position.status_changed. Those are the only two event types emitted today; select one or both resources. Click Create. If you need a webhook for something else, contact the Yield.xyz team and we will help get it added.3
Optionally narrow delivery with filters
The subscription dialog also has a filtersJson (JSON) field, a JSON object matched against each event’s
subject. Every key you set must match; an omitted or empty key matches everything.filtersJson accepts yield_ids, addresses, balance_types, and networks, each a list of strings. Values are compared as exact strings, so an address must match the casing Yield.xyz stores. For the two live event types, only addresses and networks narrow anything: both carry network and, when the action has a wallet address, address on their subject, but never a yield_id or balance_type. A filter on a key the subject doesn’t carry never matches, so setting yield_ids or balance_types filters out every event.Disabling an endpoint doesn’t pause delivery: while it’s disabled, every delivery for it, including ones already queued, is marked
dead when Yield.xyz picks it up, and nothing is resent when you re-enable it. Disabling a subscription stops new deliveries from being created for it. To delete an endpoint, delete its subscriptions first.What a delivery looks like
Every delivery is one signedPOST with a JSON body. These headers are set on every request:
No sequence or idempotency header is sent; both live in the body. Yield.xyz doesn’t set a dedicated
User-Agent, so don’t filter on one.
The body is the event envelope. Every live event has these top-level fields:
Verify the signature
Read the raw request body before any JSON-parsing middleware touches it. The signature covers the exact bytes sent (compact JSON, no whitespace), and re-serializing a parsed body won’t reproduce them. The signature is an HMAC-SHA256 over{timestamp}.{rawBody}: the X-Yield-Timestamp value, a literal ., then the raw body bytes. It’s keyed with the endpoint’s secret, hex-encoded, and prefixed sha256=. There is one secret and one signature per endpoint; a retry is signed fresh, with a new timestamp and signature over the same body. Compute the same value and compare it in constant time:
Verify a delivery
req.body is the untouched Buffer:
Express receiver
Respond quickly
Yield.xyz waits up to 15 seconds for a response, and any 2xx status counts as delivered. Anything else, including a timeout, a connection error, or a non-2xx status, is a failed attempt that will be retried. 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. Answer from the endpoint URL itself. A test send refuses redirects, and redirect handling on regular deliveries isn’t something to depend on, so point the endpoint at its final URL.Deduplicate on the event id
Delivery isn’t exactly once. The same event can reach you more than once:- A retry after an attempt that timed out on Yield.xyz’s side but succeeded on yours.
- Recovery after an interruption mid-send, which requeues the delivery.
- Two subscriptions on the same endpoint that both match the event. Each produces its own delivery, with its own
X-Yield-Deliveryid, carrying the same event.
id and skip one you’ve already processed. The id is the same in every copy, which X-Yield-Delivery is not.
Order with sequence
Retries and parallel sends mean deliveries for the same subject can arrive out of order. Each event carries asequence that Yield.xyz allocates per subject within your project: one counter per transaction (keyed on subject.transaction_id) and one per position (keyed on subject.position_id). It starts at 1 and only ever increases for that subject, and it’s the same number in every copy of the event, on every endpoint.
To apply events in order, keep the highest sequence you’ve processed per subject and ignore any event whose sequence is lower than or equal to it. Treat data as the state at the time of the event, not as an incremental update, so a newer event can simply overwrite what an older one wrote. Three notes:
- Gaps are normal. A number can be consumed without an event reaching you, so never wait for a missing sequence.
- Counters are per subject. A transaction’s
sequencesays nothing about its position’ssequence, and nothing about any other transaction. - Terminal statuses win.
sequencefollows the order events were recorded, which can differ from the order of the statuses: in rare cases a transaction’sCONFIRMEDevent gets a lowersequencethan itsBROADCASTEDevent.CONFIRMEDandFAILEDare terminal for a transaction, andSUCCESSandFAILEDare terminal for a position, so never overwrite a terminal status with a non-terminal one, whatever thesequence.
GET /v1/transactions/{transactionId} (see Enter and exit flows).
Webhooks report the same status transitions the Earn API exposes; nothing here is guaranteed exactly once or strictly ordered on arrival. Poll as the source of truth when a workflow depends on it, and use webhooks to avoid polling continuously.
Payloads
Two event types are emitted today. Ids below are placeholders.transaction.status_changed
Fires when a transaction’s status becomesBROADCASTED (after you submit a hash or a signed transaction), CONFIRMED or FAILED (from on-chain tracking), or FAILED because a submit was rejected. Earlier statuses such as CREATED and WAITING_FOR_SIGNATURE don’t fire a webhook.
transaction.status_changed
data.status is one of BROADCASTED, CONFIRMED, or FAILED; data.type is the transaction type (STAKE, UNSTAKE, CLAIM_REWARDS, and so on); data.error carries the failure reason when one is known and is null otherwise, including on every BROADCASTED and CONFIRMED event. address is omitted from subject and data when the action has no wallet address.
The BROADCASTED event, and a FAILED event raised by a rejected submit, also carry the transition:
Extra fields on a submit-driven event
CONFIRMED, and FAILED from a reverted transaction) omit previous and changes.
position.status_changed
Fires once when an action reaches a terminal status:SUCCESS when its last transaction confirms, or FAILED when a transaction fails on chain. An action that fails because a submit was rejected doesn’t fire this event; you get only the transaction.status_changed FAILED event. The position_id is the action’s id.
position.status_changed
data.status is SUCCESS or FAILED; data.type is the action type (STAKE, UNSTAKE, CLAIM_REWARDS, and so on); data.integrationId is the yield id. validatorAddress appears only when the action targets a validator, and address only when the action has a wallet address. Position events never carry previous or changes.
Retries and dead deliveries
A failed attempt is retried with increasing waits: 5 minutes, then 15 minutes, 1 hour, 6 hours, and 24 hours between each later attempt, with no jitter. After the tenth failed attempt the delivery is markeddead and not sent again; there is no way to replay a dead delivery today, so reconcile by re-reading the resource. See Webhooks for the full schedule and statuses.
Send a test event
A test send posts a signedwebhook.test event to an endpoint and returns the result immediately. It uses the same headers and signing scheme as a real delivery, works on a disabled endpoint, matches no subscription, and isn’t written to the delivery history. Redirects aren’t followed, so a 3xx comes back as a failed test. The receiver has 5 seconds to answer, and the endpoint must be a public address: a private, loopback, or link-local target, or a public hostname that resolves to one, is rejected with 412.
The dashboard doesn’t have a test button yet. The test send is exposed by the dashboard API, for a signed-in user with the owner, admin, or operator role. It authenticates with your dashboard session, not a project or admin API key, so it’s meant for checking an endpoint by hand rather than for calling from your integration:
subject, sequence, or idempotency_key. Tell it apart from real traffic by type, and return a 2xx so the test passes:
webhook.test
delivered (true on a 2xx within the timeout), httpStatus (or null if the endpoint never answered), durationMs, timeoutMs, errorCode (timeout, dns, conn_refused, network, non_2xx, or unknown), errorMessage, responseSnippet (the first 1000 characters of your response body), testId (the X-Yield-Delivery value the ping carried), and sentAt.
Verify
After creating an endpoint and subscription, trigger an action so a matching event fires, then open the endpoint with Show and click Show deliveries. The table lists deliveries newest first with Status (pending, sending, delivered, retry_scheduled, or dead), Attempts, Last HTTP, Last error, and Last attempt. Click Info on a row to open the stored event: its type, schema version, sequence, and the meta, data, and changes objects exactly as sent. A delivered row with a 2xx under Last HTTP confirms your receiver is reachable, verifies the signature correctly, and responds within the timeout.
Next steps
Webhooks
The event model, delivery statuses, and retry schedule.
Enter and exit flows
Poll transactions directly instead of, or alongside, webhooks.
Monitoring and alerting
Combine webhooks with polling to keep position state current.
Manage team roles
Who can create and manage webhook endpoints.

