Form import API

Add high-fidelity document import to your form builder.

Let your customers import the forms they already have: intake PDFs, consent Word docs, a page on their old site. Pufflet turns them into structured JSON your form editor can use.

Forms as written, not rewritten.

Convert one of your forms

Drop a file here, or

PDF, DOCX, HTML, Markdown, text · 25 MB

No form handy? Try ours on the live API.

What high fidelity means here

Source-faithful conversion

Original wording and structure are preserved. Uncertain or unsupported elements are surfaced for review, not silently omitted.

Conditional logic preserved

Show-when rules, Other follow-ups, required flags and page breaks return as structure, not sentences.

Builder-neutral JSON

Declare what your editor supports; anything outside it is downgraded on purpose and flagged. Three mapping tables turn the JSON into your editor's element types.

Four inputs, one output

PDF, DOCX, HTML and plain text, by upload or public URL.

What comes back

One JSON document per import: the form itself, a review note for every judgement call, a verification verdict, and what the source turned out to be. Here is a piece of one.

{
  "form": {
    "version": "1",
    "title": "Patient intake",
    "sections": [
      { "id": "s2", "heading": "Insurance", "description": null, "items": [
        { "id": "q7", "kind": "single_choice",
          "label": "Do you have coverage through an employer?",
          "description": null, "required": true, "show_when": null,
          "choices": ["Yes", "No"], "other": null },
        { "id": "q8", "kind": "short_answer", "label": "Policy number",
          "description": null, "required": null, "format": null,
          "show_when": { "question": "q7", "operator": "equals", "value": "Yes" } },
        { "id": "q9", "kind": "short_answer", "label": "Attach a photo of your card",
          "description": null, "required": null, "format": null, "show_when": null }
      ] }
    ]
  },
  "review_notes": [
    { "code": "kind_downgraded", "item_id": "q9",
      "message": "The original was a file upload. This editor does not support uploads, so it came through as a short answer." }
  ],
  "verification": { "status": "needs_review" },
  "source": { "format": "pdf", "page_count": 3, "char_count": 4812 }
}

Field names and structure are the same for every import, whatever the document was.

For developers

An integration is three things: the config you send with a request, a mapping from the JSON into your editor's own vocabulary, and the review notes you show your users. Here's what our editor uses.

How this builder reads the JSON

Our form builder has its own vocabulary, like yours. A small mapping carries the imported JSON into it. This is the mapping it runs on.

In the JSONIn this builderNotes
form.titleText · Title, first element of page 1Editable and removable like any element.
form.descriptionText · Body, after the title
sectionsA page break before every section after the firstThe preview shows Page x of y.
section.heading / section.descriptionText · Title / Body at the top of that page
headingText · Subtitle
explanationText · Body
short_answerQuestion · Textformat hint (email / phone / web address) kept
long_answerQuestion · Textarea
single_choiceQuestion · Single selectExactly Yes / No with no Other option becomes Yes/No.
multi_choiceQuestion · Multi select
date / time / numberQuestion · Date / Time / Number
ratingQuestion · Rating scale (from, to, end labels)
matrixQuestion · Matrix / grid (rows, columns, one or many per row)
rankingQuestion · Ranking
signatureQuestion · Signature
file_uploadNever receivedNot in this builder's supports list, so the API downgrades it to a short_answer and adds a kind_downgraded note.
unknown_questionQuestion with no type chosenThe pipeline's note is shown; pick a type.
descriptionHelper textShown only when the question has one.
required (true / false / null)Required checkboxfalse and null both show unchecked; an untouched value writes back unchanged.
other { label, prompt, input }Include Other: the option's wording and its answer boxprompt (a caption for the box) is carried through; it is only set when a follow-up question was merged into the option, which this page's config does not ask for.
show_whenConditional rule: show when <question> is/includes <value>The reserved value "other" means the Other option.
idsKept as element idsReview notes point at elements by id.

Security

Converting a document means reading it, so the honest version of this page starts with where it goes. Everything below describes the API this page is calling right now.

Where your document goes
Structuring the form is done by a large language model. We use Google Gemini, OpenAI and Anthropic, tried in that order, falling back only when one is unreachable or returns something unusable. The request carries the text of your document, our instructions and the output schema — and nothing else. No account id, no key, no file name, no user data goes with it.
What we store
Not your document. The file, the text extracted from it and the JSON we return are held in memory for the length of the request; nothing is written to a database, to object storage or to disk, and the container runs with no volumes attached. What we do keep is one metadata record per import: counts of bytes, characters, pages and questions, the review-note codes without their text, which model ran, token counts, duration and cost.
No way to fetch it back
There is no endpoint that returns a past import. The result is sent once, in the response to your own request. This page holds it in the browser tab's memory and nowhere else — not local storage, not session storage — so closing the tab is the end of it.
Keys
An API key is 32 random characters from a cryptographic generator. We store only its SHA-256 hash, never the key, and show it once when it is created. The database is closed to browser access entirely; only the server can read it. Keys carry a quota and can be revoked.
Importing from a URL
A URL import is restricted to http and https on publicly routable addresses. Loopback, private ranges, link-local and cloud metadata addresses are refused, as are internal hostnames and URLs carrying credentials. Redirects are followed at most three times and every hop is checked again. The fetch sends no credentials of any kind, times out at 20 seconds and stops at 20 MB.
Limits and access
Browser access to the API is an explicit origin allowlist, with no wildcard. Requests are rate limited per key and per source address. Uploads stop at 25 MB. Errors come back as plain messages, never stack traces.

Reviewing us on behalf of a customer? Send the questionnaire to the address below and you will get a straight answer, including to anything this page does not cover.

Starts at US$49/month

Includes 50 conversion credits. Additional credits from US$1. Volume terms for platform integrations.

Talk to a human
Danielle · Founder