ConnectErrors

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"
}
CodeWhenFix
invalid_requestrequired param missing or malformedRead the param list in authorization-flow.md.
invalid_clientclient_id unknown or client_secret wrongCheck developer portal. Don’t ship the secret.
invalid_grantcode expired/used, or refresh token revoked, or PKCE verifier mismatchRestart the flow.
unauthorized_clientclient not allowed to use this grant typeConfidential vs public mismatch. Re-register or use the correct flow.
unsupported_grant_typegrant type other than authorization_code or refresh_tokenUse one of the two supported types.
invalid_scoperequested scope doesn’t exist, or requires a BAA you don’t haveCheck scopes.md. Apply for BAA if PHI is needed.
access_denieduser clicked Deny on the consent screenShow your own copy and offer to try again.
server_errorinternal error in identityRetry with exponential backoff. Page us if persistent.
temporarily_unavailableidentity overloadedBack off and retry.

Service call errors (after you have a token)

When you call a Loop service with an OAuth access token:

HTTPerrorMeaningFix
401invalid_tokenToken expired, revoked, or malformedRefresh and retry.
401insufficient_scopeRequired scope not presentRe-authorize with the missing scope. The WWW-Authenticate header lists it.
403forbiddenToken valid, scope present, but business rule denies (e.g., wrong brand)Don’t retry; user must change context.
404not_foundResource doesn’t exist OR isn’t visible to this userCheck the resource ID + the user’s permission.
409conflictIdempotency-key collision OR business state conflictRead body for hint; usually safe to retry with new key.
422unprocessable_entityRequest well-formed but semantically invalidRead body; fix the payload.
429rate_limitedRate cap hitHonor Retry-After.
5xxserver_errorInternalRetry 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”).