Error codes
Every failure uses one envelope. code is stable and safe to branch on; message is for
humans and may be reworded; details is either null or a small object such as
{"field": "title"}.
{
"success": false,
"error": {
"code": "SITE_EXPIRED",
"message": "This registration has expired due to inactivity.",
"details": null
},
"request_id": "req_9x2..."
}
The list
| Code | HTTP | When | What to do |
|---|---|---|---|
INVALID_REQUEST | 400 | Malformed JSON, missing field, failed validation, wrong Content-Type, bad query parameter | Read error.details.field |
INVALID_API_KEY | 401 | No Authorization header, unknown key, bad registration/sweep token | Check the key and that it has not been rotated |
SITE_NOT_FOUND | 404 | Unknown public identifier, or an Origin that does not match the registration | Re-check the identifier and origin |
SITE_EXPIRED | 403 | No successful publish within the inactivity window | POST /v1/sites/reactivate |
SITE_SUSPENDED | 403 | Key revoked; operator intervention required | Contact the operator |
SITE_BLOCKED | 403 | The website was submitted for registration more than REGISTRATION_ATTEMPT_LIMIT times (4 by default) and is blocked permanently | Only an operator can unblock it — POST /v1/internal/sites/unblock. Use a different website address for a new account |
INVALID_SUBSCRIPTION | 400 | Malformed endpoint, p256dh or auth | Re-subscribe from the browser |
PUSH_ENDPOINT_NOT_ALLOWED | 400 | Endpoint host missing from ALLOWED_PUSH_HOSTS | Add the host, or leave the variable unset to use the default list |
NOTIFICATION_PAYLOAD_TOO_LARGE | 413 | Body over 32 KB or the serialised payload over MAX_PUSH_PAYLOAD_BYTES | Shorten body, data, image |
RATE_LIMIT_EXCEEDED | 429 | Window budget exhausted for the scope | Honour retry-after |
PUSH_DELIVERY_FAILED | 502 | Reserved for a publish request that cannot be handed to any push service at all | Check the delivery attempts on the notification |
INTERNAL_SERVER_ERROR | 500 | Unexpected exception; details are logged, never returned | Report the request_id |
NOT_FOUND | 404 | Unknown route or documentation page | See GET /v1 |
METHOD_NOT_ALLOWED | 405 | Known path, wrong verb; allow header lists the valid ones | Adjust the method |
NOTIFICATION_NOT_FOUND | 404 | Unknown notification id, or one belonging to another site | Check the id |
DUPLICATE_SITE | 409 | Origin already registered. details.attempts counts how many times this address has been submitted | Use the existing credentials or rotate the key — do not register again |
SITE_LIMIT_REACHED | 409 | MAX_SUBSCRIPTIONS_PER_SITE reached | Raise the cap or prune subscriptions |
REGISTRATION_DISABLED | 403 | REGISTRATION_ENABLED=false | Enable it, or register through the operator |
SERVICE_UNAVAILABLE | 503 | Required secrets or a valid VAPID configuration are missing | See Installation |
HTTP status summary
| Status | Meaning here |
|---|---|
| 200 | Success; a publish finished inline |
| 201 | Site registered, or a new subscription created |
| 202 | Notification accepted; delivery continues asynchronously |
| 204 | CORS preflight |
| 400 / 401 / 403 / 404 / 405 | Client-side problems as listed above |
| 409 | Conflict with existing state |
| 413 | Payload too large |
| 429 | Rate limited |
| 500 / 502 / 503 | Server-side problems; retry with backoff |
SDK-side error codes
The JavaScript SDK raises CreatorNotifyError with a code. API errors keep their API
code; the SDK adds MISSING_WEBSITE_ID, UNSUPPORTED, PERMISSION_DENIED,
PERMISSION_ERROR, SERVICE_WORKER_ERROR, NETWORK_ERROR,
SUBSCRIPTION_CHANGED and UNKNOWN. See the SDK reference.
Handling errors well
const response = await fetch(`${API}/v1/notifications`, { method: 'POST', headers, body });
const body = await response.json().catch(() => null);
if (!response.ok) {
const code = body?.error?.code ?? `HTTP_${response.status}`;
switch (code) {
case 'SITE_EXPIRED':
alertOnCall('Registration expired — reactivate it');
break;
case 'RATE_LIMIT_EXCEEDED':
await sleep(Number(response.headers.get('retry-after') ?? 30) * 1000);
return retry();
case 'INVALID_REQUEST':
logValidation(body.error.details);
break;
default:
logWithRequestId(response.headers.get('x-request-id'), code);
}
throw new Error(code);
}
Always log x-request-id. It is the only reliable way to correlate a user report with a Worker log
line, and it never contains secrets.