# formbase Docs (Complete) --- ## Getting Started # Introduction What formbase is, what it does, and who it is for. ## Introduction formbase collects and verifies information from your customers. Send one person a prefilled form, get structured answers back, and let your workflow or AI agent carry on.

What is formbase?

Most workflows run fine until they need something only a customer can give: billing details, a signed contract, a missing invoice line, a yes or no. The run stops, and somebody writes an email.

formbase is that step. You build a form once, then send it to one named person as a request. They open a branded form in their own language with what you already know filled in. When they finish, formbase hands the answers back to your workflow.

How a request works

Build your form →', }, { title: 'Create a request', description: 'By hand from the Share sheet, or from Make, Zapier, an AI agent, or your own code. Prefill what you know and lock what must not change. Creating requests →', }, { title: 'The recipient completes it', description: 'formbase emails the invitation and the reminders (Pro and Business; Free gets 10 invitation emails to try it), or gives you a secret link to send yourself (every plan). Each link completes once, and a request that nobody finishes expires. Invitations and reminders →', }, { title: 'The answers come back', description: 'formbase posts a signed callback with the answers under stable field keys, so a renamed question never breaks your mapping. The Requests page shows every request and where it stands. Callbacks →', }, ]} /> > ℹ️ **No automation needed** >

> Every published form also has a public link for anyone to fill in — as a link, a QR code, an embed, or a popup. Answers > from both channels land in the same inbox. Share a public link → >

What you can build

Plans

formbase is free to start: unlimited forms, workspaces, and members, and 1,000 submissions and requests a month. Pro and Business raise the limits and add request emails (Free gets 10 invitation emails to try them), custom domains, advanced analytics, AI Skills, and more.{' '} Compare plans →

Where to start

New here? Follow this section in order: sign up, build a form, send your first request. Or jump straight to what you came for.

- [Send your first request](/getting-started/send-your-first-request) — Assign a form to one person and watch the answers come back - [Automate with the API](/requests/creating-requests) — Create requests from your workflow, with prefilled values and a callback - [Connect an agent](/guides/ai-agents/connect) — Let Claude or Cursor build a form and send requests for you - [Share a public link](/getting-started/share-a-public-link) — Open the same form to anyone — link, embed, or QR code
# Sign up & workspace Create your account, set up your workspace, and invite your team. ## Sign up & workspace Create your account, set up a workspace, and invite teammates. No credit card needed.

Create your account

Open app.formbase.so. There are two ways in, and neither needs a password:

Signing in for the first time creates your account. No credit card, no trial clock.

Create your first workspace

A workspace holds your forms, submissions, integrations, and teammates. The first screen after signing in says{' '} Welcome to formbase and has one button: Create Workspace. New to the workspace/account/form distinction? See{' '} how they fit together.

All it asks for is a name — your company, team, or just Personal. Your timezone comes from your browser. Change either later in{' '} Workspace Settings, under Workspace details.

Need more than one? Every plan includes unlimited workspaces — useful for separating clients, brands, or projects. See{' '} multiple workspaces.

Invite teammates (optional)

Working solo? Skip ahead. For teams, open the workspace switcher at the top of the sidebar → Workspace Settings, then click Invite in the Members section. You get a link that anyone can use to join. Members and workspaces are unlimited on every plan, and any member can invite — renaming, removing members, and deleting the workspace are the owner's. See{' '} inviting members.

Next steps

You have an account and a workspace. Next: build a form and publish it — everything else in formbase starts from a published form.

- [Build your form](/getting-started/build-your-form) — Build and publish in minutes - [Inviting members](/workspaces-teams/inviting-members) — Roles, seats, and invitations in detail - [Plans & pricing](/subscription-billing/plans-pricing) — Compare Free, Pro, and Business features - [Getting started FAQ](/getting-started/faq) — Common questions about your first hour
# Build your form Go from an empty canvas to a published form — the one thing every request and every public link needs. ## Build your form A form is the design: the questions you ask and the way they look. Publishing it is what makes it usable — as a request sent to one person, or as a public link open to everyone. This page gets you to a published form.

Publish in four steps

The editor works like a document. Type where you want text, and press a key to add a question.

Forms in the sidebar and click Form, top-left. Or go to app.formbase.so/try to build one without an account.', }, { title: 'Add questions', description: 'Click in the editor and press / to open the block menu. Pick a question type, type the label, and hit Enter. Drag the handle on the left to reorder.', }, { title: 'Preview', description: 'Click Preview in the toolbar to walk through the form exactly as the person answering it will.', }, { title: 'Publish', description: 'Click Publish (top-right). The form goes live, its field keys freeze, and a Share button appears in the toolbar — it is only there once a form is published.', }, ]} /> > ℹ️ **Safe to experiment** >

Edits autosave as a draft. Your live form keeps the last published version until you publish again.

What publishing does

Publishing is not only "make it visible". It fixes three things you will rely on later:

> ✅ **Other ways to start** >

> Templates — open Templates in the sidebar for 19 ready-made forms in six categories.{' '} > AI — open AI chat (bottom-right) and describe what you need. See Using AI. >

Next steps

- [Send your first request](/getting-started/send-your-first-request) — Assign the form you just published to one person - [Tour the editor](/building-forms/understanding-the-editor) — Toolbar, block menu, theme, share, settings - [Question types](/building-forms/field-types) — Text, choice, file upload, payment, decision, and more - [Using AI](/ai/using-ai) — Generate a form from one prompt
# Send your first request Assign your published form to one person, watch what they see, and find the answers afterwards. ## Send your first request You have a published form. Now assign it to one person: their own link, with what you already know filled in. Here you make one and open it yourself, so you see both halves — what the recipient gets, and what you get back. > ℹ️ **Works on every plan** >

> You deliver the link yourself here, so nothing needs upgrading. Letting formbase email the invitation and chase people with reminders > needs a Pro or Business plan. On Free, formbase can email your first{' '} > 10 invitations to try it. >

Make one and open it yourself

Share in the top toolbar. An unpublished form cannot be requested, so publish first if the button is not there.', }, { title: 'Switch to the Requests channel', description: 'Requests is the channel for one named person; Public link is for anyone with the URL. Pick Requests.', }, { title: 'Fill in the Manual tab', description: "Under Create a request there are three tabs. Manual is the by-hand one: Who is it for?, Prefill answers (lock any the recipient may only confirm), Hidden fields, and Callback and options. curl and MCP hand you the same request as a snippet, already written against this form's field keys.", }, { title: 'Click Try it yourself', description: 'Under See what they will get, this turns your draft — prefill, locks, hidden fields, callback, expiry — into a test request addressed to you, and shows its link. Nothing is emailed, it spends none of your monthly allowance, and its submission never reaches the inbox, the counts, exports, or your integrations.', }, ]} />

What the recipient sees

Open the test link in a new tab and answer the form as your recipient would. It is the form you authored — your theme, your logo, your language — with no captcha and no sign-in. When you submit, you get your thank-you page.

Open the same link again afterwards. Instead of a blank form you get the outcome page — your thank-you page again, or{' '} You have already submitted, under the same cover and logo. A request is answered once, by one person, and the link says so rather than quietly collecting a second response. Expired and canceled requests show a short notice there instead.

Find it on the Requests page

