# Laso Finance Webhooks

[Laso Finance](https://laso.finance) provides banking, cards and payouts for AI agents. An agent registers one webhook URL per account with `POST /register-webhook` and then receives every account notification (banking application status, transfers, deposits, card orders, withdrawals) as a signed JSON POST. Signatures follow Standard Webhooks, so each delivery carries `webhook-id`, `webhook-timestamp` and `webhook-signature` headers.

## When to Use This Skill

* How do I receive Laso Finance webhooks?
* How do I verify a Laso Finance webhook signature (`webhook-signature`)?
* Where do I get the Laso Finance webhook secret (`whsec_...`)? (Only from the `POST /register-webhook` response.)
* How do I handle `notification.transaction`, `notification.deposit` or `notification.withdrawal` events?
* Why is my Laso Finance webhook verification failing, or failing only on Hookdeck retries and replays?
* Why did Laso Finance stop sending webhooks? (50 consecutive failures disable the registration.)

## Verification (core)

| Header | Value |
| --- | --- |
| `webhook-id` | `msg_...` message id. Your idempotency key (the body has no event id) |
| `webhook-timestamp` | Unix seconds when the delivery was signed |
| `webhook-signature` | One or more space-separated `v1,<base64>` entries |

Signed content is `{webhook-id}.{webhook-timestamp}.{raw body}`, HMAC-SHA256, base64 output. The key is the base64-**decoded** part of the `whsec_...` secret after the prefix. Compare against every `v1,` entry; ignore other versions. If `webhook-id` is absent, read `svix-id`, `svix-timestamp` and `svix-signature` instead, as Hookdeck does. The examples use the `standardwebhooks` library (npm and PyPI):

```javascript
const { Webhook } = require('standardwebhooks');

// rawBody: exact request body (Buffer or string), NOT re-serialised JSON
function verifyLaso(rawBody, headers, secret = process.env.LASO_FINANCE_WEBHOOK_SECRET) {
  const p = headers['webhook-id'] ? 'webhook' : 'svix'; // fall back to the svix-* names
  new Webhook(secret).verify(rawBody, {
    'webhook-id': headers[`${p}-id`],
    'webhook-timestamp': headers[`${p}-timestamp`],
    'webhook-signature': headers[`${p}-signature`],
  }, { jsonParse: false }); // throws on a bad signature or stale timestamp → respond 401
}

```

The library always enforces a fixed **5-minute timestamp window**. Laso Finance itself never retries, but Hookdeck retries and replays resend the original `webhook-timestamp`, so the library rejects them once they are more than 5 minutes old. If you receive through Hookdeck and need late retries to pass, use the manual verifier in [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/laso-finance-webhooks/references/verification.md), which can drop or widen the window. In Express, read the body with `express.raw({ type: '*/*' })` so any content type verifies, and parse JSON only after the check.

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

## Event Type and Payload

Every body is the same envelope. The event name is the top-level `type`, which is `notification.` plus the notification category:

```json
{
  "type": "notification.transaction",
  "timestamp": "2026-07-31T18:04:05.000Z",
  "data": {
    "user_id": "usr_abc123",
    "title": "Bank transfer complete",
    "text": "$250.00 landed in your agent wallet.",
    "category": "transaction"
  }
}

```

`data` is the notification as the account owner sees it: `title` and `text` are human-readable, and `category` repeats the bare category. There is no event id in the body: use the `webhook-id` header.

## Common Event Types

| Event | Category |
| --- | --- |
| `notification.account` | Banking application and account status. Also the signed test delivery sent when you register |
| `notification.transaction` | Bank transfers and payouts, such as a transfer completing |
| `notification.deposit` | Agent wallet deposits |
| `notification.withdrawal` | Withdrawals |
| `notification.card` | Card orders |
| `notification.giftCard` | Gift card orders |
| `notification.refund` | Refunds (named in the docs, not described) |
| `notification.balanceUpdate` | Balance changes (named in the docs, not described) |

The docs list these eight under "Categories include" without describing each one, so the list is open-ended and the descriptions above are inferred from the names and the docs' examples. The examples handle all eight and ignore other types. There is no per-type selection: one webhook receives every notification. See [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/laso-finance-webhooks/references/overview.md).

## Delivery Semantics

Respond with any `2xx` within **10 seconds**, then process asynchronously. Deliveries are **not retried**: a failed or timed-out delivery is lost, and the status endpoints (`listBankingTransactions`, `/get-withdrawal-status`) remain the source of truth, so reconcile against them. After **50 consecutive failures** the registration is disabled; `GET /get-webhook` shows the failure counters and last attempt, and registering again re-enables it. Deduplicate on `webhook-id`. Return `400` for missing headers and `401` for a bad signature.

## Environment Variables

```bash
LASO_FINANCE_WEBHOOK_SECRET=whsec_xxxxx   # full value from the register response; handlers fail closed (500) without it

```

The secret is returned **only** in the `POST /register-webhook` response and `GET /get-webhook` never returns it. Registering again rotates it, so update the variable at the same time.

## Local Development

```bash
# Express / Next.js (use 8000 for FastAPI). No install or account required.
npx hookdeck-cli listen 3000 laso-finance --path /webhooks/laso-finance

```

Register the printed URL with `POST /register-webhook` (the URL must be public HTTPS). Registering sends a signed `notification.account` test delivery to it.

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/laso-finance-webhooks/references/overview.md) - Event catalog, envelope and delivery behaviour
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/laso-finance-webhooks/references/setup.md) - Register, check and delete the webhook, handle the secret
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/laso-finance-webhooks/references/verification.md) - Library and manual verification, gotchas, debugging