Formstepdocs
Go to appApp

Requests

Create a form and a request in one call

An agent on the phone with a customer finds out she still has to upload two payslips, pay a 40 EUR fee and book a call. It sends the form and the request in the same call, and Formstep takes it from there.


When to use it

A request normally points at a form someone built in the editor and published. When the questions depend on the case, the caller sends them along instead: requests.create takes form in place of formId. Formstep builds an ordinary form from the blocks, publishes it and creates the request in one step, so both exist or neither does. We call it an inline form.

Keep using formId for a form you send to many people in the same shape and want to design by hand in the editor. Use form when an automation decides what to ask.

The call

requests.create with a form
bash
curl -X POST https://api.formstep.io/api/v1 \
  -H "Authorization: Bearer $FORMSTEP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"method":"requests.create","params":{
        "form":{
          "name":"Documents and fee for your mortgage application",
          "logo":{"type":"image","url":"https://acme.example/logo.png"},
          "cover":{"type":"color","color":"#0f4c81"},
          "emoji":"🏦",
          "blocks":[
            {"type":"paragraph","content":"Upload the two documents and pay the processing fee."},
            {"type":"file","key":"payslips","title":"Your last two payslips","maxFiles":2},
            {"type":"payment","key":"fee","title":"Processing fee","amount":40,"currency":"EUR"},
            {"type":"scheduleAppointment","key":"call","title":"Pick a time for our call","eventType":"15 min call"}]},
        "recipient":{"email":"ada@example.com","name":"Ada"},
        "delivery":"email",
        "idempotencyKey":"call-8841"}}'

Everything else about the request keeps its meaning: recipient, prefill, context, readonly, delivery, reminders, expiry, callbackUrl, test and idempotencyKey are checked against the new form exactly as against one built by hand. The response adds the form, with the keys its answers come back under:

response, trimmed
json
{
  "id": "kd7...",
  "status": "pending",
  "url": "https://form.formstep.io/r/rq_...",
  "form": {
    "id": "j58...",
    "name": "Documents and fee for your mortgage application",
    "reused": false,
    "fields": [
      { "key": "payslips", "type": "file" },
      { "key": "fee", "type": "payment" },
      { "key": "call", "type": "schedule-appointment" }
    ]
  }
}

To prefill a choice question in the same call, send option keys, as for any form. An option’s key is derived from its label, so Self-employed becomes self_employed. A label instead of a key fails with INVALID_PREFILL_VALUE, which lists the valid keys.

The form object

FieldWhat it does
nameRequired, 1 to 100 characters. The name on the dashboard and in the browser tab, the same for every recipient. Personalise through prefill and context, never through the name.
blocksRequired, 1 to 200 blocks, in the order the form shows them. The blocks in a row's columns and a repeating group's fields count too.
languageThe form's UI language. Default: English.
logo, cover, emojiYour brand, sent with every request. logo: { "type": "icon", "name": "HeartIcon" } or { "type": "image", "url": "https://…" }. cover: { "type": "color", "color": "#0f4c81" } or { "type": "image", "url": "https://…", "offsetY": 50 }. emoji: one emoji beside the name. See below.
themeA look per color mode: { "light": "solarized-light", "dark": "dracula" }. Each mode takes a preset id, free on every plan, or a whole theme object as the MCP tool formTheme_set takes it, which needs the Pro plan.
settingsThe form behaviour the settings page sets: branding, a verified email domain (emailDomainId), owner and respondent emails with their copy, the reminder schedule, the invitation email, the redirect and retention. Fields by key: respondentNotificationTo is the key of an email question, redirectQueryParams maps a parameter to a field key. Email copy is plain text with {{key}} placeholders. No password or sign-in gate and no share-link setting such as bot protection or edits after submit: a request carries its own link. Any other key is refused.
translationsThe form in more languages, with a language switcher for the recipient: { "de": { "Your name": "Ihr Name" } }, every string of the form keyed by the text as the blocks wrote it. See below.

Blocks

A block takes the parameters of the matching editor_insert* tool on the MCP server, without formId and after, plus an optional key. Without one, the key is derived from the title, so “Full name” becomes full_name. A parameter the block does not know is refused rather than dropped.

KindTypes
Questionstext, contact, number, date, time, radio, checkbox, select, switch, rating, linearScale, matrix, ranking, pictureChoice, file, signature, decision
Documentsdocuments, to show files the recipient reads or signs
Payments and bookingspayment, scheduleAppointment
Contentheader, paragraph, image (src URL), embedded, table (cells fills it), list, row (columns: the blocks of each column, 2 to 6 columns, and widths in percent). An optional key lets logic show or hide the block.
StructurepageDivider, repeatingGroup (fields: the member questions as blocks, each with its key), hiddenField (key required: it is the context key), calculatedField (its name is its key), logic

Header and paragraph text, list items and table cells read bold, italic, struck, <u>underlined</u>, code, $$equation$$, text, <span color=“red” background=“yellow” font=“font-georgia” size=“1.25”>…</span> with the editor toolbar’s colors, fonts and sizes, and {{key}}, which shows the value of the field with that key. Marks nest; an unmatched marker is text. A column of a row holds any block except pageDivider, logic, hiddenField, calculatedField, repeatingGroup, row and scheduleAppointment. A decision block takes its title and labels for approve, decline and changes in the form’s language.

Blocks refer to each other by key, never by element id: a logic block takes its rule as editor_insertLogic does, with field keys where it wants ids, option labels as condition values and page names for setNextPage.

