Metadata-Version: 2.4
Name: suparse
Version: 1.5.0
Summary: Official Python SDK and CLI for the Suparse Document Processing API
License-Expression: MIT
License-File: LICENSE
Keywords: document-processing,ocr,invoice,receipt,api,sdk
Author: Suparse
Author-email: support@suparse.com
Requires-Python: >=3.10
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Typing :: Typed
Requires-Dist: httpx (>=0.27.0)
Requires-Dist: pydantic (>=2.6.0)
Requires-Dist: pydantic-settings (>=2.2.0)
Requires-Dist: tenacity (>=8.2.0)
Project-URL: Homepage, https://suparse.com
Description-Content-Type: text/markdown

# Suparse Python SDK

<!-- docs-sync: omit-web -->
<!--
  Documentation synchronization: content between the paired
  docs-sync: omit-web markers is kept in this repository README but omitted
  from the customer-facing SDK/CLI documentation page. Use the markers for
  repository-only notes, badges, or links, and keep each marker on its own line.
-->
[![PyPI version](https://img.shields.io/pypi/v/suparse)](https://pypi.org/project/suparse/)
[![Python](https://img.shields.io/pypi/pyversions/suparse)](https://pypi.org/project/suparse/)
<!-- /docs-sync: omit-web -->

Official Python SDK and CLI for the [Suparse](https://suparse.com) Document Processing API.

Suparse is an AI-powered document processing API for extracting structured data from any document type, including invoices, receipts, bank statements, purchase orders and many more. This SDK wraps the REST API with a Python client and CLI.

## Requirements

Python 3.10+ is required. `pip install suparse` automatically installs the SDK
dependencies: `httpx`, `pydantic`, `pydantic-settings`, and `tenacity`.

## Installation

```bash
pip install suparse
```

## Authentication

You'll need an API key to use the SDK or CLI. To obtain one:

1. Sign in at [suparse.com](https://suparse.com)
2. Go to the **API Keys** tab
3. Enter a key name and click **Generate New Key**
4. Copy the key value — it will be shown only once

Set it as an environment variable:

```bash
export SUPARSE_API_KEY="your_api_key_here"
```

For the CLI, you can also store it in `~/.config/suparse/config.json`:

```json
{
  "apiKey": "your_api_key_here"
}
```

Or pass it directly to the SDK constructor (see below).

## Quick Start

With `SUPARSE_API_KEY` set, process a document from the terminal:

```bash
suparse process invoice.pdf -o results.json
```

Or use Python synchronously:

```python
from suparse import SuparseClient

with SuparseClient() as client:
    result = client.extract("invoice.pdf")
    for document in result.succeeded:
        print(document.original_file, document.data)
    for failure in result.failed:
        print(f"Failed: {failure.file}: {failure.error}")
```

For an async application:

```python
import asyncio
from suparse import AsyncSuparseClient

async def main():
    async with AsyncSuparseClient() as client:
        result = await client.extract("invoice.pdf")
        for document in result.succeeded:
            print(document.original_file, document.data)
        for failure in result.failed:
            print(f"Failed: {failure.file}: {failure.error}")

asyncio.run(main())
```

Always inspect `result.failed`, even for a single file: per-file failures are
returned there without raising an exception. Setup or cleanup errors can still
raise exceptions. See [Error Handling](#error-handling).

## CLI Usage

Run `suparse --help` or `suparse process --help` for full usage information.

Set your API key and process a file directly from the terminal. The SDK will auto-upload, poll, and download the resulting JSON.

### Process a Document

```bash
export SUPARSE_API_KEY="your_api_key_here"

# Auto-detect template
suparse process path/to/invoice.pdf -o results.json

# Use specific template without auto-splitting
suparse process path/to/invoice.pdf --template-id YOUR_TEMPLATE_ID

# Auto-split a multi-page PDF containing mixed document types (e.g. receipts + bank statements)
suparse process path/to/merged.pdf --with-split

# Use specific template with auto-splitting
suparse process path/to/invoice.pdf --template-id YOUR_TEMPLATE_ID --with-split

# Process and auto-delete documents from server after download
suparse process path/to/invoice.pdf --cleanup

# Export directly to XLSX
suparse process path/to/invoice.pdf --format xlsx

# Choose the flat XLSX layout
suparse process path/to/invoice.pdf --format xlsx --xlsx-layout flat

# Export CSV using original template columns
suparse process path/to/invoice.pdf --format csv --export-type original

# Process an encrypted PDF without putting the password in shell history
suparse process path/to/encrypted.pdf --prompt-pdf-password

# Assign uploaded documents to a folder
suparse process path/to/invoice.pdf --folder-id <folder-id>

# Start automatic schema creation and wait for the generated template
suparse quick-create path/to/example.pdf --wait

# Start schema creation for an encrypted PDF without putting the password in shell history
suparse quick-create path/to/encrypted.pdf --prompt-pdf-password

# Inspect a previously started schema-creation run
suparse quick-create-status <run-id>
```

### Process a Folder

Process all supported files (`.pdf`, `.jpg`, `.jpeg`, `.png`, `.heic`, `.heif`) in a folder. All files are uploaded and polled individually. By default, results are combined into a single JSON file.

Use `--format csv` or `--format xlsx` to export spreadsheet files; exports that
produce multiple files may be returned as a ZIP. Use `--format google_sheets`
to export to Google Sheets (requires the Google Sheets integration).

```bash
# Process all supported files in a folder
suparse process --folder path/to/receipts/

# Output to a specific file (default: {folder_name}_results.json)
suparse process --folder path/to/receipts/ -o all_results.json

# Process a folder with a specific template
suparse process --folder path/to/receipts/ --template-id YOUR_TEMPLATE_ID

# Process a folder with auto-splitting enabled
suparse process --folder path/to/receipts/ --with-split

# Process a folder and auto-delete documents from server after download
suparse process --folder path/to/receipts/ --cleanup

# Export a folder to CSV, XLSX, or Google Sheets
suparse process --folder path/to/receipts/ --format csv
suparse process --folder path/to/receipts/ --format xlsx -o ./exports
suparse process --folder path/to/receipts/ --format google_sheets
```

### Export Existing Documents

Export documents by their IDs without uploading them again:

```bash
suparse export DOCUMENT_ID --format json -o results.json
suparse export DOCUMENT_ID --format xlsx --xlsx-layout flat -o export.xlsx
suparse export DOCUMENT_ID --format csv --export-type original -o export.csv
suparse export DOCUMENT_ID --format xlsx --wait-timeout 900 \
  --idempotency-key 44444444-4444-4444-8444-444444444444 -o export.xlsx
```

`--wait-timeout` controls how long to wait for a background export (default: 900
seconds). `--idempotency-key` accepts a UUID; save it to recover an uncertain
submission. Accepted job IDs and recovery instructions are printed to stderr.

### Choose an Export Mode and XLSX Layout

`--export-type` controls which template defines the columns for CSV, XLSX, and
Google Sheets. The SDK uses the same values through `ExportType`.

| Mode | Column selection | When to use it |
|------|------------------|----------------|
| `original` | Each document's historical processing template | Preserve the fields and columns used when that document was processed |
| `unified` | Documents grouped by template group, using the latest active template's columns | Combine documents processed with different versions of the same template for reporting |

Unified export projects existing extracted values onto the selected columns;
it does not reprocess documents. Fields missing from older results appear blank.
Different template groups remain separate; an export producing multiple files
may be returned as a ZIP.

`--xlsx-layout` controls how XLSX data is arranged, independently of export mode:

| Layout | Organization | When to use it |
|--------|--------------|----------------|
| `standard` (default) | Document-level fields in a summary sheet; line items in a separate items sheet | Review document summaries and item details separately |
| `flat` | Document-level fields repeated beside each line-item row in a data sheet | Filter, build pivot tables, or import rows into another tool |

For example, an invoice `INV-001` from `Acme` with two items would have these
simplified data sheets (actual columns come from your template):

**Standard — summary sheet**

| Invoice | Supplier |
|---------|----------|
| INV-001 | Acme |

**Standard — items sheet**

| Item | Amount |
|------|--------|
| Paper | 10 |
| Ink | 20 |

**Flat — data sheet**

| Invoice | Supplier | Item | Amount |
|---------|----------|------|--------|
| INV-001 | Acme | Paper | 10 |
| INV-001 | Acme | Ink | 20 |

The example illustrates unified output; original standard output presents
summary fields as key/value pairs. Unified workbooks also include a `FileName`
column to identify the source document.

```bash
suparse export DOCUMENT_ID --format xlsx --export-type unified --xlsx-layout standard -o standard.xlsx
suparse export DOCUMENT_ID --format xlsx --export-type unified --xlsx-layout flat -o flat.xlsx
```

### Read Task Results and Resume Exports

```bash
# Read currently available results; these commands do not wait for processing.
suparse task-results TASK_ID
suparse task-results TASK_ID --jsonl
suparse task-results TASK_ID --output results.json --overwrite
suparse task-results TASK_ID --single-page --cursor TOKEN --page-size 50

# Inspect, wait for, and download a background export.
suparse export-job-status JOB_ID --format json
suparse export-job-wait JOB_ID --timeout 900 --format json
suparse export-job-download JOB_ID -o export.xlsx --overwrite
```

`task-results` prints a JSON array by default, streams JSONL with `--jsonl`, or
prints a page containing `documents` and `next_cursor` with `--single-page`.
`--page-size` accepts 1–100 (default: 100). `--cursor` requires `--single-page`;
`--jsonl` and `--single-page` are mutually exclusive.

Task-result file output is saved only after all requested pages are read
successfully. Replacing an existing file requires `--overwrite`. JSONL written
to stdout may contain partial results if a later page fails; errors are printed
to stderr.

`export-job-download` requires a completed job and does not wait. Omit `-o` to
use the artifact filename in the current directory, or pass an existing output
directory. Replacing a file requires `--overwrite`. A wait timeout does not
cancel the server job; run `export-job-wait` again to resume waiting.

CLI exit codes are 0 for success, 1 for execution failure, and 2 for invalid
arguments. Check schema-creation result status as described in
[Automatic Schema Creation](#automatic-schema-creation): starting a
run successfully does not mean schema creation has completed.

### Delete Documents

```bash
# Delete one or more documents by ID (prompts for confirmation)
suparse delete <document_id>
suparse delete <id1> <id2> <id3>

# Skip confirmation prompt
suparse delete <id1> <id2> -y
```

Deleting a parent document automatically deletes all its child documents (server-side cascade).

### List Available Templates

Templates define how a document type (invoice, receipt, bank statement, etc.) is parsed. Before processing a document, check which templates are already assigned to your account:

```bash
# List templates assigned to your account (table format)
suparse templates

# List templates in JSON format
suparse templates --format json

# Include all system templates (not yet assigned to your account)
suparse templates --include-system

# Request complete template records, including schema and saved document IDs
suparse templates --view full --format json
```

The recommended way to process documents is with auto-split enabled (`--with-split`), which handles both single-type and mixed document types automatically:

```bash
suparse process path/to/documents.pdf --with-split -o results.json
```

If you consistently process one document type, look up the template ID and pass it directly:

1. Run `suparse templates` to see templates assigned to your account.
2. Use the matching template ID:
   ```bash
   suparse process invoice.pdf --template-id <id> -o results.json
   ```
3. If no template matches, run `suparse templates --include-system` to browse all system templates. Assign one to your account via the Suparse UI.
4. If no system template fits, create a custom template using the template creator in the Suparse UI.

### Configuration

The CLI reads settings from environment variables or a `.env` file in the working directory:

| Variable | Default | Description |
|----------|---------|-------------|
| `SUPARSE_API_URL` | `https://api.suparse.com/api/v1/` | API base URL |
| `SUPARSE_API_KEY` | — | Your API key (required) |
| `POLL_INTERVAL` | `5` | Seconds between polling attempts |
| `MAX_POLL_ATTEMPTS` | `300` | Max polling attempts before timeout |
| `LOG_LEVEL` | `INFO` | Logging level |

Priority: CLI flags > environment variables > `.env` file > defaults.

For API keys, the CLI also checks `~/.config/suparse/config.json` after `SUPARSE_API_KEY` and `.env`. The config file should contain a JSON object with an `apiKey` string.

### Global Options

Place global options before the subcommand, for example
`suparse --verbose process invoice.pdf`. Use `suparse --version` to print the
installed CLI version.

| Option | Description |
|--------|-------------|
| `--api-url` | API URL (default: from `SUPARSE_API_URL` env var) |
| `--api-key` | API Key (default: from `SUPARSE_API_KEY` env var) |
| `-v`, `--verbose` | Enable verbose (DEBUG) output |

### CLI Options

| Command | Option | Description |
|---------|----------|-------------|
| `process` | `--folder` | Process all supported files in a folder (mutually exclusive with file path) |
| `process`, `export` | `-o`, `--output` | Output file path or existing output directory; omit to use a default filename |
| `process` | `--template-id` | Template ID to use (default: auto-detect) |
| `process` | `--with-split` | Auto-split multi-page PDFs containing mixed document types (e.g. receipts + bank statements) so each is processed with the correct template (default: off) |
| `process` | `--cleanup` | Delete uploaded documents after processing; cleanup is deferred if an export may still be running (default: off) |
| `process`, `export` | `--format` | Export format: `json`, `csv`, `xlsx`, or `google_sheets` (default: `json`) |
| `process`, `export` | `--export-type` | Export mode for CSV, XLSX, and Google Sheets: `unified` or `original` (default: `unified`) |
| `process`, `export` | `--xlsx-layout` | XLSX layout: `standard` or `flat` (default: `standard`) |
| `process` | `--folder-id` | Folder ID applied to every uploaded input |
| `process` | `--prompt-pdf-password` | Prompt securely for an encrypted PDF password |
| `templates` | `--format` | Output format: `table` or `json` (default: `table`) |
| `templates` | `--include-system` | Include system templates not yet assigned to your account (default: off) |
| `templates` | `--view` | Template response view: `summary` or `full` (default: `summary`) |
| `quick-create` | `--wait` | Wait for automatic schema creation to reach a terminal state |
| `quick-create` | `--format` | Output format: `table` or `json` (default: `table`) |
| `quick-create` | `--prompt-pdf-password` | Prompt securely for an encrypted PDF password |
| `quick-create-status` | `--format` | Output format: `table` or `json` (default: `table`) |
| `delete` | `-y`, `--yes` | Skip confirmation prompt |
| `export` | `--wait-timeout` | Background completion wait in seconds (default: `900`) |
| `export` | `--idempotency-key` | UUID key to retain for submission recovery |
| `task-results` | `--jsonl`, `--single-page` | Stream records or return a single page (mutually exclusive) |
| `task-results` | `--cursor`, `--page-size` | Continuation token (single-page only) and page size from 1–100 |
| `task-results`, `export-job-download` | `-o`, `--output`, `--overwrite` | Save output; explicitly allow replacing an existing file |
| `export-job-status`, `export-job-wait` | `--format` | Output format: `table` or `json` (default: `table`) |
| `export-job-wait` | `--timeout` | Local completion wait in seconds (default: `900`) |

## Python SDK Usage

The SDK provides both synchronous and asynchronous clients. Both manage connections, process multiple inputs concurrently, and retry eligible transient failures. Use the synchronous client in synchronous code and the asynchronous client in an async application.

The [Quick Start](#quick-start) shows complete examples for both clients. The
examples below use `SuparseClient` unless they demonstrate async-specific behavior.
With `AsyncSuparseClient`, use `async with` and `await` the corresponding methods;
options and result types are the same.

The client reads `SUPARSE_API_KEY` and `SUPARSE_API_URL` from environment variables or a `.env` file by default, so you can simply use `SuparseClient()` or `AsyncSuparseClient()` if those are set.

Use a context manager to close connections automatically, or call `client.close()`
(`await client.close()` for the async client).

### Extract One or More Documents

`extract()` is the primary API. It accepts a single file, a list of files, or any iterable (like `Path.glob`). Each input can be a string path, a `Path` object, or an open file handle.

`BatchResult.total` counts input sources. `BatchResult.succeeded` contains the
task export groups returned by the API, so one input can produce multiple
successful entries when splitting or template grouping is involved. Progress
callbacks run once per returned export group (or once for a failed input).
Callbacks preserve API order within one input, but callbacks from concurrently
processed inputs may be interleaved.

```python
from pathlib import Path
from suparse import FailedResult, SuparseClient

def print_progress(result):
    if isinstance(result, FailedResult):
        print(f"Failed: {result.file}: {result.error}")
    else:
        print(f"Done: {result.original_file}")

with SuparseClient() as client:
    # A list can mix string paths and Path objects. A glob iterable also works:
    # files=Path("./receipts/").glob("*.pdf")
    result = client.extract(
        files=["./receipts/jan.pdf", Path("./receipts/feb.pdf")],
        on_progress=print_progress,
    )
    for document in result.succeeded:
        print(document.original_file, document.data, document.document_ids)
    for failure in result.failed:
        print(failure.file, failure.error)
    print(f"Input files: {result.total}")
```

`document.data` contains the extracted fields. Use `document.documents` for the
`DocumentExport` records, including `credits_used`, `template_id`, `document_id`,
`file_name`, `page_start`, and `page_end`. Serialize the complete export group
with `document.model_dump(mode="json")`.

### Extract a Folder

`extract_folder()` discovers supported files and delegates to `extract()`. The
default pattern `"*"` scans the immediate directory; use `pattern="**/*"` to
include subdirectories. A `pdf_password` or `folder_id` passed to `extract()` or
`extract_folder()` applies to every input. Use separate calls when inputs need
different values.

```python
from suparse import SuparseClient

with SuparseClient() as client:
    result = client.extract_folder(
        "./receipts/",
        template_id="YOUR_TEMPLATE_ID",
        split=True,
        cleanup=True,
    )
    for document in result.succeeded:
        print(document.original_file, document.data)
    for failure in result.failed:
        print(failure.file, failure.error)
```

For async code, use `result = await client.extract_folder(...)` inside an
`async with AsyncSuparseClient()` block, then inspect the same result lists.

### Automatic Schema Creation

Create a template from an example document with
`start_quick_schema_creation()`. It accepts a string path, `Path`, or open binary
stream and returns a typed result containing a run ID. Use
`get_quick_schema_creation(run_id)` to check status once, or `wait_for_quick_schema_creation(run_id)` to wait for a terminal
result. Inspect `result.status`: failed and `completed_with_errors` runs are
returned as typed results, including error and failed-document details.

```python
from pathlib import Path
from suparse import SuparseClient

with SuparseClient() as client:
    started = client.start_quick_schema_creation("example.pdf")
    # Persist the ID before waiting so a later timeout can be recovered.
    Path("schema-run-id.txt").write_text(started.run_id, encoding="utf-8")
    result = client.wait_for_quick_schema_creation(started.run_id)
    print(result.status, result.model_dump(mode="json"))
```

Use `start_quick_schema_creation(..., pdf_password=...)` for encrypted PDFs.
The SDK leaves caller-owned streams open. Start requests are attempted once
unless both `upload_batch_id` and `source_upload_id` are supplied for an existing
batch source. If a start request fails without a response, follow
[Schema Creation Recovery](#schema-creation-recovery) before submitting again.

The `quick-create` and `quick-create-status` CLI commands return exit code 1 for
failed runs and for `completed_with_errors`; queued, processing, and fully
completed results return 0.

### Error Handling

All SDK exceptions inherit from `SuparseError`. `extract()` and
`extract_folder()` collect per-file processing errors in `result.failed`, even
for a single input. Check that list after every call. Setup or cleanup errors
can still raise exceptions.

Low-level methods raise exceptions directly, so you can handle specific types:

```python
from suparse import SuparseClient
from suparse.exceptions import (
    SuparseError,
    SuparseAuthError,
    SuparseNetworkError,
)

with SuparseClient(api_key="your_api_key_here") as client:
    try:
        client.list_templates()
    except SuparseAuthError:
        print("Invalid API key")
    except SuparseNetworkError:
        print("Connection failed")
    except SuparseError as e:
        print(f"Unexpected error: {e}")
```

With `AsyncSuparseClient`, use the same exception handlers around
`await client.list_templates()`.

File objects and in-memory binary streams can also be passed to `extract()`.
The SDK leaves caller-owned streams open; inspect `result.failed` as in the
[Quick Start](#quick-start).

### Read Task Results

Use `get_task_results()` to read one page of completed documents, or
`iter_task_results()` to traverse all available pages. These methods return
current results immediately. While a task is processing, results may be empty
or partial; an empty page does not mean processing has finished.

```python
from pathlib import Path
from suparse import SuparseClient

with SuparseClient() as client:
    task_id = client.upload_file(Path("invoice.pdf"))
    client.poll_task_status(task_id)  # Wait if you need the complete result set.

    for document in client.iter_task_results(task_id):
        print(document.id, document.extraction_data)

    page = client.get_task_results(task_id, limit=50)
    if page.next_cursor is not None:
        next_page = client.get_task_results(task_id, cursor=page.next_cursor, limit=50)
```

`limit` and the iterator's `page_size` accept integers from 1 to 100 (default:
100). Each `TaskResultPage` contains `documents` and `next_cursor`; a null
cursor indicates the end of the available pages. Save a non-null cursor to
resume pagination. `TaskResultDocument.extraction_data` contains the extracted
fields, including nested JSON; page numbers are returned as supplied by the API.

With `AsyncSuparseClient`, await `get_task_results()` and use `async for` with
`iter_task_results()`:

```python
import asyncio
from suparse import AsyncSuparseClient

async def main():
    async with AsyncSuparseClient() as client:
        task_id = "your-task-id"
        async for document in client.iter_task_results(task_id, page_size=50):
            print(document.extraction_data)

asyncio.run(main())
```

### Export Processed Documents

Use document IDs from `TaskExport.document_ids` or `poll_task_status()` to export
previously processed documents:

```python
from pathlib import Path
from suparse import ExcelLayout, ExportFormat, ExportType, SuparseClient

with SuparseClient() as client:
    result = client.extract("invoice.pdf")
    document_ids = [doc_id for group in result.succeeded for doc_id in group.document_ids]
    if document_ids:
        saved_path = client.download_results(
            document_ids,
            Path("invoices.xlsx"),
            format=ExportFormat.XLSX,
            export_type=ExportType.UNIFIED,
            xlsx_layout=ExcelLayout.FLAT,
        )
        print(saved_path)
    for failure in result.failed:
        print(failure.file, failure.error)
```

See [Choose an Export Mode and XLSX Layout](#choose-an-export-mode-and-xlsx-layout)
for column selection, layout examples, and CLI equivalents.

Choose a method based on how you want to consume the export:

| Method | Returns | Use for |
|--------|---------|---------|
| `export_documents()` / `fetch_results()` | `ExportResult` | Parsed JSON in `.task_exports`, CSV/XLSX bytes in `.data`, or Google Sheets details in `.google_sheets_export` |
| `download_results()` | Saved path as `str` | Writing an export to a file |
| `download_documents()` | `FileExport` | Exact attachment bytes in `.data`, with `.filename`, `.content_type`, and `.is_zip` metadata |

All four methods accept `format`, `export_type`, `xlsx_layout`, `wait_timeout`,
and `idempotency_key` as keyword arguments. `download_results()` also accepts an
output path. An omitted path uses the API filename or a generated fallback in
the current directory; an existing directory receives the export filename.

## Advanced Developer Workflows

Use these sections when you need direct control over uploads, recovery after
an uncertain request, or background export artifacts. For standard extraction,
start with [Quick Start](#quick-start) and [Export Processed Documents](#export-processed-documents).

### Low-Level API

For cases where you need file-based output or direct control over the upload/poll/download cycle:

```python
from pathlib import Path
from suparse import ExportFormat, SuparseClient

with SuparseClient() as client:
    # Upload a file and get back a task ID
    task_id = client.upload_file(
        Path("invoice.pdf"),
        template_id="YOUR_TEMPLATE_ID",
        split=False,
        auto_approve=True,  # Set to False to require human review in the Suparse UI
    )

    # Poll until processing completes (returns status + document IDs)
    status, doc_ids = client.poll_task_status(task_id)

    # Export results by document IDs to a JSON file. Omitting the output path
    # uses the API filename or a timestamped fallback in the current directory.
    saved_path = client.download_results(
        doc_ids,
        Path("output.json"),
    )

    # Fetch non-JSON exports in memory
    csv_export = client.fetch_results(
        doc_ids,
        format=ExportFormat.CSV,
    )
    print(csv_export.filename, csv_export.content_type, csv_export.is_zip)

    # Delete documents by ID
    client.delete_documents(doc_ids)

    # List available templates
    templates = client.list_templates()
    for t in templates:
        print(f"{t.name} ({t.template_language})")
```

The async client also provides `process_document()` for file output and
`process_batch()` for raw batch tuples. `process_document()` returns a success
boolean; `process_batch()` returns `(succeeded, failed)`, where successful entries
are `(Path, task_id, document_ids)` and failures are `(Path, task_id_or_none, exception)`.
Check these outcomes before continuing. Use `extract()` for the shared
`BatchResult` interface shown above.

For the async equivalent, await `upload_file()`, `poll_task_status()`,
`download_results()`, `fetch_results()`, `delete_documents()`, and
`list_templates()` inside `async with AsyncSuparseClient()`.

### Schema Creation Recovery

Before starting a run, retain the input filename, submission time (with time
zone), destination folder, and any existing `upload_batch_id` / `source_upload_id`.
Save the returned `run_id` immediately, as in the schema-creation example.

- **You have a run ID:** call `get_quick_schema_creation(run_id)` or run
  `suparse quick-create-status RUN_ID`. Resume waiting with
  `wait_for_quick_schema_creation(run_id)`; do not start another run.
- **The start response was lost:** the SDK and CLI have no activity-list method.
  Query the REST activity endpoint using the same API key and API base URL.
  For the default API URL:

  ```bash
  curl --get 'https://api.suparse.com/api/v1/templates/quick-create' \
    --header "X-API-Key: $SUPARSE_API_KEY" \
    --data-urlencode 'limit=50'
  ```

  Compare each entry in `items` by `filename`, `created_at`, and, when known,
  `upload_batch_id` and `folder_id`. You can filter the request by
  `upload_batch_id` or `folder_id`. Follow a non-null `next_cursor` by sending it
  as the next request's `cursor` query parameter. Use a matching entry's `run_id`
  with the status command above.

Activity is scoped to the authenticated uploader and effective team. Fully
successful runs are omitted, and failed/partial runs are visible only within
their observation window. An empty list therefore does not prove that the
request was rejected. Check the Suparse UI for the resulting documents and
template. If you cannot identify the outcome, contact
[support](mailto:support@suparse.com) with the retained filename, time, folder,
source IDs, and error details before resubmitting. Do not send your API key or
PDF password.

### Detailed Upload Confirmations

`upload_file()` returns a task ID string. Use `upload_file_detailed()` when you
need the complete confirmation, including upload or replay metadata:

```python
from pathlib import Path
from suparse import SuparseClient

with SuparseClient() as client:
    confirmation = client.upload_file_detailed(
        Path("invoice.pdf"),
    )
    print(confirmation.model_dump(mode="json"))
```

For an existing upload batch, pass its `upload_batch_id` and `source_upload_id`
to `upload_file_detailed()`. Obtain these IDs from the workflow that created the
batch; this SDK does not provide methods to create upload batches.

Upload confirmations without both IDs are attempted once. A network failure or
5xx response raises `SuparseUploadConfirmationUnknownError`: the upload may
already have been accepted. Retain the exact `upload_reference_id`, filename,
submission time (with time zone), folder ID, and any batch/source IDs. With
`extract()` or `extract_folder()`, read the reference from
`failure.upload_reference_id` in `result.failed`.

The SDK and CLI have no lookup by upload reference. To find candidate documents,
open the Suparse UI and search for the filename around the submission time, or
query the REST document list using the same API key. For example, replace the
filename and timestamp below (and the base URL if you use a custom API URL):

```bash
curl --get 'https://api.suparse.com/api/v1/documents/' \
  --header "X-API-Key: $SUPARSE_API_KEY" \
  --data-urlencode 'filename=invoice.pdf' \
  --data-urlencode 'created_after=2026-10-15T09:00:00Z' \
  --data-urlencode 'uploaded_by=me' \
  --data-urlencode 'page=1' \
  --data-urlencode 'items_per_page=100'
```

Inspect all matching pages, checking creation time, folder/batch, and document
contents; filenames alone are not unique. Export confirmed processed document
IDs with `suparse export DOCUMENT_ID`. If no candidate can be reliably matched,
ask [support](mailto:support@suparse.com) to reconcile the upload using the saved
`upload_reference_id` and submission details before uploading again. An empty
search does not establish that confirmation failed.

When both IDs are supplied, the SDK uses the server's replay protection for
bounded transient retries. A 503 response is surfaced without retry because the
upload may already have been accepted.

### Structured API Errors

`SuparseAPIError` and its subclasses expose `status_code` and the raw
`response_body`. Structured API errors also provide `.code` and `.meta`:

```python
from suparse import SuparseAPIError, SuparseClient

with SuparseClient() as client:
    try:
        client.list_templates()
    except SuparseAPIError as exc:
        print(exc.status_code, exc.code, exc.meta, exc.response_body)
```

### Background Exports and Recovery

Export methods automatically wait when the API accepts an export as a
background job. `wait_timeout` defaults to 900 seconds and bounds the background
completion wait, including polling retries. It does not set a deadline for
submission or download; individual HTTP requests have separate timeouts.

The SDK generates a UUID idempotency key for each export submission and reuses
it for automatic submission retries. To retain the key before making a request,
pass your own UUID string as `idempotency_key`. Keep it with any reported job ID
so you can recover after a timeout or uncertain response. An idempotency key
does not guarantee exactly-once generation for immediate exports.

If the submission response was lost and you only have the key, repeat the same
export request with the same document IDs, format, export mode, layout, and
`idempotency_key`. For the CLI, rerun the original `suparse export` command with
its saved `--idempotency-key`. Retain the original request options alongside the
key so you can reproduce it. Once a job ID is available, resume that job using
the methods below.

Use `get_export_job()` to inspect a job, `wait_for_export_job()` to wait for
completion, and `download_export_job()` to save its artifact:

```python
from pathlib import Path
from suparse import SuparseClient, SuparseExportError, SuparsePollingTimeoutError

export_job_id = "your-export-job-id"  # Job ID reported by an earlier export.
output_dir = Path("exports")
output_dir.mkdir(parents=True, exist_ok=True)

with SuparseClient() as client:
    job = client.get_export_job(export_job_id)
    print(job.status)
    try:
        client.wait_for_export_job(export_job_id, wait_timeout=900)
        saved_path = client.download_export_job(export_job_id, output_dir)
        print(saved_path)
    except SuparsePollingTimeoutError as exc:
        print(exc.export_job_id, exc.last_status, exc.idempotency_key)
        # Resume later with wait_for_export_job(exc.export_job_id).
    except SuparseExportError as exc:
        print(exc.reason, exc.code, exc.meta, exc.export_job_id)
```

`get_export_job()` also returns failed jobs and jobs whose artifacts are no
longer available. Job counts describe document selection, generation, and
exclusions; they are not a progress percentage.

#### Artifact Downloads and Integrity

`download_export_job()` requires a completed job with an available artifact and
does not wait. It verifies the artifact's size and SHA-256 checksum before
publishing the file. It refuses to replace an existing file unless
`overwrite=True`. `download_results()` can replace an existing file. In-memory
export methods buffer the artifact in memory.

#### Timeouts, Cancellation, and Deferred Cleanup

A local timeout or cancellation does not cancel the server job. Export errors
include `export_job_id`, `idempotency_key`, and `job` when available.
For async cancellation, use `get_export_recovery_context()` to retrieve these
details through the exception's cause/context chain on all supported Python
versions. Retain the returned context to resume later, and propagate cancellation:

```python
import asyncio
from suparse import get_export_recovery_context

try:
    result = await export_task
except asyncio.CancelledError as exc:
    recovery = get_export_recovery_context(exc)
    if recovery is not None:
        print(recovery.export_job_id, recovery.idempotency_key)
        # Persist these values; resume by job ID when available.
        # Before acceptance, only the idempotency key may be known.
    raise
```

`SuparsePollingTimeoutError` also exposes `last_status` and
`operation_may_be_running`. If processing with `cleanup=True` leaves an export
running or its submission outcome uncertain, cleanup is deferred for the
affected source documents. Inspect or resume the export, then delete those
documents explicitly. For `extract()`, recovery identifiers appear in
`FailedResult.error`.

#### Export Failure Reasons and Format Limits

`SuparseExportError.reason` identifies the failure:

| Reason | Meaning |
|--------|---------|
| `job_failure` | The export job failed |
| `artifact_unavailable` | The artifact is unavailable or expired |
| `unsupported_state` | The job is in a state the SDK cannot handle |
| `protocol_failure` | The export response does not match the expected protocol |
| `storage_failure` | The artifact could not be downloaded |
| `integrity_failure` | The downloaded artifact's size or checksum does not match |
| `incompatible_artifact` | The artifact cannot be used as the requested result type |

Parsed JSON exports require a JSON array matching `list[TaskExport]`.
Incompatible background JSON or ZIP artifacts raise `SuparseExportError`;
the SDK does not unpack ZIPs. Use `download_documents()` or
`download_export_job()` when you need the original artifact bytes.
Google Sheets exports are supported for immediate API responses; background
Google Sheets jobs raise `SuparseExportError` with `reason="incompatible_artifact"`.

## SDK Reference

### Constructor Parameters

Both `SuparseClient` and `AsyncSuparseClient` accept the same parameters:

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `api_url` | `str` | `SUPARSE_API_URL` env var or `https://api.suparse.com/api/v1/` | API base URL |
| `api_key` | `str` | `SUPARSE_API_KEY` env var | Your API key (required) |
| `poll_interval` | `int` | `5` | Seconds between polling attempts |
| `max_poll_attempts` | `int` | `300` | Max polling attempts before timeout |

### Result Objects

| Object | Properties | Description |
|--------|-----------|-------------|
| `TaskExport` | `task_id`, `original_file`, `total_documents_extracted`, `documents`, `data`, `document_ids` | Export group for a processed input; `documents` contains `DocumentExport` objects with `credits_used`, `template_id`, etc. |
| `FailedResult` | `file`, `error`, `upload_reference_id` | File that failed during extraction |
| `BatchResult` | `succeeded`, `failed`, `total` | Container for batch results; `total` counts inputs while `succeeded` contains returned task export groups |
| `ExportResult` | `format`, `content_type`, `filename`, `is_zip`, `data`, `task_exports`, `google_sheets_export` | Low-level export result for JSON, CSV, XLSX, and Google Sheets |
| `QuickSchemaStartResult`, `QuickSchemaStatusResult`, `QuickSchemaTerminalResult` | Unions of typed lifecycle models | Automatic schema-creation results; inspect `.status` and model-specific fields |
| `TaskResultPage` | `documents`, `next_cursor` | One page of currently completed task results |
| `TaskResultDocument` | `id`, `file_name`, `extraction_data`, page and template fields | Extracted document returned by task-result reads |
| `ExportJobRead` | `export_job_id`, `status`, counts, `artifact`, `error` | Background export status and recovery details |
| `ExportJobArtifact` | `filename`, `content_type`, `size_bytes`, `checksum_sha256` | Downloadable artifact metadata |
| `FileExport` | `format`, `content_type`, `filename`, `is_zip`, `data` | Exact attachment bytes from `download_documents()` |

### Export Formats

| Format | Result | Notes |
|--------|--------|-------|
| `json` | `TaskExport` list | Default structured extraction result |
| `csv` | Binary export bytes | Uses API filename when provided; may be a ZIP |
| `xlsx` | Binary export bytes | Uses API filename when provided; may be a ZIP |
| `google_sheets` | `GoogleSheetsExport` | Requires Google Sheets integration |

`export_type` selects the template columns as explained in
[Choose an Export Mode and XLSX Layout](#choose-an-export-mode-and-xlsx-layout).
It accepts `unified` or `original`. The CLI and parsed export methods
default to `unified`; `download_documents()` defaults to `original`. It affects
CSV, XLSX, and Google Sheets exports; parsed JSON returns task export groups.

`xlsx_layout` accepts `ExcelLayout.STANDARD` (the default) or `ExcelLayout.FLAT`. It only affects XLSX exports.

### Exceptions

All exceptions inherit from `SuparseError`.

| Exception | Raised When |
|-----------|-------------|
| `SuparseError` | Base exception for all SDK errors |
| `SuparseNetworkError` | Network connection fails or times out |
| `SuparsePollingTimeoutError` | Processing/schema polling exceeds its attempt limit, or a background export exceeds `wait_timeout` |
| `SuparseExportError` | Background job, artifact download, integrity, or format failure; inspect `.reason` and recovery context |
| `SuparseProcessingError` | Document fails to process on the server |
| `SuparseAPIError` | Base for HTTP error responses (has `status_code`, `response_body`, `code`, `meta`) |
| `SuparseAuthError` | 401/403 authentication or authorization error |
| `SuparseNotFoundError` | 404 resource not found |
| `SuparseRateLimitError` | 429 too many requests |
| `SuparseServerError` | 5xx server error |
| `SuparseUploadConfirmationUnknownError` | Upload confirmation outcome is unknown; inspect `upload_reference_id` before taking action |
| `SuparseSDKError` | SDK fails to parse the API response into the expected data model |

### `extract()` Parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `files` | `str`, `Path`, `IO[bytes]`, or iterable of these | required | One or more files to process |
| `template_id` | `str` | `None` | Template ID (auto-detect if omitted) |
| `split` | `bool` | `False` | Auto-split multi-page documents |
| `auto_approve` | `bool` | `True` | Set to `False` to require human review in the Suparse UI |
| `pdf_password` | `str` | `None` | Password applied to every input encrypted PDF |
| `folder_id` | `str` | `None` | Folder ID applied to every uploaded input |
| `cleanup` | `bool` | `False` | Delete documents from server after extraction |
| `on_progress` | `callable` | `None` | Called with each `TaskExport` or `FailedResult` as it completes |

### `extract_folder()` Parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `folder` | `str` or `Path` | required | Directory to scan |
| `pattern` | `str` | `"*"` | Glob pattern for file discovery (filtered by supported extensions) |
| `template_id` | `str` | `None` | Template ID (auto-detect if omitted) |
| `split` | `bool` | `False` | Auto-split multi-page documents |
| `auto_approve` | `bool` | `True` | Set to `False` to require human review in the Suparse UI |
| `pdf_password` | `str` | `None` | Password applied to every input encrypted PDF |
| `folder_id` | `str` | `None` | Folder ID applied to every uploaded input |
| `cleanup` | `bool` | `False` | Delete documents from server after extraction |
| `on_progress` | `callable` | `None` | Called with each result as it completes |

<!-- docs-sync: omit-web -->
## Documentation

Full API documentation is available at [suparse.com/docs](https://suparse.com/docs).

## License

[MIT](LICENSE)
<!-- /docs-sync: omit-web -->

<!-- docs-sync: omit-web -->
### Background JSON release gate

Backend-produced JSON/ZIP and Google Sheets job fixtures were unavailable for
this implementation. Offline fixtures exercise the documented wire contract
and legacy multi-template/split-document grouping; they do not certify backend
compatibility. Release requires representative backend artifacts and
confirmation that parsed JSON exports still match `list[TaskExport]`.
Background JSON currently passes that exact validation before being returned
or saved; incompatible JSON or ZIP raises `SuparseExportError` with job/key
recovery context. Exact-byte downloads preserve ZIP bytes and artifact
metadata. No ZIP unpacking or task-result fallback occurs. Existing HTTP 200
Google Sheets responses keep their behavior; background provider-job responses
fail explicitly pending backend contract confirmation.
<!-- /docs-sync: omit-web -->

