Gareth Wilson Gareth Wilson

Guide to Felloh Webhooks: Features and Best Practices

Published


Felloh handles card payments and customer money for UK travel businesses, allocating a protected portion of each payment to trust until the date of travel and keeping it tied to the booking for ATOL reporting. A tour operator or travel agent using Felloh needs its booking system to know when a payment succeeds, when a refund moves on, or when a booking is added or removed. Felloh's webhooks send those changes as they happen, so the booking platform doesn't have to poll the API.

This guide covers how to register a Felloh webhook, how to tell its payloads apart when none of them names its event, how to verify X-Signature, and how to cope with a retry policy that applies only to transaction events.

What are Felloh webhooks?

A Felloh webhook is created in the dashboard with a URL, a set of event kinds and, optionally, child organisations, so that a parent business can receive events for the brands or branches under it. Felloh generates a signing key when you save and shows it once.

Every delivery is a JSON POST signed with that key in X-Signature, a bare hex HMAC-SHA256 of the body. The body is the resource itself, with no event name and no shared envelope, so your receiver has to recognise a transaction, refund, booking or user from its fields.

Felloh webhook features

FeatureDetails
ConfigurationDashboard, Developer Configuration
ScopeYour organisation, optionally with child organisations
Signaturehex HMAC-SHA256
Signature headerx-signature
Signed contentThe raw request body
SecretPer webhook signing key, shown once at creation
BodyJSON, Content-Type: application/json, no event field
Timeout30 seconds; any non-2xx is a failure
RetriesTransaction status only: up to 5, about a minute apart
LogsDelivery logs kept for 30 days
TestingWebhook Playground, with X-Felloh-Test: true on test events

Events and payloads

Event (label)Triggered WhenCommon Use Cases
transaction-statusA transaction changes statusMark an order paid, reconcile payments
refund-statusA refund changes statusUpdate refund state, notify the customer
bookingA booking is createdSync bookings into your CRM
user-createA Felloh user is createdProvision access, audit new users

The booking kind covers two documented events, Booking Creation (booking:create) and Booking Deletion (booking:delete), and both use the same payload. A deletion is recognisable only by a non-null deleted_at.

Since no payload says what it is, check the shape in this order:

  1. user is an object: a user creation.
  2. transaction and status are both objects: a refund status change (refunds also carry an authorisation_code).
  3. transaction is an object and status is a string: a transaction status change.
  4. booking is an object and there is no transaction: a booking, deleted if deleted_at is set.

Acknowledge and log anything that matches none of these. This is the booking sample from the webhook samples library:

{
  "booking": {
    "id": "3d38e53b-867a-43bd-ad5d-4af786dd1b33",
    "booking_reference": "FE123456"
  },
  "customer_name": "Caroline Rennie",
  "email": "caroline@example.com",
  "amount": 250000,
  "currency": "GBX",
  "departure_date": "2027-05-01",
  "return_date": "2027-05-14",
  "customer": {
    "id": "5c2d8e1f-4a6b-4c8d-9e0f-1a2b3c4d5e6f"
  },
  "created_at": "2026-10-16T11:04:27.000Z",
  "deleted_at": null
}

Felloh gives amount in the lowest denomination of the currency, which is pence for GBX, and amount can be null for a booking created without a value. Felloh publishes no deletion example, so capture a booking:delete delivery in testing before relying on its other fields.

Setting up Felloh webhooks

  1. In the Felloh dashboard, open Developer Configuration and create a webhook.
  2. Enter the URL of your endpoint, for example https://example.com/webhooks/felloh.
  3. Select the events to listen for: transaction status, refund status, booking and user events.
  4. Optionally add child organisations whose events should also be sent.
  5. Save, and copy the signing key straight away into your server's configuration, for example as FELLOH_SIGNING_KEY. It is never shown again, so if you lose it you have to create a new webhook. The key is used as typed, not decoded.

Use the dashboard's Webhook Playground to send a test event of any kind, either with sample data or as a replay of a real event. Test deliveries carry X-Felloh-Test: true, are signed with the real key, and are never retried.

To use Hookdeck, create a source with the Felloh source type, paste the signing key as its verification secret, and register the source URL in Felloh. For local development, run npx hookdeck-cli listen 3000 felloh --path /webhooks/felloh with the Hookdeck CLI.

Securing Felloh webhooks

Take the whole X-Signature value, check it is 64 hex characters, and compare it in constant time with a hex HMAC-SHA256 of the raw body keyed with your signing key. There is no prefix, list or timestamp part. Felloh's webhook documentation includes the same check in several languages.

const crypto = require('node:crypto');

function verifySignature(rawBody, headers, secret) {
  if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
  const signature = headers['x-signature'];
  if (typeof signature !== 'string') return false;
  const key = Buffer.from(secret, 'utf8');
  const message = rawBody;
  const candidates = [signature];
  const expected = crypto.createHmac('sha256', key).update(message).digest();
  return candidates.some((candidate) => {
    if (!/^[0-9a-fA-F]{64}$/.test(candidate)) return false;
    const received = Buffer.from(candidate, 'hex');
    return received.length === expected.length && crypto.timingSafeEqual(received, expected);
  });
}

