# Extract figures

Read document numbers as figures with units, reporting context, and source locations.

Documentation index: https://webcite.co/llms.txt
Canonical page: https://webcite.co/api-docs/figures
API origin: https://api.webcite.co
Authentication: x-api-key header. Keep keys on your server.

## When to use it

Keep the source label, sheet, cell, and other provenance with each figure. A missing number is unknown, not zero. `scale` describes an already normalized value; do not apply the scale again.

## Request

POST /api/v1/extract/figures

2 credits for a billable extraction outcome.

### curl

```curl
curl --fail-with-body -X POST 'https://api.webcite.co/api/v1/extract/figures' \
  -H "x-api-key: $WEBCITE_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
  "asset_id": "YOUR_ASSET_ID"
}'
```

### Node.js

```javascript
const response = await fetch("https://api.webcite.co/api/v1/extract/figures", {
  method: "POST",
  headers: {
    "x-api-key": process.env.WEBCITE_API_KEY,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
  "asset_id": "YOUR_ASSET_ID"
}),
});
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(await response.json());
```

### python

```python
import os
import json
import requests

payload = json.loads("{\n  \"asset_id\": \"YOUR_ASSET_ID\"\n}")
response = requests.post(
    "https://api.webcite.co/api/v1/extract/figures",
    headers={"x-api-key": os.environ["WEBCITE_API_KEY"]},
    json=payload,
    timeout=(10, 300),
)
response.raise_for_status()
print(response.json())
```

## Response

Returns figures with available metric, value, unit, period, entity, basis, and provenance fields. Preserve decimalValue when supplied for exact arithmetic.

## Errors

For 400, check the request fields and source identifiers. For 401, check your API key. For 429, wait for the retry interval. See the errors guide before retrying a billable request.

## OpenAPI operation

```json
{
  "path": "/api/v1/extract/figures",
  "method": "POST",
  "operation": {
    "description": "Reads a spreadsheet and returns each known metric as a **figure**: the value normalized to its canonical unit, plus what it means (`metric`), `unit`, optional `entity`/`period`, a confidence `band`, whether it was confirmed against the cited cell (`bound`), and full `provenance` (asset, sheet, cell, source label). This is deterministic financial-model reading: header scale (\"$M\"/\"'000\"), accounting negatives, period columns (FY2023 vs FY2024), and unit-declaration checks are all honoured, so a % is never mis-read as currency.\n\nDeterministic-first. Reads **spreadsheets** (cell-level discovery), **PDFs with a text layer** (grounded and cited by page), and **every other text-bearing format** (docx, pptx, html, txt) deterministically with no model calls. **Scanned PDF pages and image files** (png/jpg/...) are transcribed by vision OCR first, then grounded, so their numbers are still extracted (provenance marked `model`); this requires a vision key and is skipped otherwise. Accepts `asset_id` (uploaded asset) or `asset_url` (your own signed URL).\n\n**An empty `figures` says why it is empty.** `state`, `reason` and `code` — the same three fields, the same vocabulary and the same values as `POST /extract` — describe **the read that fed the figure engine**, not the figure list. A workbook that opened and yielded no known metric is `state: \"complete\"` with `code: null`: the engine measured it and found nothing to tag, which is an answer. A refusal never reached the engine and carries its cause (`source_too_large`, `source_corrupt`, `source_encrypted`, `unsupported_format`, `extraction_error`). `figures` itself is untouched.\n\n**Cost: 2 credits.** Deterministic (no LLM calls); downloads and runs the full figure engine over every sheet/page.",
    "operationId": "ApiV1Controller_extractFigures",
    "parameters": [],
    "requestBody": {
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/ExtractRequestDto"
          }
        }
      },
      "required": true
    },
    "responses": {
      "200": {
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ExtractFiguresResponseDto"
            }
          }
        },
        "description": "{ figures: { metric, value, unit, entity?, period?, band, bound, provenance }[], state, reason, code }"
      },
      "400": {
        "description": "Asset not found"
      },
      "401": {
        "description": "Unauthorized - API key required"
      },
      "429": {
        "description": "Rate limit exceeded"
      }
    },
    "security": [
      {
        "x-api-key": []
      },
      {
        "bearer": []
      }
    ],
    "summary": "Extract every number from a document as a tagged, source-grounded figure",
    "tags": [
      "Public API"
    ]
  },
  "schemas": {
    "ExtractRequestDto": {
      "properties": {
        "asset_id": {
          "description": "An uploaded asset id (from POST /upload). One of asset_id / asset_url is required.",
          "type": "string"
        },
        "asset_url": {
          "description": "A direct URL to the file. One of asset_id / asset_url is required.",
          "type": "string"
        }
      },
      "type": "object"
    },
    "ExtractFiguresResponseDto": {
      "properties": {
        "code": {
          "description": "The stable classification of `reason`, safe to switch on. The same vocabulary and the same values as `POST /extract`. Null when nothing refused.\n\n| Code | What it means | What to do |\n|------|---------------|------------|\n| `source_too_large` | a size or expansion ceiling refused the read | `reason` carries the observed value and the limit; send a smaller file |\n| `source_encrypted` | the container is password-protected | send an unlocked copy |\n| `source_corrupt` | the container could not be opened as the format it declares | re-export the file |\n| `unsupported_format` | no reader is registered for this format | convert it |\n| `ocr_unavailable` | the page had no text layer and no vision provider is configured | configure a vision key, or send a text-layer file |\n| `partial_extraction` | some of the source was recovered and some was not | use what came back |\n| `empty_source` | the source was read and held nothing | a fact about the file, not a failure — nothing to retry |\n| `extraction_error` | our failure | retry |",
          "enum": [
            "source_too_large",
            "source_encrypted",
            "source_corrupt",
            "unsupported_format",
            "ocr_unavailable",
            "partial_extraction",
            "empty_source",
            "extraction_error"
          ],
          "nullable": true,
          "type": "string"
        },
        "figures": {
          "description": "Every known metric found, as `{ metric, value, unit, entity?, period?, band, bound, provenance }`. Empty both when the document held no recognised metric and when the read was refused.",
          "items": {
            "type": "object"
          },
          "type": "array"
        },
        "reason": {
          "description": "Free text naming the cause, straight from whatever refused or failed — `spreadsheet_magic_mismatch:xlsx`, `spreadsheet_too_large:bytes:33554433>33554432`, `extraction_failed:File is password-protected`. Carries detail no enum can, and is NOT stable: it includes dependency error text. Display it; do not branch on it. Null when nothing refused.",
          "nullable": true,
          "type": "string"
        },
        "state": {
          "description": "How much of the source the read recovered, in one word. `complete` on a healthy read, whatever the payload beside it turned out to contain.",
          "enum": [
            "complete",
            "partial",
            "unsupported",
            "error"
          ],
          "type": "string"
        }
      },
      "required": [
        "state",
        "reason",
        "code",
        "figures"
      ],
      "type": "object"
    }
  }
}
```
