Errors are {"detail": "..."} — a human-readable string. Exactly one endpoint
returns something a caller can branch on:
src/crypto_processing_api/api/deposits.py:168 answers a pooled-address
exhaustion with
{"detail": {"code": "DEPOSIT_TEMPORARILY_UNAVAILABLE", "message": "..."}}
Everywhere else, a platform that wants to tell "below the asset minimum" from
"destination address is invalid" — both 422 — has to match on English prose
that is not part of any contract and that changes whenever somebody improves a
sentence.
What to build
A small catalogue of stable error codes, applied consistently.
This must be additive. detail keeps working exactly as it does today for
every existing caller. The wire format is pinned by
tests/integration/test_wire_bytes.py against a committed byte corpus, and
keeping that test green is as much the acceptance criterion here as any new
test. Adding a sibling field or a header is fine; changing the shape of
detail on an endpoint that returns a string today is not.
Things to work out first
- Where the code goes. A
code beside detail changes the envelope shape
for every error. An X-Error-Code header changes no bytes in any body at
all. Both are defensible — argue for one in this issue before writing much
code.
- How many codes. Small enough to memorize. Roughly: unknown asset,
unknown resource, insufficient balance, below minimum, dust, invalid
destination, idempotency conflict, idempotency body mismatch, upstream
unavailable, upstream refused. Resist one code per message.
- They belong in the OpenAPI document, which is committed at
docs/reference/openapi.json and regenerated by
python scripts/export_openapi.py. A code an SDK cannot see is a code
nobody uses. src/crypto_processing_api/api/schemas.py holds the existing
error models and the error_responses() helper.
Scope
API surface only. The exceptions raised in services/ already distinguish
these cases by type — this is about carrying that distinction across the
boundary, not about changing any of it.
Errors are
{"detail": "..."}— a human-readable string. Exactly one endpointreturns something a caller can branch on:
src/crypto_processing_api/api/deposits.py:168answers a pooled-addressexhaustion with
{"detail": {"code": "DEPOSIT_TEMPORARILY_UNAVAILABLE", "message": "..."}}Everywhere else, a platform that wants to tell "below the asset minimum" from
"destination address is invalid" — both
422— has to match on English prosethat is not part of any contract and that changes whenever somebody improves a
sentence.
What to build
A small catalogue of stable error codes, applied consistently.
This must be additive.
detailkeeps working exactly as it does today forevery existing caller. The wire format is pinned by
tests/integration/test_wire_bytes.pyagainst a committed byte corpus, andkeeping that test green is as much the acceptance criterion here as any new
test. Adding a sibling field or a header is fine; changing the shape of
detailon an endpoint that returns a string today is not.Things to work out first
codebesidedetailchanges the envelope shapefor every error. An
X-Error-Codeheader changes no bytes in any body atall. Both are defensible — argue for one in this issue before writing much
code.
unknown resource, insufficient balance, below minimum, dust, invalid
destination, idempotency conflict, idempotency body mismatch, upstream
unavailable, upstream refused. Resist one code per message.
docs/reference/openapi.jsonand regenerated bypython scripts/export_openapi.py. A code an SDK cannot see is a codenobody uses.
src/crypto_processing_api/api/schemas.pyholds the existingerror models and the
error_responses()helper.Scope
API surface only. The exceptions raised in
services/already distinguishthese cases by type — this is about carrying that distinction across the
boundary, not about changing any of it.