# Identify the organization behind an API key
Source: https://docs.inkrelay.app/api-reference/account/identify-the-organization-behind-an-api-key
/openapi/openapi.json get /v1/whoami
Returns the organization, mode, key id, plan and current period usage of the API key that signs the request. Use it to check that a key works.
# Delete a computer
Source: https://docs.inkrelay.app/api-reference/computers/delete-a-computer
/openapi/openapi.json delete /v1/computers/{id}
Soft-deletes the computer and its printers, revokes its agent tokens and disconnects the agent. The virtual test computer cannot be deleted.
# Get a computer
Source: https://docs.inkrelay.app/api-reference/computers/get-a-computer
/openapi/openapi.json get /v1/computers/{id}
Returns one computer with its agent version, operating system and connection state.
# List computers
Source: https://docs.inkrelay.app/api-reference/computers/list-computers
/openapi/openapi.json get /v1/computers
Lists the computers running the InkRelay agent in the organization and mode of the caller, newest first. A test key sees the virtual test computer.
# Rename, disable or re-enable a computer
Source: https://docs.inkrelay.app/api-reference/computers/rename-disable-or-re-enable-a-computer
/openapi/openapi.json patch /v1/computers/{id}
Renames a computer or disables it. Disabling disconnects the agent at once; re-enabling lets it reconnect and go online on its next ping.
# List events
Source: https://docs.inkrelay.app/api-reference/events/list-events
/openapi/openapi.json get /v1/events
The organization's event feed for the caller's mode, the same events webhooks deliver. Page back with dir=desc, or poll for new events with dir=asc and after set to the last id you processed. Polling with dir=asc sees an event about 2 seconds after it happens, so events that commit out of order are never skipped.
# Cancel a print job
Source: https://docs.inkrelay.app/api-reference/print-jobs/cancel-a-print-job
/openapi/openapi.json post /v1/printjobs/{id}/cancel
Cancels a print job that is still queued or sent; the agent drops it if it already has it. Jobs the printer has received can no longer be cancelled.
# Create a print job
Source: https://docs.inkrelay.app/api-reference/print-jobs/create-a-print-job
/openapi/openapi.json post /v1/printjobs
Queues a PDF or raw document for a printer. Send an Idempotency-Key header to make retries safe: a repeat with the same body returns the first response with header Idempotent-Replayed: true. To print an earlier job again, send reprintOf with its id instead of contentType and content.
# Get a print job
Source: https://docs.inkrelay.app/api-reference/print-jobs/get-a-print-job
/openapi/openapi.json get /v1/printjobs/{id}
Returns one print job with its current state and the time of each state change so far.
# List print jobs
Source: https://docs.inkrelay.app/api-reference/print-jobs/list-print-jobs
/openapi/openapi.json get /v1/printjobs
Lists the print jobs of the organization and mode of the caller, newest first by default. Filter by printer, computer, state or creation time.
# List the print jobs of a printer
Source: https://docs.inkrelay.app/api-reference/print-jobs/list-the-print-jobs-of-a-printer
/openapi/openapi.json get /v1/printers/{id}/printjobs
Lists the print jobs sent to one printer, newest first by default. Returns 404 when the printer is not in the organization and mode of the caller.
# List the state changes of a print job
Source: https://docs.inkrelay.app/api-reference/print-jobs/list-the-state-changes-of-a-print-job
/openapi/openapi.json get /v1/printjobs/{id}/events
Lists every state change of one print job, oldest first, with the message and the actor (api, user, agent or system) behind it.
# Get a printer
Source: https://docs.inkrelay.app/api-reference/printers/get-a-printer
/openapi/openapi.json get /v1/printers/{id}
Returns one printer with its state, capabilities, default options and custom paper sizes.
# List printers
Source: https://docs.inkrelay.app/api-reference/printers/list-printers
/openapi/openapi.json get /v1/printers
Lists the printers reported by every computer in the organization and mode of the caller. A test key sees the four virtual test printers.
# List the printers on a computer
Source: https://docs.inkrelay.app/api-reference/printers/list-the-printers-on-a-computer
/openapi/openapi.json get /v1/computers/{id}/printers
Lists the printers that the agent on one computer reports. Returns 404 when the computer is not in the organization and mode of the caller.
# Update printer settings
Source: https://docs.inkrelay.app/api-reference/printers/update-printer-settings
/openapi/openapi.json patch /v1/printers/{id}
Changes the display name, the live flag, default job options, custom paper sizes or remarks of a printer. Only enabled printers accept jobs.
# Get print usage for this period
Source: https://docs.inkrelay.app/api-reference/usage/get-print-usage-for-this-period
/openapi/openapi.json get /v1/usage
Returns the plan, the live prints it includes this billing period, the live prints used so far (overage past the allowance on paid plans) and earlier periods. Lite periods are calendar months; paid periods follow the subscription's renewal date. Test mode jobs never count, so a test key sees the same live usage.
# Create a webhook endpoint
Source: https://docs.inkrelay.app/api-reference/webhooks/create-a-webhook-endpoint
/openapi/openapi.json post /v1/webhooks
Creates an endpoint in the mode of the calling key. The response holds the signing secret; store it now, it is never shown again. Live endpoints need https and every URL must resolve to a public address.
# Delete a webhook endpoint
Source: https://docs.inkrelay.app/api-reference/webhooks/delete-a-webhook-endpoint
/openapi/openapi.json delete /v1/webhooks/{id}
Deletes the endpoint and its delivery log; queued retries stop.
# Get a webhook endpoint
Source: https://docs.inkrelay.app/api-reference/webhooks/get-a-webhook-endpoint
/openapi/openapi.json get /v1/webhooks/{id}
Returns one endpoint, including whether it is enabled and why it was disabled.
# List deliveries for an endpoint
Source: https://docs.inkrelay.app/api-reference/webhooks/list-deliveries-for-an-endpoint
/openapi/openapi.json get /v1/webhooks/{id}/deliveries
Delivery log for one endpoint, newest first, kept for 30 days. Filter with status to find failed deliveries to replay.
# List webhook endpoints
Source: https://docs.inkrelay.app/api-reference/webhooks/list-webhook-endpoints
/openapi/openapi.json get /v1/webhooks
Lists webhook endpoints in the organization and mode of the calling key, newest first. Secrets are never included.
# Replay a delivery
Source: https://docs.inkrelay.app/api-reference/webhooks/replay-a-delivery
/openapi/openapi.json post /v1/webhooks/deliveries/{id}/replay
Sends the same event again as a new delivery with its own retry schedule. webhook-id stays the event id, so receivers that already processed it can skip it.
# Rotate a webhook signing secret
Source: https://docs.inkrelay.app/api-reference/webhooks/rotate-a-webhook-signing-secret
/openapi/openapi.json post /v1/webhooks/{id}/rotate-secret
Replaces the signing secret and returns the new one; store it now, it is never shown again. Until previousSecretExpiresAt every delivery carries two signatures, one per secret, so receivers can switch without dropping deliveries. A rotation during an overlap replaces the older secret.
# Send a test ping
Source: https://docs.inkrelay.app/api-reference/webhooks/send-a-test-ping
/openapi/openapi.json post /v1/webhooks/{id}/ping
Queues a signed ping delivery to the endpoint, with the usual retries, to check that it receives and verifies webhooks. The body is an envelope of type ping with empty data. Pings are not events: they never appear in GET /v1/events and never count as usage.
# Update a webhook endpoint
Source: https://docs.inkrelay.app/api-reference/webhooks/update-a-webhook-endpoint
/openapi/openapi.json patch /v1/webhooks/{id}
Changes the URL, description, events or enabled flag. Enabling clears failingSince and disabledReason.
# Changelog
Source: https://docs.inkrelay.app/changelog
Changes to the API, agent and SDKs.
* Agent 0.4.6: choosing printers is one tick box per printer in the agent's local page (alias, default paper, remarks and custom paper sizes are under Advanced). The Windows install command now shows numbered steps with download progress, opens the sign-in page and waits until the computer is connected; set `INKRELAY_NO_WAIT=1` to skip the wait. See [Install](/install-agent#install).
* Agent 0.4.6: `inkrelay-agent status --json` and `inkrelay-agent open --print-url` for scripts, and Windows uninstalls and upgrades no longer roll back when the agent is slow to stop. Agents update on their own.
* Agent 0.4.5: Windows upgrades and uninstalls wait for the agent to stop, so they no longer ask for a restart. Agents update on their own.
* Webhooks: rotate an endpoint's secret with `POST /v1/webhooks/{id}/rotate-secret`. The old secret keeps signing for an overlap of 24 hours by default (up to 7 days), and `webhook-signature` carries both signatures meanwhile. Send a test event with `POST /v1/webhooks/{id}/ping`. See [Webhooks](/guides/webhooks#rotate-the-secret).
* Postman collection: import it from `https://docs.inkrelay.app/postman/inkrelay.postman_collection.json`. See the [quickstart](/quickstart#use-postman).
* Agent 0.4.2: uses the InkRelay client id for device sign-in, cleans up the installer and updates dependencies.
* Agent 0.4.3: reports errors and crashes to InkRelay with the computer id, operating system and version only (no host name, user paths, tokens or proxy passwords). The Windows install script now fails loudly when the installer fails. Agents update on their own.
* Agent 0.4.4: error reports never include URL query strings, and crashes in background tasks are reported before the agent restarts. Agents update on their own.
* Python SDK 0.3.0: `AsyncInkRelay`, plus `webhooks.rotate_secret()` and `webhooks.ping()` on both clients. See [Python](/sdks/python).
* TypeScript SDK 0.3.0: `webhooks.rotateSecret()` and `webhooks.ping()`. See [TypeScript](/sdks/typescript).
* If you use `ir.raw` from the Python SDK 0.2.0 or earlier, listing the deliveries of an endpoint that received a test ping raises; `ir.webhooks.deliveries()` is not affected, and SDK 0.3.0 fixes it.
InkRelay launches with its own hosts and packages:
* Website: [https://inkrelay.app](https://inkrelay.app)
* Dashboard: [https://app.inkrelay.app](https://app.inkrelay.app)
* API: [https://api.inkrelay.app](https://api.inkrelay.app)
* Documentation: [https://docs.inkrelay.app](https://docs.inkrelay.app)
* Agent downloads and installers: [https://dl.inkrelay.app/agent/](https://dl.inkrelay.app/agent/)
* npm: `@inkrelay/sdk` (`import { InkRelay } from "@inkrelay/sdk"`)
* PyPI: `inkrelay` (`from inkrelay import InkRelay`)
* SDK environment variable: `INKRELAY_API_KEY`
* Webhook User-Agent: `InkRelay-Webhooks/1.0`
* Agent: `inkrelay-agent`
API keys start with `ir_live_` and `ir_test_`.
Agent 0.4 is available for Windows, macOS and Linux. Follow the steps in [Install](/install-agent#install) to set it up on each computer, then sign in.
Webhooks with Standard Webhooks signatures, retries, replay and auto-disable; `GET /v1/events`; `GET /v1/usage`; TypeScript and Python SDKs; OpenAPI reference with playground.
List endpoints now return the id of the last item as `nextCursor` even on the final page, and echo your `after` value when a page is empty, so you can poll with `dir=asc`.
Agent for Windows, macOS and Linux. Installers: one-line scripts, `.pkg`, `.msi`, `.deb`, `.rpm` and tarballs, all signed with minisign. Windows printing through the native spooler with SumatraPDF for PDFs, Linux printing through CUPS, proxy support, and signed automatic updates with staged rollouts and automatic rollback. The download page lists every installer, and the guides cover [installing the agent](/install-agent) and [migrating from PrintNode](/guides/migrating-from-printnode).
Create print jobs with `POST /v1/printjobs` (PDF or raw, by URL or base64) and follow them through `queued`, `sent`, `received`, `printing` and `done`. `Idempotency-Key` support, job cancellation and per-job event history. Computers and printers endpoints with capabilities, default options and custom paper sizes. Test mode with a virtual computer and four test printers that finish jobs in about 3 seconds. The dashboard gains Computers, Printers and Jobs pages with live updates. Agent for macOS with a local UI.
Sign in to the dashboard with an email code, create organizations and invite your team. Organization API keys (`ir_live_` and `ir_test_`) with the `read`, `print` and `admin` scopes, accepted as Bearer or Basic credentials. `GET /v1/whoami` and the OpenAPI 3.1 reference.
# Errors
Source: https://docs.inkrelay.app/guides/errors
Error format, status codes and request ids.
Errors use standard HTTP status codes and one JSON shape. Every response, successful or not, carries an `X-Request-Id` header; include it when you contact support.
```json theme={null}
{ "error": { "code": "not_found", "message": "Printer not found", "request_id": "3f2b1c9e-7a4d-4e8b-9c51-6d0a2b7e4f18" } }
```
`code` is a stable, machine readable string you can switch on. `message` is for people and can change. `request_id` matches the `X-Request-Id` header.
## Status codes
| Status | Meaning |
| - | - |
| `400` | Malformed request: bad JSON, a missing or unknown field, or a value out of range (`invalid_request`) |
| `401` | Missing or invalid key (`unauthorized`) |
| `403` | The key lacks the scope (`insufficient_scope`) or your role cannot do this (`forbidden`) |
| `404` | Not found in this organization and mode (`not_found`). A resource of another organization or mode looks the same |
| `409` | Conflict with idempotency or state: `idempotency_in_progress`, `not_cancellable` for a job that already reached the agent, or `endpoint_disabled` when you replay a delivery on a disabled webhook endpoint |
| `410` | The stored file of a job you asked to reprint is gone (`file_expired`). Files are kept for 7 days; send the document again |
| `413` | Payload too large: print content over 25 MB (`content_too_large`) |
| `422` | The request is well formed but cannot be done: `invalid_content`, `invalid_content_url`, `printer_disabled`, `mode_mismatch`, `idempotency_key_reused`, and `invalid_url` for webhook endpoints |
| `429` | Rate limited, or the test mode daily cap (`test_mode_daily_cap`). Wait for `Retry-After` |
| `5xx` | A problem on our side, such as `503 storage_unavailable`. Retry with backoff |
## Handling errors
Check the status first, then `code`. Retry `429` and `5xx` with backoff, and use an `Idempotency-Key` on `POST /v1/printjobs` so a retry cannot print twice (see [Rate limits and idempotency](/guides/rate-limits-and-idempotency)). Fix the request for any other `4xx`.
```ts TypeScript theme={null}
import { InkRelay, InkRelayError } from "@inkrelay/sdk";
const ir = new InkRelay({ apiKey: process.env.INKRELAY_API_KEY });
try {
await ir.printJobs.create({ printerId, title: "Invoice", contentType: "pdf_uri", content: url });
} catch (err) {
if (err instanceof InkRelayError) console.error(err.status, err.code, err.requestId);
else throw err;
}
```
```python Python theme={null}
from inkrelay import InkRelay, InkRelayError
ir = InkRelay()
try:
ir.print_jobs.create({"printerId": printer_id, "title": "Invoice", "contentType": "pdf_uri", "content": url})
except InkRelayError as err:
print(err.status, err.code, err.request_id)
```
A print job that was accepted can still fail later. Its `state` becomes `error` and `stateMessage` holds the reason from the operating system. Watch for `printjob.error` in [webhooks](/guides/webhooks).
# Migrating from PrintNode
Source: https://docs.inkrelay.app/guides/migrating-from-printnode
Move an existing PrintNode integration to InkRelay, with the differences in paths, ids, states and options.
InkRelay keeps the shape of the PrintNode API, so most integrations move over by changing the base URL, the API key and a few ids. This page lists every difference you need to handle.
## Before and after
A print job in PrintNode:
```bash theme={null}
curl -u "PRINTNODE_KEY:" https://api.printnode.com/printjobs \
-H "Content-Type: application/json" \
-H "X-Idempotency-Key: invoice-1042" \
-d '{
"printerId": 12345,
"title": "Invoice 1042",
"contentType": "pdf_base64",
"content": "JVBERi0...",
"source": "my-erp",
"qty": 1,
"options": { "copies": 1, "paper": "A4" }
}'
```
The same job in InkRelay:
```bash theme={null}
curl -u "ir_live_your_key:" https://api.inkrelay.app/v1/printjobs \
-H "Content-Type: application/json" \
-H "Idempotency-Key: invoice-1042" \
-d '{
"printerId": "0192f0aa-...",
"title": "Invoice 1042",
"contentType": "pdf_base64",
"content": "JVBERi0...",
"source": "my-erp",
"qty": 1,
"options": { "copies": 1, "paper": "A4" }
}'
```
The response is the job object, not a bare integer, so read the id from `id`.
## Authentication and base URL
InkRelay accepts the same Basic scheme: send the API key as the username with an empty password, or send `Authorization: Bearer ir_live_...`. Keys are created in the dashboard, one per organization, with the scopes `read`, `print` and `admin`.
The base URL is `https://api.inkrelay.app/v1`. Every path from the PrintNode API you use keeps its name and gains the `/v1` prefix.
## Ids
InkRelay ids are UUID strings, not integers. Printers, computers and jobs all use them, so change any column or type that stores a PrintNode id as a number. `POST /v1/printjobs` returns the job object (`{ "id": "0192f1a0-...", "state": "queued", ... }`), not a bare integer. Look up the new printer ids with `GET /v1/printers`.
## API differences
| PrintNode | InkRelay |
| - | - |
| Base `https://api.printnode.com` | `https://api.inkrelay.app/v1` |
| `Authorization: Basic base64(KEY:)` | Same header works; `Authorization: Bearer ir_live_...` also accepted |
| `X-Idempotency-Key` (duplicate returns 409) | `Idempotency-Key` (same key and body returns the first job; same key with another body returns 422) |
| `X-Child-Account-By-Id/Email/CreatorRef` | Not supported in v1 (integrator accounts are out of scope) |
| Integer ids, comma id sets `/printers/1,3` | String ids (UUIDv7), one id per path |
| `limit`, `after`, `dir`, plus `Records-*` headers | Same query names (`limit` 1 to 100, default 25; `after` is an id; `dir` is `asc` or `desc`). The body is `{ data, hasMore, nextCursor }`; pass `nextCursor` as the next `after`. No `Records-*` headers |
| Errors `{uid, code, message}`, `Request-Id` | `{error: {code, message, request_id}}`, `X-Request-Id` |
| 10 requests per second per account | 20 per second per API key, `429` with `Retry-After` |
| `GET /whoami`, `/computers[/{id}]`, `/printers[/{id}]`, `/computers/{id}/printers` | Same paths under `/v1` |
| `DELETE /computers/{set}` | `DELETE /v1/computers/{id}` |
| `POST /printjobs` returns a bare integer | `POST /v1/printjobs` returns the job object |
| `DELETE /printjobs/{set}` (cancel) | `POST /v1/printjobs/{id}/cancel` |
| `GET /printjobs/{set}/states` | `GET /v1/printjobs/{id}/events` |
| `POST /webhook`, `PATCH/DELETE /webhook/{id}` | `POST /v1/webhooks`, `PATCH/DELETE /v1/webhooks/{id}` (Standard Webhooks signatures) |
| `GET /download/clients` | `https://dl.inkrelay.app/agent/stable.json` |
| Scales, `/account/*`, `/account/apikey/*` | Not supported; API keys are managed in the dashboard |
## Print jobs and states
| Job field or state | InkRelay |
| - | - |
| `printerId`, `title`, `source`, `contentType`, `content`, `qty`, `options`, `authentication` | Same names and values. `contentType` is `pdf_uri`, `pdf_base64`, `raw_uri` or `raw_base64`. `printerId` is a UUID string |
| `expireAfter` default 14 days | Same field, in seconds (60 to 604800), default 24 hours |
| Body limit 50 MB | Base64 and URI content up to 25 MB decoded |
| `new` / `sent_to_client` / `done` / `error` / `expired` | `queued` / `sent`, then `received`, `printing` / `done` (spooler finished the job) / `error` / `expired`, plus `cancelled`. Map `new` to `queued` and `sent_to_client` to `sent` |
## Print options
| Option | InkRelay behaviour |
| - | - |
| `bin`, `copies`, `duplex`, `paper`, `pages`, `rotate`, `fit_to_page`, `color` | All OSes (`rotate` 180 is ignored on Windows) |
| `dpi`, `media`, `collate` | macOS and Linux; ignored with a warning for Windows PDF jobs |
| `nup` | macOS and Linux only |
| Options the printer does not support | Ignored and listed in `stateMessage` |
| Unknown option names | Rejected with `400 invalid_request` |
## Webhooks
PrintNode webhooks send a shared secret header. InkRelay signs every request with an HMAC following the [Standard Webhooks](https://www.standardwebhooks.com) format (`webhook-id`, `webhook-timestamp`, `webhook-signature`), so verify the signature instead of comparing a header. There are 15 event types (`printjob.queued` through `printjob.expired`, `computer.online`, `computer.offline` and the `printer.*` events), failed deliveries are retried for about 27 hours, and you can replay any delivery from the dashboard. See [Webhooks](/guides/webhooks) for the payload and verification code.
## Rate limit
Each API key may make 20 requests per second. Above that you get `429` with `Retry-After`. See [Rate limits and idempotency](/guides/rate-limits-and-idempotency).
## SDKs
The [TypeScript](/sdks/typescript) and [Python](/sdks/python) SDKs wrap these calls with retries, automatic idempotency keys and `createAndWait`, which replaces polling a job id by hand.
## What you gain
* A full job lifecycle with `received` and `printing` states and an event history for every job.
* Signed webhooks that follow the Standard Webhooks format, with automatic retries and a delivery log you can replay.
* Per-printer default options, so a request only sends what differs from the printer's defaults.
* Scoped API keys (`read`, `print`, `admin`), each limited to one organization.
* Test mode keys (`ir_test_...`) that accept jobs without printing on paper.
## Checklist
1. Install the [agent](/install-agent) on each computer and approve it in the dashboard.
2. Create an API key with the `print` scope and switch the base URL.
3. Replace stored printer ids with the string ids from `GET /v1/printers`.
4. Read the job id from the response object and use `POST /v1/printjobs/{id}/cancel` to cancel.
5. Register a webhook if you relied on PrintNode webhooks, and verify the signature on each delivery.
6. Send an `Idempotency-Key` header instead of `X-Idempotency-Key`, and keep your request rate under 20 per second per key.
7. Test everything with an `ir_test_` key and the virtual printers first (see [Test mode](/guides/test-mode)).
# Options and paper sizes
Source: https://docs.inkrelay.app/guides/options-and-paper-sizes
Control copies, paper, duplex, color, trays and more.
Options tune how a PDF prints. Every option is optional: anything you omit is filled from the printer's defaults in the dashboard, and options the printer does not support are ignored and listed as warnings in the job's `stateMessage`.
```json theme={null}
{ "options": { "copies": 2, "paper": "A4", "duplex": "long-edge", "color": false, "pages": "1-3,5" } }
```
| Option | Values | What it does |
| - | - | - |
| `copies` | integer, 1 to 999 | Number of copies of each page range |
| `paper` | a name from the printer's `papers` or `customPapers` | Paper size, for example `A4` or `Letter` |
| `duplex` | `long-edge`, `short-edge`, `one-sided` | Two-sided printing. `long-edge` flips on the long side (portrait books), `short-edge` on the short side (calendars) |
| `color` | `true` or `false` | `false` prints in grayscale |
| `dpi` | text such as `600x600` | Print resolution, from the printer's `dpis` |
| `bin` | a name from the printer's `bins` | Paper tray or output bin |
| `media` | a name from the printer's `medias` | Media type, for example `Plain` or `Photo` |
| `collate` | `true` or `false` | Keep multiple copies in page order |
| `fit_to_page` | `true` or `false` | Scale the page to fit the paper |
| `rotate` | `0`, `90`, `180`, `270` | Rotate the page in degrees |
| `pages` | text such as `1-3,5` | Pages to print, as numbers and ranges separated by commas |
| `nup` | integer, 1 to 16 | Pages per sheet, from the printer's `nup` |
Unknown option names are rejected with `400 invalid_request`. Raw jobs ignore options (see [Raw printing](/guides/raw-printing)).
If an option has a value the printer cannot honour, the job still prints and `stateMessage` explains what was dropped, for example `duplex ignored: printer cannot print two-sided`. Read `stateMessage` on the finished job if the output differs from what you asked for.
## Reading capabilities
`GET /v1/printers/{id}` returns what the printer supports in `capabilities`. The agent reads it from the operating system, so it reflects the installed driver.
```json theme={null}
{
"id": "PRINTER_ID",
"name": "Test A4 Laser",
"capabilities": {
"papers": { "A4": [2100, 2970], "Letter": [2159, 2794], "80mm Receipt": [800, null] },
"bins": ["Tray 1", "Tray 2"],
"dpis": ["600x600"],
"duplex": true,
"color": false,
"collate": true,
"copies": 99,
"extent": [[900, 1480], [2970, 4200]],
"medias": ["Plain"],
"nup": [1, 2, 4],
"printrate": { "unit": "ppm", "rate": 30 },
"supports_custom_paper_size": false
},
"defaultOptions": { "paper": "A4", "duplex": "long-edge" },
"customPapers": []
}
```
`papers` maps each paper name to `[width, height]` in tenths of a millimetre, so `"A4": [2100, 2970]` is 210 by 297 mm. `bins`, `dpis` and `medias` list the values you can pass to `bin`, `dpi` and `media`. A paper dimension can be `null` when the driver does not report it, as for a roll of receipt paper (`"80mm Receipt": [800, null]`, 80 mm wide with no fixed length). `extent` is `[[minWidth, minHeight], [maxWidth, maxHeight]]` in the same unit and gives the range of custom paper sizes the printer accepts, or `null`. `printrate` is the printer's rated speed, for example 30 pages per minute, or `null`. `duplex`, `color` and `collate` say whether the printer supports them, `copies` is the largest copy count it accepts, and `nup` lists the pages per sheet it offers.
## Printer defaults and custom paper sizes
Set defaults once and send only what differs per job. In the dashboard, open the printer and edit its defaults. Over the API, use `PATCH /v1/printers/{id}` with the `admin` scope:
```bash theme={null}
curl -X PATCH https://api.inkrelay.app/v1/printers/PRINTER_ID \
-H "Authorization: Bearer $INKRELAY_API_KEY" -H "Content-Type: application/json" \
-d '{
"defaultOptions": { "paper": "A4", "duplex": "long-edge", "color": false },
"customPapers": [{ "name": "Label 100x150", "width": 1000, "height": 1500 }]
}'
```
`customPapers` adds paper sizes the printer does not list, such as label stock. `width` and `height` use the same unit as `papers`, tenths of a millimetre, so the example above is 100 by 150 mm. Once saved, pass the name as `paper`. A printer accepts up to 50 custom papers. Sending `customPapers` replaces the whole list.
`PATCH /v1/printers/{id}` also sets `displayName`, `enabled` (the live switch) and `remarks`.
## Platform notes
* `nup` works on macOS and Linux only.
* On Windows, PDFs print through SumatraPDF. `pages`, `copies`, `fit_to_page`, `rotate` (90 and 270), `color`, `duplex`, `paper` and `bin` are honoured. `dpi`, `media`, `collate`, `nup` and `rotate` 180 are ignored, so a job that asks for `rotate` 180 prints without rotation. Each ignored option adds a warning to `stateMessage`.
* `dpi`, `media` and `collate` apply on macOS and Linux.
* Paper names come from the operating system and the driver. Read `papers` before you hard-code a name.
# Printing PDFs
Source: https://docs.inkrelay.app/guides/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`).
```ts TypeScript theme={null}
import { InkRelay } from "@inkrelay/sdk";
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"])
```
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)).
# Rate limits and idempotency
Source: https://docs.inkrelay.app/guides/rate-limits-and-idempotency
Stay under 20 requests per second and retry safely.
Each API key may make 20 requests per second. Above that the API answers `429` with a `Retry-After` header in seconds and `RateLimit` headers that show your remaining budget. The SDKs wait and retry for you.
## Headers
| Header | Meaning |
| - | - |
| `Retry-After` | On a `429`, the number of seconds to wait before the next request |
| `RateLimit-Limit` | Requests allowed per second for this key (20) |
| `RateLimit-Remaining` | Requests left in the current window |
| `RateLimit-Reset` | Seconds until the window resets |
Spread bursts out instead of firing them at once. For a batch of documents, send jobs at a steady pace, or limit yourself to a few requests in flight.
## Idempotency-Key
A network error leaves you unsure whether `POST /v1/printjobs` created a job. If you retry blindly you might print twice. Send an `Idempotency-Key` header to make the retry safe:
```bash theme={null}
curl https://api.inkrelay.app/v1/printjobs -H "Authorization: Bearer $INKRELAY_API_KEY" -H "Idempotency-Key: order-1042-label" -H "Content-Type: application/json" -d @job.json
```
* The key is any unique string of up to 255 characters. Use something that names the thing you are printing, such as `order-1042-label`, or a random UUID you store with your own record.
* Keys are kept for 24 hours per organization.
* The same key with the same body returns the original job, with the header `Idempotent-Replayed: true`, and creates nothing new.
* The same key with a different body is refused with `422 idempotency_key_reused`. Use a new key for a different job.
* A request that arrives while the first one with the same key is still being processed answers `409 idempotency_in_progress`. Wait a moment and retry with the same key.
* A key longer than 255 characters answers `400 invalid_request`, because the header is validated before the request is processed.
* If the first request fails (a `4xx` or `5xx` response), the key is released and the retry runs normally.
## Retrying safely
Retry on `429`, on `5xx` responses and on network errors, waiting longer each time (exponential backoff with some randomness), and honour `Retry-After` when it is present. Do not retry other `4xx` responses, because the same request will fail again.
Always send the same `Idempotency-Key` on every retry of one job. The SDKs do this for you: they set a key automatically on `printJobs.create` (`print_jobs.create` in Python) and reuse it on every retry, so a retried request never prints twice.
# Raw printing (ESC/POS, ZPL)
Source: https://docs.inkrelay.app/guides/raw-printing
Send printer language bytes straight to receipt and label printers.
Raw jobs skip rendering and send your bytes to the printer unchanged, which is how receipt printers (ESC/POS) and label printers (ZPL) are usually driven. Use `raw_base64` or `raw_uri`; options such as `paper` and `duplex` do not apply.
```bash theme={null}
# ZPL label: 4x6 with one line of text
CONTENT=$(printf '^XA^FO50,50^A0N,50,50^FDInkRelay^FS^XZ' | base64)
curl https://api.inkrelay.app/v1/printjobs -H "Authorization: Bearer $INKRELAY_API_KEY" -H "Content-Type: application/json" \
-d "{\"printerId\":\"PRINTER_ID\",\"title\":\"Label\",\"contentType\":\"raw_base64\",\"content\":\"$CONTENT\"}"
```
```python theme={null}
import base64
import os
from inkrelay import InkRelay
ir = InkRelay(api_key=os.environ["INKRELAY_API_KEY"])
printer_id = "PRINTER_ID"
# ESC/POS receipt: init, text, feed and cut
data = b"\x1b@" + b"InkRelay receipt\n" + b"\n\n\n" + b"\x1dV\x00"
ir.print_jobs.create({"printerId": printer_id, "title": "Receipt", "contentType": "raw_base64", "content": base64.b64encode(data).decode()})
```
With `raw_uri`, `content` is a URL that serves the bytes. The same rules as `pdf_uri` apply: 25 MB at most, no redirects, no private addresses, ports 80 and 443 only, and optional `authentication` for Basic protected URLs. `qty` prints the raw job that many times.
`raw_base64` content must be canonical base64 text. Unlike PDF jobs, InkRelay does not inspect the bytes beyond their size, so a malformed label prints whatever the printer makes of it.
## Choosing the right printer
The printer's driver must accept raw data.
* **Windows**: the agent writes your bytes with the `RAW` datatype, which bypasses the driver's rendering. Use the printer's own driver or the "Generic / Text Only" driver. If the printer is shared over the network, add it as a local TCP/IP printer first (see [Install the agent](/install-agent)).
* **macOS and Linux**: the agent submits the job to CUPS with the `application/vnd.cups-raw` format. Create the queue with the raw driver (`lpadmin -p NAME -E -v DEVICE_URI -m raw`, or "Raw Queue" in older CUPS) so CUPS does not try to filter the data.
Check that the printer is online and marked live before you send the first job.
## Test printers
Test mode includes two raw printers, so you can build and test without hardware:
* **Test Receipt 80mm** accepts ESC/POS bytes.
* **Test Label 4x6** accepts ZPL bytes.
Both finish in about 3 seconds like the other test printers. See [Test mode](/guides/test-mode).
## Encoding pitfalls
* Send bytes, not text. Build the payload as bytes (`bytes` in Python, `Buffer` in Node) and base64 encode that, so the characters your printer needs are not changed on the way.
* Text inside a receipt uses the printer's code page. If you need accents or non-Latin scripts, select the code page with the matching ESC/POS command (`ESC t n`) and encode the text to that code page yourself, for example `"é".encode("cp858")`. Do not send UTF-8 unless the printer is configured for it.
* Include the line feeds and the cut command your printer needs. Receipt printers print nothing until they receive a line feed, and they do not cut by themselves.
* ZPL commands end with `^XZ`. A label that is missing it waits for more data.
# Test mode
Source: https://docs.inkrelay.app/guides/test-mode
Build against virtual printers without paper or charges.
Every organization has a test mode with its own keys (`ir_test_`), its own webhook endpoints and a virtual computer called InkRelay Test Computer. Test jobs walk from `queued` to `done` in about 3 seconds through the same code as live jobs, so webhooks, the event feed and the dashboard behave exactly as they will in production.
The mode comes from the key. An `ir_test_` key only sees test data and an `ir_live_` key only sees live data, so a resource of the other mode answers `404`. In the dashboard, the Test mode toggle filters every list the same way.
## Test printers
InkRelay Test Computer has four printers. They are created the first time you use test mode.
| Printer | Behaves like |
| - | - |
| Test A4 Laser | A4 mono laser printer with duplex |
| Test Letter Color | Letter color printer with duplex |
| Test Receipt 80mm | An 80 mm receipt printer that takes raw ESC/POS |
| Test Label 4x6 | A 4x6 inch label printer that takes raw ZPL |
Only `ir_test_` keys can target these printers, and test keys can only target them. Sending a job to the wrong mode answers `422 mode_mismatch`. See [Raw printing](/guides/raw-printing) for the two raw printers.
## Forcing an error
Put `[fail]` anywhere in the job `title` and the job ends in `error` instead of `done`, so you can test your error handling and your `printjob.error` webhook handler:
```bash theme={null}
curl https://api.inkrelay.app/v1/printjobs \
-H "Authorization: Bearer $INKRELAY_API_KEY" -H "Content-Type: application/json" \
-d '{"printerId":"PRINTER_ID","title":"Invoice [fail]","contentType":"pdf_uri","content":"https://inkrelay.app/sample.pdf"}'
```
## Limits
Test mode is free and nothing is printed on paper. Each organization can create 500 test jobs per day (a UTC day). Past that, `POST /v1/printjobs` answers `429 test_mode_daily_cap` until the next day. Test jobs never count toward your plan's prints; `GET /v1/usage` reports your live usage for the current period.
## Switching to live
When your integration works against the test printers:
1. [Install the agent](/install-agent) on a computer with a real printer and mark the printer live.
2. Create a live key (it starts with `ir_live_`) and use it instead of the test key.
3. Create live webhook endpoints. Test endpoints never receive live events. Live endpoints must use `https:`.
4. Replace the test printer ids with the ids from `GET /v1/printers` on the live key.
Nothing else changes: the same requests, the same states and the same webhook payloads.
# Webhooks
Source: https://docs.inkrelay.app/guides/webhooks
Receive signed events for jobs, computers and printers.
Webhooks push events to your HTTPS endpoint as they happen, so you do not need to poll. Every request is signed with your endpoint secret following the [Standard Webhooks](https://www.standardwebhooks.com) specification, retried for about 27 hours if your endpoint fails, and visible in a delivery log you can replay from the dashboard.
## Create an endpoint
In the dashboard, open Webhooks, choose Add endpoint, enter your URL and pick the events you want. Any owner, admin or developer of the organization can create an endpoint there. Or call the API with a key that has the `admin` scope:
```bash theme={null}
curl https://api.inkrelay.app/v1/webhooks \
-H "Authorization: Bearer $INKRELAY_API_KEY" -H "Content-Type: application/json" \
-d '{"url":"https://example.com/inkrelay/webhooks","description":"ERP print status","events":["printjob.done","printjob.error"]}'
```
The response includes the signing `secret`, which starts with `whsec_`. It is shown once and never again, so store it in your secret manager now. If you lose it or it leaks, [rotate the secret](#rotate-the-secret).
Each endpoint belongs to one mode, the mode of the key that created it (or of the dashboard test mode toggle). It receives only events of that mode.
URLs must be `https:` for live endpoints, must not contain a username or password, and must resolve to a public address. The check runs when you create or change an endpoint and again before every send, so an address that later points at a private network is refused. A URL that fails answers `422 invalid_url`.
## Event types
| Event | Sent when |
| - | - |
| `printjob.queued` | A job is stored and waiting |
| `printjob.sent` | A job is pushed to the agent |
| `printjob.received` | The agent acknowledged the job |
| `printjob.printing` | The job reached the operating system spooler |
| `printjob.done` | The spooler finished the job |
| `printjob.error` | The job failed |
| `printjob.cancelled` | The job was cancelled |
| `printjob.expired` | The job was not received in time |
| `computer.online` | A computer's agent connected |
| `computer.offline` | A computer's agent disconnected |
| `printer.added` | A new printer appeared on a computer |
| `printer.removed` | A printer was removed from a computer |
| `printer.online` | A printer became available |
| `printer.offline` | A printer became unavailable |
| `printer.updated` | A printer's settings changed |
Subscribe an endpoint only to the events you handle. See [Printing PDFs](/guides/printing-pdfs) for what each job state means.
## Payload
Every request body is a JSON envelope:
```json theme={null}
{
"id": "0192f1a0-7c00-7000-8000-000000000001",
"type": "printjob.done",
"timestamp": "2026-10-14T10:00:00.000Z",
"mode": "test",
"data": {
"id": "0192f1a0-7c00-7000-8000-0000000000aa",
"printerId": "0192f1a0-7c00-7000-8000-0000000000bb",
"title": "Invoice 1042",
"state": "done",
"stateMessage": null
}
}
```
`id` identifies the event and is the same on every retry and replay. `timestamp` is when the event happened and `mode` is `live` or `test`. For `printjob.*` events, `data` is the print job object as `GET /v1/printjobs/{id}` returns it (the example is shortened). For `computer.*` events, `data` holds the computer `id` with its `name` and `state`. For `printer.*` events, `data` holds the printer `id` and the `computerId` it belongs to, with the details of the change.
## Headers
| Header | Value |
| - | - |
| `webhook-id` | The event id. Stable across retries and replays |
| `webhook-timestamp` | Unix seconds when this attempt was sent |
| `webhook-signature` | One or more `v1,` entries separated by a space (two during a secret rotation); accept the request if any entry matches. |
| `content-type` | `application/json` |
| `user-agent` | `InkRelay-Webhooks/1.0` |
## Verify signatures
Always verify the signature before you trust a request. The SDKs check the signature and reject timestamps more than 300 seconds from now, which stops replayed requests.
```ts Node theme={null}
import express from "express";
import { InkRelay } from "@inkrelay/sdk";
const ir = new InkRelay({ apiKey: process.env.INKRELAY_API_KEY });
const app = express();
app.post("/inkrelay/webhooks", express.text({ type: "application/json" }), (req, res) => {
try {
ir.webhooks.verify(req.body, req.headers, process.env.INKRELAY_WEBHOOK_SECRET!);
} catch {
return res.status(400).end();
}
const event = JSON.parse(req.body);
if (event.type === "printjob.done") console.log("printed", event.data.id);
res.status(204).end();
});
```
```python Python theme={null}
import json
import os
from flask import Flask, request
from inkrelay import webhooks, WebhookVerificationError
app = Flask(__name__)
@app.post("/inkrelay/webhooks")
def receive():
body = request.get_data(as_text=True)
try:
webhooks.verify(body, request.headers, os.environ["INKRELAY_WEBHOOK_SECRET"])
except WebhookVerificationError:
return "", 400
event = json.loads(body)
if event["type"] == "printjob.done":
print("printed", event["data"]["id"])
return "", 204
```
Verify the raw body exactly as received. Parsing and re-serialising the JSON changes the bytes and breaks the signature. In Express, read the body as text (as above) rather than through `express.json()`.
Without the SDK, compute `base64(HMAC-SHA256(key, "{webhook-id}.{webhook-timestamp}.{body}"))`, where `key` is the base64 decoded part of the secret after `whsec_`. Compare the result with each `v1,` entry of `webhook-signature` (the header can hold several, separated by spaces) using a constant time comparison, and reject timestamps more than 300 seconds from now.
```ts theme={null}
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyManually(body: string, headers: Record, secret: string, now = Math.floor(Date.now() / 1000)) {
const id = headers["webhook-id"];
const timestamp = headers["webhook-timestamp"];
if (!id || !timestamp || Math.abs(now - Number(timestamp)) > 300) return false;
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const expected = createHmac("sha256", key).update(`${id}.${timestamp}.${body}`).digest("base64");
return (headers["webhook-signature"] ?? "").split(" ").some((part) => {
const [version, signature] = part.split(",");
const a = Buffer.from(signature ?? "");
const b = Buffer.from(expected);
return version === "v1" && a.length === b.length && timingSafeEqual(a, b);
});
}
```
## Rotate the secret
Rotate an endpoint's secret without downtime. Call the endpoint with a key that has the `admin` scope, or rotate it in the dashboard as an owner, admin or developer of the organization:
```bash theme={null}
curl -X POST https://api.inkrelay.app/v1/webhooks/WEBHOOK_ID/rotate-secret \
-H "Authorization: Bearer $INKRELAY_API_KEY" -H "Content-Type: application/json" \
-d '{"overlapSeconds":86400}'
```
The body is optional. `overlapSeconds` is how long the old secret keeps signing, from `0` to `604800` (7 days), and defaults to `86400` (24 hours). The response is the endpoint with the new `secret`, shown once like the first one, and `previousSecretExpiresAt`, the time the old secret stops signing (or `null` when there is no overlap).
During the overlap every delivery carries two entries in `webhook-signature`, the new secret first and the old one second:
```text theme={null}
webhook-signature: v1, v1,
```
Receivers that accept any matching `v1,` entry (the SDKs and the manual check in [Verify signatures](#verify-signatures) both do) keep working while you roll out the new secret: deploy it to your receiver at any point during the overlap, and requests signed by either secret pass. After the overlap only the new secret signs.
Rotating again during an overlap keeps only the most recent previous secret, so the secret from before the first rotation stops working at once. Set `overlapSeconds` to `0` to drop the old secret immediately, for example when it leaked.
## Send a test event
Check that your receiver is reachable and verifies signatures without waiting for a real event:
```bash theme={null}
curl -X POST https://api.inkrelay.app/v1/webhooks/WEBHOOK_ID/ping \
-H "Authorization: Bearer $INKRELAY_API_KEY"
```
It needs the `admin` scope (or, in the dashboard, an owner, admin or developer of the organization) and no body. The response is `202` with the queued delivery, whose `eventType` is `ping`. The request your endpoint receives has this payload:
```json theme={null}
{
"id": "0192f1a0-7c00-7000-8000-000000000002",
"type": "ping",
"timestamp": "2026-10-16T10:00:00.000Z",
"mode": "test",
"data": {}
}
```
A ping is delivered, signed, retried and logged like any other event, so it appears in the delivery log. You cannot subscribe an endpoint to `ping`, and it never appears in `GET /v1/events`. A disabled endpoint answers `409 endpoint_disabled`; re-enable it first. The SDKs have the same two calls, `rotateSecret` and `ping` in TypeScript and `rotate_secret` and `ping` in Python.
## Retries
Attempt 1 is sent immediately. A delivery succeeds when your endpoint answers with any 2xx status within 10 seconds. Anything else (another status, a timeout, a connection error) is a failure, and InkRelay waits and tries again:
| After failed attempt | Wait |
| - | - |
| 1 | 5 seconds |
| 2 | 30 seconds |
| 3 | 2 minutes |
| 4 | 10 minutes |
| 5 | 30 minutes |
| 6 | 2 hours |
| 7 | 8 hours |
| 8 | 16 hours |
Each wait varies by plus or minus 20 percent (jitter). If attempt 9 also fails, the delivery is marked `failed`. A delivery is `pending` before its first send, `retrying` between attempts, `succeeded` after a 2xx and `failed` when attempts run out.
Respond quickly and do the real work afterwards, for example by putting the event on a queue. Redirects are not followed.
## Auto-disable after 5 days of failures
InkRelay records when an endpoint started failing and clears that mark on any success. An endpoint whose deliveries keep failing for 5 days is disabled with the reason `Disabled automatically after 5 days of failed deliveries`, and every organization owner gets an email. Fix your receiver, then re-enable the endpoint in the dashboard (or `PATCH /v1/webhooks/{id}` with `{"enabled": true}`). Events produced while the endpoint was disabled are not sent to it; use [the event feed](#event-feed-alternative) to catch up.
## Replays and duplicates
The delivery log in the dashboard (and `GET /v1/webhooks/{id}/deliveries`) shows every attempt with its status code and the start of the response. Replay a delivery from the dashboard or with `POST /v1/webhooks/deliveries/{id}/replay`. A replay sends the same event again with the same `webhook-id`. Replays need an enabled endpoint.
Delivery is at least once, so a request can arrive more than once, and events can arrive out of order. Store the `webhook-id` values you have handled and skip repeats. Use the job `state` in `data` to ignore an older state that arrives after a newer one. Delivery logs are kept for 30 days.
## Test mode endpoints
An endpoint created with a test key, or while the dashboard is in test mode, receives only test events, and may use an `http:` URL (for example a tunnel to your laptop). Use it with the virtual printers in [Test mode](/guides/test-mode) to exercise your receiver end to end.
## Event feed alternative
If you cannot expose an endpoint, poll `GET /v1/events`. It returns the events of your key's mode, a page at a time. Use `dir=asc` with `after` set to the `nextCursor` of your previous call to read new events in order. Make the first call without `after` (an empty `after=` is rejected with `400`):
```bash theme={null}
curl "https://api.inkrelay.app/v1/events?dir=asc&limit=100" \
-H "Authorization: Bearer $INKRELAY_API_KEY"
```
Then pass the `nextCursor` you got back as `after` on each later call:
```bash theme={null}
curl "https://api.inkrelay.app/v1/events?dir=asc&limit=100&after=$CURSOR" \
-H "Authorization: Bearer $INKRELAY_API_KEY"
```
The response has `data`, `hasMore` and `nextCursor`. When a page is empty, `nextCursor` is the `after` you sent, so you can keep polling with the same value.
With `dir=asc`, an event shows up in the feed about 2 seconds after it happens. Keep `after` at the last event id you processed and poll again; do not skip ahead to a newer id, or you could miss an event that appears a moment later.
# InkRelay
Source: https://docs.inkrelay.app/index
Print PDFs and raw jobs to any printer from your own software.
InkRelay connects your software to printers on Windows, macOS and Linux. Install the agent on a computer that has printers, mark a printer live, and send print jobs with one HTTPS request. You get a real job lifecycle, signed webhooks, test printers and SDKs for TypeScript and Python.
Print your first PDF in under 10 minutes.
Run the agent as a service on macOS, Windows or Linux.
Receive signed events for jobs, computers and printers.
Every endpoint, with an interactive playground.
## How it works
1. Install the agent on Windows, macOS or Linux. It runs as a background service and connects out to InkRelay.
2. Tick the printers that should accept jobs in the agent's local page, or mark them live in the dashboard. InkRelay reads each printer's paper sizes, trays, duplex and color support.
3. Create an API key in the dashboard and call `POST /v1/printjobs` with a PDF or raw payload. Watch the job move through queued, sent, received, printing and done.
## Test mode
Every organization gets a virtual test computer with four printers. Keys that start with `ir_test_` can only print to them, so you can build and test without a physical printer. See [Test mode](/guides/test-mode).
# Install the agent
Source: https://docs.inkrelay.app/install-agent
Run the InkRelay agent as a service on macOS, Windows or Linux.
The agent is a small background service that reports your printers and prints jobs sent to them. It lists the printers the operating system already knows, receives print jobs from the API and hands them to the local print spooler. It only makes outbound HTTPS and WebSocket connections (port 443), so no ports need to be opened. Install it once per computer.
## Downloads
The [download page](https://inkrelay.app/#download) lists every installer. You can also fetch files directly.
Every file lives under `https://dl.inkrelay.app/agent//`. The current `` is the `version` field of [`stable.json`](https://dl.inkrelay.app/agent/stable.json), and its `installers` map lists the full URL of each file.
| Platform | File |
| - | - |
| macOS | `InkRelayAgent-.pkg` |
| Windows | `InkRelayAgent--x64.msi` |
| Debian and Ubuntu | `inkrelay-agent__amd64.deb`, `inkrelay-agent__arm64.deb` |
| Fedora, RHEL and SUSE | `inkrelay-agent__amd64.rpm`, `inkrelay-agent__arm64.rpm` |
| Linux tarball | `inkrelay-agent__linux_amd64.tar.gz`, `inkrelay-agent__linux_arm64.tar.gz` |
## Install
Run the one-liner in Terminal:
```bash theme={null}
curl -fsSL https://dl.inkrelay.app/agent/install.sh | sudo sh
```
Or download the `.pkg` for your Mac and open it. Download links for every file are listed in [`stable.json`](https://dl.inkrelay.app/agent/stable.json).
The package installs the binary at `/usr/local/libexec/inkrelay-agent/inkrelay-agent`, adds a symlink at `/usr/local/bin/inkrelay-agent` and registers a LaunchDaemon with the label `com.inkrelay.agent`. Data lives in `/Library/Application Support/InkRelay`.
Open the local page with:
```bash theme={null}
sudo inkrelay-agent open
```
Unsigned builds: if macOS says the package cannot be opened, open System Settings, Privacy & Security, and choose Open Anyway, or run `sudo installer -pkg InkRelayAgent-.pkg -target /` in Terminal.
In an administrator PowerShell, run the one-liner:
```powershell theme={null}
irm https://dl.inkrelay.app/agent/install.ps1 | iex
```
The command prints numbered steps as it goes:
1. Checking this computer.
2. Downloading InkRelay Agent, with its size and a percentage every 10 percent.
3. Verifying the download.
4. Installing, which takes up to a minute (an elapsed-seconds line appears every 5 seconds).
5. Starting the agent.
It then opens the sign-in page in your browser, prints the code and the approval link, and waits up to 10 minutes until the computer is connected. Sign in, enter the code, approve the computer and tick your printers in the page that opened (see [Choose printers](#choose-printers)). The command ends with `Connected to `. If you do not approve within 10 minutes, it prints how to finish later: run `inkrelay-agent open` from an administrator prompt.
Set `INKRELAY_NO_WAIT=1` before the command to skip opening the browser and the wait, for example in a script or on a computer without a screen:
```powershell theme={null}
$env:INKRELAY_NO_WAIT = "1"; irm https://dl.inkrelay.app/agent/install.ps1 | iex
```
To install a specific version, set `INKRELAY_VERSION` the same way (for example `$env:INKRELAY_VERSION = "0.4.6"`).
Or download the `.msi` (`InkRelayAgent--x64.msi`) and run it. Download links for every file are listed in [`stable.json`](https://dl.inkrelay.app/agent/stable.json). The `.msi` does not open the sign-in page; follow [Sign in](#sign-in) afterwards.
The installer puts the agent in `C:\Program Files\InkRelay` together with `SumatraPDF.exe`, which prints PDF jobs, and the third party licences in `licenses\`. The agent runs as the Windows service `inkrelay-agent` under the LocalSystem account. Data lives in `%ProgramData%\InkRelay`.
Open the local page from an administrator prompt:
```powershell theme={null}
& "C:\Program Files\InkRelay\inkrelay-agent.exe" open
```
Unsigned builds: if SmartScreen shows Windows protected your PC, choose More info, then Run anyway.
### Windows limitation: per-user network printers
Printers connected per user (`\\server\printer`) are not visible to the InkRelay service, which runs as LocalSystem. Add the printer as a local TCP/IP printer, or make the connection machine-wide from an administrator prompt: `rundll32 printui.dll,PrintUIEntry /ga /n\\server\printer`, then restart the Print Spooler service.
The same notice is shown in the agent's local page.
### PDF options on Windows
PDF jobs print through SumatraPDF. These options are honoured: `pages`, `copies`, `fit_to_page`, `rotate` (90 and 270), `color`, `duplex`, `paper` (A2 to A6, letter, legal, tabloid, statement) and `bin`. The options `dpi`, `media`, `collate`, `nup` and `rotate` 180 are ignored, and a warning is added to the job's `stateMessage`. A `bin` name that contains a comma is also ignored with a warning.
Run the one-liner:
```bash theme={null}
curl -fsSL https://dl.inkrelay.app/agent/install.sh | sudo sh
```
Or download the `.deb`, `.rpm` or tarball. Download links for every file are listed in [`stable.json`](https://dl.inkrelay.app/agent/stable.json).
```bash theme={null}
sudo apt install ./inkrelay-agent__amd64.deb
# or
sudo dnf install ./inkrelay-agent__amd64.rpm
```
Use the `arm64` files on ARM machines.
To install from the tarball instead, extract it, put the binary on the path and register the service:
```bash theme={null}
tar -xzf inkrelay-agent__linux_amd64.tar.gz
sudo install -m 0755 inkrelay-agent /usr/local/bin/inkrelay-agent
sudo inkrelay-agent install && sudo inkrelay-agent start
```
Linux needs CUPS. Install `cups` and `cups-client`, and `cups-filters` is recommended. The agent runs as the systemd unit `inkrelay-agent`, and its data lives in `/var/lib/inkrelay`.
```bash theme={null}
systemctl status inkrelay-agent
sudo inkrelay-agent open
```
## Sign in
After installing, the agent has no organization yet. On Windows, the one-line install command does this step for you. Otherwise run `inkrelay-agent open` (with `sudo` on macOS and Linux, from an administrator prompt on Windows) to open the agent's local page, which shows a code and a link. Open `app.inkrelay.app/device`, sign in, enter the code and approve the computer. Only organization owners and admins can approve a computer. If the code expires, start sign-in again from the local page.
When approval completes, the computer appears under Computers in the dashboard and its status turns online.
## Choose printers
The agent lists every printer the operating system knows, but none of them accept API jobs until you tick them. The local page shows one list of printers on this computer, each with a tick box and an online or offline badge. Tick the printers that should accept print jobs. A change takes effect at once, with no Save button, and a printer whose change could not be saved shows the error on its row.
You can make the same choice in the dashboard (Printers), where the switch is called Live. Ticking a printer turns it live. InkRelay then reads the printer's paper sizes, trays, duplex and color support, which you can inspect with `GET /v1/printers/{id}`. See [Options and paper sizes](/guides/options-and-paper-sizes).
Everything else is optional and sits under Advanced on each printer, closed by default: alias, default paper, remarks and custom paper sizes.
Your plan limits how many computers you can connect. A computer over the limit is refused at sign-in.
## Service commands
The agent is a system service. These commands need administrator rights (`sudo` on macOS and Linux, an administrator prompt on Windows).
| Command | What it does |
| - | - |
| `inkrelay-agent status` | Prints `running`, `stopped` or `not installed`, then whether the computer is signed in |
| `inkrelay-agent status --json` | Prints the same as JSON: `service`, `signedIn`, `connected`, `organization`, `userCode` and `verificationUri` |
| `inkrelay-agent start` | Starts the service |
| `inkrelay-agent stop` | Stops the service |
| `inkrelay-agent open` | Opens the local page |
| `inkrelay-agent open --print-url` | Prints the sign-in URL instead of opening a browser |
| `inkrelay-agent uninstall` | Removes the service registration |
| `inkrelay-agent version` | Prints the installed version |
## Use a proxy
If the computer reaches the internet through an HTTP proxy, set it in one of three ways. Then restart the service.
1. At install time on Windows: `msiexec /i InkRelayAgent--x64.msi PROXY_URL="http://proxy:3128"`
2. With the CLI on any OS: `inkrelay-agent install --proxy-url http://proxy:3128`
3. In `config.json` in the data directory, set `proxy_url`:
```json theme={null}
{ "proxy_url": "http://proxy:3128" }
```
The data directory is `/Library/Application Support/InkRelay` on macOS, `%ProgramData%\InkRelay` on Windows and `/var/lib/inkrelay` on Linux. `config.json` also accepts `api_origin`, `local_ui_port` (default `38457`) and `log_level`.
The standard `HTTPS_PROXY` and `NO_PROXY` environment variables also work when they are set machine-wide, so the service can see them.
## Verify a download
Each release publishes `checksums.txt` and a minisign signature. Fetch the public key from `https://dl.inkrelay.app/agent/minisign.pub`, then verify the signature and compare the hash of the file you downloaded:
```bash theme={null}
minisign -Vm checksums.txt -P
sha256sum -c --ignore-missing checksums.txt
```
On macOS use `shasum -a 256 -c` instead of `sha256sum -c`.
## Updates
The agent updates itself. It checks `stable.json` every 6 hours and when the server asks it to, verifies the signature and checksum, stages the new version and restarts. Rollouts are staged, so not every computer updates at the same moment. A version that fails to start twice is rolled back to the previous one.
## Known limitations
* Windows: printers connected per user (`\\server\printer`) are invisible to the service because it runs as LocalSystem. Add them as local TCP/IP printers or make the connection machine-wide. Details are in the Windows tab under [Install](#install).
* Windows: PDF jobs ignore `dpi`, `media`, `collate`, `nup` and `rotate` 180, and add a warning to the job's `stateMessage`.
* Unsigned builds: macOS Gatekeeper and Windows SmartScreen show a warning until signing certificates are in place. The bypass for each is in the Install tabs.
## Troubleshooting
* Run `inkrelay-agent status` first. If it says `stopped`, run `inkrelay-agent start`.
* The local page shows whether the agent is signed in and connected, and explains platform limitations. If it does not load, another program may be using port `38457`; printing keeps working. Set `local_ui_port` in `config.json` to use another port.
* The agent writes `agent.log` in its data directory: `/Library/Application Support/InkRelay` on macOS, `%ProgramData%\InkRelay` on Windows and `/var/lib/inkrelay` on Linux. Set `"log_level": "debug"` in `config.json` for more detail, then restart the service.
* Behind a proxy, see [Use a proxy](#use-a-proxy).
* A computer shows offline in the dashboard when its agent is not connected. Check the network, then the log.
## Uninstall
```bash theme={null}
sudo inkrelay-agent uninstall && sudo rm -rf /usr/local/libexec/inkrelay-agent /usr/local/bin/inkrelay-agent && sudo pkgutil --forget com.inkrelay.agent
```
Open Settings, Apps, choose InkRelay Agent, then Uninstall.
```bash theme={null}
sudo apt remove inkrelay-agent
# or
sudo dnf remove inkrelay-agent
```
After uninstalling, delete the computer from the dashboard so its agent token is revoked.
# Quickstart
Source: https://docs.inkrelay.app/quickstart
Print your first PDF in under 10 minutes.
This guide takes you from a new account to a printed page. You will create a test key, print to a virtual test printer, then install the agent and print to a real printer with a live key.
Sign in at [app.inkrelay.app](https://app.inkrelay.app/login) with your email. We send a one time code, no password needed. Open API keys, create a key with Test mode on and the print scope, and copy it. It is shown once.
```bash theme={null}
export INKRELAY_API_KEY="ir_test_your_key"
```
Call `whoami` to confirm the key works and see which organization and mode it belongs to.
```bash theme={null}
curl https://api.inkrelay.app/v1/whoami \
-H "Authorization: Bearer $INKRELAY_API_KEY"
```
Test keys see the four virtual printers on InkRelay Test Computer. Copy the `id` of "Test A4 Laser".
```bash theme={null}
curl https://api.inkrelay.app/v1/printers \
-H "Authorization: Bearer $INKRELAY_API_KEY"
```
Send a PDF by URL. The job walks from `queued` to `done` in about 3 seconds.
```bash theme={null}
curl https://api.inkrelay.app/v1/printjobs \
-H "Authorization: Bearer $INKRELAY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"printerId":"PRINTER_ID","title":"Hello","contentType":"pdf_uri","content":"https://inkrelay.app/sample.pdf"}'
```
Or use an SDK, which also waits for the job to finish:
```ts TypeScript theme={null}
import { InkRelay } from "@inkrelay/sdk";
const ir = new InkRelay({ apiKey: process.env.INKRELAY_API_KEY });
const printerId = "PRINTER_ID";
const url = "https://inkrelay.app/sample.pdf";
const job = await ir.printJobs.createAndWait({ printerId, title: "Hello", contentType: "pdf_uri", content: url });
console.log(job.state);
```
```python Python theme={null}
from inkrelay import InkRelay
ir = InkRelay() # reads INKRELAY_API_KEY
printer_id = "PRINTER_ID"
url = "https://inkrelay.app/sample.pdf"
job = ir.print_jobs.create_and_wait({"printerId": printer_id, "title": "Hello", "contentType": "pdf_uri", "content": url})
print(job["state"])
```
[Install the agent](/install-agent) on a computer that has a printer, approve it in the dashboard and mark the printer live. Then create a key without Test mode (it starts with `ir_live_`), list printers again and repeat the print call with the real printer id.
## Use Postman
Prefer a GUI client? Import the InkRelay collection into Postman from `https://docs.inkrelay.app/postman/inkrelay.postman_collection.json` (Import, then Link). It has two collection variables: `baseUrl`, set to `https://api.inkrelay.app`, and `apiKey`, which the collection sends as a Bearer token. Set `apiKey` to your test key (`ir_test_...`) first, then send the Identify the organization behind an API key request (`GET /v1/whoami`) to confirm the key works.
## Next steps
* Read [Printing PDFs](/guides/printing-pdfs) for the job fields, limits and states.
* Subscribe to [webhooks](/guides/webhooks) instead of polling.
* Learn how [test mode](/guides/test-mode) behaves so your tests match production.
# Python SDK
Source: https://docs.inkrelay.app/sdks/python
inkrelay for Python 3.10 and later.
The Python SDK is published on PyPI as `inkrelay` and imported as `inkrelay`. It offers the same helpers as the TypeScript SDK: retries with backoff, automatic idempotency keys, pagination iterators, webhook verification and `create_and_wait`.
```bash theme={null}
pip install inkrelay
```
```python theme={null}
from inkrelay import InkRelay
ir = InkRelay() # reads INKRELAY_API_KEY
for printer in ir.printers.iterate():
print(printer["id"])
job = ir.print_jobs.create_and_wait(
{"printerId": printer_id, "title": "Invoice", "contentType": "pdf_uri", "content": url},
timeout_s=60,
)
print(job["state"])
```
Responses are plain dictionaries with the same camelCase keys as the API. `create_and_wait` creates the job and polls until it reaches `done`, `error`, `cancelled` or `expired`, then returns it. Check `job["state"]` and `job["stateMessage"]` rather than expecting an exception for a failed print.
## Configuration
```python theme={null}
ir = InkRelay(
api_key="ir_test_...", # defaults to the INKRELAY_API_KEY environment variable
base_url="https://api.inkrelay.app", # the default
max_retries=3, # the default
timeout=30.0, # seconds per request, the default
)
```
The constructor raises `ValueError` when neither `api_key` nor `INKRELAY_API_KEY` is set. The SDK retries `429` responses (waiting for `Retry-After` when present), and retries `5xx` responses and network errors with exponential backoff, but only for replay-safe requests: `GET`, `PATCH`, `DELETE`, and print job creation (which carries an idempotency key). Other `POST` requests are not retried on `5xx` errors. `print_jobs.create` and `create_and_wait` send an `Idempotency-Key` automatically and reuse it on every retry. Pass `idempotency_key="..."` to `create` to use your own key.
## Errors
A non-2xx response that survives the retries raises `InkRelayError` with `status`, `code` and `request_id`. `create_and_wait` raises `InkRelayTimeoutError` when the job is still not in a final state after `timeout_s` seconds (default 60); its `job` attribute holds the last known job.
```python theme={null}
from inkrelay import InkRelay, InkRelayError, InkRelayTimeoutError
ir = InkRelay()
try:
ir.print_jobs.create_and_wait({"printerId": printer_id, "title": "Invoice", "contentType": "pdf_uri", "content": url})
except InkRelayTimeoutError as err:
print("still not done", err.job["id"], err.job["state"])
except InkRelayError as err:
print(err.status, err.code, err.request_id)
```
See [Errors](/guides/errors) for the codes the API returns.
## Pagination
`list(**params)` returns one page as `{"data": [...], "hasMore": ..., "nextCursor": ...}`. `iterate(**params)` follows `nextCursor` for you and yields every item:
```python theme={null}
page = ir.print_jobs.list(limit=50, state="error")
print(len(page["data"]), page["hasMore"], page["nextCursor"])
for job in ir.print_jobs.iterate(state="error"):
print(job["id"], job["stateMessage"])
```
`computers`, `printers`, `print_jobs` and `webhooks` have `list`, `iterate` and `get`. `events` (and webhook deliveries) have `list` and `iterate` only, with no `get`. Pass `after` and `dir="asc"` to `events.list` to poll for new events.
## Webhooks
`inkrelay.webhooks.verify(body, headers, secret)` checks the signature of a request and raises `WebhookVerificationError` if it is invalid or older than 5 minutes. Pass the raw body, not parsed JSON.
```python theme={null}
import os
from inkrelay import webhooks, WebhookVerificationError
try:
webhooks.verify(raw_body, request_headers, os.environ["INKRELAY_WEBHOOK_SECRET"])
except WebhookVerificationError:
... # respond with 400
```
The client also manages endpoints: `ir.webhooks.create`, `update`, `delete`, `deliveries(id)` and `replay(delivery_id)`. `create` returns the signing secret once. See [Webhooks](/guides/webhooks) for a full receiver.
### Rotate a secret and send a test event
From version 0.3.0, `rotate_secret` replaces the signing secret and `ping` queues a test delivery:
```python theme={null}
endpoint = ir.webhooks.rotate_secret(webhook_id, overlap_seconds=3600)
print(endpoint["secret"]) # shown once
print(endpoint["previousSecretExpiresAt"])
delivery = ir.webhooks.ping(webhook_id)
print(delivery["eventType"]) # "ping"
```
`overlap_seconds` is how long the old secret keeps signing, from `0` to `604800`; leave it out for the default of 24 hours. See [Rotate the secret](/guides/webhooks#rotate-the-secret).
## Async client
From version 0.3.0, `AsyncInkRelay` is an async client with the same resources and options as `InkRelay`. Use it as an async context manager, `await` single calls and use `async for` over `iterate()`:
```python theme={null}
from inkrelay import AsyncInkRelay
async with AsyncInkRelay() as ir: # reads INKRELAY_API_KEY
job = await ir.print_jobs.create_and_wait(
{"printerId": printer_id, "title": "Invoice", "contentType": "pdf_uri", "content": url},
timeout_s=60,
)
async for printer in ir.printers.iterate():
print(printer["id"])
```
## Generated client
The convenience methods sit on top of a client generated from the OpenAPI spec. `ir.raw` is that client, already configured with your key, base URL and the retrying transport. Use it for an endpoint the wrapper has no helper for.
# TypeScript SDK
Source: https://docs.inkrelay.app/sdks/typescript
@inkrelay/sdk for Node 18 and later.
The TypeScript SDK wraps the REST API with typed methods, automatic retries on 429 and 5xx responses, automatic idempotency keys for print jobs, pagination iterators and webhook verification.
```bash theme={null}
npm install @inkrelay/sdk
```
```ts theme={null}
import { InkRelay } from "@inkrelay/sdk";
const ir = new InkRelay({ apiKey: process.env.INKRELAY_API_KEY });
for await (const printer of ir.printers.iterate()) console.log(printer.id);
const job = await ir.printJobs.createAndWait(
{ printerId, title: "Invoice", contentType: "pdf_uri", content: url },
{ timeoutMs: 60_000 },
);
console.log(job.state);
```
`createAndWait` creates the job and polls until it reaches `done`, `error`, `cancelled` or `expired`. It returns the final job, so check `job.state` and `job.stateMessage` rather than expecting an exception for a failed print.
## Configuration
```ts theme={null}
const ir = new InkRelay({
apiKey: "ir_test_...", // defaults to process.env.INKRELAY_API_KEY
baseUrl: "https://api.inkrelay.app", // the default
maxRetries: 3, // the default
});
```
| Option | Default | Meaning |
| - | - | - |
| `apiKey` | `INKRELAY_API_KEY` | Your `ir_test_` or `ir_live_` key. The constructor throws if neither is set |
| `baseUrl` | `https://api.inkrelay.app` | Override for a proxy or a local server |
| `maxRetries` | `3` | Retries after the first attempt |
The SDK retries `429` responses, waiting for `Retry-After` when it is present, and retries `5xx` responses and network errors with exponential backoff. `printJobs.create` and `createAndWait` send an `Idempotency-Key` automatically and reuse it on every retry, so a retry cannot print twice. Pass `{ idempotencyKey }` as the second argument to use your own key. Other `POST` requests are not retried on `5xx` errors.
## Errors
Any non-2xx response that survives the retries throws an `InkRelayError` with `status`, `code` and `requestId`. `createAndWait` throws an `InkRelayTimeoutError` when the job is still not in a final state after `timeoutMs` (default 60 seconds); its `job` property holds the last known job, which may still print.
```ts theme={null}
import { InkRelay, InkRelayError, InkRelayTimeoutError } from "@inkrelay/sdk";
try {
await ir.printJobs.createAndWait({ printerId, title: "Invoice", contentType: "pdf_uri", content: url });
} catch (err) {
if (err instanceof InkRelayTimeoutError) console.warn("still not done", err.job.id, err.job.state);
else if (err instanceof InkRelayError) console.error(err.status, err.code, err.requestId);
else throw err;
}
```
See [Errors](/guides/errors) for the codes the API returns.
## Pagination
`list(params)` returns one page, `{ data, hasMore, nextCursor }`. `iterate(params)` is an async generator that follows `nextCursor` for you and yields every item:
```ts theme={null}
const page = await ir.printJobs.list({ limit: 50, state: "error" });
console.log(page.data.length, page.hasMore, page.nextCursor);
for await (const job of ir.printJobs.iterate({ state: "error" })) console.log(job.id, job.stateMessage);
```
`computers`, `printers`, `printJobs` and `webhooks` have `list`, `iterate` and `get`. `events` (and webhook deliveries) have `list` and `iterate` only, with no `get`. Pass `after` and `dir: "asc"` to `events.list` to poll for new events.
## Webhooks
`ir.webhooks.verify(body, headers, secret)` checks the signature of a request and throws `WebhookVerificationError` if it is invalid or older than 5 minutes. Pass the raw body string, not parsed JSON.
```ts theme={null}
ir.webhooks.verify(rawBody, req.headers, process.env.INKRELAY_WEBHOOK_SECRET!);
```
The same object manages endpoints: `ir.webhooks.create`, `update`, `delete`, `deliveries(id)` and `replay(deliveryId)`. `create` returns the signing secret once. See [Webhooks](/guides/webhooks) for a full receiver.
### Rotate a secret and send a test event
From version 0.3.0, `rotateSecret` replaces the signing secret and `ping` queues a test delivery:
```ts theme={null}
const endpoint = await ir.webhooks.rotateSecret(webhookId, { overlapSeconds: 3600 });
console.log(endpoint.secret); // shown once
console.log(endpoint.previousSecretExpiresAt);
const delivery = await ir.webhooks.ping(webhookId);
console.log(delivery.eventType); // "ping"
```
`overlapSeconds` is how long the old secret keeps signing, from `0` to `604800`; leave the options out for the default of 24 hours. See [Rotate the secret](/guides/webhooks#rotate-the-secret).
## Raw generated client
The convenience methods sit on top of a client generated from the OpenAPI spec. It has one function per operation, named by its operationId, and a type for every schema:
```ts theme={null}
import { raw } from "@inkrelay/sdk";
```
Use it for an endpoint the wrapper has no helper for. The generated functions do not add retries or idempotency keys.