Introduction
The PayKit API is resource-oriented over HTTPS, JSON in and out, versioned under /v1. This is the integration surface for a client app: manage catalog, subscribe customers, take the first payment via hosted checkout, consume webhooks, and gate features by entitlement.
The topics on the left come in two kinds. API reference topics each show their endpoints, code samples, and (for GET reads) a live test console you can call right here. The Guides are conceptual walkthroughs (like signed entitlement tokens): prose only, no console.
How the API works
PayKit sits between your app and the payment gateway. You never touch card data or gateway credentials directly: you describe what a customer should be billed (a catalog plan version), and PayKit owns how: taking the payment, tokenizing the card, running renewals and dunning, and telling you the result over webhooks.
In plain terms: your backend creates a subscription, you send the customer to hosted checkout, and when they pay, PayKit activates it and calls your webhook. You unlock the feature and can check entitlements any time after. You never see card details; PayKit handles the payment, renewals, and dunning.
The full lifecycle, end to end
The journey runs in three stages (set up & authenticate, subscribe & take payment, then gate & manage), each mapped below with the exact API call named at every step. Paths are shortened: … stands for /v1/clients/{clientId}, the client-scoped base every call shares.
- Ice nodes are PayKit API calls; neutral nodes are things you build; outlined is Paystack; amber is an asynchronous webhook event.
- Thick arrows carry an id you pass to the next call (a
planVersionId,customerId, subscription id); dotted arrows are asynchronous or optional hops.
Stage 1 · Set up & authenticate
Stage 2 · Subscribe & take the first payment
Stage 3 · Gate features & manage
The request lifecycle
Every /v1 call passes through the same pipeline before it touches data:
- Authenticate. The bearer token resolves to an API key (server-to-server) or a Firebase user, scoped to exactly one client.
- Idempotency. Mutating calls require an
Idempotency-Key; a replay returns the original result instead of acting twice. - Authorize.The caller's permissions are checked (never roles); sensitive actions also require a recent sign-in (step-up).
- Tenant isolation.The client is pinned, so a caller only ever reads or writes its own tenant's data.
- Commit. The state, its event, and the outbox row are written together; workers then pick up the side effects.
What you implement
A complete integration is four responsibilities on your side:
- A backend that holds an API key. Create a scoped
mbx_key in the dashboard (Developers → API keys) and keep it server-side; never ship it to the browser. It creates customers and subscriptions and starts checkout. Amounts are never in your request bodies; they come from the catalog plan version (card-testing defense). - A checkout redirect. Send the customer to the returned
checkoutUrl; they return to yourcallbackUrl. The callback is a UX return, not a confirmation. Do not grant access on it. - A webhook endpoint. In the dashboard (Developers → Webhooks), register a public HTTPS URL and store the signing secret shown once; then verify every delivery on the raw body before acting. This is where you react to
subscription.activated,payment.failed, and the rest. Deliveries are at-least-once, so dedupe onx-mbx-delivery. - A feature gate. On each access check, read entitlements: a direct call, or a cached signed token you verify locally against the public JWKS.
Conventions at a glance
- Authentication. Bearer token via
Authorization: Bearer <token>: either a scoped API key (startsmbx_, server-to-server) or a Firebase ID token (a dashboard user). - Idempotency. Every mutating request requires an
Idempotency-Keyheader (a UUID); reuse it when retrying. - Step-up. Sensitive actions require a recent sign-in; a stale ID token returns
401 step_up_required. - IDs, money, time. IDs are UUIDs (dash-less accepted). Money is integer minor units + an ISO-4217 code (
4990 ZAR= R49.90). Timestamps are UTC ISO-8601. - Errors. Non-2xx uses one envelope:
{ "error": { "code", "message" } }.