RoomScope

Last updated

Errors

Every non-2xx response is a JSON envelope with the same four fields. Branch on code, show message to humans, log requestId, and retry only when retryable is true.

The envelope

429 Too Many Requests
{  "code": "rate_limited",  "message": "Too many requests for this workspace. Retry after 12 seconds.",  "requestId": "req_01J9X4R7Q2M8V3K6N5P1T0ZWAB",  "retryable": true}
FieldTypeMeaning
codestringStable, machine-readable identifier in snake_case. Branch on this, never on message.
messagestringHuman-readable explanation in English. Safe to show to an operator; wording may change.
requestIdstringUnique id of the request. Include it when you contact support; it is the only way we can find the request in our logs.
retryablebooleantrue when the same request may succeed later without changes (rate limits, transient failures). Everything else needs a different request.
Status codes still matter
The HTTP status is the coarse class (4xx yours, 5xx ours); code is the fine reason. A client that only looks at the status loses the distinction between an empty wallet (402) and a missing scope (403).

Handling errors

The pattern below is what the RoomScope dashboard itself uses: parse the envelope, fall back to http_error when the body is not JSON, and retry with exponential backoff only when the server marks the error retryable.

class ApiError extends Error {  constructor(status, { code, message, requestId, retryable }) {    super(message);    Object.assign(this, { status, code, requestId, retryable: Boolean(retryable) });  }} async function call(path, init) {  const res = await fetch(`https://api.roomscope.io${path}`, init);  if (res.ok) return res.status === 204 ? undefined : res.json();  let envelope = {};  try { envelope = await res.json(); } catch {}  throw new ApiError(res.status, { code: "http_error", message: `Request failed (${res.status})`, ...envelope });} // Retry only when the server says so.async function withRetry(fn, attempts = 3) {  for (let i = 0; ; i++) {    try { return await fn(); }    catch (e) {      if (!(e instanceof ApiError) || !e.retryable || i === attempts - 1) throw e;      await new Promise((r) => setTimeout(r, 500 * 2 ** i));    }  }}

Codes by status

The status column is stable. The code names below describe the current behaviour and are marked Subject to change until the OpenAPI document publishes the final list.

StatusCode Subject to changeWhenRetryable
400validation_errorA parameter is missing or malformed. The message names the field.no
401unauthorizedMissing, malformed, revoked or expired API key.no
402payment_requiredThe wallet is empty, or the plan does not allow this read.no
403forbiddenThe key lacks the scope, or the plan does not include the resource.no
404not_foundNo such resource (creator, session, rule, …).no
409conflictThe write collides with the current state (duplicate rule, stale version).no
429rate_limitedToo many requests. Honour Retry-After.yes
5xxinternal_errorSomething failed on our side. Retry with backoff; include the requestId when you report it.yes

Rate limits have their own page: Rate limits.

Was this page helpful?