The handler classifies the verified body by shape:

const express = require('express');

const app = express();

const isObject = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);

function classify(body) {
  if (isObject(body.user)) return 'user';
  if (isObject(body.transaction) && isObject(body.status)) return 'refund';
  if (isObject(body.transaction) && typeof body.status === 'string') return 'transaction';
  if (isObject(body.booking)) return body.deleted_at ? 'booking_deleted' : 'booking_created';
  return 'unknown';
}

app.post('/webhooks/felloh', express.raw({ type: '*/*' }), async (req, res) => {
  const signingKey = process.env.FELLOH_SIGNING_KEY;
  if (!signingKey) return res.sendStatus(500);
  if (!verifySignature(req.body, req.headers, signingKey)) return res.sendStatus(401);

  let body;
  try {
    body = JSON.parse(req.body.toString('utf8'));
  } catch {
    return res.sendStatus(400);
  }
  if (!body || typeof body !== 'object' || Array.isArray(body)) return res.sendStatus(400);
  try {
    const isTest = req.get('x-felloh-test') === 'true';
    await storeFellohEvent(classify(body), body, { isTest });
  } catch {
    return res.sendStatus(503);
  }

  res.sendStatus(200);
});

storeFellohEvent writes the event somewhere durable for processing after the response. Keep test events out of your real booking data by checking isTest.

Felloh signs no timestamp, so a matching signature doesn't prove the request is new. Hookdeck's Felloh source checks the same x-signature HMAC. It acknowledges Felloh with 200 straight away and flags any request whose signature doesn't match as failed verification, rather than returning 401.

Make Felloh webhooks production-ready. Hookdeck Event Gateway verifies X-Signature, stores every transaction, refund, booking and user event, and retries delivery to your booking system, including for the events Felloh itself sends only once.

Felloh webhook limitations and pain points

Most events are sent once

The Problem: A refund, booking or user event that hits an outage on your side is lost.

Why It Happens: Felloh retries transaction status events up to 5 times, about a minute apart. Refund status, booking and user events are sent once and never retried. Any non-2xx or a response slower than 30 seconds counts as failed.

Workarounds:

  • Keep the endpoint highly available and respond quickly.
  • Check the delivery logs, kept for 30 days, after any incident, and reconcile refunds and bookings through the Felloh API.

How Hookdeck Can Help: Hookdeck receives Felloh's single attempt and stores it, then retries your service as often as you configure, so a one-shot event survives your downtime.

No event name

The Problem: Your handler has to guess what kind of event it received.

Why It Happens: Bodies are bare resources. None carries an event, type or action field, and the booking creation and deletion events share a payload.

Workarounds:

  • Classify by shape, in a fixed order, as shown above.
  • Use a separate webhook URL per event kind if you'd rather route by path.

How Hookdeck Can Help: Hookdeck filters can match on the presence of fields such as transaction or booking, so each kind can go to its own destination.

The signing key is shown once

The Problem: A lost or leaked key means creating a new webhook.

Why It Happens: Felloh displays the key only when the webhook is created.

Workarounds:

  • Copy the key into a secret manager before leaving the page.
  • If you need a new key, create the replacement webhook first and remove the old one once the new key is deployed.

How Hookdeck Can Help: With Hookdeck in front, the key lives on the Hookdeck source, and replacing it doesn't involve redeploying every service that consumes Felloh events.

No delivery ID

The Problem: Duplicate transaction retries are hard to detect.

Why It Happens: There is no event or delivery ID in the body or headers.

Workarounds:

  • Deduplicate on the resource ID plus its status, for example transaction ID and status.

How Hookdeck Can Help: Hookdeck's deduplication can compare those fields and drop repeats before they reach you.

Best practices

Verify the raw body

Use express.raw({ type: '*/*' }) in Express, await request.arrayBuffer() in Next.js and await request.body() in FastAPI. Middleware that re-encodes or decompresses and rewrites the body breaks the signature.

Respond within 30 seconds

Store the event and return 200 first. With no retries for most event kinds, a slow handler means a lost event. For the pattern, see handling webhooks asynchronously.

Treat test events as real requests

Test events are signed with the live key, so they pass verification. Use X-Felloh-Test to keep them out of production data, not to skip the signature check.

Make updates idempotent

Retried transaction events repeat exactly. Our guide to webhook idempotency explains how to store a key alongside the change it caused.

Conclusion

Felloh sends transaction, refund, booking and user changes as bare JSON resources, each signed with a hex HMAC-SHA256 in X-Signature. Verify against the raw body, classify payloads by shape, answer within 30 seconds, and remember that only transaction status events are retried, so every other kind gets one chance to reach you.

Hookdeck Event Gateway verifies each Felloh delivery and keeps it until your booking system has processed it.

Get started with Hookdeck for free and handle Felloh webhooks reliably in minutes.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.