Click Requests in the sidebar, between Forms and Trash. This is every request in the workspace, across every form. Open{' '} Filter and tick Show test requests to see the one you just made — test requests are hidden by default and carry a Test badge.

Click a request to open its drawer. It answers two questions:

From here you can copy the request link, cancel the request, send a reminder (Pro), or replay a callback. A request expires 30 days after it is created unless you set your own Expires in (days), between 1 and 365.{' '} The Requests page →

What you left out

Doing it by hand is the whole loop in miniature. When a workflow creates the request instead, three things join in — and they are the reason requests exist:

> 💡 **Same thing from your own code** >

> The curl and MCP tabs on that same card are not a different feature — they run exactly what the Manual > tab runs. Copy one, swap the recipient, and you have automated the step. Creating a request → >

Next steps

- [Share a public link](/getting-started/share-a-public-link) — Open the same form to anyone, by link, embed, or QR - [Requests overview](/requests/overview) — The full model: statuses, expiry, and what you need - [Creating a request](/requests/creating-requests) — Prefill, locked fields, context, and every option - [Decisions & approvals](/requests/decisions-and-approvals) — Ask for approve, decline, or changes — and branch on it
# Share a public link Open the same form to anyone — copy the link, embed it, or print a QR code. ## Share a public link The second channel: a share link anyone can open, any number of people, any number of times. Same form, same answers inbox — no named recipient and no callback.

Copy the link

Share and pick the Public link channel to find it.", }, { title: 'Copy it', description: 'That URL is live. Open it in a new tab and answer it once — that is the first row you will see on the next page. Then send it, post it, or put it behind a button.', }, { title: 'Name it if you plan to make more', description: 'One form can have several share links, each with its own settings and analytics. Create new link adds one; Link name labels it — "Newsletter", "Social Media". The name is yours only; respondents never see it.', }, ]} />

Three ways to put it in front of people

Per-link settings

Each share link carries its own settings, grouped into Link, Query parameters, Appearance, and Limits:

A link with responses is revoked rather than deleted: it stops accepting new ones but keeps its analytics, and you can re-activate it later.

Share links →

> ℹ️ **Which channel should this be?** >

> Use a share link when anyone may answer and you do not know who in advance. Use a{' '} > request when one named person owes you one answer and your workflow needs to know > when it arrives. Both spend from the same monthly allowance, and both land in the same inbox. >

Next steps

- [See answers arrive](/getting-started/see-answers-arrive) — Where responses from both channels land - [Embedding & popups](/sharing-publishing/embedding-popups) — Put the form on your own site - [Response limits](/sharing-publishing/response-limits) — Cap how many responses a link accepts - [Custom domains](/branding-domains/custom-domains) — Serve the form from your own domain (Pro)
# See answers arrive One inbox for both channels — where answers land, how to get notified, and how to route them onward. ## See answers arrive A request and a public link collect into the same place. Here is that place, how to be told when something lands, and how to push it into the rest of your stack.

One inbox, both channels

Open the form and click Submissions in the top toolbar. It opens on the response list; every answer is here, whoever it came from, and the Channel column says which channel each one arrived through:

The test request you answered is not here: a test submission is stored on the request, but kept out of the inbox, the counts, exports, and your integrations. The public link you answered is.

The same toolbar button has three views:

See submission inbox for the full walkthrough.

> ℹ️ **A request also has a page of its own** >

> The inbox holds the answers. The Requests page holds everything around them: who has not > opened yet, what you prefilled, which reminders went out, and whether the callback was accepted. The answers are in the inbox; the state > of the request is on the Requests page. >

Get notified

Open Settings → E-mail Notifications and turn on Notify on new submission. Your own address is filled in for you. This works on Free, no upgrade needed. On Pro, you can add more recipients and write your own subject and body. See{' '} self-notifications for details.

Want the person who answered to get a confirmation too? Turn on Send confirmation to respondent (Pro) from the same panel. On a request, it goes to the recipient you named. See{' '} respondent notifications.

Route answers onward

Notifications tell a person. These two tell a system — and which one you want depends on the channel:

Next steps

- [Tour the dashboard](/getting-started/dashboard) — Sidebar, workspace switcher, AI, and where every setting lives - [Submission inbox](/submissions-analytics/submission-inbox) — Filter, search, sort, and review each response - [Self-notifications](/submissions-analytics/self-notifications) — Get emailed when new responses come in - [Respondent notifications](/submissions-analytics/respondent-notifications) — Confirm with a custom subject, body, and PDF
# Understanding the dashboard Tour the formbase dashboard — sidebar, workspace, AI panel, and settings. ## Understanding the dashboard A map of formbase outside the editor. Skim once — every section links to a dedicated page when you want depth.

The layout

Three regions:

Two shortcuts work anywhere: / opens search, and{' '} / opens and closes AI chat.

Workspace switcher

Top of the sidebar. Click to switch workspaces, create a new one, open Workspace Settings, or leave a workspace you don't own. Workspaces overview →

One line each — follow the link for full docs.

Below that:

Plan, credits, theme, and language

Just above your avatar sits one card with four rows:

Your profile

Click your avatar at the very bottom of the sidebar. The menu has two items:

AI chat

The button in the bottom-right opens AI chat — an assistant that can create, edit, and analyze your forms. It is on every page, and{' '} / opens and closes it. For what it can do, see{' '} AI chat in the editor guide and AI overview.

> ℹ️ **Connect your own agent** >

> Prefer Claude, ChatGPT, Cursor, or another tool? See Connect an AI agent. >

Settings — what lives where

Next steps

- [Understanding the editor](/building-forms/understanding-the-editor) — Full editor tour - [Folders](/workspaces-teams/folders) — Organize forms at scale - [Inviting members](/workspaces-teams/inviting-members) — Add teammates to your workspace - [Build your form](/getting-started/build-your-form) — From blank canvas to a published form
# Getting started FAQ Common questions about your first hour with formbase. ## Getting started FAQ Quick answers to the questions every new formbase user asks in their first hour.

Is formbase really free?

Yes. The Free plan includes unlimited forms, 1,000 submissions and requests a month from one shared allowance, 100 MB of storage, unlimited workspaces and members, conditional logic, file uploads, integrations, translations, CSV and Excel exports, and email notifications to your own address. No credit card to start. AI works on Free too — you buy a credit pack, the same as on every plan.

Need more room? Pro and Business raise the allowance to 50,000 a month, storage to 500 GB, and add request invitations and reminders, custom domains, custom themes, advanced analytics, and AI Skills. Business adds version history. Pro starts with a 14-day free trial on monthly billing. See plans & pricing for the full comparison.

Do respondents need a formbase account to fill out a form?

No. Anyone with the share link (or a published embed) can submit with no account. You can optionally gate a form behind a password, or require respondents to log in with a formbase account (via Google or email magic link) before they can submit. See{' '} authentication gate for details.

What is a request, and how is it different from a share link?

A share link is public: anyone who has it can submit, any number of times. A request is one assignment of the same form to one named person — with values you already know prefilled (and optionally locked), one completion, its own expiry, its own reminders, and a signed callback to your workflow the moment they finish.

Both are channels on the same form, and a form can use both at once. Create a request by hand from{' '} Share → Requests → Manual, or from Make, Zapier, Claude via MCP, or the API. Handing out the link yourself works on every plan; having formbase email the invitation and chase it needs Pro or Business, though a Free account can have formbase email its first 10 invitations to try it. See the requests overview.

