Skip to main content

Concepts

Errors

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

Errors are RFC 9457 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 Request400 Bad Request
{
  "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"
    }
  ]
}

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

CodeStatusMeaning
invalid_request400A parameter or the body is invalid; detail says why, and errors lists each problem when the request doesn't match the spec.
url_not_allowed400A webhook endpoint's url can't receive webhooks; detail says why.
unauthenticated401The API key is missing, invalid, expired or revoked.
insufficient_scope403The key lacks a scope; requiredScope names it.
plan_required403The workspace isn't on a plan with the API.
not_available403The API isn't open to your workspace yet.
account_suspended403The workspace or the key's coach is suspended.
account_restricted403The workspace has unpaid invoices.
client_limit_reached403Your plan has no room for another client.
role_required403Only a key belonging to the workspace's owner or an admin can manage webhook endpoints.
webhook_endpoint_read_only403Another key or Settings added this webhook endpoint, so this key can't change or delete it.
not_found404It doesn't exist, or your key can't reach it.
method_not_allowed405The path exists, but not with this method.
client_archived409The client is archived. Unarchive them in HubFit to read or change anything of theirs.
client_exists409A client with this email is already in your workspace; clientId names them when you can reach them.
already_reviewed409The check-in submission was already reviewed.
note_limit_reached409The client already has 100 notes.
messaging_disabled409Messaging is turned off for this client.
open_task_limit_reached409The workspace has 250 open tasks.
webhook_endpoint_exists409This key already added an endpoint with this URL.
webhook_endpoint_limit_reached409The workspace has 10 webhook endpoints.
webhook_endpoint_disabled409The endpoint was turned off for good, and can't be turned back on.
idempotency_key_in_use409The first request with this Idempotency-Key is still running.
payload_too_large413The request body is too large.
unsupported_media_type415The body must be sent as application/json.
idempotency_key_reused422The Idempotency-Key was already used with a different request.
rate_limited429Too many requests. Retry after the seconds in Retry-After.
internal_error500Something failed on our side. Retry, and quote requestId if it keeps happening.
  • 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.
  • 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.
Retry 429s and 5xx
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;
}