Go to app

Create requests from code

Two calls send a request: fields.list tells you the form's field keys, and requests.create sends the form to one recipient with the values you already know.


To try it end to end with curl first, follow Send a request with the REST API. Every parameter is listed under Requests in the API reference. What a request is, and what the recipient sees, is explained in Requests.

Copy a call from the New request sheet

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.

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 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 showsWhat you send
prefillable: trueA value in prefill, in the shape its type asks for (see value shapes below).
options, or rows and columnsThe option key, not the label. The label is there so you can match the choice you know to its key.
context: trueA hidden field. Its value goes in context, never in prefill; in prefill it fails with UNKNOWN_FIELD_KEY.
calculated: trueNothing. 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: trueA 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. 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 titleKey
Company namecompany_name
What is your VAT number?what_is_your_vat_number
Prénomprenom
Søknad om støttesoknad_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:

A repeating group keyed contacts
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

requests.create
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. 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. The API rules:

ParameterTakesRules
prefillAnswers by field key, for visible questionsThe recipient can change them. Each value must fit its type.
readonlyA list of prefilled keys to lockEvery key must also be in prefill (READONLY_REQUIRES_PREFILL), and a locked required field needs a non-empty value (READONLY_REQUIRED_EMPTY).
contextValues for the form's hidden fieldsEach 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.
metadataAny object of your ownNever 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.

TypeSend
text, email, phone, url, textareaA string
number, rating, scaleA number
switchtrue or false
date"2026-03-04"
time"09:30" or "09:30:00"
radio, selectThe option key, not its label
checkbox, ranking, picture-choiceA list of option keys
matrixAn 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-appointmentNothing; the recipient supplies these
any field with calculated: trueNothing; the form works it out
documentsNothing in prefill; use the documents parameter below

Documents for one recipient

A 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. 1

    Reserve the upload

    Call 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. 2

    Upload the bytes

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

  3. 3

    Reference it on the request

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

requests.create, documents
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. 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 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.

Delivery status

deliveryStatusMeaning
not_requestedCreated with delivery "none". You deliver the link.
queuedAccepted, not sent yet.
sentHanded to the email provider.
failedFormstep could not send it, usually a plan or address problem. Fix it, then send a reminder: it carries the same link.
bouncedThe 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 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

OptionWhat it does
languageThe 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.
expiresAtWhen the link stops working, in epoch milliseconds. Default 30 days out, at most 365 (EXPIRY_OUT_OF_RANGE).
externalIdYour own id for the request. Comes back on every read and callback, and filters requests.list.
callbackUrlWhere Formstep POSTs the event when the request ends. See Request callbacks.
domainIdThe 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.
formBuild the form in the same call instead of sending formId. See 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 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. A call over it fails with RATE_LIMITED and can be retried after retryAfterMs.

Read the answers

Wait for the callback, or read the request with 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 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:

request.completed, trimmed
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, 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.

  • 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.