Skip to main content

Billing REST API Reference

The Billing Service manages subscriptions, licensing, and usage limits for all tenants. When Billing is not deployed (self-hosted), the Gateway skips all limit checks and all requests pass freely.

See the REST API Overview for authentication, error format, pagination, and rate limiting.

note

For layout brevity, the /api/v1 base path prefix is omitted from the endpoint tables below.


Subscription Status Machine

created-at-tenant-creation


trialing ──── trial end without payment method ──▶ past_due

payment collected


active ◀─────────────────────── payment received (any stage)

payment failed


past_due (0–7 days grace — tenant fully functional)

7 days unpaid


suspended (8–37 days — read-only mode, all data accessible)

30 days more


terminated (async soft-delete over 24–48 hours)

Zero-state guarantee: A trialing subscription is automatically created for every new tenant — there is never an ambiguous "no subscription" state.


Plans

EndpointAuthDescription
GET /billing/plansbilling.viewList all available plans
GET /billing/plans/:idbilling.viewPlan details (limits, pricing, features)

Plan Object

{
"id": "plan_growth_monthly",
"name": "Growth",
"interval": "monthly",
"price": 4900,
"currency": "USD",
"limits": {
"users": 50,
"records": 100000,
"storageBytes": 10737418240,
"eventsPerDay": 500000,
"modules": 10
},
"features": ["custom_domains", "webhooks", "audit_export"]
}

Limit semantics: limit = 0 means unlimited (no enforcement). limit >= 1 is an enforced ceiling.


Subscriptions

EndpointAuthDescription
GET /billing/subscriptions/currentbilling.viewCurrent tenant subscription
POST /billing/subscriptionsbilling.plan.changeCreate subscription (initial plan selection)
PATCH /billing/subscriptions/:idbilling.plan.changeChange plan. Requires optimistic locking.
DELETE /billing/subscriptions/:idbilling.plan.changeCancel subscription
POST /billing/subscriptions/:id/cancel-downgradebilling.plan.changeCancel pending downgrade

GET /api/v1/billing/subscriptions/current

{
"id": "01j9psub0000000000000001",
"tenantId": "01j9pten0000000000000001",
"planId": "plan_growth_monthly",
"status": "active",
"version": 3,
"trialEndsAt": null,
"currentPeriodStart": "2026-04-01T00:00:00Z",
"currentPeriodEnd": "2026-05-01T00:00:00Z",
"cancelAtPeriodEnd": false,
"pendingChange": null
}

PATCH /api/v1/billing/subscriptions/:id — Change Plan

Optimistic locking: supply the current version to prevent concurrent plan changes.

PATCH https://api.septemcore.com/v1/billing/subscriptions/01j9psub0000000000000001
Authorization: Bearer <access_token>
Content-Type: application/json

{
"planId": "plan_enterprise_monthly",
"version": 3
}

Response 200 OK — updated subscription with version: 4.

ScenarioBehaviour
Wrong version409 Conflict (problems/optimistic-lock-conflict)
Plan change already in progress409 Conflict (problems/plan-change-in-progress) — mutex per tenant
Downgrade (smaller limits)Existing data is untouched. New resource creation blocked (402) immediately. After 30 days still over-limit → past_due → escalation
Trial end without payment methodStatus moves to past_due. Tenant is notified 3 days before trial end

POST /api/v1/billing/subscriptions/:id/cancel-downgrade

Cancels a downgrade that is in pending status (before the next billing cycle starts). After the billing cycle boundary — use PATCH to upgrade to a higher plan.

{ "reason": "user_changed_mind" }

Usage and Limits

EndpointAuthDescription
GET /billing/usagebilling.viewCurrent usage counters for the tenant
GET /billing/usage/historybilling.viewUsage history (daily snapshots, paginated)

GET /api/v1/billing/usage

{
"tenantId": "01j9pten0000000000000001",
"period": "2026-04",
"usage": {
"users": 23,
"records": 48200,
"storageBytes": 2147483648,
"eventsPerDay": 12000,
"modules": 4
},
"limits": {
"users": 50,
"records": 100000,
"storageBytes": 10737418240,
"eventsPerDay": 500000,
"modules": 10
}
}

Limit Enforcement Flow (Gateway)

Limit checking is the Gateway's responsibility, not the module's:

