Skip to content

Recipe

Handling error responses

The API answers with a flat `_error` field — how to recognise and handle the 400 / 401 / 403 / 404 / 429 statuses.

Beginner~20 min5 steps

Prerequisites

  • You know the QUiCK API request/response basics
  • You have an integration that submits or reads data

1. The shape of an error response

The QUiCK Public API returns a flat error object with a single _error text field. There is no per-field details array and no error-code field:

json
{
  "_error": "Authentication credentials were not provided."
}

The message is meant for humans; branch your code on the HTTP status code, not on the text.

2. What the status codes mean

StatusMeaningWhat to do
400Bad request or validation errorFix the parameters or body, do not repeat unchanged
401Missing or invalid tokenCheck the Authorization: Token <token> header
403No permission, or Quick-Company-Id is not the token''s companyVerify the company scope
404No such resourceCheck the identifier and the trailing /
429Too many requestsWait, then retry with exponential backoff

3. Unified error handling in the client

Convert the response into a typed error in a single place so call sites stay clean:

ts
export class QuickApiError extends Error {
  constructor(public status: number, message: string) {
    super(message);
    this.name = "QuickApiError";
  }
  get isAuth() { return this.status === 401 || this.status === 403; }
  get isRateLimited() { return this.status === 429; }
}

export async function quickFetch(path: string, init: RequestInit = {}) {
  const res = await fetch(`https://api.quick.riport.co.hu${path}`, {
    ...init,
    headers: {
      ...init.headers,
      Authorization: `Token ${process.env.QUICK_API_TOKEN}`,
      "Content-Type": "application/json",
    },
  });

  if (!res.ok) {
    const body = await res.json().catch(() => ({}));
    throw new QuickApiError(res.status, body._error ?? res.statusText);
  }
  return res.json();
}

4. 429 — retrying with backoff

A 429 is not a failure but a signal to slow down. Retry with exponential waits and an upper bound:

ts
async function withRetry<T>(fn: () => Promise<T>, attempts = 4): Promise<T> {
  for (let i = 0; i < attempts; i++) {
    try {
      return await fn();
    } catch (err) {
      if (!(err instanceof QuickApiError) || !err.isRateLimited || i === attempts - 1) throw err;
      await new Promise((r) => setTimeout(r, 2 ** i * 1000));
    }
  }
  throw new Error("unreachable");
}

Persistent 429s

If you hit 429 regularly, the fix is not more retries but less frequent polling and a larger page_size.

5. Showing errors to the user

Because there is no field-level error list, show a 400 message at the top of the form, not next to a specific field — otherwise you mislead the user.

ts
try {
  await createExpense(files);
} catch (err) {
  if (err instanceof QuickApiError && err.status === 400) {
    setFormError(err.message);
    return;
  }
  throw err;
}

Log the detailed cause (status + _error + request id) on the server side.

Related endpoints

  • POST/v1/invoices
  • PATCH/v1/invoices/{id}

Related terms

Common pitfalls

Expecting a 422 status

The QUiCK Public API returns `400` for validation errors, not `422`. If you only handle 422, a real error slips silently into the success path.

Trying to parse a per-field error list

There is no `error.details[]` structure — a single `_error` text arrives. Form error mapping built on `details` will silently do nothing.

Branching on the error message text

The contents of `_error` are meant for humans and may change. Always build logic on the HTTP status code.

Retrying immediately on 429

Repeating the request in a tight loop loads the API further and can lock you out for longer. Use exponential backoff.