Subscriptions
Every tenant has exactly one subscription at all times. The subscription record is created automatically when the tenant registers — there is no "no subscription" state. All billing-related state transitions are managed by this service.
Subscription Statuses
| Status | Description | Next possible statuses |
|---|---|---|
trialing | Created automatically on registration. No payment required. Access is full. | active (payment added), past_due (trial expired, no payment) |
active | Subscription in good standing. Full access. | past_due (payment failure) |
past_due | Payment failed. Grace period: up to 7 days of full access. | active (payment received), suspended (day 8) |
suspended | Payment overdue 8–37 days. Read-only mode. | active (payment received), terminated (day 38) |
terminated | Payment overdue 38+ days. All access revoked. Async soft-delete starts. | No self-service recovery. Platform Owner can restore before delete completes. |
canceled | Manually canceled. | — |
Auto-Created Subscription (Zero-State)
When POST /auth/register is called, the platform creates:
- User record
- Tenant record
- Role
Ownerassigned to the user for this tenant - System wallet for the tenant
- Subscription record with status
trialing
All five steps are created in a single atomic transaction. If any step fails, all five are rolled back. It is not possible to have a tenant without a subscription, or a subscription without a tenant.
POST https://api.septemcore.com/v1/auth/register
Content-Type: application/json
{
"email": "[email protected]",
"password": "my-secure-password-123",
"name": "Alice Wonderland"
}
Response 201 Created — the subscription in trialing status is
created as part of the same response:
{
"userId": "01j9pa5mz700000000000000",
"tenantId": "01j9ten0000000000000000",
"subscriptionId": "01j9psub0000000000000000",
"subscription": {
"status": "trialing",
"trialEndsAt": "2026-05-15T00:00:00Z"
}
}
Trial Expiry Without Payment Method
When the trial period ends and the tenant has no attached payment method:
Trial end date reached + no payment method:
→ Subscription status: trialing → past_due
→ Notification sent: 3 days before trial end ("Link a card — trial ends {date}")
→ If no payment added: standard escalation timeline (7d grace → suspended → terminated)
If Billing Service is not deployed (self-hosted), trials are not used — all tenants have unlimited access with no subscription expiry.
Plan Change
Plan changes are made via PATCH /api/v1/billing/subscriptions/:id.
Optimistic Locking
All plan changes require the current version of the subscription
to be sent. If another request has already modified the subscription
(concurrent plan change), the server increments the version and the
request receives 409 Conflict:
PATCH https://api.septemcore.com/v1/billing/subscriptions/01j9psub0000000000000000
Authorization: Bearer <access_token>
Content-Type: application/json
{
"planId": "01j9pplan200000000000000",
"version": 3
}
Response 200 OK (success):
{
"subscriptionId": "01j9psub0000000000000000",
"status": "pending_change",
"currentPlan": "starter",
"pendingPlan": "professional",
"version": 4,
"effectiveAt": "2026-05-01T00:00:00Z"
}
Response 409 Conflict (concurrent modification):
{
"type": "https://api.septemcore.com/problems/optimistic-lock-conflict",
"title": "Conflict",
"status": 409,
"detail": "Subscription was modified by another request. Current version is 4, you sent 3. Fetch the latest subscription and retry.",
"traceId": "01j9ptr0000000000000005"
}
Plan Change State Machine
current → pending_change → new
│ │ │
│ PATCH received │ Stripe webhook │
│ + version matches │ (payment confirmed) │
└──────────────────────────►│────────────────────────────►│
│
│ PATCH received while pending_change:
└─ 409 Conflict: "plan change in progress"
While the subscription is in pending_change status (awaiting
Stripe webhook confirmation), a second PATCH request returns:
{
"type": "https://api.septemcore.com/problems/plan-change-in-progress",
"title": "Conflict",
"status": 409,
"detail": "A plan change is already in progress for this subscription. Wait for confirmation or cancel the current change.",
"traceId": "01j9ptr0000000000000006"
}
Per-tenant mutex: Only one plan change can be in-flight per tenant at any given time. The mutex is at the service layer (PostgreSQL advisory lock on the subscription row).
Downgrade Grace Period
When a tenant downgrades to a plan with lower limits:
Downgrade from Professional (10 000 records) → Starter (1 000 records):
Day 0 (downgrade applied):
→ Current usage: 4 500 records (over new limit of 1 000)
→ Existing 4 500 records: UNTOUCHED (no deletion)
→ New record creation: immediately BLOCKED (402 Payment Required)
→ Admin UI: shows warning "You are 3 500 records over your new plan limit"
Day 1–30:
→ Admin must delete records to get under the new limit
→ New record creation remains blocked
Day 30+ (over-limit not resolved):
→ Subscription: active → past_due → escalation timeline
Cancel Downgrade
A pending downgrade (not yet effective at the next billing cycle) can be canceled:
POST https://api.septemcore.com/v1/billing/subscriptions/01j9psub0000000000000000/cancel-downgrade
Authorization: Bearer <access_token>
Response 200 OK:
{
"subscriptionId": "01j9psub0000000000000000",
"status": "active",
"plan": "professional",
"version": 5
}
If the downgrade has already taken effect (past the billing cycle boundary), the only way back is a full upgrade (initiates a new billing cycle).
Get Current Subscription
GET https://api.septemcore.com/v1/billing/subscriptions/current
Authorization: Bearer <access_token>
{
"subscriptionId": "01j9psub0000000000000000",
"tenantId": "01j9ten0000000000000000",
"planId": "01j9pplan100000000000000",
"planName": "Professional",
"status": "active",
"trialEndsAt": null,
"currentPeriodStart": "2026-04-01T00:00:00Z",
"currentPeriodEnd": "2026-05-01T00:00:00Z",
"version": 3
}
Cancel Subscription
DELETE https://api.septemcore.com/v1/billing/subscriptions/01j9psub0000000000000000
Authorization: Bearer <access_token>
Response 200 OK:
{
"subscriptionId": "01j9psub0000000000000000",
"status": "canceled",
"canceledAt": "2026-04-22T10:30:00Z",
"accessUntil": "2026-05-01T00:00:00Z"
}
Cancellation is effective at the end of the current billing period
(accessUntil). The tenant retains full access until that date.
Error Reference
| Scenario | HTTP | Code |
|---|---|---|
| Version mismatch (concurrent modification) | 409 | optimistic-lock-conflict |
| Plan change already in progress | 409 | plan-change-in-progress |
| No pending downgrade to cancel | 400 | no-pending-downgrade |
| Subscription not found | 404 | not-found |
| Insufficient permission (not owner/admin) | 403 | forbidden |