Skip to content

Checkout

POST
/api/billing/checkout

Buy a cart of à la carte products for this organization.

One Subscription row and one gateway subscription per product, each line independent (§12) — so this answers 201 as soon as ANY line was bought, even alongside a line that was refused. Partial success is not an error to report as one: the successful lines have already created real resources at the gateway, and telling the caller “409” because a second product was already subscribed would misdescribe what happened and invite a retry that can only fail the same way. When nothing at all was bought, the first line’s reason picks the status (_failed_cart_status). Either way the body is the full per-line result list.

Retry safety is entirely the service’s (§10 / checkout.py): a resubmit of a cart whose products are already live comes back as per-line already_subscribed rather than a second charge, backstopped by uq_subscriptions_one_live_per_product and a row lock on the buyer. Nothing here may add a second guard on top — a duplicate check at this layer would have to duplicate that lock to be correct, and a non-serialized version of it would only widen the window it is trying to close. checkout_cart is therefore called exactly once per request, with no background task or retry wrapper around it.

Coupons are equally the service’s: one applied redemption per (coupon, user) is a partial unique index, not an in-request check, so submitting the same code again on a separate request — for a different product, even a different organization — is refused coupon_already_redeemed on its line. See coupons.redeem_coupon.

Authorizations

Parameters

Query Parameters

organization_id
required
string format: uuid

Request Body required

POST /api/billing/checkout — an à la carte cart.

product_codes are PLAN codes (base_ota, addon_monitoring, …), not organization_entitlements.product values — §12 turned §2’s three fixed bundles into one plan per product, so a cart is a list of plan codes and the request-level name from §10 (plan_code) became plural. Unknown codes are deliberately NOT rejected here: checkout_cart reports each one as its own unknown_plan line, which keeps a cart with one bad code from discarding the good ones. min_length=1 is the one shape rule enforced up front, so that a NoPurchasableProductsError from the service unambiguously means “non-empty cart, nothing sellable in it” (404) rather than “empty cart” (422, raised right here by pydantic).

payment_method_id is the gateway’s own tokenized-card handle from POST /billing/stripe/setup-intent (Stripe only; omitted for razorpay/paypal per §10). It is forwarded verbatim to the gateway, so it is constrained to an opaque-token character set — this is a request field that becomes an argument to an external API call, and no legitimate token contains anything else.

object
billing_cycle
string
default: monthly
Allowed values: monthly annual
coupon_code
Any of:
string
>= 1 characters <= 64 characters
payment_method_id
Any of:
string
>= 1 characters <= 255 characters /^[A-Za-z0-9_-]+$/
product_codes
required
Array<string>
>= 1 items <= 16 items

Responses

201

Successful Response

POST /api/billing/checkout — one result per requested product code.

Returned at EVERY status, not only 201: a cart is per-product independent (§12), so “monitoring bought, crash-reporting already subscribed” is a real outcome that a bare {"detail": …} error body could not express. The status code summarizes (201 when anything was bought, else the failing line’s own code) and this body is where the client reads what actually happened.

object
results
required
Array<object>

One cart line’s outcome — mirrors checkout.CheckoutLineResult.

ok decides which half of the fields is populated: a refused line carries error/detail, a bought one carries the subscription and its client payload. error is a wire-level string the console maps to copy — CheckoutErrorCode values (already_subscribed, …) or, for a refused coupon, a CouponRejectionReason value (coupon_already_redeemed, …).

object
client_payload
Any of:
object
coupon_applied
boolean
currency
Any of:
string
detail
Any of:
string
error
Any of:
string
final_unit_price_cents
Any of:
integer
ok
required
boolean
plan_code
required
string
product
Any of:
string
provider
Any of:
string
quantity
Any of:
integer
status
Any of:
string
subscription_id
Any of:
string format: uuid

422

Validation Error

object
detail
Array<object>
object
loc
required
Array
msg
required
string
type
required
string