API reference
Upload templates, run batches, feed the Prepress and Imposition inboxes, and get told when work finishes. Everything the studio does, it does through this API.
Introduction
The base URL for every request is:
https://api.web2print.app/v1/
- HTTPS only. A plain HTTP request is refused with
403and typesecurity_error. - JSON in, JSON out. Send request bodies as a JSON object with
Content-Type: application/json. File uploads usemultipart/form-data, or the raw file as the body with?name=where noted. - One envelope. A success answers
{"data": ...}; a list addshas_moreandnext; an error answers{"error": {"message", "type"}}. Ping, Invoices and Transactions predate this envelope and are described in their own sections. - Ids are UUID v7 strings in the
idfield, for example019a0c3e-4b21-7d55-8f0e-6c1a2b3d4e5f. Every object also says what it is inobject. - Times are ISO 8601 in UTC, for example
2026-10-22T18:04:11Z. - Download links (
download_url,thumbnail_url) are presigned and work for 15 minutes. Fetch the object again for a fresh link. - CORS is open (
Access-Control-Allow-Origin: *) for bearer-token requests; cookies are never used. - OpenAPI. The whole API as an OpenAPI 3.1 document, for Postman, Swagger UI or a client generator: openapi.json.
Apps and your plan
A workspace's plan turns on some or all of the three apps: Templates, Prepress and Imposition. GET /v1/account tells you which. Calling an endpoint for an app your plan does not include answers 402 with type plan_limit and error.limit.kind module, for example "Your licenses do not include Imposition."
When a workspace's license ends (a renewal was never paid, or it was canceled), nothing is deleted. Its files stay readable: GET requests keep working for every app the ended license included. Anything that changes or runs something answers 402 plan_limit with error.limit.kind lapsed. Secret and public keys and OAuth tokens are paused (403) until a license is bought again, and then they work again unchanged.
- Templates and Batches need Templates.
- Impositions and Sheets need Imposition.
- Adding to the Queue, or sending a batch or queue item to an inbox, needs the app that owns that inbox (Prepress or Imposition).
Authentication
Send a secret API key as a bearer token on every request:
curl https://api.web2print.app/v1/ping \
-H "Authorization: Bearer $W2P_KEY"
The workspace owner creates keys in Settings, API keys. Secret keys look like sk_live_…. Keep them on your server; anyone holding one acts as you. A key acts as the person who created it, inside that person's workspace, so it sees exactly what they see.
Each key carries settings you choose when you create it:
| Setting | What it does |
|---|---|
| Scope | One entry per line, METHOD:/path. *:* allows everything. A path also covers everything below it, so GET:/v1/templates allows GET /v1/templates/{id}/versions but not /v1/templates-export. A request outside the scope answers 403. |
| Rate limit | Requests per minute for this key. See Rate limits. |
| Expiry | After this time the key answers 401 with "API key has expired." |
| IP allowlist | IPv4 CIDR blocks. When set, requests from any other address answer 403. |
Authentication failures answer 401 with type auth_error and a WWW-Authenticate: Bearer … header. Scope, IP and disabled-key refusals, and requests to a closed workspace, answer 403 with type security_error.
Errors
Every error has the same shape. Messages are written for the person using your product, so you can show them as they are.
{
"error": {
"message": "This template has no ready version yet.",
"type": "conflict"
}
}
| Status | Type | Meaning |
|---|---|---|
400 | invalid_request_error | Something in the request is missing or wrong. The message says what. |
401 | auth_error | No key, an unknown key, or an expired key. |
402 | plan_limit | A license limit: error.limit.kind is module (the app is not licensed), sessions, members, storage, api or lapsed (the license ended; the workspace is read-only). |
403 | security_error | Not HTTPS, outside the key's scope or IP allowlist, key disabled, API access not on the workspace's licenses (or paused because the license ended), or workspace closed. |
403 | permission_error | Only the owner and admins can manage the team. |
404 | not_found | No such object in your workspace. Objects in other workspaces are never revealed. |
404 | invalid_request_error | No endpoint matches the method and path. |
409 | conflict | The object is not in a state that allows this (still inspecting, already finished, held by a teammate). A few 409s, such as hitting the folder or sheet limit, use invalid_request_error. |
413 | limit_error | The upload is too large. Each file may be up to 256 MB; GET /v1/account/storage returns the exact cap as max_file_bytes. |
429 | rate_limit_error | Too many requests for this key. Wait for Retry-After seconds. |
429 | limit_error | Every editing seat on your plan is in use (Sessions). |
500 | api_error | Our fault. The message is generic; retry, with the same Idempotency-Key if you sent one. |
502 | api_error | An invite email could not be sent (Members). |
Pagination
Lists come newest first and use a cursor:
| Parameter | Type | Description |
|---|---|---|
limit | integer | 1 to 200. Default 50. |
after | string | The id of the last item you have. The next page starts after it. An id that matches nothing answers 400. |
{
"data": [ ... ],
"has_more": true,
"next": "019a0c41-0f7e-7a2b-9c3d-4e5f60718293"
}
Pass next as after to get the following page. When has_more is false, next is null.
Some small collections come back whole instead of paged: folders, sheets, members and the waiting queue. Those answer the same shape as a page, {"data": [...], "has_more": false, "next": null}, with any extra fields beside data. Batch rows page by row number, described under Batches.
Idempotency
Send an Idempotency-Key header on any POST, PATCH or DELETE so a retry or a double click never does the work, or bills, twice. Use any unique string up to 128 characters, such as your order number.
curl https://api.web2print.app/v1/batches \ -H "Authorization: Bearer $W2P_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: order-10482" \ -d '{"template": "019a0c3e-4b21-7d55-8f0e-6c1a2b3d4e5f", "rows": []}'
- Keys belong to your workspace. A request is identified by its method, path, raw body and the contents of any uploaded files.
- Same key, same request: you get the stored status and body again, with the header
Idempotent-Replayed: true. Nothing runs twice. - Same key, different request:
409conflict. - Same key while the first request is still running:
409conflict. Retry shortly. - A 5xx answer is not stored, so retrying with the same key runs the request again.
Waiting for the result
Every request that starts work (a tool, a preview, an upload's check, a batch) answers at once with the object and its job. When you would rather have the finished object in the same response, add wait: the number of milliseconds to hold, as a query parameter or a JSON body field, up to 30000. A batch is held until it is done, partial or failed (every row settled), so a small batch comes back with its rows' PDFs in one call.
curl https://api.web2print.app/v1/queue/019a0c3e-4b21-7d55-8f0e-6c1a2b3d4e5f/tool?wait=30000 \ -H "Authorization: Bearer $W2P_KEY" \ -H "Content-Type: application/json" \ -d '{"tool": "rotate", "params": {"degrees": 90}}'
- Finished in time:
200with the object in its new state (the queue item on its rotated file, the file with its page facts, the job with itsfiles), and the headerX-Web2Print-Wait: done. - Still running when the time ran out:
202with the object as it stands andX-Web2Print-Wait: pending. PollGET /v1/jobs/:idor take thejob.donewebhook. - The server is holding as many waits as it can:
503withRetry-After. Nothing was started; retry, or send the request withoutwait. - Set your HTTP client's timeout above the wait you ask for.
waiton aGETis ignored.
Rate limits
Each key has a limit in requests per minute: 60 unless you set another when creating the key. The limit works as a bucket that holds one minute's worth of requests and refills continuously, so short bursts up to the full limit are fine.
When the bucket is empty the API answers 429 with type rate_limit_error and a Retry-After header giving the seconds to wait. There are no other rate-limit headers.
HTTP/1.1 429 Too Many Requests
Retry-After: 2
{"error": {"message": "Rate limit exceeded. Retry after 2 second(s).", "type": "rate_limit_error"}}
Ping
Check that the API is reachable and your key works.
No parameters. time is the server time; workspace is the workspace the key acts in.
curl https://api.web2print.app/v1/ping \
-H "Authorization: Bearer $W2P_KEY"
{
"data": {
"object": "ping",
"time": "2026-10-24T18:04:11Z",
"workspace": { "id": "019a0a1f-2c3d-7e4f-8a5b-6c7d8e9f0a1b", "name": "Acme Transfers" }
}
}
Capabilities
What the render fleet behind your workspace can do right now. Every feature that needs a newer engine is switched on only when the whole fleet has it, and that is the switch this endpoint reports, so a client never has to know engine versions. Call it at start-up and after any 409 that names the fleet. Free, no job, open to every key.
engine.jar and engine.worker are the lowest versions among the workers serving the workspace in the last ten minutes (null with none online: every feature then reads false and renders queue until a worker appears). features.<name> is true when the request that needs it will be accepted. text_binding covers text listing and binding; stacked_steps says chained prepress steps run as one job. document describes the design document the compile accepts.
curl https://api.web2print.app/v1/capabilities -H "Authorization: Bearer $W2P_KEY"
{
"data": {
"object": "capabilities",
"engine": { "jar": "2.39.0", "worker": "0.3.0", "workers": 2 },
"features": {
"barcode_layers": true, "barcode_variables": true, "barcodes": true, "compare": true,
"cut_art": true, "cut_files": true, "cut_marks": true, "icc_profiles": true,
"linked_file_variables": true, "links_check": true, "multi_page_transforms": true,
"nest": true, "output_intent": true, "pdfx": true, "press_check_barcodes": true,
"retype": true, "roll_presses": true, "signature": true, "soft_proof": true,
"tile": true, "true_size_frames": true, "visibility_variables": true
},
"stacked_steps": true,
"text_binding": true,
"document": {
"schema": 1,
"layers": ["text", "shape", "artwork", "path", "barcode"],
"shapes": ["rect", "ellipse", "star", "polygon"],
"warps": ["none", "arc", "arc-upper", "arc-lower", "bulge", "flag", "mesh"],
"fits": ["contain", "shrink", "cover"],
"anchors": ["left", "center", "right"],
"outputs": ["fixed", "ink"],
"max_layers": 300, "max_colors": 64, "max_fonts": 32,
"artboard_in": { "min": 0.25, "max": 60 }
},
"checked_at": "2026-11-03T03:10:00+00:00"
}
}
Account
Who the key belongs to, what the plan includes, and how much has been used.
| GET | /v1/account | Workspace, plan, apps and open editing sessions. |
| GET | /v1/account/usage | Units used in a billing month. |
plan is null when the workspace has no plan. apps says which of the three apps you can call. lapsed is null, or {ended_on, plan, reason, modules} when the workspace's license ended (reason is expired or canceled): see Apps and your plan.
curl https://api.web2print.app/v1/account \
-H "Authorization: Bearer $W2P_KEY"
{
"data": {
"object": "account",
"workspace": { "id": "019a0a10-0001-7abc-8def-0123456789ab", "name": "Ortiz Print Co.", "slug": "ortiz-print" },
"plan": {
"id": "019a0a0f-77c1-7d20-9e31-4a5b6c7d8e9f", "name": "Studio", "tier": 2,
"included_units": 5000, "max_sessions": 5,
"max_storage_gb": 100, "max_team_members": 10
},
"apps": { "templates": true, "prepress": true, "imposition": true },
"sessions": { "open": 1, "max": 5 }
}
}
| Parameter | Type | Description |
|---|---|---|
period | string | Month as YYYY-MM. Default: the current month (UTC). |
Units are counted per engine run. The breakdown groups runs by lane and outcome (ok, customer_fault or service_fault). Service faults and timeouts are never billed.
curl "https://api.web2print.app/v1/account/usage?period=2026-10" \ -H "Authorization: Bearer $W2P_KEY"
{
"data": {
"object": "usage",
"period": "2026-10",
"units": 1243.5,
"included_units": 5000,
"overage_units": 0,
"breakdown": [
{ "lane": "preview", "outcome": "ok", "runs": 812, "units": 81.2 },
{ "lane": "production", "outcome": "ok", "runs": 140, "units": 1162.3 }
]
}
}
Members
The people in your workspace, and inviting more. Roles are owner, admin and member. Anyone can list; only the owner and admins can invite, resend, change roles or remove (others get 403 permission_error). Invited people count against the plan's team seats until removed.
| GET | /v1/members | Everyone, plus seats and whether you may manage. |
| POST | /v1/members | Invite someone by email. |
| POST | /v1/members/{id}/resend | Send a fresh invite. The old link stops working. 409 if they already joined. |
| PATCH | /v1/members/{id} | Change role: {"role": "admin" | "member"}. The owner's role and your own cannot change. |
| DELETE | /v1/members/{id} | Remove someone. Their sign-ins and keys stop working; their templates and batches stay. The owner and you yourself cannot be removed. |
| DELETE | /v1/members/{id}/2fa | Turn off someone's two-step sign-in when they have lost their phone. Their number and backup codes are deleted and their next sign-in asks for the password only. 409 for yourself (use Settings) or when it is already off; only the owner can do it for the owner. |
Each member carries two_step: true when they sign in with a texted code.
| Field | Type | Description |
|---|---|---|
name required | string | Up to 120 characters. |
email required | string | Where the invite goes. The link is good for 72 hours. |
role | string | admin or member. Default member. |
Answers 201 with the member, status invited. 402 when every seat is taken; 409 when the email already has an account; 502 when the email could not be sent (nothing is created).
curl https://api.web2print.app/v1/members \ -H "Authorization: Bearer $W2P_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "Dev Patel", "email": "dev@example.com", "role": "member"}'
{
"data": {
"id": "019a0a10-9c0d-7e1f-8a2b-3c4d5e6f7a8b",
"object": "member",
"name": "Dev Patel",
"email": "dev@example.com",
"role": "member",
"status": "invited",
"is_you": false,
"invited_at": "2026-10-22T18:10:42Z",
"joined_at": null,
"last_seen_at": null
}
}
GET /v1/members answers {"data": [member, ...], "has_more": false, "next": null, "seats": {"used": 3, "max": 10}, "can_manage": true}. seats.max is null when the plan sets no limit.
Sessions
Editing seats. Each open editor (a studio tab, the desktop client or an integration) holds one session; the plan's max_sessions caps how many are open at once. A session that misses heartbeats for 2 minutes expires and frees its seat. Pass a session id to template previews to tie them to it.
| GET | /v1/sessions | List. ?status= open, closed, killed or expired. Paginated. |
| GET | /v1/sessions/{id} | One session. |
| POST | /v1/sessions | Open a session. Body {"client": "spa" | "api" | "desktop"}, default api. 201, or 429 limit_error when every seat is in use. |
| POST | /v1/sessions/{id}/heartbeat | Keep it alive; send about every 30 seconds. 409 means it already ended: open a new one. |
| DELETE | /v1/sessions/{id} | Close it and free the seat now. Closing an ended session is harmless. |
curl https://api.web2print.app/v1/sessions \ -H "Authorization: Bearer $W2P_KEY" \ -H "Content-Type: application/json" \ -d '{"client": "api"}'
{
"data": {
"id": "019a0c3d-8b9c-7d0e-9f1a-2b3c4d5e6f70",
"object": "session",
"client": "api",
"status": "open",
"started_at": "2026-10-22T18:00:05Z",
"heartbeat_at": "2026-10-22T18:00:05Z",
"ended_at": null
}
}
Templates
A template is an Illustrator file (.ai, .pdf, or a package .zip with a Fonts/ folder) whose text frames become variables. Uploading creates version 1; uploading again adds a version. Every version is inspected in the background: its status goes from inspecting to ready (with variables, page size, fonts and a thumbnail) or failed (with a plain reason in error). Requires the Templates app.
| GET | /v1/templates | List templates. |
| POST | /v1/templates | Upload a new template. |
| POST | /v1/templates/sample | Add the bundled sample business card to your library. 201 with the template. |
| GET | /v1/templates/{id} | One template with its current version. |
| PATCH | /v1/templates/{id} | Rename, move to a folder, or set where its runs go. |
| DELETE | /v1/templates/{id} | Delete. Batches already produced keep their files. |
| GET | /v1/templates/{id}/versions | All versions, newest first. |
| GET | /v1/templates/{id}/versions/{n} | One version by number, with its variables. |
| POST | /v1/templates/{id}/versions | Upload a new version. |
| POST | /v1/templates/{id}/versions/{n}/restore | Make a ready version current again. |
| GET | /v1/templates/{id}/variables | Variables of the current version. |
| PATCH | /v1/templates/{id}/variables | Relabel one variable, or set its fit and anchor. |
| GET | /v1/templates/{id}/download | Your uploaded file and its PDF. Free. |
| GET | /v1/templates/{id}/text | The live text of an Illustrator file, story by story. Free. |
| POST | /v1/templates/{id}/text/bind | Make variables of chosen stories: a new version. Free. |
| POST | /v1/templates/{id}/previews | Render a preview PNG with your values. |
Paginated. Each template carries its current version, so a grid renders without more calls.
| Parameter | Type | Description |
|---|---|---|
q | string | Name contains this text. |
folder | string | A folder id, or root for templates in no folder. |
deep | 1 | With folder, include every folder below it. |
limit, after | See Pagination. |
Send multipart/form-data, or the raw file as the body with ?name=File.ai. Answers 201 with the template and its version in inspecting; poll GET /v1/templates/{id} or listen for the template.ready webhook. An .ai saved without PDF compatibility answers 400 asking you to re-save it with "Create PDF Compatible File".
| Field | Type | Description |
|---|---|---|
file required | file | One .ai, .pdf or .zip. |
name | string | Template name. Default: the file name. |
folder | string | Folder id to put it in (form field or query string). |
curl https://api.web2print.app/v1/templates \ -H "Authorization: Bearer $W2P_KEY" \ -H "Idempotency-Key: upload-card-v1" \ -F "file=@business-card.ai" \ -F "name=Business card"
A ready template
{
"data": {
"id": "019a0c3e-4b21-7d55-8f0e-6c1a2b3d4e5f",
"object": "template",
"name": "Business card",
"folder": null,
"default_destination": "download",
"current_version": {
"id": "019a0c3e-4b9a-7c10-a3d2-1e2f3a4b5c6d",
"object": "template_version",
"version": 1,
"status": "ready",
"error": null,
"page": { "pages": 1, "width_in": 3.5, "height_in": 2, "spot_colors": ["PANTONE 3005 C"] },
"fonts": {
"used": [
{ "name": "LiberationSans-Bold", "status": "embedded" }
],
"missing": []
},
"links": { "total": 1, "missing": [], "files": [{ "name": "logo.pdf", "found": true, "variable": "Logo" }] },
"thumbnail_url": "https://…",
"created_at": "2026-10-22T17:58:30Z"
},
"created_at": "2026-10-22T17:58:30Z",
"updated_at": "2026-10-22T17:58:41Z"
}
}
Each entry in fonts.used has a status of embedded, library (filled from your font library) or missing. fonts.missing lists the missing names; upload them to Fonts.
links is the placed-file check of an Illustrator master: every file Illustrator placed (linked) in the artwork, whether the uploaded package's Links/ folder holds it, and the variable bound to it, if any. The page prints without the files; swapping them per row and reopening the master elsewhere need them, so links.missing names what to add (in Illustrator, File > Package, then upload the folder as a zip). It is null for lab packages, plain PDFs and versions uploaded before the check existed (render engine 2.37.0).
When every placed file is there, POST /v1/templates/{id}/package (?version= for an older version) builds the relinked package: your upload with every placed-file record pointed at its Links/ folder, the way Illustrator's own File > Package writes it, so the master reopens from the package on any machine. It answers 200 with {object: "template_package", version, file} when the package is ready (built once, kept for a day, free) or 202 with the build job to poll; GET …/download also lists it as package once built. 409 when the version was not checked, places no files, or is missing some.
Only the fields you send change.
| Field | Type | Description |
|---|---|---|
name | string | 1 to 191 characters. |
folder | string | null | Folder id, or null for the top level. |
default_destination | string | download, prepress or imposition: where batches of this template go unless the batch says otherwise. |
Same body as uploading a template. New content answers 201 with a template_version in inspecting; identical content answers 200 with the existing version. The template's current version moves only when inspection succeeds, so batches never start on a broken file.
Makes version n current; newer versions stay in the history. Answers the template. A version that is inspecting or failed answers 409. Batches already run keep the version they ran on.
Links to the file you uploaded, exactly as uploaded (original: .ai, .pdf or package .zip), and the print PDF taken from it (pdf). Nothing is rendered and nothing counts against your plan. Add ?version=n for an older version; any version works, including one that failed inspection. Each download_url lasts 15 minutes.
{
"data": {
"object": "template_download",
"version": 1,
"original": { "object": "file", "name": "Business card.ai", "download_url": "https://...", ... },
"pdf": { "object": "file", "name": "Business card.pdf", "download_url": "https://...", ... }
}
}
The fields to fill in, in template order. 409 while the template has no ready version.
{
"data": [
{
"name": "City",
"label": "City",
"type": "text",
"default": "Austin",
"frames": 1,
"usable": true,
"fit": null,
"anchor": null
}
]
}
type is text, image or visibility. A visibility variable is a show/hide switch an Illustrator template bound to an object (its Variables panel, Visibility): its value is true (the default: the object stays) or false (the object is removed from that file; yes/no and show/hide are accepted and stored as these). It has no frame, so fit, anchor and represents are refused on it. The engine can only hide, so save the template with every bound object visible. Hiding needs the render fleet on engine 2.37.2 or later; before that a false answers 409 and nothing is queued. An image variable also has box ({w, h}, its frame in inches) and represents ({w, h} inches or null: the physical area the frame stands for, such as a mockup's 13 × 9 in chest), and its default keeps the template's own art. When a frame stands for an area, each image is placed at its real size inside it (a 6 × 6 in picture takes 6 of the 13 inches) instead of being scaled to the frame; the image's size is its pixels over the resolution it declares (300 ppi when it declares none) unless the row says otherwise with <name>_size. An image larger than the area is scaled down to fit it. fit is contain, shrink, cover or null (contain); anchor is left, center, right or null (center). frames counts the frames the variable fills. Each variable also carries a placeholder. An Illustrator template whose Variables panel bound a placed file (a Linked File variable) comes back as an image variable with linked ({file, carried}: the placed file's name and whether the uploaded package carried it in its Links/ folder; null on every other variable). Its default is that file, so a row that leaves it out keeps the template's own art; a row's image takes its place exactly where it sat (same stacking, clipping and effects, so art drawn over it stays over it), an empty string hides it, and represents sizes it as above. Linked-file swaps need the render fleet on engine 2.33.0 or later; before that the request answers 409 and nothing is queued.
Change one variable of the current version: send its name and any of label (1 to 80 characters), fit, anchor and, on an image variable, represents ({w, h} or "13 x 9", inches; values as above; null restores the default). cover is refused on text variables. A fit or anchor set here applies to every frame the variable fills. The next preview and batch use it; batches already built keep their output. Returns the variable.
{ "name": "Logo", "fit": "cover", "anchor": "left" }
Renders a PNG of the template with your values. Values you leave out keep their default; an empty string hides that frame, exactly as production would. The PNG has a transparent background.
| Field | Type | Description |
|---|---|---|
values | object | Variable name to text, up to 2,000 characters each. For an image variable, the id (or exact name) of an image uploaded with POST /v1/files; when its frame stands for an area, <name>_size ("6 x 6", inches) beside it states the image's real size. |
ppi | integer | 36, 72, 96, 144, 192 or 300. Default 96. |
version | integer | Preview a specific ready version. Default: the current version. |
session | string | An open session id. An ended session answers 409. |
wait | boolean | Default true: wait up to about 3 seconds for the render. False answers 202 at once. |
When the render finishes in time the answer is 200 with the finished job and the PNG at files[0].download_url. Otherwise it is 202 with the pending job; poll GET /v1/jobs/{id}.
curl https://api.web2print.app/v1/templates/019a0c3e-4b21-7d55-8f0e-6c1a2b3d4e5f/previews \ -H "Authorization: Bearer $W2P_KEY" \ -H "Content-Type: application/json" \ -d '{"values": {"City": "Denver"}, "ppi": 144}'
{
"data": {
"id": "019a0c3f-7e1d-7b22-8a9b-0c1d2e3f4a5b",
"lane": "preview",
"type": "master_preview",
"status": "done",
"attempts": 1,
"exit_code": 0,
"error": null,
"created_at": "2026-10-22T18:05:02+00:00",
"finished_at": "2026-10-22T18:05:03+00:00",
"object": "job",
"files": [
{
"id": "019a0c3f-8012-7a33-9b44-c55d66e77f88",
"object": "file",
"kind": "preview",
"name": "preview.png",
"mime": "image/png",
"bytes": 48211,
"sha256": "9f2c…",
"download_url": "https://…",
"page": null,
"created_at": "2026-10-22T18:05:03Z"
}
]
}
}
The preview PNG is final the moment the job's status is done: its file carries no page block (that is written only for uploads), so do not wait on page.status for a preview.
Lists the live text of an Illustrator file (its stories) so you can make variables of text the designer never bound in the Variables panel. The first call starts the listing and answers status pending; a call a moment later answers done with stories. The listing is made once per version and kept. An empty list means the file has no live text (outlined, or not an Illustrator save). Free: no units are used.
| Parameter | Type | Description |
|---|---|---|
version | integer | Which version to read. Default: the current version. |
Each story carries id (story-N, what you bind by), text, font, size (points), bbox or anchor (points from the page's bottom-left, whichever the file records), align and bound_to (the variable it already feeds, or null). Status failed carries the engine's reason in error. 409 when the render fleet behind your workspace cannot list text yet (see capabilities, text_binding).
curl https://api.web2print.app/v1/templates/019a0c3e-5b1a-7c2d-8e3f-4a5b6c7d8e9f/text \
-H "Authorization: Bearer $W2P_KEY"
{
"data": {
"object": "template_text",
"template": "019a0c3e-5b1a-7c2d-8e3f-4a5b6c7d8e9f",
"version": 1,
"status": "done",
"error": null,
"stories": [
{ "id": "story-0", "text": "BROOKLYN", "font": "Helvetica", "size": 73, "bbox": null, "anchor": [274.15, 867.57], "align": "left", "bound_to": null },
{ "id": "story-1", "text": "WOLVES", "font": "Helvetica", "size": 73, "bbox": null, "anchor": [314.52, 254.70], "align": "left", "bound_to": null }
],
"job": { "id": "019a0c3f-7a11-7b22-8c33-9d44ae55bf66", "status": "done" }
}
}
Binds stories to variables, as the designer's Variables panel would, and files the result as the template's next version. Only the file's Illustrator data changes (it gains the variable library and the bindings); the page content is untouched and the file still opens in Illustrator. The new version inspects like an upload, and its variables then list each binding as a text variable whose default is the story's text. Free: no units are used.
| Parameter | Type | Description |
|---|---|---|
bindings | array | Required. [{id, key}]: a story id from GET …/text and the variable name it feeds (letters, digits and underscores, starting with a letter). The same key on several stories makes one variable that fills several frames. At most 200. |
version | integer | Which version to bind. Default: the current version. |
wait | integer | Hold the answer up to this many milliseconds (at most 30000) until the bind finished. See Waiting for the result. |
Answers 202 with a template_text_bind in status pending; with wait, 200 once finished, with new_version (a template version, then inspecting) or status failed and the engine's reason: a story already bound, an id not in the file. Without wait, poll GET /v1/jobs/{job.id} and then GET /v1/templates/{id}/versions. 400 for a bad list, a multi-page file, a lab package or a template made in the designer (their variables are set there). 409 when the fleet cannot bind text yet.
curl -X POST https://api.web2print.app/v1/templates/019a0c3e-5b1a-7c2d-8e3f-4a5b6c7d8e9f/text/bind \ -H "Authorization: Bearer $W2P_KEY" -H "Content-Type: application/json" \ -d '{"bindings": [{"id": "story-0", "key": "Team"}, {"id": "story-1", "key": "Mascot"}], "wait": 15000}'
{
"data": {
"object": "template_text_bind",
"id": "019a0c3f-8b12-7c23-9d34-ae45bf56c067",
"template": "019a0c3e-5b1a-7c2d-8e3f-4a5b6c7d8e9f",
"version": 1,
"bindings": [ { "id": "story-0", "key": "Team" }, { "id": "story-1", "key": "Mascot" } ],
"status": "done",
"error": null,
"new_version": { "id": "019a0c3f-8c13-7d24-8e35-bf46c057d178", "object": "template_version", "version": 2, "status": "inspecting" },
"job": { "id": "019a0c3f-8b12-7c23-9d34-ae45bf56c067", "status": "done" }
}
}
A render of the new version replaces each bound story with the row's value in the story's own font. The engine matches the master's text by its rendered ink, so the font packaged with the file must be the face the file was set in; a substituted face (a converted TTF where the original was an OpenType font) makes the render fail with a message naming the variable, and the fix is to upload the file again with the right font.
Designs
A design is a canvas document the studio's designer edits: text, shapes, paths, barcodes and placed artwork on an artboard, with named plate colors and your workspace's fonts. Saving a design compiles it with the engine and files the result as a template (the first save makes the template, later saves add versions), so a design built by another product renders, batches and bills exactly like an uploaded file. Designing and saving are free. Requires the Templates app.
| GET | /v1/designs | List designs (?template=, ?status=draft|saved). |
| POST | /v1/designs | New design: {name, artboard}, a whole {document}, {from}, {from_template} or {edit_template}. |
| GET | /v1/designs/{id} | One design with its document. |
| PATCH | /v1/designs/{id} | Autosave: {name?, document?}. |
| POST | /v1/designs/{id}/save | Save as template: compile. 202 with the design and the job. |
| DELETE | /v1/designs/{id} | Delete the design. Its template stays. |
| GET | /v1/designs/assets | Fonts and colors a document may reference. |
| POST | /v1/designs/barcodes | Draw one barcode for the canvas (preview only). |
The document
Schema 1. Geometry is in inches from the artboard's top-left; layers are listed bottom first. Limits: 300 layers, 64 colors, 32 fonts, artboard 0.25 to 60 inches a side. Capabilities publishes the schema number, the layer kinds and these limits.
| Field | Type | Description |
|---|---|---|
schema_version | integer | 1. |
artboard | object | {width_in, height_in}. |
output | string | fixed (default: every render keeps the artboard; cards, labels, flyers) or ink (each render's page shrinks to its ink; transfers). |
colors | array | [{ref, name, c, m, y, k, hex?}]. name is the literal plate name the render prints; layers refer to a color by ref. |
fonts | array | [{ref, id, label?}]. id is a font id from your fonts (GET /v1/designs/assets lists what may be used); text layers refer to a font by ref. |
distress | object | Optional {pattern}: a worn-ink mask over the whole design. |
layers | array | Each has id, a kind, its geometry and may carry name, hidden (left out of the compile), rotation, flip_h, flip_v, shear and variable. |
| Layer | Fields |
|---|---|
text | text, font, size_pt, x, y (top-left of the ink box), fill?, stroke?, stroke_width_pt?, tracking?, warp? {style none|arc|arc-upper|arc-lower|bulge|flag, bend -100..100} or {style mesh, bottom [[x,y],…], top [[x,y],…]}. |
shape | shape rect|ellipse|star|polygon, x, y, w, h, fill?, stroke?, stroke_width_pt?, points?, inner_ratio?, sides?. |
artwork | file (a PDF file id), x, y, w, h, recolor? {spot name: color ref}. |
path | cmds [[op, …coords]] (M/L/Q/C with coordinates 0..1 of the box, Z closes), closed?, x, y, w, h, fill?, stroke?, stroke_width_pt?, cap?, join?. |
barcode | barcode {symbology, data, x_dim_mm?, height_mm?, bwr_mm?, hri?, ecc?}, x, y, w, h, fill. |
variable is {key, fit?, anchor?}: the variable the layer fills per row. A text layer's placeholder is its own text, so every bound text layer's text must be unique among the visible text layers. An artwork layer bound to a variable is an image variable (fit contain|shrink|cover, anchor left|center|right); a barcode layer bound to a variable takes the row's value as its data. One variable may feed text and barcode layers together, never artwork with either.
Making a template from another product
Post the whole document, then save. The save is a compile (about a second warm): the answer is 202 with the job and the design in status saving; with wait it is 200 once compiled. The design's status becomes saved (with template.id, which then inspects like any upload and is ready a few seconds later with its variables) or failed (with error naming the layer or font to fix).
curl -X POST https://api.web2print.app/v1/designs -H "Authorization: Bearer $W2P_KEY" -H "Content-Type: application/json" -d '{ "name": "Team name transfer", "document": { "schema_version": 1, "artboard": {"width_in": 10, "height_in": 3}, "output": "ink", "colors": [{"ref": "c1", "name": "PANTONE 186 C", "c": 0, "m": 100, "y": 81, "k": 4}], "fonts": [{"ref": "f1", "id": "019a0b2d-1111-7aaa-8bbb-cccddd000111"}], "layers": [ {"id": "logo", "type": "artwork", "file": "019a0b2d-2222-7aaa-8bbb-cccddd000222", "x": 0.25, "y": 0.25, "w": 2.5, "h": 2.5, "variable": {"key": "Logo", "fit": "contain", "anchor": "center"}}, {"id": "team", "type": "text", "text": "WOLVES", "font": "f1", "size_pt": 96, "fill": "c1", "x": 3, "y": 0.6, "variable": {"key": "Team"}} ] } }'
curl -X POST https://api.web2print.app/v1/designs/019a0c40-1a2b-7c3d-8e4f-5a6b7c8d9e0f/save?wait=15000 -H "Authorization: Bearer $W2P_KEY"
{
"data": {
"design": { "id": "019a0c40-1a2b-7c3d-8e4f-5a6b7c8d9e0f", "object": "design", "status": "saved",
"template": { "id": "019a0c40-2b3c-7d4e-9f50-6a7b8c9d0e1f", "version": 1 } },
"job": { "id": "019a0c40-3c4d-7e5f-8061-7b8c9d0e1f20", "status": "done" }
}
}
Rows then fill Team with text and Logo with an image (batches and previews work the same as for an uploaded file). Keep the design to add a version later: PATCH the document and save again.
Folders
Folders organize the template library. They nest up to 32 deep; a template sits in one folder or at the top level (move it with PATCH /v1/templates/{id}).
| GET | /v1/folders | Every folder at once, flat, with parent ids and template counts. |
| POST | /v1/folders | Create a folder. 201. |
| PATCH | /v1/folders/{id} | Rename, reorder or move. Moving a folder inside itself answers 400. |
| DELETE | /v1/folders/{id} | Delete. Its templates and subfolders move up to its parent; the answer lists them in moved_templates and moved_folders. |
| Field | Type | Description |
|---|---|---|
name required to create | string | Up to 120 characters. |
parent | string | null | Parent folder id, or null for the top level. |
sort | integer | Position among siblings. Default 0. |
curl https://api.web2print.app/v1/folders \ -H "Authorization: Bearer $W2P_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "Real estate"}'
{
"data": {
"id": "019a0b77-1c2d-7e3f-9a0b-1c2d3e4f5a6b",
"object": "folder",
"name": "Real estate",
"parent": null,
"sort": 0,
"templates": 0,
"created_at": "2026-10-22T16:40:12Z",
"updated_at": "2026-10-22T16:40:12Z"
}
}
Batches
A batch is a production run of a template: a list of rows, each one finished piece with an output name and values for the template's variables. Every row renders to a working PDF and a production PDF (with production-only layers). Status goes building, queued, running, then done, partial, failed or canceled. Requires the Templates app.
| GET | /v1/batches | List. ?template= id, ?status= any status above. Paginated. |
| POST | /v1/batches | Create and start a batch. |
| GET | /v1/batches/{id} | One batch with live counts. |
| GET | /v1/batches/{id}/rows | Rows with status and PDF links. |
| POST | /v1/batches/{id}/cancel | Cancel unfinished rows; they are never billed. Finished rows keep their PDFs. 409 if the batch already finished. |
| POST | /v1/batches/{id}/archive | Get the whole batch as one zip. |
Always send an Idempotency-Key so a retry never runs, or bills, twice. Answers 201 with the batch in building; poll it or listen for batch.done.
| Field | Type | Description |
|---|---|---|
template required | string | Template id. Its current version must be ready (409 otherwise). |
rows required | array | Up to 10,000 rows. May be left out when count and serial are given. |
rows[].values | object | Variable name to value. Missing values take the default; an empty string hides that frame. For an image whose frame stands for an area, <name>_size ("6 x 6", inches) states the image's real size. |
rows[].output_name | string | File name for the row, unique in the batch. Default R00001, R00002, … |
rows[].quantity | integer | Overrides queue.quantity for this row's inbox item. |
name | string | Default: the template name and the date. |
version | integer | Pin an older version number. |
destination | string | download, prepress or imposition. Default: the template's default_destination. Prepress and imposition put each finished row into that app's inbox. |
queue | object | Facts for the inbox items: quantity, due_on (YYYY-MM-DD), customer, rush, notes. |
serial | object or array | Number a text field through the rows: {"field": "Ticket", "start": 1, "step": 1, "digits": 5, "prefix": "A-", "suffix": ""}. Row i gets prefix + zero-padded (start + i × step) + suffix, replacing that field's sheet value. A list numbers up to four different fields; step may be negative. 400 names what is wrong. |
count | integer | With serial and no rows: make this many numbered files (1 to 10,000) from the template's defaults. |
curl https://api.web2print.app/v1/batches \ -H "Authorization: Bearer $W2P_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: order-10482" \ -d '{ "template": "019a0c3e-4b21-7d55-8f0e-6c1a2b3d4e5f", "name": "Order 10482", "rows": [ { "output_name": "ortiz-cards", "values": { "Name": "Maria Ortiz", "Title": "Owner" } }, { "output_name": "patel-cards", "values": { "Name": "Dev Patel", "Title": "Press lead" } } ] }'
{
"data": {
"id": "019a0c41-0f7e-7a2b-9c3d-4e5f60718293",
"name": "Order 10482",
"status": "building",
"datasets": 2,
"done": 0,
"failed": 0,
"usage": null,
"created_at": "2026-10-22 18:20:03",
"finished_at": null,
"object": "batch",
"template": "019a0c3e-4b21-7d55-8f0e-6c1a2b3d4e5f",
"template_version": "019a0c3e-4b9a-7c10-a3d2-1e2f3a4b5c6d",
"destination": "download",
"archive": null
}
}
datasets is the row count; done and failed count finished rows. Batch times are UTC in YYYY-MM-DD HH:MM:SS form.
Sending a spreadsheet
Instead of JSON rows, send the sheet itself: multipart/form-data with the file as rows (CSV, tab, ; or | separated, header row first, up to 10 MB) and the other fields as form fields (queue as JSON text). Columns map the way the studio's batch runner maps a dropped file: by field name or label, ignoring case, spaces and punctuation. A File name column gives each row's output_name; a Qty column gives its quantity when the rows go to an inbox. Image fields take upload file names. Fields with no column take the template's default.
curl https://api.web2print.app/v1/batches \ -H "Authorization: Bearer $W2P_KEY" \ -H "Idempotency-Key: order-10483" \ -F template=019a0c3e-4b21-7d55-8f0e-6c1a2b3d4e5f \ -F rows=@order-10483.csv
The batch then also carries csv: rows read, columns ([{header, field}], where field is the variable, output_name, quantity, or null for a column that was ignored) and defaulted (fields no column filled). A sheet with no matching header answers 400 naming the columns to use.
| Parameter | Type | Description |
|---|---|---|
status | string | pending, running, done or failed. |
limit | integer | 1 to 200. Default 50. |
after | integer | Row number to start after. Rows come in row order and next is a row number. |
Each row: {row, output_name, status, values, error, inks, pdf, production_pdf}. inks is the list of separations the row's PDFs print (names as the template declares them), null until the row is done. production_pdf (all layers, artboard fit to the visible ink) is set on every done row for every destination; pdf is the working file with PRODUCTION_ONLY layers hidden.
curl "https://api.web2print.app/v1/batches/019a0c41-0f7e-7a2b-9c3d-4e5f60718293/rows?limit=1" \ -H "Authorization: Bearer $W2P_KEY"
{
"data": [
{
"row": 1,
"output_name": "ortiz-cards",
"status": "done",
"values": { "Name": "Maria Ortiz", "Title": "Owner" },
"error": null,
"pdf": { "id": "019a0c41-2a3b-7c4d-8e5f-6a7b8c9d0e1f", "object": "file", "kind": "derivative", ... },
"production_pdf": { "id": "019a0c41-2a3c-7d11-b2c3-d4e5f6a7b8c9", "object": "file", "kind": "production", ... }
}
],
"has_more": true,
"next": "1"
}
If the zip already exists the answer is 200 with the batch, and archive.download_url is ready to follow. Otherwise the zip is queued and the answer is 202 with archive still null; poll the batch until it appears. The zip holds one PDF per done row (named by output name), production PDFs under production/, and a manifest.csv with every row's status and error. A batch that is still running, or has no done rows, answers 409.
Files
Everything you uploaded or the engine produced. Upload images here for image variables, and PDF printer's marks for sheets.
| GET | /v1/files | List. Paginated. |
| GET | /v1/files/{id} | One file with a fresh download link. |
| POST | /v1/files | Upload an image or mark. |
| POST | /v1/files/{id}/preflight | The engine's full preflight report of a PDF, as written. Free. |
| GET | /v1/files/{id}/preflight | The same report, for polling. |
| POST | /v1/files/{id}/objects | Lift a vector PDF's own shapes as editable objects (exact geometry, nothing traced). Free. |
| GET | /v1/files/{id}/objects | The lift, for polling (same ?with_text=&object_gap=&vectorize=). |
| Parameter | Type | Description |
|---|---|---|
kind | string | upload, template, derivative (working PDFs), production, prepress, sheet or preview. |
batch | string | Files of one batch. With kind=production, a batch's print-ready PDFs. |
template | string | Files of any version of one template. |
name | string | Exact file name. |
ink | string | Files whose pages use every listed ink (comma-separated, names as the file reports them, for example PANTONE 1505 C). inks_exact matches files using exactly those inks and inks_within files using no ink outside them; _W backers are ignored. The newest 500 read files are inspected, so a filtered list has no cursor and has_more says when older files were not looked at. |
curl "https://api.web2print.app/v1/files?batch=019a0c41-0f7e-7a2b-9c3d-4e5f60718293&kind=production" \ -H "Authorization: Bearer $W2P_KEY"
{
"data": [
{
"id": "019a0c41-2a3c-7d11-b2c3-d4e5f6a7b8c9",
"object": "file",
"kind": "production",
"name": "ortiz-cards.pdf",
"mime": "application/pdf",
"bytes": 182044,
"sha256": "5be1…",
"download_url": "https://…",
"page": null,
"created_at": "2026-10-22T18:20:19Z"
}
],
"has_more": false,
"next": null
}
Uploaded PNG, JPEG and TIFF images are measured on upload: page is {status: "ready", kind: "image", width_px, height_px, dpi, dpi_declared, w_in, h_in, thumbnail_url} (w_in and h_in from the resolution the file declares, 300 ppi when it declares none; an image GD cannot read has no page). page is set on PDFs that have been measured: {status, pages, w_in, h_in, trim: {w, h}, inks, process, thumbnail_url}, where status is pending, ready or failed. It is null on other files and on PDFs never measured.
Send multipart field file, or the raw body with ?name=logo.png. The format is read from the bytes: PNG, JPEG, TIFF or PDF, up to 50 MB. A new file answers 201; the same bytes under the same name again answer 200 with the existing file.
| Field | Type | Description |
|---|---|---|
file required | file | One file. |
name | string | Default: the uploaded file name. |
kind | string | upload (default) or mark. Marks must be PDF; they are measured in the background so the sheet editor can size them. |
curl https://api.web2print.app/v1/files \ -H "Authorization: Bearer $W2P_KEY" \ -F "file=@acme-logo.png"
Use the returned id (or the exact file name) as the value of an image variable in previews and batch rows.
Everything downstream production reads about a PDF, exactly as the engine writes it: document name, size and Illustrator variables; per page the boxes, which colour families appear, process and spot colours (with CMYK equivalents and coverage), fonts and their embedding, raster images with their effective DPI, and whether live text, vector art or pictures exist. Every PDF is preflighted when it arrives, so the usual answer is immediate and done; add wait for a file uploaded a moment ago. Free.
| Field | Type | Description |
|---|---|---|
thin_line | object | Optional {positive_pt, negative_pt}: also measure strokes thinner than positive_pt points and gaps narrower than negative_pt (either defaults to the other; 0.01 to 72). Runs once per file and limits, reused after that. |
Returns {object: "preflight", file, status, error, thin_line, report, thin_line_report, overlays}. status is none, pending, done or failed; report is the engine's preflight JSON (pageCount, variables, pages[]); thin_line_report its thin-line JSON for the limits (pages[] with positiveSpecPt, negativeSpecPt, the narrowest widths found, positiveStats, negativeStats and per-plate entries), with one violation overlay PNG per page in overlays. GET with ?positive_pt=&negative_pt= reads the same view.
Divides a vector PDF into editable objects: its own filled paths, split by ink (and optionally by distance), with per-ink path data in page inches in the report at report_url. Free; a lift with the same options is reused.
| Field | Type | Description |
|---|---|---|
with_text | boolean | Tolerate live text and return it as texts[] (runs: content, font, size_pt, anchor, fill) and, for Illustrator saves, stories[]. Without it a page with live text fails "not liftable". |
object_gap | number | Inches. Same-colour shapes closer than this form one object; by default every ink is one object. |
vectorize | boolean | object | For a page the exact lift refuses (raster images, gradients): render the page and trace it instead. true uses the default trace; an object takes the vectorize settings (mode color | bw | grays, colors, inks, paths, corners, noise, smooth, ignore_white). The report has the same shape (traced paths per ink) and vector_url is the traced PDF. A traced lift may take minutes on a large page; the job's ceiling is an hour. |
Returns {object: "art_objects", file, options, status, error, page, spots, objects, texts, stories, report_url, vector_url}; status is none, pending, done or failed.
Fonts
Your workspace's font library. Fonts arrive from uploads here and from the Fonts/ folder of uploaded template packages. The engine renders only with fonts from this library, never with fonts installed on a machine. A disabled font stays stored but is no longer used to fill missing fonts.
| GET | /v1/fonts | List. ?q= searches PostScript name and family; ?status= enabled or disabled. Paginated. |
| GET | /v1/fonts/{id} | One font. |
| POST | /v1/fonts | Upload up to 50 fonts. |
| POST | /v1/fonts/{id}/disable | Stop using a font. |
| POST | /v1/fonts/{id}/enable | Use it again. |
Send multipart field file (or file[] for several): .ttf, .otf, .ttc, .otc, .pfb or .dfont. Each file answers with its stored font or its own error. Uploading a font already in the library returns it, enabled. The status is 201 when at least one font was stored and 400 when none was; either way data lists every file.
curl https://api.web2print.app/v1/fonts \ -H "Authorization: Bearer $W2P_KEY" \ -F "file[]=@Annual-Bold.otf" \ -F "file[]=@readme.txt"
{
"data": [
{
"file": "Annual-Bold.otf",
"font": {
"id": "019a0b80-3d4e-7f5a-8b6c-7d8e9f0a1b2c",
"object": "font",
"postscript_name": "Annual-Bold",
"family": "Annual",
"style": "Bold",
"format": "otf",
"bytes": 64120,
"source": "upload",
"enabled": true,
"created_at": "2026-10-22T15:12:40Z"
}
},
{ "file": "readme.txt", "error": "Not a font file (ttf, otf, ttc, otc, pfb, dfont)." }
]
}
source is upload or package.
Jobs
Individual engine runs: inspections, previews, batch work, prepress and imposition operations. Status goes pending, running, then done, failed or canceled. A failed job carries a plain-language error; a job fetched by id lists its output files.
| GET | /v1/jobs | List, without files. ?status= any status above; ?lane= preview, production, prepress, imposition or inspect. Paginated. |
| GET | /v1/jobs/{id} | One job with its output files. |
| POST | /v1/jobs/{id}/cancel | A pending job ends now; a running job stops within a few seconds and is not billed. A finished job answers 409. |
curl https://api.web2print.app/v1/jobs/019a0c3f-7e1d-7b22-8a9b-0c1d2e3f4a5b \
-H "Authorization: Bearer $W2P_KEY"
The answer is a job like the one under previews. Job times carry a +00:00 offset rather than Z.
Queue
The Prepress and Imposition inboxes: PDFs waiting for work, with the job facts that travel with them (quantity, due date, customer, rush, notes). Send-to moves an item between inboxes. A claim is a short lease that shows teammates who is working on an item.
| GET | /v1/queue | List an inbox. |
| POST | /v1/queue | Add PDFs to an inbox. |
| GET | /v1/queue/{id} | One item with its history. |
| PATCH | /v1/queue/{id} | Edit facts or name, or send to the other inbox with {"stage": …}. |
| DELETE | /v1/queue/{id} | Remove from the inbox; the file stays in Files. An item on an imposition answers 409. |
| POST | /v1/queue/move | Send several items: {"items": [id, …], "stage": "prepress"}. Up to 500. |
| POST | /v1/queue/done | Mark Prepress items done: {"items": [id, …]}. The file is finished here and leaves the flow (to your own RIP or another tool); the item moves to status=done and inbox.item.done fires. Items already done are left as they are, so a retry is safe. An Imposition item, an image, or a file with a Prepress change still running answers 409 (images 400). Requires Prepress. |
| POST | /v1/queue/reopen | Put done Prepress items back in the inbox: {"items": [id, …]}. Fires inbox.item.reopened. Anything not done answers 409. |
| POST | /v1/queue/{id}/claim | Claim or renew for 3 minutes. {"take": true} takes a teammate's live claim; without it, their claim answers 409. |
| DELETE | /v1/queue/{id}/claim | Release your claim. |
| Parameter | Type | Description |
|---|---|---|
stage | string | prepress or imposition. Default imposition. |
status | string | waiting (default), planned (on an imposition) or done. |
q | string | Name, customer or notes contain this text. |
count | 1 | Return only waiting counts per inbox: {"object": "queue_counts", "prepress": 4, "imposition": 12}. |
The waiting inbox comes whole (up to 1,000 items), rush first, then by due date, in the list shape {"data": [...], "has_more": false, "next": null}. Planned and done items are paginated newest first.
Each PDF becomes a waiting item named after the file. PDFs are measured in the background (size, trim, inks); an item cannot go on an imposition until its file's page.status is ready. Answers 201 with the new items in the list shape.
| Field | Type | Description |
|---|---|---|
stage | string | prepress or imposition. Default imposition. Needs that app. |
file or files | string | array | Ids of PDFs already in Files. Or send multipart file uploads instead (PDF, up to 500 MB each). |
quantity | integer | 1 to 10,000,000. Default 1. |
due_on | string | Date like 2026-10-30, or null. |
customer | string | Up to 120 characters. |
rush | boolean | Rush items sort first. |
notes | string | Up to 2,000 characters. |
curl https://api.web2print.app/v1/queue \ -H "Authorization: Bearer $W2P_KEY" \ -H "Content-Type: application/json" \ -d '{"stage": "imposition", "file": "019a0c41-2a3c-7d11-b2c3-d4e5f6a7b8c9", "quantity": 500, "due_on": "2026-10-30", "customer": "Ortiz Realty"}'
{
"data": {
"object": "list",
"data": [
{
"id": "019a0c50-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"object": "queue_item",
"stage": "imposition",
"status": "waiting",
"name": "ortiz-cards",
"quantity": 500,
"due_on": "2026-10-30",
"customer": "Ortiz Realty",
"rush": false,
"notes": null,
"source": "upload",
"file": { "id": "019a0c41-2a3c-7d11-b2c3-d4e5f6a7b8c9", "object": "file", ... },
"claimed_by": null,
"claimed_until": null,
"history": [ ... ],
"created_at": "2026-10-22T18:31:00Z",
"updated_at": "2026-10-22T18:31:00Z"
}
],
"has_more": false
}
}
claimed_by is {id, name} of the teammate holding the item, while the claim is live. history holds the last 20 events (created, sent, removed, …) with their time.
Tags, fields and filters
Every item carries tags (free words), fields ({"Job#": "J123"}, from the workspace's naming patterns or set by hand), mapped (a naming pattern matched its file name) and inks (its spot inks, plus CMYK when it prints process). GET /v1/queue also takes ?tag=, ?field[Job#]=, ?unmapped=1, and on the waiting inbox ?ink=, ?inks_exact=, ?inks_within= (comma-separated ink names) and ?backers=1.
| POST | /v1/queue/tags | Tag many items: {"items": [id, …], "add": ["acme"], "remove": ["proof"], "fields": {"Job#": "J123"}}. |
| POST | /v1/queue/remap | Run the naming patterns again over waiting items ({"stage"?}). Fields from patterns are rebuilt; fields set by hand stay. Answers {items, mapped, unmapped}. |
| POST | /v1/queue/patterns | Try draft patterns without saving: {"patterns": ["{Customer}_{Job#}"], "stage"?}. Answers each pattern’s compile error, and every waiting file name with the fields it would get. |
Prepress work
Fix files in the Prepress inbox before they print. Changes run on the render fleet: the call answers 202 with the item, which carries op: {op, status: "working"} until the new file is in place (then op is gone) or status: "failed" with an error in plain words. Poll GET /v1/queue/{id} or listen for job.done. The previous file always stays in Files, so every change can be reverted. Requires the Prepress app.
Billed changes count the pages they write against your license, like any render. Checks, matches, splits, combines and vectorize tries are free.
| POST | /v1/queue/{id}/tool | Apply a tool: {"tool": "bleed", "params": {"amount": 0.125}}. Billed. Tools are listed below.Several tools in one request: {"steps": [{"tool": "resize", "params": {"width": 10, "height": 12}}, {"tool": "backer", "params": {}}], "then"?: "imposition"} (up to 20). The render worker runs the steps in order in one job, each on the last one's output, and sends back the final PDF; a step that does not apply to the file (an ink it lacks) is skipped, a step that fails stops the rest and names itself. The item still counts once. {"action": true} on revert puts back the file from before every step. |
| POST | /v1/queue/{id}/action | Run a saved action: {"action": id}. Each step bills like the tool; steps that do not apply to this file (an ink it does not have) are skipped. |
| POST | /v1/queue/action | Run an action on up to 200 items: {"action": id, "items": [id, …]}, or tools given in the request: {"steps": [{"tool", "params"}, …], "items": [id, …]}. Answers the items that started and skipped: [{id, name, reason}]. |
| POST | /v1/queue/{id}/recolor | Change spot inks: {"recolor": [{"from": "PANTONE 186 C", "to": "Red", "c": 0, "m": 100, "y": 90, "k": 0}]}. Billed. |
| POST | /v1/queue/{id}/revert | Back to the file before the last change. {"action": true} reverts every step of the last action; {"pages": true} undoes the Split or Combine that made the item. |
| POST | /v1/queue/{id}/split | One item per page, each with the same job facts and tags. Free. |
| POST | /v1/queue/combine | One PDF from 2 to 50 items, pages in the order given: {"items": [id, …]}. Free. |
| POST | /v1/queue/{id}/check | Start the preflight check (fonts, image resolution, RGB, hairlines, boxes). Free. |
| GET | /v1/queue/{id}/check | The check as plain findings, each naming the tool that fixes it. |
| POST | /v1/queue/{id}/press | Start the press check: ink coverage per plate, and with {"line_pt": 0.5, "gap_pt": 0.5} the lines and gaps thinner than that. Free. |
| GET | /v1/queue/{id}/press | Poll it (?line_pt=&gap_pt= picks a thin-line run). |
| POST | /v1/queue/{id}/match | Match the spot inks to a color book: {"book"?: "My colors", "max_de"?: 3} (no book = every book; tolerance 0.5 to 20 ΔE). Free. |
| GET | /v1/queue/{id}/match | Poll it (?book=&max_de=; no book = the newest run). |
| POST | /v1/queue/{id}/vectorize/try | Trace an image item with {"settings": {…}} and get a key to poll. Free; identical settings reuse the last try. |
| GET | /v1/queue/{id}/vectorize/try | Poll a try with ?key=; when done, report_url holds the traced paths. |
| POST | /v1/queue/{id}/vectorize | Turn the image item into a vector PDF with {"settings": {…}}. One billed page. |
| POST | /v1/queue/{id}/retype/try | Try replacing traced lettering on a vector item with live text: {"region": {"x", "y", "w", "h"}, "text", "font": fontId, "size": "fit"|pt, "tracking", "align", "ink": name|{"name", "c", "m", "y", "k"}} (inches from the trim's top-left). Free; answers a key to poll. Needs engine 2.36.0 on the render workers. |
| GET | /v1/queue/{id}/retype/try | Poll a retype try with ?key=; when done, result holds size_pt, text_box_in, removed and partial (shapes per ink), warnings and the new text's outlines (objects). |
| POST | /v1/queue/{id}/retype/art | Lift the item's shapes for the Retype room (free, once per file); GET polls it and gives report_url. |
Tools
Lengths are inches. An ink is {"name": "White", "c": 0, "m": 0, "y": 0, "k": 0} (0 to 100). Anchors are center, top-left, top, top-right, left, right, bottom-left, bottom, bottom-right. A bad value answers 400 saying which.
| Tool | Does | params |
|---|---|---|
resize | Resize the page and art | width, height (0.1 to 200; one may be left out to keep proportions) |
fit | Fit the art to a size | width, height, pad?, anchor? |
scale | Scale the art | percent (1 to 1000) |
artboard | Change the page size without scaling | to: size with width, height, anchor?; or ink (trim to the art) with pad? |
bleed | Add bleed, keeping the trim | amount (0.01 to 2) |
rotate | Rotate the page | degrees: 90, 180 or 270 |
mirror, flip | Mirror left-right, flip top-bottom | none |
cone | Tumbler wrap: warp flat art to print on a tapered cup | top, bottom (diameters), height |
align | Move the art to an edge or corner | anchor, pad? |
nudge | Move or turn the art | dx, dy (−100 to 100), turn (degrees) |
shrink | Shrink compensation for the material | width_pct, height_pct? (−50 to 50) |
crop | Clip the art to the page | none |
pages, delete_pages | Keep or delete pages | pages like 1,3-5 or 2-last |
step | Step and repeat on one page | rows, cols (1 to 50, at least 2 copies), gap?, margin?, turn? (none, rows, cols, checker), bleed_into_gap? |
marks | Crop and registration marks | set: any of crop, reg, center, bleed; bleed? |
keyline | Dieline | ink? (CutContour), box? (artboard, ink, bleed, trim), weight_pt?, inset?, radius?, dashed? |
edge | Border round the design | ink? (White), width_pt? (0.25 to 72, default 3), join? (round, miter, bevel), above? |
backer | Backer plate under the art | ink? (Adhesive), offset_pt? (−9 to 9, default 0.5), cover? (ink, area or inks with on_inks), place? (production, below, above), hidden? (production only: the plate is written hidden in the file's Illustrator payload, shown nowhere until an operator unhides it in Illustrator) |
outline_text | Turn text into outlines | none |
retype | Replace traced lettering with live text (one page; the same settings as retype/try) | region, text, font, size, tracking, align, ink |
expand_strokes | Turn strokes into filled shapes | hairline_pt? (default 0.25) |
spot_to_process | Convert spot inks to CMYK | names? (default every spot ink) |
convert_cmyk | Convert RGB to CMYK | tac? (total ink limit, 100 to 400) |
overprint | Set overprint | on (true or false) |
remove_color | Remove inks | names (inks the file has) |
simplify | Simplify curves | tolerance_pt? (0.05 to 5, default 0.35) |
trap | Trap | width_pt? (default 0.25), mode? (spread, choke, centre), join?, pairs? ("Yellow>Black; Red>Black") |
curl https://api.web2print.app/v1/queue/019a0c50-5e6f-7a8b-9c0d-1e2f3a4b5c6d/tool \ -H "Authorization: Bearer $W2P_KEY" \ -H "Content-Type: application/json" \ -d '{"tool": "edge", "params": {"ink": {"name": "White", "c": 0, "m": 0, "y": 0, "k": 0}, "width_pt": 4}}'
Vectorize settings
Prepress takes images (PNG, JPEG, TIFF, GIF, BMP) as image items (file.page.kind is image); they cannot go to Imposition until vectorized. Every setting is optional:
| Setting | Description |
|---|---|
mode | color (default), grayscale or bw. |
colors | auto (default) or 1 to 16 inks to find (1 clusters everything printed into one spot). |
inks | The inks to use instead of finding them: [{"name", "c", "m", "y", "k", "rgb": "RRGGBB"}], up to 16. |
grays, threshold | Gray levels for grayscale (2 to 32); the black/white cut for bw (0 to 255). |
paths, corners, noise | Path detail and corner sharpness (0 to 100); specks smaller than noise pixels are dropped (1 to 100). |
ignore_white, smooth, method | Leave white unprinted (default true); smooth edges; shapes abutting (default) or overlapping. |
width_in | Output width in inches (sets the resolution the image is read at). |
Actions
Named lists of tool steps a shop saves once and runs on any Prepress file ("Shirt transfer prep": Outline text, Border, Backer plate). Run one with POST /v1/queue/{id}/action, or let a hot folder run it on every file it sends. Requires the Prepress app.
| GET | /v1/actions | The workspace’s actions, by name. |
| POST | /v1/actions | Save {"name": "Shirt transfer prep", "steps": [{"tool": "outline_text", "params": {}}, {"tool": "edge", "params": {"width_pt": 4}}], "then": "imposition"}. A step that cannot run answers 400 naming it ("Step 2: …"). |
| PATCH | /v1/actions/{id} | Rename, replace the steps and/or change then. Runs already going keep what they started with. |
| DELETE | /v1/actions/{id} | Delete. Files it already changed keep their changes. |
then says where the file goes once the last step lands: stay (default, it waits in Prepress), imposition (sent to the Imposition inbox; without the Imposition app the file stays and prepress.action.failed says why) or done (marked done, as POST /v1/queue/done). A file sent on by hand meanwhile is left where it is.
An action reads {id, object: "prepress_action", name, steps, then, created_at, updated_at}. Steps use the tools and their params.
Colors
The workspace's color books, plus the shared official book. They feed the ink pickers and POST /v1/queue/{id}/match. A workspace keeps up to 5,000 colors of its own.
| GET | /v1/colors/books | The books: [{name, shared, count}], own books first. |
| GET | /v1/colors/books/ase | One book as an Adobe Swatch Exchange file (<book>.ase, spot swatches with CMYK), built from the library at request time: ?book=, plus ?shared=1 for the built-in PANTONE book. 404 when the book is empty or unknown. |
| GET | /v1/colors | One book’s colors (?book=), or colors matching ?q= across every book. |
| POST | /v1/colors | Add {"name": "Brand red", "book"?: "My colors", "cmyk": [0, 95, 85, 0], "spot"?: true, "hex"?: "D62631"}. A CMYK recipe or a screen color is required. |
| PATCH | /v1/colors/{id} | Change any of those fields. |
| DELETE | /v1/colors/{id} | Remove a color. |
| POST | /v1/colors/import | Import an Illustrator swatch export (.ase) or a CSV: multipart file and book (default: the file’s name). Same-named colors in that book are updated, the rest added. Answers 201 with {book, created, updated, skipped, parsed}. |
| DELETE | /v1/colors/books | Remove one of your books and every color in it: ?book=. The built-in PANTONE book cannot be removed. |
A color reads {id, object: "color", name, book, shared, spot, cmyk, lab, hex}.
Impositions
Items from the Imposition inbox laid out on press sheets. In auto mode the fleet works out four strategies (fewest_sheets, least_waste, fewest_overs, best_margin); you pick one, adjust it if you like, and approve. In manual mode you lay out the sheets yourself. Approving composes each sheet as a print-ready PDF with a proof image. Requires the Imposition app.
Status: draft, packing, ready, failed, imposing, done, canceled.
| GET | /v1/impositions | List, summary form. ?status=. Paginated. |
| POST | /v1/impositions | Start an imposition. |
| GET | /v1/impositions/{id} | Full form: items, strategies, layout, review and output. |
| POST | /v1/impositions/{id}/strategy | Pick a strategy: {"strategy": "fewest_sheets"}. Replaces any edits. |
| POST | /v1/impositions/{id}/repack | Work a finished auto imposition out again with changed sheets, options or per-item items overrides. Goes back to packing. |
| PATCH | /v1/impositions/{id}/layout | Save edited sheets: {"runs": [...]}, up to 200. The answer carries the review. |
| POST | /v1/impositions/{id}/approve | Approve and compose. Blocking problems answer 400 with what to fix. |
| POST | /v1/impositions/{id}/cancel | Cancel; items go back to the inbox as waiting. |
Auto answers 201 in packing; the strategies fill in when the fleet finishes, so poll GET /v1/impositions/{id}. Manual answers 201 in ready.
| Field | Type | Description |
|---|---|---|
items required | array | Queue item ids from the Imposition inbox, waiting and measured. Up to 400. |
mode | string | auto (default), manual, roll, nest or fixed (a layout you send whole; see below). |
sheets | array | Auto: sheets to consider, up to 8. Each is a saved sheet id or a preset key such as digital-12x18. |
sheet | string | Manual: the one sheet to lay out on. |
options | object | max_inks_per_run (1 to 16, default 6), spoilage ({sheets, percent}, percent up to 50), max_overs_percent, single_item_runs (boolean), time_budget_ms (2,000 to 120,000, default 20,000). |
overrides | object | Auto: by item id, {quantity, price, rotate: "any" | "none", group}. |
name | string | Optional name. |
curl https://api.web2print.app/v1/impositions \ -H "Authorization: Bearer $W2P_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: gang-2026-10-22-a" \ -d '{"mode": "auto", "items": ["019a0c50-5e6f-7a8b-9c0d-1e2f3a4b5c6d"], "sheets": ["digital-12x18", "digital-13x19"]}'
Summary fields (the full form adds more, below)
{
"data": {
"id": "019a0c55-6f70-7182-a394-b5c6d7e8f901",
"object": "imposition",
"number": 1001,
"name": null,
"mode": "auto",
"status": "packing",
"error": null,
"item_count": 1,
"strategy": null,
"sheet_count": 0,
"approved_at": null,
"created_at": "2026-10-22T18:40:00Z",
"updated_at": "2026-10-22T18:40:00Z",
...
}
}
The full form, returned by GET /v1/impositions/{id} and every action, adds items, sheets_available, options, result (the strategies), strategy_labels, layout ({runs, edited}), review (prices, plus problems that block approval and warnings), metadata, output (per sheet: run, status, error, file, proof) and thumbs (item id to thumbnail URL).
approve takes optional {"metadata": {"PO": "88123"}}: extra slug tokens with keys of capital letters and underscores and values up to 80 characters. JOB, CUSTOMER, DATE and NAME are filled in for you.
Fixed layouts
When your own system has already decided where everything goes, send the layout whole: {"layout": "fixed"} (or mode: "fixed") with workspace PDF files instead of inbox items. The answer is one plan with one run, already ready; nothing is taken from the Imposition inbox and the layout cannot be edited by hand (send a new one). approve composes the sheet exactly as any other plan and bills one imposition sheet. Positions are inches from the sheet's top-left corner, y down, for items and marks alike.
| Field | Type | Description |
|---|---|---|
sheet required | object | {w, h} in inches (1 to 120), optional rotation (0, 90, 180, 270) and mirror (boolean) applied to the finished sheet. |
items required | array | Up to 500 placements {file, x, y, w?, h?, rotation?, mirror?, recolor?}. file is a measured PDF in your workspace; w/h default to the page (turned by rotation); recolor maps one of the file's separation names to {name, c, m, y, k} (0 to 100) and renames that plate in this placement only, so the same art can appear twice in two inks. A placement off the sheet, an unknown file or a separation the file lacks is refused. |
marks | array | {type, x, y, rotation?, mirror?, origin?, ...} (origin "bottom": y is the engine's own bottom-left bounding-box y and is emitted unchanged, for callers porting a hand-built plan; default top-left) with type colorbar, colornames, barcode (text or data), slug or text (text; %TOKEN% slots fill from metadata, %INDEX% / %COLOR_NAME% per pass), corner or target (size), file (a mark PDF id). |
passes | array | Print order, first pass first: {ink, alternate?, index?, subindex?}; a recolored plate named as alternate rides that pass. index / subindex (whole numbers, 1 to 999) are the press's own numbering when it differs from the position (split screens share an index with a later subindex); they print on barcode marks as %INDEX%%SUBINDEX% and label one colour patch each. Default: position + 1 and 1. Default passes: the files' spot inks in order of first appearance. A placement's w / h is the footprint: art is anchored at its bottom-left, never scaled. |
metadata | object | Slug tokens, e.g. {"RUN_NUMBER": "R-77", "OUTPUT_FILE_NAME": "r77.pdf"} (keys of capital letters and underscores, values up to 80 characters), kept on the plan and merged with approve's. |
name | string | Optional name. |
curl https://api.web2print.app/v1/impositions?wait=20000 \ -H "Authorization: Bearer $W2P_KEY" \ -H "Content-Type: application/json" \ -d '{"layout": "fixed", "sheet": {"w": 12, "h": 9}, "items": [{"file": "019a…c6d", "x": 0.5, "y": 0.5}, {"file": "019a…c6d", "x": 4, "y": 0.5, "rotation": 90, "recolor": {"Blue": {"name": "Red", "m": 100, "y": 100}}}], "marks": [{"type": "text", "x": 0.5, "y": 8.5, "text": "Run %RUN_NUMBER%"}, {"type": "colorbar", "x": 5, "y": 8.5}], "passes": [{"ink": "Blue", "alternate": "Red"}], "metadata": {"RUN_NUMBER": "R-77"}}'
Then POST /v1/impositions/{id}/approve?wait=30000; the sheet PDF is at output[0].file.download_url.
Sheets
The press sheets a shop prints on (size, margins, gripper, gaps, printer's marks) and what a sheet costs to run. Built-in presets are always listed; save your own from scratch or from a preset. Requires the Imposition app.
| GET | /v1/sheets | Your sheets, plus the presets. |
| POST | /v1/sheets | Save a sheet. 201. Up to 200 per workspace. |
| PATCH | /v1/sheets/{id} | Edit name, spec or cost. Bumps version; impositions already made keep their copy. |
| DELETE | /v1/sheets/{id} | Delete. Impositions that used it are unaffected. |
| POST | /v1/sheets/layout | Dry-run a layout on a sheet without saving anything. |
Preset keys: digital-12x18, digital-13x19, tabloid-11x17, letter, offset-20x29, transfer-13x19.
| Field | Type | Description |
|---|---|---|
name | string | Up to 120 characters. Required unless from gives one. |
from | string | A preset key to start from; its spec, cost and name fill whatever you leave out. |
spec | object | Inches. sheet {width, height}, margins {top, right, bottom, left}, gripper {edge: none | top | bottom | left | right, size}, layout {gap: {x, y}, rotate: auto | none | always, align: center | top-left}, crop_marks {enabled, offset, length}, gang_check, and marks. Start from a preset's spec to see every field filled in. |
cost | object | US dollars: sheet, impression, plate, run, cut, and sheets_per_cut (default 100). |
curl https://api.web2print.app/v1/sheets \ -H "Authorization: Bearer $W2P_KEY" \ -H "Content-Type: application/json" \ -d '{"from": "digital-13x19", "name": "13 x 19 house stock", "cost": {"sheet": 0.18, "impression": 0.05, "cut": 0.5}}'
{
"data": {
"id": "019a0b90-7a8b-7c9d-8e0f-1a2b3c4d5e6f",
"object": "sheet",
"name": "13 x 19 house stock",
"spec": { "sheet": { "width": 13, "height": 19 }, ... },
"cost": { "sheet": 0.18, "impression": 0.05, "plate": 0, "run": 0, "cut": 0.5, "sheets_per_cut": 100 },
"version": 1,
"created_at": "2026-10-22T16:02:11Z",
"updated_at": "2026-10-22T16:02:11Z"
}
}
GET /v1/sheets answers {"data": [sheet, ...], "has_more": false, "next": null, "presets": [...]}. Each preset has id (its key), object: "sheet_preset", name, description, spec and cost.
Lays sample pieces out on a sheet and returns the cells, printable area and mark positions. Nothing is saved.
| Field | Type | Description |
|---|---|---|
spec or sheet | object | string | A spec to try, or a saved sheet id or preset key. |
items | array | Up to 20 pieces: {w, h, trim_w, trim_h, copies} in inches. Default: one 3.75 × 2.25 in piece. |
inks | array | Ink names for the marks, up to 16. |
Answers {"data": {"object": "sheet_layout", "spec", "area", "layout", "marks"}}.
Invoices
The workspace's web2print invoices (licenses, renewals, invoices sent by us). Owners and admins only; a member's key gets 403. Paged like every list; amounts are numbers in US dollars. The shapes match the invoice.* and transaction.* webhooks.
| GET | /v1/invoices | List, newest first. |
| GET | /v1/invoices/{id} | One invoice with memo, line_items and transactions. |
| Parameter | Type | Description |
|---|---|---|
status | string | open, paid, void or partially_paid. |
limit, after | integer, string | Paging, as for every list. |
curl "https://api.web2print.app/v1/invoices?status=paid" \ -H "Authorization: Bearer $W2P_KEY"
{
"data": [
{
"id": "019a0a20-1b2c-7d3e-8f4a-5b6c7d8e9f01",
"object": "invoice",
"number": "INV-001042",
"kind": "license",
"status": "paid",
"subtotal": 8000, "discount": 0, "tax": 0, "paid": 8000, "total": 8000,
"currency": "usd",
"created_at": "2026-10-01T00:00:00Z",
"updated_at": "2026-10-01T00:04:12Z"
}
],
"has_more": false,
"next": null
}
line_items are {id, description, quantity, unit_price, total}; transactions are payments as below.
Transactions
Payments and refunds on the workspace's invoices. Owners and admins only, paged like every list.
| GET | /v1/transactions | List, newest first. |
| GET | /v1/transactions/{id} | One payment. |
| Parameter | Type | Description |
|---|---|---|
status | string | authorized, captured, refunded or failed. |
method | string | card, check, bank_account or wire. |
curl https://api.web2print.app/v1/transactions \
-H "Authorization: Bearer $W2P_KEY"
{
"data": [
{
"id": "019a0a20-4e5f-7a6b-9c7d-8e9f0a1b2c3d",
"object": "transaction",
"status": "captured",
"method": "card",
"amount": 8000,
"amount_refunded": 0,
"currency": "usd",
"invoice": "019a0a20-1b2c-7d3e-8f4a-5b6c7d8e9f01",
"refunded_at": null,
"created_at": "2026-10-01T00:04:10Z"
}
],
"has_more": false,
"next": null
}
Webhooks
Instead of polling, let web2print tell your server when work finishes. The workspace owner adds endpoints in Settings, Webhooks, or an integration registers its own with the API below: an https:// URL and the events to send. The endpoint's signing secret is shown when you create it.
Managing endpoints
Owners and admins only (others get 403). A workspace keeps up to 20 endpoints. The URL must be https:// and reachable from the internet; local and private addresses answer 400.
| GET | /v1/webhook-endpoints | List. |
| POST | /v1/webhook-endpoints | Add {url, events, name?, description?}. Answers 201 with secret, shown this once. |
| GET | /v1/webhook-endpoints/{id} | One endpoint. |
| PATCH | /v1/webhook-endpoints/{id} | Change url, events (replaces the list), name, description, or enabled (false pauses deliveries). |
| DELETE | /v1/webhook-endpoints/{id} | Remove it and its delivery history. |
| POST | /v1/webhook-endpoints/{id}/secret | Replace the signing secret; the new one is returned once and signs every delivery from now on, retries included. |
| POST | /v1/webhook-endpoints/{id}/test | Send a test event to this endpoint only, whatever it subscribes to. 202 with the delivery; 409 while paused. |
| GET | /v1/webhook-endpoints/{id}/deliveries | Deliveries, newest first. ?status= pending, delivered or failed. Paginated. |
| POST | /v1/webhook-endpoints/{id}/resend | Send {delivery} again. 202. |
curl https://api.web2print.app/v1/webhook-endpoints \ -H "Authorization: Bearer $W2P_KEY" \ -H "Content-Type: application/json" \ -d '{"url": "https://example.com/webhooks/web2print", "events": ["batch.done", "batch.failed"]}'
{
"data": {
"id": "019a0c40-1a2b-7c3d-8e4f-5a6b7c8d9e0f",
"object": "webhook_endpoint",
"name": "example.com",
"url": "https://example.com/webhooks/web2print",
"description": null,
"enabled": true,
"paused_by_plan": false,
"events": ["batch.done", "batch.failed"],
"created_at": "2026-10-24T04:25:42Z",
"updated_at": "2026-10-24T04:25:42Z",
"secret": "9f2c…64 hex characters…"
}
}
A delivery reads {id, object: "webhook_delivery", event, status, attempts, response_code, response_body, error, payload, created_at, sent_at, next_attempt_at}; response_body is the first 1,000 characters your server answered.
Events
The same list is at GET /v1/webhook-endpoints/events. Objects have the shape the API returns for them: public ids, string statuses, ISO-8601 UTC times. Events are sent once the change is saved, with the object as it is then.
| Event | Sent when | data.object |
|---|---|---|
template.ready | A template (or a new version) finished inspecting and can be used. | The template as GET /v1/templates/{id} returns it; on template.ready and template.failed its current_version is that version, with its variables. |
template.failed | A template could not be read; error says why. | |
template.deleted | A template was deleted. | |
batch.started | A batch was started. | The batch as GET /v1/batches/{id} returns it. Row events carry one row: {id, object: "batch_row", batch, batch_name, row, output_name, status, values, error, inks, pdf, production_pdf}. |
batch.row.done | One row of a batch rendered; file is its PDF. | |
batch.row.failed | One row of a batch failed; error says why. | |
batch.done | A batch finished (some rows may have failed: see failed). | |
batch.failed | Every row of a batch failed. | |
batch.canceled | A batch was canceled. | |
job.done | A render job finished. | The job as GET /v1/jobs/{id} returns it, with its files. |
job.failed | A render job failed. | |
inbox.item.added | A file arrived in the Prepress or Imposition inbox. | The item as GET /v1/queue/{id} returns it (no history or claim), plus what changed: from and to when moved, imposition_number when planned or imposed. |
inbox.item.moved | A file was sent from one inbox to the other. | |
inbox.item.planned | A file was placed on an imposition. | |
inbox.item.imposed | A file was printed on an approved imposition. | |
inbox.item.removed | A file was removed from an inbox. | |
inbox.item.restored | A removed file was put back in its inbox. | |
inbox.item.done | A Prepress file was marked done (by hand or an action's then); file is the finished PDF. | The inbox item, as above. |
inbox.item.reopened | A done Prepress file was put back in the inbox. | |
prepress.action.done | A prepress action finished on a file. | The inbox item, plus action: {run, name} and, on failure, reason (or step). |
prepress.action.failed | A prepress action stopped on a file; reason says why. | |
cut.done | A cut file was made for the cutter (POST /v1/files/{id}/cuts, Prepress tools or an approved sheet). | {object: "cut_file", file, source, format, unit, origin, spots, paths, open_paths, length_in, marks, customer, name, inbox_item}: file is the DXF, SVG or PDF for the cutter and source the PDF it was read from (both with a download_url); customer and name come from the inbox item the source belongs to, when it is in one. The desktop app's "Cut files" output rule drops file in the cutter's queue folder. |
imposition.ready | An imposition is laid out and ready to approve. | The imposition as GET /v1/impositions lists it, plus output: each sheet run's {run, status, error, file, proof}, with the composed PDF as file. |
imposition.approved | An imposition was approved; its sheets are being made. | |
imposition.done | Every sheet of an imposition is made. | |
imposition.failed | An imposition could not be laid out or made. | |
imposition.canceled | An imposition was canceled. | |
member.invited | Someone was invited to the workspace. | {id, object: "member", name, email, role, status, joined_at}; status is invited, active or removed. |
member.joined | An invited person accepted and joined. | |
member.role_changed | A member became an admin or a member. | |
member.removed | A member was removed. | |
license.added | A license was added to the workspace. | {id, object: "license", plan, status, amount, currency, starts_at, ends_at, renews, canceled_at}. limit.reached sends {id, object: "limit", kind, used, allowed, unit, message}. |
license.changed | A license was upgraded, renewed or set to keep renewing. | |
license.ended | A license was set to end, ended, or lapsed unpaid. | |
limit.reached | Work was refused because a license limit was reached (sent at most once a day per limit). | |
invoice.created | An invoice was issued. | Invoices: {id, object: "invoice", number, kind, status, subtotal, discount, tax, paid, total, currency, created_at, updated_at}. Payments: {id, object: "transaction", status, method, amount, amount_refunded, currency, invoice, refunded_at, created_at}; transaction.refunded adds refund, the amount of that refund. |
invoice.paid | An invoice was paid in full. | |
invoice.voided | An invoice was voided. | |
transaction.captured | A payment went through. | |
transaction.failed | A payment failed. | |
transaction.refunded | A payment was refunded. |
Delivery
Each delivery is a POST with a JSON body:
{
"id": "019a0c42-3b4c-7d5e-8f6a-7b8c9d0e1f2a",
"object": "event",
"type": "batch.done",
"created_at": "2026-10-24T15:20:31Z",
"data": {
"object": {
"id": "019a0c41-0f7e-7a2b-9c3d-4e5f60718293",
"object": "batch",
"name": "Order 10482",
"status": "done",
"datasets": 2, "done": 2, "failed": 0,
...
},
"by": { "type": "api_key", "id": "019a0b77-…", "name": "Storefront" }
}
}
data.by says who caused it: person, api_key or system (web2print itself). It is left out where nobody did (job events). With the body come these headers:
| Header | Value |
|---|---|
Content-Type | application/json |
X-Web2Print-Signature | sha256= followed by the hex HMAC-SHA256 of the raw body, keyed with the endpoint's secret. |
X-Web2Print-Event | The event type, the same as type in the body. |
User-Agent | web2print-Webhook/1.0 |
Answer with any 2xx within 10 seconds. Anything else, or no answer, is retried: up to 6 attempts in all, waiting 1, 5, 15, 60 and then 360 minutes between them. Retries send the identical body, so use id to ignore a delivery you have already handled.
Verifying signatures
Compute the HMAC over the raw request body exactly as received, before any JSON parsing, and compare in constant time. The signature does not include a timestamp, so dedupe on id.
PHP
$secret = getenv('W2P_WEBHOOK_SECRET'); $payload = file_get_contents('php://input'); $expected = 'sha256=' . hash_hmac('sha256', $payload, $secret); $signature = $_SERVER['HTTP_X_WEB2PRINT_SIGNATURE'] ?? ''; if (!hash_equals($expected, $signature)) { http_response_code(400); exit; } $event = json_decode($payload, true); http_response_code(200);
Node.js (Express)
const crypto = require('crypto'); const express = require('express'); const app = express(); app.post('/webhooks/web2print', express.raw({ type: 'application/json' }), (req, res) => { const expected = 'sha256=' + crypto .createHmac('sha256', process.env.W2P_WEBHOOK_SECRET) .update(req.body) .digest('hex'); const given = req.get('X-Web2Print-Signature') || ''; const ok = given.length === expected.length && crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected)); if (!ok) return res.sendStatus(400); const event = JSON.parse(req.body); res.sendStatus(200); });
Event feed
Every webhook event of the last 30 days, oldest first, in the same envelope and with the same ids as the webhook deliveries, whether or not an endpoint subscribes. Read it when you cannot take webhooks (behind a firewall, on a shop computer): keep the last id you handled and ask for what came after it. Members' keys see everything except the license.*, invoice.* and transaction.* groups. File links (download_url) are signed fresh on every read; a file deleted since has null.
| Parameter | Type | Description |
|---|---|---|
after | string | An event id: return the events after it. An id older than 30 days answers 400; start again with since. |
since | string | ISO-8601 time, used when after is not given. Default: 24 hours ago. |
type | string | Comma-separated types; inbox.* takes a whole group. |
limit | integer | 1 to 100. Default 100. |
Answers {"data": [event, …], "has_more", "next"}. Ask again with ?after=next at once while has_more is true, otherwise every few seconds. When nothing is new, next is the after you sent.
Desktop app
web2print Desktop runs on the shop's computers: it watches hot folders and drops finished files in local folders and network shares. It pairs once with a code made in the studio (Account, Computers, Connect a computer) and then keeps its own key: nobody signs in on it again. Its key acts as the person who paired it and works without the API add-on.
| POST | /v1/desktop/pairings | Make a one-time code, good for 10 minutes. Studio sign-in only; API keys and paired computers get 403. |
| POST | /v1/desktop/pair | No key: {"code": "ABCD-EFGH", "device": "RIP PC"} answers 201 with token, device, workspace and person. A wrong, used or expired code answers 400; more than 10 tries from one address in 15 minutes answer 429. |
| GET | /v1/desktop/devices | Paired computers: the whole team's for owners and admins, your own otherwise. current marks the calling computer. |
| DELETE | /v1/desktop/devices/{id} | Remove a computer; its key stops working at once. |
| POST | /v1/desktop/studio | Paired computers only: {"to": "/studio/inbox"} answers 201 with the studio url to open in the browser (paths outside the studio give the studio home). It signs nobody in: the browser's own sign-in, with two-step, applies. |
Windows and Mac: download from Desktop app and double-click to install (for the signed-in person only, no admin password). Linux: curl -fsSL https://web2print.app/install.sh | sh; add -s -- --pair=ABCD-EFGH to connect in the same step, or -s -- --uninstall to remove it.
A hot folder with stage: "batch" names a template and an optional destination. Send a spreadsheet dropped there as POST /v1/batches with rows=@sheet.csv and hot_folder=<id>: the template and destination come from the hot folder.
OAuth
Besides secret keys, the API accepts OAuth 2.1 access tokens in the same Authorization: Bearer header, for apps your customers connect to their workspace. A token works like a key: it is limited to the scopes granted, and an expired token answers 401 asking you to use the refresh token.
Once client registration opens, a 401 will carry a WWW-Authenticate header with a resource_metadata URL (RFC 9728) for client discovery. Until then it carries no discovery URL.
OAuth client registration is not open yet. Use a secret key from Settings, API keys for your own integrations.