> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.milliseconds.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.milliseconds.ai/_mcp/server.

# Extract

`POST /v1/decision-machine-1/extract` fills a JSON Schema from one text. You send `text` and `schema`. The model reads the text once and returns `data` in the shape of your schema. The shape supports strings, numbers, integers, booleans, enums, string arrays and nested objects. An array-of-objects property is accepted and returned empty on text; on an `image` the vision model fills it. A field the text does not carry comes back as `null`. Nothing is generated: every value comes from a span in the text.

## When to use it

* You need several fields from one document in one call: an invoice, a résumé, an order confirmation.
* You already have a JSON Schema and want it filled.
* Use [Answer](/capabilities/answer) instead when you need one field plus its character offsets.
* Use [Entities](/capabilities/entities) instead when you want every occurrence of a type, not one value per field.
* Use [Verify](/capabilities/verify) after extraction to check a critical value against the source text.

## Three example decisions

* Pull the invoice number, the total and the payment terms from a billing email.
* Turn a shipping confirmation into `{tracking_number, carrier, delivery_date}`.
* Read a job application and fill `{candidate_name, years_experience, remote_ok}`.

## Request

| Field    | Type      | Required                | Meaning                                 | Limits                                           |
| -------- | --------- | ----------------------- | --------------------------------------- | ------------------------------------------------ |
| `text`   | string    | one of `text` / `texts` | The text to read                        | 1 to 20,000 characters                           |
| `texts`  | string\[] | one of `text` / `texts` | A batch of texts                        | 1 to 32 items, each 1 to 20,000 characters       |
| `schema` | object    | yes                     | A JSON Schema that describes the output | Must be `type: "object"` with a `properties` map |

`text` and `texts` are mutually exclusive. Send one of them, never both.

> **Note**
>
> Write a `description` on every property. The description is what the model searches for. `"total_due"` alone works; `"total amount due"` works better.

#### curl

```bash
curl -X POST https://api.milliseconds.ai/v1/decision-machine-1/extract \
  -H "content-type: application/json" \
  -H "authorization: Bearer test_sk-..." \
  -d '{
    "text": "INVOICE #4471\nBilled to: Acme Corp\nCurrency: USD\nPaid: yes\nTotal due: $2,676.00\nPayment terms: Net 30",
    "schema": {
      "type": "object",
      "title": "invoice",
      "properties": {
        "invoice_number": { "type": "string", "description": "invoice number" },
        "customer": { "type": "string", "description": "the company billed" },
        "total_due": { "type": "number", "description": "total amount due" },
        "net_terms_days": { "type": "integer", "description": "payment terms in days" },
        "paid": { "type": "boolean", "description": "is the invoice paid" },
        "currency": { "enum": ["USD", "EUR", "GBP"], "description": "currency code" }
      }
    }
  }'
```

#### Python

```python
import requests

invoice_text = "INVOICE #4471\nBilled to: Acme Corp\nCurrency: USD\nPaid: yes\nTotal due: $2,676.00\nPayment terms: Net 30"

schema = {
    "type": "object",
    "title": "invoice",
    "properties": {
        "invoice_number": {"type": "string", "description": "invoice number"},
        "customer": {"type": "string", "description": "the company billed"},
        "total_due": {"type": "number", "description": "total amount due"},
        "net_terms_days": {"type": "integer", "description": "payment terms in days"},
        "paid": {"type": "boolean", "description": "is the invoice paid"},
        "currency": {"enum": ["USD", "EUR", "GBP"], "description": "currency code"},
    },
}

response = requests.post(
    "https://api.milliseconds.ai/v1/decision-machine-1/extract",
    headers={"authorization": "Bearer test_sk-..."},
    json={"text": invoice_text, "schema": schema},
    timeout=30,
)
print(response.json()["data"])
```

#### TypeScript

