Gareth Wilson Gareth Wilson

Guide to Laso Finance Webhooks: Features and Best Practices

Published


Laso Finance gives AI agents money to work with: bank accounts, cards, payouts and an agent wallet, all driven through an API. The account owner gets notifications as things happen, a transfer landing or a card order shipping, and Laso's webhooks pass those same notifications to the agent. That lets an agent react to a completed payout or a new deposit instead of polling listBankingTransactions or /get-withdrawal-status on a timer.

Below: how an agent registers its single webhook, what the notification payload holds, how to verify it, and why Laso's no-retry delivery needs a plan for missed events.

What are Laso Finance webhooks?

Each Laso account can have one webhook URL, registered through the API by the agent. Every notification the account owner receives through push, SMS, Telegram or the app is also POSTed to that URL, signed per Standard Webhooks with an account-level whsec_ secret. There is nothing to choose: one webhook, every notification.

The payload mirrors the notification the owner sees. It has a type, a title and a line of text, not the transaction or withdrawal object, so an agent treats it as a prompt to check the relevant status endpoint. If the agent's runtime can't receive inbound HTTP, Laso suggests pointing the URL at whatever your harness offers for external events (a gateway endpoint, a relay or a queue you drain), or carrying on polling.

Laso Finance webhook features

FeatureDetails
ConfigurationAPI only: POST /register-webhook, GET /get-webhook, POST /delete-webhook
WebhooksOne per account. Public HTTPS URL
Subscription modelEvery notification. No selection
SignatureStandard Webhooks HMAC-SHA256
Headerswebhook-id (msg_...), webhook-timestamp, webhook-signature
Secretwhsec_..., returned only by POST /register-webhook
RotationRe-registering rotates the secret. No grace period documented
Body{type, timestamp, data}
Event routingBody field type (notification.<category>)
Timeout10 seconds
RetriesNone
DisablingAfter 50 consecutive failures. Re-registering re-enables
Test deliveryA signed notification.account on registration
Source IPsNot published

Events and payloads

FieldMeaning
typeThe event type: notification. plus the category. This is what handlers switch on
timestampISO 8601 time of the notification
data.user_idThe account owner
data.titleNotification title, as shown to the owner
data.textNotification text, as shown to the owner
data.categoryThe bare category, repeating the suffix of type

Laso's documented example:

{
  "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"
  }
}

Laso says categories "include" these eight, so expect more:

EventTriggered When
notification.accountBanking application or account status changes. Also the test delivery sent when you register
notification.transactionA bank transfer or payout completes
notification.depositA deposit lands in the agent wallet
notification.withdrawalA withdrawal changes state
notification.cardA card order is updated
notification.giftCardA gift card order is updated
notification.refundNamed in the docs, not described
notification.balanceUpdateNamed in the docs, not described

Only notification.transaction has an example. The other descriptions follow from the category names and the kinds of notification Laso lists. There's no event ID in the body and no ID for the underlying transaction either, so don't try to parse amounts out of text; call the status endpoints instead.

Setting up Laso Finance webhooks

Register the URL with a Bearer token from /auth:

curl -X POST https://laso.finance/register-webhook \
  -H "Authorization: Bearer $ID_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://agent.example.com/webhooks/laso-finance"}'

The response is the only place the secret ever appears:

{
  "registered": true,
  "url": "https://agent.example.com/webhooks/laso-finance",
  "secret": "whsec_EXAMPLEONLYnotarealsecretAAAAAAA",
  "signing": "standard-webhooks"
}

Store secret as LASO_WEBHOOK_SECRET immediately. Registering also sends a signed notification.account test delivery to the URL, which only needs a 2xx, and tells the account owner through their other channels that an agent registered a webhook.

Check the registration at any time:

curl https://laso.finance/get-webhook -H "Authorization: Bearer $ID_TOKEN"

It returns the URL, whether it is disabled, consecutive_failures, and the status, time and detail of the last attempt. POST /delete-webhook removes it; the owner's own notifications carry on.

If you develop the agent locally, the Hookdeck CLI gives it a public URL to register:

npx hookdeck-cli listen 3000 laso-finance --path /webhooks/laso-finance

With Hookdeck. Register the URL of a Hookdeck Laso Finance source and paste the returned whsec_ secret into the source. The source accepts only POST. Laso always gets a 200 from Hookdeck, which keeps the failure counter at zero; a delivery with a bad signature is recorded as failed verification and held back from your agent.

Securing Laso Finance webhooks

Laso's construction is standard: HMAC-SHA256 over {webhook-id}.{webhook-timestamp}.{raw body} with the base64-decoded secret after whsec_ as the key, sent as v1,<base64>. Laso's sample compares the whole header against one value; the verifier below splits the header on spaces and accepts any v1 match, which also works for a single signature:

const crypto = require('node:crypto');

function decodeBase64(value) {
  if (typeof value !== 'string' || !/^[A-Za-z0-9+/]+={0,2}$/.test(value)) return null;
  const bytes = Buffer.from(value, 'base64');
  return bytes.length > 0 && bytes.toString('base64').replace(/=+$/, '') === value.replace(/=+$/, '')
    ? bytes : null;
}

