Skip to main content

Error Handling

Every error response from https://api.septemcore.com/v1/ uses RFC 9457 Problem Details for HTTP APIs. A single, predictable format across all 15 backend services means module code only needs one error handler, and support teams only need one mental model.


application/problem+json

When the HTTP status is 4xx or 5xx, the response body is always:

Content-Type: application/problem+json

Required fields

{
"type": "https://api.septemcore.com/problems/not-found",
"title": "Resource not found.",
"status": 404,
"detail": "Wallet '01j9p3kx2e00000000000000' does not exist.",
"instance": "/v1/wallets/01j9p3kx2e00000000000000"
}
FieldTypeRequiredDescription
typeURIStable, dereferenceable identifier for the error class. Never changes for a given error class.
titlestringShort, human-readable summary. Same for every occurrence of this error class.
statusintegerHTTP status code — mirrors the response status.
detailstringHuman-readable explanation specific to this occurrence.
instanceURIRequest path where the error occurred.

Platform extensions

Two additional fields are present on every platform error response:

FieldTypeDescription
trace_idstringOpenTelemetry Trace ID. Use this when filing a support ticket or correlating with VictoriaMetrics traces.
errors[]arrayPresent only on validation errors. Each element: { "field": "email", "message": "Invalid email format", "code": "INVALID_FORMAT" }.
{
"type": "https://api.septemcore.com/problems/validation-error",
"title": "Validation failed.",
"status": 400,
"detail": "2 fields failed validation.",
"instance": "/v1/users",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"errors": [
{ "field": "email", "message": "Invalid email format", "code": "INVALID_FORMAT" },
{ "field": "password", "message": "Minimum 12 characters", "code": "TOO_SHORT" }
]
}
important

The type URI is stable and machine-readable. Build your error handling logic around type, not title or detail. Titles and details may be localised or updated across releases; the type URI is versioned and will not change within a major API version.


gRPC → HTTP Status Mapping

All platform backend services communicate with the API Gateway using gRPC. The Gateway translates gRPC status codes to HTTP and constructs the RFC 9457 body. Go services never produce HTTP status codes directly.

gRPC codeHTTP statustype suffix
codes.OK200
codes.InvalidArgument400/problems/validation-error
codes.Unauthenticated401/problems/unauthorized
codes.PermissionDenied403/problems/forbidden
codes.NotFound404/problems/not-found
codes.AlreadyExists409/problems/conflict
codes.ResourceExhausted429/problems/rate-limit-exceeded
codes.Internal500/problems/internal-error
codes.Unavailable503/problems/service-unavailable
codes.DeadlineExceeded504/problems/gateway-timeout

The detail field is populated from status.Message() of the gRPC response. The trace_id is propagated via W3C traceparent through every gRPC hop.


Platform Error Type Catalogue

The full type URI is always: https://api.septemcore.com/problems/{suffix}.

Authentication & authorisation

type suffixHTTPTrigger
unauthorized401Missing or expired Bearer token.
token-expired401Access token lifetime (15 min) elapsed. Call /auth/refresh.
refresh-token-expired401Refresh token lifetime (7 days) elapsed. Full login required.
forbidden403Token is valid but the caller lacks the required permission.
mfa-required403The endpoint requires MFA verification.
tenant-suspended402Tenant subscription is suspended (read-only mode).
tenant-terminated403Tenant has been terminated. Contact platform support.

Validation

type suffixHTTPTrigger
validation-error400One or more request fields failed schema or business validation. Includes errors[].
invalid-filter-operator400Unsupported operator in Data API query (e.g. ?field=xyz.value).
unknown-field400Field in ?select= or filter does not exist on the model.
idempotency-key-missing400Idempotency-Key header absent on a mutating POST.
idempotency-key-invalid400Idempotency-Key is not a valid UUID v4.

Resources

type suffixHTTPTrigger
not-found404Requested resource ID does not exist or is not visible to the caller's tenant.
conflict409Optimistic locking conflict (Feature Flags version mismatch) or AlreadyExists.
gone410API version has been sunset. Migrate to the current version.

Rate limiting & quota

type suffixHTTPTrigger
rate-limit-exceeded429Tenant or IP quota exceeded. Includes Retry-After header.
plan-limit-exceeded402Billing plan limit reached (users, storage, modules). Upgrade plan.
module-limit-exceeded402Tenant has reached the maximum module count (50 by default).

Money

type suffixHTTPTrigger
insufficient-funds422Debit or hold amount exceeds available balance.
wallet-not-found404Wallet ID does not exist for this tenant.
hold-expired422Attempted to confirm or cancel an expired hold (TTL 72 h by default).
reversal-window-expired422Reversal requested after the max 365-day window.
duplicate-transaction409Idempotency key collision on a money operation.
transaction-limit-exceeded422Single transaction exceeds $100 000.

Files

type suffixHTTPTrigger
file-not-found404File ID does not exist or belongs to a different tenant.
file-infected422Antivirus scanner detected a threat in the staging bucket. File rejected.
file-too-large413Upload exceeds the service limit (images: 10 MB).
staging-quota-exceeded429100 pending-scan files already exist for this tenant.

Events