```typescript
const invoiceText =
  "INVOICE #4471\nBilled to: Acme Corp\nCurrency: USD\nPaid: yes\nTotal due: $2,676.00\nPayment terms: Net 30";

const schema = {
  type: "object",
  title: "invoice",
  properties: {
    invoice_number: { type: "string", description: "invoice number" },
    customer: { type: "string", description: "the company billed" },
    total_due: { type: "number", description: "total amount due" },
    net_terms_days: { type: "integer", description: "payment terms in days" },
    paid: { type: "boolean", description: "is the invoice paid" },
    currency: { enum: ["USD", "EUR", "GBP"], description: "currency code" },
  },
};

const response = await fetch(
  "https://api.milliseconds.ai/v1/decision-machine-1/extract",
  {
    method: "POST",
    headers: { "content-type": "application/json", authorization: "Bearer test_sk-..." },
    body: JSON.stringify({ text: invoiceText, schema }),
  },
);

const { data } = await response.json();
```

Every request needs an API key. See [Authentication](/authentication).

## Response

This capture ran the same schema with `tags` and `vendor` added. The call took 0.41 s.

```json
{
  "data": {
    "invoice_number": "4471",
    "customer": "Acme Corp",
    "total_due": 2676,
    "net_terms_days": 30,
    "paid": true,
    "currency": "USD",
    "tags": ["Design work", "Hosting"],
    "vendor": { "name": "Acme Corp", "city": null }
  }
}
```

* `data` — one object in the shape of your schema. Only `data` comes back. There is no probability on this endpoint.
* Scalar values carry the text's own span, coerced to your type.
* A missing value is `null`. The key stays in place.
* A batch over `texts` wraps the results: `{"results":[{"data":{...}}, ...]}`.

## Reading the numbers

Extract returns no probability, so read the values themselves.

The scalars above are all correct: `total_due` parsed `$2,676.00` into `2676`, `net_terms_days` truncated `Net 30` to `30`, and `paid` read `yes` as `true`.

One field is wrong, and the failure is typical. `vendor.name` copied the billing name, because the text has no vendor block. The model fills a described field with the closest span it finds; it does not refuse.

Treat the result as a draft. Some fields move money or write to a record. Send those values and the source text to [Verify](/capabilities/verify). Check `matches` before you commit the value.

## Batching

Send `texts` instead of `text` to run the same schema over up to 32 texts. Each text costs one inference call.

```bash
curl -X POST https://api.milliseconds.ai/v1/decision-machine-1/extract \
  -H "content-type: application/json" \
  -H "authorization: Bearer test_sk-..." \
  -d '{
    "texts": ["Total due: $2,676.00", "Total due: $10.00"],
    "schema": {
      "type": "object",
      "properties": {
        "total_due": { "type": "number", "description": "total amount due" }
      }
    }
  }'
```

```json
{"results":[{"data":{"total_due":2676}},{"data":{"total_due":10}}]}
```

Results keep the input order. [Batching](/capabilities/batching) covers the shape on every capability.

## Schema support matrix

| Construct                                    | Supported                                                                       | Behaviour                                                                                                               |
| -------------------------------------------- | ------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
| `{"type":"string"}`                          | Yes                                                                             | The raw span                                                                                                            |
| No `type` at all                             | Yes                                                                             | Treated as a string                                                                                                     |
| `{"type":"number"}`                          | Yes                                                                             | Non-numeric characters are stripped: `$2,676.00` becomes `2676`. Unparseable becomes `null`.                            |
| `{"type":"integer"}`                         | Yes                                                                             | Same, then truncated                                                                                                    |
| `{"type":"boolean"}`                         | Yes                                                                             | `true`, `yes`, `y` and `1` become `true`                                                                                |
| `{"enum":[...]}`                             | Yes                                                                             | The model picks one member. `enum` wins over `type`.                                                                    |
| `{"type":"array","items":{"type":"string"}}` | Yes                                                                             | An array of strings                                                                                                     |
| Array of another scalar                      | Partly                                                                          | Returns strings, not numbers                                                                                            |
| Nested object                                | Yes                                                                             | Returned in its nested shape                                                                                            |
| Array of objects                             | On text, accepted and returned as an empty array for now. On an `image`, filled | On text the property stays in `data` and holds `[]`                                                                     |
| `{"type":["string","null"]}`                 | Yes                                                                             | The first non-`null` type wins                                                                                          |
| `{"type":"null"}`                            | No                                                                              | The key is dropped from `data`                                                                                          |
| `oneOf`, `anyOf`, `allOf`, `$ref`            | No                                                                              | Read as a plain string, so the value is usually `null`                                                                  |
| Array of arrays                              | No                                                                              | Resolves to an array of strings                                                                                         |
| `required`                                   | Parsed, not enforced                                                            | A required field can still come back `null`                                                                             |
| `title`, `description`                       | Read                                                                            | `description` (else `title`, else the field name) tells the model what to look for. A root `title` names the structure. |

