# Felloh Webhooks

Felloh, payments and trust-account infrastructure for UK travel businesses, sends a JSON webhook to your endpoint when a transaction changes status, a refund changes status, a booking is created or a user is created. Each request is signed: the `X-Signature` header holds the hex HMAC-SHA256 of the raw body, keyed with the signing key shown when you create the webhook.

## When to Use This Skill

* How do I receive Felloh webhooks?
* How do I verify the Felloh `X-Signature` header?
* How do I tell transaction, refund, booking and user deliveries apart? (There is no event-name field)
* Why does my Felloh webhook signature verification fail?
* How do I handle Felloh test deliveries (`X-Felloh-Test`)?

## Verification (core)

```javascript
const crypto = require('crypto');

const HEX_SHA256 = /^[0-9a-fA-F]{64}$/;

// rawBody: the Buffer exactly as received. secret: the signing key, used as UTF-8 bytes.
function verifyFellohSignature(rawBody, signature, secret) {
  if (!secret || typeof signature !== 'string' || !HEX_SHA256.test(signature)) return false;
  const expected = crypto.createHmac('sha256', secret).update(rawBody).digest();
  return crypto.timingSafeEqual(Buffer.from(signature, 'hex'), expected); // validated above
}

```

```python
import hashlib, hmac, re

def verify_felloh_signature(raw_body: bytes, signature: str | None, secret: str) -> bool:
    if not secret or not signature or not re.fullmatch(r"[0-9a-fA-F]{64}", signature):
        return False
    expected = hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(signature.lower().encode(), expected.encode())

```

* Header: `X-Signature`. The **whole value** is the bare hex digest: no `sha256=` prefix, no list, no timestamp.
* Signed content: the **raw request body bytes only**. Don't parse or re-serialise the JSON first (`express.raw({ type: '*/*' })`, Next.js `await request.arrayBuffer()`, FastAPI `await request.body()`).
* Key: the signing key as UTF-8 bytes, used as-is (not hex- or base64-decoded). It is shown once when you create the webhook.
* Check the format (exactly 64 hex characters) before decoding, then compare in constant time. Hex is case-insensitive.
* Missing, empty, malformed or mismatched signature: `401`.
* Nothing time-based is signed, so there is no replay window to check. Make handlers idempotent instead.

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/felloh-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/felloh-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/felloh-webhooks/examples/fastapi/).

## Event Types

Felloh sends **no event-name field or header**. The events are chosen when you register the webhook, and you tell deliveries apart by payload shape. The names below are labels used by this skill (and Hookdeck's sample payloads), not values on the wire.

| Event (label) | Payload shape | Retried? |
| --- | --- | --- |
| `transaction-status` | `transaction.id` plus a string `status` (for example `COMPLETE`), `amount`, `currency`, `booking`, `provider` | Yes: up to 5 times, about a minute apart |
| `refund-status` | `authorisation_code`, `status` object (`status.id`, for example `PENDING_AUTHORISATION`), `transaction` object | No |
| `booking` | `booking` object (`id`, `booking_reference`), `customer_name`, `departure_date`, `return_date` | No |
| `user-create` | `user` object (`id`), `email`, `first_name`, `last_name`, `organisation` | No |

Dashboard test deliveries carry `X-Felloh-Test: true`, are signed with the real key and are never retried.

## Important Headers

| Header | Description |
| --- | --- |
| `X-Signature` | Hex HMAC-SHA256 of the raw body |
| `X-Felloh-Test` | `true` on dashboard test deliveries |

## Delivery

Any non-2xx response is a failure. Timeout is 30 seconds. Only transaction status events are retried; refund, booking and user events are sent once, so return `2xx` quickly and process asynchronously. Felloh keeps delivery logs for 30 days.

## Environment Variables

```bash
FELLOH_WEBHOOK_SECRET=your_signing_key   # shown once when you create the webhook

```

## Local Development

```bash
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 felloh --path /webhooks/felloh

```

Use the printed URL as the webhook URL in Felloh.

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/felloh-webhooks/references/overview.md) - What Felloh webhooks are, events and payload shapes
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/felloh-webhooks/references/setup.md) - Register the webhook and get the signing key
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/felloh-webhooks/references/verification.md) - Signature details, gotchas, debugging 401s
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/felloh-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests