The Notiondesk API answers every error with the same JSON envelope and a machine-readable code, limits each key to 120 reads and 20 writes per minute, and lets you retry any change safely with an Idempotency-Key header. This article lists every Notiondesk API error code, explains the rate limits and their headers, and shows how to build retries that never change anything twice.
The Notiondesk API error envelope
Every error answers the same shape, and every field is always present:
{
"object": "error",
"code": "validation_error",
"message": "The request parameters are invalid.",
"request_id": "0f2c1e94-7a1b-4c2d-9e3f-5a6b7c8d9e0f",
"errors": [
{"field": "page_size", "message": "This value should be between 1 and 100."}
]
}| Field | Meaning |
|---|---|
object | Always error |
code | The machine-readable error. Branch your code on this field only |
message | A human-readable explanation. It can change, so never parse it |
request_id | Identifies the request when you contact support. null when the request failed before reaching a route, for example on an unknown path |
errors | Field-level problems, each with a field and a message. Empty when the error is not about a specific input |
How field names a value
- A top-level field is its name:
page_size.
- A field inside a list names the list, the entry in brackets, then the field:
items[fr].page_title.
- The bracket holds the entry's identifier as you sent it (the
localeinitems, thecategory_idinchangelog_categories) when every entry has a distinct one. Otherwise it holds the entry's zero-based position:items[0].locale.
- A problem with the request as a whole has
"field": null.
Notiondesk API error codes
code | HTTP status | Meaning | What to do |
|---|---|---|---|
authentication_error | 401 | The key or access token is missing or cannot be used | Check the key, its expiration and the plan. See Authenticate with the Notiondesk API |
insufficient_scope | 403 | The credential lacks the scope the route requires | Use a key with the scope named in WWW-Authenticate |
permission_denied | 402, 403 | The credential cannot perform this action on this resource, for example because the plan lacks the feature | Check the plan and the help center's settings. Do not retry unchanged |
invalid_version | 400 | The nd-version header names an unknown version | Send 2026-10-16 or leave the header out |
validation_error | 400, 422 | A parameter or body field is invalid, unknown or out of range | Fix the fields listed in errors |
not_found | 404, 405 | The path, the resource or the method does not exist | Check the ids and the method |
conflict_error | 409 | The request conflicts with the current state, for example a sync is already running | Wait, then retry |
idempotency_key_invalid | 400 | The Idempotency-Key header is empty or longer than 255 characters | Send 1 to 255 characters |
idempotency_key_reused | 422 | The Idempotency-Key was already used for a different request | Use a new key for a new request |
request_in_progress | 409 | A request with the same Idempotency-Key is still running | Wait, then retry with the same key |
rate_limited | 429 | Too many requests | Wait for Retry-After seconds |
internal_error | 500 and above | Notiondesk failed | Retry later, and quote the request_id if you contact support |
A request for a help center or resource outside your workspace answers 404 not_found, so the API never reveals whether it exists.
Notiondesk API rate limits
Each API key and each OAuth grant has two request budgets, counted over a sliding one-minute window:
| Budget | Routes | Requests per minute |
|---|---|---|
| Reads | GET routes | 120 |
| Writes | POST, PATCH and DELETE routes | 20 |
Analytics reports also count against a budget of 30 requests per minute per help center, shared by every key and OAuth app of the workspace.
Responses carry three headers:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Requests allowed in the current window |
X-RateLimit-Remaining | Requests left in the current window |
Retry-After | Seconds until the next request is accepted; 0 while requests remain |
A request over the budget answers 429 rate_limited. Wait the number of seconds in Retry-After before sending the next request.
Retry safely with Idempotency-Key
A network timeout leaves you unsure whether a change went through. The Idempotency-Key header lets you send the same request again without changing anything twice.
Every route that changes something accepts it: page redirect changes, settings and branding updates, integration changes, and sync triggers. Send a unique value of 1 to 255 characters, such as a UUID, with the first attempt, and the same value with every retry:
curl -X POST "https://api.notiondesk.so/sites/c74781f6-1aea-4e17-ab23-bbb8b4d174b6/page-redirects" \
-H "Authorization: Bearer $NOTIONDESK_API_KEY" \
-H "nd-version: 2026-10-16" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6f0d6b1e-create-getting-started" \
-d '{"type": "article", "source_path": "/legacy/getting-started", "target_id": "1761580412", "locale": "en"}'What happens on a retry:
| Retry | Result |
|---|---|
| Same key, same body, same route, after the first request finished | The first response again, status and body included, with X-Idempotent-Replay: true. Nothing changes |
| Same key while the first request is still running | 409 request_in_progress. Wait and retry |
| Same key with a different body, route or path ids | 422 idempotency_key_reused |
| Same key and same body after the first request answered an error | The request runs again |
A successful response carries X-Idempotent-Replay: false the first time and true on a replay. Notiondesk keeps a key for at least 24 hours. A key belongs to the API key or OAuth grant that sent it, and an OAuth grant keeps its keys across token refreshes.
Without the header, a request runs once, and a retry runs again.
A retry strategy that works
- Generate one
Idempotency-Keyper logical change, and reuse it for every retry of that change.
- On
429 rate_limited, wait forRetry-Afterseconds, then retry.
- On
409 request_in_progress, wait a few seconds, then retry with the same key. On409 conflict_error, a sync of the help center is already running: wait a few minutes before you trigger again, or skip this run.
- On
500and above, or a network error, retry with exponential backoff, for example after 1, 2, 4 and 8 seconds.
- Never retry
400,401,403,404or422unchanged. Fix the request first.
In JavaScript, for any route that changes something. It stops on conflict_error and leaves that decision to your code:
import { randomUUID } from "node:crypto";
const sleep = (seconds) => new Promise((resolve) => setTimeout(resolve, seconds * 1000));
async function notiondeskRequest(method, path, body, attempts = 5) {
const idempotencyKey = randomUUID();
for (let attempt = 0; attempt < attempts; attempt++) {
let response;
try {
response = await fetch(`https://api.notiondesk.so${path}`, {
method,
headers: {
Authorization: `Bearer ${process.env.NOTIONDESK_API_KEY}`,
"nd-version": "2026-10-16",
"Content-Type": "application/json",
"Idempotency-Key": idempotencyKey,
},
body: body ? JSON.stringify(body) : undefined,
});
} catch {
await sleep(2 ** attempt);
continue;
}
if (response.ok) {
return response.status === 204 ? null : response.json();
}
const error = await response.json().catch(() => ({}));
const retryable = response.status === 429 || response.status >= 500 || error.code === "request_in_progress";
if (!retryable) {
throw new Error(`${response.status} ${error.code}: ${error.message}`);
}
const retryAfter = Number(response.headers.get("Retry-After"));
await sleep(retryAfter > 0 ? retryAfter : 2 ** attempt);
}
throw new Error(`Notiondesk request failed after ${attempts} attempts`);
}Common questions
What are the Notiondesk API rate limits?
120 read requests and 20 write requests per minute for each API key or OAuth grant, plus 30 analytics requests per minute for each help center. A request over a budget answers 429 with a Retry-After header.
Which field should my code check on an error?
code. It is the only stable, machine-readable classifier. The message is for people and can change.
What should I send to support when a request fails?
The request_id from the error response, the route you called, and the time of the request.
Is it safe to retry a sync trigger?
Yes, with an Idempotency-Key. A retry with the same key answers the first result again and queues nothing. Without a key, a second site sync trigger while the first is running answers 409 conflict_error.