Skip to main content

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

ParameterValue
Valkey keyplan_info:{tenantId}
Cache structure{ "limits": { "users": 50, "records": 10000, ... }, "status": "active" }
TTL15 minutes
InvalidationOn billing.plan.changed or billing.subscription.changed event
Cache MISS cost1 gRPC call to Billing Service (~2–5 ms)
Cache HIT cost1 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

ResourceKeyUnitDescription
UsersusersCountMaximum number of active users in the tenant
RecordsrecordsCountMaximum number of Data Layer records across all modules
Storagestorage_bytesBytesTotal file storage across the Files primitive
Eventsevents_per_dayCount/dayMaximum events published per 24-hour window
ModulesmodulesCountMaximum 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:

OperationSuspended tenantReason
POST /api/v1/data❌ Blocked (402)Standard resource creation
POST /api/v1/files/upload❌ Blocked (402)Storage write
POST /api/v1/money/credit✅ AllowedFinCEN / user funds
POST /api/v1/money/debit✅ AllowedFinCEN / user funds
POST /api/v1/money/hold✅ AllowedFinCEN / user funds
POST /api/v1/money/transfer✅ AllowedFinCEN / user funds
POST /api/v1/money/cancel✅ AllowedFinCEN / user funds
GET /api/v1/wallets✅ AllowedRead-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 }
}
}