Build
Other payment gateway
You already take payment somewhere other than Stripe. UsageGate assigns the plan you name. It does not talk to Paystack, Paddle, or any other gateway. Stripe customers stay on Connect Stripe.
When to use this
On Payment gateway, choose Other, then Save. Pick one and stay there. Switching later does not rewrite users who are already on a plan.
Get the plan id
Open Access rules. Under each plan name is the planId. Click it to copy. That string is what you send — not the label, and not a Paystack or Paddle plan code.
What event is
Your gateway already POSTs JSON to a route in your app. That body is event. UsageGate never receives it. You read it, then call reportSubscription.
{
"id": "evt_9f2a81",
"customer_id": "user_123",
"period_end": "2026-10-25T00:00:00.000Z"
}event.id→eventId. Unique for this delivery. If they retry the same webhook, UsageGate does not grant twice.event.customer_id→userId. The same id you pass tocanAccess.event.period_end→periodEnd. When access stops. Required onactive.
Rename those three keys to whatever Paystack, Paddle, or your gateway actually sends. The plan id is not in that JSON — you paste it from Access rules.
Full example — they paid
Same API key as canAccess. File this at the URL you already gave that gateway.
import { GateClient } from "@usagegate/sdk";
const gate = new GateClient(process.env.USAGEGATE_KEY!);
// Your gateway POSTs JSON like:
// {
// "id": "evt_9f2a81",
// "customer_id": "user_123",
// "period_end": "2026-10-25T00:00:00.000Z"
// }
//
// `event` is that JSON. Rename the three fields to whatever
// Paystack / Paddle / your gateway actually sends.
export async function POST(req: Request) {
const event = await req.json();
await gate.reportSubscription({
eventId: event.id, // "evt_9f2a81"
userId: event.customer_id, // same id you use in canAccess
planId: "plan_…", // Access rules → copy under the plan name
status: "active",
periodEnd: event.period_end, // when access stops
});
return new Response("ok");
}Full example — they canceled
import { GateClient } from "@usagegate/sdk";
const gate = new GateClient(process.env.USAGEGATE_KEY!);
// Your gateway POSTs JSON like:
// {
// "id": "evt_9f2a82",
// "customer_id": "user_123"
// }
//
// `event` is that JSON. `eventId` is event.id so a retry
// does not cancel them twice.
export async function POST(req: Request) {
const event = await req.json();
await gate.reportSubscription({
eventId: event.id,
userId: event.customer_id,
planId: "plan_…",
status: "canceled",
});
return new Response("ok");
}Upgrade and downgrade
The same call with the new planId. Do not also send canceled for the old plan.
Cancel
status: "canceled" and the plan they are leaving. UsageGate returns them to the default free plan.
Period end
periodEnd is when access stops. After that instant, checks see an empty balance even if canceled never arrives. A late webhook with a period that already ended is written; access is already over.
What not to call
Do not call grant or grantPlan after a payment. grantPlan is signup onto the free plan only.
Errors
409— the workspace is still set to Stripe400— unknown plan, or missing or invalidperiodEndon an active report- A repeated
eventIdis success withalreadyApplied: true
