Skip to content

Webhooks

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 Permissions

Required API permission: Webhooks

If the permission for webhooks needs to be granted, contact your integration support team.

Event Types

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 TypeDescriptionCategory
api_key.createdNew API key createdAPI Keys
Partner event only.
api_key.deletedAPI key revoked or deletedAPI Keys
Partner event only.
invoice.createdNew invoice createdInvoices
invoice.paidInvoice marked as paidInvoices
merchant.createdNew merchant account createdMerchants
Partner event only.
transaction.ach.cancelledACH transaction canceled before processingACH Payments
transaction.ach.charged_backACH transaction returned or charged backACH Payments
transaction.ach.clearedACH transaction successfully clearedACH Payments
transaction.ach.failedACH transaction failed due to processing errorACH Payments
transaction.ach.heldACH transaction placed on hold for reviewACH Payments
transaction.ach.in_progressACH transaction submitted to networkACH Payments
transaction.ach.refundedACH transaction refunded to originatorACH Payments
transaction.ach.scheduledACH transaction created and scheduledACH Payments
transaction.card.authorizedCard payment authorization approvedCard Payments
transaction.card.capturedAuthorized card payment capturedCard Payments
transaction.card.declinedCard payment declined by issuerCard Payments
transaction.card.failedCard payment failed due to processing errorCard Payments
transaction.card.refundedCard payment refunded to cardholderCard Payments
transaction.card.voidedCard authorization voided before captureCard Payments
quick_payment.createdQuick payment link createdQuick Payments
quick_payment.paidQuick payment link paidQuick Payments
settlement.batch.completedBatch settlement has been processed and settledSettlement
subscription.createdNew subscription createdSubscriptions
subscription.delinquentSubscription entered delinquent state after repeated failuresSubscriptions
subscription.paidSubscription payment successfully collectedSubscriptions
subscription.payment_failedSubscription payment attempt failedSubscriptions
terminal.addedNew terminal registered to accountTerminals
terminal.out_of_paperTerminal paper roll is emptyTerminals

Webhook Workflows

Use the following procedures to create, manage, and monitor webhooks.

Creating a Webhook

Follow these steps to register a new webhook endpoint with Flute.

Step 1 — Identify event types

Decide which events your integration needs to receive. See the Event Types section for the full list of available identifiers.

Step 2 — Create the endpoint

Call POST /v2/webhooks/endpoints with the following parameters:

ParameterTypeRequiredDescription
namestringYesA display name for the webhook. Friendly, free-form.
endpointUrlstringYesThe 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.
eventTypesarray of stringsYesOne 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.

Step 3 — Test the endpoint

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.

Next steps

After creating your webhook:

Managing Webhooks

Use the following endpoints to manage webhooks.

EndpointDescription
GET /v2/webhooks/endpointsList 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

Monitoring Webhooks

Use the following endpoints to monitor delivery attempts and diagnose failures.

EndpointDescription
GET /v2/webhooks/delivery-logsList delivery logs of attempts across all webhooks
GET /v2/webhooks/delivery-logs/{deliveryLogId}List a specified delivery log attempt
GET /v2/webhooks/delivery-logs/exportExport delivery logs as CSV or JSON
POST /v2/webhooks/delivery-logs/{deliveryLogId}/retryManually 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}.

Webhook Delivery and Retry Behavior

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:

AttemptDelay After FailureCumulative Time
1 (initial)0
21 minute~1 min
35 minutes~6 min
430 minutes~36 min
52 hours~2.5 hours
68 hours~10.5 hours
724 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.

Client Handling of Messages

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: 1784107205

The 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, matching Flute-Webhook-ID in 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.

HMAC Verification

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:

  1. Retrieve your signing secret from your secure environment variables.
  2. 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.
  3. Extract the following values from the request headers:
    • Flute-Webhook-IDeventId
    • Flute-Webhook-TimestampeventTimestamp
    • Flute-Webhook-Signature → take the part after v1,
  4. Build the signed payload by concatenating: {eventId}.{eventTimestamp}.{rawBody}
  5. Compute an HMAC-SHA256 hash of the signed payload using your signing secret as the key, then Base64-encode the result.
  6. 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}`);
});