Errors
Every endpoint on this surface returns RFC 9457 Problem Details as application/problem+json, with one narrow exception — see Status and diagnostics.
{
"type": "https://lutraid.com/problems/offer-expired",
"title": "Credential offer expired",
"status": 404,
"code": "OFFER_EXPIRED",
"requestId": "…"
}
- Name
type- Type
- uri
- Description
A stable URI identifying the problem type. Not necessarily dereferenceable.
- Name
title- Type
- string
- Description
Stable interface text for developers, not localized UI copy — build your own user-facing messages from
code.
- Name
status- Type
- integer
- Description
The HTTP status code, repeated in the body.
- Name
code- Type
- string
- Description
A stable, uppercase identifier — this is what you should branch on. The set of codes grows as the API does; treat one you don't recognize as opaque rather than assuming it can't happen.
- Name
requestId- Type
- string
- Description
Quote this when you report a problem.
Validation errors
A 422 uses the same envelope with code: "VALIDATION_ERROR" plus a field-level errors array:
{
"type": "https://lutraid.com/problems/validation-error",
"title": "Request validation failed",
"status": 422,
"code": "VALIDATION_ERROR",
"requestId": "…",
"errors": [
{
"code": "REQUIRED",
"location": "body",
"pointer": "/metadata/redirectUri"
}
]
}
errors[].code is one of INVALID, INVALID_FORMAT, INVALID_TYPE, INVALID_VALUE, REQUIRED, TOO_BIG, TOO_SMALL, UNRECOGNIZED_KEYS; location is body, cookie, header, path or query; pointer is a JSON Pointer into the request body. No free-text detail field is included by design.
Codes you will meet
| Status | Code | Meaning |
|---|---|---|
401 | INVALID_API_KEY | missing key, malformed key, wrong secret, revoked key |
401 | API_KEY_EXPIRED | key had a configured expiry that has passed |
403 | PERMISSION_DENIED | key scoped to a different issuer, verifier or organization |
404 | RESOURCE_NOT_FOUND | issuer, verifier, profile or record not found |
404 | OFFER_NOT_FOUND | credential offer ID doesn't exist on this issuer |
404 | OFFER_EXPIRED | offer's window closed before the wallet claimed it |
409 | OFFER_ALREADY_CLAIMED | the wallet already exchanged this offer |
404 | VERIFICATION_REQUEST_NOT_FOUND | verification ID doesn't exist on this verifier |
404 | VERIFICATION_REQUEST_EXPIRED | request's window closed before the wallet responded |
422 | VALIDATION_ERROR | request body failed schema validation — see errors[] above |
500 | INTERNAL_ERROR | unexpected server error — the message is generic in production |
Wallet protocol endpoints
The token, credential, nonce and PAR endpoints — called by wallets, not by you — follow OAuth 2.0 error conventions instead:
{
"error": "invalid_proof",
"error_description": "Proof JWT signature did not verify"
}
These are returned to the wallet, not to you — they surface in the diagnostics timeline rather than in a response you handle directly.