Skip to main content
All AI API errors share a single envelope shape. The HTTP status code and error.code are the two identifiers to branch on — error.message is human-readable and may change without notice.

Error envelope

{
  "success": false,
  "error": {
    "code": "PHI_BLOCKED",
    "type": "forbidden",
    "message": "Message contains PHI that cannot be sent without redaction.",
    "param": "messages[0].content",
    "details": {
      "entity_types": ["US_SSN", "PERSON"]
    },
    "retryable": false,
    "request_id": "req_01JQREQ000000000000000000",
    "docs_hint": "https://docs.usehasp.com/ai-api/concepts/phi-guard"
  },
  "meta": {
    "request_id": "req_01JQREQ000000000000000000"
  }
}
request_id appears in both error and meta — the duplication is intentional for convenience when parsing either field.

Error codes by HTTP status

400 — invalid_request

CodeDescription
VALIDATION_FAILEDRequest body failed validation. error.param names the offending field.
PHI_IN_TOOL_SCHEMAA tool definition contains PHI. Tool schemas are stored in plaintext at the provider — this is always rejected regardless of phi_mode.
UNSUPPORTED_PARAMETERA parameter not in Anthropic’s wire format was sent to /v1/messages.

401 — unauthorized

CodeDescription
INVALID_API_KEYBearer token is missing, malformed, or revoked.

402 — payment_required

CodeDescription
BAA_REQUIREDNo active Business Associate Agreement on the org. Sign at Settings → Compliance.
AI_CREDITS_EXHAUSTEDOrg has used its full credit allotment this cycle. error.details.cycle_reset_at shows when credits reset.

403 — forbidden

CodeDescription
MISSING_SCOPEAPI key exists but lacks the required scope. error.details.required_scope names the missing scope.
PHI_BLOCKEDMessage contains PHI and phi_mode=block is configured. error.details.entity_types lists what was detected.
FEATURE_NOT_HIPAA_ELIGIBLEThe requested AI feature is not HIPAA-eligible and is disabled in the allowlist.
OPUS_NOT_ENABLEDOpus model access is disabled for this org. Enable at Settings → Billing → Model Access.

404 — not_found

CodeDescription
RESOURCE_NOT_FOUNDThe requested resource does not exist or belongs to another org.

410 — gone

CodeDescription
MODEL_RETIREDThe specified model has been retired. error.details includes retired_at, recommended_replacement, and docs_url. HASP provides at least 90 days notice before retiring a model.

422 — unprocessable_entity

CodeDescription
UNSUPPORTED_DOCUMENT_TYPEThe uploaded document format is not supported.

429 — rate_limited / budget_exceeded

CodeDescription
RATE_LIMITEDRPM or daily request limit exceeded. Check the Retry-After header and error.details.retry_after_seconds. error.retryable: true.
BUDGET_EXCEEDEDOrg-configured spend cap reached. error.details includes cycle_reset_at, current_spend_usd, cap_usd. Retryable after the cap is raised or the cycle resets.

502 — provider_error

CodeDescription
INFERENCE_UPSTREAM_FAILUREThe upstream model provider returned an error. error.retryable: true.

503 — service_unavailable

CodeDescription
INFERENCE_UNAVAILABLEThe AI inference gateway is temporarily unavailable. Retry with backoff.
DOCUMENT_STILL_PROCESSINGDocument ingestion is in progress — use webhooks instead of polling.

500 — internal_error

CodeDescription
INTERNAL_ERRORUnexpected server error. If this persists, contact support with the request_id.

Streaming errors

On SSE streams, errors after the stream has opened are delivered as events rather than HTTP status codes:
event: error
data: {"type":"error","data":{"error":{"code":"RATE_LIMITED","message":"...","retryable":true,"retry_after_seconds":12}}}
Run-scoped errors use the failure-shape envelope on run.failed (includes both data.object and data.error). The standalone error event fires for errors that occur before a run could be created.

Handling retryable errors

async function chatWithRetry(message, maxRetries = 3) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    const response = await fetch('https://api.usehasp.com/v1/ai/chat', {
      method: 'POST',
      headers: { 'Authorization': 'Bearer hasp_key_live_...', 'Content-Type': 'application/json' },
      body: JSON.stringify({ message, stream: false }),
    });

    const body = await response.json();

    if (body.success) return body;

    if (!body.error.retryable || attempt === maxRetries) throw new Error(body.error.message);

    const delay = body.error.details?.retry_after_seconds
      ? body.error.details.retry_after_seconds * 1000
      : Math.min(1000 * 2 ** attempt, 30000);

    await new Promise(resolve => setTimeout(resolve, delay));
  }
}