d2adoc2api

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.pdf

The response body is the completed application/pdf. Send "pdf": "none" or a webhook_url to get JSON instead.

Authentication

Each template has three keys.

KeySent asGrants
d2a_live_… fillX-Api-KeySecret, server-side. Fills via the API (/fill, /fill/batch); works on every endpoint.
d2a_pub_… publishableX-Api-Key / ?key=Browser-safe. The embed only (load form + submit). Rejected by the server /fill API.
d2a_admin_… adminX-Admin-KeyServer-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
}
FieldTypeDescription
dataobjectField name → value. Strings, booleans, numbers, or arrays (multi-select).
flattenbooleanFlatten (lock) the output. Default true.
signbooleanDigitally sign the final PDF (see Digital signatures).
webhook_urlstringIf 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.pdf

Digital 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>
FieldTypeDescription
templateIdstringThe template to embed. Required.
apiKeystringThe publishable key (d2a_pub_).
pdfstring"none" (values only), "base64" (return PDF), or "download".
profilestringAudience name — applies its field rules (hide/require/preset).
prefillobjectField → value to pre-populate (coerced to the field type).
signatureFieldsstring[]Fields to render a draw-to-sign pad for.
submitText / submitColorstringCustomize the submit button label & colour.
branding / brandingTextboolean / stringHide or relabel the footer (Pro & Business only).
onSubmit / onErrorfunctionCallbacks; 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:

FieldTypeDescription
400bad_requestMalformed body / missing fields.
401Invalid or missing key.
402template_limit / fill_limit / trial_limitPlan quota reached.
403origin_not_allowedPublishable key used from a disallowed origin.
404Template / profile not found.
413File or batch too large (25 MB / 200 rows).
415Unsupported upload format.
422Values or form logic rejected (see details[]).
429rate_limitedSlow 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.