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

# 문서 수정

> 초안 포스트나 폼을 게시하고, 문서를 일시정지하거나 다시 켜고, 만료일과 이메일 보호 설정을 변경합니다. GET이 반환하는 `revision` 또는 `ETag`를 그대로 복사해 요청 본문에 보내는 문서 revision으로 보호됩니다



## OpenAPI

````yaml api-reference/openapi.ko.yaml PATCH /api/preview/documents/{id}
openapi: 3.1.0
info:
  title: DocuRise 퍼블릭 API
  version: preview
  description: >
    DocuRise 프로그래매틱 접근입니다.


    **이 API는 프리뷰 단계입니다.** 모든 경로는 `/api/preview/` 하위에 버전이 지정되어 있으며,

    프리뷰 단계에서는 호환성을 보장하지 않습니다 — 필드, 값, 에러 코드가 추가, 변경 또는 제거될

    수 있습니다. 계약이 안정화되면 `/api/v1/`로 승격됩니다.

    변경 사항은 [변경 이력](/ko/changelog)에서 확인하세요.


    ### 접근 조건


    모든 라우트는 다음을 요구합니다:

    1. 유효한 API 키 (아래 인증 참고).

    2. 호출하는 계정의 요금제가 **Iconic 이상**이어야 합니다.

    3. 프리뷰 기간 동안 API는 일부 계정에서만 사용이 가능하며, 요금제와는 별개입니다.
       사용할 수 없는 계정은 아래 모든 라우트에서 `404`를 받습니다. 사용할 수 있어야
       하는 계정이라면 DocuRise 담당자에게 문의하세요.

    키 발급 및 관리는 대시보드의 **Settings → Developers**에서 할 수 있습니다.


    ### 멱등성(Idempotency)


    리소스를 생성하는 `POST` 엔드포인트는 `Idempotency-Key` 헤더가 필요합니다. 같은 키와 같은

    요청 본문으로 재요청하면 두 번째 리소스를 만드는 대신 원래 응답(동일한 상태 코드, 동일한

    본문)을 그대로 반환합니다 — 타임아웃이나 연결 끊김 시 안전하게 재시도할 수 있습니다. 키는

    7일 동안 기억되며, 그 이후에 같은 키로 요청하면 새 요청으로 처리됩니다. 같은

    키를 **다른** 본문으로 재사용하면 `IDEMPOTENCY_KEY_CONFLICT`로 거부됩니다. 매 논리적 작업마다

    새로운 키(UUID나 ULID면 충분)를 생성하세요 — HTTP 시도마다 새로 생성하지 마세요.


    예외: `POST /api/preview/tags`는 `Idempotency-Key`를 받지 않습니다. 같은 이름의 태그를 두 번

    만들어도 아무것도 바뀌지 않으므로(두 번째 호출은 기존 태그를 `created: false`로 반환), 재시도는

    항상 안전합니다.


    또 다른 예외: `POST /api/preview/segments/preview`는 `Idempotency-Key`를 받지 않습니다.

    아무것도 저장하지 않으므로 반복 호출해도 항상 안전합니다.


    ### 응답 호환성


    응답은 시간이 지나면서 필드가 추가될 수 있고, `type`/`code` 형태의 열거값(예를 들어 향후

    추가될 `LeadFormQuestion.type` 값이나 새로운 에러 `code`)도 새 값이 늘어날 수 있습니다.

    알지 못하는 필드나 열거값은 항상 무시하고 에러로 취급하지 마세요 — 이것이 `preview` 기간

    동안의 모든 추가적 변경을 하위 호환되게 만드는 원칙입니다.


    응답에서 필드가 아예 없는 것과 값이 `null`로 존재하는 것은 항상 같은 의미가 아닙니다. 타입에

    `null`이 전혀 포함되지 않는 필드는 해당하지 않을 때 아예 생략됩니다 — `null`로 전송되는 일은

    없습니다(예를 들어 `retryAfterSeconds`는 백오프 힌트가 없을 때 완전히 생략되며, `null`로

    설정되지 않습니다). 타입에 `null`을 명시적으로 허용하는 필드는 "값이 없음"과는 구분되는 실제

    의미로 `null`을 사용합니다(예를 들어 문서의 `expiresAt: null`은 "알 수 없음"이나 "해당

    없음"이 아니라, 고정 만료일이 없다는 뜻입니다).


    모든 필드명은 `notionUrl`, `leadFormQuestions`, `collectName` 등과 같이 모든 요청/응답 본문에서

    일관되게 `camelCase`를 사용합니다.


    ### 식별자


    `id` 값은 불투명한(opaque) 문자열입니다. 구조화되지 않은 토큰으로 저장하고 비교하세요 —

    파싱하거나 형식을 검증하거나 길이·문자 구성에 대해 어떤 가정도 하지 마세요.


    ### 에러


    모든 에러 응답은 동일한 형식을 갖습니다:


    ```json

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

    ```


    `message`가 아니라 `code`를 기준으로 분기하세요 — `message`는 로그/디버깅용 자유 텍스트이며

    예고 없이 문구가 바뀔 수 있습니다. `code`가 안정적인 계약입니다. `retryable`은 동일한 요청을

    그대로 재시도해도 되는지를 나타내며, 존재할 경우 `retryAfterSeconds`가 구체적인 백오프

    힌트를 제공합니다.


    요청 본문에 NUL 문자(`\u0000`)가 문자열 값이나 속성 이름에 들어 있으면 아무것도 저장하기 전에

    `400 VALIDATION_ERROR`로 거부됩니다.


    ### 속도 제한


    계정이 보유한 키 개수와 무관하게, 모든 `/api/preview/*` 라우트에 대해 계정당 분당 60회

    요청으로 제한됩니다. 초과 시 `Retry-After` 헤더와 함께 `429`가 반환됩니다.


    이와 별도로, 계정이 보유한 키 개수와 무관하게 이 라우트들에서 한 계정이 동시에 실행 중인

    요청은 최대 10개입니다. 11번째가 될 요청은 `code: TOO_MANY_CONCURRENT_REQUESTS`와

    `Retry-After: 1`이 담긴 `429`를 반환합니다. 실행 중인 요청 하나가 끝날 때까지 기다린 뒤

    재시도하세요. 이 제한은 아직 끝나지 않은 요청을 세므로, 분당 60회 이내여도 적용될 수 있습니다.
  contact:
    name: DocuRise
    url: https://docurise.io
servers:
  - url: https://docurise.io
    description: 프로덕션
security:
  - apiKey: []
tags:
  - name: Documents
    description: >-
      문서(노션 페이지, 포스트, 리드 수집 폼)를 생성하고, 내가 소유한 문서를 목록 조회하거나 읽고, 초안을 게시하며, 상태, 만료일,
      이메일 보호 설정을 변경합니다.
  - name: Analytics
    description: >-
      내 문서 하나의 조회, 방문자, 리드, 전환 집계 수치를 조회하고, 유입 채널별 분포, 이전 기간과의 비교, 폼 퍼널로도 확인하며,
      내 모든 문서를 조회, 방문자, 리드 기준으로 순위를 매겨 조회합니다.
  - name: Folders
    description: 폴더와 강의를 항목 수와 함께 조회합니다.
  - name: Products
    description: 상품 카탈로그를 조회합니다(읽기 전용).
  - name: Audiences
    description: >-
      태그와 그룹을 연락처 수와 함께 조회하고, 규칙으로 그룹을 만들고, 규칙에 맞는 연락처 수를 미리 세고, 그룹 하나를 규칙과 함께
      조회합니다. 수치만 제공하며 연락처 정보는 절대 반환하지 않습니다.
  - name: Contacts
    description: 내 연락처 하나에 태그를 추가하거나 제거합니다. 연락처 정보는 반환하지 않습니다.
  - name: Email
    description: 이메일 1회성 발송, 1회성 발송별 집계 수치, 자동화를 조회합니다. 이메일만 해당하며 카카오는 포함되지 않습니다.
  - name: Instagram
    description: Instagram 댓글-DM 자동화 규칙을 생성, 조회, 수정, 일시정지, 재개합니다.
  - name: Account
    description: 호출에 사용 중인 API 키를 확인합니다.
paths:
  /api/preview/documents/{id}:
    parameters:
      - name: id
        in: path
        required: true
        description: 문서 ID입니다.
        schema:
          type: string
          format: uuid
    patch:
      tags:
        - Documents
      summary: 문서 상태, 만료 또는 이메일 보호 수정
      description: >
        초안 포스트나 폼에 `status: live`를 지정하면 게시되며, 게시할 때 아래 확인을 거칩니다. 게시된 문서는 초안으로
        되돌릴 수 없습니다.


        문서를 일시정지하거나 다시 켜고, 만료일을 지정하거나 해제하며, 방문자에게 이메일을

        요구하는 방식을 바꿉니다. `documents:write` 범위가 필요합니다. 바꾸려는 필드만

        보내세요. 그 외의 필드(`slug`, `password`, `accessMode` 등)는 조용히 무시되지 않고

        `400 VALIDATION_ERROR`가 됩니다. 비밀번호, 접근 방식, slug는 API로

        변경할 수 없습니다.


        **요청 본문의 `revision`은 필수입니다.** 먼저 `GET /api/preview/documents/{id}`로 문서를

        읽고, 그 `revision`을 변경할 필드와 함께 본문의 `revision` 필드로 보내세요. 그 사이에

        문서가 바뀌었다면 `409 REVISION_CONFLICT`가 반환되고

        아무것도 적용되지 않으며, 그 응답의 `ETag` 헤더에 현재 revision이 들어 있으므로

        문서를 다시 확인한 뒤 재시도할 수 있습니다. 필드가 없으면

        `428 PRECONDITION_REQUIRED`입니다. `*`와 목록 값은 `400`으로 거부됩니다.

        보낼 때는 일반 타임스탬프를 쓰는 것이 기본이며, `ETag` 헤더 값도 따옴표로 감싼 `"..."`

        형태나 약한 형식 `W/"..."` 그대로 복사해 보낼 수 있습니다(일부 게이트웨이와 프록시는

        응답을 압축하면서 ETag를 약한 형식으로 바꿉니다). `revision`만 담은 본문은 변경할 내용이

        없어 `400`입니다.


        **`If-Match`를 보내지 마세요.** API를 제공하는 플랫폼은 성공 응답의 `ETag`가 요청의

        `If-Match`와 다르면 `412 PRECONDITION_FAILED`로 바꿔 응답하는데, 성공한 `PATCH`는

        항상 새 `ETag`를 반환하므로 변경은 적용되었는데 실패한 것처럼 보이게 됩니다.

        그래서 API는 `If-Match` 또는 `If-Unmodified-Since` 헤더가 담긴 요청을, 본문에 `revision`이

        함께 있더라도 아무것도 변경하기 전에 `400 VALIDATION_ERROR`로 거부합니다.

        revision 확인과 변경 적용이 한 번에 이루어지므로, 같은 revision을 가진

        두 스크립트가 모두 성공할 수는 없습니다. 현재 revision(ETag)을 읽으려면

        `documents:read`가, 설정을 바꾸려면 `documents:write`가 키에 필요합니다.

        자동화에는 두 범위를 모두 부여하세요.


        **`status`**


        | 문서 종류 | `inactive` | `live` | `draft` |

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

        | 노션, PDF, 업로드 | 일시정지 | 다시 켬 | `422 STATUS_NOT_SUPPORTED` |

        | 포스트, 폼 (게시됨) | 일시정지 | 다시 켬 | `422 CANNOT_REVERT_TO_DRAFT` |

        | 포스트, 폼 (초안) | `422 STATUS_NOT_SUPPORTED` | 게시 | 변경 없음 |


        **초안 게시.** 초안 포스트나 폼에 `live`를 지정하면 아래 확인을 거쳐 게시됩니다. 요청은 전부 적용되거나 전혀 적용되지
        않습니다. 확인 중 하나라도 실패하면

        문서는 초안으로 남고 같은 요청의 다른 변경도 적용되지 않습니다. 확인은 다음 순서로 진행됩니다.


        | 확인 | 대상 | 실패하면 |

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

        | 포스트가 이메일 1회성 발송 본문이 아님(1회성 발송 포스트는 이메일 흐름으로 게시됩니다) | 포스트 | `422
        BROADCAST_BACKED_POST` |

        | 요금제의 게시 문서 한도에 여유가 있음 | 포스트, 폼 | `403 DOCUMENT_LIMIT_REACHED` |

        | 포스트에 내용(텍스트, 이미지 또는 영상)이 있음 | 포스트 | `422 EMPTY_BODY` |

        | 본문에 `{recipient_name}` 같은 이메일 변수가 없음 | 포스트 | `422
        UNSUPPORTED_VARIABLE` |

        | 본문의 영상 중 처리에 실패했거나 길이 제한을 넘는 것이 없음(아직 인코딩 중인 영상은 괜찮습니다) | 포스트 | `422
        VIDEO_FAILED`, `422 VIDEO_OVER_LIMIT` |

        | 가격이 있는 포스트에 방문자가 구매할 수 있는 상품이 하나 이상 있음 | 포스트 | `422
        PAID_LEAD_GATE_NO_PRODUCTS` |


        초안은 요금제 한도에 포함되지 않으며, 게시하는 순간부터 포함됩니다. 포스트의 주소는 게시할 때

        바뀔 수 있으므로, 최종 `url`은 문서를 다시 조회해서 확인하세요. 게시는 본문을 다시 쓰지

        않습니다. 콘텐츠는 저장된 그대로 게시됩니다. 콘텐츠, 제목, 버튼, 상품은 대시보드에서

        수정하세요.


        게시는 문서가 켜져 있는지 여부를 바꾸지 않습니다. 가격이 있는 초안은

        `inactive` 상태로 게시됩니다. 켜려면 `status: live`를 다시 보내세요. 이 두 번째

        호출에서 가격에 방문자가 구매할 수 있는 상품이 있는지 확인하며, 없으면

        `422 PAID_LEAD_GATE_NO_PRODUCTS`입니다. 첫 호출의 응답에는 `status: inactive`가
        표시됩니다.


        `expiresAt`이나 이메일 보호 필드가 같은 요청에 함께 있으면

        게시와 같은 쓰기로 적용되거나, 전혀 적용되지 않습니다.


        초안은 항상 `draft`로 표시되므로, 초안에 `inactive`를 지정하면 어디에도 표시되지 않는 값을

        기록하는 대신 거부됩니다.


        가격은 있지만 방문자가 구매할 수 있는 상품이 없는 일시정지 문서를 `live`로

        켜려는 경우 `422 PAID_LEAD_GATE_NO_PRODUCTS`입니다. 먼저 대시보드에서

        상품을 정리하세요. 업로드나 교체가 아직 처리 중인 PDF에 `live`를 지정하면

        `422 DOCUMENT_NOT_READY`입니다. 파일 처리가 끝날 때까지 기다린 뒤 다시 시도하세요.


        **`expiresAt`**은 `Z` 또는 숫자 오프셋이 붙은 RFC 3339 날짜시간입니다

        (예: `2026-12-31T23:59:59+09:00`). UTC 시각으로 저장됩니다. `null`은

        만료일을 제거합니다. 과거 시각도 허용되며, 문서가

        즉시 만료됩니다(`status`가 `expired`로 표시됨). 새로 지정했거나 더

        이른 만료일은 방문자가 이미 잠금 해제한 세션도 종료시키므로, 방문자는 다시

        잠금을 해제해야 합니다. 열람자별 상대 만료(각 열람자가 처음 연 뒤 일정

        일수)를 쓰는 문서는 여기서 만료를 바꿀 수 없으며

        `422 RELATIVE_EXPIRY_NOT_SUPPORTED`입니다. 이 경우에도 `status`는 바꿀 수

        있습니다.


        **이메일 보호** (`requireEmail`, `requireEmailVerification`, `allowedEmails`,

        `allowedDomains`)


        | 필드 | 의미 |

        |---|---|

        | `requireEmail` | `true`이면 방문자가 문서를 보기 전에 이메일을 요구받습니다. |

        | `requireEmailVerification` | `true`이면 방문자가 해당 이메일로 보낸 링크로 이메일을 확인해야
        합니다. |

        | `allowedEmails` | 입장이 허용된 이메일 주소입니다. 최대 10,000개. `[]`는 목록을 비웁니다. |

        | `allowedDomains` | 입장이 허용된 도메인입니다. 예: `acme.com`. 정확히 일치하는 도메인만 해당하며
        하위 도메인은 포함되지 않습니다. 최대 10,000개. `[]`는 목록을 비웁니다. |


        형식이 잘못된 주소나 도메인은

        `400 VALIDATION_ERROR`이며, `message`에는 필드와 항목의 위치가 표시되고 값은

        표시되지 않습니다. 항목은 소문자로 바뀌고 중복은 제거됩니다. 같은 집합을 순서나

        대소문자만 바꿔 다시 보내면 아무것도 바뀌지 않습니다. 보내지 않은 필드는 그대로

        유지됩니다.


        허용 목록은 문서의 접근 방식이 `specific`일 때만 적용됩니다(응답의

        `protection.accessMode`). API로는 접근 방식을 설정할 수 없으므로 대시보드에서

        변경하세요. `public` 문서에서는 목록이 저장되기만 하고 적용되지는 않습니다.


        일부 규칙은 `requireEmail`을 켜 두게 합니다. 이런 문서에 `requireEmail: false`를 보내면

        `422 PROTECTION_RULE_CONFLICT`이며, 요청의 어떤 부분도 적용되지 않고

        `error.details.rule`이 규칙 이름을 알려 줍니다.


        | `details.rule` | `requireEmail`을 끌 수 없는 이유 |

        |---|---|

        | `form` | 폼은 항상 방문자의 이메일을 수집합니다. |

        | `restricted_access` | 문서의 접근 방식이 `specific`이며, 방문자를 이메일로 식별합니다. |

        | `paid_lead_gate` | 문서에 가격이 있는 리드 게이트가 있으며, 가격을 보여 주려면 이메일이 필요합니다. |

        | `free_lead_gate` | 리드 게이트(흐린 미리보기)가 켜져 있습니다. 먼저 대시보드에서 게이트를 끄세요. |


        폼은 이메일 인증과 허용 목록을 적용하지 못하므로, 폼에 `requireEmailVerification`을

        켜거나 비어 있지 않은 `allowedEmails` 또는 `allowedDomains`를 보내는 것도

        `422 PROTECTION_RULE_CONFLICT`(`details.rule:
        form_no_access_controls`)입니다. 끄거나 빈

        목록을 보내는 것은 허용됩니다.


        **세션.** 게이트를 켜거나 더 엄격하게 하면 방문자가 이미 잠금 해제한 세션이

        종료되어 다시 잠금 해제해야 합니다. `requireEmail`을 켜는 경우(문서에 이미

        비밀번호가 걸려 있으면 해당하지 않음), `requireEmailVerification`을 켜는 경우,

        허용 목록을 제한하는 경우(비어 있던 목록에 첫 항목을 추가하는 경우, 항목을 삭제하거나

        바꾸는 경우, 또는 접근 방식이 `specific`인 문서의 목록을 비우는 경우)입니다. 완화하는

        경우(설정을 끄거나 이미 항목이 있는 목록에 항목을 추가하는 경우)에는 기존 세션이

        유지됩니다. 한 요청에서 여러 가지를 바꿔도 방문자는 한 번만

        로그아웃됩니다.


        변경은 전부 적용되거나 전혀 적용되지 않습니다. 일부라도 거부되면 아무것도

        적용되지 않습니다. 아무것도 바꾸지 않는 요청은 성공하며 revision도 그대로

        유지됩니다. 이미 `live`인 문서에 `live`를 지정해도 아무것도 바뀌지 않습니다.

        revision은 그대로이고 응답은 200입니다. 고정 만료일이 지난 문서에 `live`를 지정하면 문서는 켜지지만,
        `status`는 만료를

        반영하므로 `expired`로 유지됩니다. 만료된 문서를 다시 `live`로 만들려면 `expiresAt`을 연장하세요.


        응답은 변경 후의 문서이며, `GET /api/preview/documents/{id}`와 같은 본문입니다. 새

        `revision`이 본문과 `ETag` 헤더에 들어 있습니다. 응답의 `status`는 방문자가 실제로 겪는

        상태이므로 만료를 반영하며, `live`를 지정한 직후에도 `expired`로 표시될 수 있습니다.
      operationId: updateDocument
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateDocumentRequest'
            examples:
              pause:
                summary: 문서 일시정지
                value:
                  status: inactive
              expire:
                summary: 연말(서울 시간)에 만료
                value:
                  expiresAt: '2026-12-31T23:59:59+09:00'
              clearExpiry:
                summary: 만료일 제거
                value:
                  expiresAt: null
              requireEmail:
                summary: 방문자에게 이메일 요구
                value:
                  requireEmail: true
              verifyEmail:
                summary: 방문자가 이메일을 확인하도록 요구
                value:
                  requireEmail: true
                  requireEmailVerification: true
              allowList:
                summary: 허용 목록 교체
                value:
                  allowedEmails:
                    - alice@example.com
                  allowedDomains:
                    - acme.com
              clearAllowList:
                summary: 이메일 허용 목록 비우기
                value:
                  allowedEmails: []
              publish:
                summary: 초안 포스트 또는 폼 게시
                value:
                  status: live
      responses:
        '200':
          description: 변경 후의 문서입니다.
          headers:
            ETag:
              description: 따옴표로 감싼 새 `revision`입니다. 다음 변경의 본문에 `revision`으로 보내세요.
              schema:
                type: string
                example: '"2026-10-04T01:02:09.000Z"'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/DocumentDetail'
        '400':
          description: >-
            `VALIDATION_ERROR`(`message` 앞에 문제가 된 필드가 붙습니다: `password`나
            `accessMode`처럼 알 수 없거나 범위 밖인 필드, 잘못된 `status`, `Z` 또는 오프셋이 있는 RFC
            3339가 아닌 `expiresAt`, 올바른 주소나 도메인이 아닌
            `allowedEmails`/`allowedDomains` 항목이나 10,000개를 넘는 목록, 불리언이 아닌
            `requireEmail`/`requireEmailVerification`, 빈 본문, `GET`이 반환한 문자열이 아닌
            `revision`(`*`, 목록 포함)) 또는 `INVALID_JSON`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          description: >-
            `PAST_DUE_RESTRICTED`, `PLAN_REQUIRED`, `INSUFFICIENT_SCOPE`(키에
            `documents:write` 범위가 없습니다) 또는 `DOCUMENT_LIMIT_REACHED`(초안을 게시하면
            요금제의 게시 문서 한도를 넘게 됩니다. `retryable: false`이며 `details`는 없고, 한도를 넘은 문서
            생성과 같은 본문입니다). 아무것도 적용되지 않았습니다.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          $ref: '#/components/responses/DocumentNotFound'
        '409':
          description: >-
            `REVISION_CONFLICT`(`retryable: false`): 보낸 revision 이후에 문서가 바뀌었거나,
            게시하려던 초안이 그 사이에 게시되었습니다. 아무것도 적용되지 않았습니다. 현재 revision은 본문이 아니라
            `ETag` 헤더로만 반환됩니다. 문서를 다시 읽고 다시 적용할지 판단하세요. `ETag` 헤더는
            `REVISION_CONFLICT`일 때만 전달됩니다. 또는 `SLUG_TAKEN`(`retryable: false`):
            포스트를 게시하는 동안 생성된 웹 주소를 같은 순간에 다른 문서가 사용했습니다. 아무것도 적용되지 않았고 포스트는 여전히
            초안입니다. 문서를 읽어 현재 revision을 확인한 뒤 요청을 다시 보내세요.
          headers:
            ETag:
              description: >-
                따옴표로 감싼 현재 revision입니다. `REVISION_CONFLICT`일 때만 전달되며,
                `SLUG_TAKEN`에는 `ETag` 헤더가 없습니다.
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '422':
          description: >
            요청 형식은 올바르지만 규칙에 의해 거부되었으며(`retryable: false`),

            아무것도 적용되지 않았습니다. `code`는 다음 중 하나입니다:

            `STATUS_NOT_SUPPORTED`(노션, PDF, 업로드 문서에 `draft`, 또는 초안 포스트나 폼에
            `inactive`),

            `CANNOT_REVERT_TO_DRAFT`(게시된 포스트나 폼),

            `RELATIVE_EXPIRY_NOT_SUPPORTED`(문서가 열람자별 상대 만료를 사용),

            `PAID_LEAD_GATE_NO_PRODUCTS`(사용할 수 있는 상품이 없는 유료 문서를 켜거나 게시하려는 경우),

            `DOCUMENT_NOT_READY`(PDF의 업로드나 교체가 아직 처리 중일 때 `live`를 지정한 경우이며, 파일
            처리가 끝날 때까지 기다린 뒤 다시 시도하세요),

            `EMPTY_BODY`(내용이 없는 포스트를 게시하려는 경우),

            `UNSUPPORTED_VARIABLE`(본문에 이메일 변수가 있는 포스트를 게시하려는 경우),

            `VIDEO_FAILED`(처리에 실패한 영상이 있는 포스트를 게시하려는 경우),

            `VIDEO_OVER_LIMIT`(길이 제한을 넘는 영상이 있는 포스트를 게시하려는 경우),

            `BROADCAST_BACKED_POST`(포스트가 이메일 1회성 발송의 본문이며 이메일 흐름으로 게시되는 경우),

            `PROTECTION_RULE_CONFLICT`(이메일을 계속 요구해야 하는 문서에 `requireEmail:
            false`를 보냈거나, 폼에 이메일 인증이나 허용 목록을 켜려는 경우이며, `error.details.rule`이 규칙
            이름을 알려 줍니다. 작업 설명의 표를 참고하세요).
          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`): 본문에 `revision`이 없습니다.
            문서를 읽고 그 `revision`을 본문에 보내세요(`If-Match`가 아닙니다).
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          description: '`INTERNAL_ERROR`: 예기치 않은 오류. 재시도 가능하며 원인은 반환되지 않습니다.'
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '503':
          $ref: '#/components/responses/Unavailable'
      security:
        - apiKey: []
      x-codeSamples:
        - lang: bash
          label: cURL
          source: >
            # 1) 문서를 읽고 "revision"을 확인합니다

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

            # 2) 본문에 revision을 함께 보내면서 문서를 일시정지하고 연말에 만료되도록 설정합니다

            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 (초안 게시)
          source: >
            # 1) 초안을 읽고 "revision"을 확인합니다

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

            # 2) 본문에 revision을 보내 게시합니다

            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`이 필수이며, 그 외에 최소 한 개의 필드가 필요합니다. 알 수 없는 필드는 `400
        VALIDATION_ERROR`로 거부됩니다.
      type: object
      additionalProperties: false
      minProperties: 2
      required:
        - revision
      properties:
        revision:
          type: string
          description: >-
            `GET` 또는 `PATCH`가 마지막으로 반환한 문서의 `revision`입니다(보낼 때는 일반 타임스탬프를 쓰는 것이
            기본이며, `ETag` 헤더 값도 따옴표로 감싼 `"..."` 형태나 약한 형식 `W/"..."` 그대로 허용됩니다).
            오래된 값은 `409 REVISION_CONFLICT`입니다. `If-Match`가 아니라 여기에 보내세요.
          example: '2026-10-04T01:02:03.456Z'
        status:
          type: string
          enum:
            - live
            - draft
            - inactive
          description: >-
            초안 포스트나 폼에 `live`를 지정하면 게시 확인을 거쳐 게시되며, 게시된 포스트나 폼은 `draft`로 되돌릴 수
            없습니다. 게시된 문서는 `live`와 `inactive`로 일시정지하고 다시 켤 수 있습니다. 문서 종류별로 각 값이
            하는 일은 작업 설명의 표를 참고하세요.
        expiresAt:
          type:
            - string
            - 'null'
          format: date-time
          description: >-
            `Z` 또는 숫자 오프셋이 붙은 RFC 3339이며 UTC로 저장됩니다. `null`은 만료일을 제거합니다. 만료일을
            그대로 두려면 생략하세요.
          example: '2026-12-31T23:59:59+09:00'
        requireEmail:
          type: boolean
          description: >-
            `true`이면 방문자에게 이메일을 요구하고, `false`이면 요구하지 않습니다. 폼, 접근 방식이 `specific`인
            문서, 리드 게이트가 있는 문서에 `false`를 보내면 `422 PROTECTION_RULE_CONFLICT`입니다.
            켜면 방문자가 이미 잠금 해제한 세션이 종료됩니다.
        requireEmailVerification:
          type: boolean
          description: >-
            `true`이면 방문자가 링크로 이메일을 확인해야 합니다. 켜면 방문자가 이미 잠금 해제한 세션이 종료됩니다. 폼에는
            허용되지 않습니다(`422 PROTECTION_RULE_CONFLICT`).
        allowedEmails:
          type: array
          maxItems: 10000
          items:
            type: string
            format: email
          description: >-
            허용할 이메일 주소의 전체 목록입니다(저장된 목록을 대체합니다). 소문자로 바뀌고 중복이 제거됩니다. `[]`는 목록을
            비웁니다. 접근 방식이 `specific`일 때만 적용됩니다. 폼에는 비어 있지 않은 목록을 보낼 수 없습니다.
          example:
            - alice@example.com
        allowedDomains:
          type: array
          maxItems: 10000
          items:
            type: string
          description: >-
            허용할 도메인(예: `acme.com`)의 전체 목록입니다(저장된 목록을 대체합니다). 소문자로 바뀌고 중복이 제거되며,
            정확히 일치하는 도메인만 해당하고 하위 도메인은 포함되지 않습니다. `[]`는 목록을 비웁니다. 접근 방식이
            `specific`일 때만 적용됩니다. 폼에는 비어 있지 않은 목록을 보낼 수 없습니다.
          example:
            - acme.com
      example:
        status: inactive
        expiresAt: '2026-12-31T23:59:59+09:00'
    DocumentDetail:
      description: >
        `DocumentSummary`에 설명, 만료 정보, 두 허용 목록이 포함된 보호 방식 요약, `revision`을 더한
        것입니다.

        `status`는 방문자가 실제로 겪는 상태입니다(목록의 값과 같으며 `expired`를 포함합니다). 성공한

        `PATCH /api/preview/documents/{id}`가 반환하는 본문이기도 합니다.
      allOf:
        - $ref: '#/components/schemas/DocumentSummary'
        - type: object
          required:
            - description
            - expiry
            - protection
            - revision
          properties:
            description:
              type:
                - string
                - 'null'
              description: 문서의 부제목이며, 설정하지 않았으면 `null`입니다.
            expiry:
              type: object
              required:
                - type
                - expiresAt
                - days
              properties:
                type:
                  type: string
                  enum:
                    - none
                    - fixed_date
                    - relative
                  description: '`relative`는 열람자별로 만료되며, 열람자가 처음 연 뒤 `days`일이 지나면 만료됩니다.'
                expiresAt:
                  type:
                    - string
                    - 'null'
                  format: date-time
                  description: '`fixed_date`일 때만 값이 있고, 그 외에는 `null`입니다.'
                days:
                  type:
                    - integer
                    - 'null'
                  description: '`relative`일 때만 값이 있고, 그 외에는 `null`입니다.'
              description: 문서가 만료되는 시점입니다.
            protection:
              type: object
              description: >-
                문서가 어떻게 보호되는지입니다. 이메일과 허용 목록 필드는 `PATCH`로 변경하며, 비밀번호와 접근 방식은 여기서
                읽기 전용입니다. 비밀번호는 포함되지 않습니다.
              required:
                - accessMode
                - requireEmail
                - requireEmailVerification
                - passwordProtected
                - allowedEmails
                - allowedDomains
              properties:
                accessMode:
                  type: string
                  enum:
                    - public
                    - specific
                  description: '`public`(링크가 있는 누구나) 또는 `specific`(허용된 이메일과 도메인만).'
                requireEmail:
                  type: boolean
                  description: 열람자에게 이메일을 요구합니다(리드 게이트).
                requireEmailVerification:
                  type: boolean
                  description: >-
                    문서를 열기 전에 방문자가 이메일로 받은 링크를 눌러 이메일 주소를 확인해야 하는지 여부입니다. 이메일
                    입력이 필요한 상태(`requireEmail` 또는 `accessMode: specific`)일 때만
                    적용됩니다.
                passwordProtected:
                  type: boolean
                  description: 열람자가 비밀번호를 입력해야 합니다. 비밀번호 자체는 절대 반환되지 않습니다.
                allowedEmails:
                  type: array
                  items:
                    type: string
                    format: email
                  description: 입장이 허용된 소문자 이메일 주소입니다(`accessMode`가 `specific`일 때 적용).
                allowedDomains:
                  type: array
                  items:
                    type: string
                  description: >-
                    입장이 허용된 소문자 도메인입니다(`accessMode`가 `specific`일 때 적용). 정확히 일치하는
                    도메인만 해당합니다.
            revision:
              type: string
              description: >-
                이 설정의 버전을 나타냅니다(밀리초 단위의 ISO 8601 UTC 타임스탬프). `ETag` 헤더로도 전달되며,
                `PATCH`할 때 본문의 `revision`으로 그대로 보내세요. 값의 의미는 해석하지 말고 그대로 사용하세요.
      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: 안정적인 기계 판독용 에러 코드입니다 — `message`가 아니라 이 값을 기준으로 분기하세요.
            message:
              type: string
              description: 사람이 읽을 수 있는 상세 설명입니다. 예고 없이 문구가 바뀔 수 있습니다.
            retryable:
              type: boolean
              description: 동일한 요청을 재시도했을 때 성공할 가능성이 있는지 여부입니다.
            retryAfterSeconds:
              type: integer
              description: 구체적인 백오프 힌트가 있는 경우에만 존재합니다.
            details:
              type: object
              description: >
                일부 특정 코드에만 존재합니다:

                - `PROTECTION_RULE_CONFLICT`: `{ rule: string }`, 요청을 거부한 보호
                규칙입니다
                  (`form`, `restricted_access`, `paid_lead_gate`, `free_lead_gate` 또는 `form_no_access_controls`).
                  열려 있는 값입니다. 새 값을 허용하세요.
                - `NEXT_POST_RULE_EXISTS`: `{ ruleId: string }`, 이미 있는 "다음 게시물"
                규칙입니다.

                - `NOTION_FETCH_FAILED`: `{ notionErrorCode: string }`, 다음 중
                하나입니다:
                  `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 }`, `too_large`,
                `css_rejected`, `invalid` 중 하나입니다.
                  열려 있는 값입니다. 새 값을 허용하세요.
                - `CONDITION_NOT_SUPPORTED`: `{ field: string, path: string }`,
                받지 않는 조건 타입과 그 조건이
                  요청에서 있는 위치입니다. 그 위치에서 거부된 조건 형태에는 `operator: string`이,
                  `requiredConditions`에서 대신 쓸 수 있는 연산자가 있으면 `invertTo: string`도 더해집니다.
                - `REFERENCE_NOT_FOUND`: `{ path: string, kind: string }`, 알 수
                없는 id가 요청에서 있는 위치와
                  그 id가 가리키는 대상(`document`, `product`, `tier`, `cta`, `folder`, `broadcast`, `step`)입니다.
                  열려 있는 값입니다. 새 값을 허용하세요.
                - `TAG_NOT_FOUND`(그룹 규칙에서): `{ tag: string }`, 존재하지 않는 태그 이름입니다.

                - `GROUP_REFERENCE_NOT_FOUND`: `{ groupIds: string[] }`, 계정에 없는
                그룹 id입니다.

                - `GROUP_LIMIT_REACHED`: `{ limit: integer }`, API로 계정이 만들 수 있는
                그룹의 최대 개수입니다.
          description: >-
            오류 정보입니다. API의 모든 오류 응답에 들어 있으며 `code`, `message`, `retryable`을 담고,
            일부 코드에는 `retryAfterSeconds`와 `details`도 담깁니다.
    DocumentSummary:
      type: object
      required:
        - id
        - title
        - kind
        - status
        - url
        - createdAt
        - updatedAt
        - expiresAt
      properties:
        id:
          type: string
          format: uuid
          description: 문서의 id입니다.
        title:
          type: string
          description: 문서 제목입니다.
        kind:
          type: string
          enum:
            - notion
            - pdf
            - upload
            - post
            - form
          description: >-
            문서가 무엇으로 이루어져 있는지 나타냅니다: `notion`, `pdf`, `upload`(업로드한 파일), `post`,
            `form`.
        status:
          type: string
          enum:
            - live
            - draft
            - inactive
            - expired
          description: >-
            상태입니다. `live`(공개 중, 방문자가 열 수 있음), `draft`(초안, 아직 게시하지 않은 포스트나 폼),
            `inactive`(비활성, 직접 껐거나 Notion 페이지를 더 이상 읽을 수 없는 등의 이유로 자동으로 꺼짐),
            `expired`(만료, 만료일이 지남) 중 하나입니다.
        url:
          type: string
          format: uri
          description: 공개 공유 링크입니다.
        createdAt:
          type: string
          format: date-time
          description: 문서를 만든 시각(RFC 3339, UTC)입니다.
        updatedAt:
          type: string
          format: date-time
          description: 문서를 마지막으로 바꾼 시각(RFC 3339, UTC)입니다.
        expiresAt:
          type:
            - string
            - 'null'
          format: date-time
          description: >-
            고정 만료일이며, 없으면 `null`입니다. 열람자별(상대) 만료는 단일 날짜가 없으므로 여기서는 `null`입니다. 단일
            문서 응답의 `expiry`를 확인하세요.
  responses:
    Unauthorized:
      description: '누락되었거나 형식이 잘못되었거나 폐기된 API 키입니다(`code: UNAUTHORIZED`).'
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    DocumentNotFound:
      description: |
        `DOCUMENT_NOT_FOUND`: 이 계정에 해당하는 문서가 없습니다(알 수 없는 ID, 잘못된 형식의 ID, 다른 사람의
        ID는 모두 구분되지 않습니다). API를 사용할 수 없는 계정이면
        `NOT_FOUND`입니다.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    RateLimited:
      description: >-
        속도 제한에 걸렸습니다 (`code: RATE_LIMITED`, 항상 재시도 가능): `Retry-After` 헤더와
        `X-RateLimit-*` 헤더를 참고하세요. 또는 동시에 실행 중인 요청이 너무 많습니다 (`code:
        TOO_MANY_CONCURRENT_REQUESTS`, 항상 재시도 가능): `Retry-After`는 `1`이고
        `X-RateLimit-*` 헤더는 없습니다.
      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: 제한이 초기화되는 유닉스 타임스탬프(초)입니다.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
    Unavailable:
      description: |
        서버 측 의존성이 일시적으로 다운되었습니다(`RATE_LIMIT_UNAVAILABLE` 또는
        `SERVICE_UNAVAILABLE`). 요청이나 키의 문제가 아니며, 항상 재시도 가능합니다 —
        `retryAfterSeconds`가 있으면 그에 따르세요. 그룹 엔드포인트(`POST /segments`,
        `POST /segments/preview`, `GET /segments/{id}`)는 매우 무거운 규칙의 인원을 약 10초
        안에 셀 수 없을 때도 이 코드를 반환합니다. 규칙을 단순하게 만들거나 다시 시도하세요.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ErrorResponse'
  securitySchemes:
    apiKey:
      type: http
      scheme: bearer
      bearerFormat: dr_live_<keyId>_<secret>
      description: |
        Settings → Developers에서 발급한 키를 `Authorization: Bearer
        dr_live_<12자리 16진수>_<64자리 16진수>` 형태로 전송합니다. 키는 명시적인 범위를
        가지며 — 각 엔드포인트에 필요한 범위가 해당 엔드포인트에 명시되어 있습니다. 전체 키는 발급 시점에 딱 한
        번만 표시되며 다시 조회할 수 없습니다 — 이후에는 `keyId` 접두어만 표시됩니다. 폐기된
        키는 바로 다음 요청부터 인증에 실패합니다.

````

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