1. POST /api/v1/data (create new record)
2. Gateway extracts tenantId from JWT
3. Gateway → Valkey GET plan_info:{tenantId}
├── HIT → plan limits + status resolved (O(1))
└── MISS → gRPC to Billing → resolve → cache (TTL 15 min)
4. Status check:
active / trialing → proceed
past_due → proceed (grace period)
suspended (mutating requests) → 402 Payment Required
EXCEPTION: money debit/transfer/hold/confirm/cancel → allowed
(blocking user funds withdrawal = legal risk)
terminated → 402 on all requests
5. Usage < limit → forward to Data Layer
Usage >= limit → 402 Payment Required (plan-limit-exceeded)
6. Invalidation: billing.plan.changed / billing.subscription.changed
→ Event Bus → Gateway invalidates Valkey cache

Payment Methods

EndpointAuthDescription
GET /billing/payment-methodsbilling.payment.manageList saved payment methods
POST /billing/payment-methodsbilling.payment.manageAttach a payment method (SetupIntent)
DELETE /billing/payment-methods/:idbilling.payment.manageDetach payment method
POST /billing/payment-methods/:id/set-defaultbilling.payment.manageSet as default payment method

Invoices

EndpointAuthDescription
GET /billing/invoicesbilling.viewList invoices (cursor-paginated)
GET /billing/invoices/:idbilling.viewInvoice details
GET /billing/invoices/:id/pdfbilling.exportDownload invoice PDF

Billing Info

EndpointAuthDescription
GET /billing/infobilling.viewCurrent billing contact (email, address, VAT)
PATCH /billing/infobilling.info.updateUpdate billing email, company name, address

Webhook Receiver

The Billing Service receives webhook events from Stripe and PayPal to synchronise subscription and payment state.

EndpointAuthDescription
POST /billing/webhooks/{provider}NoneReceive webhook event from provider

Supported {provider} values: stripe, paypal.

Webhook Security

ParameterValue
Stripe signatureStripe-Signature header validated via stripe.ConstructEvent(). Invalid → 401 Unauthorized
IdempotencyBy provider event_id — duplicate webhook ignored
Response timeoutPlatform must respond 200 OK within 10 seconds
Stripe retriesUp to 3 days on non-2xx responses

Handled Events

Provider eventAction
invoice.paidUpdate subscription → active. Publish billing.subscription.changed. Gateway cache invalidated
invoice.payment_failedMove to past_due. Send notification to tenant
subscription.canceledUpdate subscription → canceled. Notify tenant
customer.subscription.updatedSync plan changes from Stripe (e.g. coupon applied)

Escalation Timeline (non-payment)

StageDurationBehaviour
Grace Period0–7 days after missed paymentStatus: past_due. Fully functional. Repeated email + in-app notifications
SuspendedDay 8–37Status: suspended. Read-only — all data accessible, mutations blocked (402)
TerminatedDay 38+Status: terminated. Async background soft-delete of all tenant data (24–48 hours). Progress: GET /admin/tenants/:id/termination-status
Payment received at any stageInstantpast_due / suspendedactive. Data untouched

Principle: A tenant never loses data without warning. At minimum 37 days and 5+ notifications before any soft-delete.

Termination Status Endpoint

GET https://api.septemcore.com/v1/admin/tenants/01j9pten0000000000000001/termination-status
Authorization: Bearer <access_token>
{
"tenantId": "01j9pten0000000000000001",
"status": "terminated",
"phase": "deleting_files",
"startedAt": "2026-04-20T00:00:00Z"
}

phase values: deleting_recordsdeleting_filescompleted. Recovery is possible through a Platform Owner until phase: completed.


Permissions Reference

PermissionDescription
billing.viewRead-only: invoices, usage, current plan
billing.plan.changeUpgrade / downgrade subscription (financial impact)
billing.payment.manageAdd / remove payment methods
billing.info.updateUpdate billing email, address
billing.exportExport invoice PDFs and billing data

Error Reference

Error typeStatusTrigger
problems/plan-limit-exceeded402Resource usage at plan ceiling
problems/subscription-suspended402Mutating request while suspended
problems/subscription-terminated402Any request while terminated
problems/optimistic-lock-conflict409Wrong version on PATCH subscription
problems/plan-change-in-progress409Second plan change while first is pending_change
problems/webhook-signature-invalid401Missing or invalid Stripe-Signature header