Skip to main content
Admin API

This endpoint requires billing or admin roles. The Go API Gateway enforces RequireAnyRole("billing_admin", "finance", "tenant_admin", "platform_admin", "system_admin", "super_admin") on all /v1/billing/* routes.

Billing & Subscriptions API

Manage subscriptions, add-ons, discounts, invoices, refunds, dunning, and usage metering.

Overview

The Billing API is served at /v1/billing/* through the Go API Gateway. It provides subscription lifecycle management, add-on and discount CRUD, invoice recording, refund tracking, dunning management, and usage-based billing.

FeatureDescription
SubscriptionsCreate, get, update, pause, and resume subscriptions
Add-onsCRUD for subscription add-ons, attach/detach from subscriptions
DiscountsCRUD for discount codes, validate and apply to subscriptions
InvoicesCreate and record invoices from Stripe
RefundsList and record refunds
DunningTrack payment failures and recovery
UsageRecord and query usage-based billing metrics
EventsRecord and list subscription lifecycle events

Subscriptions

Create Subscription

POST /v1/billing/subscriptions
Authorization: Bearer {access_token}
Content-Type: application/json

Request:

{
"plan_id": "uuid",
"stripe_subscription_id": "sub_...",
"current_period_end": "2026-02-28T00:00:00Z",
"cancel_at_period_end": false,
"status": "active"
}
FieldTypeRequiredDescription
plan_idstring (UUID)YesPlan identifier
stripe_subscription_idstringYesStripe subscription ID
current_period_endstring (RFC3339)YesEnd of current billing period
cancel_at_period_endbooleanNoWhether to cancel at period end
statusstringYesOne of: active, trialing, past_due, canceled, unpaid, incomplete, incomplete_expired, paused

Get Subscription

GET /v1/billing/subscriptions/{subscription_id}
Authorization: Bearer {access_token}
ParameterTypeDescription
subscription_idpath (string)Subscription identifier

Update Subscription

PATCH /v1/billing/subscriptions/{subscription_id}
Authorization: Bearer {access_token}
Content-Type: application/json

Request (all fields optional):

{
"current_period_end": "2026-03-31T00:00:00Z",
"status": "active",
"plan_id": "uuid",
"cancel_at_period_end": true
}

Pause Subscription

POST /v1/billing/subscriptions/{subscription_id}/pause
Authorization: Bearer {access_token}

Proxied to the Rust Platform service.

Resume Subscription

POST /v1/billing/subscriptions/{subscription_id}/resume
Authorization: Bearer {access_token}

Proxied to the Rust Platform service.


Add-ons

Create Add-on

POST /v1/billing/addons
Authorization: Bearer {access_token}
Content-Type: application/json

Request:

{
"name": "Extra Location",
"description": "Additional restaurant location",
"addon_type": "per_location",
"price_cents": 9900,
"currency": "USD",
"stripe_price_id": "price_...",
"metadata_json": "{}"
}
FieldTypeRequiredDescription
namestringYesAdd-on name (1-255 chars)
descriptionstringNoDescription (max 1000 chars)
addon_typestringYesOne of: recurring, one_time, per_location
price_centsintegerYesPrice in cents (min 0)
currencystringYesISO 4217 currency code (3 chars)
stripe_price_idstringNoStripe price ID
metadata_jsonstringNoJSON metadata

List Add-ons

GET /v1/billing/addons?active_only=true
Authorization: Bearer {access_token}
ParameterTypeDescription
active_onlyquery (boolean)Filter to only active add-ons

Get Add-on

GET /v1/billing/addons/{addon_id}
Authorization: Bearer {access_token}

Update Add-on

PATCH /v1/billing/addons/{addon_id}
Authorization: Bearer {access_token}
Content-Type: application/json

Request (all fields optional):

{
"name": "Updated Name",
"description": "Updated description",
"price_cents": 14900,
"stripe_price_id": "price_...",
"is_active": true,
"metadata_json": "{}"
}

Delete Add-on

Soft-deletes the add-on by marking it inactive.

DELETE /v1/billing/addons/{addon_id}
Authorization: Bearer {access_token}

Returns 204 No Content on success.


Subscription Add-ons

Add Add-on to Subscription

POST /v1/billing/subscriptions/{subscription_id}/addons
Authorization: Bearer {access_token}
Content-Type: application/json

Request:

{
"addon_id": "uuid",
"quantity": 1
}
FieldTypeRequiredDescription
addon_idstring (UUID)YesAdd-on identifier
quantityintegerYesQuantity (min 1)

List Subscription Add-ons

GET /v1/billing/subscriptions/{subscription_id}/addons
Authorization: Bearer {access_token}

Remove Add-on from Subscription

DELETE /v1/billing/subscriptions/{subscription_id}/addons/{addon_id}
Authorization: Bearer {access_token}

Returns 204 No Content on success.


Discounts

Create Discount

POST /v1/billing/discounts
Authorization: Bearer {access_token}
Content-Type: application/json

Request:

{
"code": "SUMMER2026",
"name": "Summer Promotion",
"description": "20% off for summer",
"discount_type": "percentage",
"discount_value": 20,
"duration": "repeating",
"duration_months": 3,
"max_redemptions": 100,
"valid_from": "2026-06-01T00:00:00Z",
"valid_to": "2026-09-01T00:00:00Z",
"applies_to_plan_ids": ["uuid1", "uuid2"],
"stripe_coupon_id": "coupon_..."
}
FieldTypeRequiredDescription
codestringYesDiscount code (1-100 chars)
namestringYesDisplay name (1-255 chars)
descriptionstringNoDescription (max 1000 chars)
discount_typestringYesOne of: percentage, fixed_amount
discount_valueintegerYesDiscount value (min 1)
durationstringYesOne of: once, repeating, forever
duration_monthsintegerNoMonths for repeating duration (min 1)
max_redemptionsintegerNoMaximum redemptions (min 1)
valid_fromstring (RFC3339)YesStart date
valid_tostring (RFC3339)NoEnd date
applies_to_plan_idsarray of UUIDsNoRestrict to specific plans
stripe_coupon_idstringNoStripe coupon ID

List Discounts

GET /v1/billing/discounts?active_only=true
Authorization: Bearer {access_token}
ParameterTypeDescription
active_onlyquery (boolean)Filter to only active discounts

Get Discount

GET /v1/billing/discounts/{discount_id}
Authorization: Bearer {access_token}

Get Discount by Code

GET /v1/billing/discounts/by-code?code=SUMMER2026
Authorization: Bearer {access_token}
ParameterTypeRequiredDescription
codequery (string)YesDiscount code to look up

Validate Discount

Validates a discount code and returns the calculated discount amount.

POST /v1/billing/discounts/validate
Authorization: Bearer {access_token}
Content-Type: application/json

Request:

{
"code": "SUMMER2026",
"plan_id": "uuid",
"cart_total_cents": 29900
}
FieldTypeRequiredDescription
codestringYesDiscount code (1-100 chars)
plan_idstring (UUID)NoPlan to validate against
cart_total_centsintegerYesCart total in cents (min 0)

Update Discount

PATCH /v1/billing/discounts/{discount_id}
Authorization: Bearer {access_token}
Content-Type: application/json

Request (all fields optional):

{
"name": "Updated Name",
"description": "Updated description",
"max_redemptions": 200,
"valid_to": "2026-12-31T00:00:00Z",
"is_active": false
}

Delete Discount

Soft-deletes the discount by marking it inactive.

DELETE /v1/billing/discounts/{discount_id}
Authorization: Bearer {access_token}

Returns 204 No Content on success.


Subscription Discounts

Apply Discount to Subscription

POST /v1/billing/subscriptions/{subscription_id}/discount
Authorization: Bearer {access_token}
Content-Type: application/json

Request:

{
"discount_code": "SUMMER2026"
}

Remove Discount from Subscription

DELETE /v1/billing/subscriptions/{subscription_id}/discount
Authorization: Bearer {access_token}

Returns 204 No Content on success.


Invoices

Create Invoice

POST /v1/billing/invoices
Authorization: Bearer {access_token}
Content-Type: application/json

Request:

{
"stripe_invoice_id": "in_...",
"subscription_id": "uuid",
"amount_due": 29900,
"amount_paid": 0,
"currency": "USD",
"status": "open",
"invoice_pdf": "https://...",
"hosted_invoice_url": "https://..."
}
FieldTypeRequiredDescription
stripe_invoice_idstringYesStripe invoice ID
subscription_idstring (UUID)NoAssociated subscription
amount_dueintegerYesAmount due in cents (min 0)
amount_paidintegerNoAmount paid in cents (min 0)
currencystringYesISO 4217 currency code (3 chars)
statusstringYesOne of: draft, open, paid, void, uncollectible
invoice_pdfstring (URL)NoPDF download URL
hosted_invoice_urlstring (URL)NoHosted invoice page URL

Record Invoice (Webhook)

Records or updates an invoice from Stripe webhook events. Uses the same request body as Create Invoice.

POST /v1/billing/invoices/record
Authorization: Bearer {access_token}
Content-Type: application/json

Refunds

List Refunds

GET /v1/billing/refunds?limit=100
Authorization: Bearer {access_token}
ParameterTypeDescription
limitquery (integer)Maximum number of refunds to return (default: 100)

Record Refund

POST /v1/billing/refunds
Authorization: Bearer {access_token}
Content-Type: application/json

Request:

{
"stripe_refund_id": "re_...",
"stripe_charge_id": "ch_...",
"invoice_id": "uuid",
"amount": 2990,
"currency": "USD",
"status": "succeeded",
"reason": "requested_by_customer",
"description": "Customer requested refund"
}
FieldTypeRequiredDescription
stripe_refund_idstringYesStripe refund ID
stripe_charge_idstringYesStripe charge ID
invoice_idstring (UUID)NoAssociated invoice
amountintegerYesRefund amount in cents (min 1)
currencystringYesISO 4217 currency code (3 chars)
statusstringYesOne of: pending, succeeded, failed, canceled
reasonstringNoOne of: duplicate, fraudulent, requested_by_customer
descriptionstringNoDescription (max 1000 chars)

Dunning

Dunning management tracks payment failures and recovery for subscriptions.

Get Dunning State

GET /v1/billing/subscriptions/{subscription_id}/dunning
Authorization: Bearer {access_token}
ParameterTypeDescription
subscription_idpath (UUID)Subscription identifier

Record Payment Failure

POST /v1/billing/subscriptions/{subscription_id}/dunning/failure
Authorization: Bearer {access_token}
Content-Type: application/json

Request:

{
"stripe_subscription_id": "sub_...",
"failure_code": "card_declined",
"failure_message": "Your card was declined"
}
FieldTypeRequiredDescription
stripe_subscription_idstringYesStripe subscription ID
failure_codestringNoStripe failure code
failure_messagestringNoHuman-readable failure message

Mark Dunning Recovered

POST /v1/billing/subscriptions/{subscription_id}/dunning/recovered
Authorization: Bearer {access_token}

Usage

Record Usage

POST /v1/billing/usage
Authorization: Bearer {access_token}
Content-Type: application/json

Request:

{
"subscription_id": "uuid",
"metric_name": "ai_tokens",
"quantity": 1500,
"timestamp": "2026-02-18T12:00:00Z",
"idempotency_key": "unique-key-123"
}
FieldTypeRequiredDescription
subscription_idstring (UUID)YesSubscription identifier
metric_namestringYesMetric name (1-100 chars)
quantityintegerYesUsage quantity (min 1)
timestampstring (RFC3339)YesTimestamp of usage
idempotency_keystringYesUnique key for deduplication (1-255 chars)

Get Usage Summary

GET /v1/billing/subscriptions/{subscription_id}/usage?metric_name=ai_tokens&start_time=2026-02-01T00:00:00Z&end_time=2026-02-28T23:59:59Z
Authorization: Bearer {access_token}
ParameterTypeRequiredDescription
subscription_idpath (UUID)YesSubscription identifier
metric_namequery (string)YesMetric name
start_timequery (RFC3339)YesStart of time range
end_timequery (RFC3339)YesEnd of time range

Subscription Events

List Subscription Events

GET /v1/billing/events?subscription_id=uuid&limit=100
Authorization: Bearer {access_token}
ParameterTypeRequiredDescription
subscription_idquery (UUID)NoFilter by subscription
limitquery (integer)NoMaximum events (default: 100)

Record Subscription Event

POST /v1/billing/events
Authorization: Bearer {access_token}
Content-Type: application/json

Request:

{
"subscription_id": "uuid",
"event_type": "upgraded",
"from_plan_id": "uuid",
"to_plan_id": "uuid",
"from_mrr_cents": 9900,
"to_mrr_cents": 29900,
"currency": "USD",
"reason": "Customer requested upgrade",
"event_at": "2026-02-18T15:00:00Z"
}
FieldTypeRequiredDescription
subscription_idstring (UUID)YesSubscription identifier
event_typestringYesOne of: created, activated, upgraded, downgraded, paused, resumed, canceled, churned, reactivated
from_plan_idstringNoPrevious plan ID
to_plan_idstringNoNew plan ID
from_mrr_centsintegerNoPrevious MRR in cents
to_mrr_centsintegerNoNew MRR in cents
currencystringYesISO 4217 currency code (3 chars)
reasonstringNoReason for the event (max 1000 chars)
event_atstring (RFC3339)YesWhen the event occurred

Error Responses

Validation Error (400)

{
"error": "validation_error",
"message": "addon_id must be a valid UUID"
}

Missing Tenant (401)

Returned when the JWT does not contain a tenant_id.

{
"error": "missing_tenant",
"message": "tenant_id not found in context"
}

Upstream Error (502)

Returned when the Rust Platform service is unavailable or returns an error.