We are in official beta.

Build and publish a DocWebForm with the API

From an API key to a published form that sends each submission where you chose, one request at a time. Written so a developer, or an AI assistant working for one, can follow it end to end and know at every step which key the request needs.

Read this first: what an API key reaches today.
  • Your API key does steps 1, 2 and 9. Step 1 checks the key. Steps 2 and 9 need no key at all: the connection types are public, and so is a published form.
  • Steps 3 to 8, and bundle export and import, need the management key. They are the routes under /api/manage/, which take the management key the portal holds. A customer API key is refused on them with a 401: { "success": false, "error": "UNAUTHORIZED", "message": "Invalid admin API key", "code": "UNAUTHORIZED" }. A developer or business account key is still a customer key; none reaches them.
  • So today those steps run in the portal. The setup wizard sends exactly the requests below for you. They are documented in full anyway: they are what the wizard sends, and what your key will send if these routes open to it. Whether they will is an open product decision, not settled yet.
  • No route hands back a delivered submission, with either key. A submission is sent to its destination and not kept here once delivered, so the destination is the record. One that could not be delivered is held in your forms.
You are on production. Every request on this page goes to https://api.docbutterfly.com, and a key made on this site works there. A key made on one of our development or test sites does not: it answers 401 here.

The parts, and the words for them

WordWhat it is
Template The form's fields and their rules (the contract: each field's name, label, type, choices and whether it is required), plus a document body with a placeholder per field. One template can back many forms. The contract, key by key.
Form One published use of a template: a name, a destination, and its own public address. Retiring a form keeps its address reserved, so it is never given to anyone else.
Destination Where each submission goes: your email, a SharePoint library, or a workflow that writes to a database, Dataverse or any other connection.
Connection A saved, named link to a system you own. Its secret is never returned and never shown to a visitor.
Workflow The steps that run on each submission when the destination is more than email or a library.
Bundle (package) A file that carries a form and everything it needs to be rebuilt in another account. What is in one.

The steps, with every request

Every request goes to https://api.docbutterfly.com. The ones that take a key send it in the X-API-Key header, and a request with a body sends it as JSON (Content-Type: application/json). Why that host and no other.

#StepRequestKey
1Check your key, learn your client idGET /api/accountYour API key
2Choose where answers goGET /api/connection-typesNone
3Make the connection (skip for email)POST /api/manage/connections/{clientId}Management (the wizard)
4Read an HTML page into fieldsPOST /api/manage/form-import/{clientId}Management (the wizard)
5Save the templatePOST /api/manage/templates/{clientId}/composeManagement (the wizard)
6Ask what publishing costsGET /api/manage/form-publish-quote/{clientId}Management (the wizard)
7Publish the formPOST /api/manage/forms/{clientId}Management (the wizard)
8Rehearse a submission, unbilledPOST /api/manage/forms/{clientId}/{instanceId}/testManagement (the portal)
9Open and submit the live formGET then POST /api/f/{publicId}None: it is public

1. Check your key

Get a key from Portal → API key on this site (a developer or business account has one; which accounts do). This call is free and tells you whether the key works and which client id it belongs to. Every management route below names that id.

curl -s "https://api.docbutterfly.com/api/account" -H "X-API-Key: $DOCBUTTERFLY_API_KEY"

200 { "authenticated": true, "clientId": "...", "email": "...", "isActive": true,
      "tokenBalance": ..., "unlimited": false, "tier": "..." }

401 { "authenticated": false, "error": "UNAUTHORIZED", "message": "Invalid API key" }

A 401 here means this host does not know the key: it is mistyped, it was replaced by a regenerate, or it was made on a different environment. A key works only on the environment whose site made it; the box above says which one you are reading.

2. Choose where the answers go

The simplest destination is email: every submission is sent to the address on your account, or to one you name in step 7, and there is nothing to connect. For anything else, read the connection types (public, no key) and pick one; Connections explains each.

curl -s "https://api.docbutterfly.com/api/connection-types"

3. Make the connection

Skip this for email. Otherwise it is the one connection request. A drive (OneDrive, Google Drive, Box) is connected by signing in, so it is always made in Portal → Connections. A form can deliver straight to a SharePoint library; a database or Dataverse table is reached through a workflow the wizard creates with the form.

