Webhooks
Receive subscription and payment lifecycle events the moment they occur.
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.