Flavoso APIFundamentalsErrors
Errors
Every error has the same shape. Here are all codes with their HTTP status and what your program should do.
The shape
An error is always an error object with code, message and request_id, for invalid_request often with param as well:
{
"error": {
"code": "invalid_request",
"message": "price must be an integer amount in cents.",
"request_id": "req_4f0c2a9d1e7b3c5a8f6d2e10",
"param": "products[3].price"
}
}codeis the contract: your program can rely on it, it is never reworded.messageis an English hint for developers and may change. Do not show it to the restaurant.request_idis also in theX-Request-Idheader and lets you find the request in the log.paramnames the faulty field, even deep in the body:products[3].price.
All codes
| Code | HTTP | Meaning | What your program does |
|---|---|---|---|
invalid_request | 400 | A parameter or the body is wrong: a required field is missing, a type is wrong, the JSON is invalid, or the request contains a field or query parameter the operation does not know. param names the field, if there is one. | Do not retry unchanged. Fix the request using param and message. |
| 401 | No token, an expired or revoked token, the connection no longer exists, or the app is suspended. | Get a new access token with the refresh token and retry once. If that fails with invalid_grant, the connection no longer exists; with unauthorized_client the app is suspended: keep the tokens and try again later. | |
account_inactive | 403 | The restaurant's subscription is not active. GET /me and DELETE /connection still work. | Do not retry. Try again later or tell the restaurant. |
insufficient_scope | 403 | The connection lacks the scope for this operation. WWW-Authenticate names it. | Ask the owner to connect your app again with this scope. Until then, switch the feature off. |
plan_required | 403 | The restaurant's plan does not include the feature, for example reservations on the Starter plan. | Switch the feature off for this restaurant and show the user why. |
not_found | 404 | The object does not exist, or not in this restaurant. Also returned for an unknown path. | Check the id and the path. Remove a deleted object from your store. |
method_not_allowed | 405 | This method does not exist on this path. | Compare method and path with the reference. |
conflict | 409 | The same data was changed elsewhere at the same moment, for example by the restaurant in the dashboard, three times in a row. The request itself is fine. | Wait Retry-After seconds and send the same request again, unchanged, with the same Idempotency-Key. |
external_id_taken | 409 | Your app already uses this external_id for another object of the same kind in this restaurant. | Look up with ?external_id= which object carries it, and fix the mapping in your system. |
idempotency_in_progress | 409 | A first request with the same Idempotency-Key is still running. | Wait a moment and retry with the same key. |
invalid_transition | 409 | This status change is not possible, for example moving back or out of a final status. For reservations, message lists the statuses possible right now. | Read the object again: it was usually changed somewhere else already. Then decide. |
| 409 | The time is not a bookable time of the day or lies in the past, or at that time there is no room for this party size or these tables. | Get free times with GET /reservations/availability and offer one of them. | |
stage_not_used | 409 | The restaurant does not use this status, for example preparing. GET /store lists the ones it uses under order_statuses. | Skip the step and move to the next status. |
payload_too_large | 413 | The body is larger than the operation accepts, counted in bytes: 64 KB, 2 MB for PUT /menu. | Split the request, for example the menu into several syncs with merge. |
idempotency_key_reused | 422 | The same Idempotency-Key came with a different request (method, path or body). | Use a new key for every new operation. Look for the bug in your program. |
limit_exceeded | 422 | A limit is exceeded, for example too many products in one sync. | Split or shorten the request. The limits are listed in the guide for each case. |
rate_limited | 429 | More than 120 requests per minute for this connection. | Wait Retry-After seconds, then carry on. Spread your requests over time. |
server_error | 500 | Something went wrong on Flavoso's side. | Retry with increasing delays, for POST with the same Idempotency-Key. If it persists, send the request_id to support. |
Handling errors
An error tells you whether retrying helps. 401 means: refresh the token. 403 means: tell the owner, do not retry. 409 mostly means: the object changed somewhere else, so read it again. Any other 4xx means: the request is wrong, so do not retry it unchanged.
export class FlavosoError extends Error {
constructor(status, error) {
super(`${status} ${error.code}: ${error.message}`);
Object.assign(this, { status, code: error.code, param: error.param, requestId: error.request_id });
}
}
export async function flavoso(conn, method, path, { body, idempotencyKey } = {}) {
const res = await fetch(`https://api.flavoso.com/v1${path}`, {
method,
headers: {
Authorization: `Bearer ${await accessTokenFor(conn)}`,
...(body ? { "Content-Type": "application/json" } : {}),
...(idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
const data = await res.json();
if (res.ok) return data;
const err = new FlavosoError(res.status, data.error);
switch (err.code) {
case "unauthorized": // expired or revoked: refresh once, then give up
conn.expiresAt = 0;
break;
case "insufficient_scope": // ask the owner to connect again with this scope
case "plan_required": // the restaurant's plan does not include this
case "account_inactive": // the restaurant's subscription is not active
await flagConnection(conn, err.code);
break;
}
throw err; // log err.requestId: support can find the request with it
}When to retry
| Case | Retry? |
|---|---|
| Network error, timeout | Yes, with the same Idempotency-Key. |
429 rate_limited | Yes, after Retry-After seconds. |
500 server_error, 502, 503, 504 | Yes, with increasing delays. |
409 idempotency_in_progress, 409 conflict | Yes, after a short pause (Retry-After), unchanged. |
401 unauthorized | Once, after refreshing the token. |
| 400, 403, 404, 413, 422, other 409 | No, not unchanged. |
const RETRYABLE = new Set([429, 500, 502, 503, 504]);
// send() makes the SAME request every time, with the same Idempotency-Key.
export async function withRetries(send, attempts = 5) {
for (let attempt = 1; ; attempt++) {
let res = null;
try {
res = await send();
} catch (err) {
if (attempt >= attempts) throw err; // network error: outcome unknown, so retry
}
if (res && (!RETRYABLE.has(res.status) || attempt >= attempts)) return res;
const retryAfter = Number(res?.headers.get("retry-after"));
const backoff = Math.min(30, 2 ** attempt) * (0.5 + Math.random() / 2);
await new Promise((r) => setTimeout(r, (retryAfter > 0 ? retryAfter : backoff) * 1000));
}
}With support
Write to support@flavoso.com and include the request_id (or X-Request-Id), plus the time and the operation. That lets us find the request. The same id is in your request log.
Still unclear? Write to support@flavoso.com and include the id from X-Request-Id. It lets us find your request in the log.