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 403 and type security_error.
  • JSON in, JSON out. Send request bodies as a JSON object with Content-Type: application/json. File uploads use multipart/form-data, or the raw file as the body with ?name= where noted.
  • One envelope. A success answers {"data": ...}; a list adds has_more and next; 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 id field, for example 019a0c3e-4b21-7d55-8f0e-6c1a2b3d4e5f. Every object also says what it is in object.
  • 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:

SettingWhat it does
ScopeOne 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 limitRequests per minute for this key. See Rate limits.
ExpiryAfter this time the key answers 401 with "API key has expired."
IP allowlistIPv4 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"
  }
}
StatusTypeMeaning
400invalid_request_errorSomething in the request is missing or wrong. The message says what.
401auth_errorNo key, an unknown key, or an expired key.
402plan_limitA 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).
403security_errorNot 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.
403permission_errorOnly the owner and admins can manage the team.
404not_foundNo such object in your workspace. Objects in other workspaces are never revealed.
404invalid_request_errorNo endpoint matches the method and path.
409conflictThe 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.
413limit_errorThe upload is too large. Each file may be up to 256 MB; GET /v1/account/storage returns the exact cap as max_file_bytes.
429rate_limit_errorToo many requests for this key. Wait for Retry-After seconds.
429limit_errorEvery editing seat on your plan is in use (Sessions).
500api_errorOur fault. The message is generic; retry, with the same Idempotency-Key if you sent one.
502api_errorAn invite email could not be sent (Members).

Pagination

Lists come newest first and use a cursor:

ParameterTypeDescription
limitinteger1 to 200. Default 50.
afterstringThe 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: 409 conflict.
  • Same key while the first request is still running: 409 conflict. 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: 200 with the object in its new state (the queue item on its rotated file, the file with its page facts, the job with its files), and the header X-Web2Print-Wait: done.
  • Still running when the time ran out: 202 with the object as it stands and X-Web2Print-Wait: pending. Poll GET /v1/jobs/:id or take the job.done webhook.
  • The server is holding as many waits as it can: 503 with Retry-After. Nothing was started; retry, or send the request without wait.
  • Set your HTTP client's timeout above the wait you ask for. wait on a GET is 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.

GET/v1/ping

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.

GET/v1/capabilities

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/accountWorkspace, plan, apps and open editing sessions.
GET/v1/account/usageUnits used in a billing month.
GET/v1/account

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 }
  }
}
GET/v1/account/usage
ParameterTypeDescription
periodstringMonth 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/membersEveryone, plus seats and whether you may manage.
POST/v1/membersInvite someone by email.
POST/v1/members/{id}/resendSend 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}/2faTurn 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.

POST/v1/members
FieldTypeDescription
name requiredstringUp to 120 characters.
email requiredstringWhere the invite goes. The link is good for 72 hours.
rolestringadmin 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/sessionsList. ?status= open, closed, killed or expired. Paginated.
GET/v1/sessions/{id}One session.
POST/v1/sessionsOpen a session. Body {"client": "spa" | "api" | "desktop"}, default api. 201, or 429 limit_error when every seat is in use.
POST/v1/sessions/{id}/heartbeatKeep 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/templatesList templates.
POST/v1/templatesUpload a new template.
POST/v1/templates/sampleAdd 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}/versionsAll versions, newest first.
GET/v1/templates/{id}/versions/{n}One version by number, with its variables.
POST/v1/templates/{id}/versionsUpload a new version.
POST/v1/templates/{id}/versions/{n}/restoreMake a ready version current again.
GET/v1/templates/{id}/variablesVariables of the current version.
PATCH/v1/templates/{id}/variablesRelabel one variable, or set its fit and anchor.
GET/v1/templates/{id}/downloadYour uploaded file and its PDF. Free.
GET/v1/templates/{id}/textThe live text of an Illustrator file, story by story. Free.
POST/v1/templates/{id}/text/bindMake variables of chosen stories: a new version. Free.
POST/v1/templates/{id}/previewsRender a preview PNG with your values.
GET/v1/templates

Paginated. Each template carries its current version, so a grid renders without more calls.

ParameterTypeDescription
qstringName contains this text.
folderstringA folder id, or root for templates in no folder.
deep1With folder, include every folder below it.
limit, afterSee Pagination.
POST/v1/templates

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".

FieldTypeDescription
file requiredfileOne .ai, .pdf or .zip.
namestringTemplate name. Default: the file name.
folderstringFolder 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.

PATCH/v1/templates/{id}

