# Swan Webhooks

Swan, the embedded banking platform, sends a thin JSON webhook when a resource changes in your
project: a transaction is booked, an account or card is created, an onboarding is updated.
Webhooks are **not signed**. If the subscription has a webhook secret, Swan sends it as plain text in
the `x-swan-secret` header of every request, and your endpoint authenticates the request by comparing
that header with the secret you stored.

Set a webhook secret on the Swan webhook subscription. Subscriptions without one send nothing to verify.

## When to Use This Skill

* How do I receive Swan webhooks?
* How do I verify the `x-swan-secret` header?
* Does Swan sign webhook payloads? (No: use the shared secret)
* How do I handle `Transaction.Booked`, `Account.Created` or `Onboarding.Updated` events?
* Why does my Swan webhook endpoint return 401, or why is there no `x-swan-secret` header?

## Verification (core)

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

// timingSafeEqual throws on different lengths, so check them first.
function safeEqual(a, b) {
  const ba = Buffer.from(String(a), 'utf8');
  const bb = Buffer.from(String(b), 'utf8');
  return ba.length === bb.length && crypto.timingSafeEqual(ba, bb);
}

function verifySwanRequest(headers, secret = process.env.SWAN_WEBHOOK_SECRET) {
  if (!secret) return false;                  // never accept when unconfigured
  const received = headers['x-swan-secret'];  // the secret, plain text, whole value
  return typeof received === 'string' && received.length > 0 && safeEqual(received, secret);
}

```

* Header: `x-swan-secret: <secret>` (case-insensitive; compare the whole value, no prefix).
* In Python use `hmac.compare_digest` on bytes (`.encode("utf-8")` both sides; `str` raises on non-ASCII).
* Return `401` on a missing or different header, `400` for a body that isn't a JSON object, `200` once handled.
* Read the body raw (`express.raw({ type: '*/*' })`) and authenticate before parsing it.
* **A static shared secret authenticates the sender, not the body, and gives no replay protection** (no
  signature, no timestamp). Use HTTPS, deduplicate on `eventId`, and rotate the secret if it leaks.

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

## Notification Payload

Notifications are thin. Query the Swan API with `resourceId` for the details.

```json
{ "eventType": "Transaction.Booked", "eventId": "...", "eventDate": "2021-08-18T09:35:46.673Z", "projectId": "...", "resourceId": "..." }

```

* Respond `200 OK` within **10 seconds**; errors are retried every 30 seconds, up to 7 times.
* Delivery is **at-least-once** and **unordered**: deduplicate on `eventId` and don't assume order.

## Event Types

`eventType` is `<Resource>.<Action>`.

| Event | Sent When |
| --- | --- |
| `Transaction.Booked` | A transaction is booked |
| `Transaction.Pending` | A transaction is pending |
| `Transaction.Rejected` | A transaction is rejected |
| `Account.Created` | An account is created |
| `AccountHolder.Updated` | An account holder is updated |
| `Card.Created` | A card is created |
| `Onboarding.Updated` | An onboarding is updated |

Swan documents many more (`Consent.Granted`, `Account.Closing`, `MerchantPayment.Captured`, ...); see
[references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/swan-webhooks/references/overview.md).

## Environment Variables

```bash
SWAN_WEBHOOK_SECRET=your-webhook-secret   # the secret set on the Swan webhook subscription

```

## Local Development

Swan can't reach `localhost`. Tunnel with the Hookdeck CLI (no account required):

```bash
npx hookdeck-cli listen 3000 swan --path /webhooks/swan

```

Use the printed URL as the endpoint of the Swan webhook subscription.

## Resources

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/swan-webhooks/references/overview.md) - What Swan webhooks are, events and payload shape
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/swan-webhooks/references/setup.md) - Set the webhook secret and add the subscription
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/swan-webhooks/references/verification.md) - Secret check details, gotchas, debugging 401s
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/swan-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests