Developer API
Integrate the Payment Integration API into your application. Authentication, payments, plans, subscriptions, and webhooks.
Introduction#
The Payment Integration API is a RESTful, JSON-only service that aggregates Paystack, Flutterwave, and Hubtel behind a single tenant-scoped surface. Every endpoint expects and returns JSON; every protected endpoint is scoped to your client (tenant).
The host shown in the snippets below is https://api.example.com. Replace it with your deployment URL (typically http://localhost:8080 when running locally).
Already authenticated?
https://api.example.com/swagger/ covers every endpoint with parameter docs and live "Try it out" support.Authentication#
Two auth modes are accepted on protected routes. JWT (Bearer token) is the recommended default; API keys exist for server-to-server and demo flows.
JWT (recommended)#
Exchange email + password for an access JWT and a longer-lived refresh JWT. Send the access token as a Bearer header on subsequent calls.
curl -X POST 'https://api.example.com/api/v1/auth/login/client' \
-H 'Content-Type: application/json' \
-d '{"email":"you@example.com","password":"********"}'{
"token": "eyJhbGciOi...", // 24h access token
"refresh_token": "eyJhbGciOi...",// 30-day refresh token
"client": {
"id": "0e3c0c5a-...",
"name": "Acme Corp",
"email": "you@example.com",
"slug": "acme-corp",
"password_changed": true
}
}curl -X GET 'https://api.example.com/api/v1/me' \
-H 'Authorization: Bearer eyJhbGciOi...'API key#
API keys are issued from the dashboard (/api-keys) or programmatically. Send them as X-API-Key:
curl -X GET 'https://api.example.com/api/v1/transactions' \
-H 'X-API-Key: pi_live_abc123...'Where to use which
Refreshing tokens#
Access tokens expire (default 24h). When you get a 401, exchange your refresh token for a fresh pair. The previous refresh token is revoked atomically.
curl -X POST 'https://api.example.com/api/v1/auth/refresh' \
-H 'Content-Type: application/json' \
-d '{"refresh_token":"eyJhbGciOi..."}'The web client does this automatically on 401 — see src/services/api/client.ts.
Logging out#
Logout revokes the refresh token server-side so it cannot be used again, even if it leaks. Idempotent — unknown tokens still return 200.
curl -X POST 'https://api.example.com/api/v1/auth/logout' \
-H 'Content-Type: application/json' \
-d '{"refresh_token":"eyJhbGciOi..."}'Quick start#
Three calls: log in, initiate a one-time payment, redirect the user to the gateway.
// 1. Log in (browser or server)
const login = await fetch('https://api.example.com/api/v1/auth/login/client', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'you@example.com', password: process.env.PWD }),
}).then(r => r.json());
const token = login.token;
// 2. Initiate a payment
const payment = await fetch('https://api.example.com/api/v1/payments/initiate', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
email: 'customer@example.com',
amount: 5000, // major units (e.g. 5000 NGN)
currency: 'NGN',
provider: 'paystack',
callback: 'https://yourapp.com/payment/callback',
}),
}).then(r => r.json());
// 3. Redirect the customer to the gateway page
window.location.href = payment.redirect_url;
// On callback, you'll get ?reference=trx_xxx&provider=paystack
// GET /api/v1/payments/verify/{reference}?provider=paystackReference handling
reference in the body and the gateway will use it. Otherwise the API generates one (trx_-prefixed UUID).Payments#
Initiate a payment#
POST /api/v1/payments/initiate
Authorization: Bearer <token>
Content-Type: application/json
{
"email": "customer@example.com",
"amount": 5000,
"currency": "NGN",
"provider": "paystack", // paystack | flutterwave | hubtel
"callback": "https://yourapp.com/return",
"reference": "order_123" // optional; auto-generated if absent
}{
"is_redirect": true,
"redirect_url": "https://checkout.paystack.com/...",
"reference": "order_123"
}Verify a payment#
Call this on your callback URL with the reference the gateway returns. The API checks with the gateway, updates the local transaction status, and returns the canonical state.
GET /api/v1/payments/verify/{reference}?provider=paystack
Authorization: Bearer <token>{
"status": "success", // success | failed | pending
"internal_ref": "order_123",
"gateway_ref": "T123456",
"amount": 5000,
"currency": "NGN"
}List transactions#
GET /api/v1/transactions?page=1&page_size=20&status=completed&gateway=paystack
Authorization: Bearer <token>Filters: status, gateway, currency_code, start_date, end_date, internal_ref, gateway_ref, customer_id, subscription_id. Sort: sort_by ∈ {created_at|amount|status} + sort_order.
Plans & Subscriptions#
Create a plan#
CreatePlan is supported on Paystack and Flutterwave. Hubtel doesn't expose a plan API.POST /api/v1/plans
Authorization: Bearer <token>
Content-Type: application/json
{
"name": "Pro Monthly",
"amount": 10000,
"currency_code": "NGN",
"interval": "monthly", // daily | weekly | monthly | annually
"provider": "paystack"
}List plans#
GET /api/v1/plans
Authorization: Bearer <token>Create a subscription#
POST /api/v1/subscriptions/create
Authorization: Bearer <token>
Content-Type: application/json
{
"plan_id": "0a3...uuid",
"email": "customer@example.com",
"name": "Customer Name", // optional
"user_id": "your-user-uuid", // optional; for your own user mapping
"customer_id": "existing-customer-uuid" // optional; reuses an existing Customer
}The response includes an authorization_url when the gateway needs the customer to authorize a card on file. Redirect them there to complete the subscription setup.
Cancel a subscription#
DELETE /api/v1/gateways/{gateway}/subscriptions/{id}
Authorization: Bearer <token>Cancels at the gateway when possible (Paystack requires email_token, stored by the API in the subscription metadata) and soft-deletes the local row.
BYO gateway credentials#
Each tenant can supply their own Paystack/Flutterwave/Hubtel credentials, encrypted at rest with AES-GCM. When set, the API uses these instead of the global keys.
PUT /api/v1/client/gateways/paystack/config
Authorization: Bearer <token>
Content-Type: application/json
{
"secret_key": "sk_live_...",
"public_key": "pk_live_...",
"is_enabled": true
}The GET response is masked: it returns flags (has_secret_key, has_api_key, …) but never the plaintext.
{
"gateway": "paystack",
"configured": true,
"is_enabled": true,
"public_key": "pk_live_...",
"has_secret_key": true,
"has_api_key": false,
"has_api_secret": false,
"has_webhook_secret_hash": false
}Demo accounts
Webhooks#
URLs#
Configure these URLs in the corresponding gateway dashboards:
Paystack POST https://api.example.com/api/v1/webhooks/paystack
Flutterwave POST https://api.example.com/api/v1/webhooks/flutterwave
Hubtel POST https://api.example.com/api/v1/webhooks/hubtelThe dashboard at /account/gateways shows your specific URLs (with the right host) and a copy button.
Event types we react to#
charge.success→ updates the matching one-time transaction tocompletedsubscription.created→ flips the subscription toactivesubscription.cancelled/subscription.disable→ flips tocancelledinvoice.payment_succeeded→ records a transaction row + bumpsnext_billing_date
Other events are accepted (200 OK) but ignored.
Signatures#
Each gateway is verified before processing — the API will return 401 if the signature header is missing or wrong.
- Paystack —
X-Paystack-Signature(HMAC-SHA512 of body with the secret key) - Flutterwave —
verif-hashheader must match the dashboard secret - Hubtel — IP allowlist (gateway-side); body signed via API credentials
Events are deduplicated by gateway + event ID with a 48-hour Redis TTL. Re-deliveries return 200 with "status":"duplicate".
Errors & rate limits#
All error responses are JSON of the form {"error": "human-readable message"}. Status codes follow standard conventions:
400— invalid input (validation, malformed body)401— missing or invalid auth (try refresh)403— authenticated but not allowed (e.g. demo account hitting BYO config write)404— resource not found, or not yours409— conflict (duplicate plan name, email already in use, plan still has subscriptions)429— rate limit exceeded (seeX-RateLimit-*headers)5xx— bug or upstream gateway issue; safe to retry idempotent calls
Authenticated routes are rate-limited at 100 req/min per client by default. The public demo endpoint is capped at 5 req/hour per IP.
Full API reference#
Every endpoint, schema, and error code is documented in the interactive Swagger spec. Run the API with SWAGGER_ENABLED=true (default outside production) and point at:
https://api.example.com/swagger/The raw OpenAPI 2.0 JSON lives at https://api.example.com/swagger/doc.json — drop it into Postman, Insomnia, or any code generator (openapi-generator, oapi-codegen) for typed client SDKs.