How to build subscriptions and recurring billing with the Pagnovo API
Products, offers, subscriptions and invoices in practice — trials, cycles, pause and cancel, invoice states and the events that warn you before churn.
Pagnovo Team · 2026-08-07
Recurring billing looks simple until you have to handle trials, mid-cycle upgrades, expired cards and customers asking to pause. This guide shows how the Pagnovo API model organizes that — and how to use its events to reduce churn.
Full reference: portal.pagnovo.com/docs.
The model: product → offer → subscription → invoice
The four-layer split looks like bureaucracy at first, but it is what prevents rework later:
| Layer | What it represents | Example |
|---|---|---|
| Product | What you sell | "Professional Plan" |
| Offer | How you sell it | R$ 299/month, 7-day trial |
| Subscription | Who subscribed to what | Customer X subscribed to offer Y |
| Invoice | Each charge generated | March's charge |
The advantage: one product can have several offers (monthly, annual, promotional, partner) without duplicating records. And changing the price on a new offer does not affect people already on the old one.
Step 1 — Customer
POST /v2/customers
{
"name": "Maria Silva",
"email": "maria@company.com.br",
"document": "12345678909"
}
⚠️ The document (CPF/CNPJ) is unique and immutable. You cannot fix it later. Validate before
sending — including the check digit — because a wrong record becomes a duplicate customer you carry
forever.
There is also GET /v2/customers/:id/history, useful for support: it shows the customer's history
without you having to assemble it on your side.
Step 2 — Product and offer
POST /v2/products
{ "name": "Professional Plan" }
POST /v2/offers
{
"productId": "...",
"amount": 29900,
"billingCycle": "MONTHLY",
"billingCycleCount": 12,
"trialDays": 7,
"maxCycles": 24
}
The fields that define the plan's economics:
amount— in cents (R$ 299.00 =29900)billingCycle—MONTHLYorYEARLYtrialDays— free period before the first chargemaxCycles— ends the subscription after N cycles (useful for fixed-term plans)
Coupons and interest use basis points, not plain percentages:
1000= 10%. Fixed penalties stay in cents. Mixing this up produces a discount 100× larger than intended.
Step 3 — Subscription
POST /v2/subscriptions
{
"customerId": "...",
"offerId": "...",
"amount": 29900,
"billingCycle": "MONTHLY"
}
And the full lifecycle, without you implementing it:
POST /v2/subscriptions/:id/pause → temporary pause
POST /v2/subscriptions/:id/resume → resume
POST /v2/subscriptions/:id/cancel → cancel
PATCH /v2/subscriptions/:id → change (upgrade/downgrade)
Why "pause" matters more than it seems
When a customer wants out, they often do not want out forever — they want to stop for two months. If the only option you offer is cancellation, you turn a pause into permanent churn.
Offering pause on the cancellation screen is one of the highest-return interventions in subscription products, and here it is a single API call.
Step 4 — Invoices and their states
Each cycle generates an invoice, which moves through these states:
DRAFT → PENDING → PAID
↘ OVERDUE → (recovery)
↘ CANCELED / EXPIRED / REFUNDED
| State | Meaning |
|---|---|
DRAFT |
Created, not yet issued |
PENDING |
Issued, awaiting payment |
PAID |
Paid |
OVERDUE |
Past due — enters the dunning sequence |
REFUNDED |
Refunded |
CANCELED |
Cancelled before the due date |
EXPIRED |
Expired without payment |
Useful operations: POST /v2/invoices/:id/cancel, POST /v2/invoices/:id/mark-refunded and
POST /v2/invoices/:id/notifications/resend — the last one resends the notification to the customer,
the first step of any recovery.
Step 5 — The events that prevent churn
This is where the integration stops being passive. Subscribe to these webhooks:
| Event | What to do |
|---|---|
subscription.created |
Record it; not confirmed revenue yet |
subscription.activated |
Grant access, start onboarding |
subscription.past_due |
🚨 Act now — dunning sequence |
subscription.paused |
Suspend access, schedule a win-back contact |
subscription.canceled |
End access, trigger an exit survey |
subscription.expired |
Close the cycle, offer renewal |
The most valuable event is subscription.past_due. It tells you the charge failed — and most of
those failures are involuntary (expired card, insufficient limit), meaning a customer who
wants to stay.
Handling past_due with the tone of a technical notice, not a debt collection, recovers far more:
✅ "Hi Maria! The charge for your plan did not go through — usually that means an expired card. You can update it in 30 seconds here: [link]"
Combine it with a dunning sequence and spaced retries.
Common mistakes
- Amount in reais.
299becomes R$ 2.99. Always cents. - Confusing basis points.
10is not 10% — it is 0.1%. - Treating
createdas revenue. Onlyactivatedmeans access granted. - Not offering pause. Turns a temporary break into permanent cancellation.
- Ignoring
past_due. It is the most actionable alert of the set. - Wrong
documentat signup. Immutable — becomes a duplicate customer. - Cancellation by phone only. Besides being annoying, it causes chargebacks: the customer goes to the bank when they cannot cancel with you.
Checklist
-
documentvalidated before creating the customer - Amounts in cents; coupons in basis points
- Separate offers per modality (do not edit a live offer)
- Access granted on
subscription.activated, notcreated -
past_duewired into the dunning sequence - Pause offered before cancellation
- Self-service cancellation available
- Invoice states mirrored in your system
Explore the Recurring Billing platform and the API documentation. Talk to our team to design your subscription model.