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)한 작업은 재실행되지 않습니다: 같은 렌더가 두 번 과금되기 때문입니다.

POST /v1/create-async
# 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.activateBrewMyPDF 팀이 계정 복구
admin.account.eraseBrewMyPDF 팀이 계정 삭제
admin.account.listBrewMyPDF 팀이 계정 목록 조회
admin.account.planBrewMyPDF 팀이 플랜 변경
admin.account.suspendBrewMyPDF 팀이 계정 정지
admin.account.viewBrewMyPDF 팀이 계정을 열람
admin.job.retryBrewMyPDF 팀이 렌더 재시도
admin.job.viewBrewMyPDF 팀이 렌더 열람
admin.usage.viewBrewMyPDF 팀이 사용량 열람
api.limit.ip.setAPI IP 허용목록 변경
auth.apikey.issueAPI 키 발급
auth.apikey.rotateAPI 키 교체
auth.apikey.verifyAPI 키 사용
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#unauthorized401키가 없거나, 모르는 키이거나, 폐기된 키입니다.
E:api.auth.key#plan-required403키는 맞지만 계정 플랜에 API 이용이 포함돼 있지 않습니다. Professional 플랜부터 열립니다.
E:api.create#bad-json400요청 본문이 올바른 JSON 이 아닙니다.
E:api.create#missing-template400template 도 template_id 도 없습니다.
E:api.export.mode#unsupported400export_type 이 "json" 도 "file" 도 아닙니다.
E:api.export.mode#not-sync400export_type "file" 은 POST /v1/create 에서만 쓸 수 있습니다.
E:editor.doc.model#schema-mismatch400템플릿 문서가 스키마를 위반했습니다. 응답이 무엇이 어디서 틀렸는지 알려줍니다.
E:api.limit.payload#too-large413요청 본문이 크기 제한을 넘었습니다.
E:api.rate.limit#exceeded429레이트리밋 초과. Retry-After 만큼 기다리십시오.
E:api.limit.backlog#full429렌더가 밀려 접수를 잠시 멈췄습니다. Retry-After 만큼 기다렸다가 다시 보내십시오.
E:api.job.retry#not-failed400failed 상태의 작업만 다시 돌릴 수 있습니다.
E:api.job.retry#payload-expired400요청 데이터가 만료됐습니다(24시간) — 다시 제출하십시오.
E:acct.quota#exceeded402월 렌더 쿼터 소진. 플랜을 올리거나 다음 주기를 기다리십시오.
E:acct.quota#daily-exceeded402Free 플랜 전용 — 일 렌더 상한에 닿았습니다. UTC 00:00 에 초기화됩니다. 플랜을 올리면 일 상한이 없어지고 월 쿼터만 남습니다.
E:api.job.route#not-found404그런 작업이 없거나 남의 작업입니다. 둘을 구분해 알려주지 않습니다.
E:job.webhook#bad-url400알림 주소가 https 가 아니거나, 내부 주소이거나, 너무 깁니다.
E:render.pdf.password#bad-password400암호가 문자열이 아니거나 UTF-8 127바이트를 넘습니다.
E:render.pdf.password#not-pdf400그림에는 암호를 걸 수 없습니다.
E:render.pdf.password#failed400암호를 걸지 못했습니다. 파일은 만들지 않습니다 — 암호 없는 문서가 나가는 것보다 낫습니다.
E:render.pdfpage#too-large400배경 이미지가 2MB 를 넘습니다.
E:render.pdfpage#too-many400한 문서에 배경은 3개까지입니다.
E:render.pdfpage#too-many-pixels400투명도나 인터레이스가 있는 PNG 들의 픽셀 합계가 400만을 넘습니다 — 전면 서식은 JPEG 으로 올리십시오.
E:render.pdfpage.png#not-png400JPEG 도 PNG 도 아닙니다. WebP · GIF · SVG 는 배경으로 쓸 수 없습니다.
E:render.pdfpage.png#bad-idat400PNG 이 손상됐습니다 — 안에 든 그림이 선언한 크기와 다릅니다.
E:tpl.asset#not-found400그런 자산이 없습니다. 지워졌거나 다른 계정의 것입니다.
E:pdf.op.file#not-pdf400올린 것이 PDF 가 아닙니다 — 파일이 %PDF- 로 시작하지 않습니다.
E:pdf.op.file#not-found400그런 파일이 없습니다. 24시간이 지나 지워졌거나, 다른 계정의 파일입니다.
E:pdf.op.file#too-large400올린 PDF 가 10MB 를 넘습니다. 나눠 올리거나 먼저 줄이십시오.
E:pdf.op.merge#empty400files 가 비었습니다. 합칠 것이 없습니다.
E:pdf.op.merge#too-many400한 번에 20개까지 합칠 수 있습니다.
E:pdf.op.stamp#not-latin1400얹으려는 글자를 표준 글꼴로 그릴 수 없습니다 (Latin-1 만 가능).
E:pdf.op.stamp#empty400text 가 비었습니다. 얹을 글자가 없습니다.
E:pdf.op.stamp#too-long400얹을 글자가 200자를 넘습니다.
E:pdf.op.fields#empty400values 가 비었습니다. 넣을 값이 없습니다.
E:pdf.op.fields#too-many400한 번에 500칸까지 채울 수 있습니다.
E:pdf.op.fields#encrypted400암호가 걸린 PDF 는 저희가 열 수 없어 칸을 채울 수 없습니다.
E:pdf.op.fields#xfa400XFA 서식(Adobe LiveCycle)입니다. 값을 넣어도 뷰어에 나타나지 않으므로 "채웠다"고 응답하는 대신 거절합니다. 붙이기 전에 GET /v1/files/{id} 의 xfa 를 확인하십시오.
E:pdf.op.file#too-many-pages400받는 쪽수 상한(100쪽)을 넘었습니다. 나눠서 올려 주십시오.
E:render.pdf.merge#too-many-pages400합쳐서 100쪽을 넘습니다. 한 번에 더 적게 합쳐 주십시오.
E:api.template#not-found404그런 템플릿이 없습니다. 다른 계정 것이거나 삭제된 것일 수 있으며, 둘을 구분해 알려드리지 않습니다.
E:api.template#nothing-to-update400PUT 에 name 도 doc 도 없습니다. 바꿀 것이 없습니다.
E:tpl.crud#not-found404그런 템플릿이 없습니다. 다른 계정 것이거나 삭제된 것일 수 있으며, 둘을 구분해 알려드리지 않습니다.
E:tpl.version#not-found404이 템플릿에 그런 버전이 없습니다.
E:tpl.crud#invalid-doc400템플릿 문서가 스키마를 위반했습니다. 응답이 무엇이 어디서 틀렸는지 알려줍니다.
E:tpl.crud#missing-name400name 이 비어 있습니다. 템플릿에는 이름이 필요합니다.
E:pdf.op.fields#flatten-signature400서명 자리가 있는 문서는 평탄화하지 않습니다 — 굳히면 서명할 자리 자체가 사라집니다.
E:pdf.op.fields#flatten-failed400평탄화하지 못했습니다. 값은 넣지 않은 채로 두었으니 flatten 없이 다시 부르십시오.
POST /v1/create
curl -X POST https://brewmypdf.com/v1/create \
  -H "x-api-key: bmp_live_..." \
  -H "content-type: application/json" \
  --data-binary @invoice.json
응답
{
  "ok": true,
  "id": "01M0...",
  "format": "pdf",
  "url": "https://.../d/eyJ0...",
  "expiresAt": 1786884000
}