Every meaningful state change in the payment lifecycle produces a webhook event from HoodLayer. Set up webhook endpoints in the Dashboard or through the Provider SDK and route those events into your own systems.

Configuring endpoints

Open Settings > Webhooks in the Dashboard and add one or more HTTPS endpoints. By default each endpoint gets every event, though you can restrict any endpoint to specific event types.

Or with the SDK:

await hoodlayer.createWebhookEndpoint({
  url: "https://your-api.com/webhooks/hoodlayer",
  events: ["subscription.renewed", "subscription.payment_failed"],
  secret: "whsec_...", // optional; leave it out and HoodLayer generates one
});

Event types

Event Trigger
subscription.created A subscription is created
subscription.trial_started A trial period starts
subscription.trial_ended The trial ends and the first billing cycle kicks off
subscription.renewed A billing cycle settles successfully
subscription.payment_failed An attempt to collect payment fails
subscription.paused The subscription moves to PAUSED status
subscription.cancelled The subscription gets cancelled
allowance.depleted An allowance hits its spend cap
allowance.expiring An allowance is due to expire within 24 hours
payment.completed Proof of a one-time payment is verified

Payload structure

Every webhook event arrives in the same envelope:

{
  "id": "evt_01HX...",
  "type": "subscription.renewed",
  "created": 1750000000,
  "livemode": true,
  "data": { ... }
}

subscription.renewed

{
  "id": "evt_01HX...",
  "type": "subscription.renewed",
  "created": 1750000000,
  "livemode": true,
  "data": {
    "subscription": {
      "id": "sub_...",
      "subscriber": "0x4298e8aa4048cf8d437f9a90266a7e8c436a7bba",
      "plan": "0x7f3a...",
      "status": "ACTIVE",
      "cycleCount": 3,
      "nextBillingAt": 1752678400
    },
    "collection": {
      "amount": 49000000,
      "token": "0xecdda8f70c0880d915f746c861ace5dd58721a74",
      "txHash": "0x8f4e..."
    }
  }
}

subscription.payment_failed

{
  "id": "evt_01HY...",
  "type": "subscription.payment_failed",
  "created": 1750000000,
  "livemode": true,
  "data": {
    "subscription": {
      "id": "sub_...",
      "subscriber": "0x4298e8aa4048cf8d437f9a90266a7e8c436a7bba",
      "plan": "0x7f3a...",
      "status": "PAUSED"
    },
    "failure": {
      "reason": "InsufficientFunds",
      "attemptCount": 3,
      "lastAttemptAt": 1750006400
    }
  }
}

allowance.depleted

{
  "id": "evt_01HZ...",
  "type": "allowance.depleted",
  "created": 1750000000,
  "livemode": true,
  "data": {
    "allowance": {
      "id": "alw_...",
      "granter": "0x4298e8aa4048cf8d437f9a90266a7e8c436a7bba",
      "grantee": "0x9ca41190a7c04f2f2ce6ee32e4b9b0e6b1d1f8a3",
      "maxAmount": 10000000,
      "spent": 10000000
    }
  }
}

Signature verification

Each webhook request carries an X-HoodLayer-Signature header. It’s an HMAC-SHA256 signature computed over the raw request body using your webhook secret.

Check the signature before you act on any webhook. A payload without a valid signature can’t be trusted.

import { createHmac, timingSafeEqual } from "crypto";

function verifyWebhook(payload: string, signature: string, secret: string): boolean {
  const expected = createHmac("sha256", secret)
    .update(payload)
    .digest("hex");

  const sig = signature.replace("sha256=", "");
  return timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
}

app.post("/webhooks/hoodlayer", express.raw({ type: "application/json" }), (req, res) => {
  const valid = verifyWebhook(
    req.body.toString(),
    req.headers["x-hoodlayer-signature"],
    process.env.HOODLAYER_WEBHOOK_SECRET
  );

  if (!valid) {
    return res.status(400).send("Invalid signature");
  }

  const event = JSON.parse(req.body.toString());
  // handle event.type ...

  res.status(200).send("OK");
});

With the Provider SDK:

app.post("/webhooks/hoodlayer", express.raw({ type: "application/json" }), (req, res) => {
  const event = hoodlayer.parseWebhookPayload({
    payload: req.body.toString(),
    signature: req.headers["x-hoodlayer-signature"],
  });
  // throws WebhookSignatureError when the signature is bad

  switch (event.type) {
    case "subscription.renewed":
      await grantAccess(event.data.subscription.subscriber);
      break;
    case "subscription.payment_failed":
      await suspendAccess(event.data.subscription.subscriber);
      break;
    case "allowance.depleted":
      await notifyAgentToTopUp(event.data.allowance.granter);
      break;
  }

  res.status(200).send("OK");
});

Retry behavior

When your endpoint answers with a non-2xx status or takes longer than 30 seconds to respond, HoodLayer retries delivery with exponential backoff:

Attempt Delay after previous
1 Immediate
2 5 minutes
3 30 minutes
4 2 hours
5 8 hours

Once 5 attempts have failed, the event gets marked as undelivered. Undelivered events can be replayed from the Dashboard.

Idempotency

In rare situations (network retries, infrastructure restarts) an event can be delivered more than once. Every event carries a unique id field, so use that ID to deduplicate processing on your side.