Skip to content

developers

Documents, from one API call.

Send JSON to a published template and get the finished PDF back, or a signed webhook when it is ready. The same templates your team shares as links and embeds, with scoped keys, safe retries and a Postman collection.
POST /api/v1/templates/coi_v3/render
curl -X POST "https://api.mezdoc.com/api/v1/templates/coi_v3/render?return=url" \
  -H "Authorization: Bearer $MEZDOC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "data": {
      "insured_name": "Harbor Point LLC",
      "each_occurrence": 1000000,
      "effective_date": "2026-06-01"
    },
    "idempotency_key": "coi-2026-00481"
  }'

Quickstart

Four steps from sign-up to a PDF. Every example on this page uses https://api.mezdoc.com.

  1. 1Start freeCreate an account. No card needed. On the Free plan, PDFs from the API carry a watermark.
  2. 2Publish a templateBuild it in the editor and publish it. A key renders the version published to its environment.
  3. 3Create an API keyIn the dashboard, under API. The key is shown once and stored only as a hash. Keep it on your server.
  4. 4Send the dataPOST JSON to /api/v1/templates/:alias/render and get the PDF back.
Render a certificate
curl -X POST "https://api.mezdoc.com/api/v1/templates/coi_v3/render?return=url" \
  -H "Authorization: Bearer $MEZDOC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "data": {
      "insured_name": "Harbor Point LLC",
      "each_occurrence": 1000000,
      "effective_date": "2026-06-01"
    },
    "idempotency_key": "coi-2026-00481"
  }'

Authentication

Every request sends a secret key as a bearer token: Authorization: Bearer sk_live_.... Call the API from your backend, never from a browser or a mobile app.

The prefix names the environment

API key prefixes
EnvironmentKey prefixRenders
Productionsk_live_The version published to production.
Stagingsk_test_The version published to staging, with a STAGING watermark.
Developmentsk_dev_The version published to development, with a DEVELOPMENT watermark.

Every plan has production, staging and development environments. A key can be set to expire after 30 days, 90 days or a year, and you can revoke it at any time.

Scopes limit what a key can do

API key scopes
ScopeAllows
template:readList templates and read a template's input schema.
template:renderRender PDFs, synchronously and asynchronously.
submission:readList and read submissions, and download their PDFs.
workflow:runStart workflow runs, poll them and download their PDFs.

New keys get all four scopes. Clear the ones a key does not need: a request outside its scopes returns 403 forbidden.

Render a PDF

One endpoint renders every template, a fillable PDF or a smart document, from the same data object.

Render request body
FieldTypeNotes
dataobject, requiredValues keyed by the field names in the template's input schema, from GET /api/v1/templates/:alias.
idempotency_keystringA repeat with the same key and data returns the original result instead of rendering again.
callback_urlstringA one-off webhook for this render. It fires on completion, sync or async, and is not signed.

Send Content-Type: application/json. Bodies over 1 MB return 413.

Three ways to get the PDF back

Render modes
RequestResponseUse it when
POST .../render200 with the PDF bytes, plus X-Mezdoc-Submission-Id and X-Mezdoc-Render-Ms headers.You want the file in the response.
?return=url200 JSON: submission_id, pdf_url, page_count, render_ms.You store a link. pdf_url needs the same key to download.
?async=true202 JSON: submission_id, status, poll_url.Batches and long documents. Poll, or wait for the webhook.
Render in the background
curl -X POST "https://api.mezdoc.com/api/v1/templates/coi_v3/render?async=true" \
  -H "Authorization: Bearer $MEZDOC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "data": { "insured_name": "Harbor Point LLC" } }'

The template version is fixed when the request arrives, so a publish between the request and the render never changes the output. Poll poll_url until status is completed, when the body carries pdf_url, or wait for the submission.completed webhook.

Shaping the data object

Field types and the JSON values they take
Field type in the schemaSend
text, email, phone, url, name, address, ssn, ein, vin, iban, swiftA string.
number, numeric, currency, percentage, calculatedA number.
boolean, checkboxtrue or false.
date, date-dropdownsA string in YYYY-MM-DD, such as 2026-06-01.
dropdown, radio-groupThe chosen option's value.
multi-selectAn array of option values.
image, signatureA public image URL or a data: URL.
object, json-objectAn object keyed by the field's sub-field keys.

A field with is_array: true takes an array of those values, which is how repeating table rows arrive. A missing or wrong-typed required field returns 400 data_invalid with an errors array naming the field.