This call used `prices` as an array of number, `day` as a string, `meta` as `{"type":"null"}` and `weird` as an `anyOf`:

```json
{"data":{"prices":["30","20","10"],"day":"Tuesday","weird":null}}
```

The prices came back as strings, and `weird` came back `null`. The key `meta` is absent from `data`.

## Limits and gotchas

* `text` takes up to 20,000 characters. `texts` takes up to 32 items, each up to 20,000 characters.
* Send `text` or `texts`, never both. Sending neither also fails, with the same message: `body: provide text, texts or image`.
* A root schema that is not an object with `properties` returns `400`:

```json
{"error":{"code":"invalid_schema","message":"schema must be an object with properties"}}
```

* The common mistake is a bare field name with no `description`. Describe the field in the words the document uses.
* The second mistake is trusting `required`. Handle `null` in your own code.
* Array order is not stable between calls. Compare arrays by value, not by position.
* Keep schemas small. Root scalar fields run in groups of four, because one large structure makes fields compete for the same span.
* Text over 2,000 characters is chunked, and the records from each chunk merge. [Long text and chunking](/concepts/long-text) covers the rules.
* Extract runs on the multilingual model, so it reads non-English text. [Languages](/concepts/languages) gives the guidance.
* Observed latency: 0.41 s for a nine-property schema, 0.44 s for two texts.

## Images

Send `image` instead of `text` to run this capability over a photograph or a scanned page. `image` is
a `data:image/jpeg;base64,` URL or bare base64 of a JPEG, PNG or WebP, at most 5 MB decoded. `detail`
picks the resolution: `low` is 512 px, `medium` is 768 px and is the default, `high` is 1024 px.

```bash
curl -X POST https://api.milliseconds.ai/v1/decision-machine-1/extract \
  -H "content-type: application/json" \
  -H "authorization: Bearer test_sk-..." \
  -d '{
    "image": "data:image/jpeg;base64,/9j/4AAQSkZJRg...",
    "detail": "low",
    "schema": {
      "type": "object",
      "properties": {
        "merchant": { "type": "string" },
        "total": { "type": "number" }
      }
    }
  }'
```

The response carries the same `data` as on text. On an image the vision model fills the schema
itself, so a property typed as an array of objects comes back filled, not empty.

`image` works with `text`, never with `texts`. The image bills a fixed number of tokens:
2,000 for `low`, 4,000 for `medium`, 8,000 for `high`. The rest of the body bills as today. See [Images](/concepts/images).

> **Warning**
>
> **Extract on an image is a generative pass.** Use it for a handful of fields off one document
> image, never for full-page transcription. A call takes 2.5 to 3.5 seconds.
>
> The schema accepts at most 5 fields on an image. A larger schema returns `400` with the code
> `image_schema_too_large`. Count every leaf property. Nested object properties count, and the
> properties of an array-of-objects item count once each: `{merchant, total, items[{name, price}]}`
> is 4 fields.
>
> For whole pages and long documents, use the CloudRaker paperwork API at
> [cloudraker.com](https://cloudraker.com): it parses, extracts and grounds full documents with no
> field cap. Text you already hold goes to the text `extract` here. See [Images](/concepts/images).

## Next

* [Extract, then verify](/patterns/extract-then-verify) — check a value before you write it anywhere.
* [Invoice extraction with verification](/recipes/invoice-extraction) — the invoice header schema with a review queue.
* [Structured extraction](/openai/structured-extraction) — the same capability through an OpenAI client.