# API Errors

Standard error envelope, HTTP statuses, and the stable type and code the API and SDK share.

Every non-2xx Platform API response returns the same envelope shape.

```json
{
  "error": {
    "type": "authentication_error",
    "code": "invalid_api_key",
    "message": "API key invalid",
    "param": null,
    "doc_url": "https://medblocks.com/docs/reference/errors",
    "request_id": "9c9b6f7a-8e4f-4a3b-9c1e-6f3a2d8b7c4d"
  }
}
```

Log `error.code`, `error.message`, the HTTP status, and `error.request_id`. Show patients a friendly retry message instead of raw portal or OAuth text.

## Error Fields [#error-fields]

Every error carries the same fields. The wire envelope names them in snake\_case under `error`, and the SDK exposes them as camelCase properties on the thrown `MedblocksError`. The values are identical, so branch on the same `type` and `code` either way.

| Wire (JSON)          | SDK          | Type               | Description                                                                             |
| -------------------- | ------------ | ------------------ | --------------------------------------------------------------------------------------- |
| `error.type`         | `type`       | `string`           | Broad category such as `authentication_error`, `invalid_request_error`, or `ehr_error`. |
| `error.code`         | `code`       | `string`           | Stable machine-readable code. Branch on this.                                           |
| `error.message`      | `message`    | `string`           | Human-readable explanation for developers and logs.                                     |
| `error.param`        | `param`      | `string` or `null` | Request field or parameter related to the error, when available.                        |
| `error.request_id`   | `requestId`  | `string`           | Correlation ID. Include it in support tickets.                                          |
| `error.doc_url`      | `docUrl`     | `string`           | Link to this API errors reference. Same URL for every code.                             |
| HTTP status          | `statusCode` | `number`           | The HTTP status. Read from the status line, not the JSON body.                          |
| `Retry-After` header | `retryAfter` | `number` or `null` | Seconds to wait, set on `rate_limit_error`, `null` otherwise.                           |

## HTTP Statuses [#http-statuses]

| Status | Meaning                                                                                                                                                |
| ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `400`  | Validation error, malformed request, or unsupported API version.                                                                                       |
| `401`  | Missing, invalid, or expired API key.                                                                                                                  |
| `402`  | Your plan doesn’t cover this. Either the feature isn’t part of your plan (`feature_not_enabled`), or you’ve hit your patient limit (`quota_exceeded`). |
| `403`  | API key is valid but does not have permission for the resource.                                                                                        |
| `404`  | Resource was not found in your organization.                                                                                                           |
| `409`  | Resource conflict, such as a duplicate identifier.                                                                                                     |
| `413`  | Request payload is too large.                                                                                                                          |
| `429`  | Rate limit or quota exceeded.                                                                                                                          |
| `500`  | Unexpected Medblocks server error. Retry with backoff.                                                                                                 |
| `502`  | Upstream EHR, OAuth, email, token, or storage service failed.                                                                                          |

## Error Codes [#error-codes]

`type` is the broad category, `code` is the exact reason, and each row lists the HTTP status the response carries. Branch on `code`, the same string in the wire envelope and the SDK.

| Type                    | Code                         | HTTP  | Typical Cause                                                                  |
| ----------------------- | ---------------------------- | ----- | ------------------------------------------------------------------------------ |
| `authentication_error`  | `not_authenticated`          | `401` | The request is not authenticated.                                              |
| `authentication_error`  | `invalid_credentials`        | `401` | Login or credential verification failed.                                       |
| `authentication_error`  | `session_expired`            | `401` | A user session expired.                                                        |
| `authentication_error`  | `missing_api_key`            | `401` | `Authorization` header is missing.                                             |
| `authentication_error`  | `invalid_api_key`            | `401` | API key is invalid.                                                            |
| `authentication_error`  | `expired_api_key`            | `401` | API key is expired.                                                            |
| `permission_error`      | `forbidden`                  | `403` | Authenticated caller cannot access the operation.                              |
| `permission_error`      | `org_access_denied`          | `403` | Caller is not a member of the active organization.                             |
| `permission_error`      | `role_insufficient`          | `403` | User role is not allowed to perform the operation.                             |
| `permission_error`      | `patient_access_denied`      | `403` | Patient session is required or invalid.                                        |
| `permission_error`      | `scope_violation`            | `403` | Resource is outside the active organization.                                   |
| `permission_error`      | `insufficient_scope`         | `403` | API key lacks the required scope.                                              |
| `invalid_request_error` | `bad_request`                | `400` | Request is malformed or missing required fields.                               |
| `invalid_request_error` | `invalid_data`               | `400` | Request data failed validation.                                                |
| `invalid_request_error` | `payload_too_large`          | `413` | Request body is too large.                                                     |
| `invalid_request_error` | `invalid_image`              | `400` | Image data or format is invalid.                                               |
| `invalid_request_error` | `unsupported_api_version`    | `400` | `Version` header is not supported.                                             |
| `invalid_request_error` | `oauth_state_expired`        | `400` | OAuth state expired before callback completion.                                |
| `not_found_error`       | `resource_not_found`         | `404` | Patient, patient session, or source was not found.                             |
| `conflict_error`        | `resource_conflict`          | `409` | Request conflicts with existing data.                                          |
| `conflict_error`        | `already_linked`             | `409` | Resource is already linked to another entity.                                  |
| `conflict_error`        | `external_id_already_exists` | `409` | External identifier already exists in the organization.                        |
| `rate_limit_error`      | `throttled`                  | `429` | Too many requests.                                                             |
| `rate_limit_error`      | `quota_exceeded`             | `429` | API key quota is exceeded.                                                     |
| `rate_limit_error`      | `api_key_limit_exceeded`     | `429` | Organization has reached its active API key cap. Revoke an existing key first. |
| `billing_error`         | `feature_not_enabled`        | `402` | Workspace plan does not include the requested feature.                         |
| `billing_error`         | `quota_exceeded`             | `402` | Free-tier active-patient limit has been reached.                               |
| `ehr_error`             | `fhir_error`                 | `502` | Upstream FHIR service failed.                                                  |
| `ehr_error`             | `oauth_error`                | `502` | Upstream OAuth provider failed.                                                |
| `ehr_error`             | `email_error`                | `502` | Email delivery failed.                                                         |
| `ehr_error`             | `token_exchange_failed`      | `502` | Medblocks could not exchange an OAuth code for tokens.                         |
| `ehr_error`             | `token_unavailable`          | `502` | No valid token is available for the upstream service.                          |
| `ehr_error`             | `storage_error`              | `502` | Cloud storage operation failed.                                                |
| `api_error`             | `internal_error`             | `500` | Unexpected Medblocks server error.                                             |
| `api_error`             | `db_error`                   | `500` | Database operation failed.                                                     |
| `api_error`             | `config_missing`             | `500` | Required server configuration is missing.                                      |

