# Errors

> How the HubFit API reports errors: RFC 9457 problem details, HTTP status codes, every error code, and which requests are safe to retry.

Source: https://hubfit.com/developers/errors

## Problem details

Errors are [RFC 9457](https://www.rfc-editor.org/rfc/rfc9457) problem details, sent as `application/problem+json`. Branch on `code`, which is stable; `detail` is for people and can change.

- `title`: the HTTP status phrase.
- `status`: the HTTP status code.
- `code`: what went wrong, from the table below.
- `detail`: what went wrong, in words.
- `requestId`: this request's id, also in the `X-Request-Id` header of every response. Quote it when you contact support.
- `errors`: on `invalid_request` when the request doesn't match the spec, one entry per problem, with its `location` (`query`, `path`, `body` or `header`), `name` and `detail`.
- `requiredScope` on `insufficient_scope`, and `clientId` on `client_exists`.

**400 Bad Request (400 Bad Request)**

```json
{
  "title": "Bad Request",
  "status": 400,
  "code": "invalid_request",
  "detail": "The request didn't match the API specification. See errors.",
  "requestId": "2f8e6c1a-4b7d-4e2a-9c3f-5d1b8a7e6f40",
  "errors": [
    {
      "location": "query",
      "name": "limit",
      "detail": "must be <= 100"
    }
  ]
}
```

## Error codes

New codes can appear. Handle one you don't recognise by its HTTP status.

| Code | Status | Meaning |
| --- | --- | --- |
| `invalid_request` | 400 | A parameter or the body is invalid; `detail` says why, and `errors` lists each problem when the request doesn't match the spec. |
| `url_not_allowed` | 400 | A webhook endpoint's `url` can't receive webhooks; `detail` says why. |
| `unauthenticated` | 401 | The API key is missing, invalid, expired or revoked. |
| `insufficient_scope` | 403 | The key lacks a scope; `requiredScope` names it. |
| `plan_required` | 403 | The workspace isn't on a plan with the API. |
| `not_available` | 403 | The API isn't open to your workspace yet. |
| `account_suspended` | 403 | The workspace or the key's coach is suspended. |
| `account_restricted` | 403 | The workspace has unpaid invoices. |
| `client_limit_reached` | 403 | Your plan has no room for another client. |
| `role_required` | 403 | Only a key belonging to the workspace's owner or an admin can manage webhook endpoints. |
| `webhook_endpoint_read_only` | 403 | Another key or Settings added this webhook endpoint, so this key can't change or delete it. |
| `not_found` | 404 | It doesn't exist, or your key can't reach it. |
| `method_not_allowed` | 405 | The path exists, but not with this method. |
| `client_archived` | 409 | The client is archived. Unarchive them in HubFit to read or change anything of theirs. |
| `client_exists` | 409 | A client with this email is already in your workspace; `clientId` names them when you can reach them. |
| `already_reviewed` | 409 | The check-in submission was already reviewed. |
| `note_limit_reached` | 409 | The client already has 100 notes. |
| `messaging_disabled` | 409 | Messaging is turned off for this client. |
| `open_task_limit_reached` | 409 | The workspace has 250 open tasks. |
| `webhook_endpoint_exists` | 409 | This key already added an endpoint with this URL. |
| `webhook_endpoint_limit_reached` | 409 | The workspace has 10 webhook endpoints. |
| `webhook_endpoint_disabled` | 409 | The endpoint was turned off for good, and can't be turned back on. |
| `idempotency_key_in_use` | 409 | The first request with this `Idempotency-Key` is still running. |
| `payload_too_large` | 413 | The request body is too large. |
| `unsupported_media_type` | 415 | The body must be sent as `application/json`. |
| `idempotency_key_reused` | 422 | The `Idempotency-Key` was already used with a different request. |
| `rate_limited` | 429 | Too many requests. Retry after the seconds in `Retry-After`. |
| `internal_error` | 500 | Something failed on our side. Retry, and quote `requestId` if it keeps happening. |

## Retrying safely

- **4xx except 409 `idempotency_key_in_use` and 429:** don't retry. Fix the request first.
- **429:** wait the seconds in `Retry-After`, then retry. See [Rate limits](https://hubfit.com/developers/rate-limits).
- **5xx and network errors:** retry with exponential backoff. Send the same `Idempotency-Key` on a `POST` so a write that did go through isn't repeated. See [Idempotency](https://hubfit.com/developers/idempotency).

**Retry 429s and 5xx**

```javascript
async function hubfit(path, init = {}, attempt = 0) {
  const response = await fetch(`https://api.hubfit.com/v1${path}`, {
    ...init,
    headers: { Authorization: `Bearer ${process.env.HUBFIT_API_KEY}`, ...init.headers },
  });
  if ((response.status === 429 || response.status >= 500) && attempt < 4) {
    const wait = Number(response.headers.get("Retry-After")) || 2 ** attempt;
    await new Promise((resolve) => setTimeout(resolve, wait * 1000));
    return hubfit(path, init, attempt + 1);
  }
  return response;
}
```
