API
Customer subscriptions API
Manage subscription packages and plans, subscribe customers to plans, and pause, resume, cancel or change their subscriptions.
On this page
Overview#
These endpoints manage the subscription catalogue (packages and their plans) and the customers subscribed to it. Read the Packages, plans & customer subscriptions guide first for how trials, renewals, dunning and the collection methods work. For card subscriptions created by recurring payment links, see the separate Subscriptions API.
| Endpoint | Purpose |
|---|---|
GET /api/v1/packages | List packages with their plans |
POST /api/v1/packages | Create a package (optionally with plans) |
GET / PATCH / DELETE /api/v1/packages/{id} | Get, update or disable a package |
POST /api/v1/packages/{id}/plans | Add a plan |
GET / PATCH /api/v1/plans/{id} | Get or update a plan |
POST /api/v1/customer-subscriptions | Subscribe a customer |
GET /api/v1/customer-subscriptions | List subscriptions |
GET / PATCH /api/v1/customer-subscriptions/{id} | Get or update one |
POST …/{id}/pause, /resume, /cancel, /change-plan | Lifecycle actions |
Packages#
/api/v1/packagesReturns { "data": [ … ] } in sort order, each package with its plans. ?enabled=true returns only enabled packages with only their enabled plans (use this to build your own pricing page); ?enabled=false only disabled packages. limit is 1–100, default 50.
/api/v1/packages| Field | Type | Description |
|---|---|---|
namerequired | string | 1–120 characters. |
descriptionoptional | string | null | Up to 2000 characters. |
enabledoptional | boolean | Default true. |
sortOrderoptional | integer | -10000 to 10000. |
defaultProviderIdoptional | string | null | Payment provider used for gateway subscriptions when none is given. |
plansoptional | Plan[] | Up to 20 plans to create with the package (fields as in Plans). Create only. |
curl -X POST "https://app.centrapoint.co.za/api/v1/packages" \
-H "Authorization: Bearer $CENTRAPOINT_API_KEY" \
-H "Idempotency-Key: 5c2e8a1f-3b7d-4e9a-8f1c-2d6b0a4e7c93" \
-H "Content-Type: application/json" \
-d '{
"name": "Acme Cloud",
"description": "Hosted project management",
"plans": [
{
"name": "Pro",
"frequency": "monthly",
"amount": 499,
"trialDays": 14,
"features": [
"Unlimited projects",
"Priority support"
]
}
]
}'GET /api/v1/packages/{id} returns the package with all its plans. PATCH updates any of the fields above except plans (unknown fields are rejected). DELETE does not delete anything: it disables the package so it can't be subscribed to, and existing subscriptions keep billing.
Plans#
/api/v1/packages/{id}/plans| Field | Type | Description |
|---|---|---|
namerequired | string | 1–120 characters. |
frequencyrequired | string | daily, weekly, biweekly, monthly, quarterly, biannually or annually. |
amountrequired | number | Price per period, 0–100 000 000. 0 = free plan. |
currencyoptional | string | ISO 4217 code, default ZAR. |
setupFeeoptional | number | Added to the first charge. |
trialDaysoptional | integer | 0–730. |
billingCyclesoptional | integer | null | 1–1000 fixed cycles; null = until cancelled. |
descriptionoptional | string | null | Up to 2000 characters. |
featuresoptional | string[] | Up to 50 bullet points of up to 200 characters. |
providerIdsoptional | string[] | Payment providers allowed to collect the plan; empty = any. |
hasCallToAction, callToActionUrloptional | boolean, string | null | Show a button to your http(s) URL instead of a purchase option (URL required). |
hasContactUsoptional | boolean | Show a contact option. |
enabledoptional | boolean | Default true. |
sortOrderoptional | integer | -10000 to 10000. |
curl -X POST "https://app.centrapoint.co.za/api/v1/packages/cmg4p1a2b0001pkg0001abcd/plans" \
-H "Authorization: Bearer $CENTRAPOINT_API_KEY" \
-H "Idempotency-Key: 5c2e8a1f-3b7d-4e9a-8f1c-2d6b0a4e7c93" \
-H "Content-Type: application/json" \
-d '{
"name": "Team (annual)",
"frequency": "annually",
"amount": 4990,
"features": [
"Up to 25 users"
]
}'GET /api/v1/plans/{id} returns a plan; PATCH /api/v1/plans/{id} updates any of its fields. Price and frequency changes apply to new subscriptions only. The plan object also has id, object: "plan", packageId, createdAt and updatedAt.
The customer subscription object#
{
"id": "cmg4s9d8e0005csb0001abcd",
"object": "customer_subscription",
"status": "trialing",
"customer": {
"id": "cmg2c0s7t0003cust0001abcd",
"externalReference": "CRM-1001",
"email": "[email protected]"
},
"plan": {
"id": "cmg4p1a2b0002pln0001abcd",
"name": "Pro",
"packageId": "cmg4p1a2b0001pkg0001abcd",
"packageName": "Acme Cloud"
},
"collectionMethod": "invoice",
"quantity": 1,
"amount": 499,
"currency": "ZAR",
"frequency": "monthly",
"billingCycles": null,
"cyclesBilled": 0,
"startedAt": "2026-09-28T08:10:00.000Z",
"trialEndsAt": "2026-10-12T08:10:00.000Z",
"currentPeriodStart": null,
"currentPeriodEnd": null,
"nextBillingAt": "2026-10-12T08:10:00.000Z",
"endsAt": null,
"cancelAtPeriodEnd": false,
"cancelledAt": null,
"cancelReason": null,
"pausedAt": null,
"resumedAt": null,
"dunningAttempts": 0,
"gateway": null,
"providerId": null,
"paymentTokenId": null,
"couponId": null,
"externalReference": "SUB-CRM-1001-PRO",
"metadata": {
"crmDealId": "D-778"
},
"source": "api",
"createdAt": "2026-09-28T08:10:00.000Z",
"updatedAt": "2026-09-28T08:10:00.000Z"
}| Field | Type | Description |
|---|---|---|
statusrequired | string | incomplete, trialing, active, past_due, paused, cancelled or expired (lifecycle). |
customer, planrequired | object | Summaries of the customer and plan (with its package). |
collectionMethodrequired | string | gateway, debit_order or invoice. |
quantity, amount, currency, frequencyrequired | amount = plan price × quantity (less a forever coupon), per period. | |
billingCycles, cyclesBilledrequired | integer | null, integer | Fixed term from the plan and cycles billed so far. |
trialEndsAt, currentPeriodStart, currentPeriodEndrequired | string | null | Trial end and the current paid period. |
nextBillingAtrequired | string | null | Next charge (trial end during a trial); null when nothing more will be billed. |
endsAt, cancelAtPeriodEndrequired | string | null, boolean | Scheduled end (cancellation at period end or fixed term). |
cancelledAt, cancelReason, pausedAt, resumedAtrequired | string | null | |
dunningAttemptsrequired | integer | Failed-payment attempts so far; 0 when paid up. |
gateway, providerId, paymentTokenIdrequired | string | null | Collecting provider; paymentTokenId is the gateway subscription (see Subscriptions API). |
couponIdrequired | string | null | Coupon applied at sign-up. |
externalReference, metadatarequired | string | null, object | Your ID and key/value data. |
sourcerequired | string | dashboard, api, checkout or portal. |
eventsoptional | array | Only with ?include=events: latest 100 timeline events { id, type, details, createdAt }, newest first. |
Subscribe a customer#
/api/v1/customer-subscriptions| Field | Type | Description |
|---|---|---|
customerId or customerrequired | string | object | Exactly one: the customer ID, or { "id" } / { "externalReference" }. |
planIdrequired | string | An enabled plan of an enabled package. |
collectionMethodrequired | string | gateway, debit_order or invoice (details). |
providerIdoptional | string | Gateway or Netcash provider; defaults to the package's default gateway or the mandate's provider. |
quantityoptional | integer | 1–10000, default 1. |
couponCodeoptional | string | Needs Coupons in your plan; must be valid for the plan and customer. |
trialoptional | boolean | false skips the plan's trial. |
trialDaysoptional | integer | 0–730; overrides the plan's trial length. |
mandateIdoptional | string | debit_order only: a mandate of this customer with the plan's frequency, not linked to another subscription. |
externalReferenceoptional | string | Up to 190 characters. Repeating the call with the same value returns the existing subscription (200). |
metadataoptional | object | Key/value data (rules). With a coupon, the key coupon is reserved for the coupon terms snapshot (see Coupons). |
sendEmailoptional | boolean | Default true: email the first invoice, payment request or trial confirmation. |
curl -X POST "https://app.centrapoint.co.za/api/v1/customer-subscriptions" \
-H "Authorization: Bearer $CENTRAPOINT_API_KEY" \
-H "Idempotency-Key: 5c2e8a1f-3b7d-4e9a-8f1c-2d6b0a4e7c93" \
-H "Content-Type: application/json" \
-d '{
"customer": {
"externalReference": "CRM-1001"
},
"planId": "cmg4p1a2b0002pln0001abcd",
"collectionMethod": "invoice",
"externalReference": "SUB-CRM-1001-PRO",
"metadata": {
"crmDealId": "D-778"
}
}'Returns 201 with the subscription plus a payment object, or 200 when a subscription with the same externalReference already exists. Supports the Idempotency-Key header.
The payment object#
| Field | Type | Description |
|---|---|---|
duerequired | boolean | Money is due before the subscription becomes active. |
amount, currencyrequired | number, string | What is due now (setup fee + first period, after coupons). |
methodrequired | string | invoice: an invoice with a pay link was created; link: a payment link (gateway method); token: a stored card was charged; none: nothing to collect now (trial, free plan, debit order collects on its debit day); checkout: on a replay of an incomplete gateway subscription without an open payment request. |
payUrlrequired | string | null | Send the customer here to pay. Already emailed unless sendEmail was false. |
invoiceId, paymentLinkId, transactionIdrequired | string | null | The document created for the charge. |
{
"due": true,
"amount": 499,
"currency": "ZAR",
"method": "invoice",
"payUrl": "https://app.centrapoint.co.za/pay/4hQ2…",
"invoiceId": "cmg2i4n5v0004inv0001abcd",
"paymentLinkId": null,
"transactionId": null
}List customer subscriptions#
/api/v1/customer-subscriptions| Field | Type | Description |
|---|---|---|
customerId, planId, packageIdoptional | string | Filters. |
statusoptional | string | One of the statuses above; anything else returns 400. |
externalReferenceoptional | string | Exact match. |
limitoptional | integer | 1–100, default 20. |
Returns { "data": [ … ] }, newest first.
curl -X GET "https://app.centrapoint.co.za/api/v1/customer-subscriptions?customerId=cmg2c0s7t0003cust0001abcd&status=active" \
-H "Authorization: Bearer $CENTRAPOINT_API_KEY"Get a customer subscription#
/api/v1/customer-subscriptions/{id}curl -X GET "https://app.centrapoint.co.za/api/v1/customer-subscriptions/cmg4s9d8e0005csb0001abcd?include=events" \
-H "Authorization: Bearer $CENTRAPOINT_API_KEY"Update a customer subscription#
/api/v1/customer-subscriptions/{id}| Field | Type | Description |
|---|---|---|
metadataoptional | object | null | Replaces the whole map; null clears it. On a subscription with a coupon, the key coupon is reserved: CentraPoint stores the coupon terms there, keeps them when you replace the map and leaves them out of responses. |
externalReferenceoptional | string | null | Must not belong to another subscription. |
nextBillingAtoptional | string (date-time) | ISO date-time with offset. Moves the next charge (and the trial end during a trial). Only for active, trialing, past_due or paused subscriptions; not for debit order or gateway-billed ones; in the future, within 3 years and before endsAt. |
curl -X PATCH "https://app.centrapoint.co.za/api/v1/customer-subscriptions/cmg4s9d8e0005csb0001abcd" \
-H "Authorization: Bearer $CENTRAPOINT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"nextBillingAt": "2026-10-19T08:00:00+02:00"
}'Unknown fields are rejected. Sends subscription.updated when something changed.
Pause and resume#
/api/v1/customer-subscriptions/{id}/pause/api/v1/customer-subscriptions/{id}/resumeNo body (an empty object is accepted). Pause is allowed from active or past_due; resume from paused. Repeating either when it has already taken effect returns the subscription unchanged. Gateway-billed subscriptions are paused and resumed at the gateway first; if the gateway can't (Netcash Pay Now) or refuses, you get 400 and nothing changes. Linked debit order mandates are paused and resumed with the subscription.
curl -X POST "https://app.centrapoint.co.za/api/v1/customer-subscriptions/cmg4s9d8e0005csb0001abcd/pause" \
-H "Authorization: Bearer $CENTRAPOINT_API_KEY"Cancel#
/api/v1/customer-subscriptions/{id}/cancel| Field | Type | Description |
|---|---|---|
atPeriodEndoptional | boolean | true: cancel when the paid period (or trial) ends. Default: cancel now. |
reasonoptional | string | Up to 1000 characters; stored as cancelReason and shown to the customer. |
curl -X POST "https://app.centrapoint.co.za/api/v1/customer-subscriptions/cmg4s9d8e0005csb0001abcd/cancel" \
-H "Authorization: Bearer $CENTRAPOINT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"atPeriodEnd": true,
"reason": "Moving to another provider"
}'Gateway billing is stopped first; if the gateway refuses, you get 400 and the subscription is not cancelled. Cancelling a cancelled or expired subscription returns it unchanged. A cancellation at period end sets cancelAtPeriodEnd: true and endsAt, and sends subscription.updated; the subscription is cancelled (with subscription.cancelled) when endsAt passes.
Change plan#
/api/v1/customer-subscriptions/{id}/change-plan| Field | Type | Description |
|---|---|---|
planIdrequired | string | An enabled plan in the same currency (it may be in another package). |
quantityoptional | integer | 1–10000; default the current quantity. |
curl -X POST "https://app.centrapoint.co.za/api/v1/customer-subscriptions/cmg4s9d8e0005csb0001abcd/change-plan" \
-H "Authorization: Bearer $CENTRAPOINT_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"planId": "cmg4p1a2b0003pln0002abcd"
}'The new amount and frequency apply from the next billing date; the current period is not prorated. The customer gets the upgraded or downgraded email (by comparing the daily price) and subscription.plan_changed is sent with previousPlan and previousAmount. Not allowed for cancelled or expired subscriptions, for gateway-billed subscriptions when the amount or frequency would change, or for a frequency change on a debit order subscription.
Errors#
| Status | error | When |
|---|---|---|
| 400 | invalid_request | Validation failed or the action isn't allowed: unknown customer/plan/mandate/provider, inactive customer, disabled plan, invalid coupon, currency mismatch, wrong status for the action, the gateway refused. Nothing changed. |
| 403 | plan_restricted | No REST API or Recurring billing in your plan. |
| 403 | plan_limit | The collection method needs Invoicing or Debit orders, which your plan lacks. |
| 403 | account_restricted | The account is read-only (writes). |
| 404 | not_found | No package, plan or subscription with this ID in your organisation. |
| 409 | idempotency_conflict | Idempotency-Key reused with a different body (POST). |
| 401 / 429 / 500 | unauthorized / rate_limited / internal_error | See Errors |