Webhooks
In this guide, we will look at how to register and consume webhooks to integrate your app with Felloh. With webhooks, your app can know when something happens in Felloh, such as someone making a payment or a refund being processed.
To register a new webhook, you need to have a URL in your app that Felloh can call. You can configure a new webhook from the Felloh dashboard under Developer Configuration. Add your URL, pick the events you want to listen for, and optionally include child organisations. A signing key is generated when the webhook is created and shown only once, so store it securely.
Now, whenever something of interest happens in your app, a webhook is fired off by Felloh. In the next section, we'll look at how to consume webhooks.
Webhook Signing
To ensure secure communication and verify the authenticity of incoming webhook requests, Felloh signs all webhook payloads using an HMAC-SHA256 signature (available when creating webhooks in dashboard).
The signature is generated by hashing the full JSON payload with a shared secret key (the signing key shown when the webhook was created), and it is sent in the X-Signature HTTP header of each request. This allows recipients to confirm that the payload has not been tampered with in transit and that it originated from Felloh.
To verify the signature on your end, compute the HMAC-SHA256 hash of the raw request body using your shared secret key. Then compare the result to the value in the X-Signature header. A matching signature confirms that the payload is valid
Webhook Verification
const crypto = require('crypto');
function verifySignature(rawBody, signature, secret) {
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
Delivery and Retries
- Events are delivered as HTTP POST requests with a JSON body and a Content-Type of application/json.
- Transaction status events are sent whenever a transaction reaches a final status, including declined payments, so always check the status field.
- Any response other than a 2xx status is treated as a failed delivery. Transaction status events are retried up to 5 times, roughly a minute apart. Refund status, booking and user events are sent once and are not retried.
- Respond quickly: a delivery that has not completed within 30 seconds is abandoned.
- Delivery logs are available in the dashboard for 30 days. See the debugging webhooks guide.
Testing Webhooks
You can send a test event to any webhook you have registered from the Felloh dashboard, under Developer Configuration. Open a webhook and use Send a Test Event to pick an event type and deliver it to your endpoint.
Two kinds of payload are available:
- Sample data — a fabricated event with recognisable test values. Use this to build against a payload before you have taken a payment, and to check the cases that are easy to miss, such as a declined transaction.
- Replay a real event — one of your own recent transactions or refunds, rebuilt into the payload exactly as it was first sent. Use this to reproduce something that went wrong.
Both are produced by the same code that sends your live events, so the shape you see when testing is the shape you will receive in production.
Test events are signed with your real signing key, so your signature verification runs unchanged. They additionally carry an X-Felloh-Test header set to true. Check this header and discard the event if it is present — otherwise a test will look like a genuine payment to your system. A live event never carries it.
Test deliveries appear in your webhook logs marked as a test, and are not counted towards your delivery success rate. Unlike live events they are not retried: the response your endpoint gives is reported back to you in the dashboard, along with the status code, headers, body and the time your endpoint took to respond.
Ignoring Test Events
app.post('/webhooks/felloh', (req, res) => {
if (req.get('X-Felloh-Test') === 'true') {
return res.sendStatus(200);
}
// handle the real event
return res.sendStatus(200);
});
Transaction Status Event
- Name
amount- Type
- integer
- Description
The total value of the transaction in the lowest denomination of the currency (e.g., pence for GBX, cents for USX).
- Name
booking.id- Type
- uuid
- Description
The booking ID the transaction is linked to.
- Name
booking.booking_reference- Type
- string
- Description
Your unique reference for the customer's booking.
- Name
completed_at- Type
- datetime
- Description
The datetime at which the transaction was completed by the customer.
- Name
currency- Type
- string
- Description
The currency of the transaction. See currency documentation.
- Name
payment_link.id- Type
- uuid
- Description
The payment link ID that was used to create the transaction, if any.
- Name
ecommerce.id- Type
- uuid
- Description
The ecommerce session ID that was used to create the transaction, if any.
- Name
provider.name- Type
- string
- Description
The provider or merchant acquirer name.
- Name
provider.reference- Type
- string
- Description
The provider or merchant acquirer reference for the transaction.
- Name
status- Type
- string
- Description
The status of the transaction.
- Name
transaction.id- Type
- uuid
- Description
The transaction ID.
- Name
transaction.narrative- Type
- object
- Description
An object with reason and description explaining the outcome of the transaction, for example why a payment was declined.
- Name
surcharge.surcharge_applied- Type
- bool
- Description
Whether a surcharge has been applied to the transaction
- Name
surcharge.amount- Type
- integer
- Description
The total value of the surcharge on the transaction in the lowest denomination of the currency (e.g., pence for GBX, cents for USX), or null when no surcharge was applied.
- Name
surcharge.currency- Type
- string
- Description
The currency of the surcharge, or null when no surcharge was applied.
Example Event Payload
{
"amount": 10000,
"booking": {
"id": "3d38e53b-867a-43bd-ad5d-4af786dd1b33",
"booking_reference": "test-123"
},
"completed_at": "2021-11-03T11:04:27.000Z",
"currency": "GBX",
"payment_link": { "id": "2cc9b939-d2fa-41e5-abae-39c377236978" },
"ecommerce": { "id": null },
"provider": {
"name": "nuvei",
"reference": "93216A82FBDD1696203B7FC6099A9B66.prod02-vm-tx94"
},
"status": "COMPLETE",
"transaction": { "id": "2cc9b939-d3fa-41e5-acae-39c377236978" },
"surcharge": {
"surcharge_applied": true,
"amount": 100,
"currency": "GBX"
}
}
Refund Status Event
- Name
authorisation_code- Type
- string
- Description
A unique code used to identify a refund.
- Name
actioned_at- Type
- datetime
- Description
The datetime at which the refund status change was actioned.
- Name
amount- Type
- integer
- Description
The amount that the refund was for
- Name
metadata- Type
- object
- Description
User defined metadata attached to the refund
- Name
status.id- Type
- string
- Description
The status of the refund. All statuses can be retrieved using the enums endpoint.
- Name
transaction.id- Type
- uuid
- Description
The transaction ID.
- Name
transaction.amount- Type
- integer
- Description
The amount that the transaction was for (customer was charged)
- Name
transaction.currency- Type
- string
- Description
The currency of the transaction | see currency documentation.
- Name
transaction.booking.booking_reference- Type
- string
- Description
The booking reference assigned to the booking (attached to the transaction)
- Name
transaction.booking.customer_name- Type
- string
- Description
The customer name assigned to the booking (attached to the transaction)
Example Event Payload
{
"authorisation_code": "3d38e53b-867a-43bd-ad5d-4af786dd1b33",
"actioned_at": "2021-11-03T11:04:27.000Z",
"amount": 100,
"metadata": { "internal_id": "abc-1234" },
"status": {
"id": "PENDING_AUTHORISATION"
},
"transaction": {
"id": "2cc9b939-d3fa-41e5-acae-39c377236978",
"amount": 100,
"currency": "GBX",
"booking": {
"booking_reference": "FE123456",
"customer_name": "Caroline Rennie"
}
}
}
Booking Events
Two event types share this payload: Booking Creation (booking:create), sent when a booking is created, and Booking Deletion (booking:delete), sent when one is deleted. Subscribe to them independently.
Booking creation fires when a booking is created through the API — whether directly, or implicitly by creating a payment link or payment link group with a booking_reference. Bookings brought in by a bulk import are not announced, so an import does not flood your endpoint.
A booking created alongside a payment link carries no travel dates and no value, so treat departure_date, return_date and amount as optional.
- Name
booking.id- Type
- uuid
- Description
The booking ID.
- Name
booking.booking_reference- Type
- string
- Description
Your unique reference for the customer's booking.
- Name
customer_name- Type
- string
- Description
The name of the customer the booking is for.
- Name
email- Type
- string
- Description
The email address held against the booking.
- Name
amount- Type
- integer
- Description
The gross value of the booking in the lowest denomination of the currency (e.g., pence for GBX), or null when the booking was created without a value.
- Name
currency- Type
- string
- Description
The currency of the booking | see currency documentation.
- Name
departure_date- Type
- date
- Description
The departure date, or null when the booking has no travel dates.
- Name
return_date- Type
- date
- Description
The return date, or null when the booking has no travel dates.
- Name
customer.id- Type
- uuid
- Description
The customer record the booking is attached to, or null.
- Name
created_at- Type
- datetime
- Description
The datetime at which the booking was created.
- Name
deleted_at- Type
- datetime
- Description
The datetime at which the booking was deleted. Null on a creation event, populated on a deletion event.
Example Event Payload
{
"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
}
User Create Event
Sent when a user is added to your organisation, including API users. A person can belong to several organisations, so the organisation block tells you which one they were added to.
- Name
user.id- Type
- string
- Description
The user ID.
- Name
email- Type
- string
- Description
The user's email address.
- Name
first_name- Type
- string
- Description
The user's first name.
- Name
last_name- Type
- string
- Description
The user's last name.
- Name
status- Type
- string
- Description
The status of the user. All statuses can be retrieved using the enums endpoint.
- Name
is_api_user- Type
- bool
- Description
Whether the user is an API user rather than a person.
- Name
organisation.id- Type
- string
- Description
The organisation the user was added to.
- Name
organisation.name- Type
- string
- Description
The name of that organisation.
- Name
created_at- Type
- datetime
- Description
The datetime at which the user was created.
Example Event Payload
{
"user": { "id": "4f2b8c91-6d3e-4a75-9b0c-1e2f3a4b5c6d" },
"email": "caroline@example.com",
"first_name": "Caroline",
"last_name": "Rennie",
"status": "ACTIVE",
"is_api_user": false,
"organisation": {
"id": "X1234",
"name": "Example Travel"
},
"created_at": "2026-10-16T11:04:27.000Z"
}
