Flash ProtocolFLASH PROTOCOL
DOCUMENTATION

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.

All webhook API endpoints require authentication via your API key in the 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.

server.js
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.

Register via API
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...
Save your secret now. It is only returned once. Store it in an environment variable like 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:

Check deliveries
// 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 }
Tip: During development, use a service like 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.

Event
Description
payment.completed
A transaction finished successfully.
payment.failed
A transaction failed or polling timed out.
link.expired
A payment link passed its expiration time.

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.

POST/api/v1/webhooks

Request Body

Field
Type
Required
Description
url
string
Yes
HTTPS endpoint URL.
events
string[]
Yes
Event types to subscribe to.
description
string
No
Label for this endpoint (max 255 chars).

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"
  }'
201 Created
{
  "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."
}
Save your secret immediately. The 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.

GET/api/v1/webhooks
curl -X GET https://flash-protocol.vercel.app/api/v1/webhooks \
  -H "Authorization: Bearer pg_live_..."
200 OK
{
  "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.

GET/api/v1/webhooks/:id
200 OK
{
  "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.

PATCH/api/v1/webhooks/:id
curl -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.

DELETE/api/v1/webhooks/:id
curl -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.

POST/api/v1/webhooks/:id/replay
curl -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:

payment.completed
{
  "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:

Additional fields for payment.failed
{
  "error_message": "Polling timeout: max retries reached",
  "failure_stage": "bridge"
}

Delivery Headers

Every webhook POST includes these headers for verification and tracing:

Header
Description
X-Flash-Signature
HMAC-SHA256 signature: sha256=<hex>
X-Flash-Event
Event type (e.g. payment.completed)
X-Flash-Delivery-Id
Unique ID for this delivery attempt.
X-Flash-Timestamp
Unix timestamp (for replay protection).
Content-Type
application/json
User-Agent
FlashProtocol-Webhook/1.0

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');
});
Replay protection: Check the 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:

Attempt
Approximate Delay
1 (initial)
Immediate
2
~30 seconds
3
~2 minutes
4
~8 minutes
5
~30 minutes
6
~2 hours
7
~8 hours
8
~24 hours

After all retries are exhausted, the delivery is marked as failed. You can manually replay failed deliveries via the API or the Dashboard.

Limits

Setting
Value
Max endpoints per merchant
5
Delivery timeout
5 seconds
Max retries
8 (exponential backoff)
Delivery log retention
30 days
URL protocol
HTTPS only

Error Responses

Status
Code
Description
400
Validation Error
Invalid URL, missing events, or limit exceeded.
401
Unauthorized
Invalid or missing API key.
404
Not Found
Endpoint or delivery ID not found.
409
Conflict
Endpoint with this URL already exists.