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

  1. Navigate to Billing → Webhooks → Add Endpoint in the dashboard
  2. Enter your endpoint URL (must be publicly reachable via HTTPS)
  3. Select which event types to subscribe to
  4. 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

EventDescription
subscription.createdA new subscription was started
subscription.updatedPlan, price, or status changed on an existing subscription
subscription.canceledSubscription was canceled (immediately or at period end)
subscription.renewedSubscription successfully renewed at the start of a new billing period
subscription.trial_endingTrial ends in 3 days — prompt user to add a payment method
invoice.paidInvoice was paid successfully
invoice.payment_failedInvoice payment attempt failed
payment.succeededA one-time or subscription payment succeeded
payment.failedA payment attempt failed
usage.threshold_reachedA 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}.


Retry policy

AuthGate retries failed deliveries with exponential backoff:

AttemptDelay
1Immediate
25 minutes
330 minutes
42 hours
58 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.

Was this page helpful?