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

## Send a request with the REST API

Any tool with an HTTP step can send a request: Pipedream, a serverless function, a script. This guide does it with curl, so you can see each call before you move it into your own code.

<p>
  You need a published form. This guide uses the one built in <a href="/guides/ai-agents/build-a-form">Build a form with an AI agent</a>:
  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 <a href="/developers/rest-api">API methods</a>.
</p>

<h2 id="token">1. Create an API token</h2>

<p>
  In formbase, open <strong>OAuth and API Keys</strong> in the workspace sidebar and{' '}
  <a href="/developers/api-tokens#create">create a token</a>. The value is shown once. Keep it in an environment variable, not in your code:
</p>

```
export FORMBASE_TOKEN='fb_...'
```

<p>
  A token reaches the one workspace it was created in. Every call below is a <code>POST</code> to the same URL with the token in the{' '}
  <code>Authorization</code> header; the body names the method and its parameters.
</p>

<h2 id="form-id">2. Find the form ID</h2>

<p>
  Open the form in the editor. The form ID is the part of the address after <code>/forms/</code>:
</p>

```
https://app.formbase.so/<workspace ID>/forms/jx75hdx8vb5hy1x85gm17nqgkn8f674g/edit
```

<p>The commands below use this guide's form ID. Put your own in its place.</p>

<h2 id="fields">3. List the field keys</h2>

<p>
  A request fills in questions, and the answers come back, by <a href="/requests/field-keys">field key</a>. Ask the form for its keys
  instead of guessing them from the titles:
</p>

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

<p>The reply lists each question of the published form. Shortened:</p>

```
{
  "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
  }
}
```

<p>
  To prefill a choice question, send the option's <code>key</code>, here <code>"yes"</code>, not its label. A question with{' '}
  <code>prefillable: false</code>, like the file upload, only the recipient can answer. The other flags are explained under{' '}
  <a href="/developers/rest-api#fields-list">fields.list</a>.
</p>

<h2 id="create">4. Create a test request</h2>

<p>
  Send the request to Ada Lovelace, with the company name filled in and locked. <code>test: true</code> makes it a{' '}
  <a href="/requests/creating-requests#test-mode">test request</a>: it never emails anyone and counts nowhere, so you can repeat this step
  as often as you like, with a new <code>idempotencyKey</code> each time.
</p>

```
curl -X POST https://api.formbase.so/api/v1 \
  -H "Authorization: Bearer $FORMBASE_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
    }
  }'
```

<ul>
  <li>
    <strong>prefill</strong> fills in answers the recipient can still change. A key in <strong>readonly</strong> is{' '}
    <a href="/requests/creating-requests#locked-fields">locked</a> as well.
  </li>
  <li>
    <strong>externalId</strong> is your own id for this piece of work, like a supplier number. It comes back on every read and callback.
  </li>
  <li>
    <strong>idempotencyKey</strong> makes a retry safe. If your code sends the same call twice, formbase returns the first request with{' '}
    <code>deduplicated: true</code> instead of creating a second one.
  </li>
</ul>

<p>The reply carries the request ID and the link for the recipient:</p>

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

<p>
  <code>deliveryStatus: "not_requested"</code> means formbase sent no email; you deliver the link yourself. To have formbase send the
  invitation and reminders, add <code>"delivery": "email"</code>, which needs a Pro or Business plan. See{' '}
  <a href="/requests/invitations-and-reminders">Invitations, reminders & expiry</a>. If the form has <strong>Send reminders</strong> on, a
  real request with a recipient email still gets the scheduled reminders; add <code>"reminders": []</code> to send none.
</p>

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

<h2 id="read">5. Complete it and read the answers</h2>

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

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

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

```
"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.formbase.so/api/storage/..." }]
},
"display": {
  "company_name": "Analytical Engines Ltd",
  "do_you_accept_our_30_day_payment_terms": "Yes",
  "certificate_of_incorporation": "certificate-of-incorporation.pdf"
}
```

<p>
  Asking again and again until the status changes works for a test, but not in production. Give the request a <code>callbackUrl</code> and
  formbase calls you when it ends; the next guide sets that up.
</p>

<h2 id="next">Next</h2>

<div class="not-prose grid gap-3 sm:grid-cols-2 mb-8">
  - [Receive and verify the callback](/guides/rest-api/verify-the-callback) — Get the answers pushed to your endpoint, and check that formbase sent them.
  - [Creating requests](/requests/creating-requests) — Every option a request takes: context, metadata, documents, expiry.
</div>
