Skip to content

Subscriptions

Subscriptions bill a customer the same amount every period. You describe what you sell as products and plans, subscribe a customer to a plan, and PaySigna opens an invoice each period, collects it, retries it when it fails, and tells the customer at each step.

Object What it is
Product Something you sell on a subscription — LoveSync Premium.
Plan A price for a product: an amount every so many days, weeks, months or years, with an optional free trial. A product can have several plans — monthly, annual.
Subscription One customer on one plan.
Invoice One period’s bill for a subscription, and every attempt to collect it.
Coupon A percentage or an amount off a subscription’s bills, for one bill, several or all of them.

Invoices can also be issued by hand, outside any subscription, and reduced by credit notes or customer credit — see Invoices and credit.

Everything here is under the Billing tag in the API tester.

Read this before you write any copy for your customers.

On mobile money, every renewal is a prompt. A wallet cannot be debited without its owner: each period we open an invoice and send a payment prompt to the subscription’s renewal number, and the customer approves it on their phone. If they ignore it, nothing is collected. Do not describe a mobile-money subscription as automatic or auto-renewing — it is not, and a customer who believed it was will lose access without understanding why.

On a card, renewal can be charged without the customer. When a customer pays a bill with a card and the card network allows it to be charged again, the card is stored against the subscription (shown only as, for example, Visa •••• 4242). Each renewal tries that card first, with no prompt. If the card fails on a bill, the retries for that bill are prompts to the mobile-money number on file.

Either way we tell the customer by SMS and email before and during each bill, so a renewal is never a surprise — see What the customer receives.

These are setup, done once — from your server, or from Billing → Plans in the portal.

Terminal window
curl https://sandbox.paysigna.com/v1/products \
-H "Authorization: Bearer psg_test_…" \
-H "Content-Type: application/json" \
-d '{ "name": "LoveSync Premium", "description": "Unlimited matches" }'
Terminal window
curl https://sandbox.paysigna.com/v1/plans \
-H "Authorization: Bearer psg_test_…" \
-H "Content-Type: application/json" \
-d '{
"product": "<product id>",
"nickname": "Monthly",
"amount": 5000,
"currency": "GHS",
"interval": "month",
"interval_count": 1,
"trial_days": 7
}'
Field Notes
amount Per period, in minor units. 5000 is GHS 50.00.
interval day, week, month or year.
interval_count Bill every this many intervals. 3 with month is quarterly. Defaults to 1.
trial_days Optional free trial. Nothing is collected during it.

Every field is in the reference: create a product, create a plan.

GET /v1/plans also accepts a publishable key holding plan.read, and then returns active plans only — safe to call from a browser to render a pricing page. With a secret key it returns every plan, with its subscribers count.

POST /v1/subscriptions, with a secret key holding billing.write. The Idempotency-Key header is required: a retried create must not put the customer on the plan twice.

The customer is a stored customer’s id, or a phone, email and name — in which case they are stored for you, exactly as a payer is on a charge.

There are three ways to start, depending on whether the plan has a trial and how you want the first bill paid. All fields: create a subscription.

Terminal window
curl https://sandbox.paysigna.com/v1/subscriptions \
-H "Authorization: Bearer psg_test_…" \
-H "Idempotency-Key: 9f3c2a71-4b8e-4d1f-a6c5-0e2b7d9f8a14" \
-H "Content-Type: application/json" \
-d '{
"plan": "<plan id>",
"customer": { "phone": "0541234567", "name": "Ama Mensah", "email": "[email protected]" },
"payment_method": { "phone": "0541234567", "momo_network": "mtn" }
}'

The subscription comes back incomplete with its first invoice open, and a prompt goes to that number within a minute. It becomes active when the invoice is paid. payment_method.phone is the renewal number every later bill is prompted on; it defaults to the customer’s phone. The network is named by code or by momo_network_id, as on a charge.

Let them pay the first bill on your checkout page

Section titled “Let them pay the first bill on your checkout page”

Send checkout instead of payment_method:

{
"plan": "<plan id>",
"customer": { "email": "[email protected]", "name": "Ama Mensah" },
"checkout": { "return_url": "https://your-app.example.com/subscribed" }
}

