Every failure returns a JSON body with a stable machine-readable error code.
Branch on error, never on message — messages are written for humans and are
reworded without notice.
Response shape
{
"error": "KYC_REQUIRED",
"message": "Person must have approved KYC",
"details": { "personDid": "did:bfid:person:..." }
}
details is present only on codes that carry structured context.
Request-body validation failures carry the field-level problems instead:
{
"error": "VALIDATION_ERROR",
"message": "...",
"issues": [{ "path": ["ownerPublicKey"], "message": "Required", "code": "invalid_type" }]
}
issues lists field-level problems — path locates the offending field, which
is usually enough to fix the call without a support ticket.
Unhandled server faults return INTERNAL with a fixed "Internal error"
message. Quote your x-correlation-id (see
Authentication) when opening a support
ticket.
Codes
Authentication and authorization
| Code | HTTP | Meaning | What to do |
|---|---|---|---|
UNAUTHORIZED | 401 | Missing, malformed, revoked, or expired key; or a bad request signature | Check the bearer header. If signing is on, see the signing failure table — the message distinguishes skew, replay, and mismatch |
FORBIDDEN | 403 | Key is valid but lacks the scope, or the caller's IP is outside the allowlist | Mint a key with the right scope, or add the egress address you get from GET /v1/whoami |
RATE_LIMITED | 429 | Plan quota or the per-IP backstop exceeded | Wait x-ratelimit-plan-reset seconds, then retry with jitter |
Request problems
| Code | HTTP | Meaning | What to do |
|---|---|---|---|
VALIDATION_ERROR | 400 | Body or query failed schema validation | Read issues[].path; fix the field |
BAD_REQUEST | 400 | Well-formed but not acceptable — e.g. an Idempotency-Key over 255 characters | Read message; this is a client bug, not a transient fault |
NOT_FOUND | 404 | No such account, person, job, or key in your organization | Confirm the identifier, and that it belongs to the key's organization |
CONFLICT | 409 | State does not allow the operation, or an idempotency key was reused with a different body | Re-read current state before retrying |
Identity lifecycle
| Code | HTTP | Meaning | What to do |
|---|---|---|---|
KYC_REQUIRED | 403 | The Person has no approved KYC, and your organization requires it before this operation | Run /v1/kyc/start and wait for approval before /v1/accounts/free |
ACCOUNT_NOT_PROVISIONAL | 409 | The account is not in a lifecycle state this operation accepts — typically an unlock on an already-unlocked account | Fetch GET /v1/accounts/{did} and check lifecycle; often the previous call already succeeded |
NONCE_INVALID | 400 | Bind nonce missing, already spent, or expired | Request a fresh /v1/bind/challenge; nonces are single-use |
INVALID_SIGNATURE | 400 | The bind signature does not verify against the account's key | Confirm you signed payloadToSign verbatim with the key registered as ownerPublicKey |
PERSON_ACCOUNT_CAP | 409 | The Person already holds the maximum bound accounts; details.max is the limit | Surface it to the user rather than retrying |
An unavailable account name is not an error: GET /v1/accounts/name-available
returns 200 with {"available": false, "reason": "TAKEN"}. Check it before
you offer a name to a user, and treat reason as a display hint rather than a
failure.
Upstream and dependencies
| Code | HTTP | Meaning | What to do |
|---|---|---|---|
CHAIN_FAILURE | 502 | The chain write failed or could not be confirmed across RPC endpoints | Safe to retry with the same Idempotency-Key; do not mint a second account |
PROVIDER_FAILURE | 502 | A KYC/AML provider failed; message names which | Retry with backoff. Persistent failures are ours, not yours — quote the correlation ID |
INTERNAL | 500 | Unhandled fault | Retry idempotently with backoff; report with the correlation ID if it persists |
Retrying
| Class | Retry? |
|---|---|
429 | Yes — after x-ratelimit-plan-reset seconds, with jitter |
502, 500 | Yes — exponential backoff, and always with the original Idempotency-Key on billable endpoints |
401, 403 | No — except a signing 401, where the fix is a freshly signed request, not a resend |
400, 404, 409 | No — the request needs changing, not repeating |
Retrying a billable endpoint without its original Idempotency-Key is how a
retry becomes a double charge. See
Idempotency.
See also
- Authentication — keys, scopes, signing, idempotency
- Webhooks — delivery failures and replay
- OpenAPI spec — per-endpoint responses