Only the fields you send change.

FieldTypeDescription
namestring1 to 191 characters.
folderstring | nullFolder id, or null for the top level.
default_destinationstringdownload, prepress or imposition: where batches of this template go unless the batch says otherwise.
POST/v1/templates/{id}/versions

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.

POST/v1/templates/{id}/versions/{n}/restore

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.

GET/v1/templates/{id}/download

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://...", ... }
  }
}
GET/v1/templates/{id}/variables

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.

PATCH/v1/templates/{id}/variables

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" }
POST/v1/templates/{id}/previews

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.

FieldTypeDescription
valuesobjectVariable 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.
ppiinteger36, 72, 96, 144, 192 or 300. Default 96.
versionintegerPreview a specific ready version. Default: the current version.
sessionstringAn open session id. An ended session answers 409.
waitbooleanDefault 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.

GET/v1/templates/{id}/text

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.

ParameterTypeDescription
versionintegerWhich 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" }
  }
}
POST/v1/templates/{id}/text/bind

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.

ParameterTypeDescription
bindingsarrayRequired. [{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.
versionintegerWhich version to bind. Default: the current version.
waitintegerHold 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/designsList designs (?template=, ?status=draft|saved).
POST/v1/designsNew 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}/saveSave as template: compile. 202 with the design and the job.
DELETE/v1/designs/{id}Delete the design. Its template stays.
GET/v1/designs/assetsFonts and colors a document may reference.
POST/v1/designs/barcodesDraw 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.

FieldTypeDescription
schema_versioninteger1.
artboardobject{width_in, height_in}.
outputstringfixed (default: every render keeps the artboard; cards, labels, flyers) or ink (each render's page shrinks to its ink; transfers).
colorsarray[{ref, name, c, m, y, k, hex?}]. name is the literal plate name the render prints; layers refer to a color by ref.
fontsarray[{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.
distressobjectOptional {pattern}: a worn-ink mask over the whole design.
layersarrayEach has id, a kind, its geometry and may carry name, hidden (left out of the compile), rotation, flip_h, flip_v, shear and variable.
LayerFields
texttext, 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],…]}.
shapeshape rect|ellipse|star|polygon, x, y, w, h, fill?, stroke?, stroke_width_pt?, points?, inner_ratio?, sides?.
artworkfile (a PDF file id), x, y, w, h, recolor? {spot name: color ref}.
pathcmds [[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?.
barcodebarcode {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/foldersEvery folder at once, flat, with parent ids and template counts.
POST/v1/foldersCreate 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.
FieldTypeDescription
name required to createstringUp to 120 characters.
parentstring | nullParent folder id, or null for the top level.
sortintegerPosition 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/batchesList. ?template= id, ?status= any status above. Paginated.
POST/v1/batchesCreate and start a batch.
GET/v1/batches/{id}One batch with live counts.
GET/v1/batches/{id}/rowsRows with status and PDF links.
POST/v1/batches/{id}/cancelCancel unfinished rows; they are never billed. Finished rows keep their PDFs. 409 if the batch already finished.
POST/v1/batches/{id}/archiveGet the whole batch as one zip.
POST/v1/batches

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.

FieldTypeDescription
template requiredstringTemplate id. Its current version must be ready (409 otherwise).
rows requiredarrayUp to 10,000 rows. May be left out when count and serial are given.
rows[].valuesobjectVariable 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_namestringFile name for the row, unique in the batch. Default R00001, R00002, …
rows[].quantityintegerOverrides queue.quantity for this row's inbox item.
namestringDefault: the template name and the date.
versionintegerPin an older version number.
destinationstringdownload, prepress or imposition. Default: the template's default_destination. Prepress and imposition put each finished row into that app's inbox.
queueobjectFacts for the inbox items: quantity, due_on (YYYY-MM-DD), customer, rush, notes.
serialobject or arrayNumber 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.
countintegerWith 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.

GET/v1/batches/{id}/rows
ParameterTypeDescription
statusstringpending, running, done or failed.
limitinteger1 to 200. Default 50.
afterintegerRow 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"
}
POST/v1/batches/{id}/archive

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/filesList. Paginated.
GET/v1/files/{id}One file with a fresh download link.
POST/v1/filesUpload an image or mark.
POST/v1/files/{id}/preflightThe engine's full preflight report of a PDF, as written. Free.
GET/v1/files/{id}/preflightThe same report, for polling.
POST/v1/files/{id}/objectsLift a vector PDF's own shapes as editable objects (exact geometry, nothing traced). Free.
GET/v1/files/{id}/objectsThe lift, for polling (same ?with_text=&object_gap=&vectorize=).
GET/v1/files
ParameterTypeDescription
kindstringupload, template, derivative (working PDFs), production, prepress, sheet or preview.
batchstringFiles of one batch. With kind=production, a batch's print-ready PDFs.
templatestringFiles of any version of one template.
namestringExact file name.
inkstringFiles 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.

POST/v1/files

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.

FieldTypeDescription
file requiredfileOne file.
namestringDefault: the uploaded file name.
kindstringupload (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.

POST/v1/files/{id}/preflight

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.

FieldTypeDescription
thin_lineobjectOptional {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.

POST/v1/files/{id}/objects

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.

FieldTypeDescription
with_textbooleanTolerate 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_gapnumberInches. Same-colour shapes closer than this form one object; by default every ink is one object.
vectorizeboolean | objectFor 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/fontsList. ?q= searches PostScript name and family; ?status= enabled or disabled. Paginated.
GET/v1/fonts/{id}One font.
POST/v1/fontsUpload up to 50 fonts.
POST/v1/fonts/{id}/disableStop using a font.
POST/v1/fonts/{id}/enableUse it again.
POST/v1/fonts

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/jobsList, 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}/cancelA 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/queueList an inbox.
POST/v1/queueAdd 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/moveSend several items: {"items": [id, …], "stage": "prepress"}. Up to 500.
POST/v1/queue/doneMark 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/reopenPut done Prepress items back in the inbox: {"items": [id, …]}. Fires inbox.item.reopened. Anything not done answers 409.
POST/v1/queue/{id}/claimClaim or renew for 3 minutes. {"take": true} takes a teammate's live claim; without it, their claim answers 409.
DELETE/v1/queue/{id}/claimRelease your claim.
GET/v1/queue
ParameterTypeDescription
stagestringprepress or imposition. Default imposition.
statusstringwaiting (default), planned (on an imposition) or done.
qstringName, customer or notes contain this text.
count1Return 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.

POST/v1/queue

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.

FieldTypeDescription
stagestringprepress or imposition. Default imposition. Needs that app.
file or filesstring | arrayIds of PDFs already in Files. Or send multipart file uploads instead (PDF, up to 500 MB each).
quantityinteger1 to 10,000,000. Default 1.
due_onstringDate like 2026-10-30, or null.
customerstringUp to 120 characters.
rushbooleanRush items sort first.
notesstringUp 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/tagsTag many items: {"items": [id, …], "add": ["acme"], "remove": ["proof"], "fields": {"Job#": "J123"}}.
POST/v1/queue/remapRun 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/patternsTry 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}/toolApply 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}/actionRun 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/actionRun 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}/recolorChange spot inks: {"recolor": [{"from": "PANTONE 186 C", "to": "Red", "c": 0, "m": 100, "y": 90, "k": 0}]}. Billed.
POST/v1/queue/{id}/revertBack 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}/splitOne item per page, each with the same job facts and tags. Free.
POST/v1/queue/combineOne PDF from 2 to 50 items, pages in the order given: {"items": [id, …]}. Free.
POST/v1/queue/{id}/checkStart the preflight check (fonts, image resolution, RGB, hairlines, boxes). Free.
GET/v1/queue/{id}/checkThe check as plain findings, each naming the tool that fixes it.
POST/v1/queue/{id}/pressStart 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}/pressPoll it (?line_pt=&gap_pt= picks a thin-line run).
POST/v1/queue/{id}/matchMatch 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}/matchPoll it (?book=&max_de=; no book = the newest run).
POST/v1/queue/{id}/vectorize/tryTrace an image item with {"settings": {…}} and get a key to poll. Free; identical settings reuse the last try.
GET/v1/queue/{id}/vectorize/tryPoll a try with ?key=; when done, report_url holds the traced paths.
POST/v1/queue/{id}/vectorizeTurn the image item into a vector PDF with {"settings": {…}}. One billed page.
POST/v1/queue/{id}/retype/tryTry 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/tryPoll 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/artLift 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.

