Paystack recurring billing has four core objects. A Plan defines the price and interval. A Subscription links a customer to a plan and tracks the billing cycle. An Authorization stores the tokenized payment method. An Invoice represents each billing attempt. The customer object ties everything together. Understanding these relationships is the foundation for building any recurring billing system on Paystack.
Most Paystack billing bugs come from misunderstanding how the objects relate to each other. A developer stores the subscription code but forgets the authorization code. Another assumes cancelling a subscription also invalidates the authorization. A third tries to change a plan's price and discovers Paystack does not allow it.
This article maps every object in the Paystack recurring billing system, shows you their JSON structures, explains every important field, and traces the relationships between them. If you are building anything that charges customers more than once, start here.
This is a spoke article in the subscriptions and recurring billing cluster. The pillar covers the high-level flow. This article goes deeper on the data.
A plan is a billing template. It defines what you charge and how often. Plans do not belong to customers. They belong to your Paystack account. You create one plan and subscribe hundreds or thousands of customers to it.
{
"id": 28340,
"domain": "live",
"name": "Pro Monthly",
"plan_code": "PLN_xxxxxxxxxxxxx",
"description": "Pro tier with all features",
"amount": 500000,
"interval": "monthly",
"currency": "NGN",
"invoice_limit": 0,
"send_invoices": true,
"send_sms": true,
"hosted_page": false,
"is_archived": false,
"subscriptions_count": 47,
"active_subscriptions_count": 42,
"total_revenue": 23500000,
"createdAt": "2026-01-15T10:00:00.000Z",
"updatedAt": "2026-07-01T08:30:00.000Z"
}
Key fields to understand:
What you cannot change: Once created, a plan's amount, interval, and currency are immutable. If you need a new price, create a new plan and migrate subscribers. See grandfathering old prices.
Creating a plan via the API:
const axios = require('axios');
async function createPlan() {
const response = await axios.post(
'https://api.paystack.co/plan',
{
name: 'Pro Monthly',
amount: 500000,
interval: 'monthly',
currency: 'NGN',
description: 'Pro tier with all features',
invoice_limit: 0,
send_invoices: true,
send_sms: false,
},
{
headers: {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
'Content-Type': 'application/json',
},
}
);
console.log('Plan code:', response.data.data.plan_code);
return response.data.data;
}
Get weekly developer tips
Join 25,000+ developers. Practical guides, job tips, and new content — straight to your inbox.
No spam. Unsubscribe anytime.
A subscription is the link between a customer and a plan. It tracks the billing state: when the next charge happens, how many successful charges have been made, and whether the subscription is active, cancelled, or completed.
{
"id": 91450,
"domain": "live",
"subscription_code": "SUB_xxxxxxxxxxxxx",
"email_token": "abc123token",
"status": "active",
"start": 1720000000,
"quantity": 1,
"plan": {
"plan_code": "PLN_xxxxxxxxxxxxx",
"name": "Pro Monthly",
"amount": 500000,
"interval": "monthly",
"currency": "NGN"
},
"customer": {
"id": 54321,
"customer_code": "CUS_xxxxxxxxxxxxx",
"email": "user@example.com",
"first_name": "Ada",
"last_name": "Obi"
},
"authorization": {
"authorization_code": "AUTH_xxxxxxxxxxxxx",
"bin": "408408",
"last4": "4081",
"exp_month": "12",
"exp_year": "2030",
"card_type": "visa",
"bank": "TEST BANK",
"reusable": true
},
"next_payment_date": "2026-08-20T00:00:00.000Z",
"invoices_count": 5,
"invoice_limit": 0,
"most_recent_invoice": {
"status": "success",
"amount": 500000,
"paid_at": "2026-07-20T06:15:00.000Z"
},
"createdAt": "2026-03-20T10:00:00.000Z",
"updatedAt": "2026-07-20T06:15:00.000Z"
}
Key fields:
One customer can have multiple subscriptions to different plans. But a single subscription is always linked to exactly one plan and one authorization at a time.
Fetching a subscription:
async function getSubscription(subscriptionCode) {
const response = await axios.get(
'https://api.paystack.co/subscription/' + subscriptionCode,
{
headers: {
Authorization: 'Bearer ' + process.env.PAYSTACK_SECRET_KEY,
},
}
);
const sub = response.data.data;
console.log('Status:', sub.status);
console.log('Next payment:', sub.next_payment_date);
return sub;
}
Paystack creates an invoice before each subscription renewal. The invoice tracks the billing attempt: what was charged, whether it succeeded, and when. Invoices are the transaction layer of the subscription system.
{
"subscription": "SUB_xxxxxxxxxxxxx",
"integration": 100032,
"domain": "live",
"invoice_code": "INV_xxxxxxxxxxxxx",
"customer": {
"customer_code": "CUS_xxxxxxxxxxxxx",
"email": "user@example.com"
},
"transaction": {
"reference": "T123456789",
"status": "success",
"amount": 500000,
"currency": "NGN"
},
"amount": 500000,
"status": "success",
"paid": true,
"paid_at": "2026-07-20T06:15:00.000Z",
"period_start": "2026-07-20T00:00:00.000Z",
"period_end": "2026-08-20T00:00:00.000Z",
"createdAt": "2026-07-20T00:00:00.000Z"
}
Key fields:
Invoices drive the webhook events you listen for. When Paystack creates an invoice, you get invoice.create. When the charge succeeds, you get invoice.update. When it fails, you get invoice.payment_failed. For a deep dive into processing these events, see invoice events for subscription lifecycles.
The customer object is the anchor of the entire model. Every authorization, subscription, and transaction belongs to a customer. Paystack identifies customers by email address. When you initialize a transaction with an email, Paystack either finds the existing customer or creates a new one.
{
"id": 54321,
"customer_code": "CUS_xxxxxxxxxxxxx",
"email": "user@example.com",
"first_name": "Ada",
"last_name": "Obi",
"phone": "+2348012345678",
"risk_action": "default",
"international_format_phone": "+2348012345678",
"authorizations": [
{
"authorization_code": "AUTH_card1xxxxxxx",
"last4": "4081",
"reusable": true
},
{
"authorization_code": "AUTH_card2xxxxxxx",
"last4": "9922",
"reusable": true
}
],
"subscriptions": [
{
"subscription_code": "SUB_xxxxxxxxxxxxx",
"plan": { "plan_code": "PLN_xxxxxxxxxxxxx" },
"status": "active"
}
]
}
The relationship map:
A common confusion: developers assume that cancelling a subscription invalidates the authorization. It does not. The authorization is tied to the card, not the subscription. Even after cancelling a subscription, you can still use the authorization code to charge the customer via charge_authorization. The authorization only stops working when the underlying card expires or is revoked by the issuing bank.
Knowing the happy path is not enough. These edge cases have caused real production bugs.
Same email, different Paystack accounts
If a customer pays on your Paystack account and also pays on another merchant's account, those are separate customer objects. Authorizations from one merchant's account cannot be used by another. The authorization is scoped to the merchant's integration.
Multiple subscriptions, same customer
A customer can have subscriptions to multiple plans simultaneously. If you offer "Basic" and "Add-on Storage" as separate plans, the same customer can subscribe to both. Your application logic needs to handle this. Check all active subscriptions, not just the first one.
Multiple authorizations, subscription uses one
When a customer adds a new card, the old authorization still exists. The subscription continues charging the authorization it was created with. If the customer wants to pay with their new card, you need to update the subscription's authorization or cancel and recreate the subscription with the new authorization.
Non-reusable authorization on subscription attempt
If you try to create a subscription using a non-reusable authorization (from a bank transfer, for example), the API call will fail. Always check the reusable flag before attempting to create a subscription.
Plan archived but subscriptions still billing
Archiving a plan prevents new subscriptions but does not cancel existing ones. Those subscriptions keep billing at the old price until the customer cancels or the invoice limit is reached. This is by design, but you need to account for it in your billing reports.
Invoice limit reached
When a subscription hits its invoice limit, the status changes to "completed." This is a terminal state. The subscription will not renew again. If the customer wants to continue, you need to create a new subscription.
Do not treat Paystack as your source of truth for billing state. Mirror the key objects in your own database. This gives you faster reads, lets you add business logic Paystack does not support, and protects you if the Paystack API is temporarily unreachable.
A practical schema for your local billing tables:
-- Plans (mirror Paystack plans)
CREATE TABLE billing_plans (
id SERIAL PRIMARY KEY,
paystack_plan_code VARCHAR(50) UNIQUE NOT NULL,
name VARCHAR(100) NOT NULL,
amount_minor INTEGER NOT NULL,
currency VARCHAR(3) NOT NULL,
interval VARCHAR(20) NOT NULL,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT now()
);
-- Customer payment methods (mirror Paystack authorizations)
CREATE TABLE payment_methods (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
paystack_authorization_code VARCHAR(50) NOT NULL,
last4 VARCHAR(4),
card_type VARCHAR(20),
exp_month VARCHAR(2),
exp_year VARCHAR(4),
bank VARCHAR(100),
is_reusable BOOLEAN NOT NULL,
is_default BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ DEFAULT now()
);
-- Subscriptions (mirror Paystack subscriptions with extra state)
CREATE TABLE subscriptions (
id SERIAL PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
plan_id INTEGER REFERENCES billing_plans(id),
payment_method_id INTEGER REFERENCES payment_methods(id),
paystack_subscription_code VARCHAR(50) UNIQUE,
paystack_email_token VARCHAR(100),
status VARCHAR(20) NOT NULL DEFAULT 'active',
current_period_start TIMESTAMPTZ,
current_period_end TIMESTAMPTZ,
cancelled_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now()
);
-- Invoices (mirror Paystack invoices)
CREATE TABLE subscription_invoices (
id SERIAL PRIMARY KEY,
subscription_id INTEGER REFERENCES subscriptions(id),
paystack_invoice_code VARCHAR(50) UNIQUE,
paystack_transaction_ref VARCHAR(100),
amount_minor INTEGER NOT NULL,
currency VARCHAR(3) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'pending',
period_start TIMESTAMPTZ,
period_end TIMESTAMPTZ,
paid_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT now()
);
The status field on the subscriptions table is your own state machine. Paystack's statuses are limited. Your statuses can include "trialing", "active", "past_due", "cancelled", "expired", and anything else your business needs. Update this status based on webhook events from Paystack, but the values and transitions are yours to define.
For a complete guide on building your own billing engine around this schema, see building your own billing engine on Paystack authorizations.
Here are the API calls you will use most often when working with the data model.
const axios = require('axios');
var PAYSTACK_SECRET = process.env.PAYSTACK_SECRET_KEY;
var headers = {
Authorization: 'Bearer ' + PAYSTACK_SECRET,
'Content-Type': 'application/json',
};
// List all plans
async function listPlans() {
var response = await axios.get('https://api.paystack.co/plan', { headers: headers });
return response.data.data;
}
// Get a specific plan
async function getPlan(planCode) {
var response = await axios.get(
'https://api.paystack.co/plan/' + planCode,
{ headers: headers }
);
return response.data.data;
}
// List subscriptions (paginated)
async function listSubscriptions(page, perPage) {
var response = await axios.get(
'https://api.paystack.co/subscription?page=' + page + '&perPage=' + perPage,
{ headers: headers }
);
return response.data;
}
// Get customer with their authorizations and subscriptions
async function getCustomerFull(emailOrCode) {
var response = await axios.get(
'https://api.paystack.co/customer/' + emailOrCode,
{ headers: headers }
);
var customer = response.data.data;
console.log('Authorizations:', customer.authorizations.length);
console.log('Subscriptions:', customer.subscriptions.length);
return customer;
}
Pagination matters. If you have hundreds of subscriptions, the list endpoint returns them in pages. Always check the meta field in the response for total, page, and pageCount to know if there are more pages to fetch.
For building admin views and dashboards on top of this data, see building a SaaS billing page. For exporting this data to a warehouse, see the verification cluster articles on exporting Paystack data.
KES 2,999
The definitive guide to building payment systems with Paystack — from your first transaction to production-grade payment infrastructure across Africa. 9 modules, 47 lessons. Free preview available.
Start Paystack Course