The response carries a payment_session with an id, a client_token and expires_at. Hand the client_token to your checkout page exactly as for any payment session — it is shown once. Whatever the customer pays from becomes how the subscription renews: the mobile-money number they approve on, or the card. payment_method on the subscription is absent until that first bill is paid.

The session asks for the first bill’s actual amount, after any coupon and credit, not the plan price. If a coupon or credit covers the whole first bill, there is no payment_session in the response: the subscription is already active.

On a plan with trial_days, the subscription starts trialing, nothing is collected, and the first invoice opens when the trial ends. payment_method is required, because there is no first payment to learn the number from, and checkout is refused.

Add "coupon" with a coupon’s id, or the code the customer typed (case does not matter). The discount comes off from the first bill — or, on a trial, from the first bill after it. A code that is unknown, inactive, past its redeem_by or fully redeemed is refused with invalid_field on coupon, and the subscription is not created. See Coupons and discounts.

If the coupon, or the customer’s credit, covers the whole first bill, nothing is collected: the subscription comes back active and invoice.paid fires with amount: 0.

Status Meaning
incomplete Created; the first bill is open and not yet paid.
trialing In a free trial. Nothing collected yet.
active Paid up for the current period.
past_due An attempt to collect the current bill failed. Retries continue.
paused Billing stopped. No invoice opens while paused.
canceled Ended. cancel_reason says why: merchant, customer or unpaid.

current_period_start and current_period_end are the period that has been paid for — or is being billed. cancel_at_period_end is true when it will end at current_period_end rather than renew.

Each period opens one invoice — never two for the same period, and never more than one open at a time. GET /v1/invoices lists them (filter by subscription or status); GET /v1/invoices/{id} adds its lines, every attempt to collect it and the charge each one created.

A period’s bill is the plan’s price, less the subscription’s discount, less any credit the customer holds: amount = subtotal − discount − credit_applied. The fields are described in What an invoice is made of.

When a bill is not paid, it is attempted again on a schedule counted from the day it opened:

Default
Attempts on days 0, 1, 3, 5
If still unpaid on day 7 — the invoice becomes uncollectible and the subscription ends with cancel_reason: unpaid
Renewal notice 3 days before each renewal on monthly and longer plans, and before a trial ends

You can change all three in the portal under Billing → Retries. Retry days apply at once, including to the next retry of a bill already open (still counted from the day it opened). The end day is fixed when a bill opens — it is the invoice’s collect_until, the pay-by date the customer was told — so a change applies only to bills opened afterwards. Notice days apply from the next notice. next_attempt_at is the next attempt.

Every attempt is an ordinary charge. It appears in your transactions with the invoice number as its reference, settles into your balance like any payment, and can be refunded like any payment. invoice.transaction is the charge that paid it.

A coupon takes a percentage or a fixed amount off a subscription’s bills. POST /v1/coupons:

{
"name": "Launch offer — 20% off for 3 months",
"code": "LAUNCH20",
"percent_off": 20,
"duration": "repeating",
"duration_periods": 3,
"max_redemptions": 500,
"redeem_by": "2026-12-31T23:59:59Z"
}
Field Notes
name Required. Shown on the invoice’s discount line.
code Optional. What a customer types: 3 to 40 letters, digits, dashes or underscores. Stored in capitals; case does not matter when it is used. Leave it out for a coupon only you apply, by id.
percent_off Above 0, at most 100.
amount_off + currency A fixed amount in minor units. Send this or percent_off, not both.
duration once (the next bill), repeating (duration_periods bills, 1 to 60) or forever.
max_redemptions Optional. How many subscriptions may use it.
redeem_by Optional. The last moment it can be applied. Must be in the future.
products Optional. Product ids it is restricted to. Empty means any.

All fields: create a coupon.

A coupon cannot be edited, only switched off: POST /v1/coupons/{id} with { "active": false }. An inactive coupon cannot be applied; subscriptions that already carry it keep it.

At creation with coupon (above), or later:

