API ReferenceError reference

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.

CodeHTTPMeaningFix
invalid_request400Required param missing or malformedCheck the param list in authorization-flow
invalid_client401client_id unknown or client_secret wrongVerify credentials in the developer portal
invalid_grant400Code expired/used, or refresh token revoked, or PKCE verifier mismatchRestart the flow from /authorize
unauthorized_client400Client not allowed to use this grant typeConfidential vs public mismatch — re-register
unsupported_grant_type400Grant type other than authorization_code or refresh_tokenUse one of the two supported types
unsupported_response_type400response_type other than codeOnly code is supported
invalid_scope400Requested scope doesn’t exist, OR requires BAA you don’t haveCheck scopes; apply for BAA if needed
access_denied302 (redirect)User clicked Deny on the consent screenShow your own copy, offer to try again
server_error500Internal error in identityRetry with exponential backoff
temporarily_unavailable503Identity overloadedBack off and retry

Token-protected endpoint errors (RFC 6750)

Returned by any service when the access token is missing, invalid, or insufficient.

CodeHTTPMeaningFix
invalid_token401Token expired, revoked, or malformedRefresh the token and retry once
insufficient_scope403Required scope not present on tokenRe-authorize asking for the missing scope (listed in WWW-Authenticate)

Platform-wide errors

Returned by any service for generic outcomes.

CodeHTTPMeaningFix
validation_error400Request shape didn’t match the expected schemaCheck the endpoint’s request body / parameters
unauthenticated401No bearer token, or bearer not a recognized formProvide a valid access token
forbidden403Token valid + scoped, but business rule denies (e.g., wrong brand)Don’t retry; user must change context
not_found404Resource doesn’t exist OR isn’t visible to this userCheck the resource ID + permissions
method_not_allowed405HTTP method not supported on this pathCheck the endpoint reference
conflict409Idempotency-key conflict OR business-state conflictRead body for hint; retry with a new key if applicable
unprocessable_entity422Request well-formed but semantically invalidRead body; fix the payload
rate_limited429Rate cap hitHonor Retry-After; back off + jitter
internal_error500Server-side bug or transient failureRetry with backoff; page if persistent
bad_gateway502Upstream vendor returned an unexpected responseRetry; alert if pattern
service_unavailable503Service temporarily unavailable (deploy, overload)Retry with backoff
gateway_timeout504Upstream call timed outRetry; circuit breaker may already have opened

Idempotency errors

When sending POST/PUT/DELETE with an Idempotency-Key header:

CodeHTTPMeaningFix
missing_idempotency_key400Mutation endpoint requires this headerAdd Idempotency-Key to the request
idempotency_key_payload_mismatch409Same key reused with different bodyDon’t reuse a key across distinct operations

Webhook delivery errors (3rd-party app side)

When your webhook endpoint receives a delivery:

HeaderMeaningAction
X-Loop-Signature mismatchBody was tampered or wrong secretReject 401; rotate secret if persistent
Duplicate X-Loop-Event-IdAt-least-once delivery semanticsDedupe; return 2xx
Persistent 5xx from your endpointPlatform 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 range
  • contraindication_found — protocol can’t start due to a contraindication
  • protocol_not_eligible — patient profile doesn’t meet eligibility

Payments

  • card_declined — Stripe rejected the charge
  • insufficient_funds — wallet/balance check failed
  • dispute_in_progress — payment is locked due to dispute
  • refund_window_expired — past the refund deadline

Membership

  • tier_locked — within the 4-day commission-lock window
  • subscription_not_found — no subscription exists for the user
  • winback_already_attempted — duplicate win-back trigger

Affiliates

  • commission_already_posted — duplicate commission for the same order
  • attribution_expired — outside the 400-day window
  • payout_threshold_not_met — balance below minimum payout

Comms

  • recipient_suppressed — recipient on the suppression list (bounce/complaint)
  • template_not_found — referenced template doesn’t exist
  • quota_exceeded — outbound send quota hit for this period

Identity

  • consent_required — user hasn’t granted the required scope
  • client_disabled — OAuth client has been disabled
  • baa_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