Skip to main content
[AVAILABLE]. packages/api/src/middleware/error-handler.ts, verified against a live server in this session.

The envelope

Every non-2xx response (except POST /policies/validate, see below) is:
{ "error": "string, always present, human-readable" }
sometimes with a second field:
{ "error": "...", "code": "RUNTIME_ERROR" }
code is present only when the failure reached the shared error handler as a typed RuntimeError subclass: VerificationFailedError (VERIFICATION_FAILED), ReceiptGenerationError (RECEIPT_GENERATION_FAILED), or an uncategorized RuntimeError (RUNTIME_ERROR). It is absent from every inline route-level check (businessTransactionId format/required checks), from BusinessTransactionValidationError, PolicyValidationError, SignalValidationError, PolicyNotFoundError, DuplicateBusinessTransactionError, and from the generic 500 fallback that catches everything else. error is always a plain string, never a nested object, this is deliberate: a caller can always safely display it or log it without walking a schema first. POST /policies/validate does not use this envelope at all. Every status code it can return (200, 400, 404) is {"valid": boolean, "errors": string[]}, its own shape, independent of what the caller sent. The one exception is 401, which is generated by the caller-auth middleware before this handler ever runs, so a rejected caller sees the shared envelope ({"error": "authentication required"}), not {valid, errors}.

Why code is inconsistent by design, not by accident

The envelope was built to describe whatever the code actually raises, not a target contract layered on afterward. Two verified gaps below show what that means in practice: places where an error path that looks like it should carry a stable code in fact does not, because the exception that reaches the handler isn’t the typed class the handler’s instanceof checks expect.

Two verified gaps

1. A structurally incomplete body crashes uncoded

A POST /execute or POST /transactions body with a valid-UUID businessTransactionId but a missing required nested object (for example metadata) does not reach the 400 BusinessTransactionValidationError path the schema implies exists for it. It throws an unhandled TypeError deep inside BusinessTransactionValidator.validate (packages/runtime/src/validators/BusinessTransactionValidator.ts:14), which is not an instance of BusinessTransactionValidationError, so the handler’s instanceof check never matches. The caller sees only:
{ "error": "Internal Server Error" }
with a 500, no code, and no indication of which field was missing. The real cause is visible only in the server’s own log.

2. A regression: “no Connector registered” lost its code, this pass

Re-verified live against the current server, not carried over from an earlier capture: the “no Connector registered for this action” case used to reach the client as a coded RuntimeError ({"error":"No connector registered for action: <action>.","code": "RUNTIME_ERROR"}). It no longer does. ConnectorSdkRegistry.resolveCapability (packages/connector-sdk/src/ConnectorRegistry.ts) now throws a raw, uncaught Error: No connector registered for capability '<capability>'. that also fails the handler’s instanceof RuntimeError check, collapsing into the exact same shape as gap #1:
{ "error": "Internal Server Error" }
This is listed here as a product regression to fix in packages/connector-sdk/packages/execution-control, most likely a side effect of the recent capability-routing refactor, not a documentation change made in this pass. See the Error catalog for the full, current, real behavior of every route.

Cross-reference

Every status code documented in the OpenAPI reference links to its entry in the Error catalog, which lists the triggering condition and what a caller should do for each one.