> ## Documentation Index
> Fetch the complete documentation index at: https://docs.getomni.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Financial Spreading API

> Upload a business's financial documents, run a spread, track it to completion, and download the report and Excel workbook

Run financial spreads programmatically: upload a business's financial documents,
start a spread, track it to completion, and download the finished report and
Excel workbook. This guide walks the full loop end to end. Every step is a
copy-pasteable `curl`.

All requests go to `https://api-v2.getomni.ai` and authenticate with your
workspace API key in the `x-api-key` header. Export both once so the examples
run as-is:

```bash theme={null}
export OMNI_API_KEY="sk-..."
export OMNI_API_URL="https://api-v2.getomni.ai"
```

## Before you start

Three things to have in place before writing integration code:

1. **An API key.** Generate one in [workspace settings](https://platform.getomni.ai/settings/api).
   Keys are scoped to your workspace: every lead, document, spread, and artifact
   in this guide is only visible to keys from the same workspace. Requests
   without a valid `x-api-key` header return `401`.
2. **Leads and documents flowing into Omni.** A spread always runs against a
   lead's documents, so decide where those come from first. If you already
   create leads via the API or a CRM sync and collect documents through Omni,
   you're set — skip straight to step 4. If not, steps 2 and 3 show the minimal
   API calls to create a lead and attach files.
3. **A webhook endpoint (recommended).** To be notified when spreads finish
   rather than polling, you'll need a public HTTPS endpoint that accepts POST
   requests and responds `2xx` within 5 seconds. Step 6 covers registering it
   and authenticating deliveries.

## 1. Overview

Financial spreading takes a set of financial documents for one business — tax
returns, income statements, balance sheets — and turns them into a
standardized, multi-period spread. Omni classifies each document, extracts the
line items, consolidates everything onto a fixed underwriting template, and
computes the derived statements (income statement, balance sheet, cash flow)
plus summary ratios.

A completed spread produces two downloadable artifacts:

| Artifact type | Download format | What it is                                                                                        |
| ------------- | --------------- | ------------------------------------------------------------------------------------------------- |
| `REPORT`      | PDF             | The paginated analyst report (statements, ratios, per-document detail).                           |
| `SPREADSHEET` | xlsx            | The same consolidated data as an Excel workbook, one sheet per statement and per source document. |

A spread runs in the background and typically takes a few minutes, driven
mostly by document count and page count. You start a run, then either poll its
status or wait for a webhook. Spreads are always scoped to a lead — the
business you are underwriting — so the flow is: create a lead, upload its
documents, start the spread.

## 2. Create a lead

A lead represents the business you are spreading. If you already create leads
through the API or a CRM sync, reuse the existing lead's id and skip to step 3.

```bash theme={null}
curl -sS -X POST "$OMNI_API_URL/api/v1/leads" \
  -H "x-api-key: $OMNI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "firstName": "Jordan",
    "lastName": "Rivera",
    "businessName": "Pacific Coast Container Haulers",
    "email": "jordan@pacificcoast.example"
  }'
```

The `201` response wraps the created lead under `lead`; keep `lead.id`:

```json theme={null}
{ "success": true, "lead": { "id": "b1f0…", "businessName": "Pacific Coast Container Haulers" } }
```

```bash theme={null}
export LEAD_ID="b1f0…"
```

## 3. Upload financial documents

Attach the business's financial documents to the lead. You can either upload
the files directly (multipart) or pass a list of URLs Omni will fetch. Only PDF
documents are spreadable — other file types are stored on the lead but ignored
by the spread.

Upload local files (multipart form-data):

```bash theme={null}
curl -sS -X POST "$OMNI_API_URL/api/v1/documents" \
  -H "x-api-key: $OMNI_API_KEY" \
  -F "leadId=$LEAD_ID" \
  -F "files=@/path/to/2023-tax-return.pdf" \
  -F "files=@/path/to/2023-financials.pdf"
```

Or reference files by URL (JSON):

```bash theme={null}
curl -sS -X POST "$OMNI_API_URL/api/v1/documents" \
  -H "x-api-key: $OMNI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "leadId": "'"$LEAD_ID"'",
    "fileUrls": [
      "https://files.example/2023-tax-return.pdf",
      "https://files.example/2023-financials.pdf"
    ]
  }'
```

The `201` response lists the created documents with their ids:

```json theme={null}
{ "success": true, "documents": [ { "id": "doc-a…", "filename": "2023-tax-return.pdf" }, { "id": "doc-b…", "filename": "2023-financials.pdf" } ] }
```

These document ids are the ids you'll see everywhere else in this guide: in the
spread's `documents` array, in webhook payloads, and as the `documentIds`
filter when starting a spread.

## 4. Start the spread

Start a run for the lead with [Create Spread](/api-reference/spreads/create-spread).
By default it spreads every PDF document on the lead; pass `documentIds` to
restrict it to a subset.

```bash theme={null}
curl -sS -X POST "$OMNI_API_URL/api/v1/spreads" \
  -H "x-api-key: $OMNI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "leadId": "'"$LEAD_ID"'" }'
```

### Request body

| Field         | Type      | Required | Description                                                                                                  |
| ------------- | --------- | -------- | ------------------------------------------------------------------------------------------------------------ |
| `leadId`      | string    | yes      | The lead whose documents to spread.                                                                          |
| `documentIds` | string\[] | no       | Spread only these documents. Every id must be a document on the lead. Omit to spread all of the lead's PDFs. |

### Response — `201 Created`

The run is created and queued immediately; the response returns before any
processing happens.

```json theme={null}
{
  "success": true,
  "spread": {
    "id": "9f1e…",
    "status": "IN_PROGRESS",
    "error": null,
    "leadId": "b1f0…",
    "documents": [
      { "id": "doc-a…", "filename": "2023-tax-return.pdf", "status": "PROCESSING", "error": null },
      { "id": "doc-b…", "filename": "2023-financials.pdf", "status": "PROCESSING", "error": null }
    ],
    "artifacts": [],
    "createdAt": "2026-07-23T18:04:00.000Z",
    "updatedAt": "2026-07-23T18:04:00.000Z"
  }
}
```

```bash theme={null}
export SPREAD_ID="9f1e…"
```

### Errors

| Status | When                                                                                                                                                       |
| ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | No `leadId`; a `documentIds` value doesn't resolve to a document on the lead; or the resolved documents contain no PDFs.                                   |
| `404`  | The `leadId` isn't a lead in your workspace.                                                                                                               |
| `409`  | The lead already has a spread `IN_PROGRESS`. Only one spread per lead runs at a time; wait for it to reach `COMPLETE` or `FAILED` before starting another. |

## 5. Track progress

Poll the run with [Get Spread](/api-reference/spreads/get-spread) for its
status, per-document progress, and (once finished) its artifacts.

```bash theme={null}
curl -sS "$OMNI_API_URL/api/v1/spreads/$SPREAD_ID" \
  -H "x-api-key: $OMNI_API_KEY"
```

```json theme={null}
{
  "success": true,
  "spread": {
    "id": "9f1e…",
    "status": "IN_PROGRESS",
    "error": null,
    "leadId": "b1f0…",
    "documents": [
      { "id": "doc-a…", "filename": "2023-tax-return.pdf", "status": "EXTRACTED", "error": null },
      { "id": "doc-b…", "filename": "2023-financials.pdf", "status": "EXTRACTED", "error": null }
    ],
    "artifacts": [],
    "createdAt": "2026-07-23T18:04:00.000Z",
    "updatedAt": "2026-07-23T18:06:12.000Z"
  }
}
```

### Run lifecycle

A run's `status` is `IN_PROGRESS` from the moment it's created, then resolves
to `COMPLETE` or `FAILED`.

Each input document also has its own status: `PROCESSING` while the run works
on it, ending at `EXTRACTED` (financial data captured), `SKIPPED` (nothing
spreadable found), or `FAILED` (that document couldn't be processed). A single
failed document doesn't necessarily fail the
whole run — the other documents still spread. The run resolves to `FAILED` when
it can't produce a spread — most often because every document failed to process
or none of them contains a recognizable financial statement, though a
processing stage can also fail outright. In every case `error` carries a short,
human-readable reason (for example, "No financial statements were found in the
uploaded documents…").

Poll until `status` is `COMPLETE` or `FAILED`. A `COMPLETE` status always
includes the `artifacts` array with the report and spreadsheet.

## 6. Webhooks (recommended)

Polling works, but webhooks let you react the moment a spread finishes without
a poll loop. Subscribe once, then receive an HTTP POST for each event.

Register a subscription for the three spread events with
[Create Webhook](/api-reference/webhooks/create-webhook) (or do the same in the
Omni app under [Settings → Webhooks](https://platform.getomni.ai/settings/webhook)):

```bash theme={null}
curl -sS -X POST "$OMNI_API_URL/api/v1/webhooks" \
  -H "x-api-key: $OMNI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-app.example/webhooks/omni",
    "events": ["spread.started", "spread.completed", "spread.failed"],
    "headers": { "x-shared-secret": "choose-a-random-value" }
  }'
```

Deliveries are not signed, so `headers` is how you authenticate them: any
headers you set on the subscription are sent with every delivery. Set a secret
header here and reject requests to your endpoint that don't carry it.

### Events

| Event                                                                 | Fires when                                                             |
| --------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| [`spread.started`](/api-reference/webhooks/events/spread-started)     | A run is created and queued, including runs started from the Omni app. |
| [`spread.completed`](/api-reference/webhooks/events/spread-completed) | A run finishes and its report + spreadsheet artifacts are ready.       |
| [`spread.failed`](/api-reference/webhooks/events/spread-failed)       | A run fails.                                                           |

Every delivery has the same envelope. `actor` is always `null` for spread
events; `data` carries the run, with documents keyed by the same document ids
as the rest of the API:

```json theme={null}
{
  "event": "spread.completed",
  "actor": null,
  "occurredAt": "2026-07-23T18:08:40.000Z",
  "data": {
    "spreadId": "9f1e…",
    "leadId": "b1f0…",
    "externalLeadId": "crm-4471",
    "workspaceId": "ws-1…",
    "status": "COMPLETE",
    "documents": [
      { "id": "doc-a…", "filename": "2023-tax-return.pdf", "status": "EXTRACTED" },
      { "id": "doc-b…", "filename": "2023-financials.pdf", "status": "EXTRACTED" }
    ],
    "artifacts": [
      { "id": "art-report…", "type": "REPORT", "title": "Financial Spread - Pacific Coast Container Haulers" },
      { "id": "art-sheet…", "type": "SPREADSHEET", "title": "Financial Spread - Pacific Coast Container Haulers (Excel)" }
    ]
  }
}
```

`spread.started` carries the same shape at `status: "IN_PROGRESS"` with no
`artifacts`. `spread.failed` omits `artifacts` and adds `error`, a short,
human-readable reason for the failure.

### Things to know

* **`spread.started` fires for every run, not just yours.** If a teammate
  starts a spread from the Omni app, your endpoint still receives
  `spread.started` with the new `spreadId` — record the id, then poll or await
  the terminal event.
* **Respond within 5 seconds.** A delivery that doesn't get a `2xx` within 5
  seconds counts as failed and is retried with backoff (roughly 1, 2, 5, 10,
  then 20 minutes between attempts) for up to 6 total attempts, after which
  it's marked failed. Acknowledge fast and do heavy work (like downloading
  artifacts) asynchronously.
* **Handle repeat deliveries.** The same event can arrive more than once for a
  spread: a retry repeats an identical payload, and a regeneration (step 8)
  legitimately re-emits `spread.completed` with new artifact ids. Don't drop
  repeats by event name alone — treat each delivery as the run's latest state,
  keyed by `data.spreadId`, and process it idempotently.
* **Test leads don't fire.** Spreads on a test lead emit no webhooks.

## 7. Download the artifacts

When a run is `COMPLETE`, its `artifacts` array (from the status poll or the
`spread.completed` webhook) holds the ids you download. A `REPORT` downloads as
a PDF and a `SPREADSHEET` as an xlsx workbook.

Fetch metadata with [Get Artifact](/api-reference/artifacts/get-artifact)
(optional — gives you the content type and a suggested filename):

```bash theme={null}
curl -sS "$OMNI_API_URL/api/v1/artifacts/$ARTIFACT_ID" \
  -H "x-api-key: $OMNI_API_KEY"
```

```json theme={null}
{
  "success": true,
  "artifact": {
    "id": "art-report…",
    "type": "REPORT",
    "title": "Financial Spread - Pacific Coast Container Haulers",
    "summary": "Consolidated financial spread from 2 document(s).",
    "leadId": "b1f0…",
    "createdAt": "2026-07-23T18:08:38.000Z",
    "contentType": "application/pdf",
    "filename": "Financial Spread - Pacific Coast Container Haulers.pdf"
  }
}
```

Download the bytes with
[Download Artifact](/api-reference/artifacts/download-artifact). Use `-o` to
save the file:

```bash theme={null}
# Report -> PDF
curl -sS "$OMNI_API_URL/api/v1/artifacts/$REPORT_ARTIFACT_ID/content" \
  -H "x-api-key: $OMNI_API_KEY" \
  -o spread-report.pdf

# Spreadsheet -> xlsx
curl -sS "$OMNI_API_URL/api/v1/artifacts/$SPREADSHEET_ARTIFACT_ID/content" \
  -H "x-api-key: $OMNI_API_KEY" \
  -o spread.xlsx
```

To list every spread on a lead (for example to find a past run's artifacts),
use [List Lead Spreads](/api-reference/spreads/list-lead-spreads). Each entry
is the same shape as the single-spread response, including its `artifacts`
array:

```bash theme={null}
curl -sS "$OMNI_API_URL/api/v1/leads/$LEAD_ID/spreads" \
  -H "x-api-key: $OMNI_API_KEY"
```

The same report and spreadsheet also appear in the Omni app on the lead's
detail page — useful for eyeballing output while you build, without wiring up
downloads first.

## 8. Regenerate a spread

[Regenerate Spread](/api-reference/spreads/regenerate-spread) rebuilds a
finished run from its already-extracted data: the documents are not
re-processed; only the consolidation and report run again. Any analyst
adjustments recorded on the run inside Omni are applied, so regenerating is how
an integration picks up corrections made in the app.

```bash theme={null}
curl -sS -X POST "$OMNI_API_URL/api/v1/spreads/$SPREAD_ID/regenerate" \
  -H "x-api-key: $OMNI_API_KEY"
```

The `202` response returns the run back at `IN_PROGRESS`; on completion the run
has new artifact ids. A regenerated run re-emits `spread.started` and
`spread.completed` — the second completion carries the new artifact ids, which
is why deliveries are processed as state updates rather than dropped as
duplicates.
Regenerating a run that is still in flight returns `409`, and an unknown spread
id returns `404`.

## 9. End-to-end script

This bash script chains the whole loop: create a lead, upload a PDF, start the
spread, poll until it finishes, then download both artifacts. It needs `curl`
and [`jq`](https://jqlang.github.io/jq/).

```bash theme={null}
#!/usr/bin/env bash
set -euo pipefail

API="${OMNI_API_URL:-https://api-v2.getomni.ai}"
KEY="$OMNI_API_KEY"
PDF_PATH="${1:?usage: spread.sh <path-to-financials.pdf>}"

auth=(-H "x-api-key: $KEY")

echo "Creating lead..."
lead_id=$(curl -sS -X POST "$API/api/v1/leads" "${auth[@]}" \
  -H "Content-Type: application/json" \
  -d '{"firstName":"Jordan","lastName":"Rivera","businessName":"Pacific Coast Container Haulers"}' \
  | jq -r '.lead.id')
echo "  lead: $lead_id"

echo "Uploading document..."
curl -sS -X POST "$API/api/v1/documents" "${auth[@]}" \
  -F "leadId=$lead_id" -F "files=@$PDF_PATH" > /dev/null

echo "Starting spread..."
spread_id=$(curl -sS -X POST "$API/api/v1/spreads" "${auth[@]}" \
  -H "Content-Type: application/json" \
  -d "{\"leadId\":\"$lead_id\"}" \
  | jq -r '.spread.id')
echo "  spread: $spread_id"

echo "Polling..."
while true; do
  spread=$(curl -sS "$API/api/v1/spreads/$spread_id" "${auth[@]}")
  status=$(echo "$spread" | jq -r '.spread.status')
  echo "  status: $status"
  case "$status" in
    COMPLETE) break ;;
    FAILED)   echo "Spread failed: $(echo "$spread" | jq -r '.spread.error')"; exit 1 ;;
  esac
  sleep 8
done

report_id=$(echo "$spread" | jq -r '.spread.artifacts[] | select(.type=="REPORT") | .id')
sheet_id=$(echo "$spread"  | jq -r '.spread.artifacts[] | select(.type=="SPREADSHEET") | .id')

echo "Downloading artifacts..."
curl -sS "$API/api/v1/artifacts/$report_id/content" "${auth[@]}" -o spread-report.pdf
curl -sS "$API/api/v1/artifacts/$sheet_id/content"  "${auth[@]}" -o spread.xlsx
echo "Done: spread-report.pdf, spread.xlsx"
```
