Developers
API methods
Complete reference for every method exposed by the formbase REST API. Each method shows its parameters, example requests, and response shapes.
The same reference is available as an OpenAPI 3.1 description for code generators, API clients, and agents.
Single endpoint, many methods
Every method is POST https://api.formbase.so/api/v1 with a JSON body {"method": "...", "params": {...}}
and an Authorization: Bearer fb_… header. See API overview for auth and error
handling, and API tokens for the token itself.
Conventions
paramsmay be omitted; it defaults to{}. An unknown method is404 METHOD_NOT_FOUND.A token is bound to one workspace. Naming another workspace, or a form in one, is
403 FORBIDDENeven when you belong to both.Pagination. List methods return
{ items, nextCursor, hasMore }; most also returncanPaginate, which isfalsewhenhasMoreis true but no cursor can resume (fuzzy search). PassnextCursorback ascursor.limitis 1–100, default 20 — exceptrequests.list, whose default is 25.Rate limits. 120 calls per minute per token, shared with the MCP server;
requests.createhas its own 60 per minute. Failed authentication is limited separately, 30 per 15 minutes per IP, after which bad tokens seeRATE_LIMITEDinstead ofUNAUTHORIZED.Body size. 1 MiB. Larger bodies are rejected with
VALIDATION_ERROR.Versioning. The path carries the version. Breaking changes ship as
/api/v2; new methods and new response fields do not.
Forms
forms.list
List forms in a workspace. Supports cursor pagination and optional fuzzy name search.
workspaceIdstringrequired
workspaceIdstringrequiredWorkspace ID.
folderIdstring | nulloptional
folderIdstring | nulloptionalFilter by folder. Pass null for root-level forms only. Omit to list all.
querystringoptional
querystringoptionalFuzzy name search. Results capped at limit; not cursor-paginated.
limitnumberoptionaldefault: 20
limitnumberoptionaldefault: 20Page size (1–100).
cursorstringoptional
cursorstringoptionalPagination cursor from a previous response.
curl -X POST https://api.formbase.so/api/v1 \
-H "Authorization: Bearer fb_..." \
-H "Content-Type: application/json" \
-d '{
"method": "forms.list",
"params": { "workspaceId": "ws_abc123" }
}'{
"ok": true,
"data": {
"items": [
{
"id": "frm_abc123",
"name": "Contact Form",
"folderId": null,
"workspaceId": "ws_abc123",
"isPublished": true,
"publishedAt": 1714041851000,
"unpublishedAt": null,
"createdAt": 1714041800000,
"lastEditedAt": 1714042000000,
"emoji": null
}
],
"nextCursor": null,
"hasMore": false,
"canPaginate": false
}
}forms.get
Get full details for a single form, including questions, cover, logo, and a preview URL.
formIdstringrequired
formIdstringrequiredForm ID.
curl -X POST https://api.formbase.so/api/v1 \
-H "Authorization: Bearer fb_..." \
-H "Content-Type: application/json" \
-d '{
"method": "forms.get",
"params": { "formId": "frm_abc123" }
}'{
"ok": true,
"data": {
"id": "frm_abc123",
"name": "Event Feedback",
"folderId": null,
"workspaceId": "ws_abc123",
"isPublished": true,
"publishedAt": 1714041851000,
"unpublishedAt": null,
"createdAt": 1714041800000,
"lastEditedAt": 1714042000000,
"emoji": null,
"questions": [
{
"id": "q_1",
"type": "email-input",
"inputType": "email",
"title": "Your email",
"metadata": null
},
{
"id": "q_2",
"type": "rating-input",
"inputType": "rating",
"title": "Overall experience",
"metadata": { "kind": "rating", "maxStars": 5 }
}
],
"hasContent": true,
"cover": null,
"logo": null,
"isDeleted": false,
"trashedAt": null,
"previewUrl": "https://formbase.so/preview/abc..."
}
}forms.create
Create a new empty form. Returns the form and a preview URL.
namestringrequired
namestringrequiredForm name (1–255 characters).
workspaceIdstringrequired
workspaceIdstringrequiredWorkspace ID.
folderIdstringoptional
folderIdstringoptionalPlace the form in a folder. Omit to create at workspace root.
curl -X POST https://api.formbase.so/api/v1 \
-H "Authorization: Bearer fb_..." \
-H "Content-Type: application/json" \
-d '{
"method": "forms.create",
"params": {
"name": "Contact",
"workspaceId": "ws_abc123"
}
}'const res = await fetch('https://api.formbase.so/api/v1', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.FORMBASE_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
method: 'forms.create',
params: { name: 'Contact', workspaceId: 'ws_abc123' },
}),
})
const { ok, data } = await res.json()import os, requests
res = requests.post(
"https://api.formbase.so/api/v1",
headers={"Authorization": f"Bearer {os.environ['FORMBASE_TOKEN']}"},
json={
"method": "forms.create",
"params": {
"name": "Contact",
"workspaceId": "ws_abc123",
},
},
)
data = res.json(){
"ok": true,
"data": {
"id": "frm_new123",
"name": "Contact",
"workspaceId": "ws_abc123",
"folderId": null,
"isPublished": false,
"createdAt": 1714041851000,
"previewUrl": "https://formbase.so/preview/abc..."
}
}forms.update
Update form metadata: name, folder, emoji, cover, or logo. Does not update form content (use the editor tools for that).
formIdstringrequired
formIdstringrequiredForm ID.
namestringoptional
namestringoptionalNew form name (1–255 characters).
folderIdstring | nulloptional
folderIdstring | nulloptionalMove form to a folder. Pass null to move to workspace root.
emojistring | nulloptional
emojistring | nulloptionalForm emoji (max 10 characters). Pass null to clear.
coverobjectoptional
coverobjectoptionalCover. {"type": "color", "color": "#ffffff"}, {"type": "image", "url": "https://...", "offsetY": 50} (
offsetY 0–100, default 50), or {"type": "none"} to remove. Image URLs must be http(s) or a
data:image URI.
logoobjectoptional
logoobjectoptionalLogo. {"type": "icon", "name": "HeartIcon"}, {"type": "image", "url": "https://..."}, or
{"type": "none"} to remove. Icon names are fixed: QuestionMarkIcon, ListBulletsIcon,
ChartBarIcon, ClockCountdownIcon, HeartIcon, LightbulbIcon,
CheckCircleIcon, MagnifyingGlassIcon, TrendUpIcon, EnvelopeIcon,
PhoneIcon, CalendarIcon, LinkIcon, UsersIcon.
Pass at least one of the five updatable fields. It does not change form content — use the MCP editor tools for that.
curl -X POST https://api.formbase.so/api/v1 \
-H "Authorization: Bearer fb_..." \
-H "Content-Type: application/json" \
-d '{
"method": "forms.update",
"params": {
"formId": "frm_abc123",
"name": "Updated Name",
"emoji": "📋"
}
}'{
"ok": true,
"data": {
"id": "frm_abc123",
"name": "Updated Name",
"folderId": null,
"emoji": "📋"
}
}cover and logo come back only when you sent them. A payload that matched the current state on every scalar field
adds noChange: true.
forms.publish
Publish a form so it can accept responses, and freeze its field keys into a new snapshot. Idempotent: an
already-published form returns success with alreadyPublished: true, and an unpublished form is republished from its last snapshot.
formIdstringrequired
formIdstringrequiredForm ID.
A form with content blocks but no questions publishes with a warning. A form with no content at all cannot be published. Publishing does not create a public URL — call shareLinks.create for that.
forms.unpublish
Take a form offline. Respondents can no longer open it. Idempotent — a form that is not published returns alreadyUnpublished: true.
Reversible with forms.publish.
formIdstringrequired
formIdstringrequiredForm ID.
forms.delete
Move a form to trash. Its active share links are revoked, so their public URLs stop serving.
formIdstringrequired
formIdstringrequiredForm ID.
Restoring does not bring the links back
forms.restore returns the form, but the share links it revoked stay revoked. Mint new ones with
shareLinks.create. A form already in trash returns alreadyTrashed: true and keeps its original trash date.
forms.restore
Restore a form from trash.
formIdstringrequired
formIdstringrequiredForm ID.
folderIdstring | nulloptional
folderIdstring | nulloptionalWhere to restore it. Omit for its original folder, null for workspace root, or a folder ID.
A form that is not in trash returns alreadyRestored: true.
formSettings.get
Read a form’s behavior settings.
formIdstringrequired
formIdstringrequiredForm ID.
Returns { settings, isDefault, availableEmailDomains, defaultFromAddress, payment }. isDefault is true when
the form has no saved settings row yet and you are seeing the defaults. availableEmailDomains holds the verified domain ids
you can pass as emailDomainId, and payment reports whether Stripe is connected (connecting it is a dashboard
step).
formSettings.update
Update a form’s behavior settings. A partial update: only the fields you send are written.
formIdstringrequired
formIdstringrequiredForm ID.
Accessgroupoptional
Accessgroupoptionallanguage (BCP-47, default “en”), requireAuthentication, showBranding,
captchaEnabled, passwordEnabled, password (4 characters or more; a string implies
passwordEnabled: true, null clears the gate).
Owner notificationsgroupoptional
Owner notificationsgroupoptionalnotifyOnSubmission, notificationEmails (array), selfNotificationSubject,
selfNotificationEmailBody, pdfGenerationEnabled, showViewSubmissionButton. Owner emails are not
translatable — write them in the language you want.
Respondent notificationsgroupoptional
Respondent notificationsgroupoptionalrespondentNotificationEnabled, respondentNotificationTo (the field id of an email question, or
null), respondentNotificationSubject, respondentNotificationBody,
respondentNotificationPdfEnabled.
Remindersgroupoptional
RemindersgroupoptionalrespondentReminderEnabled, respondentReminderTo, respondentReminderSubject,
respondentReminderBody, respondentReminderRequiredFieldIds, and reminderSteps — idle offsets like
[“1d”,“3d”,“1w”], at most 5, sorted and deduplicated on save, [] for none. The schedule applies to abandoned
public-link responses and to requests alike. Pro.
After submitgroupoptional
After submitgroupoptionalredirectUrl (http(s); null or “” clears), redirectQueryParams (
[{ paramName, fieldId }]), allowAnotherResponse (mutually exclusive with a redirect),
maxSubmissionsPerRespondent (0 = unlimited, max 1000), editAfterSubmit, maxEdits (max 3; 0 means
unlimited on Pro and Business, 3 on Free).
Retentiongroupoptional
RetentiongroupoptionaldraftRetentionDays and submissionRetentionDays (0–36500, null reverts to the default). Submission
retention is Business, and setting it clears any fixed deletion date configured in the builder.
emailDomainIdstring | nulloptional
emailDomainIdstring | nulloptionalA verified email-domain id from formSettings.get, for a custom From address. null resets to the default
sender.
Subjects and bodies are plain text and accept {{variable}} placeholders; newlines become paragraphs. Customizing a
respondent subject or body makes it translatable, so its keys appear in translations.listEntries right away.
Submissions
submissions.list
List a form’s submissions, newest page first, with cursor pagination.
formIdstringrequired
formIdstringrequiredForm ID.
includeDraftsbooleanoptionaldefault: true
includeDraftsbooleanoptionaldefault: trueInclude responses that were started but never submitted. Drafts are a Pro feature: on Free only completed submissions are listed.
translationLanguagestringoptional
translationLanguagestringoptionalAttach stored AI translations of the answers under items[].translation.display, keyed like display.
items[].answers and items[].display always stay the original.
limitnumberoptionaldefault: 20
limitnumberoptionaldefault: 20Page size (1–100).
cursorstringoptional
cursorstringoptionalPagination cursor from a previous response.
{
"ok": true,
"data": {
"formId": "frm_abc123",
"formName": "Event Feedback",
"items": [
{
"id": "sub_xyz789",
"submittedAt": "2026-05-18 18:02:28",
"isCompleted": true,
"createdAt": "2026-05-18 18:02:19",
"answers": {
"email": "user@example.com",
"plan": "pro",
"contacts": [{ "name": "Ada" }, { "name": "Grace" }]
},
"display": {
"email": "user@example.com",
"plan": "Pro",
"contacts": "Ada, Grace"
}
}
],
"nextCursor": null,
"hasMore": false,
"canPaginate": false
}
}Same answers as webhooks and callbacks
Each item carries answers keyed by field key and display with the same keys
as readable text — the shape a webhook payload, a
request callback and requests.get carry. A choice answer is its option key, a repeating
group an array of instances. Call fields.list for each key’s title and option labels. This method returns no totals.
submissions.pdf
Get a link to one submission’s PDF. Built for the Zapier connector: it returns a result only when the form has an active Zapier integration configured to include the PDF, and the PDF was retained.
formIdstringrequired
formIdstringrequiredForm ID.
submissionIdstringrequired
submissionIdstringrequiredSubmission ID. Must belong to that form and be completed.
{
"ok": true,
"data": {
"url": "https://api.formbase.so/api/storage/...",
"filename": "formbase-submission-sub_xyz789.pdf",
"contentType": "application/pdf",
"byteLength": 148213
}
}submissions.sample
Build a sample submission payload for a form, without any real data. It is the exact shape a share-link submission delivery carries, so
connectors use it for field discovery; a request-born submission reaches a subscription as request.completed instead, sampled by
requests.sample. data.form.snapshotId is the form’s current published version, the same id live
events carry, or null while the form is unpublished.
formIdstringrequired
formIdstringrequiredForm ID.
{
"ok": true,
"data": {
"id": "evt_example000000000000",
"type": "submission.completed",
"createdAt": "2026-05-18T19:00:00.000Z",
"apiVersion": "2026-09-24",
"test": true,
"data": {
"form": { "id": "frm_abc123", "name": "Event Feedback", "snapshotId": "js7abc123" },
"submission": {
"id": "sub_example000000000000",
"respondentEmail": "respondent@example.com",
"submittedAt": "2026-05-18T19:00:00.000Z",
"updatedAt": null,
"editCount": 0,
"pdfUrl": null,
"language": "en"
},
"answers": { "your_email": "john@example.com" },
"display": { "your_email": "john@example.com" }
}
}
}Field and payload semantics are documented once, in the webhooks reference.
Share links
Fields
fields.list
List every field of a form’s current published version, with the key to address each one by. Call this before requests.create instead of
hard-coding keys. See Field keys.
formIdstringrequired
formIdstringrequiredForm ID. A form that has never been published has no field keys yet and answers published: false with no items.
curl -X POST https://api.formbase.so/api/v1 \
-H "Authorization: Bearer fb_..." \
-H "Content-Type: application/json" \
-d '{
"method": "fields.list",
"params": { "formId": "j57..." }
}'{
"ok": true,
"data": {
"published": true,
"items": [
{ "key": "company_name", "type": "text", "title": "Company", "required": true, "prefillable": true },
{
"key": "company_size", "type": "select", "title": "Company size", "required": false, "prefillable": true,
"options": [
{ "key": "1_50", "label": "1–50" },
{ "key": "51_200", "label": "51–200" }
]
},
{
"key": "satisfaction", "type": "matrix", "title": "How did we do?", "required": false, "prefillable": true,
"rows": [{ "key": "delivery_speed", "label": "Delivery speed" }],
"columns": [{ "key": "very_good", "label": "Very good" }, { "key": "poor", "label": "Poor" }]
},
{ "key": "case_id", "type": "hidden", "title": "Case", "required": false, "prefillable": false, "context": true },
{ "key": "total", "type": "number", "title": "Total", "required": false, "prefillable": false, "calculated": true }
],
"hasMore": false
}
}Reading the flags
context: true is a hidden field — its value belongs in context, never in prefill.
prefillable: false marks a field nobody can supply a value for (file, signature, payment, appointment, documents). For a
choice question, send the option key, not its label; a matrix lists its rows and columns the
same way and takes { "row_key": "column_key" }. calculated: true is a calculated field: the form works out
its value, you read it back in answers, and nothing can send it.
A repeating group is type: “group” with repeating: true and a members array. A Documents block is
type: “documents” and carries documents: [{ name }], the authored files every respondent already sees.
Requests
A request assigns one published form to one person and calls you back when it ends. The conceptual guide lives in Creating a request; this is the parameter list.
requests.create
Create a request. Spends one unit of the workspace’s monthly allowance, whether or not the recipient answers.
formIdstringrequired
formIdstringrequiredThe published form to assign.
recipientobjectoptional
recipientobjectoptional{ email?, name? }. An email is required when delivery is “email”; otherwise it only
identifies the person in the Requests page and on their answers.
prefillobjectoptional
prefillobjectoptionalInitial answers by field key. The recipient sees them and can change them.
readonlystring[]optional
readonlystring[]optionalPrefilled keys the recipient cannot change. Every key here must also appear in prefill, and a locked required field must be
prefilled with a non-empty value.
contextobjectoptional
contextobjectoptionalValues for the form’s hidden fields, by field key. Trusted, unchangeable, and echoed back in the callback. An unknown key is rejected
with UNKNOWN_FIELD_KEY.
metadataobjectoptional
metadataobjectoptionalYour own bookkeeping. Never reaches the form; comes back in callbacks and reads.
languagestringoptional
languagestringoptionalOne of the form’s published languages. Defaults to the form’s own default.
deliverystringoptionaldefault: none
deliverystringoptionaldefault: none“email” to have formbase send the invitation (needs a recipient email, and Pro or Business or one of a Free account’s 10
free invitations), or “none” to deliver the link yourself.
remindersstring[]optional
remindersstring[]optionalOverride the form’s reminder schedule for this request. An empty array switches reminders off.
expiresAtnumberoptional
expiresAtnumberoptionalEpoch milliseconds. Defaults to 30 days out; 365 days is the maximum.
callbackUrlstringoptional
callbackUrlstringoptionalWhere formbase POSTs the callback when the request ends. HTTPS only, and the host must resolve to a public address.
externalIdstringoptional
externalIdstringoptionalYour own id for this request. Filterable in requests.list.
idempotencyKeystringoptional
idempotencyKeystringoptionalRepeating it with the same body returns the original request with deduplicated: true. A different body is rejected. Keys
live 30 days.
domainIdstringoptional
domainIdstringoptionalMint the link on one of your custom domains. REST API only.
documentsobject[]optional
documentsobject[]optional[{ documentId, field?, name? }] — files handed to this one recipient, uploaded first with
documents.create.
testbooleanoptionaldefault: false
testbooleanoptionaldefault: falseA dry run: nothing is emailed, the callback carries “test”: true, and the submission counts nowhere. The link closes within
24 hours, and on Free a workspace may create 10 test requests a day.
curl -X POST https://api.formbase.so/api/v1 \
-H "Authorization: Bearer fb_..." \
-H "Content-Type: application/json" \
-d '{
"method": "requests.create",
"params": {
"formId": "j57...",
"recipient": { "email": "ada@acme.com", "name": "Ada" },
"prefill": { "company_name": "Acme" },
"readonly": ["company_name"],
"context": { "case_id": "CASE-9" },
"delivery": "email",
"externalId": "run-42",
"callbackUrl": "https://automation.example/webhook/resume-abc",
"idempotencyKey": "run-42"
}
}'{
"ok": true,
"data": {
"id": "kd7...",
"status": "pending",
"url": "https://form.formbase.so/r/rq_...",
"deliveryStatus": "queued",
"expiresAt": 1794787200000,
"createdAt": 1789379200000,
"externalId": "run-42",
"deduplicated": false
}
}Keep the url
url carries the one-time token. requests.get can usually rebuild it, but it comes back null for a
request created before the deployment had a request-token key. If you deliver the link yourself, store it when you create it.
deliveryStatus is not_requested until an invitation is queued, then queued → sent or
failed, and bounced once the mail provider reports a hard bounce or a complaint.
requests.get
Get one request in full: status, outcome, what was prefilled, its timeline, and — once completed — answers and display keyed by field key, the same two maps the callback carries.
requestIdstringrequired
requestIdstringrequiredRequest ID.
{
"ok": true,
"data": {
"id": "kd7...",
"formId": "j57...",
"status": "completed",
"outcome": "approve",
"isTest": false,
"recipient": { "email": "ada@acme.com", "name": "Ada" },
"language": "en",
"externalId": "run-42",
"metadata": null,
"context": { "case_id": "CASE-9" },
"prefill": { "company_name": "Acme" },
"readonlyKeys": ["company_name"],
"delivery": "email",
"deliveryStatus": "sent",
"hasCallback": true,
"callbackFailedAt": null,
"submissionId": "kp2...",
"url": "https://form.formbase.so/r/rq_...",
"answers": { "company_name": "Acme", "decision": "approve" },
"display": { "company_name": "Acme", "decision": "Approve" },
"timeline": [
{ "id": "kd7...:created", "type": "created", "at": 1789379200000 },
{ "id": "kd7...:completed", "type": "completed", "at": 1789465600000 }
],
"expiresAt": 1794787200000,
"completedAt": 1789465600000
}
}outcome vs status
status says whether the request finished; outcome says what the recipient decided — approve,
decline, changes, or null on anything but a completed request whose recipient picked one of the
three — including a form with no decision question. The callback URL itself is never
returned; hasCallback only says whether one is set.
The example above is trimmed. A full response also carries workspaceId, formSnapshotId, createdVia,
documents, reminderStep, remindersSent, reminderDueAt, dataPurgedAt, and
the rest of the timestamps (updatedAt, openedAt, startedAt, lastActivityAt,
expiredAt, canceledAt, canceledBy, cancelReason).
Two fields tell you when the copy in front of you is the only copy. callbackFailedAt is set while this request's
callback has run out of attempts, and cleared once one gets through or you replay it. dataPurgedAt is set once retention
stripped the request: context, prefill and metadata come back empty, readonlyKeys and
documents are [], and submissionId, answers and display are
null.
timeline is derived, oldest first. Each entry has an id, an at, and a type —
created, invitation, reminder, opened, started, completed,
expired, canceled, callback. Delivery entries add deliveryStatus and
attemptCount, and callbacks add eventType. Delivery rows are kept 30 days, so older timelines thin back to the
timestamps.
requests.list
List requests in a workspace or on one form, newest first. Test requests are left out unless you ask for them.
workspaceIdstringoptional
workspaceIdstringoptionalScope to a workspace. Give this or formId.
formIdstringoptional
formIdstringoptionalScope to one form.
statusstringoptional
statusstringoptionalpending, completed, expired, or canceled.
outcomestringoptional
outcomestringoptionalapprove, decline, or changes. Implies completed requests only.
externalIdstringoptional
externalIdstringoptionalYour own id, to find the request a run created.
includeTestbooleanoptionaldefault: false
includeTestbooleanoptionaldefault: falseInclude requests created with test: true.
limitnumberoptionaldefault: 25
limitnumberoptionaldefault: 25Page size (1–100).
cursorstringoptional
cursorstringoptionalPagination cursor from a previous response.
{
"ok": true,
"data": {
"items": [
{
"id": "kd7...",
"formId": "j57...",
"status": "pending",
"outcome": null,
"recipient": { "email": "ada@acme.com", "name": "Ada" },
"externalId": "run-42",
"deliveryStatus": "sent",
"expiresAt": 1794787200000,
"createdAt": 1789379200000
}
],
"nextCursor": null,
"hasMore": false
}
}List items carry the same fields as requests.get minus url, answers, display, and
timeline, and each one carries isTest. Give workspaceId or formId — neither is
400 VALIDATION_ERROR with reason SCOPE_REQUIRED. outcome overrides status, since only
a completed request has a verdict.
requests.cancel
Withdraw a pending request. The link stops working, the recipient sees a withdrawn notice, and a request.canceled callback fires.
requestIdstringrequired
requestIdstringrequiredRequest ID.
reasonstringoptional
reasonstringoptionalYour note for why, kept on the request and sent in the callback.
requests.remind
Email the recipient now, without touching the reminder schedule. Needs a recipient email and a Pro or Business plan.
requestIdstringrequired
requestIdstringrequiredRequest ID. Must still be pending, and not a test request.
Two floors apply: at least 10 minutes between manual reminders, and at most 8 reminders per request in total, manual and scheduled
together. The automatic schedule is untouched — reminderStep and reminderDueAt stay where they were.
requests.replayCallback
Re-send the callback a request fired when it ended — same payload, same event id, so a receiver that already handled it can deduplicate. Use it after fixing a broken endpoint.
requestIdstringrequired
requestIdstringrequiredRequest ID. Must be completed, expired, or canceled.
{
"ok": true,
"data": { "dispatchId": "kf9...", "eventId": "evt_kf9..." }
}requests.sample
Build a sample request event for a form, without any real request. It is the exact envelope a request_* subscription created
with webhooks.create receives, so connectors use it for field discovery. A completed sample carries the
same example answers submissions.sample shows; an expired or canceled sample carries the request block
alone.
formIdstringrequired
formIdstringrequiredForm ID.
eventTypestringrequired
eventTypestringrequiredWhich ending to sample, in the webhooks.create spelling. The envelope’s type is the dotted form.
request_completedrequest_expiredrequest_canceled{
"ok": true,
"data": {
"id": "evt_example000000000000",
"type": "request.completed",
"createdAt": "2026-05-18T19:00:00.000Z",
"apiVersion": "2026-09-24",
"test": true,
"data": {
"request": {
"id": "req_example000000000000",
"externalId": "order-1234",
"status": "completed",
"language": "en",
"recipient": { "email": "recipient@example.com", "name": "Sample Recipient" },
"metadata": { "source": "sample" },
"context": {},
"createdAt": "2026-05-18T18:00:00.000Z",
"completedAt": "2026-05-18T19:00:00.000Z"
},
"form": { "id": "frm_abc123", "name": "Event Feedback", "snapshotId": "js7abc123" },
"submission": {
"id": "sub_example000000000000",
"respondentEmail": "respondent@example.com",
"submittedAt": "2026-05-18T19:00:00.000Z",
"updatedAt": null,
"editCount": 0,
"pdfUrl": null,
"language": "en"
},
"answers": { "your_email": "john@example.com" },
"display": { "your_email": "john@example.com" }
}
}
}The request block and the outcome are documented on the callbacks page; the submission half on
the webhooks reference. Sample ids are the fixed placeholders shown above and
test is true, so a receiver can tell a sample from a live event.
documents.create
Reserve an upload for a file you will hand to one recipient through the form’s Documents block. Bytes
never travel through this API: you get a presigned PUT, you upload, and requests.create verifies the object before the request exists.
formIdstringrequired
formIdstringrequiredThe form whose Documents block will show the file. Scopes the upload to that workspace.
namestringrequired
namestringrequiredDisplay name the recipient sees (1–200 characters). Overridable per request.
contentTypestringrequired
contentTypestringrequiredapplication/pdf or an image type: image/png, image/jpeg, image/webp,
image/gif, image/svg+xml, image/avif, image/bmp, image/tiff. Office
documents are not accepted.
sizenumberrequired
sizenumberrequiredExact byte length. Maximum 25 MB (26,214,400).
sha256stringoptional
sha256stringoptionalHex digest of the bytes. Verified after upload when given.
{
"ok": true,
"data": {
"id": "kn7...",
"name": "Lease contract draft",
"contentType": "application/pdf",
"size": 412000,
"uploadUrl": "https://...",
"expiresAt": 1792198800000
}
}PUT the raw bytes to uploadUrl within the hour, with Content-Type set to the type you declared,
then reference the id from requests.create:
{
"method": "requests.create",
"params": {
"formId": "j57...",
"documents": [
{ "documentId": "kn7...", "name": "Your lease contract" },
{ "documentId": "kn8...", "field": "attachments" }
]
}
}fieldis the Documents block’s field key. Optional when the form has exactly one block; required with two or more.- The block’s authored documents stay; yours appear below them, for this one recipient.
- Caps: 25 MB per document, 100 MB of documents per request, 20 documents shown per block including the authored ones.
One upload can be referenced by any number of requests. An upload nobody references ages out. Bytes count against the workspace owner’s storage until the last request referencing them is stripped by retention.
Every failure here is 400 VALIDATION_ERROR with a details.reason: DOCUMENT_TYPE_NOT_ALLOWED,
DOCUMENT_TOO_LARGE, INVALID_DOCUMENT_NAME or INVALID_DOCUMENT_SHA256 from this method, and
DOCUMENT_NOT_FOUND, DOCUMENT_NOT_UPLOADED (you skipped the PUT), DOCUMENT_INVALID,
INVALID_DOCUMENT_TARGET, DOCUMENTS_TOO_LARGE or DOCUMENTS_TOO_MANY from
requests.create.
Webhooks
webhooks.list
List webhook subscriptions for a form.
formIdstringrequired
formIdstringrequiredForm ID.
{
"ok": true,
"data": {
"items": [
{
"subscriptionId": "int_abc123",
"formId": "frm_abc123",
"provider": "zapier",
"targetUrl": "https://hooks.zapier.com/...",
"eventType": "submission_created",
"status": "active",
"createdAt": 1714041851000
}
],
"hasMore": false
}
}webhooks.create
Subscribe a URL to form events: new or abandoned submissions, or requests on the form ending. The URL must use HTTPS.
formIdstringrequired
formIdstringrequiredForm ID.
targetUrlstringrequired
targetUrlstringrequiredHTTPS URL to receive webhook payloads.
providerstringrequired
providerstringrequiredWhich tool the subscription belongs to. It is a label for your own bookkeeping — there is no marketplace app to install, and every provider behaves the same.
zapiermaken8neventTypestringoptionaldefault: submission_created
eventTypestringoptionaldefault: submission_createdEvent type to subscribe to. The three submission_ types deliver the submission payload: types deliver the matching request event whenever a request on the form
ends that way, signed with this subscription’s secret; test requests reach no subscription.submission_created a
first submission, submission_updated a respondent’s edit, and submission_abandoned an idle draft. The three
request_
submission_createdsubmission_updatedsubmission_abandonedrequest_completedrequest_expiredrequest_canceledidleWindowstringoptional
idleWindowstringoptionalRequired when eventType is submission_abandoned; rejected for every other type.
12h1d3d1wsigningSecretstringoptional
signingSecretstringoptionalOptional HMAC signing secret, 32–255 characters. When supplied, deliveries include X-formbase-Signature. Secret is stored
but never returned by API.
curl -X POST https://api.formbase.so/api/v1 \
-H "Authorization: Bearer fb_..." \
-H "Content-Type: application/json" \
-d '{
"method": "webhooks.create",
"params": {
"formId": "frm_abc123",
"targetUrl": "https://hooks.zapier.com/hooks/catch/...",
"provider": "zapier",
"eventType": "submission_created",
"signingSecret": "whsec_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
}
}'{
"ok": true,
"data": {
"subscriptionId": "int_new123",
"formId": "frm_abc123",
"provider": "zapier",
"targetUrl": "https://hooks.zapier.com/hooks/catch/...",
"eventType": "submission_created"
}
}Abandoned-submission subscriptions return the selected idleWindow from both webhooks.create and
webhooks.list. Every other subscription omits it.
A request subscription hears the same events a callback does, but as its own delivery: its own event id,
its own signature, and its own five-attempt retry budget, after which the subscription pauses. A request created with a
callbackUrl on a form with a request_completed subscription therefore fires twice, once to each receiver.
requests.replayCallback re-sends the callback only. Use requests.sample to see the payload
before any request has ended.
webhooks.delete
Remove a webhook subscription.
subscriptionIdstringrequired
subscriptionIdstringrequiredSubscription ID from webhooks.list or webhooks.create.
{
"ok": true,
"data": {
"subscriptionId": "int_abc123",
"deleted": true
}
}Analytics
analytics.get
Get aggregated analytics metrics for a form. Supports date range, device, traffic source, and country filters.
Analytics are a Pro feature, and the rule follows the workspace owner’s plan, the same way the dashboard’s Analytics tab does. If the owner is not on Pro, this returns UPGRADE_REQUIRED — including for history recorded while they were. A free member of a Pro owner’s workspace gets the data.
formIdstringrequired
formIdstringrequiredForm ID.
fromnumberoptional
fromnumberoptionalStart of date range as Unix timestamp in milliseconds. Must be less than or equal to to when both are set.
tonumberoptional
tonumberoptionalEnd of date range as Unix timestamp in milliseconds. Omit both for all time — period then comes back as
{ "from": null, "to": null }.
devicestringoptionaldefault: all
devicestringoptionaldefault: allFilter by device type.
alldesktopmobiletablettrafficSourcestringoptional
trafficSourcestringoptionalFilter by traffic source (e.g. “Direct”, “Google”).
countrystringoptional
countrystringoptionalFilter by 2-letter country code (e.g. “US”, “DE”).
includeEventsbooleanoptionaldefault: false
includeEventsbooleanoptionaldefault: falseAlso return the sanitized analytics events behind the metrics, for your own analysis. No visitor ids.
Rates are numbers from 0 to 100, counts are integers, and totalEvents is the raw event row count before deduplication into
unique visitors.
{
"ok": true,
"data": {
"formId": "frm_abc123",
"period": { "from": null, "to": null },
"totalEvents": 17,
"metrics": {
"views": 7,
"uniqueVisitors": 7,
"engaged": 6,
"submissions": 4,
"engagementRate": 86,
"completionRate": 57,
"bounceRate": 14
},
"breakdown": {
"byBrowser": { "Chrome": 17 },
"byCountry": { "US": 10, "DE": 7 },
"byDevice": { "desktop": 14, "mobile": 3 },
"bySource": { "Direct": 12, "Google": 5 }
}
}
}Workspaces
workspaces.list
List the workspaces your token can reach. No parameters.
An API token is bound to one workspace, so this returns exactly that one — even when your account belongs to several.
{
"ok": true,
"data": {
"items": [
{
"id": "ws_abc123",
"name": "Acme Inc",
"createdAt": 1714041800000,
"role": "owner"
}
],
"nextCursor": null,
"hasMore": false,
"canPaginate": false
}
}workspaces.createInvite
Create an invite link for a workspace.
workspaceIdstringrequired
workspaceIdstringrequiredWorkspace ID.
expiresAtnumberoptional
expiresAtnumberoptionalExpiration as a future Unix timestamp in milliseconds.
maxUsesnumberoptional
maxUsesnumberoptionalMaximum number of times the invite can be used.
{
"ok": true,
"data": {
"id": "inv_abc123",
"workspaceId": "ws_abc123",
"code": "aBcDeFgH",
"expiresAt": null,
"maxUses": null,
"uses": 0,
"createdAt": 1714041851000
}
}workspaces.getInvite
Get one workspace invite. Returns the same shape as workspaces.createInvite.
inviteIdstringrequired
inviteIdstringrequiredInvite ID.
workspaces.updateInvite
Update an existing workspace invite. Provide at least one of expiresAt or maxUses, or the call is rejected.
Returns the updated invite.
inviteIdstringrequired
inviteIdstringrequiredInvite ID.
expiresAtnumberoptional
expiresAtnumberoptionalNew expiration timestamp in milliseconds.
maxUsesnumberoptional
maxUsesnumberoptionalNew max uses limit.
workspaces.revokeInvite
Permanently revoke a workspace invite.
inviteIdstringrequired
inviteIdstringrequiredInvite ID.
{
"ok": true,
"data": {
"inviteId": "inv_abc123",
"revoked": true
}
}Folders
folders.list
List folders in a workspace.
workspaceIdstringrequired
workspaceIdstringrequiredWorkspace ID.
limitnumberoptionaldefault: 20
limitnumberoptionaldefault: 20Page size (1–100).
cursorstringoptional
cursorstringoptionalPagination cursor.
{
"ok": true,
"data": {
"items": [
{
"id": "fld_abc123",
"name": "Customer Feedback",
"workspaceId": "ws_abc123",
"parentId": null,
"createdAt": 1714041800000
}
],
"nextCursor": null,
"hasMore": false,
"canPaginate": false
}
}folders.create
Create a folder in a workspace. Idempotent — returns the existing folder if a folder with the same name already exists.
workspaceIdstringrequired
workspaceIdstringrequiredWorkspace ID.
namestringrequired
namestringrequiredFolder name (1–255 characters).
parentIdstring | nulloptional
parentIdstring | nulloptionalParent folder ID for nesting. Omit for root level.
{
"ok": true,
"data": {
"id": "fld_new123",
"name": "Customer Feedback",
"workspaceId": "ws_abc123",
"parentId": null,
"createdAt": 1714041851000,
"alreadyExisted": false
}
}folders.update
Rename a folder or move it to a different parent.
folderIdstringrequired
folderIdstringrequiredFolder ID.
namestringoptional
namestringoptionalNew folder name (1–255 characters).
parentIdstring | nulloptional
parentIdstring | nulloptionalNew parent folder. Pass null to move to root.
folders.delete
Permanently delete a folder and all its contents (subfolders and forms).
folderIdstringrequired
folderIdstringrequiredFolder ID.
Destructive operation
This permanently deletes all subfolders and forms inside the folder. This action cannot be undone.
{
"ok": true,
"data": {
"folderId": "fld_abc123",
"deletedFolderIds": ["fld_abc123"],
"deletedFormIds": ["frm_in_folder"],
"message": "Folder and 1 form deleted."
}
}Translations
translations.listLanguages
List all languages configured on a form.
formIdstringrequired
formIdstringrequiredForm ID.
{
"ok": true,
"data": {
"formId": "frm_abc123",
"items": [
{
"language": "es",
"completion": { "total": 15, "current": 12, "outdated": 2, "missing": 1, "suggested": 0 },
"lastUpdatedAt": 1714041851000
}
],
"nextCursor": null,
"hasMore": false,
"canPaginate": false
}
}translations.addLanguage
Register a language on a form. Every other translation method fails with 404 NOT_FOUND until you do.
formIdstringrequired
formIdstringrequiredForm ID.
languagestringrequired
languagestringrequiredBCP-47 language tag (e.g. “es”, “pt-BR”).
{
"ok": true,
"data": {
"formId": "frm_abc123",
"language": "es",
"rowId": "tl_new123"
}
}translations.removeLanguage
Remove a language and all its translations from a form.
formIdstringrequired
formIdstringrequiredForm ID.
languagestringrequired
languagestringrequiredBCP-47 language tag.
translations.listEntries
List every source key for one language on a form, with its current state. This is how you discover the key values translations.setEntry
takes.
formIdstringrequired
formIdstringrequiredForm ID.
languagestringrequired
languagestringrequiredBCP-47 language tag. Must already be on the form.
{
"ok": true,
"data": {
"formId": "frm_abc123",
"language": "es",
"completion": { "total": 15, "current": 12, "outdated": 2, "missing": 1, "suggested": 0 },
"items": [
{
"key": "block_q_1.title",
"status": "current",
"value": "[{\"text\":\"Tu correo electrónico\"}]",
"sourceFragment": "[{\"text\":\"Your email\"}]",
"updatedAt": 1714041851000,
"block": { "id": "q_1", "type": "email-input", "label": "Your email" }
}
],
"hasMore": false
}
}status is missing (nothing stored), outdated (the source changed since), current, or
suggested (an AI suggestion staged but not accepted). Keys cover form content (block_<id>.) and, once an
author has customized them, the respondent confirmation and reminder emails (,
email.confirmation.email.reminder.*).
translations.setEntry
Set a single translation entry. The language must have been added via translations.addLanguage first.
formIdstringrequired
formIdstringrequiredForm ID.
languagestringrequired
languagestringrequiredBCP-47 language tag.
keystringrequired
keystringrequiredA key from translations.listEntries. Do not construct one by hand.
valuestringrequired
valuestringrequiredThe translated fragment, JSON-stringified. Its mark structure must match the source fragment.
curl -X POST https://api.formbase.so/api/v1 \
-H "Authorization: Bearer fb_..." \
-H "Content-Type: application/json" \
-d '{
"method": "translations.setEntry",
"params": {
"formId": "frm_abc123",
"language": "es",
"key": "block_q_1.title",
"value": "[{\"text\":\"Tu correo electrónico\"}]"
}
}'Writes here are live
The API has no draft-then-publish step: a setEntry or deleteEntry reaches respondents immediately. The
dashboard and the MCP translation tools use a draft instead.
Returns { formId, language, key }.
translations.deleteEntry
Delete a single translation entry, reverting that key to the form’s default language. Idempotent. When the last entry for a language goes, the language drops off the form’s published languages.
formIdstringrequired
formIdstringrequiredForm ID.
languagestringrequired
languagestringrequiredBCP-47 language tag.
keystringrequired
keystringrequiredTranslation key to delete.
Account
me.get
Get information about the authenticated user.
{
"ok": true,
"data": {
"id": "usr_abc123",
"email": "you@example.com",
"name": "Jane Doe"
}
}Meta
methods.list
List every method name this deployment serves, sorted. The authoritative answer when this page and the server disagree.
{
"ok": true,
"data": {
"methods": [
"methods.list",
"analytics.get",
"folders.create",
"folders.delete",
"folders.list",
"folders.update",
"formSettings.get",
"formSettings.update",
"forms.create",
"forms.delete",
"forms.get",
"forms.list",
"..."
]
}
}Error reference
Every error response has the same shape. The top-level code set is closed on purpose: a new failure mode never adds a code, it adds a
reason. Branch on code for the HTTP-level outcome and on details.reason for the fix.
{
"ok": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "No field with key \"company\" on this form.",
"details": { "reason": "UNKNOWN_FIELD_KEY", "field": "prefill.company", "validKeys": ["company_name", "contacts"] }
}
}details is present whenever the server can name the cause. Besides reason, it may carry field (the
offending parameter, dotted for nesting), validKeys, validValues (the option values a choice
question accepts), expectedType, feature (on UPGRADE_REQUIRED), and retryAfterMs (on a
throttled call). Reasons for the request surface are listed with each method above.
These are all the codes:
VALIDATION_ERROR400optional
VALIDATION_ERROR400optionalInvalid or missing parameters in the request.
UNAUTHORIZED401optional
UNAUTHORIZED401optionalMissing or invalid API token.
FORBIDDEN403optional
FORBIDDEN403optionalToken lacks access to the requested resource.
NOT_FOUND404optional
NOT_FOUND404optionalResource does not exist.
METHOD_NOT_FOUND404optional
METHOD_NOT_FOUND404optionalUnknown method name. Use methods.list to see available methods.
CONFLICT409optional
CONFLICT409optionalThe resource is not in a state that allows this call — a request that is no longer pending, an idempotency key reused with a different body.
RATE_LIMITED429optional
RATE_LIMITED429optionalOver 120 calls a minute on this token, over 60 requests.create calls a minute, or too many failed authentications from this
IP.
UPGRADE_REQUIRED402optional
UPGRADE_REQUIRED402optionalFeature requires a higher subscription tier, the workspace has spent its monthly allowance (reason
MONTHLY_ALLOWANCE_REACHED), or a Free account has spent its 10 free invitations (reason FREE_INVITATIONS_USED
).
INTERNAL_ERROR500optional
INTERNAL_ERROR500optionalUnexpected server error. Try again later.