https://api.southpay.io/api/v2.
Authentication and scope
Reads requirebilling:read, writes require billing:write. Publishable keys are rejected. Idempotency keys are not used anywhere in billing. Retrying a create makes a second record.
Products
Create a product
POST /billing/products. Scope billing:write. Fields go inside a required product object.
string
required
string
object
boolean
Accepted but ignored on create. New products are always active. Use
PATCH to deactivate one.List products
GET /billing/products. Scope billing:read.
Returns a bare array with no envelope and no pagination.
string
Send
true to return only active products. Any other value is ignored and everything is returned.Retrieve and update
GET /billing/products/{id}. Scope billing:read. Includes a prices array, newest first.
PATCH /billing/products/{id}. Scope billing:write. Accepts name, description, active and metadata. This is the only way to set active. Deactivating a product does not archive its prices or cancel subscriptions using them. Do those separately.
Unknown id returns 404 {"error": "Product not found"}.
Prices
Create a price
POST /billing/products/{product_id}/prices. Scope billing:write. Fields go inside a required price object.
integer
required
Minor units of
currency.string
required
Fiat currency. Every price on one subscription must share it.
string
required
daily, weekly, monthly or yearly.integer
How many intervals per cycle.
interval: "monthly" with interval_count: 3 bills quarterly.object
List and retrieve prices
GET /billing/products/{product_id}/prices. Scope billing:read. A bare array, newest first, unpaginated. Includes archived prices.
GET /billing/products/{product_id}/prices/{id}. Scope billing:read. Unknown id returns 404 {"error": "Price not found"}.
Archive and activate
POST /billing/products/{product_id}/prices/{id}/archive sets active: false. The price can no longer be used on new subscriptions.
POST /billing/products/{product_id}/prices/{id}/activate sets it back to true.
Both take no body and return 200 with the price.
Subscriptions
Create a subscription
POST /billing/subscriptions. Scope billing:write. Fields go inside a required subscription object.
string
The anchor price. Use this for a single-price subscription.
object[]
Multiple prices on one subscription. Use instead of
price_id.integer
default:"1"
Quantity for the anchor price.
integer
default:"0"
Days before the first charge. Non-zero starts the subscription in
trialing.integer
default:"1"
How many failed cycles are tolerated in
past_due before cancellation.object
interval, interval_count and currency. Mixing them is rejected.
The first entry in
items is the anchor price and always has id: null. It is synthesized from price_id rather than being a stored item row. Only add-on items have real ids. Do not use items[].id as a stable key without checking for null.Response fields
string
required
Start of the cycle being billed.
string
required
End of that cycle. Also the effective date for a period-end cancellation.
string | null
When the next invoice is generated. During a trial this is
trial_end.string | null
Null when
trial_period_days was zero.boolean
required
True when a cancellation is scheduled. The subscription is still
active and still billing until cancel_at.string | null
When the scheduled cancellation takes effect.
integer
required
Consecutive failed cycles. Once it exceeds
grace_cycles the subscription is canceled.integer
required
Tolerance before cancellation. Default 1.
integer
Total per cycle across all items. Present on the list view only.
List subscriptions
GET /billing/subscriptions. Scope billing:read.
string
trialing, active, past_due, paused or canceled.string
customer, amount_cents, currency, interval and interval_count on top of the base fields.
Retrieve and update
GET /billing/subscriptions/{id}. Scope billing:read. Adds price, items and customer.
PATCH /billing/subscriptions/{id}. Scope billing:write. Accepts price_id, quantity, grace_cycles, items and metadata. Sending items replaces the full set. A canceled subscription cannot be changed: 422 {"error": "Cannot change a canceled subscription"}.
Unknown id returns 404 {"error": "Subscription not found"}.
Cancel
POST /billing/subscriptions/{id}/cancel. Scope billing:write.
boolean
default:"false"
Sent at the top level, not inside a
subscription object. Omitting it cancels immediately.status becomes canceled and canceled_at is stamped. At period end: the subscription stays active, cancel_at_period_end becomes true and cancel_at is set to current_period_end.
Both return 200 with the full subscription. Canceling an already-canceled subscription is a no-op 200.
Uncancel
POST /billing/subscriptions/{id}/uncancel. Scope billing:write. No body.
Clears a scheduled period-end cancellation. It cannot revive an already-canceled subscription. That is terminal, and the call returns 200 with the subscription unchanged. Check status on the response, not the status code.
Pause and resume
POST /billing/subscriptions/{id}/pause stops billing and sets paused_at.
POST /billing/subscriptions/{id}/resume sets status back to active, clears paused_at, resets failed_cycle_count to zero, and restarts the billing period from now. current_period_start, current_period_end and next_billing_at are all recalculated. The customer is not charged for the paused span.
Both take no body and return 200 with the subscription.
Subscription lifecycle
Allowed transitions:
trialing to active or canceled; active to past_due, paused or canceled; past_due to active or canceled; paused to active or canceled.
An illegal transition returns 422 {"error": "Cannot move subscription from canceled to active"}.
Four webhook events fire: subscription.created, subscription.updated, subscription.past_due and subscription.canceled. Pausing, resuming, uncanceling and scheduling a period-end cancellation all emit subscription.updated, so inspect the payload to tell them apart. Billing events carry the full subscription object in data.object and do not fan out to platform accounts.

