API documentation
doc2api turns a PDF form into a JSON-fillable API. Upload a form once, then fill it (one at a time, in batches, or from a CSV), embed it, sign it, and receive results over signed webhooks. This is the complete REST + SDK reference.
Overview
Every request targets one template (an uploaded PDF) by id. The base URL is https://doc2api.co; endpoints live under /api/v1/templates/:id. Requests and responses are JSON unless the response is the finished PDF itself (application/pdf).
Create a template by uploading a PDF, image, or text file in the dashboard (or with no account). PDFs keep their AcroForm fields; images/text convert to a flat PDF you draw fields onto (manually or with AI detection). Each template gets three keys, and your org has one workspace key — see Authentication.
Quickstart
Fill a form and get the completed PDF back in one call:
curl -X POST 'https://doc2api.co/api/v1/templates/<TEMPLATE_ID>/fill' \
-H 'Content-Type: application/json' \
-H 'X-Api-Key: d2a_live_<your_fill_key>' \
-d '{ "data": { "full_name": "Ada Lovelace", "date": "2026-07-25" }, "flatten": true }' \
--output filled.pdfThe response body is the completed application/pdf. Send "pdf": "none" or a webhook_url to get JSON instead.
Authentication
Each template has three keys.
| Key | Sent as | Grants |
|---|---|---|
d2a_live_… fill | X-Api-Key | Secret, server-side. Fills via the API (/fill, /fill/batch); works on every endpoint. |
d2a_pub_… publishable | X-Api-Key / ?key= | Browser-safe. The embed only (load form + submit). Rejected by the server /fill API. |
d2a_admin_… admin | X-Admin-Key | Server-side. Manages audiences, logic, webhooks, and embed domains. |
Any key rotates independently from the template page; the old value stops working immediately.
Fill a PDF
POST /api/v1/templates/:id/fill — auth: fill key.
{
"data": { "field_name": "value", "consent": true },
"flatten": true, // lock the result (default true)
"sign": false, // embed a PKCS#7 digital signature
"webhook_url": "https://…" // optional: POST the result instead of returning it
}| Field | Type | Description |
|---|---|---|
| data | object | Field name → value. Strings, booleans, numbers, or arrays (multi-select). |
| flatten | boolean | Flatten (lock) the output. Default true. |
| sign | boolean | Digitally sign the final PDF (see Digital signatures). |
| webhook_url | string | If set, deliver the PDF as base64 JSON to this URL and return a summary. |
Returns the completed application/pdf (or a JSON delivery summary when webhooks are involved). Form logic and computed fields are enforced server-side. Unknown/invalid values return 422 with a details array.
Batch & CSV fill
POST /api/v1/templates/:id/fill/batch — auth: fill key. Fill up to 200 records in one call; each row is metered as one document.
JSON
{
"rows": [ { "name": "Ada" }, { "name": "Alan" } ],
"flatten": true,
"sign": false,
"format": "json" // "json" (default) or "combined"
}json → { count, succeeded, failed, results: [ { index, filename, pdf_base64, warnings } | { index, error } ] } — per-row errors are isolated. combined → a single application/pdfwith every row's pages concatenated (headers X-Doc2api-Batch-Count / X-Doc2api-Batch-Errors).
CSV
POST a CSV body with Content-Type: text/csv — the header row names the fields. Options come from query params.
curl -X POST 'https://doc2api.co/api/v1/templates/<ID>/fill/batch?format=combined&sign=true' \
-H 'X-Api-Key: d2a_live_…' -H 'Content-Type: text/csv' \
--data-binary $'name,date\nAda,2026-01-01\nAlan,2026-06-02' --output out.pdfDigital signatures
Add "sign": true to /fill, /submissions, or /fill/batch to embed a PKCS#7 (PAdES-style) signature in the document. Any change after signing invalidates it, and PDF viewers show a signature. Signing runs last, over the final bytes.
Signing uses a single platform certificate — doc2api never stores your private keys. Set SIGNING_P12_BASE64(+ passphrase) in production for a CA-chained cert; otherwise an ephemeral self-signed cert is used (valid & tamper-evident).
Embed SDK
Drop the SDK in and users fill the real PDF in their browser — signatures included. Uses the publishable key.
<div id="pdf-form"></div>
<script src="https://doc2api.co/sdk/v1.js"></script>
<script>
Doc2api.render({
container: "#pdf-form",
templateId: "<TEMPLATE_ID>",
apiKey: "d2a_pub_<publishable_key>",
pdf: "base64", // "none" | "base64" | "download"
profile: "patient", // apply this audience's rules
prefill: { patient_name: "Ada" }, // pre-populate fields
signatureFields: ["signature"], // draw-to-sign these fields
submitText: "Sign & submit", // submit button label
submitColor: "#16a34a", // submit button colour
branding: false, // hide "Powered by doc2api" (Pro/Business)
brandingText: "Powered by Acme", // custom footer text (Pro/Business)
onSubmit: function (result) {
// result.data = filled values (JSON); result.pdfBase64 = completed PDF
},
onError: function (err) { console.error(err); },
});
</script>| Field | Type | Description |
|---|---|---|
| templateId | string | The template to embed. Required. |
| apiKey | string | The publishable key (d2a_pub_). |
| string | "none" (values only), "base64" (return PDF), or "download". | |
| profile | string | Audience name — applies its field rules (hide/require/preset). |
| prefill | object | Field → value to pre-populate (coerced to the field type). |
| signatureFields | string[] | Fields to render a draw-to-sign pad for. |
| submitText / submitColor | string | Customize the submit button label & colour. |
| branding / brandingText | boolean / string | Hide or relabel the footer (Pro & Business only). |
| onSubmit / onError | function | Callbacks; onSubmit gets { data, warnings, pdfBase64 }. |
Hosted link (no code): open https://doc2api.co/embed/:id?key=<pub>&pdf=download directly, adding &profile= to apply an audience.
Submissions endpoint
POST /api/v1/templates/:id/submissions — auth: fill or publishable key. This is what the embed calls: it accepts field values (any value may be a PNG/JPEG data URL, stamped as a drawn signature) and returns the values plus the completed PDF. Every submission is recorded in your history.
{
"data": { "full_name": "Ada", "signature": "data:image/png;base64,…" },
"profile": "patient", // enforce this audience's rules
"pdf": "base64", // "base64" | "none"
"flatten": true,
"sign": false
}Audiences & field rules
Split one form between audiences — hide the practice-only section from patients, make consent compulsory, or preset values. PUT /api/v1/templates/:id/profiles — auth: admin key. Rules are enforced by the API on every submission (restricted fields rejected, required fields checked, presets applied).
{
"profiles": [
{
"name": "patient",
"hidden": ["practitioner_notes"],
"readonly": ["practice_name"],
"required": ["consent"],
"presets": { "practice_name": "Acme Dental" }
}
]
}Apply one with "profile": "patient" on a submission, or ?profile=patient on the embed / hosted link.
Form logic
Dependent fields and computed values. PUT /api/v1/templates/:id/logic — auth: admin key. Evaluated live in the embed and enforced by the API, so computed fields can't be spoofed.
{
"logic": [
{ "type": "show", "target": "spouse_name", "when": "married == true" },
{ "type": "require", "target": "reason", "when": "amount > 1000" },
{ "type": "compute", "target": "line_total", "expr": "quantity * unit_price" }
]
}Expressions support + - * /, comparisons, and/or/not, and {field} references — evaluated by a safe engine (no eval).
Webhooks
Deliver every completed PDF and its values to one or more of your servers. PUT /api/v1/templates/:id/webhooks — auth: admin key. Each endpoint gets a signing secret and subscribes to fill and/or submission events. Failed deliveries are retried with exponential backoff(1m → 5m → 30m → 2h → 6h); see the delivery log on the template page.
{
"webhooks": [
{ "name": "EHR", "url": "https://ehr.example.com/hook",
"events": ["submission"], "enabled": true }
]
}Verifying the signature
Each delivery carries X-Doc2api-Event, X-Doc2api-Timestamp, and X-Doc2api-Signature: sha256=…. The signed string is timestamp + "." + rawBody:
import crypto from "node:crypto";
function verify(req, secret) {
const ts = req.headers["x-doc2api-timestamp"];
const sig = req.headers["x-doc2api-signature"]; // "sha256=<hex>"
const expected =
"sha256=" + crypto.createHmac("sha256", secret)
.update(ts + "." + req.rawBody).digest("hex");
return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}Embed domains
Lock the embed to your own sites. PUT /api/v1/templates/:id/origins — auth: admin key.
{ "allowed_origins": ["https://app.yourco.com"] }When set, only those origins may iframe the form (a frame-ancestors policy) and the publishable key is rejected (403 origin_not_allowed) from any other origin. Empty = any site. The fill key is exempt (server-to-server).
Errors, limits & quotas
Errors return { "error": "message", "code"?: "…" } with a standard status:
| Field | Type | Description |
|---|---|---|
| 400 | bad_request | Malformed body / missing fields. |
| 401 | — | Invalid or missing key. |
| 402 | template_limit / fill_limit / trial_limit | Plan quota reached. |
| 403 | origin_not_allowed | Publishable key used from a disallowed origin. |
| 404 | — | Template / profile not found. |
| 413 | — | File or batch too large (25 MB / 200 rows). |
| 415 | — | Unsupported upload format. |
| 422 | — | Values or form logic rejected (see details[]). |
| 429 | rate_limited | Slow down (Retry-After + X-RateLimit-* headers). |
Rate limits
Per template unless noted: fill 60/min, submissions 60/min, batch 6/min, uploads 20/min per org. Public demo 12/min per IP; auth endpoints are throttled too.
Plan quotas
Free 3 templates / 50 docs/mo · Starter 15 / 1,000 · Pro ∞ / 10,000 · Business ∞ / 100,000. Retention and white-label branding scale with the plan — see pricing.
Ready to build?
Create a free account →