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

  1. Create a key at app.boltpdf.com/settings/api-keys. It's shown once, so copy it then.
  2. Send it with every request as Authorization: Bearer bpk_…. The base URL is https://app.boltpdf.com/api/v1.
Check the key
curl https://app.boltpdf.com/api/v1/account \
  -H "Authorization: Bearer $BOLTPDF_API_KEY"
Find a template and its fields
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"
Fill it and save the PDF
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
Save a client as a record, then fill any form from it
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 /pdf endpoints answer with the PDF itself. The BoltPDF-Fields-Filled and BoltPDF-Total-Fields headers say how many fields got a value, and BoltPDF-Ignored-Keys lists any formData keys that matched no field. For a document, BoltPDF-Missing-Fields lists the merge fields the record has no value for. Add finish for 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 /account The account the key acts as, its subscription and AI page allowance

Templates

  • GET /templates List and search templates
  • POST /templates Upload 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}/fields A template's fields
  • GET /templates/{templateId}/detection Field detection status
  • POST /templates/{templateId}/detection Run field detection again, on every page or one
  • POST /templates/{templateId}/standard-fields Map fields to standard fields
  • POST /templates/{templateId}/pdf Fill the template with values; the response is the PDF
  • PATCH /fields/{id} Change a field's label, type, format or standard field
  • GET /standard-fields The standard fields, with suggestions for a template
  • GET /categories Template categories

Records

  • GET /records List and search records
  • POST /records Create 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}/values Save values to a record
  • GET /records/{id}/coverage Which forms the record fills, and what each still lacks
  • POST /records/{prefillRecordId}/pdf Fill a template from the record; the response is the PDF
  • GET /record-groups List record groups
  • POST /record-groups Create a record group

Packets

  • GET /packets List packets
  • POST /packets Create 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}/pdf Fill every form and document from a record, merged into one PDF

Documents

  • GET /documents List documents (letters) and their merge fields
  • POST /documents/{documentId}/pdf Fill a document from a record; the response is the PDF

Questionnaires

  • GET /questionnaires List questionnaires; withItems=true adds their questions
  • GET /questionnaires/{questionnaireId}/answers A record's answers (prefillRecordId in the query)
  • POST /questionnaires/{questionnaireId}/answers Save answers to a record, keyed by question id

Automations

  • GET /automations List automations, with their triggers, steps and last run
  • POST /automations/{automationId}/runs Run an automation on a record now (prefillRecordId in the body)
  • GET /automation-runs Latest runs; filter by automationId or prefillRecordId
  • GET /automation-runs/{runId} A run's status and what each step did

Jobs

  • GET /bulk-generations Recent bulk generation jobs
  • POST /bulk-generations Fill 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-imports Recent CSV imports
  • POST /csv-imports Import rows as records, optionally filling each
  • GET /csv-imports/{id} An import's progress

Share links

  • GET /templates/{templateId}/share-links A template's share links
  • POST /templates/{templateId}/share-links Create a link anyone can fill in, optionally with an e-signature
  • DELETE /share-links/{id} Revoke a share link

Webhooks

  • GET /webhooks The URLs events are sent to
  • POST /webhooks Send 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.

Add a webhook
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"]}'
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.

Node.js
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))
  );
}
Python
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-Id to skip ones you've handled, and createdAt to 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.test event.

Errors

Errors answer with a status code and a JSON body that says what went wrong:

404
{
  "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-After header.
  • 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 pdfUrl instead of pdfBase64.

Questions? Email princh@boltpdf.com.