Billing
Put a customer on a plan, change it, cancel it, preview every charge first, or hand them a billing page.
billing.attach
One call for a new subscription, an upgrade, a downgrade, an add on, a one off purchase, prepaid packs or a trial.
{
"customer_id": "user_123",
"plan_id": "pro",
"success_url": "https://app.example.com/billing?done=1",
"redirect_mode": "if_required",
"feature_quantities": [{ "feature_id": "seats", "quantity": 5 }],
"promo_code": "LAUNCH20"
}| Field | Meaning |
|---|---|
entity_id | Attach the plan to a seat or workspace instead of the customer |
redirect_mode | if_required (default) charges the saved card when there is one. always sends them to checkout |
feature_quantities | How many of each prepaid item to buy, rounded up to packs |
currency | Price in this currency; a customer who has paid stays in theirs |
free_trial | false skips the trial even if they are eligible |
plan_schedule | immediate or end_of_cycle overrides upgrade now and downgrade later |
customize | A custom deal for this customer: { "price": { "currency": "USD", "amount": 90000 }, "items": [{ "feature_id": "messages", "included": 50000 }] } |
carry_over_balances, carry_over_usages | { "enabled": true, "feature_ids"?: [...] } keeps unused balance, or keeps usage so an upgrade does not reset it |
promo_code | Applies a reward before the charge |
customer_data | { "name", "email" } creates the customer if they do not exist |
What happens:
| Situation | Result |
|---|---|
| Free plan, or a trial without a card | Attached now, attached: true |
| Upgrade in the same group | Charged now for the days left, with a credit for unused time on the old plan; the billing date stays |
| Downgrade in the same group | Scheduled for the end of the period, scheduled: true, nothing charged |
| Saved reusable card | Charged now, attached: true |
No saved card, or redirect_mode: "always" | payment_url to checkout. The card is saved when they pay |
{
"customer_id": "user_123",
"plan_id": "premium",
"action": "upgrade",
"attached": true,
"status": "active",
"payment_url": null,
"invoice": { "number": "INV-202610-8F2CKQ4M", "status": "paid", "total": 2999, "currency": "USD", "charge_amount": 46485, "charge_currency": "GHS" }
}{
"attached": false,
"payment_url": "https://checkout.paystack.com/abc123",
"required_action": { "code": "payment_method_required", "reason": "The customer needs to pay at checkout" },
"invoice": { "...": "..." }
}required_action.code is payment_method_required, payment_failed (the card was declined; payment_url is a hosted invoice page where they can pay another way) or 3ds_required (their bank wants a confirmation). In every case redirect to payment_url; the plan is granted the moment the payment lands, and never twice.
Usage billed at the end of the period on the old plan is invoiced on its own when the plan changes, so nothing is lost if they abandon checkout.
billing.preview_attach
Same body as attach, nothing is charged. Show it before the customer confirms.
{
"action": "upgrade",
"schedule": "immediate",
"currency": "USD",
"line_items": [
{ "kind": "proration", "title": "Remaining time on Premium", "amount": 4999 },
{ "kind": "credit", "title": "Unused time on Pro", "amount": -2000 }
],
"subtotal": 2999,
"discount_total": 0,
"credit_applied": 0,
"total": 2999,
"charge": { "currency": "GHS", "amount": 46485 },
"trial": null,
"next_cycle": { "starts_at": "2026-11-11T13:00:00.000Z", "total": 5000 },
"payment_method_on_file": true
}billing.update
Change a plan the customer already has.
| Body | Effect |
|---|---|
{ "customer_id", "plan_id", "cancel_action": "cancel_end_of_cycle" } | Keeps access until the period ends, then their default plan comes back |
{ ..., "cancel_action": "cancel_immediately" } | Ends now and refunds the unused time to their card |
{ ..., "cancel_action": "uncancel" } | Undoes a pending cancellation, or removes a scheduled downgrade |
{ ..., "feature_quantities": [{ "feature_id": "seats", "quantity": 8 }] } | More is charged now for the days left; fewer takes effect at renewal |
billing.preview_update takes the same body and answers with line_items, total, refund and effective_at. billing.cancel with { "when": "now" | "end_of_cycle" } still works.
billing.setup_payment
{ "customer_id": "user_123", "success_url"? } returns a payment_url that saves a new card with a small charge refunded at once. Use it when a card is about to expire or after a decline.
billing.open_customer_portal
{ "customer_id": "user_123", "return_url"?: "https://app.example.com/settings" } returns { "url": "...", "expires_in": 3600 }.
The hosted page shows their balances, every plan with what it would cost them now, resume for a plan that is ending, card update, unpaid invoices to pay, past invoices and a promo code field. Links last one hour, so make a fresh one each time someone clicks Billing.
Renewals and failed payments
At the end of each period Revenue builds the renewal invoice (the plan price, prepaid packs for the next period, pay as you go usage from the period that ended, extra seats), takes discounts and account credit off, and charges the saved card. Plan checkout is card only, so this works for every new subscriber. If there is still no reusable card (a subscriber from before card only, or a removed card), the invoice gets a hosted pay link that takes a card and invoice.created fires. A failed renewal makes the plan past_due and retries on day 1, 3 and 5. When the retries run out the invoice is marked uncollectible, the plan ends and their default plan comes back.