> ## Documentation Index
> Fetch the complete documentation index at: https://docurise.io/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Errors

> The error format, every error code, and what to do about each one

Errors from `/api/preview/*` use one JSON shape, whatever the endpoint:

```json theme={null}
{
  "error": {
    "code": "REVISION_CONFLICT",
    "message": "The document changed since you read it: ...",
    "retryable": false
  }
}
```

| Field | Always present | Meaning |
| - | - | - |
| `code` | Yes | Stable, machine-readable code in `UPPER_SNAKE_CASE`. Switch on this. |
| `message` | Yes | Human-readable detail for logs and debugging. It may be reworded at any time; don't parse it. |
| `retryable` | Yes | `true` when sending the identical request again may succeed. `false` means change something first. |
| `retryAfterSeconds` | No | A concrete wait, in seconds, before retrying. Left out when there is no specific hint (never `null`). |
| `details` | No | Machine-readable specifics for a few codes (listed in the tables below). Left out for every other code. |

Handle errors by `code`, and treat a code you don't recognize by its HTTP status and `retryable`
flag: new codes may be added during the preview.

A few responses carry no JSON body: a path that doesn't exist (`404`), a method a route doesn't
support (`405`, for example `DELETE` on an Instagram rule), and, rarely, a `5xx` when authentication
or rate limiting fails unexpectedly. Treat any `5xx` whose body you can't parse as retryable.

## Retrying

* **`retryable: true`**: wait and send the same request again. Use `retryAfterSeconds` (or the
  `Retry-After` header on a `429`) when present; otherwise back off exponentially with jitter, starting at a
  second or two. Cap it at about five attempts, never retry in a tight loop, and wait at least a
  second even when `retryAfterSeconds` is `0`.
* **`retryable: false`**: the same request will fail the same way. Fix the input, re-read the
  resource, or ask a person to act in the dashboard, as the tables below say.
* **Creates with an `Idempotency-Key`** (`POST /api/preview/documents`,
  `POST /api/preview/instagram/rules`, `POST /api/preview/segments`): a retryable failure the create reports itself (for example
  a retryable `NOTION_FETCH_FAILED` or `INSTAGRAM_UNAVAILABLE`) releases the key, so retry with the **same** key and
  body. A non-retryable failure is stored against the key: resending the same key and body returns
  the same error again. After fixing the request, send it with a **new** key (reusing the old key
  with a different body is `409 IDEMPOTENCY_KEY_CONFLICT`).
* **A create that fails with `500 INTERNAL_ERROR`** may keep holding its key for up to a minute.
  Retry with the same key and body. While the earlier attempt still holds the key you get
  `409 IDEMPOTENCY_KEY_IN_PROGRESS`: wait `retryAfterSeconds` (3) between tries. It clears within
  about a minute, after which the retry runs as a fresh request; stop and investigate if it
  hasn't cleared after two minutes.

## Rate limits

Each account can make 60 requests per minute across all `/api/preview/*` routes, however many keys
it has. Over the limit, the API answers `429 RATE_LIMITED` with these headers:

| Header | Value |
| - | - |
| `Retry-After` | Seconds until the limit resets (also in `retryAfterSeconds`) |
| `X-RateLimit-Limit` | The per-minute limit |
| `X-RateLimit-Remaining` | `0` |
| `X-RateLimit-Reset` | Unix timestamp (seconds) when the limit resets |

These headers are sent only on the `429`. On the idempotent creates, a replay of a key that was
already used, a conflict and an in-progress answer don't count toward the limit.

### Requests running at once

An account can also have at most 10 requests running at the same moment across all `/api/preview/*`
routes, however many keys it has. The eleventh is refused with `429 TOO_MANY_CONCURRENT_REQUESTS`
and `Retry-After: 1`, and none of the `X-RateLimit-*` headers. It counts requests that have not
finished, so it can apply while you are well under 60 requests per minute: send requests one after
another, or a few at a time, instead of all at once. A refused request did nothing, so it is safe to
retry as is, but wait the `Retry-After` pause first: the refused request still counts toward the 60
requests per minute, except on the idempotent creates, which spend that budget only once a request is
accepted.

## Error codes

### Authentication and access

These can come from any endpoint.

