# Create a form and a request in one call

Send the blocks with the request: Formstep builds the form, publishes it and returns the link, with no editor in between.

## 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

```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:

```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

| Field | What it does |
| - | - |
| name | Required, 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. |
| blocks | Required, 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. |
| language | The form's UI language. Default: English. |
| logo, cover, emoji | Your 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. |
| theme | A 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. |
| settings | The 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. |
| translations | The 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](https://docs.formstep.io/developers/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.

| Kind | Types |
| - | - |
| Questions | text, contact, number, date, time, radio, checkbox, select, switch, rating, linearScale, matrix, ranking, pictureChoice, file, signature, decision |
| Documents | documents, to show files the recipient reads or signs |
| Payments and bookings | payment, scheduleAppointment |
| Content | header, 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. |
| Structure | pageDivider, 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:

```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](https://docs.formstep.io/building-forms/payment-collection/#connect-stripe); 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](https://docs.formstep.io/integrations/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.

| Field | Takes |
| - | - |
| logo | An icon from the builder, { "type": "icon", "name": "HeartIcon" }, or your own image, { "type": "image", "url": "https\://…/logo.png" }. |
| cover | A 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. |
| emoji | One 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.

> **Personalise through prefill and context**
>
> Put the customer’s name, the invoice number or the amount in `prefill` and `context`, never in the form name, a title or a fixed amount. A definition that changes per customer makes a new form per customer. Set `key` on every question: a derived key follows the title, so a retitled question answers under a new key. A workspace makes at most 300 new inline forms a day; past that, a call that would make a new form fails with `RATE_LIMITED`, while calls that reuse a form keep working.

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](https://docs.formstep.io/requests/managing-requests/#delete), 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.

| Reason | What to do |
| - | - |
| FORM\_SOURCE\_MISSING / FORM\_SOURCE\_AMBIGUOUS | Send exactly one of formId and form. |
| INLINE\_FORM\_INVALID | A block or a field of form fails its schema. details.issues names each one. |
| INLINE\_FORM\_DUPLICATE\_KEY | Two blocks resolve to the same key. Give one of them its own key. |
| INLINE\_FORM\_UNKNOWN\_KEY | A logic rule, amountFromField or a booking prefill names a key no block has. |
| INLINE\_FORM\_AMOUNT\_MISSING | Pass a positive number for the amount field in context. |
| INLINE\_FORM\_CONNECTION\_MISSING | Stripe or Cal.com is not connected. details.connectUrl is the page a person opens once to connect it. |
| INLINE\_FORM\_EVENT\_TYPE\_NOT\_FOUND | No Cal.com event type matches. The error lists the ones there are. |
| INLINE\_FORM\_IMAGE\_UNREACHABLE | An 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\_MISSING | A translation lacks strings the form has. details.missing lists them, as written in the blocks. |
| INLINE\_FORM\_PUBLISH\_ISSUES | Publishing 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`.

[Creating a request](https://docs.formstep.io/requests/creating-requests/) — Prefill, lock and context for any form.

[Field keys](https://docs.formstep.io/requests/field-keys/) — How keys are derived and kept stable.

[Callbacks & signing](https://docs.formstep.io/requests/callbacks/) — Receive the answers when the request ends.

[Payment collection](https://docs.formstep.io/building-forms/payment-collection/) — Connect Stripe for the workspace.
