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:

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

Checklist


Explore the Recurring Billing platform and the API documentation. Talk to our team to design your subscription model.