Skip to main content

Typeform

Prerequisites

  • A PolyDoc account and API key. Sign up for free, no credit card required; the free plan includes 150 PDF conversions per month.
  • A PolyDoc template. Note its short ID for the source field (e.g. [template:jlE-whg]).
  • A Typeform account with a form, plus a place to receive a webhook (a serverless function or a route in your backend) for Use Case 1.
  • A Typeform personal access token for Use Case 2.

Use Case 1: Generate a PDF on form submission

Turn a Typeform submission into a finished PDF the moment someone hits Submit. The example is an order / quote request form, mapped onto a PolyDoc invoice template, but the same answer mapping works for any document: registration confirmations, applications, contracts, certificates.
Typeform webhook (form_response) → your handler → PolyDoc API → email / upload

Add the webhook with a signing secret

Build your form (here, a quote request that collects the customer's name, email, company, billing address, the service they want, and a quantity). Then open the Connect tab, Webhooks, and Add a webhook.

Point the Endpoint at your handler URL. Open the saved webhook's Edit view and set a Secret: Typeform uses it to sign every delivery so your handler can prove a request really came from Typeform (Step 2). Toggle the webhook ON once your endpoint is live.

admin.typeform.com

Verify the Typeform signature

Your handler receives the POST. Before trusting it, verify the Typeform-Signature header. Typeform sends sha256=<base64> where the value is an HMAC-SHA256 of the raw request body, keyed with your secret:

import crypto from "node:crypto";

function isValidSignature(header, rawBody, secret) {
// header looks like "sha256=Base64Hmac..."
const expected =
"sha256=" +
crypto.createHmac("sha256", secret).update(rawBody).digest("base64");
// constant-time compare to avoid leaking timing information
return (
header &&
header.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(header), Buffer.from(expected))
);
}

// rawBody MUST be the raw request bytes, not the re-serialized JSON.
if (!isValidSignature(request.headers["typeform-signature"], rawBody, process.env.TYPEFORM_SECRET)) {
return response.status(401).send("bad signature");
}

Map the answers

The body is a form_response event. Each entry in answers[] carries its question's field.ref (a stable identifier) and a value under a type-specific key (text, email, number, choice.label, ...). Index the answers by ref so the mapping doesn't depend on question order:

const fr = body.form_response;

// answers[] -> { ref: value } using the type-specific key of each answer
const valueOf = (a) =>
a.text ?? a.email ?? a.number ?? a.boolean ?? a.choice?.label ?? a.date ?? a.phone_number;
const byRef = Object.fromEntries(fr.answers.map((a) => [a.field.ref, valueOf(a)]));

// fr.definition.fields[] lists every field.ref + title, so you can read the
// refs your form uses (or set meaningful ones via the Create API).

Map each ref to a PolyDoc templateData field. A quote form captures what the customer wants; your handler knows the prices, so it turns the chosen service plus quantity into a line item:

Typeform field.refPolyDoc templateDataTransform
full_namecustomer_namedirect
emailcustomer_emaildirect
company_namecustomer_companydirect
billing_streetcustomer_streetdirect
citycustomer_citydirect
countrycustomer_countrydirect
service + unitsitems[0]price from your own rate card

Call the PolyDoc API

Build the request body and POST it to PolyDoc. source renders the visual invoice from your template; templateData fills it in:

// Your own rate card - the form asks what they want, you set the price.
const RATE = { "Website Design": 1200, "Logo Design": 300, "SEO Optimization": 100 };

const service = byRef.service;
const units = Number(byRef.units) || 1;

const body = {
source: "[template:jlE-whg]", // your PolyDoc invoice template short ID
templateData: {
invoice_number: `Q-${fr.token.slice(0, 8).toUpperCase()}`,
invoice_date: fr.submitted_at.slice(0, 10),
customer_name: byRef.full_name,
customer_email: byRef.email,
customer_company: byRef.company_name,
customer_street: byRef.billing_street,
customer_city: byRef.city,
customer_country: byRef.country,
items: [
{ quantity: units, name: service, description: byRef.notes ?? service, price: RATE[service] },
],
},
};

const res = await fetch("https://api.polydoc.tech/pdf/convert", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
"X-Sandbox": "true", // drop in production
},
body: JSON.stringify(body),
});
if (!res.ok) throw new Error(`PolyDoc ${res.status}: ${await res.text()}`);
const pdf = Buffer.from(await res.arrayBuffer());

Deliver the PDF

Deliver the PDF. Two common options:

a) Email it to the customer (and yourself) by attaching the PDF buffer from Step 4 to a transactional email (SES, Resend, Postmark, ...). customer_email from the form is the recipient.

b) Store it by passing cloudStorage.presignedUrl in the request body instead. PolyDoc PUTs the PDF straight to your bucket (S3, GCS, R2, Azure Blob) and the response is just a status, so the file never flows back through your function. See the cloud storage guide for generating presigned URLs per provider.

Use Case 2: Bulk PDFs from the Responses API

Back-fill PDFs for responses you already collected, or run a scheduled job that sweeps new ones, by reading them from the Typeform Responses API instead of waiting for a webhook. Each PDF is written straight to your own bucket.
GET /forms/{id}/responses → PolyDoc API (+ cloudStorage) → PDF in your bucket

Create a token and list responses

In Typeform, open your avatar, Account settings, Personal tokens, and Generate a new token. Read-only archiving only needs the responses:read and forms:read scopes (pick Custom scopes), so prefer those over full access. Copy the token once: Typeform shows it a single time.

admin.typeform.com

List the responses for your form:

curl 'https://api.typeform.com/forms/<FORM_ID>/responses?page_size=200&completed=true' \
-H 'Authorization: Bearer <YOUR_PERSONAL_TOKEN>'

Generate and store each PDF

For each response, build the body exactly as in Use Case 1 (Step 3 mapping, Step 4 body), add a cloudStorage.presignedUrl you generate for your bucket, and POST to PolyDoc in a loop:

for (const item of responses.items) {
const byRef = indexAnswers(item.answers); // Step 3 helper
const body = mapToPolydoc(byRef, item); // Step 3 + 4 mapping
body.cloudStorage = {
presignedUrl: await presignPut(`quotes/${item.token}.pdf`),
};

await fetch("https://api.polydoc.tech/pdf/convert", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
}

See the cloud storage guides for generating presigned URLs per provider.

Index the archive

Keep a small index so you can find any generated PDF later. Map the Typeform response token to its archive location and when it was submitted:

response_tokenarchive_urlsubmitted_at
a1b2c3d4e5s3://quotes/a1b2c3d4e5.pdf2026-01-15
f6g7h8i9j0s3://quotes/f6g7h8i9j0.pdf2026-01-18