Call What it does
POST /v1/subscriptions/{id}/discount with { "coupon": "<id or code>" } Applies the coupon, replacing any the subscription had. Discounts do not stack.
DELETE /v1/subscriptions/{id}/discount Removes the discount.

Both apply from the next bill opened. A bill already open keeps its amount, because the customer may be approving it right now. A subscription that has ended cannot take a coupon.

Each application counts one redemption, towards max_redemptions and in times_redeemed. A coupon is refused if it is restricted to other products, or if it is a fixed amount in a currency other than the plan’s.

A percentage is rounded down to the minor unit. A fixed amount never takes off more than the bill. The discount is a negative discount line on the invoice, named after the coupon, and the invoice’s discount field. A once or repeating coupon counts down one bill each time it takes something off. When it reaches zero the subscription is billed at the full price again.

Call What it does
POST /v1/subscriptions/{id}/cancel Ends it now, voiding any open invoice.
… with { "at_period_end": true } Keeps it until the paid period runs out, then ends it. Nothing more renews.
POST /v1/subscriptions/{id}/pause Stops billing. No invoice opens while paused.
POST /v1/subscriptions/{id}/resume Restarts billing. If the paid period ran out while it was paused, the next one starts now and is billed at once.

The customer is emailed when a subscription ends. Customers can also cancel themselves — see below.

PaySigna sends these to your customer, on your behalf and with your name on them:

When How
Subscribed Email A welcome, with their manage code.
Before each renewal, and before a trial ends SMS and email The amount and the date.
A bill is due SMS and email Sent with the first prompt. Not sent when a stored card is being charged, since nothing is asked of them.
A payment failed SMS and email With the date they must pay by.
A bill was paid Email A receipt.
The subscription ended Email —

Every notice carries the customer’s manage code. On the PaySigna subscription page they type that code to see their subscription, Pay now a missed bill (which sends a fresh prompt), or Cancel. Cancelling there keeps what they paid for until the end of the period; if nothing has been paid yet it ends at once. The code is typed on the page; it is never put in a link. See Billing.

Event When
subscription.created A subscription was created.
subscription.activated The first invoice was paid, or a trial began.
subscription.trial_will_end A trial ends soon; the first bill follows.
subscription.renewal_upcoming A renewal notice was sent to the customer.
subscription.past_due Collecting an invoice failed; retries continue.
subscription.cancel_scheduled It will end when the current period does.
subscription.canceled It ended.
subscription.paused / subscription.resumed Billing paused or resumed.
invoice.created An invoice opened for a period.
invoice.paid An invoice was paid. Carries the paying transaction_id.
invoice.payment_failed An attempt failed. More may follow.
invoice.uncollectible The retries ran out. The subscription ends.

invoice.paid can arrive with amount: 0 and no transaction_id when a coupon or credit covered the whole bill. Treat it like any other payment: the period is paid. One-off invoices and credit notes have events of their own — see Invoices and credit.

Grant and revoke access on invoice.paid and subscription.canceled. Those are the two that change what the customer is entitled to. past_due is a warning, not an ending — the customer can still pay within the retry window. Each charge attempt also fires the ordinary transaction.* events; build entitlement on the invoice events rather than on those, because a transaction does not say which period it paid for.

As with every webhook, verify the signature, deduplicate on event id, and read the subscription or invoice when you need its current state. See Webhooks.

Scope Key Grants
billing.read Secret Read products, plans, subscriptions, invoices, coupons, credit notes and customer credit.
billing.write Secret Create products, plans and subscriptions; cancel, pause and resume; coupons and discounts; one-off invoices, voids, credit notes and credit grants. Needs verification level 1 on a live key.
refund.create Secret Also needed for a credit note with outcome: refund.
plan.read Publishable or secret List active plans, for a pricing page. Safe in a browser.

Subscriptions behave exactly as in live, including the retry schedule and the notices (prefixed [TEST]). A successful sandbox card payment stores a reusable card, so you can test card renewals without a real one. See Sandbox and testing.

Proration, changing plan mid-period, and metered usage. To move a customer to a different price, cancel at period end and subscribe them to the new plan.