Billing Webhooks
AuthGate fires outgoing webhooks to your application when billing state changes. Use these events to provision access, send emails, update your database, and keep your application in sync with subscription state.
Creating an endpoint
- Navigate to Billing → Webhooks → Add Endpoint in the dashboard
- Enter your endpoint URL (must be publicly reachable via HTTPS)
- Select which event types to subscribe to
- Click Save
AuthGate generates a signing secret for each endpoint. Copy it — you need it for signature verification.
You can create multiple endpoints and subscribe each to different event types. For example, route subscription events to your provisioning service and invoice events to your billing notifications service.
Event types
| Event | Description |
|---|---|
subscription.created | A new subscription was started |
subscription.updated | Plan, price, or status changed on an existing subscription |
subscription.canceled | Subscription was canceled (immediately or at period end) |
subscription.renewed | Subscription successfully renewed at the start of a new billing period |
subscription.trial_ending | Trial ends in 3 days — prompt user to add a payment method |
invoice.paid | Invoice was paid successfully |
invoice.payment_failed | Invoice payment attempt failed |
payment.succeeded | A one-time or subscription payment succeeded |
payment.failed | A payment attempt failed |
usage.threshold_reached | A user's usage for a metric exceeded a configured threshold |
Event payload structure
Every event shares the same envelope:
{
"id": "evt_abc123",
"type": "subscription.updated",
"created_at": "2025-04-01T12:00:00Z",
"project_id": "proj_xyz",
"data": {
"subscription": {
"id": "sub_def456",
"user_id": "user_ghi789",
"plan": { "id": "plan_starter", "name": "Starter" },
"status": "active",
"current_period_start": "2025-04-01T00:00:00Z",
"current_period_end": "2025-05-01T00:00:00Z",
"cancel_at_period_end": false
}
}
}
The shape of data varies by event type. Invoice events include an invoice object; usage events include a usage object with the metric name and current quantity.
Signature verification
AuthGate signs every webhook delivery with HMAC-SHA256. Always verify the signature before processing an event.
src/app/api/webhooks/billing/route.ts
import { createHmac, timingSafeEqual } from "crypto";
export async function POST(request: Request) {
const body = await request.text();
const signature = request.headers.get("authgate-signature");
const timestamp = request.headers.get("authgate-timestamp");
if (!signature || !timestamp) {
return new Response("Missing signature headers", { status: 400 });
}
// Reject events older than 5 minutes to prevent replay attacks
const eventAge = Date.now() - parseInt(timestamp, 10) * 1000;
if (eventAge > 5 * 60 * 1000) {
return new Response("Timestamp too old", { status: 400 });
}
const payload = `${timestamp}.${body}`;
const expectedSig = createHmac("sha256", process.env.BILLING_WEBHOOK_SECRET!)
.update(payload)
.digest("hex");
const sigBuffer = Buffer.from(signature, "hex");
const expectedBuffer = Buffer.from(expectedSig, "hex");
if (sigBuffer.length !== expectedBuffer.length || !timingSafeEqual(sigBuffer, expectedBuffer)) {
return new Response("Invalid signature", { status: 401 });
}
const event = JSON.parse(body);
// Handle the event
switch (event.type) {
case "subscription.created":
await provisionAccess(event.data.subscription.user_id);
break;
case "subscription.canceled":
await revokeAccess(event.data.subscription.user_id);
break;
case "invoice.payment_failed":
await sendPaymentFailedEmail(event.data.invoice);
break;
}
return new Response("OK", { status: 200 });
}
The authgate-signature header contains the HMAC-SHA256 hex digest. The authgate-timestamp header is a Unix timestamp (seconds). The signed payload is {timestamp}.{raw_body}.
Always read the raw request body as text before parsing JSON. Any transformation (pretty-printing, key reordering) will break signature verification.
Retry policy
AuthGate retries failed deliveries with exponential backoff:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 5 minutes |
| 3 | 30 minutes |
| 4 | 2 hours |
| 5 | 8 hours |
A delivery is considered failed if your endpoint returns a non-2xx status code or does not respond within 10 seconds. After 5 failed attempts, the event is marked as failed and no further retries are made.
Return a 200 response as quickly as possible. Perform any long-running work asynchronously after responding.
Event log
The full delivery log is available in the dashboard under Billing → Webhooks → [Endpoint] → Event Log. For each delivery you can see:
- Event type and ID
- Delivery timestamp and HTTP status received
- Request headers and body sent
- Response body received
To manually retry a failed event, click Retry in the event log. This sends the original payload again with a fresh timestamp and new signature.