# Progress and previews

[HTML](https://pdfcraft.ai/en/docs/api/preview/) · [Site index](https://pdfcraft.ai/llms.txt)

Query processing progress and read text and image previews.


For a background integration that only needs the result, poll `GET /transforms/{uuid}`. Use observation when your application displays progress or content while processing.

## Observation response

`GET /transforms/{uuid}/observation?preview_limit=100`

```json
{
  "status": "running",
  "progress": {
    "queued": false,
    "stage": "converting",
    "completed_words": 1250,
    "total_words": 8000
  },
  "preview": {
    "cursor": "opaque-cursor",
    "updated": [
      {
        "file": "converting/body",
        "url": "https://storage.example/body.jsonl?signed",
        "range": {"start": 0, "end": 240}
      }
    ]
  }
}
```

The example URLs and offsets illustrate the response shape. Read the exact URL and range returned for your task.

`progress` may be returned while the task is running. If `queued` is true, the task is still waiting and a progress percentage is unavailable. Processing stages are `preparing`, `extracting`, `converting`, `packaging` and `uploading`.

Page counts use `completed_pages` and `total_pages`; source-text counts use `completed_words` and `total_words`. These fields may be absent. Do not treat missing values as zero, and only calculate a percentage when the total is greater than zero.

Counts describe the current operation, so totals may change between stages. They do not represent charges.

Completed work can include fallback handling; it does not prove that every word has a translated counterpart. Only `status: "succeeded"` indicates successful completion.

## Read updates and advance the cursor

Keep one cursor per task. Send it unchanged with `URLSearchParams` on the next observation request. `preview.cursor` is always present; `updated` may be absent when there is no new content.

1. Fetch each `updated` URL without API credentials and without caching.
2. For a `range`, request `Range: bytes=start-(end-1)`. Offsets count UTF-8 bytes; `start` is inclusive and `end` exclusive.
3. Require `206`, read the complete byte range, decode UTF-8 and parse complete JSONL records. A metadata update has no range and is a complete JSON document.
4. Save the new cursor only after all updates have been read and applied. If any step fails, keep the old cursor and retry after a delay. Retries may return the same content; deduplicate by task, stream and byte range to avoid adding it twice.

This function reads JSONL records within the requested byte range:

```javascript
async function readPreviewRange(update) {
  const { start, end } = update.range;
  const response = await fetch(update.url, {
    headers: { Range: `bytes=${start}-${end - 1}` },
    cache: "no-store",
  });
  if (response.status !== 206) throw new Error(`Preview HTTP ${response.status}`);
  const bytes = await response.arrayBuffer();
  if (bytes.byteLength !== end - start) throw new Error("Incomplete preview range");
  const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
  if (!text.endsWith("\n")) throw new Error("Incomplete JSONL record");
  return text.slice(0, -1).split("\n").map(line => JSON.parse(line));
}
```

This helper does not manage polling, metadata or your application state. Never attach the API Authorization header to its fetch. Render document content as untrusted input.

## Preview limits

`preview_limit` limits each JSONL stream independently, defaults to 500 and is capped at 500. The initial request returns only the tail. If you fall behind, older records can be skipped: cursor polling is not pagination through the entire document. Download the final result for complete content.

Process any final updates before stopping on a terminal state. Successful observation also includes `{get_url, head_url}` in `download_urls`. These use default filenames; request `/download-urls?filename=...` separately for a custom name. Failed tasks may retain partial preview, which is not a successful output.

## Streams and assets

| Stream | Content |
| --- | --- |
| `extraction/metadata`, `converting/metadata` | Complete `{metadata, original?}` JSON; replace the previous view rather than merge patches |
| `extraction/body`, `extraction/footnotes` | PDF extraction records containing a `content` tree and optional source regions |
| `converting/body`, `converting/footnotes` for PDF | Paragraph records with `text`, optional `original`, and source `regions` |
| `converting/body` for EPUB | Plain-text fragments with `original`, `translated` and optional `epubcfi` |

PDF extraction previews can differ from the final chapter structure. EPUB has no extraction or separate footnote stream. Records may contain an optional `emitted_at`; accept older records without it and ignore unknown additive fields.

Images and formulas in previews are identified by resource hashes. Insert the hash into `GET /transforms/{uuid}/preview-assets/{extraction|converting}/{hash}.{png|pdf}` to obtain the file URL.

Reading the file does not require an API key. If the link expires, call the endpoint again.