type suffixHTTPTrigger
event-not-declared403Module attempted to publish an event not listed in events.publishes[].
event-topic-protected403Module attempted to publish to a kernel-owned topic (auth.*, money.*, etc.).
event-subscribe-not-declared403Module attempted to subscribe to an event not in events.subscribes[].

Modules

type suffixHTTPTrigger
module-not-active404Module requested but status is not active for this tenant.
module-install-failed422Module installation state machine reached failed status.
module-version-mismatch422Required shared dependency version incompatible with the shell.
manifest-invalid400module.manifest.json failed schema validation. Includes errors[].

External integrations

type suffixHTTPTrigger
provider-circuit-open503Circuit breaker for the external provider is OPEN (5 failures in 30 s). Retry after 60 s.
provider-timeout504External provider did not respond within its configured timeout.
provider-not-found404Integration provider ID does not exist for this tenant.

Infrastructure

type suffixHTTPTrigger
internal-error500Unhandled server-side error. Always includes trace_id for debugging.
service-unavailable503An upstream service (gRPC) is unavailable. Gateway circuit breaker is OPEN.
gateway-timeout504gRPC deadline (5 s, GRPC_CALL_TIMEOUT_MS) exceeded before the upstream service responded.
api-version-sunset410The requested API version (/v1/) has passed its sunset date.

Batch / Partial Success — 207 Multi-Status

Batch endpoints (Data API batch CRUD, bulk notify, batch money transfers) return 207 Multi-Status when part of the batch succeeds and part fails:

// POST https://api.septemcore.com/v1/data/crm/contacts/batch
// 207 Multi-Status

{
"results": [
{
"index": 0,
"status": 201,
"data": { "id": "01j9p3kx2e00000000000000" }
},
{
"index": 1,
"status": 400,
"error": {
"type": "https://api.septemcore.com/problems/validation-error",
"title": "Validation failed.",
"status": 400,
"detail": "Field 'email' is required.",
"instance": "/v1/data/crm/contacts/batch[1]",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"errors": [
{ "field": "email", "message": "Required field", "code": "REQUIRED" }
]
}
},
{
"index": 2,
"status": 201,
"data": { "id": "01j9p4mn3c00000000000000" }
}
],
"meta": {
"total": 3,
"succeeded": 2,
"failed": 1
}
}

Each error in results[].error is a full RFC 9457 object. The outer response status is always 207 when the batch is mixed — never 200 or 400.


SDK Error Handling

The TypeScript SDK wraps all HTTP errors in typed exceptions:

import { kernel } from '@platform/sdk-core';
import { PlatformError } from '@platform/sdk-core/errors';

try {
const wallet = await kernel.money().retrieve('01j9p3kx2e00000000000000');
} catch (err) {
if (err instanceof PlatformError) {
switch (err.type) {
case 'https://api.septemcore.com/problems/not-found':
// Handle missing wallet gracefully
break;
case 'https://api.septemcore.com/problems/forbidden':
// Caller lacks the required permission
break;
default:
// Unexpected error — surface trace_id to user
console.error('Unexpected error', err.traceId);
}
}
}

PlatformError exposes:

PropertyTypeDescription
typestringThe full type URI from RFC 9457.
titlestringShort stable description.
statusnumberHTTP status code.
detailstringOccurrence-specific description.
instancestringRequest path.
traceIdstringOpenTelemetry trace ID for debugging.
errorsarrayValidation errors (if any).

Defining Custom Error Types (Module Authors)

Modules may define their own type URIs for domain-specific errors. Rules:

  1. The base URI must use the module's registered slug: https://api.septemcore.com/problems/{module-slug}/{suffix}.
  2. The module must document all custom types in its own docs/errors.md.
  3. Custom types must not reuse kernel-reserved type suffixes.
  4. The Go service returns status.Error(codes.InvalidArgument, "...") or another appropriate gRPC code — the Gateway constructs the HTTP error. Modules returning raw HTTP status codes from gRPC is prohibited.
// CRM module — Go service example
import "google.golang.org/grpc/status"
import "google.golang.org/grpc/codes"

func (s *ContactService) Create(ctx context.Context,
req *pb.CreateContactRequest) (*pb.CreateContactResponse, error) {

if isDuplicate {
return nil, status.Error(
codes.AlreadyExists,
"contact with this email already exists in your CRM",
)
// Gateway maps AlreadyExists → 409
// type: "https://api.septemcore.com/problems/conflict"
}
// ...
}

For module-specific problem types, the module must register a custom error handler in its API declaration so the Gateway uses the module's type URI instead of the default one:

// Module-specific errors registered in module API handler init
errMapping[codes.AlreadyExists] = "https://api.septemcore.com/problems/crm/duplicate-contact"

Error Handling Checklist

✅ Do❌ Do not
Match on type URI for programmatic handlingMatch on title or detail strings
Surface trace_id in user-facing error messagesDiscard trace_id
Use errors[] for field-level validation feedbackShow generic "Something went wrong" for validation errors
Retry 503 and 504 after the Retry-After intervalRetry immediately without back-off
Treat 402 as a soft block — show upgrade promptTreat 402 as fatal
Pass Idempotency-Key on retry after network failureUse a new key per retry