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.