# Send a request with the REST API

Create an API token, read a form's field keys, and create a prefilled request with curl, from any tool that can make an HTTP call.

You need a published form. This guide uses the one built in [Build a form with an AI agent](https://formstep.io/docs/guides/ai-agents/build-a-form/): company name, contact email, VAT number, a yes or no question about payment terms, and a file upload. Every method and option used here is described in the [API reference](https://formstep.io/docs/developers/rest-api/).

## 1. Create an API token

In Formstep, open **OAuth and API Keys** in the workspace sidebar and [create a token](https://formstep.io/docs/developers/api-tokens/#create). The value is shown once. Keep it in an environment variable, not in your code:

```bash
export FORMSTEP_TOKEN='fs_...'
```

A token reaches the one workspace it was created in. Every call below is a `POST` to the same URL with the token in the `Authorization` header; the body names the method and its parameters.

## 2. Find the form ID

Open the form in the editor. The form ID is the part of the address after `/forms/`:

```text
https://app.formstep.io/<workspace ID>/forms/jx75hdx8vb5hy1x85gm17nqgkn8f674g/edit
```

The commands below use this guide’s form ID. Put your own in its place.

## 3. List the field keys

A request fills in questions, and the answers come back, by [field key](https://formstep.io/docs/developers/requests-api/#field-keys). Ask the form for its keys instead of guessing them from the titles:

```bash
curl -X POST https://api.formstep.io/api/v1 \
  -H "Authorization: Bearer $FORMSTEP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"method": "fields.list", "params": {"formId": "jx75hdx8vb5hy1x85gm17nqgkn8f674g"}}'
```

The reply lists each question of the published form. Shortened:

```json
{
  "ok": true,
  "data": {
    "published": true,
    "items": [
      { "key": "company_name", "type": "text", "title": "Company name", "required": true, "prefillable": true },
      { "key": "contact_email", "type": "email", "title": "Contact email", "required": true, "prefillable": true },
      { "key": "vat_number", "type": "text", "title": "VAT number", "required": false, "prefillable": true },
      {
        "key": "do_you_accept_our_30_day_payment_terms", "type": "radio", "title": "Do you accept our 30-day payment terms?", "required": true, "prefillable": true,
        "options": [{ "key": "yes", "label": "Yes" }, { "key": "no", "label": "No" }]
      },
      { "key": "certificate_of_incorporation", "type": "file", "title": "Certificate of incorporation", "required": true, "prefillable": false }
    ],
    "hasMore": false
  }
}
```

To prefill a choice question, send the option’s `key`, here `"yes"`, not its label. A question with `prefillable: false`, like the file upload, only the recipient can answer. The other flags are explained under [fields.list](https://formstep.io/docs/developers/rest-api/#fields-list).

## 4. Create a test request

Send the request to Ada Lovelace, with the company name filled in and locked. `test: true` makes it a [test request](https://formstep.io/docs/developers/requests-api/#test-mode): it never emails anyone and counts nowhere, so you can repeat this step with a new `idempotencyKey` each time (on Free, up to 10 times a day). Its link closes within 24 hours.

```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": {
      "formId": "jx75hdx8vb5hy1x85gm17nqgkn8f674g",
      "recipient": { "email": "ada@acme.example", "name": "Ada Lovelace" },
      "prefill": { "company_name": "Analytical Engines Ltd" },
      "readonly": ["company_name"],
      "externalId": "supplier-2043",
      "idempotencyKey": "supplier-2043",
      "test": true
    }
  }'
```

- `prefill` fills in answers the recipient can still change. A key in `readonly` is [locked](https://formstep.io/docs/requests/creating-requests/#three-buckets) as well.

- `externalId` is your own id for this piece of work, like a supplier number. It comes back on every read and callback.

- `idempotencyKey` makes a retry safe. If your code sends the same call twice, Formstep returns the first request with `deduplicated: true` instead of creating a second one.

The reply carries the request ID and the link for the recipient:

```json
{
  "ok": true,
  "data": {
    "id": "m17ayhcnj9xvzkff3atek49bdd8f6872",
    "status": "pending",
    "url": "https://form.formstep.io/r/rq_...",
    "deliveryStatus": "not_requested",
    "externalId": "supplier-2043",
    "deduplicated": false,
    "createdAt": 1790539225370,
    "expiresAt": 1793131225370
  }
}
```

`deliveryStatus: "not_requested"` means Formstep sent no email; you deliver the link yourself. To have Formstep send the invitation and reminders, add `"delivery": "email"`, which needs a Pro or Business plan; a Free account can try it on its own address. If the form has **Send reminders** on, a real request with a recipient email still gets the scheduled reminders; add `"reminders": []` to send none. See [Invitations and reminders](https://formstep.io/docs/developers/requests-api/#delivery).

> **Test requests stay out of sight**
>
> The **Requests** page lists test requests only when you turn on the **Show test requests** filter. Open the `url` yourself to complete it. Drop `test` when you send the real thing.

## 5. Complete it and read the answers

Open the `url` in a browser. The company name is filled in and locked; answer the rest and submit. Then read the request back with its ID:

```bash
curl -X POST https://api.formstep.io/api/v1 \
  -H "Authorization: Bearer $FORMSTEP_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"method": "requests.get", "params": {"requestId": "m17ayhcnj9xvzkff3atek49bdd8f6872"}}'
```

Once `status` is `completed`, the reply holds two maps keyed by field key. `answers` is for code: a choice answer is the option key, a file answer is a list of files with download URLs. `display` is for people: labels and file names.

```json
"answers": {
  "company_name": "Analytical Engines Ltd",
  "do_you_accept_our_30_day_payment_terms": "yes",
  "certificate_of_incorporation": [{ "name": "certificate-of-incorporation.pdf", "type": "application/pdf", "size": 635, "url": "https://api.formstep.io/api/storage/..." }]
},
"display": {
  "company_name": "Analytical Engines Ltd",
  "do_you_accept_our_30_day_payment_terms": "Yes",
  "certificate_of_incorporation": "certificate-of-incorporation.pdf"
}
```

Asking again and again until the status changes is fine for a test. In production, give the request a `callbackUrl` and Formstep calls you when it ends; the next guide sets that up.

## Next steps

[Receive and verify the callback](https://formstep.io/docs/developers/quickstart-receive-the-callback/) — Get the answers pushed to your endpoint, and check that Formstep sent them.

[Create requests from code](https://formstep.io/docs/developers/requests-api/) — Every option a request takes: context, metadata, documents, expiry.
