WzGate
Build your own website

Errors

The two error shapes the public API returns, and a table of every code you can branch on — site resolution, keys, bot checks, viewings, validation and rate limits.

There are two error shapes, and only one of them has a code.

Domain errors carry a code

{
  "success": false,
  "statusCode": 409,
  "code": "VIEWING_SLOT_TAKEN",
  "message": "That time has just been booked — please pick another.",
  "details": { "propertyId": "…", "date": "2026-09-20", "slot": "11:00" },
  "path": "/api/public/viewing-requests",
  "method": "POST",
  "timestamp": "2026-09-14T09:30:27.019Z"
}

code is at the top level, not nested. details is optional and differs per code. Branch on code; never on message, which is written for humans and is translated over time.

Validation errors carry none

A body or query string the API refuses is a 400 with an array of strings in message and no code at all:

{
  "success": false,
  "statusCode": 400,
  "message": ["property type should not exist"],
  "error": "Bad Request",
  "path": "/api/public/properties",
  "method": "GET"
}

One helper in your HTTP layer covers both:

function apiError(body: any) {
  if (body?.code) return { code: body.code as string, message: body.message };
  const message = Array.isArray(body?.message) ? body.message.join(', ') : body?.message;
  return { code: 'VALIDATION_FAILED', message };
}

Without that check, half your forms render [object Object].

Site resolution

CodeStatusWhen
SITE_NOT_FOUND404The site is a draft, or the organization runs no site at all. Deliberately the same answer for both.
SITE_PAUSED503The site is paused in the CRM. Retry later; nothing on your side is wrong.
SITE_ORIGIN_MISMATCH403X-Site names a site that does not belong to the domain (or organization) the request came from.
PUBLIC_TENANT_REQUIRED400Nothing identified a site: no verified domain, no X-Site, no X-Subdomain.

See Identify your site.

API keys

CodeStatusWhen
SITE_KEY_REQUIRED401A write arrived with no X-Api-Key, or with a key the CRM does not know.
SITE_KEY_REVOKED401The key was revoked or rotated away.
SITE_KEY_ORIGIN_MISMATCH403A publishable key used from an origin that is not one of the site's domains, or a key belonging to a different site.

While the rollout is in monitor mode these are logged, not returned. See Keys and CORS.

Bot check

CodeStatusWhen
BOT_CHECK_REQUIRED400A protected write with a publishable key and no X-Turnstile-Token.
BOT_CHECK_FAILED403Cloudflare rejected the token — expired, reused or forged.

See Bot protection.

Viewing requests

CodeStatusWhat the UI should do
VIEWING_SLOT_INVALID400The time is not one of the site's configured slots. Re-fetch the slots.
VIEWING_DAY_CLOSED400The site takes no viewings that weekday. Re-fetch the slots.
VIEWING_SLOT_TOO_SOON400Inside the site's minimum notice period. Offer a later time.
VIEWING_SLOT_TAKEN409Somebody booked it between the page load and the submit. Re-fetch the slots and clear the chosen time.

All four mean the same thing to a visitor — "that time is gone, pick another" — so map them to one message, invalidate your cached GET /public/properties/{id}/viewing-slots, and clear the selected time. The slots endpoint already marks taken times available: false; the 409 only closes the race between two visitors.

Visitor accounts

VISITOR_TOKEN_REQUIRED, VISITOR_TOKEN_INVALID, VISITOR_EMAIL_TAKEN, VISITOR_CREDENTIALS_INVALID, VISITOR_EMAIL_NOT_VERIFIED, OTP_INVALID, OTP_EXPIRED, OTP_LOCKED, RESET_TOKEN_INVALID and friends are listed with their handling on Visitor accounts.

Not found, and caps

404s from the catalogue are uniform: PROPERTY_NOT_FOUND, PROJECT_NOT_FOUND, LOCATION_NOT_FOUND, VIEWING_REQUEST_NOT_FOUND, VISITOR_SAVED_SEARCH_NOT_FOUND, OWNER_SUBMISSION_NOT_FOUND, each with details: { id }. FAVORITES_CAP (409) is the eleventh favourite of a type.

Rate limits

A 429 carries no code — treat the status itself as the signal. Stop, back off, and tell the visitor to try again shortly; never retry in a tight loop, and never retry a write automatically at all.

See Rate limits for the ceilings.

Errors you will only meet in the CRM

These come from the staff API, not from /api/public/*. They are here because they are what you see while setting a site up.

CodeStatusSurface
DOMAIN_INVALID400Adding a domain that is not a hostname (localhost, a bare word, a URL with a path).
DOMAIN_TAKEN409The host is already attached to a site — possibly another organization's. The message never says whose.
DOMAIN_NOT_FOUND404Acting on a domain that is not on this site.
DOMAIN_NOT_VERIFIED400Making a domain primary when it was left pending by the retired TXT-record flow. Remove it and add it again.
RICH_TEXT_TOO_LONG400Saving an About or legal text over its limit (5 000 plain characters for About, 50 000 for legal). details carries { field, locale, max, actual }.
SITE_TYPE_NOT_ALLOWED400Creating or retyping a site whose type needs a module the organization does not have enabled.
SITE_LIMIT400Creating a second site without the multi-site feature.

On this page