REST API
기본 주소 하나, 키 하나, 렌더 방식 둘. 템플릿과 데이터를 보내면 PDF 또는 이미지가 나옵니다.
인증
모든 /v1 요청은 x-api-key 헤더에 API 키가 필요합니다. 키는 콘솔에서 발급하며 화면에 한 번만 보입니다 — 저장하는 것은 해시뿐이라 잃어버린 키는 복구가 아니라 재발급입니다.
키가 없든, 틀렸든, 폐기됐든 응답은 모두 같은 401 입니다. 어느 쪽인지 알려주지 않습니다 — 그 차이는 키를 추측하는 쪽에만 쓸모가 있습니다.
즉시 렌더
POST /v1/create 는 같은 요청 안에서 렌더하고 답합니다. 응답에는 파일이 아니라 서명된 다운로드 URL 이 담깁니다 — 그래야 20 KB 라벨과 40 MB 리포트가 같은 응답 형태를 씁니다.
URL 은 만료됩니다. 받은 즉시 내려받고, 저장하거나 캐시하지 마십시오.
백그라운드 렌더
POST /v1/create-async 는 작업 id 를 즉시 돌려주고 큐에서 렌더합니다. 문서가 크거나 호출한 쪽이 기다릴 수 없을 때 씁니다.
GET /v1/jobs/{id} 가 상태를, 끝났으면 같은 서명 URL 을 알려줍니다.
# 1. enqueue the job
curl -X POST https://brewmypdf.brewmypdf.workers.dev/v1/create-async \
-H "x-api-key: bmp_live_..." \
-H "content-type: application/json" \
--data-binary @invoice.json
# 2. poll for the result
curl https://brewmypdf.brewmypdf.workers.dev/v1/jobs/01M0... \
-H "x-api-key: bmp_live_..."한 번에 여러 건
POST /v1/batch 는 items 배열을 받습니다 — 최대 50건 — 그리고 함께 큐에 넣습니다. 항목마다 자기 template 또는 template_id 와 자기 data 를 갖습니다. 한 번의 호출로 고객 전원의 명세서를 만들 수 있습니다. 응답은 job id 목록이고, 결과는 각각 /v1/jobs/{id} 에서 받습니다. 기다리지 않습니다 — 50건을 기다리면 그 요청이 먼저 끝납니다.
merge 를 true 로 두면 대신 PDF 한 개가 나옵니다. 보낸 순서대로 이어 붙습니다. 이쪽은 20건이 상한입니다 — 한 작업 안에서 차례로 렌더하기 때문입니다. 이때는 기다렸다가 /v1/create 처럼 내려받기 주소를 돌려줍니다. 이미지를 합치는 것은 조용히 PDF 로 바꾸지 않고 거절합니다.
쿼터는 배치 전체를 놓고 큐에 넣기 전에 한 번 봅니다. 잔여 3건인 계정의 50건 배치는 통째로 거절합니다 — 일부만 받는 것보다 아예 안 받는 편이 낫습니다. 반쯤 처리된 배치는 환불도 재시도도 애매합니다.
합친 파일은 각 문서의 머리글·바닥글을 그대로 지닙니다. 합친 뒤 쪽번호를 다시 매기지 않습니다. 다시 매기면 그것이 한 문서라는 뜻인데, 실제로는 여러 문서를 한 파일에 담은 것입니다. 이어지는 쪽번호가 필요하면 반복 그룹으로 한 문서를 만드십시오.
끝나면 알림
비동기 요청과 배치에 webhook 을 넣으면, 작업이 끝났을 때 그 주소로 POST 를 한 번 보냅니다. 폴링을 없애려고 있는 기능입니다. https 만 받고, 사설·내부 주소는 거절합니다. 주소는 큐에 넣기 전에 검사하므로, 못 쓸 주소는 렌더 쿼터를 쓰기 전에 400 으로 돌아옵니다.
본문은 이것뿐입니다: {"job_id":"...","status":"done"} — 실패면 "status":"failed" 와 error_code 가 함께 옵니다. 내려받기 주소는 들어 있지 않습니다. 저희 내려받기 주소는 그 자체가 권한이라서, 알림에 실으면 그 알림을 본 쪽이 문서를 가져갑니다. 주소는 알림을 받은 뒤 여러분의 API 키로 GET /v1/jobs/{id} 를 불러 얻으십시오.
그래서 알림 자체는 증거가 아닙니다. 서명이 없고, 누구나 같은 형식으로 여러분의 주소에 POST 할 수 있습니다. 알림은 "가서 확인하라"는 신호로만 쓰고, 무엇이 참인지는 항상 GET /v1/jobs/{id} 로 정하십시오. 같은 알림이 두 번 올 수 있으니 job_id 로 멱등 처리도 해 두십시오.
전달은 최대 세 번 시도합니다 — 실패하면 1초, 2초 뒤. 5xx 와 응답 없음만 다시 보냅니다. 4xx 는 다시 보내도 같으므로 한 번에 멈춥니다. 응답은 5초 안에 주셔야 하고, 리다이렉트는 따라가지 않습니다. 200~299 를 곧바로 돌려주고 처리는 그 뒤에 하십시오.
알림이 실패해도 렌더는 성공입니다 — 파일은 그대로 있고 GET /v1/jobs/{id} 로 받을 수 있습니다. 전달 결과는 같은 응답의 webhook_status 에 남습니다: HTTP 상태 그대로이고, 아예 못 보냈으면 0 입니다. 필드가 없으면 알림을 쓰지 않은 작업입니다. merge 없는 배치는 작업이 여러 개이므로 알림도 작업 수만큼 옵니다.
PNG · JPEG
같은 템플릿을 PDF 대신 그림으로 받을 수 있습니다. 요청에 format 을 더하면 됩니다 — "png" 또는 "jpeg". 빼면 예전 그대로 PDF 입니다. JPEG 에는 quality 를 1~100 으로 줄 수 있습니다.
quality 는 JPEG 에만 적용됩니다. PNG 와 함께 보내면 무시하지 않고 거절합니다 — 조용히 무시된 옵션은 저희 쪽 결함처럼 보이고, 렌더러 자체도 그 조합을 거부합니다.
그림 크기는 설계한 용지 크기 그대로이고, 선명하도록 2배로 그리며, 페이지 여백을 유지합니다. 그림과 PDF 모두 월 쿼터에서 렌더 1건으로 셉니다.
그림이 가질 수 없는 것: 페이지입니다. 인쇄하면 세 장이 될 문서는 한 장의 긴 그림이 됩니다 — 페이지 나눔은 인쇄 개념이기 때문입니다. 같은 이유로 머리글도, 바닥글도, 쪽번호도 없습니다. [[page]] 와 [[bpage]] 는 인쇄하면서 채워지는데, 인쇄를 하지 않기 때문입니다.
한도
레이트리밋은 키·계정·IP 세 축을 동시에 셉니다. 하나만 걸면 키를 하나 더 발급하거나 다른 호스트에서 부르는 것으로 간단히 우회되기 때문입니다.
한도를 넘으면 Retry-After 헤더와 함께 429 가 옵니다. 그 값을 지켜 주십시오 — 즉시 재시도하면 상황이 나빠집니다.
월 렌더 쿼터는 이와 별개로 계정 단위로 셉니다. 소진되면 429 가 아니라 402 입니다 — 기다려서 풀리는 것과 아닌 것을 구분해야 하기 때문입니다.
내용 한도는 요금 한도와 별개이고, 사람들이 놀라는 쪽은 이쪽입니다. 배열이 바인딩된 표는 max 를 올리지 않으면 최대 500행까지 그립니다. 요청 하나가 펼치는 노드는 최대 20,000개, 문서 JSON 전체는 1MB 미만, 인라인 이미지는 요청당 합계 30MB, 템플릿 중첩은 최대 4단계입니다. 무언가를 잘랐으면 말합니다 — 응답의 warnings 배열이 어느 노드에서 무슨 일이 있었는지 알려줍니다.
렌더에는 브라우저 안에서 5초의 예산이 있습니다. 그보다 오래 걸리는 문서는 매달리는 대신 실패하고, 큐도 다시 시도하지 않습니다 — 지금 너무 큰 문서는 두 번째에도 너무 큽니다.
MCP 서버
AI 에이전트가 brewmypdf 를 직접 부릴 수 있습니다. MCP 끝점은 /mcp 이고, HTTP 위의 JSON-RPC 2.0 으로 말하며, REST API 와 같은 x-api-key 헤더로 인증합니다 — 에이전트는 별도 신원이 아니므로 쿼터와 감사 기록이 계정에 그대로 남습니다.
도구는 넷입니다: list_templates, describe_template_schema(템플릿이 어떤 데이터를 요구하는지), preview_template(렌더 쿼터를 쓰지 않고 미리보기), render_pdf(POST /v1/create 와 같은 경로).
MCP 클라이언트를 https://brewmypdf.brewmypdf.workers.dev/mcp 에 키와 함께 연결하면 됩니다. 설명으로 템플릿을 만드는 것은 콘솔의 별도 기능이고 크레딧으로 계량합니다 — MCP 도구는 모델을 부르지 않습니다.
에러
모든 에러에는 바뀌지 않는 코드가 있습니다. 메시지가 아니라 코드로 분기하십시오 — 메시지는 다시 쓰이고 번역됩니다.
| 코드 | 상태 | 의미 |
|---|---|---|
E:api.auth.key#unauthorized | 401 | 키가 없거나, 모르는 키이거나, 폐기된 키입니다. |
E:api.create#bad-json | 400 | 요청 본문이 올바른 JSON 이 아닙니다. |
E:api.create#missing-template | 400 | template 도 template_id 도 없습니다. |
E:editor.doc.model#schema-mismatch | 400 | 템플릿 문서가 스키마를 위반했습니다. 응답이 무엇이 어디서 틀렸는지 알려줍니다. |
E:api.limit.payload#too-large | 413 | 요청 본문이 크기 제한을 넘었습니다. |
E:api.rate.limit#exceeded | 429 | 레이트리밋 초과. Retry-After 만큼 기다리십시오. |
E:acct.quota#exceeded | 402 | 월 렌더 쿼터 소진. 플랜을 올리거나 다음 주기를 기다리십시오. |
E:api.job.route#not-found | 404 | 그런 작업이 없거나 남의 작업입니다. 둘을 구분해 알려주지 않습니다. |
E:job.webhook#bad-url | 400 | 알림 주소가 https 가 아니거나, 내부 주소이거나, 너무 깁니다. |