| Code | HTTP | Retryable | What happened and what to do |
| - | - | - | - |
| `UNAUTHORIZED` | 401 | No | The `Authorization` header is missing or malformed, or the key is unknown or revoked. Check the header (`Bearer dr_live_...`); a revoked key never works again, so generate a new one in **Settings → Developers**. |
| `NOT_FOUND` | 404 | No | The API is not available to this account (it is open to some accounts only). Every route answers this way, with the same `404` status as a route that doesn't exist. Also returned in the rare case that the account behind the key no longer exists. Contact DocuRise. |
| `PLAN_REQUIRED` | 403 | No | Your plan does not include API access. Stop and tell a person: it works again once the account is on a plan with API access. |
| `PAST_DUE_RESTRICTED` | 403 | No | The account's payment is past due, so the whole API (reads and writes) is paused. Stop and tell a person to update the payment method in the dashboard; access comes back by itself once the payment succeeds. |
| `INSUFFICIENT_SCOPE` | 403 | No | The key lacks the scope this endpoint needs; `message` names it. Add the scope with **Edit scopes** in **Settings → Developers** (the key itself does not change). See [Authentication](https://docurise.io/docs/authentication.md#scopes). |

### Rate limits and availability

| Code | HTTP | Retryable | What happened and what to do |
| - | - | - | - |
| `RATE_LIMITED` | 429 | Yes | Over 60 requests per minute for the account. Wait `Retry-After` seconds, then retry. See [Rate limits](#rate-limits). |
| `TOO_MANY_CONCURRENT_REQUESTS` | 429 | Yes | The account already has 10 requests running at once. Wait for one to finish (`Retry-After` is 1 second), then retry. See [Requests running at once](#requests-running-at-once). |
| `RATE_LIMIT_UNAVAILABLE` | 503 | Yes | Rate limiting is temporarily unavailable. Not a problem with your request or key; retry after `retryAfterSeconds` (5). |
| `SERVICE_UNAVAILABLE` | 503 | Yes | API key verification is temporarily unavailable. Your key is not the problem; retry after `retryAfterSeconds` (60). `POST /segments` also answers this when the create could not be started: nothing was created, and retrying with the same `Idempotency-Key` is safe. The group endpoints (`POST /segments`, `POST /segments/preview`, `GET /segments/{id}`) answer it too when a very heavy rule could not be counted within about 10 seconds: simplify the rule or retry. |
| `INTERNAL_ERROR` | 500 | Yes | Something went wrong on our side while handling the request. Nothing about the request was wrong; retry with backoff (for a create, with the same `Idempotency-Key`; see [Retrying](#retrying)). If it persists, contact DocuRise. |

The API can also answer `502` with `BATCH_SCHEDULE_FAILED` or `INSTAGRAM_UNAVAILABLE` (see
[Instagram](#instagram)); both are retryable.

### Request format

| Code | HTTP | Retryable | What happened and what to do |
| - | - | - | - |
| `VALIDATION_ERROR` | 400 | No | A field, path or query parameter is invalid: a wrong type or value, a value out of range, an unknown or repeated query parameter, a NUL character in `q` or in a request body, an unknown body field (also inside a form question), a `kind: post` create that sends both `content` and `html` or neither (send exactly one), an `html` over 200,000 characters, or a date that is not RFC 3339 or a range longer than 366 days. `message` names the field. Fix the request. On `POST /segments` and `POST /segments/preview`, a bad `name` or `filter` (a wrong value, a list of more than 20 conditions, an id that is not a UUID, a combination of keys a condition does not allow) is also this code, and `message` starts with the path of the bad part, for example `filter.conditions.2.value:`. On `PATCH /documents/{id}`, also any request that carries `If-Match` or `If-Unmodified-Since` (send `revision` in the body instead). |
| `INVALID_JSON` | 400 | No | The request body is not valid JSON. Endpoints with a body: `POST` and `PATCH` on documents and Instagram rules, and `POST /segments` and `POST /segments/preview`. |
| `INVALID_CURSOR` | 400 | No | `cursor` is not one this API issued for your account. Pass back `nextCursor` from the previous page exactly as returned, or start again without a cursor. List endpoints only. |
| `INVALID_INPUT` | 400 | No | `POST /documents`: the request was well-formed but the document could not be created from it, for a reason with no more specific code. Check the body against the [reference](https://docurise.io/docs/api-reference/create-document.md). |

### Idempotency

`POST /api/preview/documents`, `POST /api/preview/instagram/rules` and `POST /api/preview/segments`
require an `Idempotency-Key` header. `POST /api/preview/segments/preview` does not take one: it saves
nothing, so repeating it is always safe.

| Code | HTTP | Retryable | What happened and what to do |
| - | - | - | - |
| `IDEMPOTENCY_KEY_REQUIRED` | 400 | No | The `Idempotency-Key` header is missing. Send one per logical operation (a UUID, or the id of whatever triggered the request). |
| `IDEMPOTENCY_KEY_TOO_LONG` | 400 | No | The key is longer than 255 characters. Use a shorter one. |
| `IDEMPOTENCY_KEY_CONFLICT` | 409 | No | This key was already used with a different body (or for a different endpoint or document `kind`). Use a new key for a new request; reuse a key only to retry the identical request. |
| `IDEMPOTENCY_KEY_IN_PROGRESS` | 409 | Yes | A request with this key is still being processed, or an earlier attempt failed with `500 INTERNAL_ERROR` and still holds the key. Wait `retryAfterSeconds` (3) and retry with the same key and body; if the first request finished you get its result. If the earlier attempt failed, this clears within about a minute and the retry then runs as a new request. |

### Documents

| Code | HTTP | Retryable | Returned by | What happened and what to do |
| - | - | - | - | - |
| `DOCUMENT_NOT_FOUND` | 404 | No | `/documents/{id}` and its analytics | No such document in your account. An unknown id, a malformed id and someone else's document look the same. Check the id (for example against `GET /documents`). |
| `DOCUMENT_LIMIT_REACHED` | 403 | No | `POST /documents`, `PATCH /documents/{id}` | Creating or publishing would exceed your plan's published-document limit. Drafts don't count. Stop and tell a person to make room or change plan in the dashboard. No `details`. |
| `PRECONDITION_REQUIRED` | 428 | No | `PATCH /documents/{id}` | The body has no `revision`. Read the document with `GET /documents/{id}` and send its `revision` in the PATCH body. |
| `REVISION_CONFLICT` | 409 | No | `PATCH /documents/{id}` | The document changed since you read it (in the dashboard or through the API), and nothing was written. The response's `ETag` header holds the current revision. Re-read the document with `GET`, re-apply your change to what it now says, and send the new `revision`. Don't blindly resend. |
| `PROTECTION_RULE_CONFLICT` | 422 | No | `PATCH /documents/{id}` | A protection change conflicts with how the document is set up (for example `requireEmail: false` where the document must collect emails, or access controls on a form). Nothing was applied. `details.rule` names the rule; new values may be added. Change the request or the document in the dashboard. |
| `STATUS_NOT_SUPPORTED` | 422 | No | `POST /documents`, `PATCH /documents/{id}` | The `status` is valid but not available for this document: `draft` exists only for posts and forms, a draft can't be set to `inactive` (publish it first), and `inactive` can't be set at create time. Send a status the document supports. |
| `CANNOT_REVERT_TO_DRAFT` | 422 | No | `PATCH /documents/{id}` | A published post or form can't go back to `draft`. Use `inactive` to take it offline. |
| `DOCUMENT_NOT_READY` | 422 | No | `PATCH /documents/{id}` | The document's file is still being uploaded or processed. Try again once processing has finished (minutes, not seconds). |
| `RELATIVE_EXPIRY_NOT_SUPPORTED` | 422 | No | `PATCH /documents/{id}` | The document uses a per-viewer expiry, which the API can't change. Change it in the dashboard. |
| `EMPTY_BODY` | 422 | No | `POST /documents`, `PATCH /documents/{id}` | A post would go live with no content. Add text, an image or a video, or create it as a `draft`. |
| `UNSUPPORTED_VARIABLE` | 422 | No | `POST /documents`, `PATCH /documents/{id}` | A post would go live with an email variable such as `{recipient_name}`, which a published post can't fill in. Remove it, or create the post as a `draft`. |
| `VIDEO_FAILED` | 422 | No | `PATCH /documents/{id}` | Publishing a draft: a video in the post failed to process. Remove it or upload it again in the dashboard editor. |
| `VIDEO_OVER_LIMIT` | 422 | No | `PATCH /documents/{id}` | Publishing a draft: a video in the post is longer than allowed. Replace it with a shorter one in the dashboard editor. |
| `BROADCAST_BACKED_POST` | 422 | No | `PATCH /documents/{id}` | Publishing a draft: the post is the body of an email broadcast and is published through the email flow, not this endpoint. |
| `PAID_LEAD_GATE_NO_PRODUCTS` | 422 | No | `PATCH /documents/{id}` | Publishing or reactivating would make a priced document live with no product a viewer can buy. Fix its products in the dashboard first. |
| `SLUG_TAKEN` | 409 | No | `PATCH /documents/{id}` | Publishing a draft: the web address generated for it was taken by another document at the same moment. Nothing was published. Re-read the document and send the request again with its current `revision`. |
| `NOTION_FETCH_FAILED` | 422 | Varies | `POST /documents` (`kind: notion`) | The Notion page could not be fetched. `details.notionErrorCode` says why (for example `NOTION_ERROR_PRIVATE`: the page is not published to the web). If `retryable` is `true` (a network problem or Notion rate limit), retry with the same key; otherwise fix the page or URL and use a new key. |
| `VIDEO_CONTENT_UNSUPPORTED` | 422 | No | `POST /documents` (`kind: post`) | Post content with a placeholder for an uploaded video (`data-video-id`) can't be created through the API. Remove it, or add the video in the dashboard editor. YouTube and Vimeo embeds (`data-video-embed`) are accepted in `content`. |
| `FORM_FILE_QUESTION_DISABLED` | 403 | No | `POST /documents` (`kind: form`) | The `file` question type is not enabled for this account yet. Use another question type. |
| `HTML_MODE_DISABLED` | 403 | No | `POST /documents` (`kind: post`, `html`) | HTML posts are not enabled for this account, and nothing was created. Create the post with `content` instead, or ask for HTML posts to be enabled for the account. |
| `HTML_REJECTED` | 422 | No | `POST /documents` (`kind: post`, `html`) | The server could not accept the HTML, and nothing was created. `details.reason` says why: `too_large` (over the post size limit once sanitized), `css_rejected` (a `<style>` block was refused: remove or simplify it, and avoid backslashes in CSS) or `invalid` (any other HTML it could not accept). New reasons may be added. Fix the HTML and send it with a new `Idempotency-Key`. |
| `NOT_A_FORM_DOCUMENT` | 422 | No | `GET /documents/{id}/analytics/funnel` | The funnel exists only for forms. Call it with a form's id. |

### Contacts

| Code | HTTP | Retryable | Returned by | What happened and what to do |
| - | - | - | - | - |
| `CONTACT_NOT_FOUND` | 404 | No | `PUT`, `DELETE /contacts/{contactId}/tags/{tagName}` | No such contact in your account. Check the id. |
| `TAG_NOT_FOUND` | 404, 422 | No | `PUT`, `DELETE /contacts/{contactId}/tags/{tagName}` (404); `POST /segments`, `POST /segments/preview` (422) | `404`: no tag with that name in your account. These calls never create a tag: create it with `POST /tags` or in the dashboard (see `GET /tags` for names). `422`: a `tag` condition in a group rule names a tag your account does not have (names are matched exactly, case included); `details.tag` is that name. Nothing was created. |
| `TAG_ALREADY_EXISTS` | 409 | No | `POST /tags` | A tag that differs only by letter case or surrounding spaces already exists, and nothing was created. `details.existingTag` is its `{ id, name }`: use that tag. |
| `TAG_LIMIT_REACHED` | 422 | No | `POST /tags` | The account is at its tag limit, and nothing was created. `details.limit` is the limit (200). Delete tags you no longer use in the dashboard, then try again. |
| `TAG_NOT_MANUAL` | 409 | No | `DELETE /contacts/{contactId}/tags/{tagName}` | The contact has this tag from an automatic rule, which the API can't remove. Nothing was changed. |

### Groups

A group rule is checked the same way by `POST /segments` and `POST /segments/preview`. A
refusal here never created or changed anything. The rule format is on
[Group conditions](https://docurise.io/docs/api-reference/group-conditions.md).

| Code | HTTP | Retryable | Returned by | What happened and what to do |
| - | - | - | - | - |
| `EMPTY_FILTER` | 422 | No | `POST /segments`, `POST /segments/preview` | The rule has no condition in `conditions`, `requiredConditions` or `excludeConditions`, so it would match every contact. Add at least one condition. No `details`. |
| `CONDITION_NOT_SUPPORTED` | 422 | No | `POST /segments`, `POST /segments/preview` | Either a condition uses a type the API does not accept yet (`custom_field`), or it has a form that gives a wrong answer for some contacts in the list it is in (for example `location` with `is` in `excludeConditions`, or `tier_access` with `not_enrolled` and a `productId` in any list; see Group conditions for the full list and what to use instead). `details` is `{ field, path }`: the type and where it sits in the request; for the second kind it also holds `operator` and, when there is an equivalent operator for `requiredConditions`, `invertTo`. Remove the condition or change it as Group conditions says. |
| `REFERENCE_NOT_FOUND` | 422 | No | `POST /segments`, `POST /segments/preview` | A condition carries an id that is not in your account: unknown, deleted, in the trash or someone else's, all indistinguishable. `details` is `{ path, kind }`, with `kind` one of `document`, `product`, `tier`, `cta`, `folder`, `broadcast` or `step`. New kinds may be added. Fix the id (see where to get ids on [Group conditions](https://docurise.io/docs/api-reference/group-conditions.md#ids-in-conditions)). |
| `GROUP_REFERENCE_NOT_FOUND` | 422 | No | `POST /segments`, `POST /segments/preview` | A `group_membership` condition refers to a group that is not in your account. `details` is `{ groupIds }`, the ids that were not found. Use ids from `GET /segments`. |
| `GROUP_LIMIT_REACHED` | 422 | No | `POST /segments` | The account is at the limit of groups it can create through the API, and nothing was created. `details` is `{ limit }` (100). Delete groups you no longer use in the dashboard, then send the request again with a new `Idempotency-Key`. |
| `GROUP_NOT_FOUND` | 404 | No | `GET /segments/{id}` | No such group in your account (unknown, malformed or someone else's id). Check the id against `GET /segments`. |

### Email

| Code | HTTP | Retryable | Returned by | What happened and what to do |
| - | - | - | - | - |
| `BROADCAST_NOT_FOUND` | 404 | No | `GET /email/broadcasts/{id}/insights` | No such broadcast in your account (unknown, malformed or someone else's id). Check the id. |

### Instagram

| Code | HTTP | Retryable | Returned by | What happened and what to do |
| - | - | - | - | - |
| `RULE_NOT_FOUND` | 404 | No | `/instagram/rules/{id}` | No such rule on your connected account (unknown, malformed or someone else's id, or a rule of an account that is no longer connected). |
| `INSTAGRAM_NOT_CONNECTED` | 409 | No | `POST /instagram/rules` | The account has no connected Instagram account. Connect one in the dashboard first. |
| `NEXT_POST_RULE_EXISTS` | 409 | No | `POST /instagram/rules` | The account already has a next-post rule (one per account). `details.ruleId` is the existing rule: edit it with `PATCH`, or delete it in the dashboard. A retry of a create that did succeed also gets this, unless it reuses the same `Idempotency-Key`. |
| `BATCH_RULE_NOT_PAUSABLE` | 409, 422 | No | `PATCH`, `POST /instagram/rules` | A rule with a scheduled batch send can't be paused. `PATCH` (409): remove the batch send with `PATCH` first. `POST` (422): don't combine `status: paused` with `schedule.batchSendAt`. |
| `BATCH_SEND_MISSED` | 409 | No | `PATCH /instagram/rules/{id}` | The scheduled batch send was missed, so resuming would DM everyone who commented at once. `PATCH` a new schedule (or `null` to send immediately), then set `status: active`. |
| `BATCH_SCHEDULE_FAILED` | 502 | Yes | `POST`, `PATCH /instagram/rules` | The batch send could not be scheduled. Retrying is safe. |
| `RULE_NOT_EDITABLE` | 409 | No | `PATCH /instagram/rules/{id}` | Only the next-post rule (type `standard`) can be edited through the API. Edit other rules in the dashboard. |
| `RULE_NOT_PAUSABLE` | 409 | No | `PATCH /instagram/rules/{id}` | Only the next-post rule (type `standard`) can have its `status` changed through the API. |
| `STATUS_CHANGE_MUST_BE_ALONE` | 422 | No | `PATCH /instagram/rules/{id}` | `status` was sent together with other fields. Send the field changes and the status change as two separate requests. Nothing was written. |
| `INSTAGRAM_TOKEN_EXPIRED` | 409 | No | `PATCH /instagram/rules/{id}` | The Instagram connection has expired. Stop and tell a person to reconnect Instagram in the dashboard, then set `status: active` again. |
| `INSTAGRAM_UNAVAILABLE` | 502 | Yes | `PATCH /instagram/rules/{id}` | Instagram could not be reached. Retrying is safe. |
| `CONCURRENT_MODIFICATION` | 409 | Yes | `PATCH /instagram/rules/{id}` | The rule was changed by another request at the same time, and nothing was applied. Retry the same request; it is applied to the rule's new state. |

Instagram write errors that fall into none of these codes are `500 INTERNAL_ERROR` (retryable).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.