Error reference
Every error the platform can return, in one place.
The error envelope (every JSON error):
{
"error": "<code>",
"error_description": "<human-readable message>",
"error_uri": "https://platform.loop.health/reference/errors#<code>"
}OAuth flow errors (RFC 6749 §5.2)
Returned by /v1/oauth/authorize and /v1/oauth/token.
| Code | HTTP | Meaning | Fix |
|---|---|---|---|
invalid_request | 400 | Required param missing or malformed | Check the param list in authorization-flow |
invalid_client | 401 | client_id unknown or client_secret wrong | Verify credentials in the developer portal |
invalid_grant | 400 | Code expired/used, or refresh token revoked, or PKCE verifier mismatch | Restart the flow from /authorize |
unauthorized_client | 400 | Client not allowed to use this grant type | Confidential vs public mismatch — re-register |
unsupported_grant_type | 400 | Grant type other than authorization_code or refresh_token | Use one of the two supported types |
unsupported_response_type | 400 | response_type other than code | Only code is supported |
invalid_scope | 400 | Requested scope doesn’t exist, OR requires BAA you don’t have | Check scopes; apply for BAA if needed |
access_denied | 302 (redirect) | User clicked Deny on the consent screen | Show your own copy, offer to try again |
server_error | 500 | Internal error in identity | Retry with exponential backoff |
temporarily_unavailable | 503 | Identity overloaded | Back off and retry |
Token-protected endpoint errors (RFC 6750)
Returned by any service when the access token is missing, invalid, or insufficient.
| Code | HTTP | Meaning | Fix |
|---|---|---|---|
invalid_token | 401 | Token expired, revoked, or malformed | Refresh the token and retry once |
insufficient_scope | 403 | Required scope not present on token | Re-authorize asking for the missing scope (listed in WWW-Authenticate) |
Platform-wide errors
Returned by any service for generic outcomes.
| Code | HTTP | Meaning | Fix |
|---|---|---|---|
validation_error | 400 | Request shape didn’t match the expected schema | Check the endpoint’s request body / parameters |
unauthenticated | 401 | No bearer token, or bearer not a recognized form | Provide a valid access token |
forbidden | 403 | Token valid + scoped, but business rule denies (e.g., wrong brand) | Don’t retry; user must change context |
not_found | 404 | Resource doesn’t exist OR isn’t visible to this user | Check the resource ID + permissions |
method_not_allowed | 405 | HTTP method not supported on this path | Check the endpoint reference |
conflict | 409 | Idempotency-key conflict OR business-state conflict | Read body for hint; retry with a new key if applicable |
unprocessable_entity | 422 | Request well-formed but semantically invalid | Read body; fix the payload |
rate_limited | 429 | Rate cap hit | Honor Retry-After; back off + jitter |
internal_error | 500 | Server-side bug or transient failure | Retry with backoff; page if persistent |
bad_gateway | 502 | Upstream vendor returned an unexpected response | Retry; alert if pattern |
service_unavailable | 503 | Service temporarily unavailable (deploy, overload) | Retry with backoff |
gateway_timeout | 504 | Upstream call timed out | Retry; circuit breaker may already have opened |
Idempotency errors
When sending POST/PUT/DELETE with an Idempotency-Key header:
| Code | HTTP | Meaning | Fix |
|---|---|---|---|
missing_idempotency_key | 400 | Mutation endpoint requires this header | Add Idempotency-Key to the request |
idempotency_key_payload_mismatch | 409 | Same key reused with different body | Don’t reuse a key across distinct operations |
Webhook delivery errors (3rd-party app side)
When your webhook endpoint receives a delivery:
| Header | Meaning | Action |
|---|---|---|
X-Loop-Signature mismatch | Body was tampered or wrong secret | Reject 401; rotate secret if persistent |
Duplicate X-Loop-Event-Id | At-least-once delivery semantics | Dedupe; return 2xx |
| Persistent 5xx from your endpoint | Platform retries with backoff (1m / 5m / 30m / 2h / 12h) | Fix endpoint; events go to DLQ after final retry |
Domain-specific errors
Each service exports its own typed error codes from services/<svc>/src/errors.ts. Common patterns:
Clinical
biomarker_out_of_range— value falls outside reference rangecontraindication_found— protocol can’t start due to a contraindicationprotocol_not_eligible— patient profile doesn’t meet eligibility
Payments
card_declined— Stripe rejected the chargeinsufficient_funds— wallet/balance check faileddispute_in_progress— payment is locked due to disputerefund_window_expired— past the refund deadline
Membership
tier_locked— within the 4-day commission-lock windowsubscription_not_found— no subscription exists for the userwinback_already_attempted— duplicate win-back trigger
Affiliates
commission_already_posted— duplicate commission for the same orderattribution_expired— outside the 400-day windowpayout_threshold_not_met— balance below minimum payout
Comms
recipient_suppressed— recipient on the suppression list (bounce/complaint)template_not_found— referenced template doesn’t existquota_exceeded— outbound send quota hit for this period
Identity
consent_required— user hasn’t granted the required scopeclient_disabled— OAuth client has been disabledbaa_required— PHI scope requested without a signed BAA
How to handle errors in code
import { LoopError } from "@platform/sdk";
try {
await loop.clinical.listBiomarkers({ userId });
} catch (err) {
if (err instanceof LoopError) {
switch (err.code) {
case "invalid_token":
await loop.refresh();
return retry();
case "insufficient_scope":
return promptUserToReauthorize(err.requiredScope);
case "rate_limited":
return scheduleRetry(err.retryAfterMs);
case "not_found":
return showEmptyState();
default:
throw err;
}
}
throw err;
}What error responses never include
- The user’s password (we never have it)
- Other users’ data
- Stack traces or file paths from inside the platform
- Hints that would help an attacker — we never say “user exists but wrong password,” only
invalid_grant
Related
- Auth model — token + scope flow
- Rate limits and circuit breakers
- Idempotency and retries
- Webhooks