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

# Printing PDFs

> Send a PDF by URL or as base64 and track it to done.

A print job targets one printer and carries one document. Send a public or signed URL with `contentType` `pdf_uri`, or embed the file with `pdf_base64`. InkRelay stores the job, delivers it to the agent and reports every state change.

```json theme={null}
{
  "printerId": "PRINTER_ID",
  "title": "Invoice 1042",
  "contentType": "pdf_base64",
  "content": "JVBERi0xLjQK...",
  "qty": 1,
  "expireAfter": 86400,
  "options": { "copies": 1, "paper": "A4", "duplex": "long-edge" }
}
```

Send it with `POST /v1/printjobs`. The response is `201` with the job object in state `queued`.

| Field | Meaning |
| - | - |
| `printerId` | Id of a printer that is marked live. Required |
| `title` | Shown in the dashboard and in the operating system's print queue. 1 to 255 characters. Required |
| `contentType` | `pdf_uri`, `pdf_base64`, `raw_uri` or `raw_base64`. Required |
| `content` | The URL or the base64 text. Required |
| `source` | Free text that names your system, for example `my-erp`. Defaults to `api` |
| `qty` | How many times to print the document, 1 to 100. Defaults to 1 |
| `expireAfter` | Seconds the job may wait before the printer's computer receives it, 60 to 604800. Defaults to 86400 (24 hours) |
| `options` | Print options, see [Options and paper sizes](/guides/options-and-paper-sizes) |
| `authentication` | Credentials for a URL protected with Basic authentication |

## URL or base64

Both forms accept files up to 25 MB.

* **`pdf_base64`**: the file as standard base64 text, decoded size at most 25 MB. The bytes must start with `%PDF-`, otherwise the API answers `422 invalid_content`. A body that is too large answers `413 content_too_large`.
* **`pdf_uri`**: an `http` or `https` URL of at most 2048 characters. InkRelay fetches it once, with a 30 second timeout, and keeps the file for the job. Redirects are not followed, private and reserved addresses are refused, and only ports 80 and 443 are allowed. A URL that fails these checks at creation answers `422 invalid_content_url`. The fetch itself happens right after the job is created, so a URL that cannot be fetched later (an error status, a timeout, a file that is too large) ends the job in `error`, with the reason in `stateMessage`. Use a signed URL that stays valid for a few minutes.

For a URL protected with Basic authentication, add `authentication`:

```json theme={null}
{
  "contentType": "pdf_uri",
  "content": "https://files.example.com/invoice-1042.pdf",
  "authentication": { "type": "BasicAuth", "credentials": { "user": "u", "pass": "p" } }
}
```

The URL and the credentials are kept only in short lived storage, for at most one hour, until the file is fetched. They are never stored with the job or written to logs.

The printer must be live. A printer that is not marked live answers `422 printer_disabled`, and a key of the wrong mode answers `422 mode_mismatch` (see [Test mode](/guides/test-mode)).

## Job states

Each job moves forward through these states. Every change is recorded as an event you can read with `GET /v1/printjobs/{id}/events` and receive as a [webhook](/guides/webhooks).

| State | Set by | Meaning |
| - | - | - |
| `queued` | API | Stored, waiting for the computer |
| `sent` | API or worker | Pushed to the agent over its connection |
| `received` | Agent | The agent acknowledged the job and downloaded the file |
| `printing` | Agent | Handed to the operating system spooler, which reports processing |
| `done` | Agent | The spooler reports the job completed |
| `error` | Agent or system | The operating system reported an error, explained in `stateMessage` |
| `cancelled` | API | Cancelled before the agent received it |
| `expired` | Worker | Not received within `expireAfter` |

A job in `sent` that is not acknowledged within 60 seconds is sent again, up to 3 times, then returns to `queued`. A job in `printing` that is not reported `done` within 15 minutes becomes `error` with the message `spooler timeout`. `done` means the spooler finished the job; a printer can still be out of paper.

`stateMessage` also carries warnings about options the printer ignored.

## Waiting for completion

The simplest option is the SDK, which creates the job and polls until it reaches a final state (`done`, `error`, `cancelled` or `expired`).

<CodeGroup>
  ```ts TypeScript theme={null}
  import { InkRelay } from "inkrelay";

  const ir = new InkRelay({ apiKey: process.env.INKRELAY_API_KEY });
  const job = await ir.printJobs.createAndWait(
    { printerId, title: "Invoice 1042", contentType: "pdf_uri", content: url },
    { timeoutMs: 60_000 },
  );
  console.log(job.state, job.stateMessage);
  ```

  ```python Python theme={null}
  from inkrelay import InkRelay

  ir = InkRelay()  # reads INKRELAY_API_KEY
  job = ir.print_jobs.create_and_wait(
      {"printerId": printer_id, "title": "Invoice 1042", "contentType": "pdf_uri", "content": url},
      timeout_s=60,
  )
  print(job["state"], job["stateMessage"])
  ```
</CodeGroup>

For production, subscribe to `printjob.done` and `printjob.error` with [webhooks](/guides/webhooks) instead of holding a request open. You can also read `GET /v1/printjobs/{id}` at any time.

## Cancelling

`POST /v1/printjobs/{id}/cancel` cancels a job while it is `queued` or `sent`. It needs a key with the `print` scope, and dashboard users with the member role get `403`. Once the agent has received the job it can no longer be cancelled, and the API answers `409 not_cancellable`.

```bash theme={null}
curl -X POST https://api.inkrelay.app/v1/printjobs/JOB_ID/cancel \
  -H "Authorization: Bearer $INKRELAY_API_KEY"
```

## Reprinting

A job that ended in `error` is not retried automatically. To print an earlier job again without sending the document again, call `POST /v1/printjobs` with `printerId`, `title` and `reprintOf` (the id of the earlier job) instead of `contentType` and `content`. InkRelay copies the stored file; send exactly one of the two forms, otherwise the API answers `400 invalid_request`.

```json theme={null}
{ "printerId": "PRINTER_ID", "title": "Invoice 1042 (again)", "reprintOf": "JOB_ID" }
```

Stored files are kept for 7 days. After that, a reprint answers `410 file_expired` and you need to send the document again with `contentType` and `content`. Use a new `Idempotency-Key` for each new attempt, because the same key returns the first job (see [Rate limits and idempotency](/guides/rate-limits-and-idempotency)).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.