Skip to main content
Pods can POST action updates to an HTTPS endpoint you control. Use this when you want push updates alongside GET /actions/{id} and the WebSocket at wss://api.pods.finance/updates. The webhook body and the GET /actions/{id} body carry the same action object. The webhook adds a messageType envelope and three signature headers.

Register the endpoint

The default URL is saved from the Pods admin panel with PUT /customers/me/webhook. Email [email protected] with:
  • the HTTPS URL that should receive POST requests
  • any extra headers Pods should send on every delivery, such as Authorization: Bearer <token>
Support stores that URL and those headers, then shares the signing secret with you over a secure channel. GET /customers/me/webhook and PUT /customers/me/webhook return only:
The response leaves out the signing secret. Ask support for the secret after the URL is saved. There is no API field for it. The secret is one value per customer. It is whsec_ plus 32 random bytes encoded as 64 hex characters. Pods creates it the first time the admin panel saves a webhookURL. If a delivery happens before that row exists, Pods creates the same style of secret at send time. A later save of the URL reuses the existing secret. Headers you put in webhookHeaders are sent with every delivery. The names X-Deframe-Webhook-Id, X-Deframe-Webhook-Timestamp, and X-Deframe-Webhook-Signature are reserved. A customer header with one of those names is dropped, and the signature headers above are the ones that arrive. A single quote or strategy bytecode request can pass webhookURL to deliver that operation to a different URL. That value overrides the customer default for that quote or action. The signing secret stays the customer secret.

What arrives

ACTION_UPDATE is the event to handle. data is the formatted action returned by GET /actions/{id}: same id, type, status, amounts, and transactions.
The snippet above is only the envelope. A live delivery includes the full action object. Pods sends ACTION_UPDATE when an action that has a wallet address changes. You can receive several deliveries for one action (INITIAL, then PENDING, then SUCCESS, FAILED, REFUND_REQUIRED, or REFUNDED). EXPIRED stays on GET /actions/{id} and is not pushed. A provisional cross-chain transfer that the API later merges into the bridge action is also left off the webhook. A successful strategy, transfer, or fiat-ramp action can be followed by a legacy NEW_HISTORY delivery. It uses the same signature and a different data object (from, action, amount, fee, transactionHash, strategyId, and related fields). Read status from ACTION_UPDATE. Acknowledge NEW_HISTORY with 2xx if you do not use the legacy shape. Other messageType values can arrive for specific routes. A TeleSwap bitcoin bridge can send BTC_NATIVE_BRIDGE_TRANSACTION_STARTED. Respond 2xx for a type you do not handle, and keep the handler tolerant of new types.

Signature

Every delivery is signed. Read the three headers, then verify them against the raw request body before JSON.parse. The signed text is this UTF-8 string:
rawBody is the exact POST body: JSON.stringify({ messageType, data }). It is the bytes on the wire. A re-serialized object, a changed key order, or different whitespace produces a different signature. With Express, use express.raw() and verify req.body before you parse JSON. Pods does not reject an old timestamp. A five-minute window and deduplication by X-Deframe-Webhook-Id are checks you can add in your handler. The track and positions example shows both. They are not enforced by the sender.

Worked example

This body is a signature fixture. A real ACTION_UPDATE uses an action status such as SUCCESS, not confirmed.
Signed text:
Headers:

Verify in Node

Compare the hex digests with a length check before timingSafeEqual. The function throws when the buffers differ in length.

Delivery

Pods treats any HTTP 2xx as accepted. A timeout, a network error, or any other status is stored on the delivery and is not retried. After you acknowledge, do the slow work on your side. If a delivery is missing, read GET /actions/{id} for the current action. Keep the handler idempotent. The same action can move through more than one status, and each push has its own X-Deframe-Webhook-Id.