Scribe Finance emits a webhook event for every meaningful state change in the payment lifecycle. Register HTTPS endpoints (in the Dashboard or through the Provider SDK), and those events flow directly into your own systems.

Configuring endpoints

In the Dashboard, go to Settings > Webhooks and register one or more HTTPS endpoints. An endpoint receives all events unless you narrow it to specific event types.

The same configuration through the SDK:

await scribefinance.createWebhookEndpoint({
  url: "https://your-api.com/webhooks/scribefinance",
  events: ["subscription.renewed", "subscription.payment_failed"],
  secret: "whsec_...", // optional; omit it and Scribe Finance generates one for you
});

Event types

Event Trigger
subscription.created A new subscription comes into existence
subscription.trial_started A trial period begins
subscription.trial_ended A trial concludes and the first billing cycle starts
subscription.renewed A billing cycle settles
subscription.payment_failed A collection attempt does not succeed
subscription.paused A subscription enters PAUSED status
subscription.cancelled A subscription is cancelled
allowance.depleted An allowance reaches its spend cap
allowance.expiring An allowance has less than 24 hours before expiry
payment.completed A one-time payment’s proof is verified

Payload structure

All events share a single 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": "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168",
      "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

Every delivery includes an X-ScribeFinance-Signature header: an HMAC-SHA256 over the raw request body, keyed with your webhook secret.

Verify the signature before acting on a payload. An unverified payload proves nothing about its origin.

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/scribefinance", express.raw({ type: "application/json" }), (req, res) => {
  const valid = verifyWebhook(
    req.body.toString(),
    req.headers["x-scribefinance-signature"],
    process.env.SCRIBEFINANCE_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");
});

The Provider SDK performs verification and parsing in one step:

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

  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 an endpoint returns a non-2xx status or takes more than 30 seconds to respond, Scribe Finance schedules redelivery with exponential backoff:

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

After the fifth failed attempt, the event is marked undelivered. The Dashboard lets you replay undelivered events at any time.

Idempotency

Network retries and infrastructure restarts occasionally cause an event to arrive twice. Each event’s id field is unique, so deduplicate on that ID before processing.