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.
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
trialingsubscription is automatically created for every new tenant — there is never an ambiguous "no subscription" state.
Plans
| Endpoint | Auth | Description |
|---|---|---|
GET /billing/plans | billing.view | List all available plans |
GET /billing/plans/:id | billing.view | Plan 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 = 0means unlimited (no enforcement).limit >= 1is an enforced ceiling.
Subscriptions
| Endpoint | Auth | Description |
|---|---|---|
GET /billing/subscriptions/current | billing.view | Current tenant subscription |
POST /billing/subscriptions | billing.plan.change | Create subscription (initial plan selection) |
PATCH /billing/subscriptions/:id | billing.plan.change | Change plan. Requires optimistic locking. |
DELETE /billing/subscriptions/:id | billing.plan.change | Cancel subscription |
POST /billing/subscriptions/:id/cancel-downgrade | billing.plan.change | Cancel 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.
| Scenario | Behaviour |
|---|---|
Wrong version | 409 Conflict (problems/optimistic-lock-conflict) |
| Plan change already in progress | 409 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 method | Status 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
| Endpoint | Auth | Description |
|---|---|---|
GET /billing/usage | billing.view | Current usage counters for the tenant |
GET /billing/usage/history | billing.view | Usage 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
| Endpoint | Auth | Description |
|---|---|---|
GET /billing/payment-methods | billing.payment.manage | List saved payment methods |
POST /billing/payment-methods | billing.payment.manage | Attach a payment method (SetupIntent) |
DELETE /billing/payment-methods/:id | billing.payment.manage | Detach payment method |
POST /billing/payment-methods/:id/set-default | billing.payment.manage | Set as default payment method |
Invoices
| Endpoint | Auth | Description |
|---|---|---|
GET /billing/invoices | billing.view | List invoices (cursor-paginated) |
GET /billing/invoices/:id | billing.view | Invoice details |
GET /billing/invoices/:id/pdf | billing.export | Download invoice PDF |
Billing Info
| Endpoint | Auth | Description |
|---|---|---|
GET /billing/info | billing.view | Current billing contact (email, address, VAT) |
PATCH /billing/info | billing.info.update | Update billing email, company name, address |
Webhook Receiver
The Billing Service receives webhook events from Stripe and PayPal to synchronise subscription and payment state.
| Endpoint | Auth | Description |
|---|---|---|
POST /billing/webhooks/{provider} | None | Receive webhook event from provider |
Supported {provider} values: stripe, paypal.
Webhook Security
| Parameter | Value |
|---|---|
| Stripe signature | Stripe-Signature header validated via stripe.ConstructEvent(). Invalid → 401 Unauthorized |
| Idempotency | By provider event_id — duplicate webhook ignored |
| Response timeout | Platform must respond 200 OK within 10 seconds |
| Stripe retries | Up to 3 days on non-2xx responses |
Handled Events
| Provider event | Action |
|---|---|
invoice.paid | Update subscription → active. Publish billing.subscription.changed. Gateway cache invalidated |
invoice.payment_failed | Move to past_due. Send notification to tenant |
subscription.canceled | Update subscription → canceled. Notify tenant |
customer.subscription.updated | Sync plan changes from Stripe (e.g. coupon applied) |
Escalation Timeline (non-payment)
| Stage | Duration | Behaviour |
|---|---|---|
| Grace Period | 0–7 days after missed payment | Status: past_due. Fully functional. Repeated email + in-app notifications |
| Suspended | Day 8–37 | Status: suspended. Read-only — all data accessible, mutations blocked (402) |
| Terminated | Day 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 stage | Instant | past_due / suspended → active. 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_records → deleting_files → completed.
Recovery is possible through a Platform Owner until phase: completed.
Permissions Reference
| Permission | Description |
|---|---|
billing.view | Read-only: invoices, usage, current plan |
billing.plan.change | Upgrade / downgrade subscription (financial impact) |
billing.payment.manage | Add / remove payment methods |
billing.info.update | Update billing email, address |
billing.export | Export invoice PDFs and billing data |
Error Reference
| Error type | Status | Trigger |
|---|---|---|
problems/plan-limit-exceeded | 402 | Resource usage at plan ceiling |
problems/subscription-suspended | 402 | Mutating request while suspended |
problems/subscription-terminated | 402 | Any request while terminated |
problems/optimistic-lock-conflict | 409 | Wrong version on PATCH subscription |
problems/plan-change-in-progress | 409 | Second plan change while first is pending_change |
problems/webhook-signature-invalid | 401 | Missing or invalid Stripe-Signature header |