PlatformError model

Error model

The structure of error responses in V3 Custody: the error/message/code/details envelope, HTTP statuses, and machine-readable codes.

The API returns standard HTTP statuses with a single JSON error envelope. Validation errors include per-field details, and domain errors include a machine-readable code.

How to branch your logic on a response:

Error envelope

{
  "error": "Bad Request",
  "message": "The request contains invalid parameters or malformed data",
  "code": 400,
  "details": [
    { "field": "email", "message": "Invalid email format" }
  ]
}
  • error: a short error name (Bad Request, Unauthorized, Not Found…).
  • message: a human-readable description.
  • code: the HTTP status as a number.
  • details: per-field errors (for 400 validation errors): field + message.

HTTP statuses

StatusMeaning
200 / 201Success
202Queued: an approval request was created
400Bad request (validation; see details)
401Not authenticated
403Forbidden by policy (deny/block; default-deny)
404Not found
410The resource is no longer valid (for example, a revoked or accepted invitation)
422Unprocessable

Machine-readable codes

In addition to the HTTP status, domain errors carry a machine-readable code, which is the most convenient thing to branch your logic on:

CodeWhen
INSUFFICIENT_BALANCEInsufficient funds for the transfer
AMOUNT_EXCEEDEDA policy limit was exceeded
REPORT_PERIOD_TOO_LARGEThe report result is too large; narrow the filters
REPORT_UNSUPPORTED_FORMATThe export format isn't supported yet (xlsx/pdf)

Where the machine-readable code lives varies by endpoint: in some it's a string code, in others a nested error.code, and for the Policy Engine it's the reason in the decision (with a trace of the matched policies). See the API Reference for the exact shape.

Handling errors

Check the response status and read code/details:

const res = await fetch(url, { headers });
if (!res.ok) {
  const err = await res.json();
  console.error(`${err.code} ${err.error}: ${err.message}`);
  for (const d of err.details ?? []) {
    console.error(`  ${d.field}: ${d.message}`);
  }
}

When to retry

  • 5xx and network failures: retry with exponential backoff. Retry transfers with the same idempotencyKey so the payment isn't duplicated (see Idempotency).
  • 400 / 422: don't retry blindly; fix the request using details.
  • 403: this is a Policy Engine decision. Check reason and trace to see which policy blocked the request (see Policy Engine).

Common mistakes

SymptomCause
branching on textmessage is human-readable; branch on code or the machine-readable code
403 mistaken for an authentication issue403 is a policy decision (deny), not a token problem; authentication failures are 401
202 mistaken for final successit's the queue: an approval request was created, and the operation isn't final yet
5xx isn't retriedretry with backoff (for transfers, with the same idempotencyKey)
looking for the code in one placethe code's location varies by endpoint (code / error.code / reason)

Next steps