Webhooks
Hear about subscription and payment lifecycle changes the moment they happen.
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.