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.

example POST body from your gateway
{
  "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 to canAccess.
  • event.period_end → periodEnd. When access stops. Required on active.

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.

app/api/webhooks/your-gateway/route.ts
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

app/api/webhooks/your-gateway/route.ts
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 Stripe
  • 400 — unknown plan, or missing or invalid periodEnd on an active report
  • A repeated eventId is success with alreadyApplied: true