Endpoints

Nine endpoints, all under /api/v1. Keys and webhooks are managed in the dashboard.

API endpoints
EndpointScopeReturns
GET /api/v1/templatestemplate:readYour templates: alias, name, kind, status and live version.
GET /api/v1/templates/:aliastemplate:readOne template and its input schema: the data keys to send.
POST /api/v1/templates/:alias/rendertemplate:renderThe PDF, JSON with ?return=url, or 202 with ?async=true.
GET /api/v1/submissionssubmission:readSubmissions, newest first. Filter by status, environment or template; page with created_before.
GET /api/v1/submissions/:idsubmission:readOne submission's status and metadata. This is the async poll URL.
GET /api/v1/submissions/:id/pdfsubmission:readThe PDF bytes.
POST /api/v1/workflows/:alias/runsworkflow:runRuns a workflow: the merged packet and each document, or 202 with ?async=true.
GET /api/v1/workflows/runs/:runIdworkflow:runOne run's status. This is the async poll URL for runs.
GET /api/v1/workflows/runs/:runId/pdfworkflow:runThe merged packet, one document with ?doc=0, or everything as a zip with ?doc=zip.

pdf_url and poll_url values point at these endpoints, never at raw storage, and need the same key. Opening one in a browser returns 401, which is expected.

Workflow runs

A workflow puts several templates behind one set of answers. One call fills every document that applies and returns the packet.

Run a workflow
curl -X POST "https://api.mezdoc.com/api/v1/workflows/renewal_packet/runs" \
  -H "Authorization: Bearer $MEZDOC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "data": { "insured_name": "Harbor Point LLC", "state": "TX" } }'

The body is { data }, keyed by the workflow's field aliases. Add ?async=true to get 202 with a poll_url and render in the background, then poll the run or wait for workflow.run.completed.

For a worked insurance example, see the quote-to-binder document workflow for US MGAs.

Safe retries

Put an idempotency_key in the render body and a retry can never render twice.

  • A repeat with the same key and the same data returns the original submission: the same PDF or JSON in sync mode, the same submission_id and its current status in async mode.
  • Keys are unique per template within your organization, so reuse your own order or policy id.
  • The same key with different data returns 409 idempotency_conflict. So does a sync repeat while the first attempt is still processing or has failed.
  • A replay that returns an existing submission does not count against your plan.

Webhooks

Mezdoc POSTs a signed JSON event to your endpoint when a render or a workflow run finishes, so you never have to poll.

Set up an endpoint

In the dashboard, under API, add an endpoint: an https URL we can reach, the environment it listens to, the events it wants and the sources that fire it. Private, loopback and cloud metadata addresses are refused and checked again at every delivery. The signing secret, whsec_..., is shown once.

Webhook events
EventFires when
submission.completedA render finished. The payload links the PDF.
submission.failedA render could not be produced. The payload carries the error.
workflow.run.completedEvery document in a run is done and the packet is ready.
workflow.run.failedA run could not be completed.
Which renders fire an endpoint
SourceFires by default
Async API renders (?async=true)Yes
Hosted forms and embedsYes
Sync API rendersNo. The PDF is already in the response; turn it on per endpoint.

What a delivery looks like

Webhook delivery headers
HeaderExample
Content-Typeapplication/json
User-AgentMezdoc-Webhooks/1
Mezdoc-Event-Idevt_3f9a1c2b...
Mezdoc-Event-Typesubmission.completed
Mezdoc-Signaturet=1790519400,v1=5f8b...
submission.completed
{
  "id": "evt_3f9a1c2b7d8e4f60a1b2c3d4e5f60718",
  "type": "submission.completed",
  "created": 1790519400,
  "data": {
    "submission_id": "sub_8Kq2mR4tVx",
    "template_id": "utpl_2Hc7Qw",
    "kind": "dynamic",
    "environment": "production",
    "status": "completed",
    "page_count": 3,
    "sha256": "ee1922af94c5...",
    "error": null,
    "pdf_url": "https://app.mezdoc.com/api/v1/submissions/sub_8Kq2mR4tVx/pdf",
    "created_at": "2026-09-27T14:30:00.000Z",
    "completed_at": "2026-09-27T14:30:02.140Z"
  }
}

pdf_url is an authenticated API link: download it with your key. A webhook never carries the PDF itself. Workflow events carry run_id and workflow_id in place of submission_id and template_id.