Where do submissions go?

Into one inbox per form, whichever channel they came through. The Submissions button in the form toolbar has three views:{' '} Summary (charts per question), Submissions (the response list), and Analytics (traffic, conversion, and device breakdown — Pro). You can also get an email on every response (more addresses on Pro), or route submissions to Google Sheets, Notion, Airtable, Slack, Discord, Linear, GitHub, or a signed webhook. See{' '} see answers arrive for the walkthrough.

Can I delete my account and all my data?

Yes. Click your avatar at the bottom of the sidebar → Settings, then scroll to Delete Account in the Danger Zone. Type your email address to confirm. This permanently removes your forms, submissions, files, and the workspaces you own. There is no undo.

If you have an active Pro or Business subscription, deleting your account also cancels it — formbase tells Polar to stop billing as part of the deletion, so you won't be charged again.

Can I use formbase without AI?

Yes. AI is optional throughout — ignore the AI button and build every form by hand with the slash menu. Credits are only spent when you ask AI to do something. When they run out, the editor keeps working; only AI chat stops.

Can I use formbase on mobile?

Yes. The editor, dashboard, and all management features work on mobile browsers. Respondent-facing forms are fully responsive.

What's the difference between a workspace, an account, and a form?

Can I transfer a form or workspace to someone else?

You can duplicate a form within the same workspace and share access by inviting that person to the workspace. There is no self-serve way to move a form to a different workspace or transfer workspace ownership — contact support for either.

I deleted a form by accident — can I get it back?

Yes. Open Trash in the sidebar and click Restore. You pick the folder it comes back to. Deleted forms stay in Trash for 30 days before formbase removes them for good.

Will my forms be deleted if I stop using them?

On the Free plan, yes — eventually. A free-plan form that goes more than a year without any edits or submissions is automatically moved to Trash — and only after formbase emailed you a warning at least 30 days earlier. You then have 30 days to restore it before it's permanently deleted along with all its submissions and files. Pro and Business forms are never auto-archived for inactivity. See{' '} Limits & quotas for full details.

If I turn on authentication, do my respondents need to create a formbase account?

Yes. When authentication is required, respondents must log in with a formbase account — via Google or an email magic link. See{' '} authentication gate. If you only need to confirm an email answer, Business plans offer respondent email verification — a one-time code on the email question itself, no account needed.

What integrations does formbase support?

formbase delivers submissions to Google Sheets, Notion, Airtable, Slack, Discord, Linear, GitHub, and signed webhooks, and connects to Cal.com for Schedule appointment blocks. Every plan can stream form analytics to Google Analytics and Axiom. There is no per-plan cap on how many integrations a form has. Zapier and Make are listed as Coming soon on the Integrations page — but both already run the whole request loop through their HTTP steps today. See the Integrations section.

Next steps

- [Understanding the editor](/building-forms/understanding-the-editor) — Slash menu, blocks, drag-to-reorder, and more - [Plans & pricing](/subscription-billing/plans-pricing) — Compare Free, Pro, and Business side by side - [Authentication gate](/sharing-publishing/authentication-gate) — Password protect or require login to submit
--- ## Requests # Requests overview Ask one specific person to fill in one form, then resume your workflow when they are done. ## Requests overview A request is one assignment of a published form to one person. Your automation creates it, formbase hands back a secret link, the recipient fills in a prefilled form, and formbase calls your workflow back so the run continues.

What a request is

A form is a reusable design. A request is one assignment of that design to one named person — with their data already filled in, their case number attached, and a callback waiting for the moment they finish.

You do not build a different form for this. Any published form can be sent as a request. Nothing about the form changes; what changes is who opens it and what your automation knows when they are done.

Two channels, one form

In the Share sheet, a published form has two tabs — Public link and Requests. These are the two{' '} channels a form reaches people through, and a form can use both at once. The public link goes on your website; requests go to the three suppliers you are chasing this week. Same form, same submission inbox.

> ℹ️ **Three ways to create a request** >

> Open a published form, press Share, and pick the Requests channel. The Manual tab > creates one request by hand — fill in the recipient, prefill what you know, and press Send invitation or{' '} > Create link. The curl and MCP tabs hand you a ready-made snippet for the same thing > from Make, Zapier, Claude, or a direct API call. >

>

> A request made by hand behaves exactly like one an automation created: same lifecycle, same reminders, same signed callback.{' '} > Try it yourself creates a test request addressed to you and hands you its link — nothing is emailed — so you can walk > the whole flow through before you wire anything up. >

> ℹ️ **Step by step in Zapier and n8n** >

> The Zapier guides walk through it click by click:{' '} > send a request from a Zap, act on its outcome, > and look it up, remind or cancel it. The same three jobs have{' '} > guides for self-hosted n8n. >

The lifecycle

  1. Your automation calls requests.create with a form id, the recipient, and any values you already know.
  2. formbase returns a request link — a secret URL like https://form.formbase.so/r/rq_…. It opens as many times as the recipient needs, from any device.
  3. The recipient gets the link, either through the request invitation email formbase sends, or through your own channel if you would rather send it yourself.
  4. They open a normal formbase form: your branding, your theme, your language — with their values already in place and the fields you locked shown read-only.
  5. They submit. The answers land in the form's inbox like any other submission, and formbase POSTs a signed callback to the URL your automation gave it, with the answers keyed by field key.

If they never finish, reminders chase them on a schedule, and the request eventually expires. Either way your automation is told — the callback fires on expiry and cancellation too, so a workflow run is never left hanging.

Statuses

Completed, expired, and canceled are terminal: the link keeps working but shows an outcome page — a receipt, or a plain "this request has expired" notice — instead of the form.

While a request is pending, formbase also records when it was first opened, when the first answer was saved, and the last activity. That is what the Requests page means by "not opened", "opened", and "in progress" — activity, not extra statuses.

What you need

Next

- [Create a request](/requests/creating-requests) — Discover field keys, prefill, lock, and attach context. - [Field keys](/requests/field-keys) — The stable names automations address your fields by. - [Invitations & reminders](/requests/invitations-and-reminders) — Email delivery, the shared schedule, and expiry. - [Callbacks & signing](/requests/callbacks) — Resume your workflow and verify that the call is ours. - [The Requests page](/requests/managing-requests) — See what you are waiting on and act on it.
# Creating a request Discover a form's field keys, then create a request with prefilled values, locked fields, and context. ## Creating a request Two API calls: ask the form what it can be told, then assign it to one person with the values you already know.

Start in the Share sheet

Open your published form, click Share, and pick the Requests tab. The card there gives you everything you need to make the first call:

> ⚠️ **Publish first** >

> An unpublished form cannot be requested, and the snippets stay disabled until you publish. Field keys are frozen at first publish — that > is what lets your automation keep addressing company_name a year later. See Field keys. >

Step 1 — Discover the fields

fields.list returns every field of the form's current published version, with the key to address it by, the value shape it takes, and which bucket it belongs in.

``` curl -X POST https://api.formbase.so/api/v1 \ -H "Authorization: Bearer $FORMBASE_TOKEN" \ -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": "case_id", "type": "hidden", "title": "Case", "required": false, "prefillable": false, "context": true }, { "key": "total", "type": "number", "title": "Total", "required": false, "prefillable": false, "calculated": true }, { "key": "signature", "type": "signature", "title": "Sign here", "required": true, "prefillable": false } ], "hasMore": false } } ```