## Handling Errors [#handling-errors]

The SDK throws a single `MedblocksError` for every failed request. Discriminate on `err.type` and `err.code`, the same strings the API returns. Use the `isMedblocksError` guard to tell an API error apart from a native or network failure, then re-throw what the guard rejects.

```ts title="handle-errors.ts"
import { isMedblocksError } from "medblocks";

try {
  await mb.patientSession.init(input);
} catch (err) {
  if (!isMedblocksError(err)) throw err; // a native error, such as a network failure
  if (err.type === "authentication_error") {
    // 401, alert on a bad or expired key
  } else if (err.type === "invalid_request_error") {
    console.error("invalid", { param: err.param, code: err.code });
  } else if (err.type === "rate_limit_error") {
    // 429, wait err.retryAfter seconds
  } else {
    console.error("medblocks error", { code: err.code, requestId: err.requestId });
  }
}
```

A `type` this SDK version predates still arrives as a `MedblocksError`, so the final `else` catches it and a future category never slips past `isMedblocksError`. A transport failure that outlives the SDK’s retries is the exception. A dropped connection, a DNS failure, or a timeout surfaces as the runtime’s own error, such as a `TypeError` or an abort, not a `MedblocksError`, which is why the example re-throws it.

Log the request id on every error path. It’s the most useful field in a support ticket, because it lets us find your exact call.

```ts title="log-errors.ts"
import { isMedblocksError } from "medblocks";

catch (err) {
  if (isMedblocksError(err)) {
    logger.error("medblocks api error", {
      type: err.type,
      code: err.code,
      requestId: err.requestId,
      statusCode: err.statusCode,
      param: err.param,
    });
  }
  throw err;
}
```

`MedblocksSignatureError` is separate. It comes from webhook verification, never from an API call, and has no HTTP status. See [Webhook Signature Errors](#webhook-signature-errors).

## Retries [#retries]

The SDK retries transient failures for you before it throws. That covers rate limits (`429`), upstream errors (`502`, `503`, `504`), and network-level failures. It doesn’t retry other `4xx` errors or `500`, since retrying those won’t help. By the time an error reaches your `catch`, it has already been retried up to `maxNetworkRetries` times, three by default.

On a `429`, the SDK honors the server’s `Retry-After` during its own retries. If those run out, the final `MedblocksError` still carries `retryAfter` for your own backoff.

## Webhook Signature Errors [#webhook-signature-errors]

`MedblocksSignatureError` is not an API response error. `Medblocks.webhooks.constructEvent` throws it while verifying a webhook delivery, entirely client-side, so it has no HTTP status, no envelope, and no `doc_url`. Read `err.reason` to tell the failure modes apart, and return `400` on any of them.

The full reason table and a `constructEvent` handler live on the [webhook signatures](/docs/webhooks/signatures) page.

## Related Articles [#related-articles]

* [API Overview](/docs/reference/conventions) for authentication and versioning.
* [Pagination](/docs/patient-access/get-the-data/pagination) for cursor paging on list endpoints.
* [Medblocks-hosted page](/docs/patient-access/create-patient-session/medblocks-hosted-page) for the hosted patient session flow.
* [Your own UI](/docs/patient-access/create-patient-session/your-own-ui) for source search and return URL handling.
* [Generated API Reference](/docs/reference/api) for exact endpoint schemas.

## Need help?

Running into an issue or have a question the docs do not answer? Get support from the Medblocks team through https://forms.gle/dtVxkmfL6Yimp6SBA and we will get back to you.
