VRPlatformVRPlatform

Error Contract

Handle structured API errors without parsing messages

API failures use a structured envelope:

{
  "code": "FORBIDDEN",
  "message": "Cannot modify transaction - attached to published statement",
  "issues": [],
  "context": { "lockReason": "statement" }
}
  • code is the stable error class for program behavior.
  • issues contains field or domain-specific validation details.
  • context contains typed recovery information such as IDs, refs, invalid values, lock reasons, supported archive outcomes, next actions, or retry delays. Private provider and infrastructure details are never copied into the response. It is omitted when the error carries no public context.
  • message is static display text for one error condition. It never embeds UUIDs, refs, emails, indexes, raw values, or other request-specific data and must not be parsed.
  • retryable appears as true only when the server explicitly classifies the same idempotent operation as safe to retry. Its absence means do not retry unchanged unless the operation documents another recovery rule.
  • Retry-After can accompany a retryable error when the server has a concrete delay. Respect the header before retrying the same idempotency identity.
  • links appears only on strict query-validation errors and points to the public interactive reference (docs) and OpenAPI document (schema). It is never an empty object.

Codes And HTTP Status

code values on the wire map to HTTP status as follows:

codeHTTP status
BAD_REQUEST400
UNAUTHORIZED401
FORBIDDEN403
NOT_FOUND404
METHOD_NOT_SUPPORTED405
CONFLICT409
AUDIT_EVENT_REVISED409
CONNECTION_RECONNECT_REQUIRED409
JOURNAL_RECALCULATION_PENDING409
GONE410
AUDIT_CURSOR_EXPIRED410
PAYLOAD_TOO_LARGE413
MISDIRECTED_REQUEST421
UNPROCESSABLE_CONTENT422
EXPORT_REQUIRES_POST422
LOCKED423
TEAM_MIGRATION_FROZEN423
RATE_LIMITED429
INTERNAL_SERVER_ERROR500
NOT_IMPLEMENTED501
BAD_GATEWAY502
SERVICE_UNAVAILABLE503
GATEWAY_TIMEOUT504
INVALID_PLAID_CONNECT_MODE400
PLAID_CONNECTION_NOT_FOUND404
PLAID_CONNECT_IN_PROGRESS409
PLAID_CONNECT_CONFIGURATION_CHANGED409
PLAID_CONNECT_EXPIRED410
PLAID_CONNECT_REQUIRES_NEW_LINK422
PLAID_PROVIDER_UNAVAILABLE503
RAMP_VENDOR_OWNER_REQUIRED422

Branch on code, not on raw status, and treat unknown codes as non-retryable failures.

Batch item failures use the same public code registry. Internal repository aliases are mapped before the response: USER_ERROR becomes BAD_REQUEST, INTERNAL_VALIDATION_ERROR becomes UNPROCESSABLE_CONTENT, and INTERNAL_ERROR becomes INTERNAL_SERVER_ERROR.

JOURNAL_RECALCULATION_PENDING blocks statement finalization, payout preview, or payout while an affected reservation journal is not current. Its context contains journalStatus (stale, recalculating, or failed) and safe operationIds that the caller can poll. Re-read the statement after the work settles; do not retry the unchanged accounting action while the status remains non-current.

Domain Issue Compatibility

Domain issue codes use lower camel case. Existing values balanceMismatch_start, balanceMismatch_end, and line_unassignedAccount remain supported compatibility exceptions. Plaid product keys transactions_updates and item_logins keep Plaid's native format. Clients must not infer a general snake-case convention from these values.

Export Admission

Generated artifact GET routes return EXPORT_REQUIRES_POST when the selected work is not expected to finish within the awaited request budget:

{
  "code": "EXPORT_REQUIRES_POST",
  "message": "Export must be queued",
  "issues": [],
  "context": {
    "recommendedMethod": "POST",
    "reason": "selectionTooLarge"
  }
}

Send the same selector fields to POST on the same path, then poll the returned operation. The rejected GET does not create an operation. Branch on the exact error code because other 422 responses have different recovery rules.

Awaited renderer failures return BAD_GATEWAY; renderer timeouts return GATEWAY_TIMEOUT. Timeouts, aborted requests, 5xx responses, and other error codes do not indicate that POST is required. Surface or retry them according to their own error contract. See Generated exports for both supported client flows.

Lock Context

Lock failures are 403 FORBIDDEN and identify the lock in context:

  • Mutation locks return context.lockReason, either "statement" (attached to a published owner statement) or "period" (blocked by books closing). Period locks also include context.booksClosedAt, the first open date in YYYY-MM-DD format.
  • Delete locks on referenced entities return 400 BAD_REQUEST with context.lockReasons (an array of human-readable reason strings) and context.suggestedOnLocked (for example "archive") when the operation supports an alternative outcome such as archiving instead of deleting.

Other domain errors can include nextAction or supportAction. Present only the actions returned or documented for that operation. See Requests, Locks & Issues.

Plaid Connect Recovery Context

PLAID_CONNECT_REQUIRES_NEW_LINK returns one strict recovery directive:

{ "restartMode": "create" }

or:

{
  "restartMode": "replace",
  "connectionId": "6a4fb5d4-9822-46dc-9812-9e6742fda0bc"
}

Create recovery never includes connectionId; replace recovery always does. Provider failures can add plaidErrorCode, plaidErrorMessage, or plaidRequestId to the same context. Use the recovery directive for the next POST /plaid/connect and do not infer a different mode from client state.

Request Validation Errors

Invalid path parameters, query parameters, and JSON body fields return 400 with code: "BAD_REQUEST", a static message, and field-level issues. Schema validation issues include the operation schema location and a field path when available. The links object points to the API documentation and OpenAPI schema. The response does not echo the request body.

Malformed JSON also returns 400 with code: "BAD_REQUEST", the message Malformed JSON in request body, and an empty issues array.

Query parameters are strict. An unsupported parameter returns 400 with links to the API schema and an issue that identifies the operation schema location. Remove the parameter; it is not silently ignored.

{
  "code": "BAD_REQUEST",
  "message": "Invalid query parameters. Check OpenAPI and remove unsupported parameters.",
  "issues": [
    {
      "message": "Unrecognized key: \"foo\"",
      "schema": "#/paths/~1transactions/get"
    }
  ],
  "links": {
    "docs": "https://api.vrplatform.app/spec",
    "schema": "https://api.vrplatform.app/openapi.json"
  }
}

Rate Limits

Webhook subscription, verification-test, and replay operations enforce the limits documented in Webhooks. An excess request returns RATE_LIMITED with HTTP 429 and Retry-After. Other API surfaces do not currently enforce application-level quotas.

Availability

Transient database connectivity exhaustion is returned as SERVICE_UNAVAILABLE. The API uses bounded retries for compiled database selects after known connection failures, including an established connection being lost, but does not replay mutations or raw or unknown queries after an ambiguous connection loss. Callers should retry only idempotently and with bounded backoff. A domain-specific service-unavailable response can include structured delay or pending-resource context.

Regional misroutes return MISDIRECTED_REQUEST. Its context contains dataRegion, apiBaseUrl, and teamId when the request selected a team. Send the request to the returned API base URL without changing the selected team.

Connection provider failures return BAD_GATEWAY; the 25-second awaited provider limit returns GATEWAY_TIMEOUT. Provider details remain private.

See Error handling and retry policy and Requests, Locks & Issues.

On this page