Step 2 — Create the request

``` curl -X POST https://api.formbase.so/api/v1 \ -H "Authorization: Bearer $FORMBASE_TOKEN" \ -H "Content-Type: application/json" \ -d '{"method":"requests.create","params":{ "formId":"j57...", "recipient":{"email":"ada@acme.com","name":"Ada"}, "context":{"case_id":"CASE-9"}, "prefill":{"company_name":"Acme","company_size":"51_200"}, "readonly":["company_name"], "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 } } ```

deliveryStatus is "queued" when formbase emails the invitation and "not_requested" when you deliver the link yourself.

Prefill, locked fields, and context

Three different things can be attached to a request, and mixing them up is the most common first mistake.

Prefill

Initial answers for the visible questions, so the recipient reviews and corrects instead of typing from scratch. Everything you already know about them belongs here — the company name from your CRM, the amount from the invoice, last year's answers.

Locked fields

List a prefilled key in readonly and the recipient sees the value but cannot change it. Use it for the facts they are confirming rather than supplying — the contract number, the agreed price. Locking is per request: the form itself is untouched, and the same field is freely editable on the next request.

Every locked key must also be prefilled, and a locked required field must be prefilled with something non-empty — otherwise the recipient would face a form they could never submit, and formbase rejects the call instead of creating that trap.

Context

Trusted values for the form's hidden fields — a case number, a workflow run id, an amount. Context feeds variables, conditional logic, calculated fields, and email copy, comes back unchanged in the callback, and the recipient cannot alter it. That last part is the difference from seeding a hidden field through a URL on a public link, where anyone can edit the query string; request links ignore URL query parameters entirely. Context values must be a string, number, or boolean.

Context is not free-form: every key must be a hidden field on the form's published version, and any other key is rejected with{' '} UNKNOWN_FIELD_KEY. Bookkeeping that has no hidden field, like an execution id, belongs in metadata.

Hidden fields are not shown on the form, but a context value is not secret from the recipient. They see it wherever the form or the invitation shows it: a mention in the form content or email copy, or a visible question that uses that hidden field as its default value. In that last case the recipient sees the context value pre-filled in that question and can edit the answer. The context value itself stays unchanged. A{' '} prefill for that question's own key wins over the default.

Metadata

Your own bookkeeping — an execution id, a CRM record id. It never reaches the form at all, so it cannot be piped into copy or read by logic; it just rides along and comes back in every callback and status read.

Value shapes

Send values in the shape the type from fields.list asks for. A wrong shape comes back as a validation error naming the key, the expected type, and — for choice questions — the values that would have been accepted.

Documents

A Documents block hands files to the respondent. Its authored files are the same for everyone and always stay; a request adds files for its one recipient below them — the customer's own lease contract, an ID copy to check. Bytes never travel through the API call itself: upload first, then reference.

``` "documents": [ { "documentId": "kn7...", "name": "Your lease contract" }, { "documentId": "kn8..." } ] ```

name overrides the display name stored on the upload. When the form has more than one Documents block, name the target with{' '} field, the block's field key (fields.list lists it, together with the authored documents every respondent already gets). The files land below those authored documents — a request adds files, it never replaces one. One upload can be referenced by as many requests as you like — a price list uploaded once serves five hundred requests.

Limits: PDF and images only, 25 MB per document, 100 MB per request (DOCUMENTS_TOO_LARGE), and at most 20 documents per block counting the authored ones (DOCUMENTS_TOO_MANY). The files count against your workspace storage and are freed once the requests referencing them fall out of the form's retention window. The submission records the list the recipient saw under the block's field key, so the callback tells you exactly which files this person was given.

The rest of the options

Test mode

Pass test: true to exercise the whole shape before a real run. A test request is real in every way that matters for wiring: the link opens and can be completed, the callback fires as usual, and requests.get returns the answers. What it never does is reach anyone or anything you would have to clean up afterwards:

Try it yourself on the Share sheet is this mode with a single click: it takes the Manual tab's draft — prefill, locks, context, callback, expiry — addresses the request to your own account, sends no email, and hands you the link to open yourself.

What a request costs

Every plan has one monthly allowance shared by both channels: a share-link submission spends one unit, and so does every request you create — whether the recipient answers, ignores it, or you cancel it. The submission a request collects is already paid for and counts nowhere. Free includes 1,000 units a month, Pro and Business 50,000; the count resets on the 1st of each month, UTC. At the cap, requests.create fails with UPGRADE_REQUIRED and reason MONTHLY_ALLOWANCE_REACHED; requests you already created stay answerable.

On Free, a request created with "delivery": "email" also spends one of the account's{' '} 10 free invitations. They never reset; once they are gone, email delivery fails with UPGRADE_REQUIRED and reason FREE_INVITATIONS_USED.

Idempotency

Pass the same idempotencyKey with the same body and you get the original request back, with deduplicated: true{' '} and the original link — no second request, no second email. Reuse the key with a different body and formbase refuses with{' '} IDEMPOTENCY_CONFLICT rather than guessing which one you meant. Keys are scoped to the workspace and honoured for 30 days; after that the same key starts a new request.

In a workflow tool, the execution id is the natural key: a run that is retried after a network blip picks up the request it already created.

Rate limit

requests.create and documents.create share a budget of 60 calls a minute, counted per API token (or per user, for a call made without one). A backlog you are draining should pace itself; a burst over the budget is refused and can be retried.

Custom domains

If the form is already published on one of your custom domains, request links are minted there automatically — https://forms.yourcompany.com/r/rq_…. Name domainId explicitly when the form is published on more than one. The domain must belong to the same workspace as the form.

What the recipient sees

Exactly the form you authored — same theme, same logo, same language — with their values in place, locked fields read-only, and no captcha to solve. When they submit, they get your thank-you page. If they come back to the link afterwards, they get the outcome page instead of a blank form.

There is no message from your automation on the page. Anything the recipient needs to be told belongs in the form itself, where you can personalise it by mentioning a context value or a prefilled field.

An AI agent runs the same two steps as fields_list and request_create, with the same options — documents and{' '} domainId included. See Requests on the MCP server.

- [Field keys](/requests/field-keys) — Where those keys come from and how to keep them stable. - [Callbacks & signing](/requests/callbacks) — What arrives when the recipient is done. - [Troubleshooting](/requests/troubleshooting) — Every rejection reason and what to do about it. - [API reference](/developers/rest-api) — Full parameter list for every request method.
# Field keys The stable names automations use to address your fields — where they come from, and how to keep them stable. ## Field keys A field key is the developer-facing name of one field in one form — company_name, contacts. It is how an automation prefills a field, locks it, or reads the answer back, and it survives retitling and duplicating.

Why they exist

Without field keys, an automation has to address your questions by internal ids that mean nothing to anyone — and a webhook payload arrives full of them. With field keys, both sides read the same way:

``` { "company_name": "Acme", "employees": 120, "contacts": [{ "name": "Ada" }] } ```

Field keys are used in two places: prefill, context, and readonly when creating a request; and the{' '} answers and display maps in every callback and every webhook payload.

Where to find them

Every key the form publishes lives in one place, the Keys table: each field, and under a choice question each option, under a matrix each row and column. It ends with a preview of the answers object your callback will carry, with your keys in it.

