> For the complete documentation index, see [llms.txt](https://docs.ai.insly.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.ai.insly.com/nora/agentic-workflows/run-agentic-extraction-via-api.md).

# Run agentic extraction via API

Use this to run [agentic extraction](/nora/agentic-workflows/agentic-extraction.md) from your own system without email: upload the documents, start a run, poll it until it finishes, then read the submission it created.

All requests go to `https://main.formflow-dev.net` and use the same Bearer token as the rest of the API (see [Authentication and limits](/nora/api-documentation/authentication-and-limits.md)). Your API client token already has access to these endpoints. They are not yet listed in the API reference.

{% hint style="info" %}
These endpoints only start **agentic** runs. To process a submission the classic way, follow [Process a submission via API](/nora/configuration-guide/process-a-submission-via-api.md).
{% endhint %}

{% hint style="warning" %}
**Runs started over the API are not exported to Ledger automatically.** Automatic export needs an email sender to notify, and an API run has none. When a run has finished, export the submission yourself (see [Read the result](/nora/agentic-workflows/run-agentic-extraction-via-api.md#read-the-result)).
{% endhint %}

{% stepper %}
{% step %}

### Request upload URLs

`POST /api/studio/uploads`

List every file you are going to upload, with its exact size in bytes. You can list up to 20 files.

```json
{
  "files": [
    { "filename": "proposal-form.pdf", "size": 284113 },
    { "filename": "claims-history.xlsx", "size": 40211 }
  ]
}
```

Leave out `threadId` to start a new conversation, which is what you normally want. To add files to a conversation you started earlier through the API, pass its `threadId`.

**201 response**

```json
{
  "threadId": "5f0c3a6e-2b1d-4f7a-9c3e-0d8b6a1e4f21",
  "files": [
    {
      "fileId": "a1c2e3f4-5b6d-4e7f-8a9b-0c1d2e3f4a5b",
      "filename": "proposal-form.pdf",
      "mimeType": "application/pdf",
      "size": 284113,
      "uploadUrl": "https://…s3…amazonaws.com/…",
      "method": "PUT",
      "headers": { "Content-Type": "application/pdf", "Content-Length": "284113" },
      "expiresIn": 900
    }
  ]
}
```

Keep `threadId` and every `fileId`. You need them to start the run.

* The file type comes from the filename extension. Allowed: `pdf`, `doc`, `docx`, `xls`, `xlsx`, `csv`, `jpg`, `jpeg`, `png`, `gif`, `heic`.
* Each file can be up to 30 MB.
* If any one file is rejected, the whole request fails with `400`, and nothing is created. A `threadId` that is unknown returns `404`. One that was not created through the API returns `400`.
  {% endstep %}

{% step %}

### Upload the bytes

`PUT` each file's bytes to its `uploadUrl` within `expiresIn` seconds (15 minutes). Send every header listed in `headers`, with exactly those values. The body must be exactly `size` bytes, because the URL is signed for that type and length. Anything else is rejected by storage.

```bash
curl -X PUT "$UPLOAD_URL" \
  -H "Content-Type: application/pdf" \
  --data-binary @proposal-form.pdf
```

`curl` sets `Content-Length` from the file itself. This request goes straight to storage, so do not send your Bearer token with it.
{% endstep %}

{% step %}

### Confirm each upload

`POST /api/studio/uploads/{fileId}/complete`

No request body. Call this once per file after its `PUT` has succeeded.

**200 response**

```json
{ "fileId": "a1c2e3f4-5b6d-4e7f-8a9b-0c1d2e3f4a5b", "status": "completed", "size": 284113 }
```

`409` means the file has not reached storage. If the `PUT` is still in progress, retry the call when it has finished. If the upload URL expired first, it can't be reused: request a new upload (step 1), which gives a new `fileId`, and upload to that. A file that hasn't been confirmed can't be used in a run.
{% endstep %}

{% step %}

### Start the run

`POST /api/studio/extraction-runs`

```json
{
  "threadId": "5f0c3a6e-2b1d-4f7a-9c3e-0d8b6a1e4f21",
  "fileIds": ["a1c2e3f4-5b6d-4e7f-8a9b-0c1d2e3f4a5b", "b2d3f4a5-6c7e-4f8a-9b0c-1d2e3f4a5b6c"],
  "templateId": 7
}
```

<table><thead><tr><th width="150">Field</th><th>Description</th></tr></thead><tbody><tr><td><code>threadId</code></td><td>Required. The <code>threadId</code> from step 1.</td></tr><tr><td><code>fileIds</code></td><td>Required, 1–20. Confirmed uploads on that conversation. You can name some of its files rather than all of them.</td></tr><tr><td><code>templateId</code></td><td>Required. The template to extract against. See <a href="/nora/api-documentation/templates.md">Templates</a>.</td></tr><tr><td><code>workflowId</code></td><td>Optional. Only needed if your organisation has more than one active workflow that starts from documents. The error message tells you when it is needed.</td></tr></tbody></table>

**202 response**

```json
{
  "runId": "9e8d7c6b-5a49-4382-8170-6f5e4d3c2b1a",
  "workflowId": "c0ffee00-1234-4abc-9def-0123456789ab",
  "threadId": "5f0c3a6e-2b1d-4f7a-9c3e-0d8b6a1e4f21",
  "status": "running",
  "fileIds": ["a1c2e3f4-5b6d-4e7f-8a9b-0c1d2e3f4a5b", "b2d3f4a5-6c7e-4f8a-9b0c-1d2e3f4a5b6c"]
}
```

The run starts straight away. Over the API nothing is ever asked: if the workflow needs something this request doesn't give, the request fails with `400` and says what is missing.

<table><thead><tr><th width="90">Status</th><th>When</th></tr></thead><tbody><tr><td><code>400</code></td><td><ul><li>One or more <code>fileIds</code> aren't confirmed uploads on that conversation. The whole request is refused, so no run reads only some of your files.</li><li>The conversation was not created through the API.</li><li>Several workflows could run and <code>workflowId</code> is missing.</li><li>The named workflow is inactive or not fully set up.</li><li>The workflow needs a parameter this request can't supply. The message lists it.</li></ul></td></tr><tr><td><code>404</code></td><td>Unknown thread, template or workflow. Or your organisation has no active workflow that starts from documents.</td></tr></tbody></table>
{% endstep %}

{% step %}

### Poll the run

`GET /api/studio/extraction-runs/{runId}`

Poll until `finished` is `true`. A run usually takes a few minutes, so polling every 15–30 seconds is enough.

**200 response, while running**

```json
{
  "runId": "9e8d7c6b-5a49-4382-8170-6f5e4d3c2b1a",
  "status": "running",
  "finished": false,
  "currentStepIndex": 2,
  "stepsTotal": 7,
  "submissionId": "3b7f0a1e-9c2d-4e5f-8a6b-7c8d9e0f1a2b",
  "completedAt": null
}
```

**200 response, finished**

```json
{
  "runId": "9e8d7c6b-5a49-4382-8170-6f5e4d3c2b1a",
  "status": "completed",
  "finished": true,
  "currentStepIndex": 7,
  "stepsTotal": 7,
  "submissionId": "3b7f0a1e-9c2d-4e5f-8a6b-7c8d9e0f1a2b",
  "message": "Your submission has been processed, but it was not exported to the ledger.",
  "completedAt": "2026-10-07T09:41:12.000Z"
}
```

<table><thead><tr><th width="170">Field</th><th>Description</th></tr></thead><tbody><tr><td><code>status</code></td><td><code>pending</code>, <code>running</code>, <code>completed</code> or <code>failed</code>.</td></tr><tr><td><code>finished</code></td><td><code>true</code> once the run is <code>completed</code> or <code>failed</code>. Stop polling when it is.</td></tr><tr><td><code>currentStepIndex</code></td><td>Progress only, counted from 0. On a completed run it equals <code>stepsTotal</code>, even when validation stopped the run early. On a failed run it stays at the step that failed. Use <code>finished</code> to tell whether the run is done.</td></tr><tr><td><code>stepsTotal</code></td><td>Number of steps in the workflow.</td></tr><tr><td><code>submissionId</code></td><td>The submission the run created. Appears once the first step has run.</td></tr><tr><td><code>missingFields</code></td><td>Only when validation stopped the run. A readable list of what is missing or invalid.</td></tr><tr><td><code>message</code></td><td>The run's final message. On a failed run, a short technical reason, for example which step failed or timed out.</td></tr><tr><td><code>completedAt</code></td><td>When the run finished, or <code>null</code>.</td></tr></tbody></table>
{% endstep %}

{% step %}

### Read the result

The poll result tells you how the run went. The extracted data is on the submission. Fetch it with [`GET /api/submission/{submissionId}`](/nora/api-documentation/submissions.md) and read its `payload`, as in classic processing. To check the outcome in code, read the submission's `status`. Don't parse `message`, whose wording can change.

<table><thead><tr><th width="280">Poll result</th><th>Meaning and what to do</th></tr></thead><tbody><tr><td><code>completed</code>, no <code>missingFields</code></td><td>The run went through, but the submission was not exported, because API runs never export automatically. Check the submission's <code>status</code>. If it is held for review, it waits for a reviewer. Otherwise check it with <code>GET /api/submission/{submissionId}/validate</code>, then export it with <code>POST /api/submission/{submissionId}/export</code> or from the web app.</td></tr><tr><td><code>completed</code> with <code>missingFields</code></td><td>The submission was extracted, but it does not pass validation yet. See <a href="/nora/agentic-workflows/run-agentic-extraction-via-api.md#missing-fields-over-the-api">Missing fields over the API</a>.</td></tr><tr><td><code>failed</code></td><td>A step failed, and <code>message</code> says which. <code>submissionId</code> is there if the submission was created before the failure.</td></tr></tbody></table>
{% endstep %}
{% endstepper %}

### No webhooks: polling is the way to get results

{% hint style="warning" %}
There is no webhook or callback for agentic runs. You can't register a URL to be told when a run finishes, and no run events are published. Polling `GET /api/studio/extraction-runs/{runId}` is how you get the result.
{% endhint %}

The [submission webhooks](/nora/configuration-guide/setting-up-webhooks.md) may fire for the submission a run creates, for example `extracted`. Use them to follow the submission. They never tell you whether the run finished, failed or stopped for missing fields.

### Missing fields over the API

Over email, Nora asks the sender for missing fields. Over the API there is nobody to ask. The run stops, returns the list in `missingFields`, and leaves the submission in Nora without exporting it. To finish it:

1. Add the missing values in the Nora web app, or with [`PATCH /api/submission/{submissionId}`](/nora/api-documentation/submissions.md).
2. Check it with [`GET /api/submission/{submissionId}/validate`](/nora/api-documentation/submissions.md).
3. Export it from the web app, or with [`POST /api/submission/{submissionId}/export`](/nora/api-documentation/submissions.md).

{% hint style="warning" %}
Don't call `POST /api/studio/extraction-runs` again for the same documents. Every API run creates a **new** submission, so you would end up with a duplicate instead of a completed original.
{% endhint %}
