Skip to main content

Error Catalog

All Platform-Kernel APIs return errors in RFC 9457 Problem Details format with Content-Type: application/problem+json.

Base URI: https://platform-kernel.io/problems/

Response structure:

{
"type": "https://platform-kernel.io/problems/not-found",
"title": "Not Found",
"status": 404,
"detail": "User usr_01HX0... not found in tenant ten_01HX1...",
"instance": "/api/v1/users/usr_01HX0...",
"traceId": "4bf92f3577b34da6a3ce929d0e0e4736"
}

For validation errors, an additional errors array is included:

{
"type": "https://platform-kernel.io/problems/validation-error",
"title": "Bad Request",
"status": 400,
"detail": "2 fields failed validation.",
"instance": "/api/v1/users",
"traceId": "...",
"errors": [
{ "field": "email", "message": "Invalid email format", "code": "INVALID_FORMAT" },
{ "field": "name", "message": "Required field", "code": "REQUIRED" }
]
}

Standard Gateway Errors

These error types are produced by services/gateway/internal/errors/rfc9457.go and may be returned by any API endpoint.

HTTPError Type URITitleTrigger
400.../problems/validation-errorBad RequestRequest body or query parameter failed OpenAPI schema validation
401.../problems/unauthorizedUnauthorizedJWT is missing, expired, or has an invalid signature
402.../problems/payment-requiredPayment RequiredTenant subscription is suspended or terminated
403.../problems/forbiddenForbiddenAuthenticated user lacks the required RBAC permission
404.../problems/not-foundNot FoundRequested resource does not exist or is invisible due to tenant RLS
405.../problems/method-not-allowedMethod Not AllowedHTTP method is not defined for this endpoint
429.../problems/rate-limit-exceededToo Many RequestsGateway rate limit exceeded (local token bucket or Valkey global cap)
500.../problems/internal-errorInternal Server ErrorUnhandled server-side error; check traceId in observability
502.../problems/bad-gatewayBad GatewayUpstream gRPC service returned an error or is unreachable
503.../problems/service-unavailableService UnavailableCircuit breaker is OPEN for the target upstream service
504.../problems/gateway-timeoutGateway TimeoutUpstream gRPC call exceeded deadline

429 Rate Limit Response Headers

When a 429 is returned, the response includes:

Retry-After: 1
X-RateLimit-Limit: 10000
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1745302800

IAM Service Errors

Produced by services/iam/internal/handler/.

Authentication Errors

HTTPError Type URITrigger
400.../problems/validation-errorInvalid request body, missing required fields
401.../problems/unauthorizedInvalid credentials, expired JWT, invalid TOTP code
404.../problems/not-foundUser, role, or provider not found
409.../problems/conflictDuplicate email on registration

Tenant Hierarchy Errors

These are produced by tenant_handler.go with full URI constants:

HTTPError Type URITrigger
400https://platform.kernel/problems/slug-takenTenant slug already registered
400https://platform.kernel/problems/rbac-limit-exceededRBAC role assignment exceeds plan limit
403https://platform.kernel/problems/insufficient-tierOperation requires a higher subscription tier
403https://platform.kernel/problems/insufficient-roleCaller role does not permit this delegation
403https://platform.kernel/problems/self-delegationCaller cannot delegate to themselves
403https://platform.kernel/problems/not-a-descendantTarget tenant is not in caller's subtree
402https://platform.kernel/problems/plan-limit-exceededBilling plan limit reached
410https://platform.kernel/problems/tenant-deletedTenant has been soft-deleted
423https://platform.kernel/problems/tenant-suspendedTenant is in suspended billing state

Remediation

ErrorAction
unauthorizedRefresh token via POST /auth/refresh
insufficient-tierUpgrade subscription via POST /api/v1/billing/subscriptions
tenant-suspendedResolve outstanding payment; contact Platform Owner
not-a-descendantVerify B2B2B hierarchy with TenantHierarchyService.IsDescendant

Data Layer Errors

HTTPCodeTrigger
400validation-errorMissing module/model path, invalid filter operator
400validation-errorRecord exceeds 1 MB (DATA_MAX_RECORD_SIZE_BYTES)
400validation-errorJSONB field exceeds 256 KB (DATA_MAX_JSONB_SIZE_BYTES)
403forbiddenRLS: accessing another tenant's data
404not-foundRecord ID does not exist or RLS makes it invisible
422validation-errorRelation depth > 2 levels, or > 10 filters in one query

Money Service Errors

Produced by services/money/internal/handler/handler.go and services/money/internal/service/service.go.

