be4link개발자 문서 · v1OpenAPI 다운로드

콘텐츠 자동발행 API

be4link 자체 병원 블로그에 외부 서버, n8n, Make가 이미지와 글을 전달하는 API입니다. 글·승인·게시 상태는 콘텐츠 메뉴와 동일하며 원문은 be4link에 보관됩니다.

콘텐츠 API v1은 가입된 회원만 사용할 수 있습니다. 회원 메뉴 → API 키 관리에서 로그인한 프로젝트 소유자가 키를 발급합니다. 프로젝트의 실제 주소는 해당 화면 또는 콘텐츠 → API 연동에서 확인합니다.

시작하기

  1. 병원 홈페이지를 생성하고 게시합니다.
  2. 가입한 프로젝트 소유자가 상단 회원 메뉴 → API 키 관리에서 키를 발급합니다.
  3. 이미지가 필요하면 media:write, 자동발행하려면 content:publish를 추가합니다. 기본 권한은 content:read, content:write입니다.
  4. 연결을 조회하고 이미지를 업로드한 뒤 글을 생성합니다.
  5. 승인이 필요한 글은 병원 담당자가 검토합니다. 승인하면 보관된 발행 요청이 자동으로 이어집니다.
  6. 글과 발행 작업의 상태를 GET으로 확인합니다.

API 키는 서버의 환경 변수나 자동화 도구의 비밀 저장소에 보관합니다. 프런트엔드 코드·URL·Git·로그에 넣지 않습니다. 브라우저용 CORS는 제공하지 않습니다.

주소와 공통 규칙

메서드와 권한

메서드상대 경로기능필요한 권한
GET/연결·정책·권한 확인content:read
GET/posts목록·상태 필터content:read
GET/posts/{postId}본문·상태·ETagcontent:read
POST/posts글 생성, 선택적 발행content:write, 발행 시 content:publish 추가
PATCH/posts/{postId}예약·게시 전 글 수정content:write
POST/posts/{postId}/publish즉시·예약 발행 요청content:publish
GET/publish-jobs/{jobId}작업·실패 확인content:read
POST/media이미지 파일 업로드media:write
GET/media/{mediaId}이미지 URL·정보content:read

콘텐츠 PUT·DELETE, 게시 글 수정·철회, 예약 취소는 제공하지 않습니다. 해당 요청은 405와 Allow를 반환합니다. 키 발급·조회·폐기는 로그인한 회원 중 프로젝트 소유자가 회원 메뉴에서 수행합니다. 비로그인·익명 계정은 발급할 수 없습니다. 키를 폐기해도 이미 접수된 발행 요청은 유지됩니다.

연결 확인

아래 환경 변수 값은 자리표시자입니다. 키 원문을 명령 기록에 직접 쓰지 말고 비밀 저장소에서 환경 변수로 주입하세요.

curl --fail-with-body "$CONTENT_API_URL" \
  --header "Authorization: Bearer $CONTENT_API_KEY"

프로젝트는 한 키에 하나만 연결됩니다. 키는 90일 후 만료되며 원문은 발급 직후 한 번만 표시됩니다. 새 키로 자동화 설정을 바꾸고 연결을 확인한 뒤 이전 키를 폐기하세요. 매 요청에서 발급자의 현재 회원 계정과 소유자 권한을 확인합니다. 탈퇴·계정 비활성화·익명 계정 전환·프로젝트 관리 권한 회수도 호출을 차단합니다.

이미지 업로드

curl --fail-with-body "$CONTENT_API_URL/media" \
  --header "Authorization: Bearer $CONTENT_API_KEY" \
  --header "Idempotency-Key: image-preparation-001" \
  --form 'file=@preparation.png;type=image/png' \
  --form 'alt=방문 전 준비물 안내'

multipart의 file은 하나만 전달하며 alt는 선택·최대 200자입니다. JPG·PNG·WebP, 10MiB·4,000만 화소까지 받습니다. 실제 형식이 다르거나 손상·애니메이션·SVG인 파일은 거절합니다. 서버는 방향 보정·메타데이터 제거 후 긴 변 최대 1920px의 WebP로 변환해 저장합니다. 원본 파일의 별도 보관 기능은 아닙니다.

결과의 data.url을 본문에 ![방문 전 준비물 안내](이미지_URL) 형태로 독립된 줄에 삽입합니다. 외부 HTTPS 이미지 주소도 사용할 수 있지만 외부 파일을 be4link가 복사·보관하지는 않습니다. 임시 로컬 이미지 주소, data URL, raw HTML은 사용하지 않습니다. Emulator 실행에서는 로컬 Storage의 http://127.0.0.1:9199/v0/b/... 주소를 테스트용으로 허용합니다.

업로드 URL은 주소를 아는 사람이 접근할 수 있는 공개 콘텐츠용 주소입니다. 비공개 자료를 올리지 마세요. 글 초안·키·미디어 관리 레코드는 인증으로 보호합니다. 업로드 중 장애가 나면 동일 파일·alt·키로 재시도합니다. REQUEST_IN_PROGRESS일 때는 Retry-After 이후 다시 요청합니다.

글 생성: JavaScript

