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 withPUT /customers/me/webhook. Email [email protected] with:
- the HTTPS URL that should receive
POSTrequests - any extra headers Pods should send on every delivery, such as
Authorization: Bearer <token>
GET /customers/me/webhook and PUT /customers/me/webhook return only:
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.
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 beforeJSON.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 realACTION_UPDATE uses an action status such as SUCCESS, not confirmed.
Verify in Node
timingSafeEqual. The function throws when the buffers differ in length.
Delivery
Pods treats any HTTP2xx 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.