ToolDoesparams
resizeResize the page and artwidth, height (0.1 to 200; one may be left out to keep proportions)
fitFit the art to a sizewidth, height, pad?, anchor?
scaleScale the artpercent (1 to 1000)
artboardChange the page size without scalingto: size with width, height, anchor?; or ink (trim to the art) with pad?
bleedAdd bleed, keeping the trimamount (0.01 to 2)
rotateRotate the pagedegrees: 90, 180 or 270
mirror, flipMirror left-right, flip top-bottomnone
coneTumbler wrap: warp flat art to print on a tapered cuptop, bottom (diameters), height
alignMove the art to an edge or corneranchor, pad?
nudgeMove or turn the artdx, dy (−100 to 100), turn (degrees)
shrinkShrink compensation for the materialwidth_pct, height_pct? (−50 to 50)
cropClip the art to the pagenone
pages, delete_pagesKeep or delete pagespages like 1,3-5 or 2-last
stepStep and repeat on one pagerows, cols (1 to 50, at least 2 copies), gap?, margin?, turn? (none, rows, cols, checker), bleed_into_gap?
marksCrop and registration marksset: any of crop, reg, center, bleed; bleed?
keylineDielineink? (CutContour), box? (artboard, ink, bleed, trim), weight_pt?, inset?, radius?, dashed?
edgeBorder round the designink? (White), width_pt? (0.25 to 72, default 3), join? (round, miter, bevel), above?
backerBacker plate under the artink? (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_textTurn text into outlinesnone
retypeReplace traced lettering with live text (one page; the same settings as retype/try)region, text, font, size, tracking, align, ink
expand_strokesTurn strokes into filled shapeshairline_pt? (default 0.25)
spot_to_processConvert spot inks to CMYKnames? (default every spot ink)
convert_cmykConvert RGB to CMYKtac? (total ink limit, 100 to 400)
overprintSet overprinton (true or false)
remove_colorRemove inksnames (inks the file has)
simplifySimplify curvestolerance_pt? (0.05 to 5, default 0.35)
trapTrapwidth_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:

SettingDescription
modecolor (default), grayscale or bw.
colorsauto (default) or 1 to 16 inks to find (1 clusters everything printed into one spot).
inksThe inks to use instead of finding them: [{"name", "c", "m", "y", "k", "rgb": "RRGGBB"}], up to 16.
grays, thresholdGray levels for grayscale (2 to 32); the black/white cut for bw (0 to 255).
paths, corners, noisePath detail and corner sharpness (0 to 100); specks smaller than noise pixels are dropped (1 to 100).
ignore_white, smooth, methodLeave white unprinted (default true); smooth edges; shapes abutting (default) or overlapping.
width_inOutput 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/actionsThe workspace’s actions, by name.
POST/v1/actionsSave {"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/booksThe books: [{name, shared, count}], own books first.
GET/v1/colors/books/aseOne 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/colorsOne book’s colors (?book=), or colors matching ?q= across every book.
POST/v1/colorsAdd {"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/importImport 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/booksRemove 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/impositionsList, summary form. ?status=. Paginated.
POST/v1/impositionsStart an imposition.
GET/v1/impositions/{id}Full form: items, strategies, layout, review and output.
POST/v1/impositions/{id}/strategyPick a strategy: {"strategy": "fewest_sheets"}. Replaces any edits.
POST/v1/impositions/{id}/repackWork a finished auto imposition out again with changed sheets, options or per-item items overrides. Goes back to packing.
PATCH/v1/impositions/{id}/layoutSave edited sheets: {"runs": [...]}, up to 200. The answer carries the review.
POST/v1/impositions/{id}/approveApprove and compose. Blocking problems answer 400 with what to fix.
POST/v1/impositions/{id}/cancelCancel; items go back to the inbox as waiting.
POST/v1/impositions

Auto answers 201 in packing; the strategies fill in when the fleet finishes, so poll GET /v1/impositions/{id}. Manual answers 201 in ready.

FieldTypeDescription
items requiredarrayQueue item ids from the Imposition inbox, waiting and measured. Up to 400.
modestringauto (default), manual, roll, nest or fixed (a layout you send whole; see below).
sheetsarrayAuto: sheets to consider, up to 8. Each is a saved sheet id or a preset key such as digital-12x18.
sheetstringManual: the one sheet to lay out on.
optionsobjectmax_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).
overridesobjectAuto: by item id, {quantity, price, rotate: "any" | "none", group}.
namestringOptional 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.

FieldTypeDescription
sheet requiredobject{w, h} in inches (1 to 120), optional rotation (0, 90, 180, 270) and mirror (boolean) applied to the finished sheet.
items requiredarrayUp 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.
marksarray{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).
passesarrayPrint 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.
metadataobjectSlug 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.
namestringOptional 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/sheetsYour sheets, plus the presets.
POST/v1/sheetsSave 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/layoutDry-run a layout on a sheet without saving anything.

Preset keys: digital-12x18, digital-13x19, tabloid-11x17, letter, offset-20x29, transfer-13x19.

POST/v1/sheets
FieldTypeDescription
namestringUp to 120 characters. Required unless from gives one.
fromstringA preset key to start from; its spec, cost and name fill whatever you leave out.
specobjectInches. 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.
costobjectUS 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.

POST/v1/sheets/layout

Lays sample pieces out on a sheet and returns the cells, printable area and mark positions. Nothing is saved.

FieldTypeDescription
spec or sheetobject | stringA spec to try, or a saved sheet id or preset key.
itemsarrayUp to 20 pieces: {w, h, trim_w, trim_h, copies} in inches. Default: one 3.75 × 2.25 in piece.
inksarrayInk 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/invoicesList, newest first.
GET/v1/invoices/{id}One invoice with memo, line_items and transactions.
ParameterTypeDescription
statusstringopen, paid, void or partially_paid.
limit, afterinteger, stringPaging, 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/transactionsList, newest first.
GET/v1/transactions/{id}One payment.
ParameterTypeDescription
statusstringauthorized, captured, refunded or failed.
methodstringcard, 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-endpointsList.
POST/v1/webhook-endpointsAdd {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}/secretReplace the signing secret; the new one is returned once and signs every delivery from now on, retries included.
POST/v1/webhook-endpoints/{id}/testSend a test event to this endpoint only, whatever it subscribes to. 202 with the delivery; 409 while paused.
GET/v1/webhook-endpoints/{id}/deliveriesDeliveries, newest first. ?status= pending, delivered or failed. Paginated.
POST/v1/webhook-endpoints/{id}/resendSend {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.

EventSent whendata.object
template.readyA 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.failedA template could not be read; error says why.
template.deletedA template was deleted.
batch.startedA 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.doneOne row of a batch rendered; file is its PDF.
batch.row.failedOne row of a batch failed; error says why.
batch.doneA batch finished (some rows may have failed: see failed).
batch.failedEvery row of a batch failed.
batch.canceledA batch was canceled.
job.doneA render job finished.The job as GET /v1/jobs/{id} returns it, with its files.
job.failedA render job failed.
inbox.item.addedA 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.movedA file was sent from one inbox to the other.
inbox.item.plannedA file was placed on an imposition.
inbox.item.imposedA file was printed on an approved imposition.
inbox.item.removedA file was removed from an inbox.
inbox.item.restoredA removed file was put back in its inbox.
inbox.item.doneA Prepress file was marked done (by hand or an action's then); file is the finished PDF.The inbox item, as above.
inbox.item.reopenedA done Prepress file was put back in the inbox.
prepress.action.doneA prepress action finished on a file.The inbox item, plus action: {run, name} and, on failure, reason (or step).
prepress.action.failedA prepress action stopped on a file; reason says why.
cut.doneA 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.readyAn 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.approvedAn imposition was approved; its sheets are being made.
imposition.doneEvery sheet of an imposition is made.
imposition.failedAn imposition could not be laid out or made.
imposition.canceledAn imposition was canceled.
member.invitedSomeone was invited to the workspace.{id, object: "member", name, email, role, status, joined_at}; status is invited, active or removed.
member.joinedAn invited person accepted and joined.
member.role_changedA member became an admin or a member.
member.removedA member was removed.
license.addedA 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.changedA license was upgraded, renewed or set to keep renewing.
license.endedA license was set to end, ended, or lapsed unpaid.
limit.reachedWork was refused because a license limit was reached (sent at most once a day per limit).
invoice.createdAn 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.paidAn invoice was paid in full.
invoice.voidedAn invoice was voided.
transaction.capturedA payment went through.
transaction.failedA payment failed.
transaction.refundedA 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:

HeaderValue
Content-Typeapplication/json
X-Web2Print-Signaturesha256= followed by the hex HMAC-SHA256 of the raw body, keyed with the endpoint's secret.
X-Web2Print-EventThe event type, the same as type in the body.
User-Agentweb2print-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.

GET/v1/events
ParameterTypeDescription
afterstringAn event id: return the events after it. An id older than 30 days answers 400; start again with since.
sincestringISO-8601 time, used when after is not given. Default: 24 hours ago.
typestringComma-separated types; inbox.* takes a whole group.
limitinteger1 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/pairingsMake a one-time code, good for 10 minutes. Studio sign-in only; API keys and paired computers get 403.
POST/v1/desktop/pairNo 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/devicesPaired 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/studioPaired 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.