Skip to main content

Errors

Error envelope

Failed requests return a JSON object with an error message, and occasionally a machine-readable code:

{ "error": "hand is required" }
{ "error": "Already subscribed", "code": "ALREADY_SUBSCRIBED" }

The code field is currently used by the billing endpoints (see below). Treat error as a human-readable string that may change; branch on status and code, not on message text.

HTTP status codes

StatusMeaningTypical causes
200 OKSuccess.Reads, and writes that don't create a resource.
201 CreatedResource created.Assessment submissions.
204 No ContentSuccess, empty body.TTS produced no audio.
400 Bad RequestValidation failed.Missing required field, bad code format, invalid email/plan.
401 UnauthorizedMissing/invalid token on a required route.Expired/absent JWT.
403 ForbiddenAuthenticated but insufficient role, or session mismatch.Non-provider hitting provider routes.
404 Not FoundResource doesn't exist or expired.Unknown code, expired anonymous record.
500 Internal Server ErrorUnhandled server error.DB failure, unexpected exception.
502 Bad GatewayUpstream dependency failed.Biomechanics (Python) or Stripe error.
503 Service UnavailableFeature not configured.Billing called without Stripe keys.

Machine-readable codes

codeEndpointMeaning
ALREADY_SUBSCRIBEDPOST /billing/checkoutThe user already holds this tier or higher.
USE_PORTAL_TO_UPGRADEPOST /billing/checkoutCore→Plus upgrade must go through the customer portal.

Notable per-domain behaviors

  • Biomechanics routes wrap upstream failures from the Python analysis service as 502 with { "error": "Biomechanics server error", "message": "..." }.
  • Billing returns 503 { "error": "Billing not configured" } when Stripe keys are absent, and 502 { "error": "Billing service error: ..." } on Stripe API failures.
  • Lookup email only includes the underlying failure detail when NODE_ENV !== 'production'; in production it returns a generic message.
  • Validation is largely hand-rolled in the route handlers (not Zod-enforced at the boundary). Required fields are checked explicitly and return 400 with a specific message.

Handling errors with the SDK

The SDK converts every failure into a typed exception:

import { DigitsApiError, DigitsConnectionError } from '@digits/clinical-sdk';

try {
  await digits.billing.checkout('plus');
} catch (err) {
  if (err instanceof DigitsApiError) {
    // HTTP-level failure
    switch (err.code) {
      case 'ALREADY_SUBSCRIBED':   return showManageBilling();
      case 'USE_PORTAL_TO_UPGRADE': return openPortal();
    }
    if (err.isAuthError) return redirectToLogin();   // 401 / 403
    if (err.isNotFound)  return show404();            // 404
    if (err.isServerError) return retryLater();       // 5xx
    console.error(err.status, err.message, err.body);
  } else if (err instanceof DigitsConnectionError) {
    // Network failure / timeout / aborted — never reached the server
    console.error('Connection problem:', err.message, err.cause);
  } else {
    throw err;
  }
}

DigitsApiError fields: status, code?, message, body (parsed response), request ({ method, path }), plus the getters isAuthError, isNotFound, isServerError.

Retries

The SDK automatically retries idempotent requests (GET, or calls explicitly flagged idempotent) on 5xx responses and connection errors, using exponential backoff with jitter (maxRetries, default 2). POST writes are not retried automatically to avoid creating duplicate records — handle those failures in your own logic if a retry is safe.