Each option's hint row — the grey line under the option you are editing — ends with its key as a chip. Click the chip to edit the key in place, press Enter or Escape when done. A matrix shows the chip for the row or column whose label you are editing. A chip turns amber when the form is published and the key you typed would move the one automations already use.

The same table appears read-only where you wire an integration: under the webhook's field mapping, and under the request snippets in the Share sheet's Requests card. You can also read every key at once with fields.list.

How a key is derived

You do not have to set anything. Until you edit it, a key is derived from the question's title: accents are folded and letters such as ø, æ and ß become o, ae and ss, everything is lowercased, each run of anything that is not a letter or digit becomes a single underscore, leading and trailing underscores are dropped, and the result is cut to 64 characters.

A title written only in a non-Latin script, such as Arabic, Hebrew, Cyrillic, Greek, Chinese or Japanese, has nothing to fold, so its key is field_ plus its position, and an option's is option_ plus its position. These keys are as stable as any other once published, but they say nothing about the question. When an automation reads the answers, set a readable key yourself in the Keys table.

If two fields would end up with the same key, formbase breaks the tie in document order by appending _2, _3, and so on. A key you set yourself always wins its claim, and the derived one yields.

Setting your own key

Type a name in the Field key input to override the derived one. Clearing the input goes back to the derived key. Allowed characters are letters, digits, _, . and -, up to 64 characters — anything else is refused with{' '} "Use only letters, numbers and _ . - (max 64 characters)." Spaces are turned into underscores as you type, so "contact name" becomes contact_name.

Keys are case-sensitive and must be unique within one form. Reusing one already taken by another question, repeating group, hidden field, or calculated field is refused with "Another field already uses this key."

Keys freeze at first publish

> ⚠️ **Renaming a published key breaks automations** >

> On a published form, the Field key input warns you that the form is published and automations using the current key will break. Nothing > stops you, but every workflow that prefills or reads that key stops matching the moment you publish the change. Update the automation in > the same sitting. Publish warns you again before the change goes live. >

The first time you publish, every field's key is written into that published version. Every later publish carries the same keys forward, which means:

Publishing is also where key collisions are caught, and both of them stop the publish rather than silently renaming a field:

Both show up beside the Publish button with every other pre-flight finding — see{' '} Publish checks.

When a published key is about to disappear

A key belongs to the field it was published on, not to its title. So there are two ways to lose one without meaning to: type a different key on a published field, or delete a question and add a new one in its place. The new question is a new field — it gets a fresh key derived from its own title, and the old key is gone.

Nothing fails on formbase's side when that happens. The webhook still fires and the callback still arrives, only without that answer, and{' '} requests.create starts refusing the old key with UNKNOWN_FIELD_KEY. So publish checks for it first. When a key the live version publishes would not exist in the next one, the publish dialog and the issue indicator beside the Publish button show a warning:

``` Field key "company_name" will no longer exist after this publish. Integrations and requests that use it will stop receiving that answer. A new field "Company" publishes as "company". Set its field key to "company_name" to keep them working. ```

Repeating groups and hidden fields

A repeating group has its own key, and so does each field inside it. Automations address the group as a whole and nest the members:

``` { "contacts": [{ "name": "Ada", "email": "ada@acme.com" }, { "name": "Grace", "email": "grace@acme.com" }] } ```

A field inside a group is only reachable through its group — there is no top-level name here, only{' '} contacts[0].name. Rename the group's key and the whole array moves; rename one member's key and only that name inside each object changes.

A hidden field's parameter name is its field key. That is the key you put in{' '} context when creating a request, and the same name you would use in a URL parameter on a public link.

A calculated field's name is its field key, and it shares the form's one set of keys with everything else. It is read-only: the form works out its value, so you can never send one — fields.list lists it with{' '} calculated: true and requests.create refuses it in prefill and context. You do read it back: it arrives in answers under its name (answers.total). Renaming a published calculated field renames its key, with the same publish warning as any other field.

Option keys: the choices inside a question

The parts of a question that an answer names get keys too. Every option of a radio, select, checkbox, picture choice or ranking question, and every row and column of a matrix, has an option key: derived from its label the same way a field key is derived from a title, editable, and frozen at publish. So an answer reads as a name on both sides:

``` { "plan": "pro", "interests": ["billing", "api"], "satisfaction": { "delivery_speed": "very_good" } } ```

A workflow branches on answers.plan == "pro" whatever language the respondent answered in, and a request prefills a choice with {'{ "plan": "pro" }'}. The keys are listed by fields.list under each field's options, or a matrix's rows and columns, and edited in the Keys table or on the option's chip. Each question's options are their own namespace, so two questions may both have a yes, and a matrix row and column may share a key. Two options of one question that would derive the same key are told apart with _2, like fields.

The decision question is an ordinary radio whose three options carry the keys{' '} approve, decline and changes; that is what makes a request's outcome a closed set.

- [Create a request](/requests/creating-requests) — Put those keys to work. - [Webhook reference](/developers/webhooks-reference) — How field keys shape every event payload.
# Decisions & approvals Ask for a verdict — approve, decline, or changes — and let your workflow branch on it without reading labels. ## Decisions & approvals Plenty of requests exist to get one thing: a yes, a no, or a 'not like that'. The decision question captures that verdict in a shape your automation can branch on directly, without guessing which field held the answer.

Why a special question

Every choice already has an option key, so a workflow can branch on any radio. What a workflow cannot do is know, across every form that has an approval, which key means "yes": one author's radio says sign_off, another's approved.

The decision question fixes the names. It is a single-choice question with the field key decision, whose three options carry the option keys approve, decline and changes. Its labels stay yours to reword and translate; the keys underneath never move. When the recipient picks one, the request ends with an outcome. Any radio keyed decision whose options carry those three keys counts — the preset just sets them for you.

Add one to your form

/ in the editor and pick Decision — "Approve, decline, or request changes".', }, { title: 'Reword the labels', description: 'You get a question titled "Do you approve?" with the choices Approve, Decline, and Request changes. Rewrite any of them to your own wording — "Sign off", "Reject", "Send back for edits". Translate them like any other content.', }, { title: 'Ask why, if you need to', description: 'A verdict alone rarely explains itself. Add a long-text question after it, and show it only for decline and changes with conditional logic.', }, { title: 'Publish', description: 'Requests created from this published version end with an outcome. Like every field key, decision only exists once the form is published.', }, ]} /> > ⚠️ **Only the preset produces an outcome** >

> The outcome is read from the question whose field key is decision, and only when the chosen option's key is one of the > three. A radio you build by hand with options labelled Approve, Decline and Changes derives exactly those keys, so it works without the > preset; a radio with other labels works once you type the three keys in the Keys table. >

>

The three values

These three values are a public contract, like a field key: an automation, an agent, and a spreadsheet formula all read them.{' '} Add a fourth option and picking it is an ordinary answer, not a verdict — that request completes with no outcome, rather than with a value your receiver has never heard of.

Where the outcome shows up

``` { "id": "evt_...", "type": "request.completed", "test": false, "data": { "request": { "id": "kd7...", "externalId": "run-42", "status": "completed", "outcome": "changes", "context": { "case_id": "CASE-9" } }, "answers": { "decision": "changes", "reason": "Please split the line items per site." } } } ```

