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" }
}codeis the stable error class for program behavior.issuescontains field or domain-specific validation details.contextcontains 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.messageis 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.retryableappears astrueonly 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-Aftercan accompany a retryable error when the server has a concrete delay. Respect the header before retrying the same idempotency identity.linksappears 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:
code | HTTP status |
|---|---|
BAD_REQUEST | 400 |
UNAUTHORIZED | 401 |
FORBIDDEN | 403 |
NOT_FOUND | 404 |
METHOD_NOT_SUPPORTED | 405 |
CONFLICT | 409 |
AUDIT_EVENT_REVISED | 409 |
CONNECTION_RECONNECT_REQUIRED | 409 |
JOURNAL_RECALCULATION_PENDING | 409 |
GONE | 410 |
AUDIT_CURSOR_EXPIRED | 410 |
PAYLOAD_TOO_LARGE | 413 |
MISDIRECTED_REQUEST | 421 |
UNPROCESSABLE_CONTENT | 422 |
EXPORT_REQUIRES_POST | 422 |
LOCKED | 423 |
TEAM_MIGRATION_FROZEN | 423 |
RATE_LIMITED | 429 |
INTERNAL_SERVER_ERROR | 500 |
NOT_IMPLEMENTED | 501 |
BAD_GATEWAY | 502 |
SERVICE_UNAVAILABLE | 503 |
GATEWAY_TIMEOUT | 504 |
INVALID_PLAID_CONNECT_MODE | 400 |
PLAID_CONNECTION_NOT_FOUND | 404 |
PLAID_CONNECT_IN_PROGRESS | 409 |
PLAID_CONNECT_CONFIGURATION_CHANGED | 409 |
PLAID_CONNECT_EXPIRED | 410 |
PLAID_CONNECT_REQUIRES_NEW_LINK | 422 |
PLAID_PROVIDER_UNAVAILABLE | 503 |
RAMP_VENDOR_OWNER_REQUIRED | 422 |
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 includecontext.booksClosedAt, the first open date inYYYY-MM-DDformat. - Delete locks on referenced entities return
400 BAD_REQUESTwithcontext.lockReasons(an array of human-readable reason strings) andcontext.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.
