Usage Limits
The API Gateway enforces subscription plan limits automatically on every mutating request. Modules do not implement limit checks — this is a platform-level concern handled transparently by the Gateway before the request reaches any upstream service.
Enforcement is only active when Billing Service is deployed. In self-hosted mode without Billing, the Gateway skips all limit checks. All requests pass freely.
Enforcement Flow
POST /api/v1/data (create new record) arrives at Gateway
Step 1: Extract tenantId from JWT
Step 2: Valkey GET plan_info:{tenantId}
┌─ Cache HIT → plan limits and subscription status available (O(1))
└─ Cache MISS → gRPC CheckLimit to Billing Service
resolve plan → { limits, status }
write to Valkey TTL = 15 minutes
Step 3: Check subscription status
✅ active / trialing → proceed to Step 4
⚠️ past_due → proceed to Step 4 (grace period, full access)
❌ suspended → POST/PATCH/DELETE → 402 (subscription-suspended)
EXCEPTION: Money debit/transfer/hold/confirm/cancel
→ always forwarded (FinCEN)
GET requests → forwarded (read-only mode)
❌ terminated → ALL requests → 402 (subscription-terminated)
Step 4: Check usage < limit
✅ Under limit → forward to upstream service
❌ At limit → 402 Payment Required (plan-limit-exceeded)
Step 5: Cache invalidation
billing.plan.changed or billing.subscription.changed event →
Event Bus → Gateway worker invalidates plan_info:{tenantId}
Valkey Cache
| Parameter | Value |
|---|---|
| Valkey key | plan_info:{tenantId} |
| Cache structure | { "limits": { "users": 50, "records": 10000, ... }, "status": "active" } |
| TTL | 15 minutes |
| Invalidation | On billing.plan.changed or billing.subscription.changed event |
| Cache MISS cost | 1 gRPC call to Billing Service (~2–5 ms) |
| Cache HIT cost | 1 Valkey GET (~0.5 ms) |
The 15-minute TTL is a deliberate trade-off. Plan changes by a Platform Owner take effect within at most 15 minutes across all Gateway instances without requiring explicit invalidation. Kafka event-based invalidation shortens this to near-instant for normal plan upgrades.
Limitable Resources
| Resource | Key | Unit | Description |
|---|---|---|---|
| Users | users | Count | Maximum number of active users in the tenant |
| Records | records | Count | Maximum number of Data Layer records across all modules |
| Storage | storage_bytes | Bytes | Total file storage across the Files primitive |
| Events | events_per_day | Count/day | Maximum events published per 24-hour window |
| Modules | modules | Count | Maximum number of installed active modules |
Limit Semantics
limit = 0 → unlimited (no enforcement for this resource)
limit >= 1 → enforced (Gateway blocks when usage >= limit)
A plan with { "users": 0 } allows unlimited users. A plan with
{ "users": 50 } allows at most 50 users. Once the 50th user is
created, the 51st POST /api/v1/users returns 402 Payment Required.
402 Responses
Plan Limit Exceeded
HTTP/1.1 402 Payment Required
Content-Type: application/problem+json
{
"type": "https://api.septemcore.com/problems/plan-limit-exceeded",
"title": "Payment Required",
"status": 402,
"detail": "Your plan allows 50 users. Current usage: 50. Upgrade your plan to add more users.",
"instance": "/api/v1/users",
"traceId": "01j9ptr0000000000000007",
"limit": {
"resource": "users",
"allowed": 50,
"current": 50,
"planId": "01j9pplan100000000000000"
}
}
Subscription Suspended
HTTP/1.1 402 Payment Required
Content-Type: application/problem+json
{
"type": "https://api.septemcore.com/problems/subscription-suspended",
"title": "Payment Required",
"status": 402,
"detail": "Your subscription is suspended due to a failed payment. The platform is in read-only mode. Please update your payment method to restore full access.",
"instance": "/api/v1/data/records",
"traceId": "01j9ptr0000000000000008"
}
Subscription Terminated
HTTP/1.1 402 Payment Required
Content-Type: application/problem+json
{
"type": "https://api.septemcore.com/problems/subscription-terminated",
"title": "Payment Required",
"status": 402,
"detail": "Your subscription has been terminated. Contact your platform administrator to restore access.",
"instance": "/api/v1/data/records",
"traceId": "01j9ptr0000000000000009"
}
FinCEN Exemption: Money Operations
When a subscription is in suspended status, the Gateway blocks
all mutating requests except Money Service operations:
| Operation | Suspended tenant | Reason |
|---|---|---|
POST /api/v1/data | ❌ Blocked (402) | Standard resource creation |
POST /api/v1/files/upload | ❌ Blocked (402) | Storage write |
POST /api/v1/money/credit | ✅ Allowed | FinCEN / user funds |
POST /api/v1/money/debit | ✅ Allowed | FinCEN / user funds |
POST /api/v1/money/hold | ✅ Allowed | FinCEN / user funds |
POST /api/v1/money/transfer | ✅ Allowed | FinCEN / user funds |
POST /api/v1/money/cancel | ✅ Allowed | FinCEN / user funds |
GET /api/v1/wallets | ✅ Allowed | Read-only (always allowed in suspended) |
Legal rationale: Wallet balances belong to users, not to the platform. Blocking withdrawals or transfers of funds that belong to a user during a payment dispute creates liability under FinCEN (Financial Crimes Enforcement Network) regulations. The platform does not hold user funds hostage for its own billing disputes.
A terminated subscription blocks all requests including Money
operations — at this stage the platform has made a final decision
on account closure through the full escalation process (37+ days,
5+ notifications).
SDK: Check Usage Manually
Modules do not need to check limits before creating resources — the Gateway handles this. However, the SDK exposes a usage query for informational purposes (e.g. showing the user how close they are to their limits):
const usage = await kernel.billing().getUsage();
// usage.users: { current: 47, limit: 50, percentage: 94 }
// usage.records: { current: 8430, limit: 10000, percentage: 84 }
// usage.storageBytes: { current: 2147483648, limit: 10737418240, percentage: 20 }
// usage.eventsPerDay: { current: 12000, limit: 100000, percentage: 12 }
// usage.modules: { current: 3, limit: 10, percentage: 30 }
GET https://api.septemcore.com/v1/billing/usage
Authorization: Bearer <access_token>
{
"tenantId": "01j9ten0000000000000000",
"planId": "01j9pplan100000000000000",
"planName": "Professional",
"usage": {
"users": { "current": 47, "limit": 50, "percentage": 94 },
"records": { "current": 8430, "limit": 10000, "percentage": 84 },
"storageBytes": { "current": 2147483648, "limit": 10737418240, "percentage": 20 },
"eventsPerDay": { "current": 12000, "limit": 100000, "percentage": 12 },
"modules": { "current": 3, "limit": 10, "percentage": 30 }
}
}