# Classify a document

Identify a document category and the document types it covers.

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

## When to use it

Classification uses extracted content and spreadsheet sheet names. Check the read state before treating a category as established. It does not certify that the document meets a diligence requirement.

## Request

POST /api/v1/classify

1 credit for a billable classification outcome.

### curl

```curl
curl --fail-with-body -X POST 'https://api.webcite.co/api/v1/classify' \
  -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/classify", {
  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/classify",
    headers={"x-api-key": os.environ["WEBCITE_API_KEY"]},
    json=payload,
    timeout=(10, 300),
)
response.raise_for_status()
print(response.json())
```

## Response

Returns category, covers, state, reason, and code. A bundled document can cover more than one type.

## 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/classify",
  "method": "POST",
  "operation": {
    "description": "Deterministic, model-free document classification. Downloads an uploaded asset, extracts its content + spreadsheet sheet names, and returns a coarse **category** plus the fine multi-type **covers** it holds (a bundled workbook covers several). Works with no model configured (air-gapped safe).\n\nPass an optional `taxonomy` preset (`vc` = venture data-room, `ma` = M&A categories; default `vc`).\n\n**`unfiled` is an answer, not a failure.** A document that was read completely and matched no category gets it, so it cannot also mean \"we never read this\". `state`, `reason` and `code` — the same three fields, the same vocabulary and the same values as `POST /extract` — say whether the classifier ever saw any content. A refused, corrupt, unsupported or unreadable asset returns `unfiled` with a non-null `code`; a genuinely empty document returns `unfiled` with `code: \"empty_source\"`; a complete read returns `code: null`. `category` and `covers` are untouched by this — anything routing on them today keeps working.\n\n**Cost: 1 credit.** Deterministic (no LLM calls), but downloads + parses the asset.",
    "operationId": "ApiV1Controller_classify",
    "parameters": [],
    "requestBody": {
      "content": {
        "application/json": {
          "schema": {
            "$ref": "#/components/schemas/ClassifyRequestDto"
          }
        }
      },
      "required": true
    },
    "responses": {
      "200": {
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/ClassifyResponseDto"
            }
          }
        },
        "description": "{ category, covers[], 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": "Classify a document into a category and covered types",
    "tags": [
      "Public API"
    ]
  },
  "schemas": {
    "ClassifyRequestDto": {
      "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 (e.g. the consumer's own signed GCS URL). One of asset_id / asset_url is required.",
          "type": "string"
        },
        "taxonomy": {
          "description": "Taxonomy preset to classify against. Default \"vc\".",
          "enum": [
            "vc",
            "ma"
          ],
          "type": "string"
        }
      },
      "type": "object"
    },
    "ClassifyResponseDto": {
      "properties": {
        "basis": {
          "enum": [
            "content",
            "filename",
            "fallback"
          ],
          "type": "string"
        },
        "category": {
          "description": "The coarse category, or `unfiled` when nothing matched. Unchanged by a refusal: check `code` to learn whether the document was read at all.",
          "type": "string"
        },
        "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"
        },
        "confidence": {
          "description": "Heuristic signal score, not a calibrated probability.",
          "maximum": 1,
          "minimum": 0,
          "type": "number"
        },
        "confidenceAssessment": {
          "properties": {
            "basis": {
              "enum": [
                "heuristic"
              ],
              "type": "string"
            },
            "calibrated": {
              "enum": [
                false
              ],
              "type": "boolean"
            },
            "language": {
              "description": "Unknown; the vocabulary census is not a document language detector.",
              "nullable": true,
              "type": "string"
            },
            "languageBasis": {
              "enum": [
                "unknown"
              ],
              "type": "string"
            },
            "perLanguageCalibration": {
              "enum": [
                "unavailable"
              ],
              "type": "string"
            }
          },
          "required": [
            "basis",
            "calibrated",
            "language",
            "languageBasis",
            "perLanguageCalibration"
          ],
          "type": "object"
        },
        "covers": {
          "description": "The fine multi-type covers the document holds. Empty when nothing matched.",
          "items": {
            "type": "string"
          },
          "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"
        },
        "sufficiency": {
          "properties": {
            "reason": {
              "type": "string"
            },
            "status": {
              "enum": [
                "unavailable"
              ],
              "type": "string"
            }
          },
          "required": [
            "status",
            "reason"
          ],
          "type": "object"
        }
      },
      "required": [
        "state",
        "reason",
        "code",
        "confidence",
        "basis",
        "category",
        "covers"
      ],
      "type": "object"
    }
  }
}
```
