Paystack Plans and Subscriptions: The Full Data Model
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.
Why the Data Model Matters
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.
The Plan Object
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:
- plan_code: The stable identifier you use in API calls. It never changes after creation. Hard-code it in your config or store it in a settings table.
- amount: In the smallest currency unit. 500000 means 5,000 NGN (kobo). For KES, the same principle applies. See amount handling for currency specifics.
- interval: One of daily, weekly, monthly, quarterly, biannually, or annually. You cannot use custom intervals like "every 10 days" with the Subscriptions API.
- invoice_limit: Set to 0 for unlimited renewals. Set to a positive integer to cap the number of charges. After that many successful charges, the subscription completes automatically. Useful for fixed-term contracts.
- send_invoices and send_sms: Control whether Paystack sends its default notifications. You can disable these and send your own branded emails instead. See disabling subscription emails.
- is_archived: Archived plans cannot accept new subscriptions but existing subscriptions continue to bill. This is how you phase out a pricing tier.
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;
}
The Subscription Object
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:
- subscription_code: The stable identifier. Use this for enable, disable, and fetch calls.
- email_token: A token the customer can use to manage their subscription (cancel, update card) through Paystack's hosted management page. You can build this into a "Manage Subscription" link in your app.
- status: One of active, non-renewing, attention, or completed. "non-renewing" means the customer cancelled but the current billing period has not ended. "attention" means the last charge failed and Paystack is retrying. "completed" means the invoice_limit was reached.
- next_payment_date: When Paystack will attempt the next charge. This is set automatically based on the plan's interval.
- invoices_count: How many billing cycles have been charged. This is your count of successful renewals.
- authorization: The payment method linked to this subscription. This is the card Paystack will charge on each renewal.
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;
}
The Invoice Object
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:
- status: "success" if paid, "failed" if the charge failed, "pending" if Paystack is still attempting the charge.
- period_start and period_end: The billing period this invoice covers. Use these to determine what access the customer has paid for.
- transaction: The Paystack transaction associated with this invoice. The transaction reference lets you cross-reference with your payment records.
- paid_at: When the payment was confirmed. This may differ from the invoice creation time if retries were needed.
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 and How It Ties Everything Together
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:
- One Customer can have many Authorizations (different cards)
- One Customer can have many Subscriptions (different plans)
- One Subscription belongs to exactly one Plan and one Customer
- One Subscription is linked to exactly one Authorization at any time
- One Subscription generates many Invoices (one per billing cycle)
- One Invoice corresponds to one Transaction (the actual charge attempt)
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.
Relationship Edge Cases That Cause Bugs
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.
Mirroring the Model in Your Database
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.
Fetching and Listing Objects via the API
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.
Key Takeaways
- ✓A Plan is a billing template with a fixed amount, interval, and currency. You cannot change a plan's amount after creation.
- ✓A Subscription links a customer to a plan and holds the billing state: next payment date, number of invoices paid, and active/cancelled status.
- ✓An Authorization is a tokenized payment method. Only authorizations with reusable: true can be used for recurring charges.
- ✓An Invoice is created by Paystack before each subscription renewal. It tracks whether the charge succeeded, failed, or is pending.
- ✓The Customer object is the anchor. One customer can have multiple authorizations (different cards) and multiple subscriptions (different plans).
- ✓Plan codes and subscription codes are stable identifiers. Store them in your database and use them for API calls.
- ✓Understanding the relationships between these objects prevents the most common billing bugs: double charges, missed renewals, and orphaned subscriptions.
Frequently Asked Questions
- Can I change a Paystack plan's amount after creating it?
- No. Plan amount, interval, and currency are immutable after creation. If you need a new price, create a new plan and migrate existing subscribers by cancelling their old subscriptions and creating new ones on the updated plan. You can archive the old plan to prevent new signups.
- What is the difference between subscription_code and email_token?
- The subscription_code is the primary identifier you use in API calls to fetch, enable, or disable a subscription. The email_token is a secondary token designed for customer self-service. You can include it in links that let the customer manage their own subscription through Paystack's hosted page without exposing your API keys.
- Can a customer have multiple active subscriptions?
- Yes. A customer can subscribe to multiple plans simultaneously. Each subscription is independent with its own billing cycle, authorization, and status. Your application needs to check all active subscriptions when determining a customer's access level.
- Does cancelling a subscription invalidate the authorization code?
- No. The authorization is tied to the card, not the subscription. Cancelling a subscription stops future automatic charges on that plan, but the authorization code remains valid. You can still use it to create a new subscription or make manual charges via charge_authorization.
- How do I know which authorization a subscription is using?
- Fetch the subscription using GET /subscription/:code. The response includes an authorization object with the authorization_code, last4, card_type, and other details. This is the payment method Paystack will charge on each renewal.
Ready to build real-world apps?
Join the McTaba Labs full-stack marathon (4 months full-time · 6 months part-time). Learn M-Pesa, USSD, and WhatsApp engineering while shipping 8 production apps.
Apply to the McTaba Marathon