> ## 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.

# Update a document

> Publish a draft post or form, pause or resume a document, set or clear its expiry, and change its email protection, guarded by the document revision in the request body, copied from the `revision` or `ETag` that GET returns



## OpenAPI

````yaml api-reference/openapi.yaml PATCH /api/preview/documents/{id}
openapi: 3.1.0
info:
  title: DocuRise Public API
  version: preview
  description: >
    Programmatic access to DocuRise.


    **This is a preview API.** Every path is versioned under `/api/preview/`

    and carries no compatibility guarantee while it is in preview: fields,

    values and error codes may be added, changed or removed. Once the

    contract stabilizes it will be promoted to `/api/v1/`. See the

    [changelog](/changelog) for what changed and when.


    ### Access


    Every route requires:

    1. A valid API key (see Authentication below).

    2. The calling account's plan must be **Iconic or above**.

    3. During the preview the API is available to some accounts only, whatever
       the plan. Accounts without access get a `404` from every route below; if
       you expect access, check with your DocuRise contact.

    Generate and manage keys from **Settings → Developers** in the dashboard.


    ### Idempotency


    `POST` endpoints that create a resource require an `Idempotency-Key`

    header. Reusing the same key with the same request body replays the

    original response (same status code, same body) instead of creating a

    second resource — safe to retry on timeout or connection loss. A key is

    remembered for 7 days; after that the same key is treated as a new

    request. Reusing the same key with a **different** body is rejected with

    `IDEMPOTENCY_KEY_CONFLICT`. Generate a fresh key (a UUID or ULID is fine)

    per logical operation, not per HTTP attempt.


    One exception: `POST /api/preview/tags` takes no `Idempotency-Key`. Creating
    the same

    tag name twice changes nothing (the second call returns the existing tag
    with

    `created: false`), so a retry is always safe.


    Another exception: `POST /api/preview/segments/preview` takes no
    `Idempotency-Key`.

    It saves nothing, so repeating it is always safe.


    ### Response compatibility


    Responses may gain additional fields over time, and `type`/`code`-style

    enum values (for example a future `LeadFormQuestion.type` member, or a

    new error `code`) may gain new members. Always ignore fields and enum

    values you don't recognize rather than treating them as errors — this is

    what makes every additive change under `preview` non-breaking.


    A field absent from a response is not always the same as a field present

    with value `null`. A field whose type never includes `null` is omitted

    when it doesn't apply — it is never sent as `null` (for example,

    `retryAfterSeconds` is left out entirely when there's no backoff hint, not

    set to `null`). A field whose type explicitly allows `null` uses it to

    mean something real and distinct from "not present" (for example,

    `expiresAt: null` on a document means it has no fixed expiry date, not

    "unknown" or "not applicable").


    All field names use `camelCase` — `notionUrl`, `leadFormQuestions`,

    `collectName`, and so on — consistently across every request and

    response body.


    ### Identifiers


    `id` values are opaque strings. Store and compare them as unstructured

    tokens — don't parse them, validate their format, or assume anything

    about their length or character set.


    ### Errors


    Every error response has the same shape:


    ```json

    { "error": { "code": "STRING", "message": "human-readable", "retryable":
    false } }

    ```


    Switch on `code`, not `message` — `message` is free text for logs/debugging

    and may be reworded without notice; `code` is the stable contract.
    `retryable`

    tells you whether the same request is safe to retry as-is; when present,

    `retryAfterSeconds` is a concrete backoff hint.


    A request body that contains a NUL character (`\u0000`), in a string value

    or in a property name, is refused with `400 VALIDATION_ERROR` before
    anything

    is saved.


    ### Rate limiting


    60 requests/minute per account across all `/api/preview/*` routes,

    regardless of how many keys that account has. Exceeding it returns `429`

    with a `Retry-After` header.


    Separately, an account can have at most 10 requests running at the same

    moment across these routes, however many keys it has. A request that would

    be the eleventh returns `429` with `code: TOO_MANY_CONCURRENT_REQUESTS` and

    `Retry-After: 1`; wait for one of your requests to finish and retry. This

    limit counts requests that have not finished, so it can apply even while

    you are under 60 requests per minute.
  contact:
    name: DocuRise
    url: https://docurise.io
