Skip to main content

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

StatusDescriptionNext possible statuses
trialingCreated automatically on registration. No payment required. Access is full.active (payment added), past_due (trial expired, no payment)
activeSubscription in good standing. Full access.past_due (payment failure)
past_duePayment failed. Grace period: up to 7 days of full access.active (payment received), suspended (day 8)
suspendedPayment overdue 8–37 days. Read-only mode.active (payment received), terminated (day 38)
terminatedPayment overdue 38+ days. All access revoked. Async soft-delete starts.No self-service recovery. Platform Owner can restore before delete completes.
canceledManually canceled.

Auto-Created Subscription (Zero-State)

When POST /auth/register is called, the platform creates:

  1. User record
  2. Tenant record
  3. Role Owner assigned to the user for this tenant
  4. System wallet for the tenant
  5. 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

ScenarioHTTPCode
Version mismatch (concurrent modification)409optimistic-lock-conflict
Plan change already in progress409plan-change-in-progress
No pending downgrade to cancel400no-pending-downgrade
Subscription not found404not-found
Insufficient permission (not owner/admin)403forbidden