const base = process.env.CONTENT_API_URL;
const key = process.env.CONTENT_API_KEY;
const response = await fetch(`${base}/posts`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${key}`,
    "Content-Type": "application/json",
    "Idempotency-Key": "article-preparation-001",
  },
  body: JSON.stringify({
    title: "병원 방문 전 준비사항",
    excerpt: "예약 안내와 준비물을 방문 전에 확인하는 방법입니다.",
    body: "## 방문 전 확인\n\n병원을 방문하기 전에는 예약 날짜와 시간을 확인해 주세요. 병원에서 미리 안내받은 준비물이 있다면 목록을 살펴보고 빠진 것이 없는지 확인합니다. 이전에 받은 안내와 다른 내용이 있거나 준비 방법이 명확하지 않다면 임의로 판단하지 말고 병원에 문의해 주세요. 방문 목적에 따라 필요한 자료가 달라질 수 있으므로 예약 시 전달받은 안내를 우선하여 준비합니다.",
    authorName: "병원 안내팀",
    mode: "publish",
  }),
});
const result = await response.json();
if (!response.ok) throw new Error(`${result.error.code}: ${result.error.message}`);
// postId와 status만 작업 상태에 보관합니다. 키와 원문 전체를 로그에 남기지 마세요.
const postId = result.data.id;
필드제한
title필수, 앞뒤 공백 제거 후 2–160자
excerpt필수, 2–400자
body필수, Markdown 최대 20,000자, 그림을 제외한 순수 본문 최소 100자
authorName필수, 1–100자
modedraft 기본값, publish 선택
publishAtpublish에서만 사용, 생략 시 즉시, 미래 최대 30일

본문은 기존 편집기와 같은 제목(##, ###), 굵게, 목록, 인용, HTTP(S) 링크, HTTPS 이미지, 줄바꿈을 지원합니다. HTML을 실행하지 않습니다. API는 서버 관리 필드(상태·승인·위험·작성자 ID·프로젝트·출처)를 입력받지 않습니다.

수정·발행·결과 확인

# 응답 헤더의 ETag를 CONTENT_ETAG에 저장합니다.
curl --fail-with-body --include "$CONTENT_API_URL/posts/$POST_ID" \
  --header "Authorization: Bearer $CONTENT_API_KEY"

curl --fail-with-body --request PATCH "$CONTENT_API_URL/posts/$POST_ID" \
  --header "Authorization: Bearer $CONTENT_API_KEY" \
  --header "Content-Type: application/json" \
  --header "Idempotency-Key: article-edit-001" \
  --header "If-Match: $CONTENT_ETAG" \
  --data '{"title":"방문 준비사항 확인 안내"}'

# 수정 응답에서 갱신된 ETag를 사용합니다. {}는 즉시 발행 요청입니다.
curl --fail-with-body --request POST "$CONTENT_API_URL/posts/$POST_ID/publish" \
  --header "Authorization: Bearer $CONTENT_API_KEY" \
  --header "Content-Type: application/json" \
  --header "Idempotency-Key: article-publish-001" \
  --header "If-Match: $CONTENT_ETAG" \
  --data '{}'

예약하려면 발행 요청 본문에 미래의 publishAt을 지정합니다. 생성은 201, 발행 요청 접수는 202입니다. 접수 성공이 게시 완료를 뜻하지 않습니다.

프로젝트의 all 정책에서는 모든 글, risk-only에서는 위험이 감지된 글이 승인을 거칩니다. 정책 미설정은 all입니다. 의료광고 위험 감지는 전문가 검토를 대신하지 않습니다.

발행 요청이 있는 글을 승인하면 자동으로 예약을 이어갑니다. 예약 시간이 지났으면 승인 직후 발행합니다. 초안을 수정하면 이전 승인과 발행 요청이 해제되므로 다시 발행을 요청해야 합니다. 실행 직전 정책이 강화되면 검토 대기로 돌아갈 수 있습니다.

publishJobId가 생긴 뒤 /publish-jobs/{jobId}를 조회합니다. 최초 큐 등록 후 자동 복구는 최대 4회이며, 소진 시 RECOVERY_EXHAUSTED입니다. 작업 큐의 처리 자체도 제한된 횟수로 재시도합니다. 기존 키 폐기는 접수된 작업을 취소하지 않습니다.

공개 상세 글 조회는 최신 50개 목록과 분리됩니다. 목록·홈페이지는 기존 캐시 때문에 반영이 늦을 수 있습니다. published는 검색엔진 색인·노출 완료를 뜻하지 않습니다.

목록 페이지네이션

GET /posts?status=published&limit=20으로 시작합니다. meta.nextCursor가 있으면 같은 필터와 cursor를 보내 다음 페이지를 조회합니다. 기본 20·최대 100개, 생성일과 ID 내림차순입니다. 목록은 본문을 제외하며 상세 GET에서 본문을 가져옵니다. 조회 중 새 글은 다음 전체 조회에서 보일 수 있습니다. 필터를 바꾸면 cursor를 비우세요.

n8n · Make 설정

n8n의 HTTP Request 노드 또는 Make의 HTTP 모듈에서 다음과 같이 설정합니다.

  1. 인증용 헤더를 비밀 자격증명에 저장: Authorization: Bearer <API_KEY>.
  2. 이미지 노드: POST /media, multipart 파일 필드 file, 선택 필드 alt.
  3. 글 노드: POST /posts, JSON 본문, 이전 노드의 data.url을 Markdown 이미지 주소로 매핑.
  4. 각 노드의 Idempotency-Key는 외부 글 ID와 작업 종류로 만들고 재시도에는 같은 값을 유지.
  5. 상세 GET의 ETag를 수정·발행 노드의 If-Match 헤더에 매핑.
  6. review-required는 오류 재시도 대상으로 삼지 않고 병원 승인을 기다림. 작업 상태는 GET으로 조회.
  7. 429/503은 Retry-After를 따르고 지수 백오프를 사용. 실행 기록에 비밀 헤더·본문을 남기지 않도록 설정.

완성된 워크플로 계정 연결이나 외부 서비스 실행은 이 API 제공 범위에 포함하지 않습니다.

오류·요청 제한

HTTP대표 코드대응
400INVALID_REQUEST, INVALID_PUBLISH_TIME, INVALID_CURSOR, IF_MATCH_REQUIRED입력·시간·조회 버전 확인
401INVALID_API_KEY, MEMBER_NOT_FOUND, MEMBER_DISABLED키 만료·폐기·발급자 계정 확인
403MEMBERSHIP_REQUIRED, INSUFFICIENT_SCOPE, PROJECT_FORBIDDEN, ISSUER_PERMISSION_REVOKED프로젝트·키 권한·소유자 확인
404POST_NOT_FOUND, JOB_NOT_FOUND, MEDIA_NOT_FOUND현재 프로젝트의 리소스인지 확인
405METHOD_NOT_ALLOWEDAllow 헤더 확인
409IDEMPOTENCY_CONFLICT, REQUEST_IN_PROGRESS, POST_NOT_EDITABLE, SITE_NOT_PUBLISHED요청 충돌·처리 중·글 상태·홈페이지 게시 확인
412REVISION_CONFLICT상세 GET 후 변경 사항을 검토하고 새 멱등 키로 요청
413REQUEST_TOO_LARGE, IMAGE_TOO_LARGEJSON·파일 용량 줄이기
415UNSUPPORTED_MEDIA_TYPE, INVALID_IMAGEContent-Type·실제 파일 형식 확인
429RATE_LIMITEDRetry-After 이후 동일 요청 재시도
500INTERNAL_ERRORrequestId를 운영자에게 전달
503SERVICE_UNAVAILABLE, MEDIA_STORAGE_UNAVAILABLERetry-After 이후 동일 키로 재시도

초기 한도는 키당 분당 60회·프로젝트당 분당 300회이며, 이미지 업로드는 키당 분당 10회입니다. 시간 창이 끝나면 초기화됩니다. 인증·권한을 통과한 요청에는 재전송도 횟수에 포함됩니다.

버전·호환성·운영

v1 내에서는 기존 필드를 제거하거나 의미·필수 조건을 조용히 변경하지 않습니다. 소비자는 응답의 추가 필드를 허용하고 문서의 변경 이력을 확인하세요. 파괴적 변경은 새 주 버전으로 제공합니다. 상세 규격은 v1/openapi.yaml입니다.

기존 ingestContentApi는 Firebase ID 토큰과 projectId 본문, 기존 평면 응답을 유지하는 호환 경로입니다. 새 연동은 v1을 사용하세요. 기존 콘솔·호환 경로도 같은 승인·발행 서비스를 사용합니다.

배포는 인덱스 준비와 Enabled 확인 → Rules·Functions → 콘솔·공개 문서·사이트 런타임 → 테스트 프로젝트 검증 순서입니다. getPublishedPost를 사이트 런타임보다 먼저 배포합니다. 실패 시 신규 진입점을 숨기고 API 기능을 중단하되 접수된 요청·보관 원문을 삭제하지 않습니다. 기존 큐·프로젝트 설정은 보존합니다.

운영 담당자는 content_api_request의 requestId·프로젝트·상태 코드와 content_queue_pending 및 작업 실패 코드를 확인합니다. 키·본문·이미지 원문은 로그에 남기지 않습니다. 지연된 예약은 15분 주기의 복구 작업이 다시 시도합니다. contentApiRequests는 7일, 요청 제한 카운터는 만료 후 Firestore TTL로 정리합니다.

규격 기준: OpenAPI 3.1.1, Cloud Tasks 예약 한도, HTTP 202. 확인일: 2026-09-19.

OpenAPI 상세 규격

아래 항목은 저장소의 OpenAPI 원본에서 표시합니다. 키를 입력하거나 운영 API를 실행하는 화면은 아닙니다.

GET /projects/{projectId}Connection and approval policy

Required scope: content:read.

{
  "operationId": "getProject",
  "summary": "Connection and approval policy",
  "description": "Required scope: content:read.",
  "x-required-scopes": [
    "content:read"
  ],
  "responses": {
    "200": {
      "description": "Success",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/ProjectResponse"
          }
        }
      }
    },
    "400": {
      "$ref": "#/components/responses/ApiError"
    },
    "401": {
      "$ref": "#/components/responses/ApiError"
    },
    "403": {
      "$ref": "#/components/responses/ApiError"
    },
    "404": {
      "$ref": "#/components/responses/ApiError"
    },
    "405": {
      "$ref": "#/components/responses/ApiError"
    },
    "409": {
      "$ref": "#/components/responses/ApiError"
    },
    "412": {
      "$ref": "#/components/responses/ApiError"
    },
    "413": {
      "$ref": "#/components/responses/ApiError"
    },
    "415": {
      "$ref": "#/components/responses/ApiError"
    },
    "429": {
      "$ref": "#/components/responses/ApiError"
    },
    "500": {
      "$ref": "#/components/responses/ApiError"
    },
    "503": {
      "$ref": "#/components/responses/ApiError"
    }
  }
}
GET /projects/{projectId}/postsList posts

Required scope: content:read.

{
  "operationId": "listPosts",
  "summary": "List posts",
  "description": "Required scope: content:read.",
  "x-required-scopes": [
    "content:read"
  ],
  "responses": {
    "200": {
      "description": "Success",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/PostListResponse"
          }
        }
      }
    },
    "400": {
      "$ref": "#/components/responses/ApiError"
    },
    "401": {
      "$ref": "#/components/responses/ApiError"
    },
    "403": {
      "$ref": "#/components/responses/ApiError"
    },
    "404": {
      "$ref": "#/components/responses/ApiError"
    },
    "405": {
      "$ref": "#/components/responses/ApiError"
    },
    "409": {
      "$ref": "#/components/responses/ApiError"
    },
    "412": {
      "$ref": "#/components/responses/ApiError"
    },
    "413": {
      "$ref": "#/components/responses/ApiError"
    },
    "415": {
      "$ref": "#/components/responses/ApiError"
    },
    "429": {
      "$ref": "#/components/responses/ApiError"
    },
    "500": {
      "$ref": "#/components/responses/ApiError"
    },
    "503": {
      "$ref": "#/components/responses/ApiError"
    }
  },
  "parameters": [
    {
      "name": "status",
      "in": "query",
      "schema": {
        "type": "string",
        "enum": [
          "idea",
          "draft",
          "review-required",
          "approved",
          "scheduled",
          "published",
          "failed"
        ]
      }
    },
    {
      "name": "limit",
      "in": "query",
      "schema": {
        "type": "integer",
        "minimum": 1,
        "maximum": 100,
        "default": 20
      }
    },
    {
      "name": "cursor",
      "in": "query",
      "schema": {
        "type": "string",
        "maxLength": 1024
      },
      "description": "meta.nextCursor from same project and filter. createdAt and ID descending."
    }
  ]
}
POST /projects/{projectId}/postsCreate a post with optional publication intent

Required scope: content:write. mode=publish additionally requires content:publish. Default mode is draft.

{
  "operationId": "createPost",
  "summary": "Create a post with optional publication intent",
  "description": "Required scope: content:write. mode=publish additionally requires content:publish. Default mode is draft.",
  "x-required-scopes": [
    "content:write"
  ],
  "responses": {
    "201": {
      "description": "Success",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/PostResponse"
          }
        }
      },
      "headers": {
        "ETag": {
          "schema": {
            "type": "string"
          },
          "description": "Current post revision"
        },
        "Location": {
          "schema": {
            "type": "string"
          },
          "description": "Created resource URL"
        }
      }
    },
    "400": {
      "$ref": "#/components/responses/ApiError"
    },
    "401": {
      "$ref": "#/components/responses/ApiError"
    },
    "403": {
      "$ref": "#/components/responses/ApiError"
    },
    "404": {
      "$ref": "#/components/responses/ApiError"
    },
    "405": {
      "$ref": "#/components/responses/ApiError"
    },
    "409": {
      "$ref": "#/components/responses/ApiError"
    },
    "412": {
      "$ref": "#/components/responses/ApiError"
    },
    "413": {
      "$ref": "#/components/responses/ApiError"
    },
    "415": {
      "$ref": "#/components/responses/ApiError"
    },
    "429": {
      "$ref": "#/components/responses/ApiError"
    },
    "500": {
      "$ref": "#/components/responses/ApiError"
    },
    "503": {
      "$ref": "#/components/responses/ApiError"
    }
  },
  "parameters": [
    {
      "$ref": "#/components/parameters/IdempotencyKey"
    }
  ],
  "requestBody": {
    "required": true,
    "content": {
      "application/json": {
        "schema": {
          "$ref": "#/components/schemas/CreatePost"
        }
      }
    }
  }
}
GET /projects/{projectId}/posts/{postId}Post detail and current state

Required scope: content:read.

{
  "operationId": "getPost",
  "summary": "Post detail and current state",
  "description": "Required scope: content:read.",
  "x-required-scopes": [
    "content:read"
  ],
  "responses": {
    "200": {
      "description": "Success",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/PostResponse"
          }
        }
      },
      "headers": {
        "ETag": {
          "schema": {
            "type": "string"
          },
          "description": "Current post revision"
        }
      }
    },
    "400": {
      "$ref": "#/components/responses/ApiError"
    },
    "401": {
      "$ref": "#/components/responses/ApiError"
    },
    "403": {
      "$ref": "#/components/responses/ApiError"
    },
    "404": {
      "$ref": "#/components/responses/ApiError"
    },
    "405": {
      "$ref": "#/components/responses/ApiError"
    },
    "409": {
      "$ref": "#/components/responses/ApiError"
    },
    "412": {
      "$ref": "#/components/responses/ApiError"
    },
    "413": {
      "$ref": "#/components/responses/ApiError"
    },
    "415": {
      "$ref": "#/components/responses/ApiError"
    },
    "429": {
      "$ref": "#/components/responses/ApiError"
    },
    "500": {
      "$ref": "#/components/responses/ApiError"
    },
    "503": {
      "$ref": "#/components/responses/ApiError"
    }
  }
}
PATCH /projects/{projectId}/posts/{postId}Edit an unscheduled, unpublished post

Required scope: content:write.

{
  "operationId": "patchPost",
  "summary": "Edit an unscheduled, unpublished post",
  "description": "Required scope: content:write.",
  "x-required-scopes": [
    "content:write"
  ],
  "responses": {
    "200": {
      "description": "Success",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/PostResponse"
          }
        }
      },
      "headers": {
        "ETag": {
          "schema": {
            "type": "string"
          },
          "description": "Current post revision"
        }
      }
    },
    "400": {
      "$ref": "#/components/responses/ApiError"
    },
    "401": {
      "$ref": "#/components/responses/ApiError"
    },
    "403": {
      "$ref": "#/components/responses/ApiError"
    },
    "404": {
      "$ref": "#/components/responses/ApiError"
    },
    "405": {
      "$ref": "#/components/responses/ApiError"
    },
    "409": {
      "$ref": "#/components/responses/ApiError"
    },
    "412": {
      "$ref": "#/components/responses/ApiError"
    },
    "413": {
      "$ref": "#/components/responses/ApiError"
    },
    "415": {
      "$ref": "#/components/responses/ApiError"
    },
    "429": {
      "$ref": "#/components/responses/ApiError"
    },
    "500": {
      "$ref": "#/components/responses/ApiError"
    },
    "503": {
      "$ref": "#/components/responses/ApiError"
    }
  },
  "parameters": [
    {
      "$ref": "#/components/parameters/IdempotencyKey"
    },
    {
      "$ref": "#/components/parameters/IfMatch"
    }
  ],
  "requestBody": {
    "required": true,
    "content": {
      "application/json": {
        "schema": {
          "$ref": "#/components/schemas/PatchPost"
        }
      }
    }
  }
}
POST /projects/{projectId}/posts/{postId}/publishRequest immediate or scheduled publication

Required scope: content:publish.

{
  "operationId": "publishPost",
  "summary": "Request immediate or scheduled publication",
  "description": "Required scope: content:publish.",
  "x-required-scopes": [
    "content:publish"
  ],
  "responses": {
    "202": {
      "description": "Accepted; poll GET for completion",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/PostResponse"
          }
        }
      },
      "headers": {
        "ETag": {
          "schema": {
            "type": "string"
          },
          "description": "Current post revision"
        }
      }
    },
    "400": {
      "$ref": "#/components/responses/ApiError"
    },
    "401": {
      "$ref": "#/components/responses/ApiError"
    },
    "403": {
      "$ref": "#/components/responses/ApiError"
    },
    "404": {
      "$ref": "#/components/responses/ApiError"
    },
    "405": {
      "$ref": "#/components/responses/ApiError"
    },
    "409": {
      "$ref": "#/components/responses/ApiError"
    },
    "412": {
      "$ref": "#/components/responses/ApiError"
    },
    "413": {
      "$ref": "#/components/responses/ApiError"
    },
    "415": {
      "$ref": "#/components/responses/ApiError"
    },
    "429": {
      "$ref": "#/components/responses/ApiError"
    },
    "500": {
      "$ref": "#/components/responses/ApiError"
    },
    "503": {
      "$ref": "#/components/responses/ApiError"
    }
  },
  "parameters": [
    {
      "$ref": "#/components/parameters/IdempotencyKey"
    },
    {
      "$ref": "#/components/parameters/IfMatch"
    }
  ],
  "requestBody": {
    "required": true,
    "content": {
      "application/json": {
        "schema": {
          "$ref": "#/components/schemas/PublishPost"
        }
      }
    }
  }
}
GET /projects/{projectId}/publish-jobs/{jobId}Publication job status

Required scope: content:read.

{
  "operationId": "getPublishJob",
  "summary": "Publication job status",
  "description": "Required scope: content:read.",
  "x-required-scopes": [
    "content:read"
  ],
  "responses": {
    "200": {
      "description": "Success",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/PublishJobResponse"
          }
        }
      }
    },
    "400": {
      "$ref": "#/components/responses/ApiError"
    },
    "401": {
      "$ref": "#/components/responses/ApiError"
    },
    "403": {
      "$ref": "#/components/responses/ApiError"
    },
    "404": {
      "$ref": "#/components/responses/ApiError"
    },
    "405": {
      "$ref": "#/components/responses/ApiError"
    },
    "409": {
      "$ref": "#/components/responses/ApiError"
    },
    "412": {
      "$ref": "#/components/responses/ApiError"
    },
    "413": {
      "$ref": "#/components/responses/ApiError"
    },
    "415": {
      "$ref": "#/components/responses/ApiError"
    },
    "429": {
      "$ref": "#/components/responses/ApiError"
    },
    "500": {
      "$ref": "#/components/responses/ApiError"
    },
    "503": {
      "$ref": "#/components/responses/ApiError"
    }
  }
}
POST /projects/{projectId}/mediaUpload an image

Required scope: media:write.

{
  "operationId": "uploadMedia",
  "summary": "Upload an image",
  "description": "Required scope: media:write.",
  "x-required-scopes": [
    "media:write"
  ],
  "responses": {
    "201": {
      "description": "Success",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/MediaResponse"
          }
        }
      },
      "headers": {
        "Location": {
          "schema": {
            "type": "string"
          },
          "description": "Created resource URL"
        }
      }
    },
    "400": {
      "$ref": "#/components/responses/ApiError"
    },
    "401": {
      "$ref": "#/components/responses/ApiError"
    },
    "403": {
      "$ref": "#/components/responses/ApiError"
    },
    "404": {
      "$ref": "#/components/responses/ApiError"
    },
    "405": {
      "$ref": "#/components/responses/ApiError"
    },
    "409": {
      "$ref": "#/components/responses/ApiError"
    },
    "412": {
      "$ref": "#/components/responses/ApiError"
    },
    "413": {
      "$ref": "#/components/responses/ApiError"
    },
    "415": {
      "$ref": "#/components/responses/ApiError"
    },
    "429": {
      "$ref": "#/components/responses/ApiError"
    },
    "500": {
      "$ref": "#/components/responses/ApiError"
    },
    "503": {
      "$ref": "#/components/responses/ApiError"
    }
  },
  "parameters": [
    {
      "$ref": "#/components/parameters/IdempotencyKey"
    }
  ],
  "requestBody": {
    "required": true,
    "content": {
      "multipart/form-data": {
        "schema": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "file": {
              "type": "string",
              "format": "binary",
              "description": "One JPEG/PNG/WebP file, <=10MiB and <=40 million pixels, no animation."
            },
            "alt": {
              "type": "string",
              "maxLength": 200,
              "default": ""
            }
          },
          "required": [
            "file"
          ]
        }
      }
    }
  }
}
GET /projects/{projectId}/media/{mediaId}Image URL and metadata

Required scope: content:read.

{
  "operationId": "getMedia",
  "summary": "Image URL and metadata",
  "description": "Required scope: content:read.",
  "x-required-scopes": [
    "content:read"
  ],
  "responses": {
    "200": {
      "description": "Success",
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/MediaResponse"
          }
        }
      }
    },
    "400": {
      "$ref": "#/components/responses/ApiError"
    },
    "401": {
      "$ref": "#/components/responses/ApiError"
    },
    "403": {
      "$ref": "#/components/responses/ApiError"
    },
    "404": {
      "$ref": "#/components/responses/ApiError"
    },
    "405": {
      "$ref": "#/components/responses/ApiError"
    },
    "409": {
      "$ref": "#/components/responses/ApiError"
    },
    "412": {
      "$ref": "#/components/responses/ApiError"
    },
    "413": {
      "$ref": "#/components/responses/ApiError"
    },
    "415": {
      "$ref": "#/components/responses/ApiError"
    },
    "429": {
      "$ref": "#/components/responses/ApiError"
    },
    "500": {
      "$ref": "#/components/responses/ApiError"
    },
    "503": {
      "$ref": "#/components/responses/ApiError"
    }
  }
}
요청·응답 스키마
{
  "CreatePost": {
    "type": "object",
    "additionalProperties": false,
    "properties": {
      "title": {
        "type": "string",
        "minLength": 2,
        "maxLength": 160
      },
      "excerpt": {
        "type": "string",
        "minLength": 2,
        "maxLength": 400
      },
      "body": {
        "type": "string",
        "maxLength": 20000,
        "description": "Markdown with at least 100 plain-text characters; HTTPS images only. Raw HTML is not executed."
      },
      "authorName": {
        "type": "string",
        "minLength": 1,
        "maxLength": 100
      },
      "mode": {
        "type": "string",
        "enum": [
          "draft",
          "publish"
        ],
        "default": "draft"
      },
      "publishAt": {
        "type": "string",
        "format": "date-time",
        "description": "Only with mode=publish. Future time within 30 days; omitted means now."
      }
    },
    "required": [
      "title",
      "excerpt",
      "body",
      "authorName"
    ],
    "allOf": [
      {
        "if": {
          "required": [
            "publishAt"
          ]
        },
        "then": {
          "required": [
            "mode"
          ],
          "properties": {
            "mode": {
              "const": "publish"
            }
          }
        }
      }
    ]
  },
  "PatchPost": {
    "type": "object",
    "additionalProperties": false,
    "properties": {
      "title": {
        "type": "string",
        "minLength": 2,
        "maxLength": 160
      },
      "excerpt": {
        "type": "string",
        "minLength": 2,
        "maxLength": 400
      },
      "body": {
        "type": "string",
        "maxLength": 20000,
        "description": "Markdown with at least 100 plain-text characters; HTTPS images only. Raw HTML is not executed."
      },
      "authorName": {
        "type": "string",
        "minLength": 1,
        "maxLength": 100
      }
    },
    "required": [],
    "minProperties": 1
  },
  "PublishPost": {
    "type": "object",
    "additionalProperties": false,
    "properties": {
      "publishAt": {
        "type": "string",
        "format": "date-time",
        "description": "Future time within 30 days; omitted means now."
      }
    },
    "required": []
  },
  "PostSummary": {
    "type": "object",
    "additionalProperties": false,
    "properties": {
      "id": {
        "type": "string",
        "pattern": "^[A-Za-z0-9_-]{1,128}$"
      },
      "projectId": {
        "type": "string",
        "pattern": "^[A-Za-z0-9_-]{1,128}$"
      },
      "title": {
        "type": "string"
      },
      "slug": {
        "type": "string"
      },
      "excerpt": {
        "type": "string"
      },
      "authorName": {
        "type": "string"
      },
      "source": {
        "type": "string",
        "enum": [
          "manual",
          "search-console",
          "ai-suggestion",
          "api"
        ]
      },
      "status": {
        "type": "string",
        "enum": [
          "idea",
          "draft",
          "review-required",
          "approved",
          "scheduled",
          "published",
          "failed"
        ]
      },
      "revision": {
        "type": "integer",
        "minimum": 1
      },
      "createdAt": {
        "type": "string",
        "format": "date-time"
      },
      "updatedAt": {
        "type": "string",
        "format": "date-time"
      },
      "approvalPolicy": {
        "type": "string",
        "enum": [
          "all",
          "risk-only"
        ]
      },
      "requiresApproval": {
        "type": "boolean"
      },
      "risks": {
        "type": "array",
        "items": {
          "type": "string",
          "enum": [
            "efficacy-claim",
            "comparative-claim",
            "guarantee-claim",
            "clinical-claim",
            "unverified-credential"
          ]
        }
      },
      "publishRequestedAt": {
        "type": [
          "string",
          "null"
        ],
        "format": "date-time"
      },
      "requestedPublishAt": {
        "type": [
          "string",
          "null"
        ],
        "format": "date-time"
      },
      "scheduledAt": {
        "type": [
          "string",
          "null"
        ],
        "format": "date-time"
      },
      "publishedAt": {
        "type": [
          "string",
          "null"
        ],
        "format": "date-time"
      },
      "publishJobId": {
        "type": [
          "string",
          "null"
        ]
      },
      "publishFailureCode": {
        "type": [
          "string",
          "null"
        ]
      },
      "publicUrl": {
        "type": [
          "string",
          "null"
        ],
        "format": "uri"
      }
    },
    "required": [
      "id",
      "projectId",
      "title",
      "slug",
      "excerpt",
      "authorName",
      "source",
      "status",
      "revision",
      "createdAt",
      "updatedAt",
      "approvalPolicy",
      "requiresApproval",
      "risks",
      "publishRequestedAt",
      "requestedPublishAt",
      "scheduledAt",
      "publishedAt",
      "publishJobId",
      "publishFailureCode",
      "publicUrl"
    ]
  },
  "Post": {
    "type": "object",
    "additionalProperties": false,
    "properties": {
      "id": {
        "type": "string",
        "pattern": "^[A-Za-z0-9_-]{1,128}$"
      },
      "projectId": {
        "type": "string",
        "pattern": "^[A-Za-z0-9_-]{1,128}$"
      },
      "title": {
        "type": "string"
      },
      "slug": {
        "type": "string"
      },
      "excerpt": {
        "type": "string"
      },
      "authorName": {
        "type": "string"
      },
      "source": {
        "type": "string",
        "enum": [
          "manual",
          "search-console",
          "ai-suggestion",
          "api"
        ]
      },
      "status": {
        "type": "string",
        "enum": [
          "idea",
          "draft",
          "review-required",
          "approved",
          "scheduled",
          "published",
          "failed"
        ]
      },
      "revision": {
        "type": "integer",
        "minimum": 1
      },
      "createdAt": {
        "type": "string",
        "format": "date-time"
      },
      "updatedAt": {
        "type": "string",
        "format": "date-time"
      },
      "approvalPolicy": {
        "type": "string",
        "enum": [
          "all",
          "risk-only"
        ]
      },
      "requiresApproval": {
        "type": "boolean"
      },
      "risks": {
        "type": "array",
        "items": {
          "type": "string",
          "enum": [
            "efficacy-claim",
            "comparative-claim",
            "guarantee-claim",
            "clinical-claim",
            "unverified-credential"
          ]
        }
      },
      "publishRequestedAt": {
        "type": [
          "string",
          "null"
        ],
        "format": "date-time"
      },
      "requestedPublishAt": {
        "type": [
          "string",
          "null"
        ],
        "format": "date-time"
      },
      "scheduledAt": {
        "type": [
          "string",
          "null"
        ],
        "format": "date-time"
      },
      "publishedAt": {
        "type": [
          "string",
          "null"
        ],
        "format": "date-time"
      },
      "publishJobId": {
        "type": [
          "string",
          "null"
        ]
      },
      "publishFailureCode": {
        "type": [
          "string",
          "null"
        ]
      },
      "publicUrl": {
        "type": [
          "string",
          "null"
        ],
        "format": "uri"
      },
      "body": {
        "type": "string"
      }
    },
    "required": [
      "id",
      "projectId",
      "title",
      "slug",
      "excerpt",
      "authorName",
      "source",
      "status",
      "revision",
      "createdAt",
      "updatedAt",
      "approvalPolicy",
      "requiresApproval",
      "risks",
      "publishRequestedAt",
      "requestedPublishAt",
      "scheduledAt",
      "publishedAt",
      "publishJobId",
      "publishFailureCode",
      "publicUrl",
      "body"
    ]
  },
  "Project": {
    "type": "object",
    "additionalProperties": false,
    "properties": {
      "projectId": {
        "type": "string",
        "pattern": "^[A-Za-z0-9_-]{1,128}$"
      },
      "name": {
        "type": "string"
      },
      "approvalPolicy": {
        "type": "string",
        "enum": [
          "all",
          "risk-only"
        ]
      },
      "sitePublished": {
        "type": "boolean"
      },
      "scopes": {
        "type": "array",
        "items": {
          "type": "string",
          "enum": [
            "content:read",
            "content:write",
            "content:publish",
            "media:write"
          ]
        }
      },
      "apiVersion": {
        "type": "string",
        "const": "v1"
      }
    },
    "required": [
      "projectId",
      "name",
      "approvalPolicy",
      "sitePublished",
      "scopes",
      "apiVersion"
    ]
  },
  "Media": {
    "type": "object",
    "additionalProperties": false,
    "properties": {
      "mediaId": {
        "type": "string",
        "pattern": "^[A-Za-z0-9_-]{1,128}$"
      },
      "url": {
        "type": "string",
        "format": "uri"
      },
      "mimeType": {
        "type": "string",
        "const": "image/webp"
      },
      "size": {
        "type": "integer",
        "minimum": 1
      },
      "width": {
        "type": "integer",
        "minimum": 1,
        "maximum": 1920
      },
      "height": {
        "type": "integer",
        "minimum": 1,
        "maximum": 1920
      },
      "alt": {
        "type": "string",
        "maxLength": 200
      }
    },
    "required": [
      "mediaId",
      "url",
      "mimeType",
      "size",
      "width",
      "height",
      "alt"
    ]
  },
  "PublishJob": {
    "type": "object",
    "additionalProperties": false,
    "properties": {
      "id": {
        "type": "string",
        "pattern": "^[A-Za-z0-9_-]{1,128}$"
      },
      "postId": {
        "type": "string",
        "pattern": "^[A-Za-z0-9_-]{1,128}$"
      },
      "projectId": {
        "type": "string",
        "pattern": "^[A-Za-z0-9_-]{1,128}$"
      },
      "status": {
        "type": "string",
        "enum": [
          "queued",
          "running",
          "completed",
          "failed"
        ]
      },
      "scheduledAt": {
        "type": "string",
        "format": "date-time"
      },
      "completedAt": {
        "type": [
          "string",
          "null"
        ],
        "format": "date-time"
      },
      "errorCode": {
        "type": [
          "string",
          "null"
        ]
      },
      "terminal": {
        "type": "boolean"
      },
      "recoveryAttempts": {
        "type": "integer",
        "minimum": 0,
        "maximum": 4
      }
    },
    "required": [
      "id",
      "postId",
      "projectId",
      "status",
      "scheduledAt",
      "completedAt",
      "errorCode",
      "terminal",
      "recoveryAttempts"
    ]
  },
  "Meta": {
    "type": "object",
    "additionalProperties": false,
    "properties": {
      "requestId": {
        "type": "string",
        "format": "uuid"
      },
      "replayed": {
        "type": "boolean"
      },
      "nextCursor": {
        "type": [
          "string",
          "null"
        ]
      }
    },
    "required": [
      "requestId"
    ]
  },
  "Error": {
    "type": "object",
    "additionalProperties": false,
    "properties": {
      "error": {
        "type": "object",
        "additionalProperties": false,
        "properties": {
          "code": {
            "type": "string"
          },
          "message": {
            "type": "string"
          },
          "requestId": {
            "type": "string",
            "format": "uuid"
          },
          "details": {}
        },
        "required": [
          "code",
          "message",
          "requestId"
        ]
      }
    },
    "required": [
      "error"
    ]
  },
  "PostList": {
    "type": "object",
    "additionalProperties": false,
    "properties": {
      "posts": {
        "type": "array",
        "items": {
          "$ref": "#/components/schemas/PostSummary"
        }
      }
    },
    "required": [
      "posts"
    ]
  },
  "PostResponse": {
    "type": "object",
    "additionalProperties": false,
    "properties": {
      "data": {
        "$ref": "#/components/schemas/Post"
      },
      "meta": {
        "$ref": "#/components/schemas/Meta"
      }
    },
    "required": [
      "data",
      "meta"
    ]
  },
  "ProjectResponse": {
    "type": "object",
    "additionalProperties": false,
    "properties": {
      "data": {
        "$ref": "#/components/schemas/Project"
      },
      "meta": {
        "$ref": "#/components/schemas/Meta"
      }
    },
    "required": [
      "data",
      "meta"
    ]
  },
  "MediaResponse": {
    "type": "object",
    "additionalProperties": false,
    "properties": {
      "data": {
        "$ref": "#/components/schemas/Media"
      },
      "meta": {
        "$ref": "#/components/schemas/Meta"
      }
    },
    "required": [
      "data",
      "meta"
    ]
  },
  "PublishJobResponse": {
    "type": "object",
    "additionalProperties": false,
    "properties": {
      "data": {
        "$ref": "#/components/schemas/PublishJob"
      },
      "meta": {
        "$ref": "#/components/schemas/Meta"
      }
    },
    "required": [
      "data",
      "meta"
    ]
  },
  "PostListResponse": {
    "type": "object",
    "additionalProperties": false,
    "properties": {
      "data": {
        "$ref": "#/components/schemas/PostList"
      },
      "meta": {
        "$ref": "#/components/schemas/Meta"
      }
    },
    "required": [
      "data",
      "meta"
    ]
  }
}