# Create requests from code

List a form's field keys, create a request with prefill, locked fields and context, hand the recipient documents, and read the answers. The value shapes, idempotency, test requests and the limits that apply.

To try it end to end with curl first, follow [Send a request with the REST API](https://formstep.io/docs/developers/quickstart-send-a-request/). Every parameter is listed under [Requests](https://formstep.io/docs/developers/rest-api/#requests) in the API reference. What a request is, and what the recipient sees, is explained in [Requests](https://formstep.io/docs/requests/overview/).

## Copy a call from the New request sheet

![From an automation in the New request sheet, showing curl and MCP prompt, the form id and a ready-made requests.create call](https://formstep.io/docs/images/requests/request-automation.png)

From an automation: the form id and a call already filled in with this form's field keys and what you filled in.

Open a published form, click **New request** (the paper plane icon), and fill in the sheet as if you were sending it by hand. Then open the **⋯** menu at the bottom of the sheet and pick **From an automation**. It shows the form id and the same request as a curl call or an MCP prompt, with this form's real field keys. **OAuth and API Keys** in the same menu opens the page where you create the [API token](https://formstep.io/docs/developers/api-tokens/).

Only a published form that is open for submissions takes requests. A form that was never published fails with `FORM_NOT_PUBLISHED`, a closed one with `FORM_CLOSED`.

## List the fields

[fields.list](https://formstep.io/docs/developers/rest-api/#fields-list) returns every field of the form's current published version, with its key, its type and a few flags. Read the keys from it instead of guessing them from question titles. The flags tell you where a value goes:

| The field shows | What you send |
| - | - |
| prefillable: true | A value in prefill, in the shape its type asks for (see value shapes below). |
| options, or rows and columns | The option key, not the label. The label is there so you can match the choice you know to its key. |
| context: true | A hidden field. Its value goes in context, never in prefill; in prefill it fails with UNKNOWN\_FIELD\_KEY. |
| calculated: true | Nothing. The form works the value out, and you read it back under its key in answers. |
| prefillable: false (and no other flag) | Nothing. File upload, signature, payment, appointment booking and Documents blocks are for the recipient. |
| type: "group", repeating: true | A repeating group. Its members are listed under members, and you send a list of items. |

### Field keys

A field key is the name your code uses for one field, such as `company_name`. Authors see and edit keys in the editor's **Fields** tab; see [Field keys](https://formstep.io/docs/requests/field-keys/). Until someone edits it, a key comes from the question title: lowercase, accents removed, every space or symbol turned into an underscore, up to 64 characters.

| Question title | Key |
| - | - |
| Company name | company\_name |
| What is your VAT number? | what\_is\_your\_vat\_number |
| Prénom | prenom |
| Søknad om støtte | soknad\_om\_stotte |
| الاسم الكامل | field\_3: no Latin letters or digits, so the key is field\_ plus its position |

- When two fields would get the same key, the later one gets `_2`, `_3` and so on.

- A key set by hand uses letters, digits, `_`, `.` and `-`, up to 64 characters. Keys are case-sensitive and unique within the form.

- From the first publish, a field keeps its key when the question is renamed, moved or given another type. A new type may need a new value shape. A duplicated question gets a new key.

- A key disappears when someone types a new one, or deletes a question and adds another in its place. Callbacks then arrive without that answer, and `requests.create` refuses the old key with `UNKNOWN_FIELD_KEY`. Publishing warns first, and the MCP tool `form_publish` returns the same messages in `warnings`.

- Publishing stops on a clash: two fields with one key (questions, repeating groups, hidden fields and calculated fields share one set of keys), a key already published on another field, two options of one question with one key, or a hidden field whose parameter name is invalid or used twice. A dropped field key or option key is only ever a warning. The same checks run when the API or an agent publishes.

- Each option of a choice question, and each row and column of a matrix, has an option key, derived from its label the same way and unique within its question. A workflow can branch on `answers.plan == "pro"` whatever language the recipient used.

A repeating group has its own key, and so does each field inside it. The members are nested, never top-level:

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

A hidden field's key is also its URL parameter on a share link. A calculated field's name is its key, so its value comes back as, for example, `answers.total`.

## Create the request

```json
{
  "method": "requests.create",
  "params": {
    "formId": "j57...",
    "recipient": { "email": "ada@acme.com", "name": "Ada" },
    "prefill": { "company_name": "Acme", "company_size": "51_200" },
    "readonly": ["company_name"],
    "context": { "case_id": "CASE-9" },
    "metadata": { "crm_record": "rec_88" },
    "delivery": "email",
    "externalId": "run-42",
    "callbackUrl": "https://automation.example/webhook/resume-abc",
    "idempotencyKey": "run-42"
  }
}
```

The reply carries the request `id`, its `status` (`pending`), the recipient's `url`, `deliveryStatus` and `expiresAt`; the full response is under [requests.create](https://formstep.io/docs/developers/rest-api/#requests-create). Store the `url` if you deliver the link yourself.

A request carries four kinds of values. What each one means for the recipient is explained in [Prefill, locked fields and context](https://formstep.io/docs/requests/creating-requests/#three-buckets). The API rules:

| Parameter | Takes | Rules |
| - | - | - |
| prefill | Answers by field key, for visible questions | The recipient can change them. Each value must fit its type. |
| readonly | A list of prefilled keys to lock | Every key must also be in prefill (READONLY\_REQUIRES\_PREFILL), and a locked required field needs a non-empty value (READONLY\_REQUIRED\_EMPTY). |
| context | Values for the form's hidden fields | Each key must be a hidden field on the published form (UNKNOWN\_FIELD\_KEY; a visible question gives CONTEXT\_KEY\_NOT\_HIDDEN\_FIELD). A value is a string, number or boolean. Request links ignore URL parameters, so the recipient cannot change it. |
| metadata | Any object of your own | Never reaches the form. Comes back in every callback and read. |

## Value shapes

Send each value in the shape its `type` asks for. A wrong shape fails with `INVALID_PREFILL_VALUE`, and the error names the key, the expected type and, for a choice question, the accepted option keys.

| Type | Send |
| - | - |
| text, email, phone, url, textarea | A string |
| number, rating, scale | A number |
| switch | true or false |
| date | "2026-03-04" |
| time | "09:30" or "09:30:00" |
| radio, select | The option key, not its label |
| checkbox, ranking, picture-choice | A list of option keys |
| matrix | An object of row key to column key: { "row\_key": "column\_key" } |
| group (repeating) | A list of at most 100 items: \[{ "member\_key": value }, ...] |
| file, signature, payment, schedule-appointment | Nothing; the recipient supplies these |
| any field with calculated: true | Nothing; the form works it out |
| documents | Nothing in prefill; use the documents parameter below |

## Documents for one recipient

A [Documents block](https://formstep.io/docs/building-forms/documents-block/) shows files to the recipient. A request can add files for its one recipient, such as their own contract, below the files every recipient gets. You upload the file to a URL Formstep gives you, so the bytes do not pass through the API:

1. **Reserve the upload**

   Call [documents.create](https://formstep.io/docs/developers/rest-api/#documents-create) with the formId, a name, the contentType, the exact size in bytes and, if you like, a sha256. You get back an id and an uploadUrl that is valid for one hour.

2. **Upload the bytes**

   PUT the file to uploadUrl with the same Content-Type.

3. **Reference it on the request**

   Pass documents: \[{ documentId, field?, name? }] on requests.create. Formstep checks the uploaded file before the request is created.

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

`name` replaces the display name for this request. Set `field` to the block's field key when the form has more than one Documents block. One upload can be used by any number of requests. The accepted types and the size caps are listed under [documents.create](https://formstep.io/docs/developers/rest-api/#documents-create). The callback lists the files this recipient was given, under the block's field key.

## Invitations and reminders

`delivery: "email"` has Formstep email the invitation; it needs `recipient.email`. The default, `"none"`, leaves the link to you. Invitations and reminders need Pro or Business. On Free, an invitation uses one of the [free invitations](https://formstep.io/docs/subscription-billing/limits-quotas/#free-invitations) and goes only to your own address: when they are used up the call fails with `FREE_INVITATIONS_USED`, and another address fails with `FREE_INVITATIONS_OWN_ADDRESS_ONLY`. Your own address is the one you sign in with, or the one your API token or MCP connection belongs to, and spellings of one address count as one: a `+tag` or dots in a Gmail address do not make a new one. A deduplicated call and a test request spend no free invitation. The email itself, and how the reminder schedule runs, are explained in [Invitations, reminders and expiry](https://formstep.io/docs/requests/invitations-and-reminders/).

### Delivery status

| deliveryStatus | Meaning |
| - | - |
| not\_requested | Created with delivery "none". You deliver the link. |
| queued | Accepted, not sent yet. |
| sent | Handed to the email provider. |
| failed | Formstep could not send it, usually a plan or address problem. Fix it, then send a reminder: it carries the same link. |
| bounced | The provider reported a hard bounce or a complaint. Create a new request for the right address; a reminder will not help. |

### Email limits

- One recipient address gets at most 10 emails a day from one form, invitations and reminders together. Over it, the call fails with `RATE_LIMITED` and reason `RECIPIENT_EMAIL_LIMITED`.

- An account sends at most 30 invitations and reminders an hour in its first 7 days, then 200 an hour. Over it, the call fails with reason `INVITATION_RATE_LIMITED`; retry after `retryAfterMs`. A scheduled reminder over it goes out later.

- A request emails its recipient at most nine times: the invitation and up to eight reminders, manual and scheduled together. [requests.remind](https://formstep.io/docs/developers/rest-api/#requests-remind) sends one now, at least 10 minutes after the last.

### The reminders parameter

Without `reminders`, a request with a recipient email follows the form's schedule when **Send reminders** is on. `reminders` replaces that schedule for one request, and applies even when the setting is off:

- Up to five offsets of idle time, in whole days, hours or minutes, each at least one hour: `["2d", "12h", "90m"]`.

- `[]` sends no reminders.

- A schedule of your own needs a recipient email and Pro or Business. A duplicate offset, an offset under one hour or a sixth step fails with `INVALID_REMINDER_SCHEDULE`.

## Other options

| Option | What it does |
| - | - |
| language | The language the form opens in and the invitation is written in: one of the form's published languages, or LANGUAGE\_NOT\_PUBLISHED. The recipient can still switch. |
| expiresAt | When the link stops working, in epoch milliseconds. Default 30 days out, at most 365 (EXPIRY\_OUT\_OF\_RANGE). |
| externalId | Your own id for the request. Comes back on every read and callback, and filters requests.list. |
| callbackUrl | Where Formstep POSTs the event when the request ends. See [Request callbacks](https://formstep.io/docs/developers/callbacks/). |
| domainId | The custom domain the link is minted on. Without it, the link uses the domain the form already has a share link on. Needed when there are several. |
| form | Build the form in the same call instead of sending formId. See [Inline forms](https://formstep.io/docs/developers/inline-forms/). |

### Test requests

`test: true` runs the whole flow without a real recipient. The call is checked like a real one, the link opens and can be completed, the callback fires with `"test": true`, and `requests.get` returns the answers. A test request:

- sends no invitation and no reminders, and costs nothing from the monthly allowance;

- keeps its answers out of the inbox, submission counts, exports, integrations, subscriptions and notifications;

- is hidden on the Requests page until you turn on **Show test requests**, and left out of `requests.list` unless you pass `includeTest: true`;

- closes within 24 hours, whatever `expiresAt` says.

On Free, a workspace can create 10 test requests a day. The next fails with `RATE_LIMITED` and reason `TEST_REQUEST_LIMIT_REACHED`.

### Idempotency

Send the same `idempotencyKey` with the same body, and you get the original request back with `deduplicated: true`: no second request and no second email. The same key with a different body fails with `IDEMPOTENCY_CONFLICT`. Keys apply per workspace for 30 days. In a workflow tool, use the execution id as the key, so a retried run finds the request it already created.

### What a request costs

Each request spends one unit of the workspace's [monthly allowance](https://formstep.io/docs/subscription-billing/limits-quotas/) when it is created, answered or not. When the allowance is spent, `requests.create` fails with `UPGRADE_REQUIRED` and reason `MONTHLY_ALLOWANCE_REACHED`. Requests already created can still be answered.

### Rate limit

`requests.create` and `documents.create` share a limit of 60 calls a minute per token, on top of the [general limit](https://formstep.io/docs/developers/overview/#rate-limits). A call over it fails with `RATE_LIMITED` and can be retried after `retryAfterMs`.

## Read the answers

Wait for the [callback](https://formstep.io/docs/developers/callbacks/), or read the request with [requests.get](https://formstep.io/docs/developers/rest-api/#requests-get). Once `status` is `completed`, it holds two maps keyed by field key: `answers` for code (option keys, numbers, file lists with download URLs) and `display` for people (labels, file names). A request is deleted once the form's retention window passes after it ends, so read what you need when it ends.

### Outcome

`status` says whether the request finished; `outcome` says what the recipient decided. A [decision question](https://formstep.io/docs/requests/decisions-and-approvals/) is a radio keyed `decision` whose options have the keys `approve`, `decline` and `changes`. When the recipient picks one of the three, the request completes with that outcome:

```json
"request": { "id": "kd7...", "externalId": "run-42", "status": "completed", "outcome": "changes" },
"answers": { "decision": "changes", "reason": "Please split the line items per site." }
```

- The outcome is on `data.request.outcome` in the callback and next to `status` on `requests.get`. The verdict is also an ordinary answer under `decision`.

- `requests.list` takes an `outcome` filter, which implies completed requests only.

- There is no outcome when the recipient picks a fourth option, when the option keys were changed (publishing warns about that), when the decision question sits inside a repeating group, or when the request expired or was canceled.

## From an AI agent

Over the [MCP server](https://formstep.io/docs/developers/mcp-server/), the same calls are the tools `fields_list`, `request_create`, `request_get`, `request_list`, `request_cancel`, `request_remind` , `request_replayCallback` and `document_create`, with the same parameters.

- `fields_list` adds a `usage` line to each key that names the argument it belongs in. Read it again after `form_publish`.

- Every request tool's result has a `next` line telling the agent what to do from there.

- The ids for `domainId` come from `formShareLink_list`, as `availableCustomDomains`.

- `editor_insertDecisionQuestion` adds a decision question; it takes a title and the three labels in the form's language.

- An agent with nobody to set up a receiver leaves out `callbackUrl` and polls `request_get` every few minutes until `status` leaves `pending`, then reads `outcome`. See [Callbacks or polling](https://formstep.io/docs/developers/mcp-server/#requests-callbacks).

- A `request_list` page can come back with no items and `hasMore: true`. The list goes on, so pass the cursor back and keep going.

[Request callbacks](https://formstep.io/docs/developers/callbacks/) — What arrives when the request ends, and how to verify it.

[Error reference](https://formstep.io/docs/developers/rest-api/#error-reference) — Every error code and reason, and what to do.
