REST API
Fill forms, keep client records and run bulk jobs from your own code, scripts and automation tools. The API does everything the MCP server does, with the same keys.
Quick start
- Create a key at app.boltpdf.com/settings/api-keys. It's shown once, so copy it then.
-
Send it with every request as
Authorization: Bearer bpk_…. The base URL ishttps://app.boltpdf.com/api/v1.
curl https://app.boltpdf.com/api/v1/account \
-H "Authorization: Bearer $BOLTPDF_API_KEY" curl "https://app.boltpdf.com/api/v1/templates?search=W-9" \
-H "Authorization: Bearer $BOLTPDF_API_KEY"
curl https://app.boltpdf.com/api/v1/templates/TEMPLATE_ID \
-H "Authorization: Bearer $BOLTPDF_API_KEY" curl https://app.boltpdf.com/api/v1/templates/TEMPLATE_ID/pdf \
-H "Authorization: Bearer $BOLTPDF_API_KEY" \
-H "Content-Type: application/json" \
-d '{"formData": {"FIELD_ID": "Ana Cruz"}}' \
-o filled.pdf curl https://app.boltpdf.com/api/v1/records \
-H "Authorization: Bearer $BOLTPDF_API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Ana Cruz"}'
curl https://app.boltpdf.com/api/v1/records/RECORD_ID/values \
-H "Authorization: Bearer $BOLTPDF_API_KEY" \
-H "Content-Type: application/json" \
-d '{"fieldData": [{"standardKey": "taxpayer.full_name", "fieldKey": "name", "value": "Ana Cruz"}]}'
curl https://app.boltpdf.com/api/v1/records/RECORD_ID/pdf \
-H "Authorization: Bearer $BOLTPDF_API_KEY" \
-H "Content-Type: application/json" \
-d '{"templateId": "TEMPLATE_ID"}' \
-o ana-w9.pdf Requests and responses
- IDs in the path go in the path. GET and DELETE take everything else as query parameters; POST and PATCH take a JSON body.
- Each endpoint takes the same inputs as the matching MCP tool, and answers with JSON. Creating something answers 201, anything else 200.
-
The
/pdfendpoints answer with the PDF itself. TheBoltPDF-Fields-FilledandBoltPDF-Total-Fieldsheaders say how many fields got a value, andBoltPDF-Ignored-Keyslists anyformDatakeys that matched no field. For a document,BoltPDF-Missing-Fieldslists the merge fields the record has no value for. Addfinishfor page or Bates numbers, a watermark or a password. - For files you can link to later, start a bulk generation instead: each finished item has a download URL.
The full description, with every input, is an OpenAPI 3.1 document at https://app.boltpdf.com/api/v1/openapi.json. Import it into Postman or Insomnia, or generate a client from it. Each operation is named after its MCP tool.
Endpoints
51 endpoints, all under https://app.boltpdf.com/api/v1.
Account
-
GET /accountThe account the key acts as, its subscription and AI page allowance
Templates
-
GET /templatesList and search templates -
POST /templatesUpload a PDF (pdfUrl or pdfBase64) and start field detection -
GET /templates/{id}A template with its sections, fields and each field's fieldId -
PATCH /templates/{id}Rename a template or change its description -
DELETE /templates/{id}Delete a template -
GET /templates/{templateId}/fieldsA template's fields -
GET /templates/{templateId}/detectionField detection status -
POST /templates/{templateId}/detectionRun field detection again, on every page or one -
POST /templates/{templateId}/standard-fieldsMap fields to standard fields -
POST /templates/{templateId}/pdfFill the template with values; the response is the PDF -
PATCH /fields/{id}Change a field's label, type, format or standard field -
GET /standard-fieldsThe standard fields, with suggestions for a template -
GET /categoriesTemplate categories
Records
-
GET /recordsList and search records -
POST /recordsCreate a record -
GET /records/{id}A record with its saved values -
PATCH /records/{id}Rename a record or move it to a group -
DELETE /records/{id}Delete a record -
POST /records/{prefillRecordId}/valuesSave values to a record -
GET /records/{id}/coverageWhich forms the record fills, and what each still lacks -
POST /records/{prefillRecordId}/pdfFill a template from the record; the response is the PDF -
GET /record-groupsList record groups -
POST /record-groupsCreate a record group
Packets
-
GET /packetsList packets -
POST /packetsCreate a packet from templates and documents, in page order -
GET /packets/{packetId}A packet's forms and documents -
PATCH /packets/{packetId}Rename a packet or change its forms and documents -
DELETE /packets/{packetId}Delete a packet -
POST /packets/{packetId}/pdfFill every form and document from a record, merged into one PDF
Documents
-
GET /documentsList documents (letters) and their merge fields -
POST /documents/{documentId}/pdfFill a document from a record; the response is the PDF
Questionnaires
-
GET /questionnairesList questionnaires; withItems=true adds their questions -
GET /questionnaires/{questionnaireId}/answersA record's answers (prefillRecordId in the query) -
POST /questionnaires/{questionnaireId}/answersSave answers to a record, keyed by question id
Automations
-
GET /automationsList automations, with their triggers, steps and last run -
POST /automations/{automationId}/runsRun an automation on a record now (prefillRecordId in the body) -
GET /automation-runsLatest runs; filter by automationId or prefillRecordId -
GET /automation-runs/{runId}A run's status and what each step did
Jobs
-
GET /bulk-generationsRecent bulk generation jobs -
POST /bulk-generationsFill a template, packet or document for many records in the background -
GET /bulk-generations/{id}A job's progress, with download URLs when done -
GET /csv-importsRecent CSV imports -
POST /csv-importsImport rows as records, optionally filling each -
GET /csv-imports/{id}An import's progress
Share links
-
GET /templates/{templateId}/share-linksA template's share links -
POST /templates/{templateId}/share-linksCreate a link anyone can fill in, optionally with an e-signature -
DELETE /share-links/{id}Revoke a share link
Webhooks
-
GET /webhooksThe URLs events are sent to -
POST /webhooksSend events to a URL; answers with its signing secret -
DELETE /webhooks/{id}Stop sending events to a URL
Webhooks
Webhooks tell your server, or a tool like Zapier, when something
happens in BoltPDF. Add a URL at Settings → Webhooks or with POST /webhooks, and pick the events to send.
Each one is a POST of JSON to your URL, which must be public and use
https.
-
share_link.submitted - Someone filled in a share link, and their answers became a new record. Signed links include the signed PDF's URL.
-
client_request.submitted - A client sent their answers for review (signed by everyone, when the request needs signing).
-
client_request.approved - You approved a client's answers, and they were saved to the record.
-
client_request.declined - A client, or someone asked to sign after them, declined a request.
-
bulk_generation.finished - A bulk generation job finished: completed, partly failed or failed.
-
csv_import.finished - A CSV import finished.
Events carry IDs, names and links, not the answers themselves. To read
a record's values, call GET /records/{id} with the record's
ID.
curl https://app.boltpdf.com/api/v1/webhooks \
-H "Authorization: Bearer $BOLTPDF_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/boltpdf", "events": ["share_link.submitted"]}' POST /boltpdf HTTP/1.1
Content-Type: application/json
BoltPDF-Signature: t=1760000000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
BoltPDF-Event-Id: 0b6f4a8e-0d1f-4c7e-9a43-6f1d2c3b4a5e
BoltPDF-Event-Type: share_link.submitted
BoltPDF-Delivery-Id: 9c2e1d7a-5b3f-4e8a-b1c6-2d4f6a8e0c1b
{
"id": "0b6f4a8e-0d1f-4c7e-9a43-6f1d2c3b4a5e",
"type": "share_link.submitted",
"createdAt": "2026-10-09T14:03:12.481Z",
"data": {
"shareLink": { "id": "5d1c…" },
"template": { "id": "8a2e…", "name": "W-9" },
"record": {
"id": "c47b…",
"name": "Ana Cruz",
"url": "https://app.boltpdf.com/prefill-records/c47b…"
},
"submitter": { "name": "Ana Cruz", "email": "ana@example.com" },
"signature": null,
"submittedAt": "2026-10-09T14:03:12.402Z"
}
} Checking the signature
Each webhook has a signing secret (whsec_…), shown when
you add it and in its menu in Settings. The BoltPDF-Signature header holds a Unix timestamp t and v1,
the hex HMAC-SHA256 of t, a period and the raw body,
keyed with the secret. Compute it from the body exactly as received,
compare in constant time, and refuse old timestamps so a captured
request can't be replayed.
import crypto from "node:crypto";
export function verifyBoltPDF(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const expected = crypto
.createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
return (
fresh &&
typeof parts.v1 === "string" &&
parts.v1.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected))
);
} import hashlib, hmac, time
def verify_boltpdf(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
signed = parts["t"].encode() + b"." + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
fresh = abs(time.time() - int(parts["t"])) < 300
return fresh and hmac.compare_digest(expected, parts.get("v1", "")) Answering and retries
- Answer with any 2xx status within 10 seconds. Do slow work after answering. Redirects aren't followed, so use the final URL.
- Anything else is tried again, up to 10 times over about 9 hours, waiting longer each time.
-
An event can arrive more than once, or out of order. Use
BoltPDF-Event-Idto skip ones you've handled, andcreatedAtto order them. - After 20 events in a row fail, the webhook is turned off and its owner sees why in Settings. Answering 410 Gone turns it off at once.
-
Settings shows each webhook's deliveries from the last 30 days, with
what was sent and what your server answered. You can send any of
them again, or send a
webhook.testevent.
Errors
Errors answer with a status code and a JSON body that says what went wrong:
{
"error": {
"code": "not_found",
"message": "Prefill record not found"
}
} - 400
- The input is invalid. The message says what to fix.
- 401
- The key is missing, wrong or revoked.
- 402
- The account has no active subscription. GET /account still works.
- 403
- The key's account can't use this.
- 404
- Not found, or not something the key's account can see.
- 429
- Too many requests. Wait for Retry-After seconds.
- 500
- Something went wrong on our side. Try again.
A 400 for invalid input also lists each problem under error.details.issues.
Limits and security
-
Each account can make 120 requests a minute. Past that, requests
answer 429 with a
Retry-Afterheader. - A key acts as the user who made it, in the organization it was made in, with the same access, subscription and AI allowance as the web app.
- Keep keys on a server. The API doesn't answer browsers on other sites, so a key never has to sit in a web page.
- Revoke a key from Settings → API Keys; it stops working on the next request.
-
Request bodies can be up to about 4 MB. For larger PDFs, upload with
pdfUrlinstead ofpdfBase64.
Questions? Email princh@boltpdf.com.