Payments

A payment block takes amount and currency, or amountFromField: the key of a hidden field or a number question. With a hidden field, each request passes its own amount in context, so one form charges every customer their own amount:

one form, an amount per request
json
{
  "form": {
    "name": "Pay your invoice",
    "blocks": [
      { "type": "hiddenField", "key": "invoice_total" },
      { "type": "payment", "key": "invoice", "title": "Invoice", "currency": "EUR", "amountFromField": "invoice_total" }
    ]
  },
  "recipient": { "email": "ada@example.com" },
  "context": { "invoice_total": 1200 }
}

A number question that sets the amount must be required and have a min above 0, so nobody reaches checkout with an empty, zero or negative amount.

The form charges through the workspace’s default Stripe account, or the one connection names. A person connects Stripe once for the workspace; an automation cannot. Without a usable account the call fails and tells you where to connect it.

Bookings

A scheduleAppointment block takes eventType, the title or id of an event type on the workspace’s Cal.com connection, and connection when the workspace has several. Formstep looks the event type up when the request is created. prefill: { name, email } names the questions whose answers fill in the booking.

Your brand

An automation cannot invent your look, so you give it to it once and it sends it with every request: logo, cover and emoji beside the blocks, and theme for the colors and type.

FieldTakes
logoAn icon from the builder, { "type": "icon", "name": "HeartIcon" }, or your own image, { "type": "image", "url": "https://…/logo.png" }.
coverA solid color, { "type": "color", "color": "#0f4c81" }, or an image, { "type": "image", "url": "https://…/cover.jpg", "offsetY": 50 }, where offsetY from 0 to 100 picks the part of a tall image that shows.
emojiOne emoji, shown beside the form name.

Every image, the src of an image block included, must be a public JPEG, PNG or WebP. Formstep fetches each once when the form is created and keeps its own copy, so the form and the PDFs of its submissions outlive the source. A URL that cannot be fetched fails the call with INLINE_FORM_IMAGE_UNREACHABLE naming the field. Without a brand, the form looks like a new form from the dashboard.

Translations

translations publishes the form in more languages at once, and the recipient gets the language switcher a translated form has: { "de": { "Your name": "Ihr Name", "Order for **{{customer}}**": "Bestellung für **{{customer}}**" } }. Every string of the form must be there, keyed by the text exactly as the blocks wrote it: question titles, options, placeholders, headers, paragraphs, list items, table cells, group names and button labels. Markup and {{key}} stay as in the source. A string missing fails the call with INLINE_FORM_TRANSLATION_MISSING, which lists the strings to add. Email copy is not translated.

One definition, one form

A later call with the same form object reuses the same form, and the response says “reused”: true. A hundred requests with one definition make one form, with one set of answers to export. Changing a block, the name, the brand or a translation makes a new form; requests already sent keep the form they were created with. A form you close for submissions or unpublish is not reused: the next call with the same definition makes a new one.

When a call makes a new form although an open inline form has the same block types in the same order, the response names that form in form.similarForm. Usually a changed name or title caused it. To read every answer to one inline form, list its submissions instead of reading each request.

On the dashboard

An inline form belongs to its request, so it lives on the request’s page rather than on the Forms page. The page lists the answers by field key, and Preview opens the form as the recipient saw it, filled in with their answers. A link to an inline form’s editor or submissions opens the Requests page filtered to that form instead. Agents still see inline forms in form_list, and the API refuses to change one.

An inline form has no share link, and none can be added: its requests are the only way in, and the callback carries every answer. It has no integrations either.

Because it has no page of its own, an inline form is never deleted by hand. It goes with its last request: when you delete a request, or the form’s retention window takes it, and no other request still uses the form, the form is deleted too.

When the call is refused

Each refusal is a VALIDATION_ERROR whose details.reason your automation can branch on. Nothing is created.

ReasonWhat to do
FORM_SOURCE_MISSING / FORM_SOURCE_AMBIGUOUSSend exactly one of formId and form.
INLINE_FORM_INVALIDA block or a field of form fails its schema. details.issues names each one.
INLINE_FORM_DUPLICATE_KEYTwo blocks resolve to the same key. Give one of them its own key.
INLINE_FORM_UNKNOWN_KEYA logic rule, amountFromField or a booking prefill names a key no block has.
INLINE_FORM_AMOUNT_MISSINGPass a positive number for the amount field in context.
INLINE_FORM_CONNECTION_MISSINGStripe or Cal.com is not connected. details.connectUrl is the page a person opens once to connect it.
INLINE_FORM_EVENT_TYPE_NOT_FOUNDNo Cal.com event type matches. The error lists the ones there are.
INLINE_FORM_IMAGE_UNREACHABLEAn image URL is no public JPEG, PNG or WebP. details.field names it. Use a URL anyone can open, or drop the image.
INLINE_FORM_TRANSLATION_MISSINGA translation lacks strings the form has. details.missing lists them, as written in the blocks.
INLINE_FORM_PUBLISH_ISSUESPublishing refused the form, for instance an event type that cannot be booked from a form.

From an agent

On the MCP server, request_create takes the same form. Before building a form with a payment or a booking, an agent reads workspaceConnection_list: it shows the connected Stripe and Cal.com accounts and, when one is missing, the link a person must open once. scheduler_listEventTypes lists the event types a booking can name, and paymentConnection_setDefault changes the default Stripe account. Over the REST API the same three are connections.list, scheduler.listEventTypes and paymentConnections.setDefault.