Branch on data.request.outcome. status says whether the request finished; outcome says what the recipient decided. An expired or canceled request has a status and no outcome, and so does a completed request on a form without a decision question.

From an agent

The MCP tool is editor_insertDecisionQuestion, taking a title and the three labels in the form's language. An agent that cannot host a callback endpoint polls request_get instead and reads outcome when the status leaves{' '} pending. Requests on the MCP server →

- [Callbacks & signing](/requests/callbacks) — The full payload and how to verify it. - [The Requests page](/requests/managing-requests) — Filter by outcome and follow one request. - [Conditional logic](/building-forms/conditional-logic) — Ask for a reason only when it is needed. - [Question types](/building-forms/field-types) — Every other question you can ask.
# Invitations, reminders & expiry Let formbase email the request link, chase people who go quiet, and close requests that go stale. > ✅ **Pro feature** — included in the Pro plan. ## Invitations, reminders & expiry Delivering the link, chasing the people who go quiet, and deciding when a request stops being worth waiting for. > ℹ️ **Two ways to deliver a link** >

> Every request comes back with a url. You can send it yourself — in your own email, a Slack message, an SMS — or ask > formbase to email it. Sending it yourself works on every plan. Letting formbase send invitations and reminders requires a{' '} > Pro or Business plan; a Free account can have formbase email its first 10 invitations to try it (see{' '} > free invitations). >

The request invitation

Create a request with delivery: "email" and a recipient email address, and formbase sends the{' '} request invitation: a short email carrying a button that opens the request link.

You author it once per form, not once per request. In the editor, open Settings → E-mail Notifications and scroll to the{' '} Requests group at the bottom:

There is no free-text message from the caller — an automation cannot inject a paragraph into this email. What it can do is supply values: mention a context field or a prefilled field in the subject or body and each recipient gets their own version. That keeps the copy under your control while still reading as if it were written for them.

> 💡 **Translate it** >

> If the form has published languages, the invitation follows the request's language. Translate the email copy alongside the > rest of the form — see Translating form content. >

Delivery status

Reminders

A reminder schedule is an ordered list of idle offsets — 1 day, 3 days, 1 week — after which formbase emails someone who has gone quiet. One schedule serves two kinds of target: a public-link respondent sitting on an unfinished draft, and a request recipient who has not completed.

It lives in Settings → E-mail Notifications → Send reminders. That switch is the master: turn it off and neither channel gets the form's schedule.

How the schedule runs

Overriding per request

An automation can pass its own reminders when creating a request — ["2d","5d"] for a schedule of its own, or an empty list to switch reminders off for that one request. An explicit schedule wins even over the form's master switch, because a caller that passed one asked for reminders on this request specifically.

Leave reminders out and the request inherits the form's schedule, if the master switch is on and the request has a recipient address.

Chasing someone now

You do not have to wait for the next step. Send reminder in the request drawer — or requests.remind from an automation — queues one immediately. The automatic schedule is untouched: the next scheduled reminder still lands when it was going to.

Two floors apply: at least ten minutes between manual reminders, and the same overall ceiling of eight per request.

Expiry

Every request has a hard end. When it passes, the link stops working, the status becomes expired, and your automation is told through the callback — so a workflow that has been parked for a month resumes instead of hanging forever.

``` { "method": "requests.create", "params": { "formId": "j57...", "recipient": { "email": "ada@acme.com", "name": "Ada" }, "delivery": "email", "reminders": ["1d", "3d"], "expiresAt": 1795392000000, "callbackUrl": "https://automation.example/webhook/resume-abc" } } ```
- [The Requests page](/requests/managing-requests) — Remind and cancel by hand. - [Troubleshooting](/requests/troubleshooting) — When an invitation fails or bounces. - [Custom email domains](/branding-domains/custom-email-domains) — Send from your own domain.
# Callbacks & signing Resume your workflow when a request ends, and prove the call really came from formbase. ## Callbacks & signing When a request reaches its end — completed, expired, or canceled — formbase POSTs a signed notification to the URL your automation gave it. That call is what resumes the run.

What fires, and when

All three arrive at the same URL, so branch on type before assuming there are answers. That is the whole point of firing on every ending: a workflow parked on a customer resumes whether they answered, ignored you, or were called off.

What arrives

``` { "id": "evt_kj7...", "type": "request.completed", "createdAt": "2026-03-04T09:31:40.000Z", "apiVersion": "2026-09-24", "test": false, "data": { "request": { "id": "kd7...", "externalId": "run-42", "status": "completed", "outcome": "approve", "language": "en", "recipient": { "email": "ada@acme.com", "name": "Ada" }, "metadata": { "runId": "run-42" }, "context": { "case_id": "CASE-9" }, "createdAt": "2026-03-04T09:20:00.000Z", "completedAt": "2026-03-04T09:31:40.000Z" }, "form": { "id": "j57...", "name": "Vendor onboarding", "snapshotId": "kx2..." }, "submission": { "id": "jd7...", "respondentEmail": "ada@acme.com", "submittedAt": "2026-03-04T09:31:40.000Z", "updatedAt": null, "editCount": 0, "pdfUrl": null, "language": "en" }, "answers": { "company_name": "Acme", "contacts": [{ "name": "Ada" }] }, "display": { "company_name": "Acme", "contacts": "Ada" } } } ``` > ⚠️ **Deduplicate on id** >

> id is stable across every retry and every replay of the same event. If your receiver might act twice on the same id — a > duplicate invoice, a duplicate ticket — remember the ids you have already handled. >

Verify the signature

Every callback carries a signature header, X-formbase-Signature: t={unix seconds},sha256={hex}. The hex is an HMAC-SHA256 of the timestamp, a dot, and the raw request body, computed with your workspace's{' '} request signing secret.

Two rules, whatever language you use:

  1. Hash the raw body, before any parsing or re-serializing. Re-encoded JSON is not the same bytes.
  2. Compare in constant time — crypto.timingSafeEqual, hmac.compare_digest — never with ==.
``` import crypto from 'node:crypto' const parts = Object.fromEntries(header.split(',').map((p) => p.split('='))) const expected = crypto.createHmac('sha256', secret).update(`${parts.t}.${rawBody}`).digest('hex') const a = Buffer.from(expected, 'hex') const b = Buffer.from(parts.sha256 ?? '', 'hex') if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return false return Math.abs(Date.now() / 1000 - Number(parts.t)) <= toleranceSeconds } ``` ``` import hashlib, hmac, time def verify_formbase_callback(raw_body: bytes, header: str, secret: str, tolerance_seconds: int = 300) -> bool: parts = dict(p.split('=', 1) for p in header.split(',') if '=' in p) t, received = parts.get('t'), parts.get('sha256') if not t or not received: return False expected = hmac.new(secret.encode(), f'{t}.'.encode() + raw_body, hashlib.sha256).hexdigest() if not hmac.compare_digest(expected, received): return False return abs(time.time() - int(t)) <= tolerance_seconds ```

The request signing secret

One secret per workspace signs every callback from it. Find it on OAuth and API Keys in the workspace sidebar, in the{' '} Request signing secret card. It is masked by default; Reveal secret shows it and the copy button copies it. It is not a one-time value — you can come back and read it again. The secret is minted the first time it is needed, so a workspace that has never opened that card and never created a request with a callbackUrl has none yet.

