Errors
OAuth endpoint errors (RFC 6749 §5.2)
All errors return application/json:
{
"error": "invalid_grant",
"error_description": "Authorization code expired",
"error_uri": "https://platform.loop.health/connect/errors#invalid_grant"
}| Code | When | Fix |
|---|---|---|
invalid_request | required param missing or malformed | Read the param list in authorization-flow.md. |
invalid_client | client_id unknown or client_secret wrong | Check developer portal. Don’t ship the secret. |
invalid_grant | code expired/used, or refresh token revoked, or PKCE verifier mismatch | Restart the flow. |
unauthorized_client | client not allowed to use this grant type | Confidential vs public mismatch. Re-register or use the correct flow. |
unsupported_grant_type | grant type other than authorization_code or refresh_token | Use one of the two supported types. |
invalid_scope | requested scope doesn’t exist, or requires a BAA you don’t have | Check scopes.md. Apply for BAA if PHI is needed. |
access_denied | user clicked Deny on the consent screen | Show your own copy and offer to try again. |
server_error | internal error in identity | Retry with exponential backoff. Page us if persistent. |
temporarily_unavailable | identity overloaded | Back off and retry. |
Service call errors (after you have a token)
When you call a Loop service with an OAuth access token:
| HTTP | error | Meaning | Fix |
|---|---|---|---|
| 401 | invalid_token | Token expired, revoked, or malformed | Refresh and retry. |
| 401 | insufficient_scope | Required scope not present | Re-authorize with the missing scope. The WWW-Authenticate header lists it. |
| 403 | forbidden | Token valid, scope present, but business rule denies (e.g., wrong brand) | Don’t retry; user must change context. |
| 404 | not_found | Resource doesn’t exist OR isn’t visible to this user | Check the resource ID + the user’s permission. |
| 409 | conflict | Idempotency-key collision OR business state conflict | Read body for hint; usually safe to retry with new key. |
| 422 | unprocessable_entity | Request well-formed but semantically invalid | Read body; fix the payload. |
| 429 | rate_limited | Rate cap hit | Honor Retry-After. |
| 5xx | server_error | Internal | Retry with back-off. |
SDK errors
The SDK normalizes the above into typed LoopError instances:
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(); // SDK auto-refreshes once; this is the manual path
return retry();
case "insufficient_scope":
return promptUserToReauthorize(err.requiredScope);
case "rate_limited":
return scheduleRetry(err.retryAfterMs);
default:
throw err;
}
}
throw err;
}Using typed constants
Use the typed constants instead of string literals:
import { OAUTH_ERRORS } from "@platform/contracts";
// In your error handling
switch (err.code) {
case OAUTH_ERRORS.INVALID_TOKEN:
await loop.refresh();
return retry();
case OAUTH_ERRORS.INSUFFICIENT_SCOPE:
return promptUserToReauthorize(err.requiredScope);
}Available constants: INVALID_REQUEST, INVALID_CLIENT, INVALID_GRANT, UNAUTHORIZED_CLIENT, UNSUPPORTED_GRANT_TYPE, INVALID_SCOPE, ACCESS_DENIED, SERVER_ERROR, TEMPORARILY_UNAVAILABLE, INSUFFICIENT_SCOPE, INVALID_TOKEN, UNSUPPORTED_RESPONSE_TYPE.
What error responses never include
- The user’s password.
- Other users’ data.
- Stack traces with file paths from inside Loop.
- Hints that would help an attacker (we don’t say “user exists but wrong password” — only “invalid_grant”).