Skip to content
WrongNotebook
ProductsPersonal BaaSDocumentationQuickstart
Sign inCreate workspace
Browse documentation

Start here

  • Quickstart
  • API keys and scopes

Working with data

  • Collections
  • Records
  • Filtering, sorting and paging
  • Schemas and validation
  • Files

Integrate

  • Webhooks
  • Recipes

Reference

  • Errors
  • Limits and rate limits

Start here

  • Quickstart
  • API keys and scopes

Working with data

  • Collections
  • Records
  • Filtering, sorting and paging
  • Schemas and validation
  • Files

Integrate

  • Webhooks
  • Recipes

Reference

  • Errors
  • Limits and rate limits
  1. WrongNotebook
  2. /
  3. Docs
  4. /
  5. Errors

Errors

Every error has a code, a readable message and a request id. Here is what each one means and how to fix it.

Every failure comes back in the same shape, so you only have to handle one:

JSON
{
  "error": {
    "code": "INSUFFICIENT_SCOPE",
    "message": "This API key is not allowed to perform this action (records.create)",
    "details": { "required": "records.create" },
    "requestId": "8ce3d344-6260-4437-973e-54b82feb263b"
  }
}
  • code is stable. Branch on this, never on the message.
  • message is for a human.
  • details is there when the error has something specific to add, such as which scope was missing or which fields failed validation.
  • requestId also comes back as the X-Request-Id header. Log it — it is how you find one request later.

Database errors and stack traces are never returned. They are logged against the request id instead.

Handling them

JavaScript
async function baas(path, options) {
  const res = await fetch(`${BASE}${path}`, options);
  const body = await res.json().catch(() => null);
  if (res.ok) return body;

  const error = body?.error ?? { code: 'UNKNOWN', message: res.statusText };
  console.error('API error', { code: error.code, requestId: error.requestId });

  switch (error.code) {
    case 'SCHEMA_VALIDATION_FAILED':
      return showFieldErrors(error.details.issues); // map onto your form
    case 'VERSION_CONFLICT':
      return refetchAndRetry();
    case 'RATE_LIMITED':
      return waitAndRetry(Number(res.headers.get('retry-after') ?? 1));
    default:
      throw new Error(`${error.code}: ${error.message}`);
  }
}

The ones you will actually meet

CodeStatusWhat it meansWhat to do
VALIDATION_ERROR400The request body or query is malformedRead details; it names the field
INVALID_FILTER400The filter could not be parsedCheck the filter syntax; the message gives the position
INVALID_JSON400The body was not valid JSONUsually a missing Content-Type: application/json
UNAUTHENTICATED401No usable credentialSend Authorization: Bearer <key>
INVALID_API_KEY401The key is wrong, revoked or expiredCheck for a stray space or newline; make a new key
TOKEN_EXPIRED401A session token ran outSign in again
INSUFFICIENT_SCOPE403The key lacks a permissiondetails.required names it; create a key with that scope
FORBIDDEN403Your role does not allow itAsk a workspace owner or admin
COLLECTION_NOT_FOUND404Wrong id or slug — or not visible to youCheck the slug and workspace
RECORD_NOT_FOUND404No such record, or it is in the trashAdd ?deleted=include to look in the trash
VERSION_CONFLICT409Someone changed the record firstRe-read, re-apply, retry
SLUG_TAKEN409That collection slug is in usePick another
LIMIT_EXCEEDED409A limit was reachedDelete something, or split the work up
SCHEMA_VALIDATION_FAILED422The record does not match the schemadetails.issues lists each problem with a path
SCHEMA_MIGRATION_BLOCKED422Existing records would become invalidFix them, or allow it explicitly
PAYLOAD_TOO_LARGE413The body is over the size limitSee limits
RATE_LIMITED429Too many requestsWait for Retry-After seconds
INTERNAL_ERROR500Something broke on our sideRetry; quote the requestId if it persists

Why "not found" instead of "not allowed"

Anything you are not allowed to see returns 404, not 403. If it returned 403, that would confirm the thing exists — and someone could discover other people's collections by guessing. You get 403 only when you can see a resource but may not do that particular thing to it.

Rate limiting

A 429 reply includes Retry-After in seconds. Every reply carries your current budget:

HTTP
RateLimit-Limit: 1200
RateLimit-Remaining: 1198
RateLimit-Reset: 18

See limits for the numbers.

Retrying safely

Retry 429 and 5xx; do not retry 4xx, since the same request will fail again.

When you retry something that creates data, send the same Idempotency-Key you used the first time. The original response is replayed instead of creating a second record.

JavaScript
const key = crypto.randomUUID();
for (let attempt = 0; attempt < 3; attempt++) {
  const res = await fetch(url, {
    method: 'POST',
    headers: { ...headers, 'Idempotency-Key': key },
    body,
  });
  if (res.ok || res.status < 500) return res;
  await new Promise((r) => setTimeout(r, 2 ** attempt * 500));
}

Next steps

  • Limits — the numbers behind 413, 429 and LIMIT_EXCEEDED.
  • API keys — fixing 401 and 403.

View this page as Markdown — handy for copying into an editor or an AI assistant.

PreviousRecipesNextLimits and rate limits

On this page

  • Handling them
  • The ones you will actually meet
  • Why "not found" instead of "not allowed"
  • Rate limiting
  • Retrying safely
  • Next steps
WrongNotebook

Personal BaaS is one focused product from WrongNotebook.

Documentation

  • All pages
  • Quickstart
  • Recipes
  • Errors

Product

  • Personal BaaS
  • All products
  • View the demo

Get started

  • Sign in
  • Create workspace

© 2026 WrongNotebook. All rights reserved.