> ❗ **Regenerating has no grace period** >

> Only the workspace owner can regenerate the secret, and the moment they do, the old one stops working — including for callbacks that are > already being retried. Update your receiver first, then regenerate. There is no window where both secrets are accepted. >

Retries

A callback gets eight attempts: the first, then seven retries at least 1, 2, 4, 8, 16, 32 and 60 minutes apart. formbase looks for due retries every 30 minutes, so a retry can come up to half an hour after its gap ends, and the last attempt comes about four hours after the request ended. Every attempt carries the same bytes and the same id: the payload is frozen at the moment the request ended, so a retry describes what happened then, not what the request looks like now. The destination URL and the signing secret are read at each attempt, not frozen with it.

If the budget runs out — your receiver was down for the afternoon — the callback is not lost. The request gets a{' '} Callback failed badge, the workspace owner is emailed once with the host, the reason and the attempt count, and the answers stay readable through requests.get. To push it again, open the request in the{' '} Requests page and press Replay, or call requests.replayCallback. It re-sends the same frozen payload with the same id, which is exactly what a receiver that deduplicates wants.

Subscriptions hear the same events

A callback URL belongs to one request. When every request on a form should reach the same receiver, subscribe once instead: the formbase apps for Zapier and n8n do this for you, and webhooks.create does it from code with request_completed,{' '} request_expired or request_canceled as the event type. A subscription receives this same envelope, signed with its own secret rather than the workspace request signing secret, with its own event id and its own retry budget. A request that has both a callback URL and a matching subscription fires twice, once to each. Replay re-sends the callback only; a subscription retries on its own and pauses after five failed attempts.

> 💡 **A resume URL is not authentication** >

> Workflow tools hand you a hard-to-guess resume URL and it is tempting to treat that as proof. It is a bearer secret — it can leak into > logs, and it does not tell you the body was not tampered with. Verify the signature in the resumed branch as well. >

- [Troubleshooting](/requests/troubleshooting) — When a callback keeps failing. - [Webhook reference](/developers/webhooks-reference) — The answers and display maps a completion carries, in full.
# The Requests page See everything you are waiting on, follow one request through its timeline, and act on it. ## The Requests page However a request was created — by hand in the Share sheet or by an automation — it is yours to watch. The Requests page in the sidebar shows everything you are waiting on from customers, across every form in the workspace.

The list

Click Requests in the sidebar, between Forms and Trash. Each card is one request: the recipient, the form it belongs to, its status, and — for a pending one — where it has got to.

Sort by Created, Last activity, or Name — click the field again to flip the direction. Search matches the recipient name, the recipient email address, and the external id. Filter by status (Pending, Completed, Expired, Canceled), by outcome (Approved, Declined, Changes requested), by form, or by{' '} Callback failed when you are hunting for deliveries to replay. Test requests are hidden until you tick{' '} Show test requests in the same filter.

Two shortcuts land you here pre-filtered: Open Requests on the Share sheet's Requests card, and the "pending requests" link above a form's submissions list.

One request in detail

Click a card to open the drawer. Its address bar keeps the request id, so you can share the link with a colleague or bookmark it.

What the drawer shows

> ℹ️ **Where the timeline comes from** >

> It is derived from the request's own timestamps plus its delivery attempts. Delivery rows are cleaned up after 30 days, so an old > request's timeline thins back to the milestones — created, opened, completed — while the email and callback lines drop away. >

What you can do

Cancel, replay, and copy link work on every plan. Cancelling asks first; after it, the recipient sees a withdrawn notice instead of the form. Everything else acts immediately and confirms with a toast.

Sending yourself a test

Try it yourself on the Share sheet's Requests card creates a request in{' '} test mode from the Manual tab's draft and shows you its link. Nothing is emailed: open the link yourself to walk through the form as the recipient would. The request shows in this list, with a Test badge, once you tick Show test requests.

- [Troubleshooting](/requests/troubleshooting) — What to do when something failed. - [Invitations & reminders](/requests/invitations-and-reminders) — Set the schedule these actions work with.
# Troubleshooting requests Rejected calls, invitations that never arrived, and callbacks that never landed. ## Troubleshooting requests Where to look when a request was refused, an invitation never arrived, or a workflow is still waiting for a callback that already happened.

Reading an error

Every rejection carries two things: a code for the kind of failure, and a details.reason for the specific cause. Branch on the code; read the reason to know what to fix. Where it helps, details also names the offending field, the keys that would have been accepted, or the option keys a choice question takes.

The request methods use four codes: VALIDATION_ERROR (the call was wrong), CONFLICT (the request is in the wrong state, or an idempotency key was reused), NOT_FOUND, and UPGRADE_REQUIRED (a plan gate or the monthly allowance). Calling too fast returns RATE_LIMITED instead, with retryAfterMs — see{' '} the rate limit.

``` { "ok": false, "error": { "code": "VALIDATION_ERROR", "message": "No field with key \"company\".", "details": { "reason": "UNKNOWN_FIELD_KEY", "field": "prefill.company", "validKeys": ["company_name", "company_size"] } } } ```

Reasons you might see

Getting the call right

Acting on an existing request

The invitation never arrived

Open the request in the Requests page and read the timeline. The first invitation line tells you which case you are in.

> ℹ️ **No timeline entry at all** >

> Then no email was ever asked for. The request was created with delivery: "none" — deliver the link yourself, or create a > new request with delivery: "email". >

Invitations and reminders share a budget of ten emails per day per form and recipient address, and a request never emails its recipient more than nine times in its life — one invitation and up to eight reminders.

The callback never landed

The request drawer's Callback section shows the URL and the outcome. Callback failed after N attempts means formbase tried and gave up — eight attempts over about four hours.

  1. Check the URL. It is shown in the drawer. A workflow tool's resume URL belongs to one run, and a run that was deleted or re-created no longer answers on it.
  2. Check what your endpoint returned. Anything outside 2xx is a failure. A 4xx other than 408 or 429 stops the retries immediately — formbase reads it as "your endpoint rejected this", and re-sending identical bytes cannot change that.
  3. Fix the receiver, then press Replay. The same payload goes out again with the same event id, so a receiver that deduplicates is safe.
> ⚠️ **Signature check failing?** >

> Almost always the raw body. If you parse the JSON and re-serialize it before hashing, the bytes differ and the signature will never > match. Hash the body exactly as it arrived. The other common cause is a regenerated signing secret that the receiver has not picked up — > there is no grace period. >

Other things people hit

- [Callbacks & signing](/requests/callbacks) — Retries, replay, and how to verify. - [The Requests page](/requests/managing-requests) — Where the timeline and the actions live.
--- ## Public Link # Choosing a channel A published form reaches people two ways — a public link for anyone, or a request for one named person. How to pick. ## Choosing a channel One form, two ways out: a public link anyone can open, or a request addressed to one person. You can use both, and the form itself never changes.

The two channels

A channel is one of the two ways a published form reaches people. You choose it in the Share sheet:

Nothing about the form changes between the two. The same questions, logic, theme and settings serve both. A form can use both at once, and a form's channel is never a setting you declare — it is simply observed from the share links and requests that exist.

Which one to use

What requests add

A request is addressed, so it can carry things a public link cannot:

> ⚠️ **A request link ignores URL parameters** >

