Webhooks
Receive real-time notifications for payment events.
Overview
Instead of polling the Transactions API for status changes, you can register webhook endpoints to receive real-time HTTP POST notifications when payment events occur. Flash Protocol signs every payload with HMAC-SHA256 so you can verify authenticity.
Deliveries are retried automatically with exponential backoff (up to 8 retries over ~24 hours) if your server returns a non-2xx response or times out.
Authorization header. See Authentication for details.Quick Start
Get webhook notifications working in 3 steps:
Step 1: Create an endpoint on your server
Set up an HTTPS route that accepts POST requests. Your endpoint must respond with a 2xx status within 5 seconds, otherwise the delivery is treated as failed and retried.
import crypto from 'crypto';
import express from 'express';
const app = express();
// IMPORTANT: You need the raw body string for signature verification.
// Use express.raw() or a middleware that preserves it.
app.use('/webhooks/flash', express.raw({ type: 'application/json' }));
app.post('/webhooks/flash', (req, res) => {
const rawBody = req.body.toString();
const signature = req.headers['x-flash-signature'];
// 1. Verify signature
const secret = process.env.FLASH_WEBHOOK_SECRET; // whsec_...
const rawSecret = secret.startsWith('whsec_') ? secret.slice(6) : secret;
const expected = 'sha256=' + crypto
.createHmac('sha256', rawSecret)
.update(rawBody)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).send('Invalid signature');
}
// 2. Check timestamp to prevent replay attacks (reject if older than 5 min)
const timestamp = parseInt(req.headers['x-flash-timestamp']);
if (Math.abs(Date.now() / 1000 - timestamp) > 300) {
return res.status(401).send('Stale timestamp');
}
// 3. Process the event
const event = JSON.parse(rawBody);
switch (event.type) {
case 'payment.completed':
fulfillOrder(event.data.payment_link_id, event.data.transaction_id);
break;
case 'payment.failed':
markOrderFailed(event.data.payment_link_id, event.data.error_message);
break;
}
// 4. Respond 200 — this tells Flash Protocol the delivery succeeded
res.status(200).send('OK');
});Step 2: Register your endpoint with Flash Protocol
Call the API to register your URL and choose which events you want to receive. Save the secret from the response — you will need it to verify signatures.
const response = await fetch('https://flash-protocol.vercel.app/api/v1/webhooks', {
method: 'POST',
headers: {
'Authorization': 'Bearer pg_live_...',
'Content-Type': 'application/json'
},
body: JSON.stringify({
url: 'https://yoursite.com/webhooks/flash',
events: ['payment.completed', 'payment.failed']
})
});
const { secret } = await response.json();
// Store this as FLASH_WEBHOOK_SECRET in your environment variables
// e.g. whsec_a1b2c3d4e5f6...FLASH_WEBHOOK_SECRET on your server.Step 3: Test it
Create a payment link and complete a test payment. When the payment finishes, your endpoint will receive a payment.completed POST within seconds. You can check delivery status in the Dashboard → Settings → Webhooks page, or via the API:
// List your endpoints and see delivery stats
const response = await fetch('https://flash-protocol.vercel.app/api/v1/webhooks', {
headers: { 'Authorization': 'Bearer pg_live_...' }
});
const { data } = await response.json();
console.log(data[0].recent_deliveries);
// { total: 1, successful: 1, failed: 0 }webhook.site or ngrok to expose your local server and inspect incoming webhook payloads.Event Types
Subscribe your endpoints to one or more event types. New event types can be added without breaking existing integrations.
Register an Endpoint
Register an HTTPS URL to receive webhook deliveries. You can register up to 5 endpoints per merchant. The response includes a secret (prefixed with whsec_) that you must save immediately — it is only shown once.
/api/v1/webhooksRequest Body
Request Example
curl -X POST https://flash-protocol.vercel.app/api/v1/webhooks \
-H "Authorization: Bearer pg_live_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://yoursite.com/webhooks/flash",
"events": ["payment.completed", "payment.failed"],
"description": "Production webhook"
}'{
"id": "550e8400-e29b-41d4-a716-446655440000",
"url": "https://yoursite.com/webhooks/flash",
"events": ["payment.completed", "payment.failed"],
"description": "Production webhook",
"active": true,
"created_at": "2026-03-30T12:00:00.000Z",
"secret": "whsec_a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2",
"warning": "Save this secret securely. You won't see it again."
}whsec_ signing secret is only returned once during creation. If you lose it, delete the endpoint and create a new one.List Endpoints
Retrieve all webhook endpoints for your merchant account, with delivery statistics.
/api/v1/webhookscurl -X GET https://flash-protocol.vercel.app/api/v1/webhooks \
-H "Authorization: Bearer pg_live_..."{
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"url": "https://yoursite.com/webhooks/flash",
"events": ["payment.completed", "payment.failed"],
"description": "Production webhook",
"active": true,
"created_at": "2026-03-30T12:00:00.000Z",
"recent_deliveries": {
"total": 142,
"successful": 138,
"failed": 4
}
}
]
}Get Endpoint Details
Retrieve details for a single endpoint, including its 20 most recent delivery attempts.
/api/v1/webhooks/:id{
"id": "550e8400-e29b-41d4-a716-446655440000",
"url": "https://yoursite.com/webhooks/flash",
"events": ["payment.completed", "payment.failed"],
"active": true,
"deliveries": [
{
"id": "delivery-uuid",
"event_type": "payment.completed",
"response_status": 200,
"delivered": true,
"duration_ms": 120,
"attempt": 1,
"created_at": "2026-03-30T12:01:00.000Z"
},
{
"id": "delivery-uuid-2",
"event_type": "payment.failed",
"response_status": 500,
"error_message": null,
"delivered": false,
"duration_ms": 340,
"attempt": 1,
"created_at": "2026-03-30T11:55:00.000Z"
}
]
}Update Endpoint
Update the URL, subscribed events, description, or active status. All fields are optional. You cannot change the signing secret — delete and recreate the endpoint instead.
/api/v1/webhooks/:idcurl -X PATCH https://flash-protocol.vercel.app/api/v1/webhooks/550e8400... \
-H "Authorization: Bearer pg_live_..." \
-H "Content-Type: application/json" \
-d '{
"events": ["payment.completed"],
"active": false
}'Delete Endpoint
Permanently delete a webhook endpoint. Delivery logs are retained for 30 days.
/api/v1/webhooks/:idcurl -X DELETE https://flash-protocol.vercel.app/api/v1/webhooks/550e8400... \
-H "Authorization: Bearer pg_live_..."Replay a Delivery
Re-send a specific failed delivery with the original payload. A new delivery log entry is created.
/api/v1/webhooks/:id/replaycurl -X POST https://flash-protocol.vercel.app/api/v1/webhooks/550e8400.../replay \
-H "Authorization: Bearer pg_live_..." \
-H "Content-Type: application/json" \
-d '{ "delivery_id": "delivery-uuid" }'Payload Format
Every webhook delivery POSTs a JSON body with this structure:
{
"id": "evt_a1b2c3d4e5f6a7b8c9d0e1f2",
"type": "payment.completed",
"created_at": "2026-03-30T12:00:00.000Z",
"data": {
"transaction_id": "tx-uuid",
"payment_link_id": "pl-uuid",
"status": "completed",
"customer_wallet": "0xabc...",
"from_chain_id": "137",
"from_token_symbol": "USDC",
"from_amount": "50.00",
"to_chain_id": "1",
"to_token_symbol": "ETH",
"to_amount": "0.025",
"provider": "lifi",
"source_tx_hash": "0x...",
"dest_tx_hash": "0x...",
"completed_at": "2026-03-30T12:00:00.000Z"
}
}For payment.failed events, the data object also includes:
{
"error_message": "Polling timeout: max retries reached",
"failure_stage": "bridge"
}Delivery Headers
Every webhook POST includes these headers for verification and tracing:
sha256=<hex>payment.completed)Verifying Signatures
Always verify the X-Flash-Signature header to ensure the payload was sent by Flash Protocol and has not been tampered with. The HMAC key is the hex portion of your secret — strip the whsec_ prefix before using it.
import crypto from 'crypto';
function verifyWebhook(rawBody, signature, secret) {
// Strip the whsec_ prefix before using as HMAC key
const rawSecret = secret.startsWith('whsec_') ? secret.slice(6) : secret;
const expected = 'sha256=' + crypto
.createHmac('sha256', rawSecret)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// Express example
app.post('/webhooks/flash', (req, res) => {
const signature = req.headers['x-flash-signature'];
const isValid = verifyWebhook(req.rawBody, signature, process.env.FLASH_WEBHOOK_SECRET);
if (!isValid) {
return res.status(401).send('Invalid signature');
}
const event = req.body;
switch (event.type) {
case 'payment.completed':
// Fulfill the order
fulfillOrder(event.data.payment_link_id, event.data.transaction_id);
break;
case 'payment.failed':
// Handle failure
markOrderFailed(event.data.payment_link_id, event.data.error_message);
break;
}
res.status(200).send('OK');
});X-Flash-Timestamp header and reject payloads older than 5 minutes to prevent replay attacks.Retries & Delivery
If your endpoint returns a non-2xx status code, times out (5-second limit), or is unreachable, Flash Protocol retries the delivery automatically with exponential backoff:
After all retries are exhausted, the delivery is marked as failed. You can manually replay failed deliveries via the API or the Dashboard.