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

# Hosted Document Jobs: Lifecycle, Status, and Expiry

> Every hosted parse or compare call creates a durable job. Learn the status lifecycle, idempotency keys, SDK auto-polling, and when results expire.

When you use the hosted FileRouter API, your parse or compare request does not execute inline — it creates a **job** that runs durably in the background. The job has a unique UUID, moves through a defined set of statuses, and stores its result in temporary cloud storage until you retrieve it. This model lets you fire off long-running parses without holding an open HTTP connection, and it means the SDK can safely retry polling without risk of submitting the same work twice.

## Job lifecycle

Every job follows a linear path from creation to completion:

```text job-lifecycle.txt theme={null}
queued → running → complete
                 ↘ failed
```

<Steps>
  <Step title="queued">
    The job has been accepted and persisted. The provider has not been contacted yet. This is the initial status returned when you `POST /api/v1/jobs`.
  </Step>

  <Step title="running">
    The job has been picked up and a request has been sent to the provider. The provider is actively processing the document.
  </Step>

  <Step title="complete">
    The provider returned a successful result. The normalized `ParseResult` (or `CompareResult`) is stored and available to retrieve.
  </Step>

  <Step title="failed">
    The provider returned an error, or the job encountered an unrecoverable problem. The error message is stored alongside the job record.
  </Step>
</Steps>

<Note>
  If you use the TypeScript SDK (`FileRouterClient`), you never need to manage polling yourself. The client's `parse()` and `compare()` methods create the job, poll until it reaches `complete` or `failed`, and return the result (or throw an error) directly. Manual polling is only necessary if you call the REST API directly.
</Note>

## Using the TypeScript client

The `FileRouterClient` handles the full job lifecycle for you:

```ts hosted-client.ts theme={null}
import { FileRouterClient } from "@file_router/sdk"

const client = new FileRouterClient({
  apiKey: process.env.FILEROUTER_API_KEY,
})

// Creates a job, polls until complete, and returns the ParseResult
const result = await client.parse("https://example.com/report.pdf", {
  outputs: ["markdown", "pages"],
  provider: "llamaparse",
})

console.log(result.outputs.markdown)
```

The client polls on a 1-second interval by default. You can adjust this with `pollingIntervalMs` when constructing the client. If the job does not complete within the timeout window (default **10 minutes**), the client throws a `FileRouterError` with code `"Timeout"`.

```ts client-with-timeout.ts theme={null}
import { FileRouterClient } from "@file_router/sdk"

const client = new FileRouterClient({
  apiKey: process.env.FILEROUTER_API_KEY,
  pollingIntervalMs: 2000,  // poll every 2 seconds
})

const result = await client.parse(file, {
  outputs: ["markdown"],
  timeoutMs: 5 * 60 * 1000,  // override default 10-minute timeout
})
```

## REST API

If you use the REST API directly, you create and poll jobs with two endpoints:

<Tabs>
  <Tab title="Create a job">
    **`POST /api/v1/jobs`**

    Send a JSON body to parse a document from a public URL:

    ```json create-job.json theme={null}
    {
      "operation": "parse",
      "source": { "url": "https://example.com/report.pdf" },
      "outputs": ["markdown", "pages"],
      "provider": "llamaparse"
    }
    ```

    Or send a binary body with `X-FileRouter-*` headers for file uploads. You must include the `Idempotency-Key` header on every request (see below).

    A `202 Accepted` response means the job was created. A `200 OK` response means an existing job was returned for your idempotency key.

    ```json job-accepted.json theme={null}
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "status": "queued"
    }
    ```
  </Tab>

  <Tab title="Poll a job">
    **`GET /api/v1/jobs/{jobId}`**

    Poll this endpoint until `status` is `"complete"` or `"failed"`:

    ```json job-complete.json theme={null}
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "status": "complete",
      "result": { ... }
    }
    ```

    ```json job-failed.json theme={null}
    {
      "id": "550e8400-e29b-41d4-a716-446655440000",
      "status": "failed",
      "error": "Provider returned an error: ..."
    }
    ```

    While the job is still in progress, the response contains only `id` and `status` (either `"queued"` or `"running"`). Keep polling until the status changes.
  </Tab>
</Tabs>

## Idempotency

Every job creation request requires an `Idempotency-Key` header. The key must be between **8 and 255 characters** long (after trimming whitespace).

If you send two requests with the same idempotency key, the second request returns the existing job without creating a new one (HTTP `200`). This makes it safe to retry a failed network request — you will not accidentally submit the same document twice or be charged twice.

<Info>
  The TypeScript SDK generates a UUID idempotency key automatically for every `parse()` and `compare()` call. You only need to supply your own key if you want to control retry behavior manually — for example, to reuse the same key across a retry loop in your own code.
</Info>

You can supply a custom key via `idempotencyKey` in `HostedParseOptions` or `HostedCompareOptions`:

```ts custom-idempotency-key.ts theme={null}
import { FileRouterClient } from "@file_router/sdk"

const client = new FileRouterClient({ apiKey: process.env.FILEROUTER_API_KEY })

// Using a stable key lets you safely retry this call
const result = await client.parse(file, {
  outputs: ["markdown"],
  idempotencyKey: "my-pipeline-run-2024-07-01-report-v3",
})
```

## Result retention and expiry

<CardGroup cols={2}>
  <Card title="Results expire after 7 days" icon="clock">
    The normalized parse result is stored for **7 days** after the job completes. After that, a `GET /api/v1/jobs/{jobId}` request returns HTTP `410 Gone`. Retrieve and persist results before they expire.
  </Card>

  <Card title="Job records kept for 30 days" icon="calendar">
    The job metadata record (ID, status, timing) is retained for **30 days** after creation. After 30 days the job record itself is removed.
  </Card>
</CardGroup>

<CardGroup cols={2}>
  <Card title="Source documents are deleted on completion" icon="trash">
    The document you uploaded is deleted from cloud storage as soon as the job finishes — regardless of whether it succeeded or failed. FileRouter does not retain your source files.
  </Card>

  <Card title="Retrieve results before they expire" icon="download">
    If your workflow needs the result beyond 7 days, download and store it in your own storage after the job completes.
  </Card>
</CardGroup>

<Warning>
  A `410 Gone` response on `GET /api/v1/jobs/{jobId}` means the result has expired and can no longer be retrieved. Plan your retrieval workflow to fetch results promptly after job completion.
</Warning>
