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

# Classify Document

> Lightweight routing step that runs **before** `/extract`. Given a raw document
(file upload or `file_url`) and a set of caller-defined classifications,
it returns which classification the document belongs to — plus that classification's
`pipeline_id`, so you can send the document to the right pipeline next.

By default, it evaluates the first five pages. For PDFs and images,
billing is based on the effective `page_range`. For Office and HTML
files, billing applies to every page.

Billed at **0.5 credits per page** — half the `/extract` rate.

Accepted document types are identical to `/extract`, including the same
size limits and URL validation.

Set `async: true` to return immediately with a job_id for polling via
GET /job/{jobId}. Otherwise processes synchronously.

## Overview

<Info>
  **Routing step (optional, runs before extraction)** — Classify takes a **raw document**, not a saved extraction. Use it to decide which pipeline (and therefore which [`/extract`](/api-reference/endpoint/extract) settings) a document should be routed to.
</Info>

`POST /classify` is a lightweight routing step that runs **before** `/extract`. Given a raw document and a set of caller-defined classifications, it returns which classification the document belongs to — plus that classification's `pipeline_id`, so you can send the document to the right pipeline next.

Why classify *before* extract: extraction settings (chunking, schema, tables, …) differ per document type, so you need to know the type before you extract. By default, `/classify` evaluates the first five pages and costs **0.5 credits per page** — half the `/extract` rate. See [Credits](#credits).

### Accepted Document Types

`/classify` uses the same upload path as [`/extract`](/api-reference/endpoint/extract) — the same accepted formats, the same size limits, and both direct file uploads and `file_url`.

| Category | Extensions                                          |
| -------- | --------------------------------------------------- |
| PDF      | `.pdf`                                              |
| Images   | `.jpg`, `.jpeg`, `.png`, `.webp`                    |
| Office   | `.docx`, `.pptx`, `.xlsx`, `.xlsm`, `.xls`, `.xlsb` |
| Data     | `.csv`                                              |
| Web      | `.html`, `.htm`                                     |

Formats that need normalizing (`.webp`, `.csv`, `.xlsb`) are converted on upload before classification, exactly as they are for `/extract`.

<Note>
  How a format is handled affects billing, not acceptance: PDFs and images are billed only for the pages in the effective `page_range`, while Office and HTML files are billed for every page. See [Credits](#credits).
</Note>

### Async Mode

Set `async: true` to return immediately with a job ID for polling. See [Polling for Results](/api-reference/endpoint/poll). On completion, the job's result carries the same body as the sync response.

<Note>
  `/classify` is currently REST-only: it is not yet exposed as a Python/TypeScript SDK method, a CLI command, or an MCP tool. Call it over HTTP as shown below.
</Note>

***

## Request

Provide **either** `file` (multipart upload) **or** `file_url` (JSON body) — not both.

### Request Body

| Field             | Type    | Required | Description                                                                                                   |
| ----------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------- |
| `file`            | file    | XOR      | The document to classify (multipart/form-data upload).                                                        |
| `file_url`        | string  | XOR      | URL of the document to classify (application/json body).                                                      |
| `classify_config` | object  | Yes      | Candidate classifications (JSON **string** when sent as a multipart form field).                              |
| `page_range`      | string  | No       | Pages to inspect, e.g. `"1-5"` or `"1,3,5-7"`. Default: first 5 pages, clamped to the document length.        |
| `async`           | boolean | No       | If `true`, returns immediately with a `job_id` for [polling](/api-reference/endpoint/poll). Default: `false`. |
| `parent_job_id`   | uuid    | No       | Optional parent job ID for tracking.                                                                          |

### Classify Config (`classify_config`)

```jsonc theme={null}
{
  "classifications": {
    "<name>": {
      "description": "string, required — what documents belong to this class",
      "pipeline_id": "string, optional — pipeline to route matches to"
    }
    // ... one entry per candidate classification
  }
}
```

* `<name>` is any label you choose; Pulse returns exactly one of these names.
* `description` tells Pulse what belongs in each class — make it specific and mutually distinct.
* `pipeline_id` is **optional routing metadata**. When provided, it must reference a pipeline your organization owns (otherwise the request is rejected with `PIPELINE_003`). The matched classification's `pipeline_id` is echoed back so you can route the document next.

### Always include a catch-all classification

<Warning>
  **Classification is forced-choice.** `/classify` always returns one of the names you supplied — there is no built-in "none of the above", no `null` result, and no confidence score. A document that matches none of your classifications is not rejected: it is assigned to whichever class comes closest, with nothing in the response to flag it as a poor match.
</Warning>

Define the escape hatch yourself: add a catch-all classification and **leave off its `pipeline_id`**.

```jsonc theme={null}
{
  "classifications": {
    "bank_statement": {
      "description": "Bank or account statements: balances, transaction lists, deposits and withdrawals.",
      "pipeline_id": "b1a2c3d4-..."
    },
    "invoice": {
      "description": "Invoices or bills requesting payment: line items, amounts due, payment terms.",
      "pipeline_id": "e5f6a7b8-..."
    },
    "unrecognized": {
      // No pipeline_id — so classify_output.pipeline_id comes back null
      "description": "Any document that does not clearly match one of the other classifications — including unrelated document types, blank or illegible scans, and standalone cover pages."
    }
  }
}
```

A `null` `classify_output.pipeline_id` then becomes your "don't extract this" signal:

```json theme={null}
{
  "classification": "unrecognized",
  "classify_output": {
    "classification": "unrecognized",
    "pipeline_id": null,
    "page_range": "1-5"
  }
}
```

Branch on it before you route — send those documents to manual review instead of into a pipeline built for a different document type.

<Tip>
  Give the catch-all a description that says what it absorbs, not just the word "other". A thin description competes poorly against your detailed classes, and Pulse will keep preferring a wrong-but-well-described class over a vague catch-all. For the same reason, keep the specific descriptions tight — a broad description like "financial documents" pulls in documents the catch-all should have caught.
</Tip>

Without a catch-all there is no clean "no match" outcome — you get either a confidently wrong classification, or a `PROC_001` error in the case where Pulse answers off-list, since any name outside your `classifications` keys is rejected.

***

## Response

### Synchronous Response (200)

| Field                            | Type         | Description                                                                                                                                          |
| -------------------------------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `classification`                 | string       | The winning classification name — always one of your `classifications` keys ([never a "no match" value](#always-include-a-catch-all-classification)) |
| `classify_output.classification` | string       | Same as `classification`                                                                                                                             |
| `classify_output.pipeline_id`    | uuid \| null | The matched classification's `pipeline_id`, or `null` if it had none — see [catch-all classifications](#always-include-a-catch-all-classification)   |
| `classify_output.page_range`     | string       | The effective page range used for classification (after defaulting/clamping)                                                                         |
| `job_id`                         | uuid         | Identifier of the request                                                                                                                            |
| `credits_used`                   | number       | Credits deducted for this call                                                                                                                       |

### Async Response (202)

| Field     | Type   | Description                                        |
| --------- | ------ | -------------------------------------------------- |
| `job_id`  | string | Job ID for [polling](/api-reference/endpoint/poll) |
| `status`  | string | `"pending"`                                        |
| `message` | string | Human-readable description                         |

***

## Example Usage

<CodeGroup>
  ```bash curl (file upload) theme={null}
  curl -X POST https://api.runpulse.com/classify \
    -H "x-api-key: YOUR_API_KEY" \
    -F "file=@document.pdf" \
    -F 'classify_config={
      "classifications": {
        "bank_statement": {
          "description": "Bank or account statements: balances, transaction lists, deposits and withdrawals.",
          "pipeline_id": "b1a2c3d4-..."
        },
        "invoice": {
          "description": "Invoices or bills requesting payment: line items, amounts due, payment terms.",
          "pipeline_id": "e5f6a7b8-..."
        },
        "unrecognized": {
          "description": "Any document that does not clearly match one of the other classifications, including unrelated types and illegible scans."
        }
      }
    }'
  ```

  ```bash curl (file URL) theme={null}
  curl -X POST https://api.runpulse.com/classify \
    -H "x-api-key: YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "file_url": "https://example.com/document.pdf",
      "page_range": "1-5",
      "classify_config": {
        "classifications": {
          "bank_statement": {
            "description": "Bank or account statements: balances, transaction lists.",
            "pipeline_id": "b1a2c3d4-..."
          },
          "invoice": {
            "description": "Invoices or bills requesting payment.",
            "pipeline_id": "e5f6a7b8-..."
          },
          "unrecognized": {
            "description": "Any document that does not clearly match one of the other classifications, including unrelated types and illegible scans."
          }
        }
      }
    }'
  ```

  ```python Python theme={null}
  import json
  import requests

  resp = requests.post(
      "https://api.runpulse.com/classify",
      headers={"x-api-key": "YOUR_API_KEY"},
      files={"file": open("document.pdf", "rb")},
      data={
          "classify_config": json.dumps({
              "classifications": {
                  "bank_statement": {
                      "description": "Bank or account statements: balances, transaction lists.",
                      "pipeline_id": "b1a2c3d4-...",
                  },
                  "invoice": {
                      "description": "Invoices or bills requesting payment.",
                      "pipeline_id": "e5f6a7b8-...",
                  },
                  "unrecognized": {
                      "description": "Any document that does not clearly match one of the other classifications, including unrelated types and illegible scans.",
                  },
              }
          })
      },
  )
  result = resp.json()
  print(result["classification"], result["classify_output"]["pipeline_id"])
  ```
</CodeGroup>

### Example Response

```json theme={null}
{
  "classification": "bank_statement",
  "classify_output": {
    "classification": "bank_statement",
    "pipeline_id": "b1a2c3d4-...",
    "page_range": "1-5"
  },
  "job_id": "e3b0c442-...",
  "credits_used": 2.5
}
```

<Note>
  `classification` is returned at the top level for quick access and repeated inside `classify_output`, which keeps the routing decision (`classification`, `pipeline_id`, `page_range`) self-contained for pipeline consumers. Both always hold the same value.
</Note>

***

## Routing to a Pipeline

`/classify` only tells you *which* pipeline to use — it does not execute it. Take the returned `pipeline_id` and run that pipeline with the same document; its own extraction settings then apply.

`classify` can also be used as the **first step of an ad-hoc pipeline**, where it classifies the raw document before the downstream steps run. It cannot be combined with `batch_extract` (classify needs a single document), and it is currently supported with inline config only.

***

## Credits

`/classify` is billed at **0.5 credits per page** — half the `/extract` rate (1 credit/page).

* **PDFs / images:** billed for the pages in the (defaulted/clamped) `page_range`.
* **Office / HTML:** billed for every page, even when `page_range` is smaller.

See the full rate table in [Credit Usage](/api-reference/introduction#credit-usage).

***

## Error Responses

| Status | Error               | Description                                                                                                                                                                     |
| ------ | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 400    | `REQ_004`           | Missing `classify_config` or empty `classifications`                                                                                                                            |
| 400    | `REQ_002`           | `classify_config` is not valid JSON                                                                                                                                             |
| 400    | `REQ_006`           | Invalid or out-of-range `page_range`                                                                                                                                            |
| 400    | `PIPELINE_003`      | A classification's `pipeline_id` doesn't exist for your organization                                                                                                            |
| 400    | `FILE_*`            | Unsupported file type, file too large, or bad/blocked URL (same as `/extract`)                                                                                                  |
| 401    | Unauthorized        | Invalid or missing API key                                                                                                                                                      |
| 429    | Rate limit exceeded | Too many requests                                                                                                                                                               |
| 500    | `PROC_001`          | Processing failed — including the case where Pulse returns a name that isn't one of your `classifications` keys ([add a catch-all](#always-include-a-catch-all-classification)) |

***

## Best Practices

<AccordionGroup>
  <Accordion title="Include a catch-all classification">
    Classification is forced-choice, so a document that fits none of your classes is still assigned to one of them. A catch-all with no `pipeline_id` gives unexpected documents somewhere to land and gives you a `null` `pipeline_id` to branch on. See [Always include a catch-all classification](#always-include-a-catch-all-classification).
  </Accordion>

  <Accordion title="Write specific, mutually distinct descriptions">
    The description tells Pulse what belongs in each class. Describe what the documents contain, not just their name, and make classes clearly distinguishable from each other. This matters most for the catch-all, which loses to better-described classes when its own description is vague.
  </Accordion>

  <Accordion title="Keep the default page range unless you have a reason not to">
    The first 5 pages are usually enough to identify a document type. An explicit `page_range` has no hard cap, but large ranges scale credits linearly.
  </Accordion>

  <Accordion title="Treat the result as routing guidance">
    Classification is a probabilistic hint, not a guarantee, and the response carries no confidence score — a borderline guess and a certain match look identical. Validate downstream where correctness matters. Nothing from `/classify` is persisted — the routed pipeline's `/extract` produces the durable output.
  </Accordion>
</AccordionGroup>


## OpenAPI

````yaml POST /classify
openapi: 3.1.0
info:
  title: Pulse API Structure
  version: 0.1.0
  description: >-
    Canonical contract for the Pulse extraction APIs. This specification is the
    single source of truth for shared request/response models that client and
    server packages consume.
servers:
  - url: https://api.runpulse.com
    description: Default Pulse API base URL
security:
  - ApiKey: []
paths:
  /classify:
    post:
      tags:
        - Classify
      summary: Classify a document to route it to a pipeline
      description: >-
        Lightweight routing step that runs **before** `/extract`. Given a raw
        document

        (file upload or `file_url`) and a set of caller-defined classifications,

        it returns which classification the document belongs to — plus that
        classification's

        `pipeline_id`, so you can send the document to the right pipeline next.


        By default, it evaluates the first five pages. For PDFs and images,

        billing is based on the effective `page_range`. For Office and HTML

        files, billing applies to every page.


        Billed at **0.5 credits per page** — half the `/extract` rate.


        Accepted document types are identical to `/extract`, including the same

        size limits and URL validation.


        Set `async: true` to return immediately with a job_id for polling via

        GET /job/{jobId}. Otherwise processes synchronously.
      operationId: classifyDocument
      requestBody:
        required: true
        content:
          multipart/form-data:
            schema:
              $ref: '#/components/schemas/ClassifyInput'
            encoding:
              classify_config:
                contentType: application/json
          application/json:
            schema:
              $ref: '#/components/schemas/ClassifyInput'
      responses:
        '200':
          description: Classification result (when async=false or omitted)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClassifyResponse'
        '202':
          description: Classify job accepted (when async=true)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AsyncSubmissionResponse'
        '400':
          description: >-
            Invalid request parameters, unsupported file type, or a
            classification references a pipeline_id that does not exist for your
            organization
        '401':
          description: Authentication failed or missing API key
        '429':
          description: Rate limit exceeded
components:
  schemas:
    ClassifyInput:
      type: object
      description: |-
        Request body for classifying a document. Provide exactly one of
        `file` (multipart upload) or `file_url` (JSON body). In
        multipart/form-data requests, `classify_config` is sent as a JSON
        string form field.
      required:
        - classify_config
      properties:
        file:
          type: string
          format: binary
          description: >-
            Document to upload directly (multipart/form-data only). Required
            unless file_url is provided.
        file_url:
          type: string
          format: uri
          description: >-
            Public or pre-signed URL that Pulse will download and classify.
            Required unless file is provided.
        classify_config:
          allOf:
            - $ref: '#/components/schemas/ClassifyConfig'
          description: Candidate classifications the document is matched against.
        page_range:
          type: string
          description: >-
            Pages to inspect, e.g. "1-5" or "1,3,5-7". Defaults to the first 5
            pages, clamped to the document length. For Office/HTML formats,
            every page is billed.
        async:
          type: boolean
          default: false
          description: >-
            If true, returns immediately with a job_id for polling via GET
            /job/{jobId}. Otherwise processes synchronously.
        parent_job_id:
          type: string
          format: uuid
          description: Optional parent job id for tracking.
    ClassifyResponse:
      type: object
      description: Result of document classification.
      required:
        - classification
        - classify_output
      properties:
        classification:
          type: string
          description: >-
            The winning classification name — one of the keys from the request's
            classifications object.
        classify_output:
          description: Details of the winning classification.
          allOf:
            - $ref: '#/components/schemas/ClassifyOutput'
        job_id:
          type: string
          format: uuid
          description: Identifier of the request for tracking and support.
        credits_used:
          type: number
          format: float
          nullable: true
          description: Number of credits consumed by this request.
    AsyncSubmissionResponse:
      type: object
      description: >-
        Acknowledgement returned when a request is submitted for asynchronous
        processing. Poll `GET /job/{job_id}` to check status and retrieve
        results.
      required:
        - job_id
        - status
      properties:
        job_id:
          type: string
          description: Identifier assigned to the asynchronous job.
        status:
          type: string
          description: Initial status reported by the server.
          enum:
            - pending
            - processing
            - completed
            - failed
            - canceled
        message:
          type: string
          description: Human-readable description of the accepted job.
        queuedAt:
          type: string
          format: date-time
          deprecated: true
          description: >-
            **Deprecated** — Timestamp indicating when the job was accepted.
            Retained for backward compatibility. Use `GET /job/{jobId}` for
            timing details.
        credits_used:
          type: number
          format: float
          nullable: true
          description: >-
            Number of credits consumed by this request. Only present when the
            organization has the credit billing system enabled.
    ClassifyConfig:
      type: object
      description: >-
        Candidate classifications, keyed by a caller-chosen name. Pulse returns
        exactly one of these names.
      required:
        - classifications
      properties:
        classifications:
          type: object
          minProperties: 1
          description: >-
            Mapping of classification names to their definitions. Include a
            catch-all entry (e.g. "other") for documents that fit nothing else.
          additionalProperties:
            $ref: '#/components/schemas/ClassificationDefinition'
    ClassifyOutput:
      type: object
      description: Details of the winning classification.
      properties:
        classification:
          type: string
          description: >-
            The winning classification name — one of the keys from the request's
            classifications object.
        pipeline_id:
          type: string
          format: uuid
          nullable: true
          description: >-
            The matched classification's pipeline_id, or null if it had none.
            Run this pipeline with the same document as the next step.
        page_range:
          type: string
          description: >-
            The effective page range used for classification, after defaulting
            and clamping to the document length.
    ClassificationDefinition:
      type: object
      description: A candidate classification a document may belong to.
      required:
        - description
      properties:
        description:
          type: string
          description: >-
            What documents belong to this classification. Make it specific and
            mutually distinct from the other classifications.
        pipeline_id:
          type: string
          format: uuid
          description: >-
            Optional routing metadata. When provided, it must reference a
            pipeline your organization owns. The matched classification's
            pipeline_id is echoed back in the response so you can route the
            document next.
  securitySchemes:
    ApiKey:
      type: apiKey
      in: header
      name: x-api-key
      x-fern-header:
        name: apiKey
        env: PULSE_API_KEY

````