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

# Documents and parsing

> Create a Book, upload its files, and control parsing as separate API calls.

Use these endpoints when your application needs to control each part of the Book lifecycle. You can create an empty Book, add files over time, inspect the uploaded documents, and start parsing when the package is ready.

For a single request that creates, uploads, and starts parsing, use [`POST /submit-book`](/api-reference/endpoint/submit-book).

## Complete manual flow

```python theme={null}
import time
from pathlib import Path

import requests

API_BASE = "https://api.lendpathway.com/api"
TOKEN = "pat_your_token_here"
HEADERS = {"Authorization": f"Bearer {TOKEN}"}

# 1. Create the Book.
response = requests.post(
    f"{API_BASE}/books/",
    headers=HEADERS,
    json={"name": "Acme Coffee", "description": "March renewal review"},
    timeout=30,
)
response.raise_for_status()
book_id = response.json()["id"]

# 2. Upload one or more documents.
paths = [Path("january.pdf"), Path("february.pdf")]
handles = [path.open("rb") for path in paths]
try:
    response = requests.post(
        f"{API_BASE}/documents/",
        headers=HEADERS,
        params={"book_id": book_id},
        files=[("files", (path.name, handle, "application/pdf")) for path, handle in zip(paths, handles)],
        timeout=120,
    )
    response.raise_for_status()
    documents = response.json()
finally:
    for handle in handles:
        handle.close()

# 3. Start parsing.
response = requests.post(
    f"{API_BASE}/parse/{book_id}/parse-book",
    headers=HEADERS,
    timeout=30,
)
response.raise_for_status()
parse_job_id = response.json()["parse_job_id"]

# 4. Wait for a terminal status.
while True:
    response = requests.get(
        f"{API_BASE}/books/{book_id}",
        headers=HEADERS,
        timeout=30,
    )
    response.raise_for_status()
    book = response.json()

    if book["parse_status"] in {"completed", "failed", "cancelled"}:
        break
    time.sleep(3)

print(book["parse_status"], book.get("parse_status_message"))
```

## Create an empty Book

```http theme={null}
POST /books/
Content-Type: application/json
```

```json theme={null}
{
  "name": "Acme Coffee",
  "description": "March renewal review"
}
```

The response is the newly created [`Book`](/api-reference/endpoint/books#complete-book-response). Its initial `parse_status` is `new`.

## Upload documents

```http theme={null}
POST /documents/?book_id={book_id}
Content-Type: multipart/form-data
```

Repeat the `files` form field for every file. A ZIP file is expanded during upload. The response contains every Document currently attached to the Book.

<CodeGroup>
  ```python Python theme={null}
  with open("statement.pdf", "rb") as statement:
      response = requests.post(
          f"{API_BASE}/documents/",
          headers=HEADERS,
          params={"book_id": BOOK_ID},
          files={"files": ("statement.pdf", statement, "application/pdf")},
          timeout=120,
      )

  response.raise_for_status()
  print(response.json())
  ```

  ```javascript JavaScript theme={null}
  import { openAsBlob } from "node:fs";

  const form = new FormData();
  form.append("files", await openAsBlob("statement.pdf"), "statement.pdf");

  const response = await fetch(`${API_BASE}/documents/?book_id=${BOOK_ID}`, {
    method: "POST",
    headers: { Authorization: `Bearer ${TOKEN}` },
    body: form,
  });

  if (!response.ok) {
    throw new Error(`${response.status}: ${await response.text()}`);
  }

  console.log(await response.json());
  ```

  ```bash cURL theme={null}
  curl --fail-with-body \
    -X POST \
    "https://api.lendpathway.com/api/documents/?book_id=99cc93e6-1f3f-42b1-9fe4-ba5a95be9c78" \
    -H "Authorization: Bearer pat_your_token_here" \
    -F "files=@statement.pdf"
  ```
</CodeGroup>

<Accordion title="Document response model">
  ```python theme={null}
  from datetime import datetime
  from typing import Literal
  from uuid import UUID

  from pydantic import BaseModel

  DocumentType = Literal[
      "pending",
      "bank_statement",
      "month_to_date",
      "plaid_asset_report",
      "loan_application_form",
      "receipt",
      "decisionlogic",
      "credit_report",
      "tax_form",
      "ar_report",
      "photo_id",
      "voided_check",
      "unsupported_file",
  ]
  DocumentStatus = Literal["pending", "new", "processing", "completed", "failed"]


  class DocumentMeta(BaseModel):
      failure_reason: str | None = None


  class Document(BaseModel):
      id: UUID
      org_id: UUID | None = None
      book_id: UUID | None = None
      document_type: DocumentType = "pending"
      document_status: DocumentStatus = "pending"
      uploaded_file_name: str
      original_filename: str
      file_size: int | None = None
      document_meta: DocumentMeta | None = None
      is_deleted: bool = False
      created_by: UUID | None = None
      created_at: datetime
      updated_at: datetime
      presigned_url: str | None = None
      presigned_url_expires: datetime | None = None
      mime_type: str | None = None
      page_count: int | None = None
  ```
</Accordion>

## Inspect and download documents

| Request                                 | Result                                                 |
| --------------------------------------- | ------------------------------------------------------ |
| `GET /documents/?book_id={book_id}`     | Every active document attached to the Book             |
| `GET /documents/{document_id}`          | One Document with its latest classification and status |
| `GET /documents/{document_id}/download` | A fresh one-hour `download_url` for the source file    |

Document classification happens during parsing. Before that work runs, `document_type` can be `pending` and `document_status` can be `new`.

## Start parsing

```http theme={null}
POST /parse/{book_id}/parse-book
```

```json theme={null}
{
  "parse_job_id": "d1f05046-b5ba-45a2-8f3b-ae3acc08fd11"
}
```

The request atomically moves the Book into `processing` and returns immediately. A Book already processing returns `400`. Organization usage limits can return `403`.

Poll either resource:

| Request                                  | Use                                                                                      |
| ---------------------------------------- | ---------------------------------------------------------------------------------------- |
| `GET /books/{book_id}`                   | Status plus the complete Book. Fetch this when you will read the result after completion |
| `GET /parse/{book_id}/parse-book-status` | Lightweight status for one Book                                                          |
| `GET /parse/books/statuses`              | Lightweight status for every Book in the organization                                    |

The terminal states are `completed`, `failed`, and `cancelled`. Once completed, choose the response that fits the job:

* [`GET /books/{book_id}`](/api-reference/endpoint/books) for the entire stored Book and raw parser results
* [`GET /books/{book_id}/analytics`](/api-reference/endpoint/analytics) for complete computed underwriting data
* `GET /books/{book_id}/statements` for the smaller account-first bank view

## Stop an active parse

```http theme={null}
POST /parse/{book_id}/stop-parse
```

The response is `{"message": "Parse cancelled"}` when a running parse was cancelled, or `{"message": "No active parse"}` when there was nothing to stop.

<Warning>
  Starting a new parse replaces the Book's existing `book_meta`. Transaction tags, position edits, and exclusions stored with the previous parse are discarded.
</Warning>
