Forms API
Read forms, create them, embed them on any site, and collect responses. All endpoints
use a forms key and respect every form feature: invite-only gating,
open/close schedules, response limits, conditional visibility and required-field
validation.
Base https://dev.bodek.us/v1/forms
Auth Authorization: Bearer bf_live_…
List forms
Lists the key owner's forms. Scope forms.read.
| Query | Description |
|---|---|
search | Filter by title. |
limit / offset | Pagination (default 25). |
curl https://dev.bodek.us/v1/forms \
-H "Authorization: Bearer bf_live_XXXX"
Create a form
Scope forms.write. Send a title, a list of questions, and optional settings.
curl -X POST https://dev.bodek.us/v1/forms \
-H "Authorization: Bearer bf_live_XXXX" -H "Content-Type: application/json" \
-d '{
"title": "Event signup",
"questions": [
{ "kind": "text", "preset": "title", "size": "xl", "bold": true,
"html": "Join us for the Spring Mixer" },
{ "question_text": "About you",
"answers": [
{ "type": "text", "label": "Full name", "required": true },
{ "type": "email", "label": "Email", "required": true }
] },
{ "question_text": "Will you attend?",
"answers": [
{ "type": "checkbox", "single": true, "options": ["Yes","No"], "required": true } ] }
],
"settings": { "thank_you_message": "See you there!", "max_responses": 100 }
}'
Returns the new form's id and its normalized schema. Mix in
kind: "text" items anywhere to add headings, instructions, or lists.
Get a form
Scope forms.read. Returns the full schema — items, answers (with each
answer's field name), and settings. Every item in questions
carries a kind: either "question" (the default — has
question_text and answers) or "text" (a
display-only content block — see below). Older forms created before content blocks
existed simply omit kind on their questions; treat a missing
kind as "question".
Content blocks
A content block is a styled, display-only item — a heading, a paragraph, or a bullet/numbered list — that renders inline among your questions but collects no answer. Use it for section titles, instructions, legal text, and the like. Content blocks never appear in responses, CSV exports, or response analytics, and they are skipped by required-field and conditional-logic evaluation on submit.
A content block is any item in questions with "kind": "text":
{
"kind": "text",
"preset": "title", // "title" | "subtitle" | "paragraph" | "custom"
"size": "xl", // "sm" | "md" | "lg" | "xl"
"align": "left", // "left" | "center" | "right"
"bold": true,
"color": "#1a2230", // "" or a #RRGGBB hex; blank inherits the form color
"html": "<b>Welcome</b> — please read <a href=\"https://x.tld\">the terms</a>.<ul><li>One</li><li>Two</li></ul>"
}
| Field | Notes |
|---|---|
preset | UI convenience that seeds the other style fields. title, subtitle, paragraph, or custom when manually tweaked. Optional; defaults to paragraph. |
size | One of sm, md, lg, xl. Defaults to md. |
align | left, center, or right. |
bold | Boolean; bolds the whole block. |
color | #RRGGBB or empty. |
html | Rich content. Sanitized server-side against a strict allowlist — only p, br, b, strong, i, em, u, s, ul, ol, li, a, span survive, all attributes are stripped except href on links (http/https/mailto only, forced to rel="noopener noreferrer nofollow"). Anything else is removed. Max 20 000 characters. |
Create or update forms with content blocks exactly as you would questions — just put
kind: "text" items in the questions array, in the order you
want them rendered. They may be freely interleaved with questions.
Branding & theme
Forms carry an optional settings.theme object for custom branding: a logo
at the top of the form plus page, card, text and accent colors. When
enabled is false (or the object is absent) the form uses the
clean standard theme — this is the default, and existing forms are unaffected.
On read (GET /v1/forms/{id} and the embed config) the theme
is returned with a resolved absolute logo_url:
"theme": {
"enabled": true,
"page_bg": "#f4f6f9",
"card_bg": "#ffffff",
"text_color": "#1a2230",
"accent": "#0d6efd",
"field_bg": "#ffffff",
"field_text": "#1a2230",
"logo_url": "https://forms.bodek.us/questionnaires/ABC1234567/creator/logo_….png",
"logo_align": "left",
"logo_height": 64,
"custom_css": ".bf-form{font-family:'Inter',sans-serif}"
}
On write (create/update) send the same fields, but the logo is set by
uploading through the form builder UI rather than the JSON API — the API accepts and
preserves an already-uploaded logo but does not accept binary uploads. Colors must be
#RRGGBB (3-digit hex is expanded); invalid colors are dropped. Heights are
clamped to 24–200 px.
| Field | Notes |
|---|---|
enabled | Master switch. If false and no custom values are set, the theme is omitted entirely. |
page_bg / card_bg | Page and question-card background, #RRGGBB or blank. |
text_color / accent | Body text and accent (buttons, highlights). The selected rating and submit button automatically pick a readable text color from the accent luminance. |
field_bg / field_text | Background and text color of question input fields (text boxes, dropdowns). Blank keeps the default. |
logo_align / logo_height | Logo placement and pixel height (24–200). |
custom_css | Raw CSS (max 20 000 chars) applied to the form, for brand matching and custom fonts (@import / @font-face are allowed). Sanitized server-side: HTML-tag breakouts and the javascript:/vbscript:/expression()/-moz-binding/behavior: vectors are stripped. Applies even when enabled is false. |
Custom CSS targets these stable classes, present on both the hosted form and embedded (SDK) forms:
| Class | Element |
|---|---|
.bf-form | Form wrapper |
.q-form-logo | Logo container |
.q-form-title | Form title |
.q-render-card | Each question card |
.q-num | Question number badge |
.q-render-title | Question text |
.q-answer-label | Field label |
.bf-field | Any input, select, or textarea |
.q-rating-row / .q-rating-opt | Rating row and each option |
.q-text-block | Content / text block |
.q-submit | Submit button |
Embed config
A public-safe render config used by the SDK: items (questions and content
blocks, each tagged with kind), the accepting state and
reason, invite requirements, the submit URL, and the theme object
(with a resolved logo_url) so embedded forms can match your branding.
Submit a response
Scope forms.submit. Field names are q{question}_a{answer}
(see each answer's field in the schema). Gating, response limits,
conditional visibility and required-field checks all run server-side; validation
failures return 422 with the offending fields.
curl -X POST https://dev.bodek.us/v1/forms/ABC1234567/responses \
-H "Authorization: Bearer bf_live_XXXX" -H "Content-Type: application/json" \
-d '{
"fields": { "q0_a0": "Jane Doe", "q0_a1": "jane@example.com", "q1_a0": "Yes" },
"invite_token": "OPTIONAL_IF_INVITE_ONLY"
}'
File answers. Upload the file(s) first (see
File uploads), then send the returned upload_id
as the field value. Single-file answers take a string; answers with
multiple: true take an array:
"fields": {
"q2_a0": "9fA3kZ1mQ0pX7bT2",
"q3_a0": ["Up1d000000000001", "Up1d000000000002"]
}
File uploads
Files are uploaded out-of-band, in resumable chunks, before the response is
submitted. Each upload yields a short upload_id you then reference from the
matching q{q}_a{a} field. Uploads are charged to the form owner's storage
and stored on the owner's configured backend. The flow is
init → chunk(s) → complete, all under scope forms.submit.
Start an upload. Body: field (the q{q}_a{a} it's for),
name, size (bytes), and optional mime. Returns an
upload_key (the capability for the following calls), the public
upload_id, the server's chunk_size, total_chunks, and
missing_chunks (indices still needed — supports resuming). A repeated init for
the same file resumes the existing session.
curl -X POST https://dev.bodek.us/v1/forms/ABC1234567/uploads \
-H "Authorization: Bearer bf_live_XXXX" -H "Content-Type: application/json" \
-d '{ "field": "q2_a0", "name": "resume.pdf", "size": 8388608, "mime": "application/pdf" }'
# → { "data": { "upload_key": "…32hex…", "upload_id": "9fA3kZ1mQ0pX7bT2",
# "chunk_size": 5242880, "total_chunks": 2, "missing_chunks": [0,1] } }
Send one chunk as multipart/form-data: a chunk file part plus the
zero-based chunk_index. Slice the file into chunk_size pieces and
send each index. Chunks are idempotent and may be retried or sent in any order. Returns
{ received: true, complete: bool }.
curl -X POST https://dev.bodek.us/v1/forms/ABC1234567/uploads/KEY/chunk \
-H "Authorization: Bearer bf_live_XXXX" \
-F "chunk_index=0" -F "chunk=@chunk0.bin"
Finalize once every chunk is received. The server reassembles, verifies the size, ships the
file to the owner's storage backend, and returns the stored file's upload_id,
name, size and a private url. Returns 422
with missing_chunks if anything is still outstanding, or 507 if the
owner is out of storage.
# → { "data": { "upload_id": "9fA3kZ1mQ0pX7bT2", "name": "resume.pdf",
# "size": 8388608, "url": "https://forms.bodek.us/review/ABC1234567/uploads/9fA3kZ1mQ0pX7bT2" } }
Returns the session status and remaining missing_chunks — useful to
resume after an interruption.
Abort an in-progress upload and discard its chunks.
Fetch a stored upload's metadata (name, size, mime) and
its serving url. The URL is owner-authenticated: it streams (or redirects to a
short-lived signed link) only for the form owner's session, so files stay private.
List responses
Scope forms.read. Returns submitted responses (owner key) with their
decoded answer data. Supports search, limit, offset.
File answers come back denormalized so you don't need extra lookups. A single-file answer
carries a file object; a multi-file answer carries a files array.
Each has id, name, size and an absolute, owner-only
url:
"q2_a0": {
"value": "9fA3kZ1mQ0pX7bT2",
"file": { "id": "9fA3kZ1mQ0pX7bT2", "name": "resume.pdf", "size": 8388608,
"url": "https://forms.bodek.us/review/ABC1234567/uploads/9fA3kZ1mQ0pX7bT2" }
}
Field types
| type | Value you send |
|---|---|
text / longtext | string |
email | valid email string |
number | number |
date | YYYY-MM-DD |
dropdown | one of options |
checkbox (single) | one of options |
checkbox (multi) | array of options |
rating | integer between from and to |
file (single) | an upload_id string (see File uploads) |
file (multiple: true) | array of upload_id strings |
For multi-select with "other" enabled, also send q{q}_a{a}_other_text.
File answers expose a multiple boolean in the schema — when true the input
accepts more than one file and the field value is an array. Conditional questions/answers
carry a show_if rule; hidden fields are skipped and never required. Items with
kind: "text" are content blocks, not fields — they take no value and are skipped
on submit.
Storage backends
Every form owner has a storage backend that holds uploaded files. The default is
Bodek storage, which shares one combined 5 GB allowance with Bodek
Files. Owners can instead connect their own Amazon S3 or
Backblaze B2 bucket (configured in form settings) for their own limit or
unlimited capacity. The choice is transparent to API clients — you always reference files by
upload_id and fetch them through the owner-authenticated
/files/{upload_id} URL.
When an owner using Bodek storage is over their limit, uploads are refused with
507, any form that collects files stops accepting submissions, and the owner is
emailed. Owners on their own bucket are bounded only by the limit they set (if any).
Embeddable SDK
Drop a form onto any site with one div and one script. Use a publishable
forms key restricted to your domains (scopes forms.read +
forms.submit).
<div data-bodek-form="ABC1234567" data-bodek-key="bf_live_XXXX"></div>
<script src="https://forms.bodek.us/sdk/bodek-forms.js"></script>
The widget injects a self-contained default stylesheet (so embedded forms match the
hosted look out of the box), renders inputs by type, draws content blocks and applies your
branding (logo and colors) via CSS variables, applies conditional logic client-side
(re-validated server-side), and submits. File answers are uploaded automatically in resumable
chunks the moment they're picked, with a progress card per file and support for multi-file
answers; submission is blocked until uploads finish, and empty required fields are flagged
with a red border. Your custom_css is injected last so it overrides the defaults;
target the same classes documented under Branding. For invite-only forms add
data-bodek-invite="<token>".