Recipe
Handling error responses
The API answers with a flat `_error` field — how to recognise and handle the 400 / 401 / 403 / 404 / 429 statuses.
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:
{
"_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
| Status | Meaning | What to do |
|---|---|---|
400 | Bad request or validation error | Fix the parameters or body, do not repeat unchanged |
401 | Missing or invalid token | Check the Authorization: Token <token> header |
403 | No permission, or Quick-Company-Id is not the token''s company | Verify the company scope |
404 | No such resource | Check the identifier and the trailing / |
429 | Too many requests | Wait, 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:
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:
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.
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.