Errors & rate limits
The error envelope
Every error response is JSON with a machine-readable code:
{ "code": "INVALID_CREDENTIALS", "error": "Invalid identifier or password" }
Always branch on code, never on error — error is an English debug string that can change
without notice and should never be shown to an end user as-is.
Some codes carry extra fields alongside code/error:
| Extra field | Appears on |
|---|---|
retryAfter | RATE_LIMITED — seconds until the limit resets |
attemptsLeft | INVALID_OTP — remaining attempts before lockout |
channel | VERIFICATION_REQUIRED — which channel (EMAIL/CELLPHONE) needs proving |
field | DUPLICATE_CUSTOMER — which field collided (email/cellphone) |
errors | VALIDATION_ERROR — a field → { code, message } map |
Edge's own codes
These originate in the gateway itself, regardless of which backend eventually handles the call:
| HTTP | code | When |
|---|---|---|
| 401 | TOUCHPOINT_UNAUTHORIZED | Missing/invalid/disabled touchpoint credential |
| 401 | UNAUTHENTICATED | Missing/invalid Customer JWT on a customer-scoped call |
| 404 | ROUTE_NOT_FOUND | No such endpoint |
| 429 | RATE_LIMITED | App-level rate limit exceeded (retryAfter included) |
| 501 | NOT_IMPLEMENTED | A reserved seam — the backend operation doesn't exist yet |
| 400 | MALFORMED_REQUEST | Body isn't valid JSON |
| 400 | INVALID_REQUEST | A path/query value is malformed (e.g. a non-UUID id) |
| 400 | VALIDATION_ERROR | A field failed validation — see errors |
| 415 | UNSUPPORTED_MEDIA_TYPE | Wrong/missing Content-Type |
| 405 | METHOD_NOT_ALLOWED | Wrong HTTP method for the path |
| 500 | INTERNAL_ERROR | Unhandled failure |
Backend codes (pass through unchanged)
The Edge API forwards most operations to backend services, and their business-rule errors pass
through verbatim — same status, same code, same extra fields. You key off one stable set of
codes regardless of which service actually produced it:
| HTTP | code | Extra fields | Meaning |
|---|---|---|---|
| 401 | INVALID_CREDENTIALS | Wrong identifier/password, or wrong current password on change | |
| 400 | INVALID_OTP | attemptsLeft | Wrong one-time code |
| 400 | INVALID_REGISTRATION_TOKEN | Bogus, expired, or already-consumed registration session token | |
| 409 | VERIFICATION_REQUIRED | channel | Touchpoint requires proof of this channel before an account exists |
| 404 | CUSTOMER_NOT_FOUND | No such customer (e.g. a stale JWT) | |
| 403 | CUSTOMER_INACTIVE | Account exists but isn't LIVE + ACTIVE | |
| 409 | VERSION_CONFLICT | Optimistic-lock: PATCH /me sent a stale version | |
| 409 | DUPLICATE_CUSTOMER | field | New email/cellphone collides with another customer |
| 404 | COUNTRY_NOT_FOUND | Unknown country code on the subdivisions lookup |
Rate limiting
The Edge API enforces a per-identifier and per-touchpoint rate limit on sensitive operations (login, registration, password reset). Exceeding it returns:
{ "code": "RATE_LIMITED", "error": "Too many requests", "retryAfter": 30 }
retryAfter is in seconds. Back off and retry after that window — retrying immediately will
repeatedly hit the same limit.
Reserved seams return 501, never fake data
A handful of documented endpoints don't have a backend behind them yet (see the Roadmap). Calling them always returns:
{ "code": "NOT_IMPLEMENTED", "error": "Not implemented yet" }
with HTTP 501. Thalamus Lite never fabricates a plausible-looking success response for an
unbuilt feature — if you see 501, the operation genuinely isn't live yet.