4. Read an HTML page into fields

Send the page itself or its address, exactly one of the two:

Body fieldWhat it takes
htmlThe page's markup, as one JSON string. Up to 1 MB of page; the whole request body may be up to 2 MB.
urlAn http or https address we read for you. An internal or numbered address, a page that is not HTML, one too large or one too slow is refused with a 400 that says which.
baseUrlOptional, with html only. The address the pasted page lives at, so its relative stylesheet links (/css/site.css) can be read for the form's look. The sheets are fetched with the same checks as url. A page's own <base href> is honored as well.
mode"preview" (the default) or "draft".
expectSourceHashOptional. The preview's sourceHash, sent back with the draft; a page whose markup changed between the two reads is refused with PAGE_CHANGED. A stylesheet that reads differently the second time does not refuse the draft: it is saved with the look as it reads then, and the answer says so with lookReread: true and a warning.
formIndex or formIdOptional. Which <form> to read on a page with several: its position counting from 0, or its id or name. The first form by default.

preview reads the page and writes nothing: the fields it found (up to 300), what it left behind and why (dropped), and warnings. Read the dropped list; anything the import could not carry over is named there. draft reads it again and adds the contract and a drafted documentHtml, which step 5 saves as they are.

POST https://api.docbutterfly.com/api/manage/form-import/{clientId}
{ "mode": "preview",
  "html": "<form><label>Full name <input name=\"full_name\" required></label><label>Email <input type=\"email\" name=\"email\" required></label><label>Census file <input type=\"file\" name=\"census\" accept=\".xlsx,.csv\"></label></form>" }

200 { "success": true, "title": "...",
      "fields": [ { "name": "full_name", "label": "Full name", "type": "string", "required": true, "optionCount": 0 },
                  { "name": "email", "label": "Email", "type": "email", "required": true, "optionCount": 0 },
                  { "name": "census", "label": "Census file", "type": "file", "required": false, "optionCount": 0, "accept": [".xlsx", ".csv"] } ],
      "dropped": [], "warnings": [], "forms": [ ... ], "chosenIndex": 0, "fieldCount": 3,
      "caps": { "maxFields": 300, "maxHtmlBytes": 1048576 }, "sourceHash": "...", "styled": false,
      "stylesheets": { "linked": 0, "read": 0 } }

POST https://api.docbutterfly.com/api/manage/form-import/{clientId}
{ "mode": "draft", "html": "<the same markup>", "expectSourceHash": "<from the preview>" }

200 { ...the same..., "contract": { "fields": [ ... ], "tables": [ ... ] }, "documentHtml": "..." }

In a preview row, type is the field's dataType and required is the page's own required mark. A document upload's row also carries accept, the file types its upload takes on the published form. An upload that takes several files comes across as a repeating section, shown as a row with "type": "table" and a columnCount; rows come in the page's order, a section between the fields it sat between. A dropped entry names the control and why: { "name", "tag", "type", "reason", "detail" }. With url instead of html the answer also carries the fetchedUrl that was read. styled says whether the page's look (fonts, colors, spacing, its button wording) came across; when it did, the draft's contract carries it as design, a stylesheet written for the form's own markup with no remote address in it, and the published form wears it. stylesheets counts the linked sheets found and read. Starting from nothing instead? Skip this step and write the contract yourself in step 5.

5. Save the template

POST https://api.docbutterfly.com/api/manage/templates/{clientId}/compose
{ "name": "Intake", "html": "<documentHtml from step 4, or your own>", "contract": <contract from step 4, or your own> }

201 { "success": true, "templateId": "...", "bytes": ...,
      "inputContract": { ... }, "placeholders": { "used": [ ... ], "unknown": [ ... ], "unused": [ ... ] },
      "sanitized": [ ... ] }