HTTPCodeTrigger
400validation_erroruserId or currency missing on wallet creation
400invalid_idWallet ID is not a valid UUID v7
400invalid_bodyRequest JSON is malformed
400missing_tenantX-Tenant-ID header is absent
400missing_idempotency_keyidempotency_key field missing on write operation
404not-foundWallet ID not found (repository.ErrWalletNotFound)
409duplicate_walletWallet with same currency + userId already exists
409duplicate_idempotency_keyRequest with this idempotency key already processed
422insufficient_fundsWallet available balance < debit or hold amount (service.ErrInsufficientFunds)
422hold_limit_exceededConcurrent holds on wallet exceed 100 (MONEY_MAX_HOLDS_PER_WALLET)
422reversal_window_exceededReversal requested > 365 days after original transaction
422hold_expiredConfirm/Cancel requested on an already-expired hold

Money Service Remediation

ErrorAction
insufficient_fundsCheck balance via GET /api/v1/wallets/:id/balance before debit
missing_idempotency_keyGenerate idempotencyKey: crypto.randomUUID() and include in request
duplicate_idempotency_keyIdempotent — the original response was already committed; do not retry
hold_limit_exceededCancel stale holds via POST /api/v1/wallets/:id/cancel

File Storage Errors

Produced by services/files/internal/handler/handler.go.

HTTPCode / TitleTrigger
400invalid-multipart-formMultipart content-type mismatch or corrupt boundary
400missing-file-fieldfile field absent in multipart form
400failed-to-read-fileIO error reading uploaded bytes
400missing-tenant-idX-Tenant-ID header or tenantId query param absent
400missing-file-fieldPresign request body does not include a filename
413File size exceeds FILES_MAX_IMAGE_SIZE_MB (10 MB) — enforced at proxy
422virus-detectedClamAV INSTREAM scanner identified malware; file is quarantined
404not-foundFile ID does not exist or belongs to another tenant
422staging-timeoutFile was not confirmed within 24-hour staging TTL
422max-pending-exceededTenant has 100 in-progress uploads (FILES_STAGING_TTL_HOURS)

File Storage Remediation

ErrorAction
virus-detectedFile is permanently rejected. Re-upload a clean file. Do not retry the same bytes.
missing-tenant-idAdd X-Tenant-ID: {tenantId} header to every file request
staging-timeoutRetry upload — the original staging slot has expired

Notification Service Errors

HTTPCodeTrigger
400validation-errorMissing channel, userId, or templateId
400missing-tenantX-Tenant-ID header absent
404not-foundTemplate or channel not found
422batch-limit-exceededBatch size > 500 notifications
429rate-limit-exceededTenant exceeds 100/min or module exceeds 50/min

Billing Service Errors

HTTPCodeTrigger
400validation-errorInvalid plan ID, missing required billing fields
402payment-requiredSubscription past due; action requires active subscription
403forbiddenMissing billing.{action} RBAC permission
404not-foundPlan or subscription not found
409conflictTenant already has an active subscription to this plan
422invalid-transitionInvalid subscription state transition (e.g., terminatedactive)
423tenant-suspendedTenant in suspension window (days 8–37 after due date)

Integration Hub Errors

HTTPCodeTrigger
400validation-errorMissing provider URL, invalid configuration
404not-foundProvider ID not found
409conflictProvider slug already registered for this tenant
422circuit-openCircuit breaker is OPEN for this provider (5 failures / 30 s)
429rate-limit-exceededOutgoing rate exceeds 100 req/s for this provider
502bad-gatewayProvider returned a non-2xx response after all retries
503service-unavailableProvider is in circuit-breaker OPEN state

Audit Service Errors

HTTPCodeTrigger
400validation-errorMissing action, entityType, or entityId
400missing-tenantX-Tenant-ID header absent
404not-foundAudit record ID not found
422anonymize-failedGDPR anonymization conflict (record already anonymized)

Domain Resolver Errors

HTTPCodeTrigger
400validation-errorInvalid hostname format
403forbiddenDomain belongs to another tenant
404not-foundDomain ID not found
422domain-limit-exceededTenant has 5 custom domains (DOMAINS_MAX_PER_TENANT)
422dns-verification-failedCNAME or TXT record not propagated; retry after propagation
422ssl-provision-failedACME challenge failed (Let's Encrypt)

Error Handling Best Practices

Always check type — not status

HTTP status codes are coarse. Use the type URI as the machine- readable discriminator:

import { KernelApiError } from "@platform/sdk-core";

try {
await kernel.money.debit({ walletId, amount, idempotencyKey });
} catch (err) {
if (err instanceof KernelApiError) {
switch (err.type) {
case "https://platform-kernel.io/problems/insufficient_funds":
showInsufficientFundsUI();
break;
case "https://platform-kernel.io/problems/rate-limit-exceeded":
scheduleRetry(err.retryAfterMs);
break;
default:
reportToSentry(err);
}
}
}

Idempotency on retry

409 duplicate_idempotency_key means the original request was already committed. Treat this response as success — read the original result instead of retrying with a new idempotency key.

Trace ID correlation

Every 500-class error includes a traceId. Include this in your support ticket or internal runbook when escalating. Traces are available in Jaeger/Tempo via the observability stack.

# Find the trace in logs
kubectl logs -l app=platform-gateway | jq 'select(.trace_id=="4bf92f35...")'

See Also