Verify the signature

Mezdoc-Signature is t=<unix seconds>,v1=<hex>. Compute HMAC-SHA256 of <t>.<raw body> with your secret, compare it with v1 in constant time, and reject a t more than 300 seconds from now.

Verify a delivery
// Express. Hash the raw body, before any JSON parsing.
const crypto = require("crypto");

function verifyMezdoc(secret, rawBody, header, toleranceSec = 300) {
  const parts = {};
  for (const seg of header.split(",")) {
    const i = seg.indexOf("=");
    if (i > 0) parts[seg.slice(0, i).trim()] = seg.slice(i + 1).trim();
  }
  const t = Number(parts.t);
  if (!t || !parts.v1) return false;
  if (Math.abs(Math.floor(Date.now() / 1000) - t) > toleranceSec) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(parts.v1);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

app.post("/webhooks/mezdoc", express.raw({ type: "application/json" }), (req, res) => {
  const ok = verifyMezdoc(process.env.MEZDOC_WEBHOOK_SECRET, req.body.toString(), req.get("Mezdoc-Signature") || "");
  if (!ok) return res.status(400).send("bad signature");
  res.status(200).send("ok"); // respond fast, then process the event
});

Delivery rules

  • Answer with a 2xx within 10 seconds, then do the work. Redirects are not followed.
  • Anything else is retried up to 5 times with growing backoff. Every attempt shows in the dashboard delivery log.
  • Delivery is at least once: dedupe on Mezdoc-Event-Id. Order is not guaranteed, so use created and your own state.
  • A callback_url on a single render always fires for that render but is not signed: confirm it by fetching the submission with your key.

Errors

Every error uses one envelope: a code you can switch on and a message you can show.

Error body
# 400 Bad Request
{
  "code": "data_invalid",
  "message": "Field \"effective_date\" is required.",
  "errors": [{ "alias": "effective_date", "message": "Field \"effective_date\" is required." }]
}
Error codes
Status and codeWhen
400 bad_requestThe body is not valid JSON, or a query parameter is out of range.
400 data_invaliddata does not match the template's schema. errors names each field.
401 unauthorizedThe key is missing, malformed, revoked or expired.
403 forbiddenThe key lacks the scope this endpoint needs.
404 not_foundNo template, submission or run with that alias or id.
409 not_publishedNothing is published to the key's environment.
409 template_invalidThe published version cannot be rendered.
409 idempotency_conflictThe idempotency_key was used with different data, or its first attempt is still processing or failed.
413 payload_too_largeThe body is over 1 MB.
429 rate_limitedYour workspace has reached its available monthly usage, so additional production is paused until the allowance resets or the plan changes. The body includes limit, used, resets_at and upgrade_url, and Retry-After gives the seconds until the reset.
500 render_failedThe renderer failed. A failed submission is recorded for the audit log.
500 internalSomething unexpected went wrong on our side.

Limits

API limits
LimitValue
Request body1 MB
Submissions per list page50 by default, up to 100 with ?limit=
Webhook response time10 seconds
Webhook retriesUp to 5, with backoff
Signature tolerance300 seconds

Try every endpoint in Postman.

The collection has all nine requests with sample bodies. Set token to your API key and press Send.

Download the collection

Questions from developers.

How do I authenticate with the Mezdoc API?

Send your secret key as a bearer token: Authorization: Bearer sk_live_... Keys are created in the dashboard, shown once and stored only as a hash. The prefix names the environment the key renders from, and each key carries scopes that limit what it can do.

Should I render synchronously or asynchronously?

A plain render call waits and returns the PDF, or JSON with a link to it when you add ?return=url. Add ?async=true to get a 202 with a poll_url straight away, then poll it or wait for the submission.completed webhook. The template version is fixed when the request arrives either way.

How do I make retries safe?

Put an idempotency_key in the render body. A repeat with the same key and the same data returns the original submission instead of rendering again, and does not count against your plan. The same key with different data returns 409 idempotency_conflict.

How do I check that a webhook came from Mezdoc?

Each delivery to an endpoint you configured carries a Mezdoc-Signature header with a timestamp and an HMAC-SHA256 of the timestamp and the raw body, keyed with your whsec_ secret. Recompute it, compare in constant time, reject timestamps older than five minutes, and dedupe on Mezdoc-Event-Id.

Send your first render.

Start free, publish a template, create a key and POST your data. The finished PDF comes back in the same response.

Free plan, no card