Handle Notiondesk API errors, rate limits, and retries

Notiondesk API error codes and envelope, rate limits of 120 reads and 20 writes per minute, Retry-After, and safe retries with Idempotency-Key.

6 min read

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."}
  ]
}
FieldMeaning
objectAlways error
codeThe machine-readable error. Branch your code on this field only
messageA human-readable explanation. It can change, so never parse it
request_idIdentifies the request when you contact support. null when the request failed before reaching a route, for example on an unknown path
errorsField-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 locale in items, the category_id in changelog_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

codeHTTP statusMeaningWhat to do
authentication_error401The key or access token is missing or cannot be usedCheck the key, its expiration and the plan. See Authenticate with the Notiondesk API
insufficient_scope403The credential lacks the scope the route requiresUse a key with the scope named in WWW-Authenticate
permission_denied402, 403The credential cannot perform this action on this resource, for example because the plan lacks the featureCheck the plan and the help center's settings. Do not retry unchanged
invalid_version400The nd-version header names an unknown versionSend 2026-10-16 or leave the header out
validation_error400, 422A parameter or body field is invalid, unknown or out of rangeFix the fields listed in errors
not_found404, 405The path, the resource or the method does not existCheck the ids and the method
conflict_error409The request conflicts with the current state, for example a sync is already runningWait, then retry
idempotency_key_invalid400The Idempotency-Key header is empty or longer than 255 charactersSend 1 to 255 characters
idempotency_key_reused422The Idempotency-Key was already used for a different requestUse a new key for a new request
request_in_progress409A request with the same Idempotency-Key is still runningWait, then retry with the same key
rate_limited429Too many requestsWait for Retry-After seconds
internal_error500 and aboveNotiondesk failedRetry 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:

BudgetRoutesRequests per minute
ReadsGET routes120
WritesPOST, PATCH and DELETE routes20

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:

HeaderMeaning
X-RateLimit-LimitRequests allowed in the current window
X-RateLimit-RemainingRequests left in the current window
Retry-AfterSeconds 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.

Note: Rate limits are counted per key or per OAuth grant, not per workspace. Two integrations with separate keys do not slow each other down, except on the shared analytics budget.

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:

RetryResult
Same key, same body, same route, after the first request finishedThe first response again, status and body included, with X-Idempotent-Replay: true. Nothing changes
Same key while the first request is still running409 request_in_progress. Wait and retry
Same key with a different body, route or path ids422 idempotency_key_reused
Same key and same body after the first request answered an errorThe 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

  1. Generate one Idempotency-Key per logical change, and reuse it for every retry of that change.
  1. On 429 rate_limited, wait for Retry-After seconds, then retry.
  1. On 409 request_in_progress, wait a few seconds, then retry with the same key. On 409 conflict_error, a sync of the help center is already running: wait a few minutes before you trigger again, or skip this run.
  1. On 500 and above, or a network error, retry with exponential backoff, for example after 1, 2, 4 and 8 seconds.
  1. Never retry 400, 401, 403, 404 or 422 unchanged. 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.

What's next

Related articles

Was this page helpful?

Still need help? Ask us