!IMPORTANT Your webhook endpoint must be publicly accessible via HTTPS and respond with a
2xxstatus code to confirm receipt.
Flow Payments sends webhooks for the following events:
| Event | Description |
|---|---|
invoice.created | A new invoice has been created |
invoice.confirmed | An invoice has received its first blockchain confirmation |
invoice.paid | An invoice has been successfully paid (sufficient confirmations) |
invoice.expired | An invoice has expired without being paid |
| Event | Description |
|---|---|
transaction.pending | A payment deposit has been detected on the blockchain |
transaction.confirmed | A payment deposit has received additional confirmations |
| Event | Description |
|---|---|
payout.completed | A payout has been successfully completed |
Each webhook sent contains the following information:
{
"event": "invoice.paid",
"data": {
"id": 123,
"external_id": "order_456",
"amount_fiat": "100.00",
"currency_fiat": "USD",
"status": "paid",
"asset": {
"id": 1,
"symbol": "BTC",
"name": "Bitcoin",
"chain": "bitcoin"
},
"address": {
"address": "bc1qxy2kgdygjrsqtzq2n0yrf2493p83kkfjhx0wlh"
},
"created_at": "2024-12-04T16:30:00.000000Z",
"expires_at": "2024-12-04T18:30:00.000000Z"
}
}
To ensure that webhooks actually come from Flow Payments, each webhook request includes an HMAC-SHA256 signature in the Signature header.
The signature is calculated using your signing key (provided when creating the webhook) and the request body.
<?php
function verifyWebhookSignature($payload, $signature, $signingKey) {
$computedSignature = hash_hmac('sha256', $payload, $signingKey);
return hash_equals($computedSignature, $signature);
}
// In your webhook endpoint
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_SIGNATURE'] ?? '';
$signingKey = 'your_signing_key';
if (!verifyWebhookSignature($payload, $signature, $signingKey)) {
http_response_code(401);
die('Invalid signature');
}
// Process the webhook
$data = json_decode($payload, true);
// ...
const crypto = require('crypto');
const express = require('express');
const app = express();
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['signature'];
const signingKey = 'your_signing_key';
const computedSignature = crypto
.createHmac('sha256', signingKey)
.update(req.body)
.digest('hex');
if (signature !== computedSignature) {
return res.status(401).send('Invalid signature');
}
// Process the webhook
const event = JSON.parse(req.body);
console.log('Event:', event.event);
res.status(200).send('OK');
});
!CAUTION Always verify the signature before processing a webhook. Never trust unverified data.
If your endpoint does not respond with a 2xx status code, Flow Payments will attempt to resend the webhook according to the following schedule:
!NOTE After 6 failed attempts, the webhook will be marked as failed and will no longer be resent. You can view failed webhooks in your dashboard.
Your endpoint should respond quickly (< 5 seconds) with a 2xx code. If you need to perform long processing:
// Respond immediately
http_response_code(200);
echo 'OK';
fastcgi_finish_request(); // Close the connection
// Process the webhook asynchronously
processWebhookAsync($data);
Webhooks can be sent multiple times. Ensure your code is idempotent:
// Use a unique identifier to avoid double processing
$eventId = $data['data']['id'];
$eventType = $data['event'];
if (hasProcessedEvent($eventId, $eventType)) {
return; // Already processed
}
processEvent($data);
markEventAsProcessed($eventId, $eventType);
Log all errors to facilitate debugging:
try {
processWebhook($data);
} catch (Exception $e) {
error_log("Webhook error: " . $e->getMessage());
http_response_code(500);
}
Use tools like webhook.site or ngrok to test your webhooks in development.
View webhook logs in your dashboard:
You will find:
You can manually resend a failed webhook from the dashboard: