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 을 알려줍니다.
실패한 작업은 POST /v1/jobs/{id}/retry 로 다시 돌릴 수 있습니다. failed 상태에서만 되고, 요청 데이터는 24시간 보관되므로 그 안에만 됩니다 — 지나면 다시 제출하십시오. 성공(done)한 작업은 재실행되지 않습니다: 같은 렌더가 두 번 과금되기 때문입니다.
# 1. enqueue the job
curl -X POST https://brewmypdf.com/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.com/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]] 는 인쇄하면서 채워지는데, 인쇄를 하지 않기 때문입니다.
배경 이미지 올리기
템플릿에 깔 배경 이미지는 POST /v1/assets 로 올립니다. 본문에 파일을 그대로 담고 content-type 을 image/jpeg 또는 image/png 로 주면 됩니다. 응답의 asset_id 를 템플릿의 page.backgrounds[].assetId 에 넣습니다.
⚠ 렌더에 쓰는 PDF 업로드(POST /v1/files)와 다른 주소입니다. 저쪽은 24시간 뒤 지워지지만 배경은 템플릿의 일부라 지울 때까지 남습니다. 주소를 나눈 이유가 그것입니다 — 파라미터 하나로 갈랐다면 잘못 줬을 때 다음 날 템플릿이 깨지는데 응답은 똑같습니다.
JPEG 과 PNG 만 받습니다. WebP · GIF · SVG 는 거절합니다. 개당 2MB 까지이고 한 문서에 3개까지 깔 수 있습니다.
PNG 은 압축을 풀지 않고 그대로 싣습니다 — 빠르고 파일도 커지지 않습니다. 다만 투명도(알파 채널)나 인터레이스가 있는 PNG 은 서버가 다시 압축해야 해서, 그런 이미지들의 픽셀 합계가 400만을 넘으면 거절합니다. 200dpi A4 한 장이 약 387만 픽셀입니다. 300dpi 전면 서식은 JPEG 으로 올리십시오 — 그쪽은 픽셀 제한이 없고 결과도 같습니다.
응답의 passthrough 가 false 면 그 이미지가 위 제한을 먹는 쪽입니다. 올린 직후에 알 수 있으니 확인해 두십시오.
DELETE /v1/assets/{id} 로 지웁니다. 없는 것을 지워도 성공으로 답합니다 — 있고 없고를 구분해 알려주면 남의 자산이 있는지 알려주는 셈이기 때문입니다. 저희가 자동으로 지우지 않으므로, 안 쓰는 자산은 직접 지우십시오.
배경은 모든 쪽에 깔리고 항상 맨 밑입니다. 특정 쪽만 지정할 수 없습니다 — 저희는 렌더 전에 쪽수를 모릅니다. 위에 얹으려면 POST /v1/pdf/stamp 를 쓰십시오. 좌표는 용지 전체 기준이며 노드 좌표(여백 안쪽)와 다릅니다. PDF 를 배경으로 쓰는 것은 아직 안 됩니다.
암호 걸기
요청에 password 를 넣으면 그 암호 없이는 열 수 없는 PDF 가 나옵니다. 계약서·급여명세처럼 파일이 새어도 열리면 안 되는 문서를 위한 것입니다. 내려받기 주소는 전달 경로를 지킬 뿐, 내려받은 파일은 누구나 열 수 있기 때문입니다.
owner_password 를 함께 주면 그 암호로 열었을 때는 제한이 없습니다. 안 주면 열람 암호가 곧 권한 암호가 되어, 아래 제한이 사실상 의미가 없어집니다 — 제한을 걸 생각이면 둘을 다르게 주십시오.
permissions 로 print · copy · modify · annotate · fill 을 각각 끌 수 있습니다. 안 주면 전부 허용입니다. 폼 필드가 있는 문서에서 fill 을 끄면 그 칸들이 무용지물이 되니 주의하십시오. 화면 읽기 프로그램을 위한 텍스트 추출은 저희가 막지 않습니다.
암호는 UTF-8 로 127바이트까지입니다 — 한글은 한 글자가 3바이트이므로 42자쯤입니다. 넘으면 잘라서 거는 대신 거절합니다. 조용히 잘린 암호는 아무도 열 수 없는 파일이 되고, 그때는 되돌릴 방법이 없기 때문입니다.
저희는 암호를 보관하지 않습니다. 요청과 함께 왔다가 작업이 끝나면 지워집니다 — 그러므로 잃어버리면 저희도 그 파일을 열어 드릴 수 없습니다. 그림(PNG · JPEG)에는 암호를 걸 수 없어 요청하면 거절합니다.
이미 있는 PDF 다루기
가지고 계신 PDF 로도 일할 수 있습니다. 먼저 POST /v1/files 로 파일을 올리십시오 — 본문에 PDF 를 그대로 담고 content-type 만 application/pdf 로 주면 됩니다. 응답의 file_id 를 아래 조작에 씁니다. 올린 파일은 24시간 뒤 지워집니다. 파일 하나는 10MB 이하이면서 100쪽 이하여야 합니다. 합치기는 파일 20개, 합쳐서 100쪽까지 — 같은 선입니다. 합치기의 결과물은 파일 하나이고, 실제로 열리는 것이 그것이기 때문입니다. 쪽수 제한을 용량과 따로 둔 이유가 있습니다: 3,000쪽 PDF 가 600KB 도 안 될 수 있어서 용량만으로는 걸리지 않습니다. 그리고 이 선을 정한 것은 저희 서버가 아니라 받는 쪽입니다 — 저희 서버가 멀쩡히 처리하는 1,000쪽 PDF 도 휴대폰에서는 멈춥니다. 받는 분이 못 여는 파일을 만들어 드리느니 거절하는 편을 택했습니다.
GET /v1/files/{id} 는 그 파일이 무엇인지 알려줍니다 — 쪽수, 쪽마다의 크기와 회전, 암호 여부, XFA 서식 여부(xfa: true — 아래 입력칸 항목의 주의를 보십시오), 그리고 안에 있는 입력칸 이름들입니다. 크기는 뷰어가 보여주는 크기입니다 — CropBox 를 MediaBox 로 자르고 양수로 정규화한 값이라, 파일 내부의 원시 좌표가 아니라 화면에서 보는 쪽과 일치합니다. 붙이기 전에 이것부터 보십시오.
POST /v1/pdf/merge 는 files 에 준 순서대로 이어 붙입니다 — 한 번에 20개까지. POST /v1/pdf/stamp 는 글자를 얹습니다: text 는 필수이고 pages(1부터, 비우면 전부) · x · y(쪽 좌하단 기준 pt) · size · opacity · rotate · color 를 줄 수 있습니다. 워터마크와 글자 추가는 같은 것이고 옵션만 다릅니다.
POST /v1/pdf/fields 는 이미 있는 입력칸에 값을 넣습니다. values 에 이름과 값을 주면 되고, flatten 을 true 로 하면 칸이 그림으로 굳어 더는 고칠 수 없게 됩니다 — 되돌릴 수 없으니 신중히 쓰십시오. 서명 자리가 있는 문서는 평탄화하지 않습니다: 굳히면 서명할 자리 자체가 사라지기 때문입니다. 응답의 filled · missing · rejected 를 꼭 확인하십시오: 문서에 없는 이름과 넣지 못한 값을 저희가 조용히 넘기지 않고 알려 드립니다.
못 하는 것: 글자는 Latin-1 만 얹을 수 있습니다 — 한글·한자·이모지는 거절합니다. 표준 글꼴로는 그릴 수 없고, 깨진 글자를 조용히 찍는 것보다 거절하는 편이 낫기 때문입니다. 암호가 걸린 PDF 는 저희가 열 수 없어 조작할 수 없습니다. 목록이 있는 칸에는 그 목록에 있는 값만 넣습니다. XFA 서식(Adobe LiveCycle — 관공서·금융 서식에 흔합니다)은 통째로 거절합니다: 칸이 채워지는 것처럼 보여도 뷰어는 XFA 쪽을 그리므로, 넣은 값이 조용히 사라지기 때문입니다. 이 조작들은 브라우저를 쓰지 않으므로 월 렌더 쿼터를 깎지 않습니다 — 레이트리밋만 적용됩니다.
API 로 보는 내 계정
GET /v1/account 는 한도가 거절로 알려주기 전에 현재 위치를 알려줍니다 — 플랜, 이번 달 렌더 사용/잔여, 템플릿 수와 상한, 이번 달 작업 요약(성공·실패·바이트). 이 숫자들은 한도를 집행하는 바로 그 함수에서 나옵니다 — 여기 보이는 잔여가 곧 쿼터 게이트가 아직 받아줄 수입니다. 읽기 전용입니다: 플랜 변경은 결제 포털에서, API 키 관리는 콘솔에서만 합니다.
활동 기록
계정에서 일어난 모든 동작이 덧붙이기 전용 기록으로 남습니다 — 누가, 언제, 어디서, 성공했는지. 콘솔의 활동에서 읽습니다. 90일이 지나면 지우지 않고 보관소로 옮깁니다.
각 기록에는 바뀌지 않는 동작 코드가 붙습니다. 화면에는 사람이 읽는 이름이 보이지만, 분기는 코드로 하십시오 — 문구를 다듬어도 코드는 그대로입니다. 우리 직원이 남긴 기록은 BrewMyPDF 팀으로 표시되므로, 우리가 계정을 열어볼 때마다 보입니다.
| 코드 | 의미 |
|---|---|
acct.billing.webhook | 결제 변화 |
acct.team.accept | 팀 초대 수락 |
acct.team.invite | 팀원 초대 |
acct.team.remove | 팀원 제거 |
acct.team.role | 팀원 역할 변경 |
admin.account.activate | BrewMyPDF 팀이 계정 복구 |
admin.account.erase | BrewMyPDF 팀이 계정 삭제 |
admin.account.list | BrewMyPDF 팀이 계정 목록 조회 |
admin.account.plan | BrewMyPDF 팀이 플랜 변경 |
admin.account.suspend | BrewMyPDF 팀이 계정 정지 |
admin.account.view | BrewMyPDF 팀이 계정을 열람 |
admin.job.retry | BrewMyPDF 팀이 렌더 재시도 |
admin.job.view | BrewMyPDF 팀이 렌더 열람 |
admin.usage.view | BrewMyPDF 팀이 사용량 열람 |
api.limit.ip.set | API IP 허용목록 변경 |
auth.apikey.issue | API 키 발급 |
auth.apikey.rotate | API 키 교체 |
auth.apikey.verify | API 키 사용 |
auth.login.logout | 로그아웃 |
auth.login.submit | 로그인 |
auth.password.change | 비밀번호 변경 |
auth.reset.confirm | 비밀번호 재설정 완료 |
auth.reset.request | 비밀번호 재설정 요청 |
auth.signup.form | 가입 신청 |
auth.signup.verify | 이메일 인증 |
job.dlq | 렌더 최종 실패 |
job.queue.consume | 렌더 완료 |
job.queue.enqueue | 렌더 요청 |
legal.erase | 계정 삭제 |
store.byo.deliver | 내 저장소로 전달 |
store.eu.set | 데이터 소재지 변경 |
tpl.crud.create | 템플릿 만들기 |
tpl.crud.delete | 템플릿 삭제 |
tpl.crud.update | 템플릿 수정 |
tpl.version.restore | 이전 버전 복원 |
팀
Professional 플랜부터 콘솔의 팀 메뉴에서 사람을 초대할 수 있습니다. 편집자는 템플릿을 고칠 수 있고, 조회자는 보기와 미리보기만 합니다. 구성원은 템플릿에만 닿습니다 — 결제·API 키·설정은 소유자 전용인데, 이는 제한이 아니라 설계입니다: 그 셋은 계정의 자격 그 자체라서, 위임하는 순간 "누가 결제했나"와 "누가 유출했나"의 답이 사라집니다. 초대는 이메일로 가며, 초대받은 주소로 로그인한 상태에서만 수락됩니다. 구성원이 한 일은 전부 그 사람의 이름으로 감사 로그에 남습니다. 팀 API 는 없습니다 — 사람을 관리하는 일은 신중해야 해서, 콘솔에서만 합니다.
API 로 템플릿 관리하기
템플릿도 API 로 관리할 수 있습니다 — 저장소에 두고 코드처럼 배포할 수 있다는 뜻입니다. GET /v1/templates 는 목록을 줍니다(id·이름·시각, ?limit 은 200까지). POST /v1/templates 는 { name, doc } 으로 만들고 template_id 와 version_id 를 돌려줍니다. doc 은 편집기가 저장하는 것과 같은 문서 JSON 이며 아래 스키마 절이 설명합니다. 저장 시점에 검증합니다 — 렌더에서 실패할 템플릿은 저장에서 거절되고, 응답에 위반 목록이 실립니다.
GET /v1/templates/{id} 는 현재 doc 과 schema_version 을 포함해 돌려줍니다. PUT /v1/templates/{id} 로 수정합니다 — doc 을 주면 새 버전이 쓰이고, name 을 주면 이름이 바뀝니다. 둘 다 없으면 성공을 가장하는 대신 거절합니다. 수정은 절대 덮어쓰지 않습니다: doc 을 쓸 때마다 새 버전이 생기고 템플릿이 그것을 가리키므로, 렌더에 썼던 과거가 사라지지 않습니다. DELETE /v1/templates/{id} 는 소프트 삭제입니다 — 과거 렌더 이력은 남고, 목록에서만 사라집니다.
GET /v1/templates/{id}/versions 는 버전 목록을 줍니다(doc 본문 없이 — 최신 순, 현재 버전 표시). POST /v1/templates/{id}/versions/{vid}/restore 는 옛 버전을 다시 현재로 만듭니다. 복원은 복사가 아니라 포인터 이동이라 즉시 끝나고, 그 한 호출이 곧 롤백입니다.
이 호출들은 키의 레이트리밋을 함께 쓰지만 렌더 쿼터를 깎지 않습니다. API 키로 한 변경은 감사 로그에 키로 남아, 사람이 콘솔에서 한 수정과 구분됩니다. 플랜의 템플릿 수 한도는 콘솔과 똑같이 생성에 적용됩니다.
에디터 임베드
저희 템플릿 에디터를 귀사 제품 안에 넣을 수 있습니다 — 저희 브랜드 없이, 귀사 사용자가 BrewMyPDF 계정을 만들 필요도 없이. 귀사 서버에서 API 키로 POST /v1/embed/sessions 를 부르십시오 — { template_id, mode, ttl_seconds } 이며 mode 는 "edit" 또는 "view", 수명은 기본 15분·최대 1시간입니다. 응답의 url 을 iframe 에 넣으면 됩니다. 브라우저에서 직접 부르면 안 됩니다: 요점이 바로 API 키는 서버에 남고 짧은 URL 만 화면에 내려간다는 것입니다.
세션 안에서 저장하면 그 템플릿의 새 버전이 쓰이고(감사에는 API 키로 남습니다), 미리보기는 일반 파이프라인으로 렌더되어 쿼터를 씁니다 — 임베드라고 싸지지 않습니다. "view" 세션은 저장할 수 없습니다. 세션이 만료되면 저장이 403 으로 거절됩니다: 서버에서 새 세션을 열어 URL 을 다시 넣으십시오. 토큰이 URL 에 실리므로, 임베드 URL 자체를 수명 짧은 비밀로 다루십시오.
내 버킷으로 받기
Professional 플랜부터 콘솔(설정 → BYO)에서 성공한 렌더 전부를 귀사 소유 S3 호환 버킷(Cloudflare R2·AWS S3)에 복사하게 할 수 있습니다 — 선택한 접두 경로 아래 {job_id}.{ext} 이름으로. 계약을 분명히 적습니다: 배달은 복사이지 대체가 아닙니다. 저희 저장본과 서명 URL 은 그대로 동작하며, 배달 실패는 작업을 실패시키지 않습니다 — 감사 로그에 남고, 다음 렌더가 다시 시도할 뿐입니다. 설정을 저장하면 즉시 테스트 업로드가 나가므로, 잘못된 자격은 첫 실제 렌더 전에 드러납니다. 비밀 키는 암호화되어 저장되며 다시 표시되지 않습니다.
EU 데이터 레지던시
Premium 플랜부터 설정 → 데이터 레지던시에서 문서를 EU 에 고정할 수 있습니다: 켜면 렌더 산출물과 요청 데이터가 Cloudflare R2 의 EU 관할 버킷에 저장되고, 다운로드는 항상 캐시 없이 제공됩니다. 경계를 정확히 적습니다: 이 보장은 전환 이후 만들어지는 문서 본문(산출물·요청 데이터)에 적용됩니다 — 전환 전에 저장된 객체는 이동하지 않고, 템플릿 자산과 폰트는 글로벌 공용이며, 작업 메타데이터(상태·감사 기록)는 글로벌 데이터베이스에 있습니다. 렌더가 실행되는 위치는 네트워크 엣지가 정하며 이 보장에 포함되지 않습니다.
한도
레이트리밋은 키·계정·IP 세 축을 동시에 셉니다. 하나만 걸면 키를 하나 더 발급하거나 다른 호스트에서 부르는 것으로 간단히 우회되기 때문입니다.
한도를 넘으면 Retry-After 헤더와 함께 429 가 옵니다. 그 값을 지켜 주십시오 — 즉시 재시도하면 상황이 나빠집니다.
월 렌더 쿼터는 이와 별개로 계정 단위로 셉니다. 소진되면 429 가 아니라 402 입니다 — 기다려서 풀리는 것과 아닌 것을 구분해야 하기 때문입니다.
내용 한도는 요금 한도와 별개이고, 사람들이 놀라는 쪽은 이쪽입니다. 쪽수는 한 렌더에 100쪽까지입니다 — 넘으면 만들기 전에 400 으로 거절합니다(자르지 않습니다. 100장만 나온 청구서 묶음은 완성된 것처럼 보여서 더 나쁩니다). 나머지 한도가 둘이고 서로 다른 것을 셉니다. 표 행은 렌더 한 번에 모두 합해 2,000행까지이고 표 하나는 500행까지입니다 — 표가 여럿이면 그 합이 2,000에서 끊깁니다. 새 페이지를 시작하는 반복 블록(같은 양식을 데이터마다 되풀이하며 벌마다 새 쪽을 여는 것)은 한 렌더에 100벌까지입니다 — 한 벌이 한 쪽이므로 그것이 곧 100쪽입니다. 새 쪽을 열지 않는 블록은 이 수에 들지 않습니다. 둘은 별개 예산이라 서로를 깎지 않습니다 — 회사 100곳에 각각 8줄짜리 청구서를 만들면 블록 100 · 행 800 으로 둘 다 안쪽입니다. 그 이상은 자르고 warnings 로 알려드립니다. 이 선은 원가와 예측 가능성에서 나옵니다 — 상한이 없으면 문서 하나가 렌더 시간을 몇 배로 쓰고, 어디서 잘릴지는 열 수에 따라 달라져 미리 알 수 없습니다. 요청 하나가 펼치는 노드는 최대 20,000개, 문서 JSON 전체는 1MB 미만, 인라인 이미지는 요청당 합계 30MB, 템플릿 중첩은 최대 4단계입니다. 무언가를 잘랐으면 말합니다 — 응답의 warnings 배열이 어느 노드에서 무슨 일이 있었는지 알려줍니다.
렌더에는 브라우저 안에서 5초의 예산이 있습니다. 그보다 오래 걸리는 문서는 매달리는 대신 실패하고, 큐도 다시 시도하지 않습니다 — 지금 너무 큰 문서는 두 번째에도 너무 큽니다.
렌더가 밀리면 접수 자체를 거절합니다 — 429 와 Retry-After 헤더가 옵니다. 받아 놓고 몇 분씩 늦게 주는 것보다 당장 못 받는다고 말하는 쪽이 낫기 때문입니다. Retry-After 만큼 기다렸다가 그대로 다시 보내면 됩니다.
API 접근을 귀사 서버로 못박을 수 있습니다: 콘솔의 설정 → API IP 허용목록에 IP·CIDR 을 20개까지 등록하면, 목록 밖에서 온 API 키 요청은 유효한 키라도 403 을 받습니다. 콘솔 로그인은 의도적으로 예외라, 목록 실수로 스스로를 잠가도 언제나 고칠 수 있습니다. MCP 엔드포인트에도 같은 규칙이 적용됩니다.
MCP 서버
AI 에이전트가 BrewMyPDF 를 직접 부릴 수 있습니다. MCP 끝점은 /mcp 이고, HTTP 위의 JSON-RPC 2.0 으로 말하며, REST API 키를 Authorization: Bearer <키> 로 보내 인증합니다 — 헤더 이름은 REST API(x-api-key)와 다릅니다. 키 자체는 같은 것이므로 에이전트는 별도 신원이 아니고 쿼터와 감사 기록이 계정에 그대로 남습니다.
도구는 넷입니다: list_templates, describe_template_schema(템플릿이 어떤 데이터를 요구하는지), preview_template(렌더 쿼터를 쓰지 않고 미리보기), render_pdf(POST /v1/create 와 같은 경로).
MCP 클라이언트를 https://brewmypdf.com/mcp 에 키와 함께 연결하면 됩니다. 설명으로 템플릿을 만드는 것은 콘솔의 별도 기능이고 크레딧으로 계량합니다 — MCP 도구는 모델을 부르지 않습니다.
표 파일 실행
GET /v1/templates/{id}/form?format=xlsx (또는 format=csv&sheet=doc) 는 콘솔이 주는 것과 같은 데이터 양식을 돌려줍니다. 배열 깊이마다 시트 하나, 1행 헤더, 템플릿 id 와 경로 집합의 12자리 해시를 실은 숨김 시트(xlsx) 또는 파일명(csv). 접수할 때 그 해시를 돌려보냅니다.
POST /v1/intake/runs 는 이미 data 객체로 접은 문서를 — 표 파싱은 호출자 몫입니다 — 최대 50건씩 받습니다: { template_id, hash, total, seq_start, docs: [{ key, data }], run_id }. 첫 청크는 run_id 없이 보내고 응답에서 받습니다. seq_start + docs.length 가 total 에 닿으면 실행이 큐에 들어갑니다. 청크마다 /v1/batch 와 같은 쿼터·백로그·예산·쪽수 검사를 받고, 거절되면 실행은 그 지점에서 failed 가 되며 이미 접수된 것은 그대로 만들어집니다.
GET /v1/intake/runs/{id} 는 status(submitting · queued · done · partial · failed), 개수, 완료 문서마다 key 와 서명 url, 실패 문서의 error_code, ZIP 이 있으면 zip_url 을 돌려줍니다. POST /v1/intake/runs/{id}/zip 은 끝난 실행의 ZIP 을 만듭니다 — 저장소로 흘려보내며 합계 500MB 까지이고, 넘으면 413 이며 문서를 개별로 내려받습니다.
에러: E:intake.serve#stale-form (409, 양식이 템플릿과 안 맞음 — 새로 받으세요), E:intake.serve#bad-chunk (400), E:intake.run#chunk-failed (원인은 안에: 쿼터·예산·쪽수), E:intake.run#not-found (404), E:intake.run#zip-not-ready (409), E:intake.run#zip-too-large (413).
커넥터 만들기
Zapier·Make·n8n 을 비롯한 자동화 도구는 모두 같은 다섯 가지를 요구하고, 다섯 가지가 모두 여기 있습니다. 키 확인은 GET /v1/account 로 하십시오 — 가볍고 부작용이 없습니다. 템플릿 드롭다운은 GET /v1/templates, 필드 매핑 UI 는 GET /v1/templates/{id}/schema, 생성은 POST /v1/create, 오래 걸리는 것은 GET /v1/jobs/{id} 로 확인하거나 webhook 을 주시면 알려 드립니다.
알아 두실 것은 스키마 엔드포인트입니다. paths(data.customer · data.items[*].name 같은 평면 목록)와 schema(JSON Schema)를 돌려줍니다. 입력 칸으로 만들 것은 평면 목록이고, JSON Schema 는 타입이 필요할 때 쓰십시오. 배열 경로는 [*] 로 끝납니다 — 한 항목이 모든 원소를 설명하므로, 입력 한 줄을 만들어 반복하시면 됩니다.
두 가지에서 자주 걸립니다. 계정 응답의 plan 은 문자열이 아니라 객체입니다({ id, renders_per_month, templates_max }) — 그대로 라벨에 쓰면 [object Object] 가 찍힙니다. 그리고 표현식이 없는 템플릿은 paths 가 빈 목록인데, 그것이 맞습니다: 그 템플릿은 데이터를 받지 않습니다.
고객이 이미 갖고 있는 API 키를 그대로 쓰십시오. 커넥터 전용 신원도, OAuth 도, 별도 한도도 없습니다 — 어느 경로로 부르시든 쿼터·감사·한도가 그 계정에 남습니다.
에러
모든 에러에는 바뀌지 않는 코드가 있습니다. 메시지가 아니라 코드로 분기하십시오 — 메시지는 다시 쓰이고 번역됩니다.
| 코드 | 상태 | 의미 |
|---|---|---|
E:api.auth.key#unauthorized | 401 | 키가 없거나, 모르는 키이거나, 폐기된 키입니다. |
E:api.auth.key#plan-required | 403 | 키는 맞지만 계정 플랜에 API 이용이 포함돼 있지 않습니다. Professional 플랜부터 열립니다. |
E:api.create#bad-json | 400 | 요청 본문이 올바른 JSON 이 아닙니다. |
E:api.create#missing-template | 400 | template 도 template_id 도 없습니다. |
E:api.export.mode#unsupported | 400 | export_type 이 "json" 도 "file" 도 아닙니다. |
E:api.export.mode#not-sync | 400 | export_type "file" 은 POST /v1/create 에서만 쓸 수 있습니다. |
E:editor.doc.model#schema-mismatch | 400 | 템플릿 문서가 스키마를 위반했습니다. 응답이 무엇이 어디서 틀렸는지 알려줍니다. |
E:api.limit.payload#too-large | 413 | 요청 본문이 크기 제한을 넘었습니다. |
E:api.rate.limit#exceeded | 429 | 레이트리밋 초과. Retry-After 만큼 기다리십시오. |
E:api.limit.backlog#full | 429 | 렌더가 밀려 접수를 잠시 멈췄습니다. Retry-After 만큼 기다렸다가 다시 보내십시오. |
E:api.job.retry#not-failed | 400 | failed 상태의 작업만 다시 돌릴 수 있습니다. |
E:api.job.retry#payload-expired | 400 | 요청 데이터가 만료됐습니다(24시간) — 다시 제출하십시오. |
E:acct.quota#exceeded | 402 | 월 렌더 쿼터 소진. 플랜을 올리거나 다음 주기를 기다리십시오. |
E:acct.quota#daily-exceeded | 402 | Free 플랜 전용 — 일 렌더 상한에 닿았습니다. UTC 00:00 에 초기화됩니다. 플랜을 올리면 일 상한이 없어지고 월 쿼터만 남습니다. |
E:api.job.route#not-found | 404 | 그런 작업이 없거나 남의 작업입니다. 둘을 구분해 알려주지 않습니다. |
E:job.webhook#bad-url | 400 | 알림 주소가 https 가 아니거나, 내부 주소이거나, 너무 깁니다. |
E:render.pdf.password#bad-password | 400 | 암호가 문자열이 아니거나 UTF-8 127바이트를 넘습니다. |
E:render.pdf.password#not-pdf | 400 | 그림에는 암호를 걸 수 없습니다. |
E:render.pdf.password#failed | 400 | 암호를 걸지 못했습니다. 파일은 만들지 않습니다 — 암호 없는 문서가 나가는 것보다 낫습니다. |
E:render.pdfpage#too-large | 400 | 배경 이미지가 2MB 를 넘습니다. |
E:render.pdfpage#too-many | 400 | 한 문서에 배경은 3개까지입니다. |
E:render.pdfpage#too-many-pixels | 400 | 투명도나 인터레이스가 있는 PNG 들의 픽셀 합계가 400만을 넘습니다 — 전면 서식은 JPEG 으로 올리십시오. |
E:render.pdfpage.png#not-png | 400 | JPEG 도 PNG 도 아닙니다. WebP · GIF · SVG 는 배경으로 쓸 수 없습니다. |
E:render.pdfpage.png#bad-idat | 400 | PNG 이 손상됐습니다 — 안에 든 그림이 선언한 크기와 다릅니다. |
E:tpl.asset#not-found | 400 | 그런 자산이 없습니다. 지워졌거나 다른 계정의 것입니다. |
E:pdf.op.file#not-pdf | 400 | 올린 것이 PDF 가 아닙니다 — 파일이 %PDF- 로 시작하지 않습니다. |
E:pdf.op.file#not-found | 400 | 그런 파일이 없습니다. 24시간이 지나 지워졌거나, 다른 계정의 파일입니다. |
E:pdf.op.file#too-large | 400 | 올린 PDF 가 10MB 를 넘습니다. 나눠 올리거나 먼저 줄이십시오. |
E:pdf.op.merge#empty | 400 | files 가 비었습니다. 합칠 것이 없습니다. |
E:pdf.op.merge#too-many | 400 | 한 번에 20개까지 합칠 수 있습니다. |
E:pdf.op.stamp#not-latin1 | 400 | 얹으려는 글자를 표준 글꼴로 그릴 수 없습니다 (Latin-1 만 가능). |
E:pdf.op.stamp#empty | 400 | text 가 비었습니다. 얹을 글자가 없습니다. |
E:pdf.op.stamp#too-long | 400 | 얹을 글자가 200자를 넘습니다. |
E:pdf.op.fields#empty | 400 | values 가 비었습니다. 넣을 값이 없습니다. |
E:pdf.op.fields#too-many | 400 | 한 번에 500칸까지 채울 수 있습니다. |
E:pdf.op.fields#encrypted | 400 | 암호가 걸린 PDF 는 저희가 열 수 없어 칸을 채울 수 없습니다. |
E:pdf.op.fields#xfa | 400 | XFA 서식(Adobe LiveCycle)입니다. 값을 넣어도 뷰어에 나타나지 않으므로 "채웠다"고 응답하는 대신 거절합니다. 붙이기 전에 GET /v1/files/{id} 의 xfa 를 확인하십시오. |
E:pdf.op.file#too-many-pages | 400 | 받는 쪽수 상한(100쪽)을 넘었습니다. 나눠서 올려 주십시오. |
E:render.pdf.merge#too-many-pages | 400 | 합쳐서 100쪽을 넘습니다. 한 번에 더 적게 합쳐 주십시오. |
E:api.template#not-found | 404 | 그런 템플릿이 없습니다. 다른 계정 것이거나 삭제된 것일 수 있으며, 둘을 구분해 알려드리지 않습니다. |
E:api.template#nothing-to-update | 400 | PUT 에 name 도 doc 도 없습니다. 바꿀 것이 없습니다. |
E:tpl.crud#not-found | 404 | 그런 템플릿이 없습니다. 다른 계정 것이거나 삭제된 것일 수 있으며, 둘을 구분해 알려드리지 않습니다. |
E:tpl.version#not-found | 404 | 이 템플릿에 그런 버전이 없습니다. |
E:tpl.crud#invalid-doc | 400 | 템플릿 문서가 스키마를 위반했습니다. 응답이 무엇이 어디서 틀렸는지 알려줍니다. |
E:tpl.crud#missing-name | 400 | name 이 비어 있습니다. 템플릿에는 이름이 필요합니다. |
E:pdf.op.fields#flatten-signature | 400 | 서명 자리가 있는 문서는 평탄화하지 않습니다 — 굳히면 서명할 자리 자체가 사라집니다. |
E:pdf.op.fields#flatten-failed | 400 | 평탄화하지 못했습니다. 값은 넣지 않은 채로 두었으니 flatten 없이 다시 부르십시오. |