name and html are required. html is the document body each submission fills: a {{field_name}} placeholder per field, and {{#table_name}} … {{/table_name}} around a repeating row. Scripts and event handlers are removed from it on save and listed in sanitized. placeholders reports fields the body does not place (unused) and placeholders that name no field (unknown). A contract the rules below refuse is a 400 naming the field, and nothing is saved.

The contract, key by key

contract is { "fields": [ ... ], "tables": [ ... ] }. Each entry in fields:

KeyRequired?What it takes
nameYesLetters, digits and underscores, starting with a letter or underscore; parts may be joined by dots (applicant.email). Up to 120 characters, unique in the contract. It is also the name the answer is submitted under and the placeholder in the body.
labelNoWhat the visitor reads. Made from the name when absent.
dataTypeNo, defaults to stringOne of the types in the next table. Any other value is refused.
optionsFor choice, yesThe choices, as [ { "value": "basic", "label": "Basic" } ]. A { "basic": "Basic" } map or a plain list [ "Basic", "Plus" ] is accepted too. Up to 250. The visitor picks a label; the submission carries its value.
inputNoThe form's rules for the field, each optional: required (true: the visitor must answer), maxLength, minLength, min and max (a number, or a date such as "2026-08-11"), step, pattern (a regular expression the whole answer must match), placeholder, help (a line under the control), rows (for multiline, 1 to 40), section (a heading shown above the field), and for file only accept (narrowed to the document types below). A rule that does not make sense is dropped rather than refused.
modeNo, defaults to dynamicdynamic (the visitor answers it) or static (a fixed value the form never asks for).
valueFor static, yesThe fixed value.

A top-level "required": true on a field is accepted but stored as false: it is not the form's rule. Make a field required with input.required, or list it in the form's requiredFields at step 7.

dataTypeWhat the visitor gets
stringA one-line text box (255 characters unless maxLength says otherwise).
multilineA text area (4000 characters, 4 rows unless set).
richtextA text area too; the form offers no formatting editor.
numberA number box, whole steps unless step says otherwise.
currencyA number box in steps of 0.01.
dateA date picker; the answer is YYYY-MM-DD.
datetimeA date and time picker.
booleanA checkbox; the answer is true or false.
emailAn email box, checked as an address.
phoneA phone box.
urlA web address box.
choiceA dropdown of its options. An answer that is not one of them is refused.
imageA picture upload: PNG, JPEG, GIF or WebP.
fileA document upload: a PDF, Excel (.xlsx, .xls), CSV or Word (.docx) file, up to 10 MB each, judged by its content rather than its name. One field holds one document; for several, use a repeating section with a file column.
tableAccepted, but on a field it is a plain text box. A repeating section goes in tables.

Each entry in tables is a repeating section: name (the same rules as a field), label, columns (up to 40, each with the same name, label, dataType and options keys as a field; a column with options and no type is a choice), and "addRows": true to let the visitor add rows. position is optional: how many of the form's fields come before the section, so it sits between them (the HTML import sets it to where the page had the section). Without one, the section comes after the last field.

A whole template, with a choice, a document upload and a repeating section:

POST https://api.docbutterfly.com/api/manage/templates/{clientId}/compose
{
  "name": "Intake",
  "html": "<h1>Intake</h1><p>{{full_name}}, {{email}}</p><p>Plan: {{plan}}</p><p>Census: {{census}}</p><ul>{{#dependents}}<li>{{dep_name}} ({{relationship}})</li>{{/dependents}}</ul>",
  "contract": {
    "fields": [
      { "name": "full_name", "label": "Full name", "dataType": "string", "input": { "required": true, "maxLength": 120 } },
      { "name": "email", "label": "Email", "dataType": "email", "input": { "required": true } },
      { "name": "plan", "label": "Plan", "dataType": "choice",
        "options": [ { "value": "basic", "label": "Basic" }, { "value": "plus", "label": "Plus" } ] },
      { "name": "census", "label": "Census file", "dataType": "file", "input": { "accept": ".xlsx,.csv" } }
    ],
    "tables": [
      { "name": "dependents", "label": "Dependents", "addRows": true,
        "columns": [ { "name": "dep_name", "label": "Name" },
                     { "name": "relationship", "label": "Relationship", "options": { "spouse": "Spouse", "child": "Child" } } ] }
    ]
  }
}

6. Ask what publishing costs

Publishing a form charges tokens once, for its public address. Ask first; the answer is for your own account, and asking is free. Add ?instanceId=<id> to price re-publishing one of your forms instead.

GET https://api.docbutterfly.com/api/manage/form-publish-quote/{clientId}

200 { "success": true,
      "publish": { "operation": "webform_publish", "scope": "account",
                   "tokens": <what publishing will charge this account>, "listTokens": <the catalog price>,
                   "billed": true, "once": true,
                   "summary": "...", "detail": "..." } }

billed is false and tokens is 0 on an account that is not charged. summary and detail are the sentences the portal shows. With instanceId, the answer is for that form ("scope": "instance") and adds instanceId, found and chargeBasis.

7. Publish the form

Name the template and the destination. destination is the whole delivery choice, in one of three shapes:

  • { "kind": "email" }: each submission is emailed to the address on your account.
  • { "kind": "email", "email": "intake@example.com" }: to an address you name. A free or trial account may name only its own address or one it has confirmed through a verification link; a paid account may name any. Anything else is refused with a 403 (RECIPIENT_NOT_ALLOWED_SANDBOX) that says how to confirm the address.
  • { "kind": "connection", "connectionType": "sharepoint", "connectionId": "conn_...", "sharepoint": { "libraryName": "Intake", "folderPath": "forms" } }: files each submission into a SharePoint library. A database or Dataverse connection is refused here (DESTINATION_NEEDS_WORKFLOW); it is reached through a workflow the wizard creates.
POST https://api.docbutterfly.com/api/manage/forms/{clientId}
{ "templateId": "...", "name": "Intake", "destination": { "kind": "email" }, "requiredFields": [ "full_name", "email" ] }

POST https://api.docbutterfly.com/api/manage/forms/{clientId}
{ "templateId": "...", "name": "Intake", "destination": { "kind": "email", "email": "intake@example.com" } }

200 { "success": true,
      "instance": { "instanceId": "...", "publicId": "...", "submitAction": "forward",
                    "destinationEmail": "intake@example.com", ... },
      "publicPath": "/api/f/{publicId}", "fieldCount": 4, "tokensCharged": ... }

templateId is required. requiredFields is optional: contract field names the visitor must answer, on top of any input.required; a name the template does not have is refused. A balance that cannot cover publishing is a 402 with required and available, and nothing is created. An optional X-DBF-Idempotency-Key header makes a retried publish return the first answer instead of publishing twice.

The raw delivery fields. Before destination, a form's delivery was set with the fields below, and the create route still takes them. A body that sets destination together with any of them is refused with a 400, because it would be unclear which one won.

Raw fieldWhat it sets
submitAction"forward" (email the answers), "document" (fill the template and email the document; bills each submission), "workflow", "sharepoint" or "azuredevops".
destinationEmailThe address for forward and document.
submitTargetThe workflow (its id or slug) that workflow runs.
autoCreateWorkflowtrue with workflow: create a new workflow with the form, instead of naming one.
notifyEmailWith autoCreateWorkflow only: also email each submission to this address.
destinationConnectionIdWith autoCreateWorkflow only: one of your connections (its id) that the new workflow is set up to write to. It must be yours, switched on, and able to receive answers; otherwise the create is refused before anything is published (DESTINATION_CONNECTION_UNVERIFIED).
sharepointThe library config for sharepoint.
azureDevOpsThe project and work item config for azuredevops.
starterA starter destination picked in the wizard.

8. Rehearse a submission

Runs the real destination with the form's own validation and charges nothing. A value that fails validation comes back as VALIDATION_FAILED with the reasons.

POST https://api.docbutterfly.com/api/manage/forms/{clientId}/{instanceId}/test
{ "data": { "full_name": "Test Person", "email": "test.person@example.com" } }

9. Open and submit the live form

The form lives at https://api.docbutterfly.com plus its publicPath. Share that address. Neither request takes a key. A visitor opens it in a browser and submits the page it serves, and that is all most forms need. To submit from code, do what the browser does:

  1. GET the form. The answer is an HTML page. Its <form> holds one control per field and two hidden inputs of ours, __dbfRenderToken and __dbfSubmitId. Copy both into your submission. (A form that only signed-in accounts may fill in adds a third, __dbfVisitorToken, after the sign-in.)
  2. POST the answers to the same address, within two hours of the GET (the token's lifetime). The body can be any of:
    • multipart/form-data: what the page sends when the form has an image or file field. A file is a file part under the field's name.
    • application/x-www-form-urlencoded: what the page sends otherwise.
    • application/json: one flat object of name and answer. A file is a data: URI string ("data:application/pdf;base64,...").
curl -s "https://api.docbutterfly.com/api/f/{publicId}"
# read __dbfRenderToken and __dbfSubmitId out of the page, then:

curl -s -X POST "https://api.docbutterfly.com/api/f/{publicId}" \
  -F "__dbfRenderToken=<from the page>" -F "__dbfSubmitId=<from the page>" \
  -F "full_name=Test Person" -F "email=test.person@example.com" -F "plan=plus" \
  -F "census=@census.csv;type=text/csv" \
  -F "dependents.0.dep_name=Sam" -F "dependents.0.relationship=child"
  • Names. Each answer goes under its field's name exactly (a dotted name is one key). A repeating-section cell is table.row.column, rows counted from 0: dependents.0.dep_name. A choice sends the option's value; a boolean sends true or false. A name the contract does not declare is dropped without an error.
  • Files. A file field takes a PDF, Excel, CSV or Word document up to 10 MB, judged by its bytes. An image field takes PNG, JPEG, GIF or WebP. The whole submission may be up to 512 KB, plus room for one full-size document per file field (three per file column in a repeating section); more than that is a 413.
  • The token. Without a valid __dbfRenderToken from a page the form served, the submission is refused with a 403. __dbfSubmitId makes a repeat safe: a second post with the same one returns the first answer instead of submitting twice. A browser posting from another site's page is refused too, unless the form was published to be embedded there.
  • Answers. Accepted is a 200 thank-you page (the form's success message), or a 303 to the form's redirect address when it has one. A missing required answer or a value that fails a rule is a 400 that serves the form again with each problem next to its field; an entirely blank submission is refused the same way. A form that has reached its daily limit answers 429 with Retry-After.

Submissions that could not be delivered are listed in your forms, where they can be sent again.

Bundles: what a package contains

A bundle (the file is a package) carries a form and everything it needs, so the same form can be rebuilt in another account or another environment:

  • The form: its name, its settings and its destination choice.
  • Its template: the contract (the fields) and the document body.
  • The workflow it feeds, when it has one, and every template that workflow uses.
  • The connections it needs, as requirements only. A package never carries a credential. On import each requirement is matched to a connection in the importing account, and a dry run shows any that are missing before anything is written. The account the import writes to is always the one importing, never one named in the file.
GET  https://api.docbutterfly.com/api/manage/packages/{clientId}/export?form={instanceId}
POST https://api.docbutterfly.com/api/manage/packages/{clientId}/import        { ..., "dryRun": true }

Both are management routes, like steps 3 to 8; in the portal, Import a bundle and each form's export are on your forms. The field-by-field meaning of the package file is not published yet.

Is there an OpenAPI file for this?

Not for these routes. The OpenAPI (Swagger) definition describes the billed document endpoints in the API Reference; the form routes on this page are not in it, so this page is their reference. The definition itself is a download from Portal → Connectors, which needs a sign-in (signing up is free); no address serves it to a key alone. With only a key, /Docs/ApiCatalog is the API Reference's endpoint list as JSON (not OpenAPI), open to anyone.

If you are an AI assistant following this page

  • Keep the API key in an environment variable. Never print it, and never put it in a URL.
  • Call the host this page shows, https://api.docbutterfly.com, and no other. A key belongs to the environment whose site made it.
  • Start with step 1. If it answers 401, stop: the key is wrong or was made on another environment, and nothing else will work.
  • A 401 on a /api/manage/ route with a working key is the limit described at the top of this page, not a mistake in your request. Say so, name the step, and hand that step to a person in the portal.
  • Use email as the destination unless you were told otherwise. It needs no connection.
  • Report what you did at each step, the status code you got, and the public address if you reached step 9.