> Request links do not read ?param= values at all. The recipient holds that link, so honouring parameters would let them > rewrite what the caller set. On a request, the caller's context is the only source. >

Both channels spend the same allowance

Your workspace has one monthly allowance shared across both channels. A submission collected through a share link spends one unit, and{' '} creating a request spends one unit — whether or not the recipient ever answers. The submission a request collects is already paid for by the request and is not counted again. See{' '} limits & quotas.

Telling them apart afterwards

The submissions table has a Source column showing Public link or Request for each row, and you can filter by it. Analytics covers both channels together with a channel filter rather than separate pages, and adds a request funnel when requests exist. See analytics & insights.

> ℹ️ **Step by step in Zapier and n8n** >

> Send public link submissions to another app sends each submission to email, Sheets, > Slack or a CRM through Zapier. Send a request from a Zap does the same for requests. The > n8n guides do both in a self-hosted n8n. >

Next steps

- [Sharing & embedding](/sharing-publishing/sharing-embedding) — Set up the public link channel - [Requests overview](/requests/overview) — Ask one named person - [Hidden fields](/building-forms/hidden-fields) — Carry data the respondent never sees - [Limits & quotas](/subscription-billing/limits-quotas) — What each channel spends
# Share links Create, manage, and track share links for your form. ## Share links Every published form gets a unique link you can share directly. Create multiple links per form — each with its own name, expiration, response limit, and analytics — so you can track which channel drives the most completions.

A share link is how respondents get to your form. Each link has its own 8-character code and its own settings, so you can create separate links for different campaigns, channels, or audiences.

> ℹ️ **Two channels, one sheet** >

> The Share panel has two tabs. Public link — this page — is one link anyone can open. Requests sends > the form to one named recipient who submits once. See creating requests. >

Link name and Custom URL sit in the always-open Link section. The rest live in three collapsible sections below it: Appearance, Limits, and Query parameters.

Query parameter defaults

The Query parameters section bakes default values into the link URL. formbase always shows the three UTM fields it reads for analytics (utm_source, utm_medium, utm_campaign), plus a row for every hidden field on the form. Fill in a value, leave the rest empty, and formbase appends it to the copied link and the QR code automatically. Embed and popup snippets don't include these defaults, and a parameter named lang is ignored because the default language uses it.

This saves you from editing the URL by hand. Set utm_medium to "email" on your newsletter link once, and every copy carries it.

A hidden-field value set here also fills any visible field that uses that hidden field as its{' '} default value, so one link can pre-fill answers for a whole audience.

> ℹ️ **What gets saved where** >

> Default parameters are added to the link URL, not to each response. The three UTM fields feed the{' '} > Traffic Sources chart in Analytics. To save a > value on every submission, add a hidden field with the same name (for example utm_source) — > it then shows in both the submissions table and analytics. >

Click Revoke in the Share panel footer. Anyone who opens the link then sees "Link revoked", and the link stops taking new responses. Its settings can't be changed while it's revoked. Changed your mind? Click Re-activate in the same footer. Revoking keeps the responses the link already collected, and it doesn't affect the other links for the same form.

Moving a form to Trash revokes all of its links. Restoring the form leaves them revoked until you click Re-activate on each one.

Delete a link from the ⋯ menu in the Share panel footer. If the link has no submitted responses yet (unfinished drafts don't count), it's removed for good.

If the link already has responses, formbase revokes it instead of deleting it. A prompt explains this before anything happens. Revoking keeps the existing data — your analytics and per-channel breakdown stay intact — but stops the link from taking new responses. This way you never lose responses or the channel they came from just by clearing out an old link.

Creating a separate link per channel lets you compare performance in the Analytics tab. Filter by share link to see views, submissions, and bounce rate for each channel independently. Common patterns:

UTM parameters for campaign tracking

Append UTM parameters to any share link URL to tag traffic for the Analytics tab's Traffic Sources chart. formbase reads three standard parameters:

``` https://forms.yoursite.com/feedback?utm_source=linkedin&utm_medium=social&utm_campaign=product-launch ```

Source and medium are matched case-insensitively; campaign names keep their case. Without UTM parameters, formbase falls back to the browser referrer to classify traffic — but some platforms strip referrers, so UTM parameters give you reliable attribution. See{' '} How traffic sources are classified for the full rules.

To skip editing the URL by hand, set these as defaults in the link's Query parameters section.

QR codes

Every share link can generate a QR code. It encodes exactly the URL the Copy link button gives you, including any query parameter defaults and the language pin, so expiration, response limit, and analytics all apply the same way.

Print it on a sign, a slide, packaging, or a receipt. Create one share link per placement (for example qr-event-london,{' '} qr-product-package) so the Analytics tab shows responses per placement.

> 💡 **Test before printing** >

> Always scan the printed QR with the camera you expect respondents to use. Some printers crush dark areas — test at the intended print > size before distributing. >

Social preview metadata

When a share link is posted on social media or messaging apps, formbase generates an Open Graph preview with the form title and description. You can customize the preview title, description, image, and favicon per form under{' '} Appearance → Share link preview in the Share panel.

If you use a custom domain, you can also set domain-level defaults that apply to all forms on that domain. A "Hide from search engines" toggle adds a noindex tag, at the domain level or per form (the form setting wins). On the default formbase.so domain, search engine indexing is always disabled, so the toggle only takes effect on custom domains.

> 💡 **Test your link before distributing** >

> Open the share link in an incognito window to experience exactly what respondents will see — including authentication gates, password > prompts, and the default language setting. >

Next steps

- [Embedding & popups](/sharing-publishing/embedding-popups) — Embed or show your form as a popup - [Custom domains](/branding-domains/custom-domains) — Serve forms from your own domain - [Share link analytics](/submissions-analytics/share-link-analytics) — Compare performance per link - [Response limits](/sharing-publishing/response-limits) — Cap how many responses a link accepts
# Embedding & popups Embed your form on a website or show it as a popup overlay. ## Embedding & popups Put your form inside any webpage as an inline embed or a popup overlay. You configure it in the Share panel and paste one snippet into your site.

Both snippets are built from the share link you have selected, so they carry that link's expiration, response limit, custom domain, and analytics. Create the link first — see share links. Query parameter defaults set on the link are the one exception: they are not added to embed or popup snippets.

Inline embed

An inline embed places your form directly inside a page using an iframe. Open the Share panel, click the{' '} Embed card, and copy the generated snippet into your site's HTML.

Layout

Display

Click View code to see the generated HTML. Use Copy embed code to copy it, then paste it into your site.{' '} Reset to defaults restores every setting.

What the snippet contains

With auto-resize off, the snippet is a single iframe pointing at your share link with ?embed=1 appended. The display options add their own parameters: hideCover=1, hideLogo=1, and theme=dark or theme=light.

```html ```

With auto-resize on, the iframe is wrapped in a div and followed by a script. The embedded form posts a{' '} formbase:resize message on every height change; the script checks the message origin and the sending frame, then sets the iframe height. Nothing else on your page is touched, and no external script is loaded.

A popup shows your form as an overlay on top of your page. Open the Share panel, click the Popup card, set up your options, and paste the generated code snippet into your site.

Choose how the popup opens:

Click View code to see the generated snippet. Use Copy popup code to copy it, then paste it into your site. Reset to defaults restores every setting.

The popup snippet is one self-contained {'