# Swipelux Webhooks

Swipelux ([swipelux.com](https://swipelux.com)) is a stablecoin and fiat on/off-ramp, accounts and
transfers platform (API v3). It sends an HTTPS `POST` with a signed JSON body when customers,
capabilities, applications, recipients, destinations, accounts or transfers change. New webhook
integrations are delivered by Svix, which follows Standard Webhooks
and signs with the `svix-*` header names: `svix-id`, `svix-timestamp` and `svix-signature`.

The body is a v3 envelope:
`{ "id": "evt_...", "type": "transfer.state_changed", "createdAt": "...", "attempt": 1, "resource": { "type": "transfer", "id": "tr_..." }, "data": { "object": { ... }, "previous": { "state": "processing" } } }`.
`id` is the event id (your dedupe key). `attempt` is **not** a dedupe key or retry counter. `resource`
locates the object, `data.object` is its state and `data.previous` holds the changed fields.

> **Only new integrations are supported.** Hookdeck's configuration alert for this source reads:
> "Only new Swipelux webhook integrations, delivered by Svix, are supported. Legacy integrations use a different scheme and fail verification."
> Subscribe with v3 event names (`POST /v3/webhooks`).

## When to Use This Skill

* How do I receive Swipelux webhooks?
* How do I verify Swipelux webhook signatures (`svix-signature`)?
* How do I handle `transfer.state_changed`, `customer.created` or `capability.status_changed` events?
* How do I get the Swipelux signing secret (`GET /v3/webhooks/portal`)?
* Why is my Swipelux webhook signature verification failing, or rejecting Hookdeck retries?

## Verification (core)

Signed content is `{svix-id}.{svix-timestamp}.{raw body}`. Svix signs it with HMAC-SHA256 using
the base64-decoded secret (the part after `whsec_`), base64-encodes the result, and sends one or
more space-separated `v1,<signature>` entries. Use the official `svix` library, which compares in
constant time. Read the `svix-*` headers, and when `svix-id` is absent fall back to the `webhook-*`
headers (`webhook-id`, `webhook-timestamp`, `webhook-signature`), as Hookdeck does.

```javascript
import { Webhook } from 'svix'; // svix 2.x is ESM-only

const prefix = req.headers['svix-id'] ? 'svix' : 'webhook';
const id = req.headers[`${prefix}-id`];
const timestamp = req.headers[`${prefix}-timestamp`];
const signature = req.headers[`${prefix}-signature`];
// rawBody: the raw Buffer/string, never re-serialized JSON
new Webhook(process.env.SWIPELUX_WEBHOOK_SECRET).verify(rawBody, {
  'svix-id': id, 'svix-timestamp': timestamp, 'svix-signature': signature,
  'webhook-id': id, 'webhook-timestamp': timestamp, 'webhook-signature': signature,
}); // throws on a bad signature or a timestamp outside the window
const event = JSON.parse(rawBody.toString('utf8')); // parse only after verify passes

```

```python
from svix.webhooks import Webhook, WebhookVerificationError

wh = Webhook(os.environ["SWIPELUX_WEBHOOK_SECRET"])  # "whsec_..."
wh.verify(raw_body, {"svix-id": id, "svix-timestamp": ts, "svix-signature": sig,
                     "webhook-id": id, "webhook-timestamp": ts, "webhook-signature": sig})

```

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

Each endpoint has its own signing secret. Request an app portal link with `GET /v3/webhooks/portal`
(API key with `webhooks:write`; the link expires after an hour), open it, and reveal the endpoint's
secret. Swipelux doesn't print the secret's form, but Svix secrets start with `whsec_`. Keep the
full value in `SWIPELUX_WEBHOOK_SECRET`. Handlers fail closed: with the variable unset (or not a
`whsec_` value) they return 500 instead of accepting anything. A missing header or bad signature
gets a 400.

Read the raw body: Swipelux's docs say to "Keep the raw body available until verification
completes". With Express use `express.raw({ type: '*/*' })` so any content type works and the
exact bytes are verified.

### Hookdeck retries and replays hit the 5-minute window

The `svix` library always enforces a fixed 5-minute timestamp window, and it can't be configured.
Hookdeck retries and replays resend the **original** `svix-timestamp`, so the library rejects any
retry or replay that happens more than 5 minutes after the original delivery. If you verify
behind Hookdeck (or replay old events), use the manual verifier in
[references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/swipelux-webhooks/references/verification.md), where the window can be dropped or
widened. Deduplicate on the envelope `id` instead of relying on the timestamp.

## Common Event Types

12 events are documented (the list may not be exhaustive). The name is `type` in the JSON body,
and the examples handle all of them:

| Event | Triggered When |
| --- | --- |
| `customer.created` / `customer.updated` / `customer.archived` | A customer is created, changed or archived |
| `capability.created` | A capability is created for a customer |
| `capability.status_changed` | A capability's status changes after creation |
| `application.status_changed` | An institution application's status changes |
| `recipient.status_changed` | A recipient becomes `active`, `rejected` or `archived` |
| `destination.status_changed` | A payout destination's status changes |
| `account.created` | An account is created |
| `account.status_changed` | An account's status changes (e.g. `ready`, `suspended`, `archived`) |
| `account.details_changed` | An account's details (bank or wallet details, routing) change |
| `transfer.state_changed` | A v3 transfer changes public state (`awaiting_funds`, `processing`, `action_required`, `completed`, `failed`, `canceled`) |

Delivery is at least once: duplicates, delays and reordering are normal. Persist the envelope `id`,
return `2xx`, then refetch the resource named by `resource.type` and `resource.id` before acting.
See [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/swipelux-webhooks/references/overview.md) for payloads.

## Environment Variables

```bash
# Signing secret of the webhook endpoint (Svix format, starts with whsec_)
SWIPELUX_WEBHOOK_SECRET=whsec_xxxxx

```

## Local Development

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

```

Register the tunnel URL with `POST /v3/webhooks` (see [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/swipelux-webhooks/references/setup.md)),
then trigger a change in sandbox or replay a delivery from the portal.

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/swipelux-webhooks/references/overview.md) - Swipelux webhook concepts, events, payload shapes, delivery
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/swipelux-webhooks/references/setup.md) - Create an endpoint, get the secret, change events, recover deliveries
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/swipelux-webhooks/references/verification.md) - Svix verification details, header fallback, manual verifier, timestamp window