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"
  }
}
  • code is the contract: your program can rely on it, it is never reworded.
  • message is an English hint for developers and may change. Do not show it to the restaurant.
  • request_id is also in the X-Request-Id header and lets you find the request in the log.
  • param names the faulty field, even deep in the body: products[3].price.

All codes

CodeHTTPMeaningWhat your program does
invalid_request400A 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.
unauthorized401No 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_inactive403The restaurant's subscription is not active. GET /me and DELETE /connection still work.Do not retry. Try again later or tell the restaurant.
insufficient_scope403The 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_required403The 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_found404The 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_allowed405This method does not exist on this path.Compare method and path with the reference.
conflict409The 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_taken409Your 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_progress409A first request with the same Idempotency-Key is still running.Wait a moment and retry with the same key.
invalid_transition409This 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.
slot_unavailable409The 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_used409The 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_large413The 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_reused422The 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_exceeded422A 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_limited429More than 120 requests per minute for this connection.Wait Retry-After seconds, then carry on. Spread your requests over time.
server_error500Something 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
}

401 and a lost connection

On 401, get a new access token with the refresh token and retry once. If that also fails with invalid_grant, the connection no longer exists: the owner or your app disconnected, the refresh token expired or was revoked, or the restaurant is no longer active. Show this in your software and offer “Connect again”; retrying does not help here.

If the token endpoint answers unauthorized_client instead, your app is suspended, but its connections remain. Keep the tokens, show “temporarily suspended” and try again later.

When to retry

CaseRetry?
Network error, timeoutYes, with the same Idempotency-Key.
429 rate_limitedYes, after Retry-After seconds.
500 server_error, 502, 503, 504Yes, with increasing delays.
409 idempotency_in_progress, 409 conflictYes, after a short pause (Retry-After), unchanged.
401 unauthorizedOnce, after refreshing the token.
400, 403, 404, 413, 422, other 409No, 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.