Corpland Revenue

Webhooks

Know when a plan changes, a balance runs out or a payment lands, and verify every request.

Add an endpoint under Webhooks in the dashboard and pick the events. Each endpoint has its own whsec_ signing secret.

Events

EventWhenTypical use
billing.updatedA plan is activated, scheduled, renewed, changed, cancelled or ends. data.plan_changes lists each change with an action (activated, scheduled, updated, expired) and tags say why (renewed, trial_ended, past_due, downgrade_scheduled, quantity_changed)Sync the customer's plan into your database
balances.limit_reachedA customer goes from allowed to blocked. limit_type is included, spend_limit, max_purchase or usage_limitShow an upgrade prompt or email them
balances.usage_alert_triggeredUsage crosses an alert lineWarn before they run out
billing.auto_topup_succeededAn auto top up was charged and grantedSend a receipt
billing.auto_topup_failedAn auto top up could not run, with a reasonAsk them to update their card
invoice.createdRevenue built an invoice, including renewals that need a pay linkEmail hosted_invoice_url to customers with no saved card
invoice.paidAn invoice was paidSend a receipt
invoice.payment_failedA charge was declined, or a pay link is still unpaid on a retry dayAsk them to pay or update their card
customers.created, customers.deletedA customer record was made or removedKeep your records in step
entities.created, entities.deletedA seat or workspace was added or removedSeat counts in your app

balances.usage_alert, the older name, is sent only to endpoints that pick it by name.

Each request is a JSON POST with the headers Revenue-Event, Revenue-Event-Id and Revenue-Signature.

{ "id": "evt_6707...", "type": "balances.limit_reached", "env": "live", "created_at": "2026-10-10T18:00:00.000Z", "data": { "customer_id": "user_123", "feature_id": "videos" } }

Verify the signature

Revenue-Signature: t=1760119200,v1=5f2b... where v1 is the hex HMAC SHA256 of the timestamp, a dot and the raw body, keyed with the endpoint secret. Use the raw body, not a parsed and restringified one.

import crypto from "node:crypto";
import express from "express";

const app = express();

app.post("/webhooks/revenue", express.raw({ type: "application/json" }), (req, res) => {
  const header = req.get("Revenue-Signature") || "";
  const parts = Object.fromEntries(header.split(",").map((part) => part.split("=")));
  const expected = crypto
    .createHmac("sha256", process.env.REVENUE_WEBHOOK_SECRET)
    .update(`${parts.t}.${req.body}`)
    .digest("hex");
  const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
  const valid = parts.v1 && expected.length === parts.v1.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
  if (!fresh || !valid) return res.status(400).end();

  const event = JSON.parse(req.body);
  handle(event);
  res.status(200).end();
});

Retries

Answer with any 2xx within ten seconds. Anything else is retried after 1, 5, 30, 120, 360 and 1440 minutes, then marked failed. Every delivery is in the dashboard log and can be replayed by hand. Use Revenue-Event-Id to ignore a delivery you already handled.

On this page