Guides
Account standing
How a subscription platform uses CentraPoint to require a card on paid plans, show grace-period banners and lock an account to billing until it is paid.
On this page
Overview#
CentraPoint works out each customer's account standing from their subscriptions, saved payment methods and invoices: ok, grace (something is due; show a banner) or locked (the grace period ended; limit the account to billing). Your platform reads it from the API or follows the customer.standing.changed webhook, shows the banner and enforces the lock. CentraPoint never blocks your users itself.
It works together with saved cards: customers add a card on a hosted page, and the standing response gives you the link to send them to (links.addCardUrl) and the link to pay what is overdue (links.payUrl).
The rules#
| Situation | Grace | Then |
|---|---|---|
| A new customer on a paid plan without a payment method | 3 days from the start of the paid plan | Locked until a payment method is added |
| An invoice past its due date with a balance | 3 days from the due date | Locked until the invoice is paid |
| Customers who existed when the rules were switched on | 14 days from that date before any rule applies | The rules above |
| Free plans | - | No payment method needed (invoices still count) |
Locked means billing only: the customer can sign in, see and pay their invoices and add a card; everything else on your platform is blocked and their data is kept. Paying or adding a card unlocks at once: standing is calculated on every request. An invoice whose card payment is with the bank does not count while that payment is pending; if the bank rejects it, the account is locked again. With automatic collection on, an invoice CentraPoint would charge to the customer's default card but cannot yet (saved cards cannot be charged until a card debit gateway is set up) does not count either: the customer is never locked because a charge could not be made.
Settings#
The rules are off until you switch them on under Settings → Cards & standing; while off the standing is always ok (rulesEnabled: false). The day you first switch them on is the go-live date the existing customers' 14 days count from.
| Setting | Default |
|---|---|
| Apply the grace and lock rules | Off |
| Paid plans need a payment method | On |
| Grace period (days) | 3 |
| Existing customers (days) | 14 |
| Offer saved cards on the pay page | On |
| Charge the default card when an invoice falls due | Off |
What counts as a paid plan and a payment method#
- Paid plan: a customer subscription that is trialing, active, past due or paused with a price above zero (including subscriptions your platform bills itself and mirrors into CentraPoint). If you keep plans only in your own system, send
paidPlan=trueorpaidPlan=falsewith the request. - Payment method: a usable saved card (not expired, failed or removed), an active debit order mandate, or a card subscription the gateway bills.
- Outstanding: sent or overdue invoices with a balance (pro forma invoices are not counted).
Get a customer's standing#
/api/v1/customers/{id}/standing| Field | Type | Description |
|---|---|---|
returnUrloptional | string | Where the add-card page returns the customer (https, on an allowed origin). links.addCardUrl reuses an open add-card session for the same return URL, so polling does not create new ones. |
paidPlanoptional | boolean | true / false: overrides the paid-plan check (plans kept outside CentraPoint). |
curl -X GET "https://app.centrapoint.co.za/api/v1/customers/cmg2c0s7t0003cust0001abcd/standing?returnUrl=https%3A%2F%2Fplatform.example.co.za%2Fbilling" \
-H "Authorization: Bearer $CENTRAPOINT_API_KEY"{
"object": "customer_standing",
"customerId": "cmg2c0s7t0003cust0001abcd",
"status": "grace",
"reason": "unpaid_invoice",
"reasons": [
"unpaid_invoice"
],
"graceEndsAt": "2026-10-08T07:00:00.000Z",
"lockedSince": null,
"lockMode": "billing_only",
"rulesEnabled": true,
"rulesApplyFrom": null,
"paidPlan": true,
"hasPaymentMethod": true,
"paymentMethods": {
"cards": 1,
"defaultCard": {
"object": "card",
"id": "cmgcard00001visa4242abcd",
"customerId": "cmg2c0s7t0003cust0001abcd",
"brand": "visa",
"last4": "4242",
"expiryMonth": 12,
"expiryYear": 2028,
"holderName": "T NKOSI",
"isDefault": true,
"status": "active",
"expiring": false,
"source": "card_setup",
"createdAt": "2026-09-01T08:00:00.000Z",
"lastUsedAt": "2026-09-01T08:00:00.000Z"
},
"debitOrder": false,
"gatewaySubscription": false
},
"outstanding": {
"amount": 299,
"overdueAmount": 299,
"currency": "ZAR",
"invoiceIds": [
"cmg4i5n6v0001inv0001abcd"
],
"invoices": [
{
"id": "cmg4i5n6v0001inv0001abcd",
"number": "INV-000123",
"dueDate": "2026-10-05T07:00:00.000Z",
"balance": 299,
"currency": "ZAR",
"overdue": true,
"paymentPending": false,
"awaitingCollection": false
}
]
},
"links": {
"addCardUrl": "https://app.centrapoint.co.za/card/Q2x9…",
"payUrl": "https://app.centrapoint.co.za/pay/k7Rt…"
},
"checkedAt": "2026-10-06T09:00:00.000Z"
}The standing object#
| Field | Type | Description |
|---|---|---|
statusrequired | string | ok, grace or locked. |
reasonrequired | string | null | no_payment_method or unpaid_invoice: what locked the account, or what will lock it first. |
reasonsrequired | string[] | Every reason that applies now. |
graceEndsAtrequired | string (date-time) | null | When the (first) grace period ends or ended. |
lockedSincerequired | string (date-time) | null | Set while locked. |
lockModerequired | string | billing_only. |
rulesEnabledrequired | boolean | The rules are switched on. |
rulesApplyFromrequired | string (date-time) | null | Existing customers: the end of their 14 days. |
paidPlanrequired | boolean | The customer is on a paid plan. |
hasPaymentMethodrequired | boolean | A usable saved card, an active debit order or a gateway card subscription. |
paymentMethodsrequired | object | cards (usable saved cards), defaultCard (a card or null), debitOrder, gatewaySubscription. |
outstandingrequired | object | amount, overdueAmount, currency, invoiceIds and invoices (id, number, dueDate, balance, overdue, paymentPending, awaitingCollection: waiting for an automatic card charge that cannot be made yet, not counted). |
links.addCardUrlrequired | string | null | Hosted add-card page for the customer (null when cards cannot be saved). |
links.payUrlrequired | string | null | Pay page of the oldest outstanding invoice (null when nothing is outstanding). |
checkedAtrequired | string (date-time) | When this was calculated. |
Integrating your platform#
Sign-up: save a card#
- Create or update the customer (upsert by externalReference) and their subscription.
- For a paid plan, call
POST /api/v1/customers/{id}/cards/setupwith areturnUrlback into your sign-up flow and redirect the customer tourl. They pay R1.00, which is credited to their next invoice. - On return (
?cardSetup=…&status=…), confirm with the session or thecustomer.payment_method.addedwebhook. If they skipped it, the grace period gives them 3 days.
Banners during the grace period#
Read the standing when the customer signs in (and cache it briefly, or keep it from the webhook). While status is grace, show a banner:
| reason | Banner | Button |
|---|---|---|
no_payment_method | Add a card by graceEndsAt to keep using your account. | links.addCardUrl |
unpaid_invoice | Invoice number is overdue: pay by graceEndsAt to keep full access. | links.payUrl |
Paid plans without a payment method can show an "add a card" banner even when the rules are off (paidPlan && !hasPaymentMethod).
Locking to billing#
When status is locked, let the customer sign in and use your billing screens only: see invoices, pay (links.payUrl) and add a card (links.addCardUrl). Block everything else (your API for them included) with a clear message, and keep their data.
Unlocking#
Paying the overdue invoice (on the pay page, by EFT recorded in CentraPoint, or by a card payment the bank confirms) or adding a card changes the standing at once; customer.standing.changed with change: "unlocked" follows. Remove the lock when the standing is ok or grace.
Platforms with their own invoices#
The standing endpoint looks at invoices in CentraPoint. If your platform keeps its own plans and invoices (and takes payments through payment links and saved card charges), work out the standing in your platform with the same rules, and use CentraPoint only for the payment method:
- Payment method: the customer has a card with
status: "active"inGET /api/v1/customers/{id}/cards(keep it up to date fromcustomer.payment_method.*webhooks). - No payment method on a paid plan: grace until the plan started (sign-up or upgrade) + 3 days, then locked to billing.
- Unpaid invoice: grace until its due date + 3 days, then locked to billing. An invoice with a saved card charge still
pendingdoes not count until that charge fails. If you cannot charge saved cards yet (capabilities.chargeSavedCardsisfalse) and the customer has a card, do not lock them for an invoice you would have charged. - Existing customers (signed up before you switch the rules on): nothing locks before your go-live date + 14 days.
- Locked to billing: as above: sign in, see and pay invoices, add a card; everything else blocked, data kept. Unlock as soon as the invoice is paid or a card is added.
You can still call the standing endpoint with paidPlan=true for its hasPaymentMethod and links.addCardUrl, but its status does not include your own invoices.
Webhooks#
customer.standing.changed is sent when a customer's standing changes. data is the standing object (without links values) plus change, previous and customer:
| change | Meaning |
|---|---|
grace_started | Was ok, now in grace. |
locked | The grace period ended. |
unlocked | Was locked, now grace or ok. |
resolved | Was in grace, now ok. |
reason_changed | Same status, another reason. |
{
"id": "evt_7f3b19c9e1a5d2b8f4c6e0a3",
"type": "customer.standing.changed",
"created": "2026-10-08T07:05:00.000Z",
"data": {
"object": "customer_standing",
"customerId": "cmg2c0s7t0003cust0001abcd",
"status": "locked",
"reason": "unpaid_invoice",
"reasons": [
"unpaid_invoice"
],
"graceEndsAt": "2026-10-08T07:00:00.000Z",
"lockedSince": "2026-10-08T07:00:00.000Z",
"lockMode": "billing_only",
"rulesEnabled": true,
"rulesApplyFrom": null,
"paidPlan": true,
"hasPaymentMethod": true,
"paymentMethods": {
"cards": 1,
"defaultCard": {
"object": "card",
"id": "cmgcard00001visa4242abcd",
"customerId": "cmg2c0s7t0003cust0001abcd",
"brand": "visa",
"last4": "4242",
"expiryMonth": 12,
"expiryYear": 2028,
"holderName": "T NKOSI",
"isDefault": true,
"status": "active",
"expiring": false,
"source": "card_setup",
"createdAt": "2026-09-01T08:00:00.000Z",
"lastUsedAt": "2026-09-01T08:00:00.000Z"
},
"debitOrder": false,
"gatewaySubscription": false
},
"outstanding": {
"amount": 299,
"overdueAmount": 299,
"currency": "ZAR",
"invoiceIds": [
"cmg4i5n6v0001inv0001abcd"
],
"invoices": [
{
"id": "cmg4i5n6v0001inv0001abcd",
"number": "INV-000123",
"dueDate": "2026-10-05T07:00:00.000Z",
"balance": 299,
"currency": "ZAR",
"overdue": true,
"paymentPending": false,
"awaitingCollection": false
}
]
},
"links": {
"addCardUrl": "https://app.centrapoint.co.za/card/Q2x9…",
"payUrl": "https://app.centrapoint.co.za/pay/k7Rt…"
},
"checkedAt": "2026-10-06T09:00:00.000Z",
"change": "locked",
"previous": {
"status": "grace",
"reason": "unpaid_invoice"
},
"customer": {
"id": "cmg2c0s7t0003cust0001abcd",
"externalReference": "platform-org:42"
}
}
}Changes caused by time passing (a grace period ending) are picked up by a scheduled check, so the webhook can come some time after graceEndsAt. The standing endpoint is always current. See Webhooks for signatures, retries and ordering, and saved card events.
Checklist#
- Allow your platform's origin under Settings → API keys → Allowed return URLs.
- Check
GET /api/v1/me:capabilities.savedCardsandcapabilities.accountStanding. - Paid plan sign-ups go to the add-card page.
- Show the grace banner with the right link; enforce the billing-only lock on the server.
- Subscribe to
customer.standing.changedandcustomer.payment_method.*. - Switch the rules on under Settings → Cards & standing when your platform is ready.