Webhooks are notifications that are automatically sent when specific predefined events occur.
Webhooks are commonly used, for instance, when payment processors need to notify a client or end user that a charge succeeded or when a subscription renews.
They replace polling. This polling might have been by:
- Software polling for a status change either through a loop, or timed queries.
- Manual polling by a user explicitly polling expecting a response or pending status to be returned.
Each merchant defines their own set of webhooks. A merchant can only affect their own set of webhooks. They cannot view or modify other merchants' webhooks. Likewise, a partner can only affect merchants associated with them. They cannot affect merchants of other partners.
Required API permission: Webhooks
If the permission for webhooks needs to be granted, contact your integration support team.
An event type is an identifier for a specific occurrence in Flute. For example, when a card payment is captured or an ACH transaction clears.
Use GET /v2/webhooks/event-types to retrieve the current list of available identifiers. New event types may be added periodically. Values passed in eventTypes when creating a webhook must match identifiers returned by this endpoint. Unrecognized identifiers are rejected.
Each event type is evaluated independently. A webhook can subscribe to one or more event types. Multiple webhooks can be created, each with its own set of event types.
| Event Type | Description | Category |
|---|---|---|
| api_key.created | New API key created | API Keys Partner event only. |
| api_key.deleted | API key revoked or deleted | API Keys Partner event only. |
| invoice.created | New invoice created | Invoices |
| invoice.paid | Invoice marked as paid | Invoices |
| merchant.created | New merchant account created | Merchants Partner event only. |
| transaction.ach.cancelled | ACH transaction canceled before processing | ACH Payments |
| transaction.ach.charged_back | ACH transaction returned or charged back | ACH Payments |
| transaction.ach.cleared | ACH transaction successfully cleared | ACH Payments |
| transaction.ach.failed | ACH transaction failed due to processing error | ACH Payments |
| transaction.ach.held | ACH transaction placed on hold for review | ACH Payments |
| transaction.ach.in_progress | ACH transaction submitted to network | ACH Payments |
| transaction.ach.refunded | ACH transaction refunded to originator | ACH Payments |
| transaction.ach.scheduled | ACH transaction created and scheduled | ACH Payments |
| transaction.card.authorized | Card payment authorization approved | Card Payments |
| transaction.card.captured | Authorized card payment captured | Card Payments |
| transaction.card.declined | Card payment declined by issuer | Card Payments |
| transaction.card.failed | Card payment failed due to processing error | Card Payments |
| transaction.card.refunded | Card payment refunded to cardholder | Card Payments |
| transaction.card.voided | Card authorization voided before capture | Card Payments |
| quick_payment.created | Quick payment link created | Quick Payments |
| quick_payment.paid | Quick payment link paid | Quick Payments |
| settlement.batch.completed | Batch settlement has been processed and settled | Settlement |
| subscription.created | New subscription created | Subscriptions |
| subscription.delinquent | Subscription entered delinquent state after repeated failures | Subscriptions |
| subscription.paid | Subscription payment successfully collected | Subscriptions |
| subscription.payment_failed | Subscription payment attempt failed | Subscriptions |
| terminal.added | New terminal registered to account | Terminals |
| terminal.out_of_paper | Terminal paper roll is empty | Terminals |
Use the following procedures to create, manage, and monitor webhooks.
Follow these steps to register a new webhook endpoint with Flute.
Decide which events your integration needs to receive. See the Event Types section for the full list of available identifiers.
Call POST /v2/webhooks/endpoints with the following parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | A display name for the webhook. Friendly, free-form. |
| endpointUrl | string | Yes | The HTTPS URL where Flute will deliver webhook events. Must be publicly reachable. Localhost URLs cannot be reached from Flute's servers and will fail the ping test. |
| eventTypes | array of strings | Yes | One or more event type identifiers from the Event Types section. Must contain at least one item. |
The response includes the endpointId needed for subsequent API calls, and the hmacSecret used to verify webhook signatures:
{
"endpointId": "4718f5ef-33c9-4f64-870e-3b0391698f5e",
"webhookName": "My webhook",
"endpointUrl": "https://example.com/webhooks/flute",
"status": "Active",
"eventTypes": ["settlement.batch.completed"],
"createdAt": "2026-01-15T10:30:00Z",
"hmacSecret": "whsec_aB3kL9mNpQ2rS7tU..."
}The hmacSecret is returned only once at creation time and cannot be retrieved again. Store it immediately in your environment variables or secrets manager. If lost, you must delete and recreate the webhook to obtain a new secret.
Webhooks are environment-specific. A webhook created in the sandbox is not available in production. This step must be repeated in each environment.
This step is optional but recommended. Send a ping to verify that your endpoint is reachable.
Use POST /v2/webhooks/endpoints/{endpointId}/ping, where endpointId is the value returned in the Step 2 response.
A successful ping returns HTTP 200 and creates an entry in the delivery log. If the endpoint is unreachable, the failure is recorded. Check GET /v2/webhooks/delivery-logs/{deliveryLogId} to diagnose.
After creating your webhook:
- Managing Webhooks. This is for updating the name, endpoint URL, event types, or to delete the webhook.
- Monitoring Webhooks. This is for viewing the delivery attempt logs and to diagnose failures.
- Webhook Delivery and Retry Behavior. This is for understanding what happens when your endpoint is temporarily unreachable.
- Client Handling of Messages. This is for understanding the request format your endpoint will receive.
- HMAC Verification. This is for validating incoming deliveries originate from Flute.
Use the following endpoints to manage webhooks.
| Endpoint | Description |
|---|---|
GET /v2/webhooks/endpoints | List all webhooks for the merchant |
GET /v2/webhooks/endpoints/{endpointId} | Retrieve details for a specified webhook |
PATCH /v2/webhooks/endpoints/{endpointId} | Update a webhook. This allows to hange its name, endpoint URL, or event types without re-creating it |
DELETE /v2/webhooks/endpoints/{endpointId} | Delete a webhook permanently. The delivery log entries are preserved |
Use the following endpoints to monitor delivery attempts and diagnose failures.
| Endpoint | Description |
|---|---|
GET /v2/webhooks/delivery-logs | List delivery logs of attempts across all webhooks |
GET /v2/webhooks/delivery-logs/{deliveryLogId} | List a specified delivery log attempt |
GET /v2/webhooks/delivery-logs/export | Export delivery logs as CSV or JSON |
POST /v2/webhooks/delivery-logs/{deliveryLogId}/retry | Manually retry a failed delivery |
The nextRetryAt and attemptNumber fields described in Webhook Delivery and Retry Behavior are visible in responses from GET /v2/webhooks/delivery-logs/{deliveryLogId}.
A delivery is successful when your endpoint returns a 200-series response within the timeout limit. Any other outcome, such as a non-2xx response, a timeout, a DNS failure, or a refused connection, is treated as a failure.
Failed deliveries are retried with exponential backoff:
| Attempt | Delay After Failure | Cumulative Time |
|---|---|---|
| 1 (initial) | — | 0 |
| 2 | 1 minute | ~1 min |
| 3 | 5 minutes | ~6 min |
| 4 | 30 minutes | ~36 min |
| 5 | 2 hours | ~2.5 hours |
| 6 | 8 hours | ~10.5 hours |
| 7 | 24 hours | ~34.5 hours |
The following fields in the delivery log response indicate the retry state:
nextRetryAt. This is the date-time of the next scheduled attempt.attemptNumber. This is the number of the upcoming attempt.
If all retry attempts are exhausted, the delivery is:
- Canceled. No further attempts will be made.
- Marked as failed. It is recorded permanently in the delivery log.
To manually retry a failed delivery, use the endpoint in the Monitoring Webhooks section.
The webhook sends a message to the URL specified by the client. This is the endpointUrl of POST {{baseURL}}/v2/webhooks/endpoints.
Each webhook delivery includes the following HTTP headers:
User-Agent: Flute-Webhooks/1.0
Content-Type: application/json; charset=utf-8
Flute-Webhook-ID: 0f27fcce-a282-4049-b984-e82325ea1d6b
Flute-Webhook-Signature: v1,XUuFXv7vu6ZNHzC0Tab6eVLQfWLXbQmtDLwBQj5oilY=
Flute-Webhook-Timestamp: 1784107205The request body contains a JSON envelope:
{
"id": "0f27fcce-a282-4049-b984-e82325ea1d6b",
"data": {
"object": {
"id": "555e779c-467c-4004-b031-952856164ddf",
"resourceType": "settlement"
}
},
"type": "settlement.batch.completed",
"created": 1784068840,
"apiVersion": "v2"
}The key fields to use in your handler:
type. This is the event type identifier (see Event Types). Use this to route the event to the right handler.data.object.id. This is the identifier of the resource that triggered the event.id. This is the delivery identifier, matchingFlute-Webhook-IDin the headers.
Your endpoint must return a 200-series response within the timeout limit. If it does not, Flute will treat the delivery as failed and retry according to the schedule in Webhook Delivery and Retry Behavior.
After delivery, it is the client's responsibility to process the message. As a best practice, verify that the message originated from Flute by performing HMAC verification (see HMAC Verification) before processing it. This reduces the risk of spoofing, tampering, or data injection.
When a webhook endpoint is created, the POST /v2/webhooks/endpoints response includes an hmacSecret field. This is the signing secret Flute uses to sign every delivery to that endpoint.
The hmacSecret is only returned once at creation time and cannot be retrieved again. Store it securely in your environment variables or secrets manager immediately. If you did not store it when the webhook was created, delete the webhook and recreate it to obtain a new secret.
Use the signing secret to verify that each incoming request originated from Flute before processing it:
- Retrieve your signing secret from your secure environment variables.
- Read the raw request body as a string before any JSON parsing occurs. It must be the exact bytes received. Do not parse, reformat, or alter whitespace or encoding in any way. Any modification will cause signature verification to fail.
- Extract the following values from the request headers:
Flute-Webhook-ID→eventIdFlute-Webhook-Timestamp→eventTimestampFlute-Webhook-Signature→ take the part afterv1,
- Build the signed payload by concatenating:
{eventId}.{eventTimestamp}.{rawBody} - Compute an HMAC-SHA256 hash of the signed payload using your signing secret as the key, then Base64-encode the result.
- Use a constant-time comparison to check that your computed signature matches the one extracted from the header.
The following examples implement a complete webhook listener with HMAC signature verification. Each example reads the raw body, verifies the signature, acknowledges receipt with a 200 response, and then processes the event. Each example assumes the hmacSecret has been stored in an environment variable named WEBHOOK_SECRET.
const express = require('express');
const crypto = require('crypto');
// In production, load this from an environment variable or a secrets manager.
// Never hardcode secrets in source code. Example: load from environment variable "process.env.WEBHOOK_SECRET"
const WEBHOOK_SECRET = 'your_webhook_secret_here';
const PORT = 3000;
const app = express();
function verifySignature(req) {
const header = req.headers['flute-webhook-signature'];
const eventId = req.headers['flute-webhook-id'];
const eventTimestamp = req.headers['flute-webhook-timestamp'];
if (!header || !eventId || !eventTimestamp) return false;
const receivedSignature = header.split(',')[1];
if (!receivedSignature) return false;
// req.body is a raw Buffer when using express.raw()
const rawBody = req.body.toString('utf8');
// The signed payload is the event ID, timestamp, and raw body concatenated with dots.
const signedPayload = `${eventId}.${eventTimestamp}.${rawBody}`;
const computedSignature = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(signedPayload)
.digest('base64');
try {
return crypto.timingSafeEqual(
Buffer.from(receivedSignature),
Buffer.from(computedSignature)
);
} catch {
return false;
}
}
// Called after the webhook signature has been verified. Add your business logic here
// to handle each event type and trigger the appropriate actions in your application.
function handleEvent(event) {
// TODO: handle the event based on event.type
// Example: if (event.type === 'transaction.card.captured') { ... }
console.log(`Received event: ${event.type}`, event);
}
// express.raw() preserves the body as a Buffer, required for HMAC verification.
// Do not use express.json() here — parsing the body first would break the signature check.
app.post('/webhook-listener', express.raw({ type: '*/*' }), (req, res) => {
if (!verifySignature(req)) {
return res.status(401).send('Unauthorized');
}
let event;
try {
event = JSON.parse(req.body.toString('utf8'));
} catch {
return res.status(400).send('Bad Request');
}
// Acknowledge receipt before processing so the payment processor doesn't time out.
res.status(200).send('OK');
handleEvent(event);
});
app.listen(PORT, () => {
console.log(`Webhook listener running on port ${PORT}`);
});