Webhooks
Receive real-time events for subscription and payment lifecycle changes.
Recur emits webhook events for every significant state change in the payment lifecycle. Configure webhook endpoints in the Dashboard or via the Provider SDK to pipe these events into your own infrastructure.
Configuring endpoints
In the Dashboard, go to Settings > Webhooks and add one or more HTTPS endpoints. Each endpoint receives all events by default; you can filter to specific event types per endpoint.
Via the SDK:
await recur.createWebhookEndpoint({
url: "https://your-api.com/webhooks/recur",
events: ["subscription.renewed", "subscription.payment_failed"],
secret: "whsec_...", // optional; Recur generates one if not provided
});
Event types
| Event | Trigger |
|---|---|
subscription.created |
A new subscription is created |
subscription.trial_started |
Trial period begins |
subscription.trial_ended |
Trial period ends, first billing cycle starts |
subscription.renewed |
Billing cycle completes successfully |
subscription.payment_failed |
Collection attempt fails |
subscription.paused |
Subscription enters PAUSED status |
subscription.cancelled |
Subscription is cancelled |
allowance.depleted |
An allowance’s spend cap is reached |
allowance.expiring |
An allowance will expire within 24 hours |
payment.completed |
A one-time payment proof is verified |
Payload structure
All webhook events share a common 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": "AgentWalletPublicKey...",
"plan": "PlanPublicKey...",
"status": "ACTIVE",
"cycleCount": 3,
"nextBillingAt": 1752678400
},
"collection": {
"amount": 49000000,
"token": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v",
"txSignature": "5RhM..."
}
}
}
subscription.payment_failed
{
"id": "evt_01HY...",
"type": "subscription.payment_failed",
"created": 1750000000,
"livemode": true,
"data": {
"subscription": {
"id": "sub_...",
"subscriber": "AgentWalletPublicKey...",
"plan": "PlanPublicKey...",
"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": "AgentWalletPublicKey...",
"grantee": "ProviderWalletPublicKey...",
"maxAmount": 10000000,
"spent": 10000000
}
}
}
Signature verification
Every webhook request includes an X-Recur-Signature header. This is an HMAC-SHA256 signature of the raw request body, signed with your webhook secret.
Always verify this signature before processing a webhook. Do not trust the payload without it.
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/recur", express.raw({ type: "application/json" }), (req, res) => {
const valid = verifyWebhook(
req.body.toString(),
req.headers["x-recur-signature"],
process.env.RECUR_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/recur", express.raw({ type: "application/json" }), (req, res) => {
const event = recur.parseWebhookPayload({
payload: req.body.toString(),
signature: req.headers["x-recur-signature"],
});
// throws WebhookSignatureError if invalid
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
If your endpoint returns a non-2xx status code or does not respond within 30 seconds, Recur retries the webhook delivery with exponential backoff:
| Attempt | Delay after previous |
|---|---|
| 1 | Immediate |
| 2 | 5 minutes |
| 3 | 30 minutes |
| 4 | 2 hours |
| 5 | 8 hours |
After 5 failed attempts, the event is marked as undelivered. You can replay undelivered events from the Dashboard.
Idempotency
Webhook events may be delivered more than once in rare cases (network retries, infrastructure restarts). Each event has a unique id field. Use this ID to deduplicate processing in your system.