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.
- 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.
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
| Word | What 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.
| # | Step | Request | Key |
|---|---|---|---|
| 1 | Check your key, learn your client id | GET /api/account | Your API key |
| 2 | Choose where answers go | GET /api/connection-types | None |
| 3 | Make the connection (skip for email) | POST /api/manage/connections/{clientId} | Management (the wizard) |
| 4 | Read an HTML page into fields | POST /api/manage/form-import/{clientId} | Management (the wizard) |
| 5 | Save the template | POST /api/manage/templates/{clientId}/compose | Management (the wizard) |
| 6 | Ask what publishing costs | GET /api/manage/form-publish-quote/{clientId} | Management (the wizard) |
| 7 | Publish the form | POST /api/manage/forms/{clientId} | Management (the wizard) |
| 8 | Rehearse a submission, unbilled | POST /api/manage/forms/{clientId}/{instanceId}/test | Management (the portal) |
| 9 | Open and submit the live form | GET 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 field | What it takes |
|---|---|
html | The page's markup, as one JSON string. Up to 1 MB of page; the whole request body may be up to 2 MB. |
url | An 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. |
baseUrl | Optional, 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". |
expectSourceHash | Optional. 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 formId | Optional. 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:
| Key | Required? | What it takes |
|---|---|---|
name | Yes | Letters, 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. |
label | No | What the visitor reads. Made from the name when absent. |
dataType | No, defaults to string | One of the types in the next table. Any other value is refused. |
options | For choice, yes | The 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. |
input | No | The 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. |
mode | No, defaults to dynamic | dynamic (the visitor answers it) or static (a fixed value the form never asks for). |
value | For static, yes | The 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.
dataType | What the visitor gets |
|---|---|
string | A one-line text box (255 characters unless maxLength says otherwise). |
multiline | A text area (4000 characters, 4 rows unless set). |
richtext | A text area too; the form offers no formatting editor. |
number | A number box, whole steps unless step says otherwise. |
currency | A number box in steps of 0.01. |
date | A date picker; the answer is YYYY-MM-DD. |
datetime | A date and time picker. |
boolean | A checkbox; the answer is true or false. |
email | An email box, checked as an address. |
phone | A phone box. |
url | A web address box. |
choice | A dropdown of its options. An answer that is not one of them is refused. |
image | A picture upload: PNG, JPEG, GIF or WebP. |
file | A 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. |
table | Accepted, 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 field | What it sets |
|---|---|
submitAction | "forward" (email the answers), "document" (fill the template and email the document; bills each submission), "workflow", "sharepoint" or "azuredevops". |
destinationEmail | The address for forward and document. |
submitTarget | The workflow (its id or slug) that workflow runs. |
autoCreateWorkflow | true with workflow: create a new workflow with the form, instead of naming one. |
notifyEmail | With autoCreateWorkflow only: also email each submission to this address. |
destinationConnectionId | With 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). |
sharepoint | The library config for sharepoint. |
azureDevOps | The project and work item config for azuredevops. |
starter | A 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:
-
GETthe form. The answer is an HTML page. Its<form>holds one control per field and two hidden inputs of ours,__dbfRenderTokenand__dbfSubmitId. Copy both into your submission. (A form that only signed-in accounts may fill in adds a third,__dbfVisitorToken, after the sign-in.) -
POSTthe answers to the same address, within two hours of theGET(the token's lifetime). The body can be any of:multipart/form-data: what the page sends when the form has animageorfilefield. 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 adata: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
nameexactly (a dotted name is one key). A repeating-section cell istable.row.column, rows counted from 0:dependents.0.dep_name. Achoicesends the option'svalue; abooleansendstrueorfalse. A name the contract does not declare is dropped without an error. -
Files. A
filefield takes a PDF, Excel, CSV or Word document up to 10 MB, judged by its bytes. Animagefield takes PNG, JPEG, GIF or WebP. The whole submission may be up to 512 KB, plus room for one full-size document perfilefield (three perfilecolumn in a repeating section); more than that is a 413. -
The token. Without a valid
__dbfRenderTokenfrom a page the form served, the submission is refused with a 403.__dbfSubmitIdmakes 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.