servers:
  - url: https://docurise.io
    description: Production
security:
  - apiKey: []
tags:
  - name: Documents
    description: >-
      Create documents (a Notion page, a post or a lead-capture form), list or
      read the documents you own, publish drafts, and change their status,
      expiry and email protection.
  - name: Analytics
    description: >-
      Read aggregate view, visitor, lead and conversion numbers for one of your
      documents, split by traffic channel, compared with the previous period, or
      as a form funnel, and rank all of your documents by views, visitors or
      leads.
  - name: Folders
    description: Read your folders and courses, with item counts.
  - name: Products
    description: Read your product catalog (read-only).
  - name: Audiences
    description: >-
      List your tags and groups with contact counts, create a group from a rule,
      count who a rule would match, and read one group with its rule. Counts
      only; no member data is ever returned.
  - name: Contacts
    description: >-
      Add or remove a tag on one of your contacts. Contact details are never
      returned.
  - name: Email
    description: >-
      Read email broadcasts, their aggregate delivery numbers, and automations.
      Email only; Kakao is not included.
  - name: Instagram
    description: >-
      Create, read, edit, pause and resume Instagram comment-to-DM automation
      rules.
  - name: Account
    description: Inspect the API key you are calling with.
paths:
  /api/preview/documents/{id}:
    parameters:
      - name: id
        in: path
        required: true
        description: The document id.
        schema:
          type: string
          format: uuid
    patch:
      tags:
        - Documents
      summary: Update a document's status, expiry or email protection
      description: >
        `status: live` on a draft post or form publishes it; publishing runs the
        checks listed below and a published document cannot go back to draft.


        Pauses or resumes a document, sets or clears its expiry, and changes how
        it asks

        visitors for an email. Requires the `documents:write` scope. Send only
        the fields you

        want to change; anything else (`slug`, `password`, `accessMode`, ...) is
        a

        `400 VALIDATION_ERROR`, never silently ignored. The password, the access
        mode and the slug cannot be changed through the API.


        **`revision` is required, in the request body.** Read the document first

        (`GET /api/preview/documents/{id}`), then send its `revision` as the
        `revision`

        field of the body, next to the fields you want to change. If the
        document changed

        since, you get `409 REVISION_CONFLICT`,

        nothing is applied, and the `ETag` header of that response holds the
        current

        revision so you can re-read the document and retry. Without the field
        the

        request is `428 PRECONDITION_REQUIRED`. `*` or a list is refused with
        `400`.

        The plain timestamp is the form to send; the `ETag` header value is
        accepted

        too, as the quoted `"..."` or the weak `W/"..."` form (some gateways and
        proxies turn the

        ETag weak when they compress the response), so you can copy it as is. A
        body

        that carries only `revision` changes nothing and is `400`.


        **Do not send `If-Match`.** The platform that serves the API answers
        `412

        PRECONDITION_FAILED` to any successful response whose `ETag` differs
        from the

        request's `If-Match`, and a successful `PATCH` always returns a new
        `ETag`, so the

        change would be applied but reported as a failure. So the API refuses a
        request

        that carries `If-Match` or `If-Unmodified-Since` with `400
        VALIDATION_ERROR`

        before it changes anything, even when `revision` is also in the body.

        The revision is checked and the change is written in one step, so two

        scripts holding the same revision cannot both succeed. A key needs

        `documents:read` to read the current revision (ETag) and
        `documents:write`

        to change settings; give an automation both scopes.


        **`status`**


        | Document kind | `inactive` | `live` | `draft` |

        |---|---|---|---|

        | Notion, PDF, upload | pauses | resumes | `422 STATUS_NOT_SUPPORTED` |

        | Post, form (published) | pauses | resumes | `422
        CANNOT_REVERT_TO_DRAFT` |

        | Post, form (draft) | `422 STATUS_NOT_SUPPORTED` | publishes | no
        change |


        **Publishing a draft.** Setting `live` on a draft post or form publishes
        it, with the checks below. The request is all-or-nothing: if any check

        fails, the document stays a draft and nothing else in the request is
        applied. The checks, in order:


        | Check | Applies to | If it fails |

        |---|---|---|

        | The post is not the body of an email broadcast (those are published
        through the email flow) | post | `422 BROADCAST_BACKED_POST` |

        | Your plan's published-document limit has room | post, form | `403
        DOCUMENT_LIMIT_REACHED` |

        | The post has some content (text, an image, or a video) | post | `422
        EMPTY_BODY` |

        | The body uses no email variable such as `{recipient_name}` | post |
        `422 UNSUPPORTED_VARIABLE` |

        | No video in the body failed to process or is over the duration limit
        (a video that is still encoding is fine) | post | `422 VIDEO_FAILED`,
        `422 VIDEO_OVER_LIMIT` |

        | A post with a price has at least one product a visitor can buy | post
        | `422 PAID_LEAD_GATE_NO_PRODUCTS` |


        A draft does not count toward the plan limit; publishing it does. The
        address of a post can change

        when it is published, so read the document again for its final `url`.
        Publishing does not rewrite

        the body: the content is published exactly as stored. Edit the content,
        title, buttons and products

        in the dashboard.


        Publishing does not change whether the document is on; a priced draft
        publishes as

        `inactive`. Send `status: live` again to turn it on; that second call
        checks that the price has a

        product a visitor can buy (`422 PAID_LEAD_GATE_NO_PRODUCTS` otherwise).
        The response of the first

        call shows `status: inactive`.


        A request that also sets `expiresAt` or an email-protection field
        applies it in the same write as

        the publish, or not at all.


        A draft always reads `draft`, so `inactive` on a draft is refused rather
        than recorded where

        nothing would show it.


        Setting `live` on a paused document that has a price but no product a
        visitor

        can buy is `422 PAID_LEAD_GATE_NO_PRODUCTS`: fix its products in the
        dashboard

        first. Setting `live` on a PDF whose upload or replacement has not
        finished

        processing is `422 DOCUMENT_NOT_READY`: wait until the file is
        processed, then

        retry.


        **`expiresAt`** is an RFC 3339 datetime with `Z` or a numeric offset,
        for

        example `2026-12-31T23:59:59+09:00`, stored as the UTC instant. `null`

        removes the expiry. A time in the past is allowed and expires the
        document

        right away (`status` reads `expired`). A newly

        set or earlier expiry also ends sessions visitors already unlocked, so
        they

        must unlock again. A document with a per-viewer relative expiry (a
        number of

        days after each viewer first opens it) cannot have its expiry changed
        here:

        `422 RELATIVE_EXPIRY_NOT_SUPPORTED`. Its `status` can still be changed.


        **Email protection** (`requireEmail`, `requireEmailVerification`,
        `allowedEmails`,

        `allowedDomains`)


        | Field | Meaning |

        |---|---|

        | `requireEmail` | `true` asks visitors for an email before they see the
        document. |

        | `requireEmailVerification` | `true` makes visitors confirm the email
        through a link sent to it. |

        | `allowedEmails` | The email addresses allowed in. Up to 10,000. `[]`
        empties the list. |

        | `allowedDomains` | Bare domains allowed in, for example `acme.com`.
        Exact match, subdomains are not included. Up to 10,000. `[]` empties the
        list. |


        A malformed address or domain

        is a `400 VALIDATION_ERROR` whose message names the field and the
        position of the entry

        (never the value). Entries are lowercased and duplicates removed.
        Sending the same set

        again, in any order or case, changes nothing. An omitted field is left
        alone.


        The allow lists only take effect when the document's access mode is
        `specific`

        (`protection.accessMode` in the response). The API cannot set the access
        mode: change it

        in the dashboard. On a `public` document the lists are stored but not
        enforced.


        A few rules keep `requireEmail` on. Sending `requireEmail: false` for
        such a document is

        `422 PROTECTION_RULE_CONFLICT`: nothing in the request is applied, and
        `error.details.rule` names the rule:


        | `details.rule` | Why `requireEmail` must stay on |

        |---|---|

        | `form` | A form always collects the visitor's email. |

        | `restricted_access` | The document's access mode is `specific`: the
        email is how a visitor is recognised. |

        | `paid_lead_gate` | The document has a priced lead gate, which needs
        the email to show the price. |

        | `free_lead_gate` | The lead gate (blurred preview) is turned on; turn
        it off in the dashboard first. |


        A form cannot enforce email verification or allow lists, so turning on

        `requireEmailVerification`, or sending a non-empty `allowedEmails` or
        `allowedDomains`,

        for a form is also `422 PROTECTION_RULE_CONFLICT` (`details.rule:
        form_no_access_controls`).

        Turning them off, or sending empty lists, is accepted.


        **Sessions.** Turning a gate on or making it stricter ends the sessions
        visitors already

        unlocked, so they must unlock again: turning `requireEmail` on (not when
        a password

        already protects the document), turning `requireEmailVerification` on,
        and restricting an allow list (adding the first entry to empty lists,

        removing or replacing an entry, or emptying a list of a document with
        `specific` access).

        Loosening (turning a setting off, or adding entries to lists that
        already have some) leaves

        existing sessions alone. When one request

        changes several things, visitors are signed out once.


        The change is all-or-nothing: if any part is refused, none of it is
        applied.

        A request that changes nothing succeeds and leaves the revision as it
        was.

        Setting `live` on a document that is already live changes nothing: the

        revision stays the same and the response is 200.

        Setting `live` on a document whose fixed expiry has passed turns it on,
        but `status`

        reflects expiry and stays `expired`: extend `expiresAt` to make an
        expired document live again.


        The response is the document after the change: the same body as

        `GET /api/preview/documents/{id}`, with the new `revision` in the body
        and in the

        `ETag` header. Its `status` is the one visitors get, so it reflects
        expiry and can

        read `expired` even right after you set `live`.
      operationId: updateDocument
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateDocumentRequest'
            examples:
              pause:
                summary: Pause a document
                value:
                  status: inactive
              expire:
                summary: Expire it at the end of the year (Seoul time)
                value:
                  expiresAt: '2026-12-31T23:59:59+09:00'
              clearExpiry:
                summary: Remove the expiry
                value:
                  expiresAt: null
              requireEmail:
                summary: Ask visitors for an email
                value:
                  requireEmail: true
              verifyEmail:
                summary: Require visitors to confirm their email
                value:
                  requireEmail: true
                  requireEmailVerification: true
              allowList:
                summary: Replace the allow lists
                value:
                  allowedEmails:
                    - alice@example.com
                  allowedDomains:
                    - acme.com
              clearAllowList:
                summary: Empty the email allow list
                value:
                  allowedEmails: []
              publish:
                summary: Publish a draft post or form
                value:
                  status: live
      responses:
        '200':
          description: The document after the change.
          headers:
            ETag:
              description: >-
                The new `revision`, quoted. Send the new `revision` in the body
                of the next change.
              schema:
                type: string
                example: '"2026-10-04T01:02:09.000Z"'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DocumentDetail'
        '400':
          description: >-
            `VALIDATION_ERROR` (`message` is prefixed with the offending field:
            an unknown or out-of-scope field such as `password` or `accessMode`,
            an invalid `status`, an `expiresAt` that is not RFC 3339 with `Z` or
            an offset, an `allowedEmails` or `allowedDomains` entry that is not
            a valid address or domain or a list over 10,000 entries, a
            `requireEmail` or `requireEmailVerification` that is not a boolean,
            an empty body, a `revision` that is not the string `GET` returned
            (`*` or a list included)) or `INVALID_JSON`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: >-
            `PAST_DUE_RESTRICTED`, `PLAN_REQUIRED`, `INSUFFICIENT_SCOPE` (the
            key lacks `documents:write`) or `DOCUMENT_LIMIT_REACHED` (publishing
            a draft would exceed the plan's published-document limit;
            `retryable: false`, with no `details`, the same body as creating a
            document over the limit). Nothing was applied.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          $ref: '#/components/responses/DocumentNotFound'
        '409':
          description: >-
            `REVISION_CONFLICT` (`retryable: false`): the document changed since
            the revision you sent, or a draft you meant to publish was published
            in the meantime. Nothing was applied. The current revision is
            returned only in the `ETag` header (not in the body); read the
            document again and decide whether to re-apply. The `ETag` header is
            sent only with `REVISION_CONFLICT`. Or `SLUG_TAKEN` (`retryable:
            false`): while publishing a post, the web address generated for it
            was taken by another document at the same moment. Nothing was
            applied and the post is still a draft; send the request again (read
            the document first for the current revision).
          headers:
            ETag:
              description: >-
                The current revision, quoted. Sent only with
                `REVISION_CONFLICT`; `SLUG_TAKEN` has no `ETag` header.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '422':
          description: >
            The request is well formed but a rule refuses it (`retryable:
            false`),

            and nothing was applied. `code` is one of:

            `STATUS_NOT_SUPPORTED` (`draft` on a Notion, PDF or upload document,
            or `inactive` on a draft post or form),

            `CANNOT_REVERT_TO_DRAFT` (a published post or form),

            `RELATIVE_EXPIRY_NOT_SUPPORTED` (the document has a per-viewer
            relative expiry),

            `PAID_LEAD_GATE_NO_PRODUCTS` (turning on, or publishing, a priced
            document that has no usable product),

            `DOCUMENT_NOT_READY` (status `live` on a PDF whose upload or
            replacement has not finished processing; wait until the file is
            processed, then retry),

            `EMPTY_BODY` (publishing a post with no content),

            `UNSUPPORTED_VARIABLE` (publishing a post whose body uses an email
            variable),

            `VIDEO_FAILED` (publishing a post that contains a video that failed
            to process),

            `VIDEO_OVER_LIMIT` (publishing a post that contains a video over the
            duration limit),

            `BROADCAST_BACKED_POST` (the post is the body of an email broadcast
            and is published through the email flow),

            `PROTECTION_RULE_CONFLICT` (`requireEmail: false` for a document
            that must keep asking for an email, or turning on email verification
            or an allow list for a form; `error.details.rule` names the rule,
            see the table in the operation description).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                protectionRuleConflict:
                  value:
                    error:
                      code: PROTECTION_RULE_CONFLICT
                      message: >-
                        requireEmail cannot be false: a form always collects the
                        visitor email, so it cannot be turned off
                      retryable: false
                      details:
                        rule: form
        '428':
          description: >-
            `PRECONDITION_REQUIRED` (`retryable: false`): the body has no
            `revision`. Read the document and send its `revision` in the body
            (not as `If-Match`).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          description: >-
            `INTERNAL_ERROR`: unexpected failure. Retryable; the cause is not
            returned.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          $ref: '#/components/responses/Unavailable'
      security:
        - apiKey: []
      x-codeSamples:
        - lang: bash
          label: cURL
          source: >
            # 1) Read the document and note its "revision"

            curl
            "https://docurise.io/api/preview/documents/7f1c0f7e-3c5e-4b1a-9d34-2f0a5b6c8d90"
            \
              -H "Authorization: Bearer $DOCURISE_API_KEY"

            # 2) Pause the document and expire it at the end of the year,
            sending the revision back in the body

            curl -X PATCH
            "https://docurise.io/api/preview/documents/7f1c0f7e-3c5e-4b1a-9d34-2f0a5b6c8d90"
            \
              -H "Authorization: Bearer $DOCURISE_API_KEY" \
              -H "Content-Type: application/json" \
              -d '{"revision":"2026-10-04T01:02:03.456Z","status":"inactive","expiresAt":"2026-12-31T23:59:59+09:00"}'
        - lang: bash
          label: cURL (publish a draft)
          source: >
            # 1) Read the draft and note its "revision"

            curl
            "https://docurise.io/api/preview/documents/7f1c0f7e-3c5e-4b1a-9d34-2f0a5b6c8d90"
            \
              -H "Authorization: Bearer $DOCURISE_API_KEY"

            # 2) Publish it, sending the revision back in the body

            curl -X PATCH
            "https://docurise.io/api/preview/documents/7f1c0f7e-3c5e-4b1a-9d34-2f0a5b6c8d90"
            \
              -H "Authorization: Bearer $DOCURISE_API_KEY" \
              -H "Content-Type: application/json" \
              -d '{"revision":"2026-10-04T01:02:03.456Z","status":"live"}'
components:
  schemas:
    UpdateDocumentRequest:
      description: >-
        `revision` is required, and at least one other field. Any unknown field
        is rejected with `400 VALIDATION_ERROR`.
      type: object
      additionalProperties: false
      minProperties: 2
      required:
        - revision
      properties:
        revision:
          type: string
          description: >-
            The `revision` of the document as last returned by `GET` or `PATCH`
            (the plain timestamp is the form to send; the `ETag` header value is
            accepted too, as the quoted `"..."` or the weak `W/"..."` form). A
            stale value is `409 REVISION_CONFLICT`. Send it here, not as
            `If-Match`.
          example: '2026-10-04T01:02:03.456Z'
        status:
          type: string
          enum:
            - live
            - draft
            - inactive
          description: >-
            `live` on a draft post or form publishes it, with the publishing
            checks; a published post or form cannot go back to `draft`. `live`
            and `inactive` pause and resume a published document. See the table
            on the operation for what each value does per document kind.
        expiresAt:
          type:
            - string
            - 'null'
          format: date-time
          description: >-
            RFC 3339 with `Z` or a numeric offset, stored as UTC. `null` removes
            the expiry. Omit it to leave the expiry alone.
          example: '2026-12-31T23:59:59+09:00'
        requireEmail:
          type: boolean
          description: >-
            `true` asks visitors for an email, `false` stops asking. `false` is
            refused with `422 PROTECTION_RULE_CONFLICT` for a form, a document
            with `specific` access, and a document with a lead gate. Turning it
            on ends the sessions visitors already unlocked.
        requireEmailVerification:
          type: boolean
          description: >-
            `true` makes visitors confirm their email through a link. Turning it
            on ends the sessions visitors already unlocked. Refused for a form
            (`422 PROTECTION_RULE_CONFLICT`).
        allowedEmails:
          type: array
          maxItems: 10000
          items:
            type: string
            format: email
          description: >-
            The full list of allowed email addresses (it replaces the stored
            list). Lowercased and de-duplicated. `[]` empties it. Enforced only
            when the access mode is `specific`. A non-empty list is refused for
            a form.
          example:
            - alice@example.com
        allowedDomains:
          type: array
          maxItems: 10000
          items:
            type: string
          description: >-
            The full list of allowed bare domains such as `acme.com` (it
            replaces the stored list). Lowercased and de-duplicated; exact
            match, subdomains are not included. `[]` empties it. Enforced only
            when the access mode is `specific`. A non-empty list is refused for
            a form.
          example:
            - acme.com
      example:
        status: inactive
        expiresAt: '2026-12-31T23:59:59+09:00'
    DocumentDetail:
      description: >
        A `DocumentSummary` plus description, expiry, a protection summary with
        the two allow lists, and the

        `revision`. `status` is the one visitors get (the same value as in the
        list, including `expired`).

        This is also the body a successful `PATCH /api/preview/documents/{id}`
        returns.
      allOf:
        - $ref: '#/components/schemas/DocumentSummary'
        - type: object
          required:
            - description
            - expiry
            - protection
            - revision
          properties:
            description:
              type:
                - string
                - 'null'
              description: The document's subtitle, or `null` when none is set.
            expiry:
              type: object
              required:
                - type
                - expiresAt
                - days
              properties:
                type:
                  type: string
                  enum:
                    - none
                    - fixed_date
                    - relative
                  description: >-
                    `relative` expires per viewer, a number of `days` after they
                    first open it.
                expiresAt:
                  type:
                    - string
                    - 'null'
                  format: date-time
                  description: Set for `fixed_date`, otherwise `null`.
                days:
                  type:
                    - integer
                    - 'null'
                  description: Set for `relative`, otherwise `null`.
              description: When the document expires.
            protection:
              type: object
              description: >-
                How the document is gated. Change the email and allow-list
                fields with `PATCH`; the password and the access mode are
                read-only here. Never contains the password.
              required:
                - accessMode
                - requireEmail
                - requireEmailVerification
                - passwordProtected
                - allowedEmails
                - allowedDomains
              properties:
                accessMode:
                  type: string
                  enum:
                    - public
                    - specific
                  description: >-
                    `public` (anyone with the link) or `specific` (only allowed
                    emails and domains).
                requireEmail:
                  type: boolean
                  description: Viewers are asked for an email (a lead gate).
                requireEmailVerification:
                  type: boolean
                  description: >-
                    Whether a viewer must confirm their email by clicking a link
                    we email them before opening the document. It only applies
                    when an email gate is on (`requireEmail`, or `accessMode:
                    specific`).
                passwordProtected:
                  type: boolean
                  description: >-
                    Viewers must enter a password. The password itself is never
                    returned.
                allowedEmails:
                  type: array
                  items:
                    type: string
                    format: email
                  description: >-
                    Lowercase email addresses allowed in (when `accessMode` is
                    `specific`).
                allowedDomains:
                  type: array
                  items:
                    type: string
                  description: >-
                    Lowercase bare domains allowed in (when `accessMode` is
                    `specific`), exact match.
            revision:
              type: string
              description: >-
                Identifies this version of the settings (an ISO 8601 UTC
                timestamp, milliseconds). Also sent as the `ETag` header; send
                it back as `revision` in the body of `PATCH`. Treat it as
                opaque.
      example:
        id: 7f1c0f7e-3c5e-4b1a-9d34-2f0a5b6c8d90
        title: Pricing deck
        kind: pdf
        status: live
        url: https://docurise.io/v/pricing-deck
        createdAt: '2026-09-30T12:00:00.000Z'
        updatedAt: '2026-10-01T08:30:00.000Z'
        expiresAt: '2026-12-31T00:00:00.000Z'
        description: Q4 pricing
        expiry:
          type: fixed_date
          expiresAt: '2026-12-31T00:00:00.000Z'
          days: null
        protection:
          accessMode: public
          requireEmail: true
          requireEmailVerification: false
          passwordProtected: false
          allowedEmails: []
          allowedDomains: []
        revision: '2026-10-01T08:30:00.000Z'
    ErrorResponse:
      type: object
      required:
        - error
      properties:
        error:
          type: object
          required:
            - code
            - message
            - retryable
          properties:
            code:
              type: string
              description: >-
                Stable machine-readable error code — switch on this, not
                `message`.
            message:
              type: string
              description: Human-readable detail. May change wording without notice.
            retryable:
              type: boolean
              description: Whether retrying the identical request may succeed.
            retryAfterSeconds:
              type: integer
              description: Present only when a concrete backoff hint applies.
            details:
              type: object
              description: >
                Present only for a few specific codes:

                - `PROTECTION_RULE_CONFLICT`: `{ rule: string }`, the protection
                rule that refused the request
                  (`form`, `restricted_access`, `paid_lead_gate`, `free_lead_gate` or `form_no_access_controls`).
                  Open-ended: tolerate new values.
                - `NEXT_POST_RULE_EXISTS`: `{ ruleId: string }`, the existing
                next-post rule.

                - `NOTION_FETCH_FAILED`: `{ notionErrorCode: string }`, one of
                  `NOTION_ERROR_PRIVATE`, `NOTION_ERROR_NOT_FOUND`,
                  `NOTION_ERROR_NETWORK`, `NOTION_ERROR_RATE_LIMIT`,
                  `NOTION_ERROR_TEMPORARILY_UNAVAILABLE`,
                  `NOTION_ERROR_PAGE_REJECTED`, `NOTION_ERROR_UNKNOWN`.
                - `HTML_REJECTED`: `{ reason: string }`, one of `too_large`,
                `css_rejected` or `invalid`.
                  Open-ended: tolerate new values.
                - `CONDITION_NOT_SUPPORTED`: `{ field: string, path: string }`,
                the condition type that is not
                  accepted and where it sits in the request. For a condition shape refused where it sits
                  also `operator: string` and, when there is an equivalent operator for `requiredConditions`,
                  `invertTo: string`.
                - `REFERENCE_NOT_FOUND`: `{ path: string, kind: string }`, where
                the unknown id sits in the
                  request and what it refers to: `document`, `product`, `tier`, `cta`, `folder`, `broadcast` or
                  `step`. Open-ended: tolerate new values.
                - `TAG_NOT_FOUND` (from a group rule): `{ tag: string }`, the
                tag name that does not exist.

                - `GROUP_REFERENCE_NOT_FOUND`: `{ groupIds: string[] }`, the
                group ids that are not in the account.

                - `GROUP_LIMIT_REACHED`: `{ limit: integer }`, the most groups
                an account can create through the API.
          description: >-
            The error. Every error response from the API has it: `code`,
            `message`, `retryable` and, for some codes, `retryAfterSeconds` and
            `details`.
    DocumentSummary:
      type: object
      required:
        - id
        - title
        - kind
        - status
        - url
        - createdAt
        - updatedAt
        - expiresAt
      properties:
        id:
          type: string
          format: uuid
          description: The document's id.
        title:
          type: string
          description: The document's title.
        kind:
          type: string
          enum:
            - notion
            - pdf
            - upload
            - post
            - form
          description: >-
            What the document is made of: `notion`, `pdf`, `upload` (a file you
            uploaded), `post` or `form`.
        status:
          type: string
          enum:
            - live
            - draft
            - inactive
            - expired
          description: >-
            The status: `live` (visitors can open it), `draft` (a post or form
            that is not published yet), `inactive` (turned off, by you or
            automatically, for example when a Notion page can no longer be read)
            or `expired` (its expiry date has passed).
        url:
          type: string
          format: uri
          description: The public share link.
        createdAt:
          type: string
          format: date-time
          description: When the document was created (RFC 3339, UTC).
        updatedAt:
          type: string
          format: date-time
          description: When the document was last changed (RFC 3339, UTC).
        expiresAt:
          type:
            - string
            - 'null'
          format: date-time
          description: >-
            The fixed expiry date, or `null` when there is none. A per-viewer
            (relative) expiry has no single date, so it is `null` here; see
            `expiry` on the single-document response.
  responses:
    Unauthorized:
      description: 'Missing, malformed, or revoked API key (`code: UNAUTHORIZED`).'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    DocumentNotFound:
      description: |
        `DOCUMENT_NOT_FOUND`: no such document for this account (unknown,
        malformed or someone else's id, all indistinguishable). Or `NOT_FOUND`
        if the API is not available to the account.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    RateLimited:
      description: >-
        Rate limited (`code: RATE_LIMITED`, always retryable): see the
        `Retry-After` header and `X-RateLimit-*` headers. Or too many requests
        running at once (`code: TOO_MANY_CONCURRENT_REQUESTS`, always
        retryable): `Retry-After` is `1` and there are no `X-RateLimit-*`
        headers.
      headers:
        Retry-After:
          schema:
            type: integer
        X-RateLimit-Limit:
          schema:
            type: integer
        X-RateLimit-Remaining:
          schema:
            type: integer
        X-RateLimit-Reset:
          schema:
            type: integer
          description: Unix timestamp (seconds) when the limit resets.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Unavailable:
      description: |
        A server-side dependency is temporarily down (`RATE_LIMIT_UNAVAILABLE`
        or `SERVICE_UNAVAILABLE`). Not a problem with the request or key;
        always retryable — honor `retryAfterSeconds` when present. The group
        endpoints (`POST /segments`, `POST /segments/preview`,
        `GET /segments/{id}`) also return it when a very heavy rule could not be
        counted within about 10 seconds: simplify the rule or retry.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      bearerFormat: dr_live_<keyId>_<secret>
      description: |
        A key generated from Settings → Developers, sent as
        `Authorization: Bearer dr_live_<12 hex chars>_<64 hex chars>`. Keys
        carry explicit scopes — each endpoint states the one it requires.
        The full key is shown exactly once at generation time and cannot be
        retrieved again; only the `keyId` prefix is shown afterward. A
        revoked key fails auth on its very next request.

````

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