Webhooks
Receive a signed JSON POST when your Phoenixd node receives a payment. Each node supports one webhook endpoint.
Add an endpoint
- Create a backend route that accepts
POSTrequests at a public HTTPS URL, such ashttps://example.com/webhooks/nodana. Use the final URL without redirects. For local testing, use a public HTTPS tunnel. - Open your node in the Nodana dashboard, select Webhooks, and click Add endpoint.
- Enter your URL and an optional description, then click Add endpoint.
- Copy the signing secret and store it in your backend, for example as
NODANA_WEBHOOK_SECRET. It is only shown once. If lost, delete and recreate the endpoint, then update your receiver.
Receive an event
The request body is the Phoenixd event payload. A received payment includes fields such as:
{
"type": "payment_received",
"amountSat": 1000,
"paymentHash": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
}| Header | Purpose |
|---|---|
X-Nodana-Signature | Signature used to verify the raw request body |
X-Nodana-Event-Id | Delivery ID for detecting repeated deliveries |
X-Nodana-Event-Type | Event type, such as payment_received |
Verify the signature before acting on the event. Handle payment_received and tolerate additional payload fields. Acknowledge verified event types you do not use with a 2xx response.
Verify the signature
Use the entire signing secret, including its whsec_ prefix, as a UTF-8 HMAC-SHA256 key. Compare the digest of the exact request body bytes with the hex value in X-Nodana-Signature using a constant-time comparison.
This Node.js helper accepts the raw body as a Buffer and the signature header:
import { createHmac, timingSafeEqual } from "node:crypto";
const secret = process.env.NODANA_WEBHOOK_SECRET;
if (!secret) throw new Error("Set NODANA_WEBHOOK_SECRET before starting");
function validSignature(body, signature) {
if (typeof signature !== "string" || !/^[a-f0-9]{64}$/i.test(signature)) {
return false;
}
const expected = createHmac("sha256", secret).update(body).digest();
return timingSafeEqual(expected, Buffer.from(signature, "hex"));
}Preserve the raw body in your framework: parsing and re-serialising JSON changes the signed bytes. Reject invalid signatures before processing the payload. Keep the secret out of browser code and logs.
Acknowledge deliveries
Verify the signature, then safely store or queue the event before returning 2xx. Return an error if storage fails so Nodana can retry. Keep slow processing outside the receiver: deliveries time out after 10 seconds.
Store X-Nodana-Event-Id durably with a uniqueness constraint and acknowledge repeated accepted deliveries with 2xx. Also prevent duplicate payment fulfilment using the node and paymentHash. Do not rely on delivery order.
Failed deliveries are retried up to three times, after approximately 1 minute, 5 minutes, and 30 minutes following each failure.
Test and troubleshoot
Create and pay an invoice on your node, then check your receiver and the node's Webhooks delivery history. A successful delivery shows delivered with your 2xx response.
- No delivery: check that the endpoint is enabled and publicly reachable over HTTPS. Confirm the exact path accepts
POSTwithout redirects or browser authentication. - Signature verification fails: check the signing secret and confirm the raw body is preserved before JSON parsing.
- Timeouts or repeated deliveries: acknowledge after durable storage and move slow work to a worker. Make sure duplicate events cannot fulfil a payment twice.
- Missed payments during an outage: reconcile with your node. Events received while the endpoint is disabled are not replayed when it is re-enabled.
Delivery history is retained for three days. Keep your own payment records; deleting an endpoint also removes its delivery history.