function verifySignature(rawBody, headers, secret) {
  if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
  const prefix = headers['webhook-id'] !== undefined ? 'webhook' : 'svix';
  const id = headers[`${prefix}-id`];
  const timestamp = headers[`${prefix}-timestamp`];
  const signature = headers[`${prefix}-signature`];
  if (typeof id !== 'string' || !id || typeof timestamp !== 'string' ||
      !/^[0-9]+$/.test(timestamp) || typeof signature !== 'string') return false;
  const key = decodeBase64(secret.replace(/^whsec_/, ''));
  if (!key) return false;
  const message = Buffer.concat([Buffer.from(`${id}.${timestamp}.`, 'utf8'), rawBody]);
  const candidates = signature.split(/\s+/).filter((item) => item.startsWith('v1,')).map((item) => item.slice(3));
  const expected = crypto.createHmac('sha256', key).update(message).digest();
  return candidates.some((candidate) => {
    const received = decodeBase64(candidate);
    if (!received) return false;
    return received.length === expected.length && crypto.timingSafeEqual(received, expected);
  });
}

Since there are no retries, the handler should do as little as possible before acknowledging:

const express = require('express');

// Returns the parsed body when it is a JSON object, otherwise null.
function parseJsonObject(rawBody) {
  try {
    const value = JSON.parse(rawBody.toString('utf8'));
    return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
  } catch {
    return null;
  }
}

const app = express();

app.post('/webhooks/laso-finance', express.raw({ type: '*/*' }), async (req, res) => {
  const secret = process.env.LASO_WEBHOOK_SECRET;
  if (!secret) return res.sendStatus(500);
  if (!verifySignature(req.body, req.headers, secret)) return res.sendStatus(401);

  const notification = parseJsonObject(req.body);
  if (!notification) return res.sendStatus(400); // signed, but not a JSON object
  if (!notification.data || typeof notification.data !== 'object') return res.sendStatus(400);
  // Wake the agent; it reads the real state from the status endpoints.
  try {
    await agentInbox.push({
      messageId: req.headers['webhook-id'],
      category: notification.data.category,
      title: notification.data.title,
    });
  } catch {
    return res.sendStatus(503); // not processed, so don't acknowledge
  }
  return res.sendStatus(200);
});

agentInbox stands for whatever your agent harness uses to receive external events.

Laso asks receivers to reject timestamps "more than a few minutes old" but gives no figure. Standard Webhooks libraries use 5 minutes. Hookdeck has no timestamp tolerance and keeps Laso's original webhook-timestamp on retries and replays, so a short window behind Hookdeck would reject the very redeliveries that make up for Laso's lack of retries. Check the signature only, as above, and deduplicate on webhook-id.

Make Laso Finance webhooks production-ready. Hookdeck Event Gateway gives Laso's one-shot deliveries the retries they lack: it verifies each notification, stores it, and keeps trying your agent until it answers.

Laso Finance webhook limitations and pain points

No retries

The Problem: If your agent is down or slow for one delivery, that notification is gone.

Why It Happens: Laso times out after 10 seconds and never retries. A missed delivery only counts towards the failure total.

Workarounds:

  • Acknowledge within a few hundred milliseconds and do everything else afterwards.
  • Keep polling the status endpoints as a backstop, at a slower interval.

How Hookdeck Can Help: Hookdeck accepts each notification and then delivers it to your agent with retries, so a restart or deploy no longer loses events.

Auto-disable after 50 failures

The Problem: Notifications stop arriving and nothing tells the agent.

Why It Happens: After 50 consecutive failures, Laso disables the registration. Re-registering re-enables it, but also rotates the secret.

Workarounds:

  • Check GET /get-webhook on a schedule and alert on disabled or a rising consecutive_failures.
  • When you re-register, update the stored secret in the same step.

How Hookdeck Can Help: Hookdeck answers Laso with 200, so the failure counter doesn't climb while your agent is unavailable.

Rotation by re-registration

The Problem: Deliveries fail verification right after someone re-registers.

Why It Happens: Each registration issues a new secret, and Laso describes no overlap for the old one.

Workarounds:

  • Only re-register from code that saves the new secret atomically.

How Hookdeck Can Help: With Hookdeck's URL registered, the secret change is one update on the source.

Notifications, not records

The Problem: The agent can't tell which transfer or card order a notification refers to.

Why It Happens: data holds the notification's title and text, with no object IDs.

Workarounds:

  • Use each notification as a trigger to fetch recent transactions or withdrawals and reconcile.

How Hookdeck Can Help: Hookdeck's filters can send each category to a different handler, so only the relevant status check runs.

Best practices

Verify before trusting money movements

Read the raw body with express.raw({ type: '*/*' }), await request.text() in a Next.js handler, or FastAPI's await request.body(), and check the signature before the agent acts on anything.

Don't act on text

The text is for humans. Fetch the transaction or withdrawal state from Laso's API before an agent moves money in response.

Expect new categories

Return 2xx for unfamiliar notification.* types so they never count as failures.

Remember the owner is told

Registering a webhook notifies the account owner. Make sure owners expect it, especially in multi-tenant agent products.

Conclusion

Laso Finance's webhooks are a direct line from an agent's bank account to the agent, signed with a standard scheme. They are also unforgiving: one URL, one secret that only appears at registration, a 10-second timeout, no retries and auto-disable after 50 failures. Answer instantly, verify, fetch real state from the API, and monitor the registration.

Hookdeck Event Gateway fills the biggest gap by storing and retrying every notification for your agent. Create a free account and register a Hookdeck source URL with Laso.


Gareth Wilson

Gareth Wilson

Product Marketing

Multi-time founding marketer, Gareth is PMM at Hookdeck and author of the newsletter, Community Inc.