UK print on demand from independent printershello@weprintlocal.co.uk · Check my application
weprintlocal API · v1

Print on demand, as an API.

Quote exact prices, upload artwork, place orders and follow them from a local press to your customer's door. REST and JSON, documented with OpenAPI, with a sandbox that behaves like production.

Test keys, real behaviour

wpl_test_ keys give you a full sandbox: orders are priced, checked and tracked, but never charged or printed.

Simulate production

POST /v1/sandbox/jobs/{ref}/advance moves a test order to InProduction, Shipped or Delivered, with a tracking number.

Bring your own orders

CSV upload, Shopify and WooCommerce webhooks, or any JSON with a field mapping. Every inbound order is logged and replayable.

Manage every order

Hold, release, re-address or cancel before production; raise a problem with the printer after despatch.

Signed webhooks

Order events delivered to your URL with X-WPL-Signature: t=…,v1=HMAC-SHA256. Check the signature before acting on an event.

Already approved? Create your keys under Account → Developers in your store. The same guide appears there with your own products filled in.

Quick start

Base address: https://api.weprintlocal.co.uk/v1. Authenticate with Authorization: Bearer wpl_test_… while you build: test orders behave like real ones but are never charged or printed. Switch to a wpl_live_ key once live trading is enabled.

1. Set your key

Keep it on your server only, never in a web page or mobile app. wpl_test_ keys work in the sandbox; wpl_live_ keys place real orders.

Terminal
export WPL_API_KEY="wpl_test_…"

2. List your products

Everything your store can sell, with its options and the artwork files each needs (requiredFiles).

Terminal
curl https://api.weprintlocal.co.uk/v1/products -H "Authorization: Bearer $WPL_API_KEY"

3. Get an exact price

Quotes are fully priced (items, delivery, VAT), sealed and held for a while. Order against the quoteId.

Terminal
curl -X POST https://api.weprintlocal.co.uk/v1/quotes -H "Authorization: Bearer $WPL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "productCode": "SQ-HARDBACK-BOOK",
  "quantity": 1,
  "options": {
    "pages": "24"
  },
  "deliveryServiceCode": "STANDARD"
}'

4. Upload artwork

Ask for an upload grant for each file the product needs (Cover, Text), then stream the file with the grant in X-Upload-Token.

Terminal
curl -X POST https://api.weprintlocal.co.uk/v1/files/uploads -H "Authorization: Bearer $WPL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"role":"Cover","fileName":"cover.pdf","contentType":"application/pdf","sizeBytes":123456}'
# → { "fileId": "…", "uploadToken": "…", "uploadPath": "/v1/files/uploads/…/content" }

curl -X PUT "https://api.weprintlocal.co.uk$UPLOAD_PATH" -H "Authorization: Bearer $WPL_API_KEY" \
  -H "X-Upload-Token: $UPLOAD_TOKEN" -H "Content-Type: application/pdf" --data-binary @cover.pdf

5. Place the order

Order quotes directly with their artwork by role. Send your own reference and an Idempotency-Key so a retry never creates a second order. We deliver within the UK only: countryCode must be GB and postcodes are checked and tidied (an invalid one is a 422 NOT_UK_ADDRESS). Add "draft": true to create the order on hold: it is checked and priced but not sent to a printer until you release it.

Terminal
curl -X POST https://api.weprintlocal.co.uk/v1/orders -H "Authorization: Bearer $WPL_API_KEY" \
  -H "Idempotency-Key: my-order-1001" -H "Content-Type: application/json" \
  -d '{"externalReference":"1001",
       "lines":[{"quoteId":"$QUOTE_ID","files":{ "Cover": "$COVER_FILE_ID", "Text": "$TEXT_FILE_ID" }}],
       "customer":{"name":"Jo Bloggs","email":"jo@example.com"},
       "deliveryAddress":{"name":"Jo Bloggs","line1":"1 High St","city":"Leeds","postcode":"LS1 1AA","countryCode":"GB"}}'

6. Track it

Status, timeline and, once despatched, the carrier and tracking number on each line. Statuses: AwaitingArtwork, OnHold, Accepted, InProduction, Shipped, Delivered. Each order also has dispatchBy, the UK working day (YYYY-MM-DD) it must be despatched by, and slaStatus (OnTrack, DueToday, Late, Met or Missed). Search and filter the list (q, status, channel, from, to, onHold, tag, sla=Late|DueToday|OnTrack), or download the same selection as CSV.

Terminal
curl https://api.weprintlocal.co.uk/v1/orders/$ORDER_REFERENCE -H "Authorization: Bearer $WPL_API_KEY"
curl "https://api.weprintlocal.co.uk/v1/orders?page=1&status=Shipped&q=LS1" -H "Authorization: Bearer $WPL_API_KEY"
curl "https://api.weprintlocal.co.uk/v1/orders?sla=Late&tag=rush" -H "Authorization: Bearer $WPL_API_KEY"
curl "https://api.weprintlocal.co.uk/v1/orders/export?from=2026-11-01&to=2026-12-01&sla=Late" -H "Authorization: Bearer $WPL_API_KEY" -o orders.csv

Sandbox: move a test order through production

Test-key orders never reach a printer, so you move them along yourself to exercise your status handling, webhooks and tracking. Pass an order reference (advances every job on it) or a job reference.

Terminal
curl -X POST https://api.weprintlocal.co.uk/v1/sandbox/jobs/$ORDER_REFERENCE/advance -H "Authorization: Bearer $WPL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"to":"Shipped","trackingNumber":"TEST123"}'   # InProduction | Shipped | Delivered

Choose the printer

Send printerCode (for example LEEDS-PRESS) on the order, or on a line to override it, and that printer gets the job. Leave it out and we send it to a printer near the delivery address. A printer that isn't on the network or doesn't make the product is a 422 PRINTER_UNAVAILABLE. If your printer declines, nothing goes to anyone else: you get order.printer_declined, then choose again with PUT /v1/orders/{ref}/lines/{lineNo}/printer (a code, or null to let us choose). CSV orders use a printer_code column; Shopify and WooCommerce use a line property or meta key named printer.

Manage an order

Until a printer accepts an order you can hold, release (this also confirms a draft), re-address or cancel it. Once it is in production these answer 409 (ORDER_IN_PRODUCTION, ADDRESS_LOCKED, ORDER_SHIPPED); the order's can object says what is allowed now. Within 30 days of despatch you can report a problem with a line (Damaged, Misprinted, WrongItem, MissingItems, LostInTransit, Other). The printer decides on a reprint or refund; notes on the order carry the conversation.

Terminal
curl -X POST https://api.weprintlocal.co.uk/v1/orders/$ORDER_REFERENCE/hold -H "Authorization: Bearer $WPL_API_KEY" -H "Content-Type: application/json" -d '{"reason":"Checking stock"}'
curl -X POST https://api.weprintlocal.co.uk/v1/orders/$ORDER_REFERENCE/release -H "Authorization: Bearer $WPL_API_KEY"
curl -X PUT https://api.weprintlocal.co.uk/v1/orders/$ORDER_REFERENCE/delivery-address -H "Authorization: Bearer $WPL_API_KEY" -H "Content-Type: application/json" \
  -d '{"name":"Jo Bloggs","line1":"2 Park Row","city":"Leeds","postcode":"LS1 5HD","countryCode":"GB"}'
curl -X POST https://api.weprintlocal.co.uk/v1/orders/$ORDER_REFERENCE/cancel -H "Authorization: Bearer $WPL_API_KEY" -H "Content-Type: application/json" -d '{"reason":"Customer changed their mind"}'
curl -X POST https://api.weprintlocal.co.uk/v1/orders/$ORDER_REFERENCE/issues -H "Authorization: Bearer $WPL_API_KEY" -H "Content-Type: application/json" \
  -d '{"lineNo":1,"kind":"Damaged","description":"Cover creased on arrival","affectedQuantity":1}'
curl -X POST https://api.weprintlocal.co.uk/v1/orders/$ORDER_REFERENCE/notes -H "Authorization: Bearer $WPL_API_KEY" -H "Content-Type: application/json" -d '{"body":"Gift: no prices in the parcel"}'
curl https://api.weprintlocal.co.uk/v1/orders/$ORDER_REFERENCE/activity -H "Authorization: Bearer $WPL_API_KEY"

Order desk: dispatch-by, tags, items, duplicates, artwork and saved views

Each order has a dispatchBy day (UK working days) and a slaStatus: OnTrack, DueToday or Late while it is open, then Met or Missed once despatched. Filter the list and the CSV export with sla=Late and tag=rush. Tags are your own short labels; sending tags replaces them all (an invalid tag is a 422). You can cancel one item until a printer starts it (can.cancelItems); after that it is a 409 ITEM_IN_PRODUCTION or ITEM_SHIPPED. Nothing is refunded automatically, and cancelling the last item cancels the order. A duplicate (can.duplicate) is a new order on your account, not charged to a card, with duplicatedFrom set; it starts on hold so you can check it, then release it. Saved views are shared with your team or private to one user.

Terminal
# Tags: replace the whole set (letters, numbers and - _ . : up to 40 characters; at most 20)
curl -X PUT https://api.weprintlocal.co.uk/v1/orders/$ORDER_REFERENCE/tags -H "Authorization: Bearer $WPL_API_KEY" -H "Content-Type: application/json" -d '{"tags":["rush","school:leeds"]}'

# Cancel one item (line 2) before a printer starts it; the rest of the order carries on
curl -X POST https://api.weprintlocal.co.uk/v1/orders/$ORDER_REFERENCE/lines/2/cancel -H "Authorization: Bearer $WPL_API_KEY" -H "Content-Type: application/json" -d '{"reason":"Customer no longer needs it"}'

# Duplicate: same items, artwork and address as a new order on account at today's prices; starts on hold unless "hold": false
curl -X POST https://api.weprintlocal.co.uk/v1/orders/$ORDER_REFERENCE/duplicate -H "Authorization: Bearer $WPL_API_KEY" -H "Content-Type: application/json" -d '{"hold":true}'

# Download one of the order's artwork files (fileId from lines[].files)
curl https://api.weprintlocal.co.uk/v1/orders/$ORDER_REFERENCE/files/$FILE_ID -H "Authorization: Bearer $WPL_API_KEY" -o artwork.pdf

# Saved views of the orders list (filters are the list's query parameters)
curl https://api.weprintlocal.co.uk/v1/order-views -H "Authorization: Bearer $WPL_API_KEY"
curl -X POST https://api.weprintlocal.co.uk/v1/order-views -H "Authorization: Bearer $WPL_API_KEY" -H "Content-Type: application/json" \
  -d '{"name":"Late rush orders","shared":true,"filters":{"sla":"Late","tag":"rush"}}'
curl -X DELETE https://api.weprintlocal.co.uk/v1/order-views/$VIEW_ID -H "Authorization: Bearer $WPL_API_KEY"

Order intake: CSV, Shopify, WooCommerce and any JSON

Already taking orders somewhere else? Map your SKUs to our products once (POST /v1/intake/sku-mappings, or a CSV with columns sku, product_code, opt:<option>), then send orders in. Each inbound order is logged with any issues and can be replayed after you fix a mapping. Connections for Shopify, WooCommerce and mapped JSON give you a webhook URL to paste into that platform — manage them under Account → Integrations.

CSV upload
curl -X POST https://api.weprintlocal.co.uk/v1/intake/csv -H "Authorization: Bearer $WPL_API_KEY" -H "Content-Type: text/csv" --data-binary @orders.csv

# orders.csv — rows sharing order_ref form one order
order_ref,customer_name,customer_email,address1,city,postcode,country,sku,quantity,artwork_url
1001,Jo Bloggs,jo@example.com,1 High St,Leeds,LS1 1AA,GB,BOOK-21,1,https://example.com/jo.pdf
Canonical JSON
curl -X POST https://api.weprintlocal.co.uk/v1/intake/orders -H "Authorization: Bearer $WPL_API_KEY" -H "Content-Type: application/json" \
  -d '[{"externalReference":"1002","customer":{"name":"Jo Bloggs","email":"jo@example.com"},
        "delivery":{"name":"Jo Bloggs","line1":"1 High St","city":"Leeds","postcode":"LS1 1AA","countryCode":"GB"},
        "lines":[{"sku":"BOOK-21","quantity":1,"artwork":[{"role":"Cover","url":"https://example.com/art.pdf"}]}]}]'

Webhooks

Subscribe a public https:// URL to order events under Account → Webhooks & postbacks or POST /v1/webhooks. The signing secret is shown once. Every delivery carries X-WPL-Signature: t=<unix>,v1=<hex> — an HMAC-SHA256 of t + "." + body.

order.receivedWe have the order: checked, priced and given its reference and dispatch-by day. Sent once, when the order is created.
order.status_changedEvery time the order moves to a new status. Subscribe to this alone if you only want one event. (Not sent to postback URLs, which get the specific events instead.)
order.acceptedPaid and artwork checked; a local printer has been chosen.
order.in_productionThe printer has started making it.
order.item_status_changedOne item moved on: accepted, in production, shipped (with tracking), delivered, cancelled or failed. Also sent for reprints.
order.delayedThe printer told us an item will be late, with the reason and the new dispatch-by day.
order.printer_declinedThe printer you named can’t take an item. Nothing goes to another printer: choose another printer for it, or let us choose one near the delivery address.
order.shippedDespatched, with carrier and tracking number per line.
order.deliveredThe carrier confirmed delivery.
order.cancelledCancelled by you, the customer or our team. Nothing is refunded automatically.
order.artwork_failedA file failed our print checks and needs replacing.
order.failedSomething went wrong that our team is looking at.
order.on_holdHeld by you, by a connection set to hold new orders, or created as an API draft. Nothing is printed until it is released.
order.address_updatedThe delivery address was changed before production; the order may move to a different local printer.
order.refundedMoney went back to the card, or a credit was added to your next invoice.
order.issue_updatedA problem with a dispatched item was reported, or the printer answered it (a reprint, a refund they paid you directly, or a reply).
Node.js — verify a webhook
import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(rawBody, header, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return parts.v1?.length === expected.length &&
    timingSafeEqual(Buffer.from(parts.v1, "hex"), Buffer.from(expected, "hex"));
}

Postbacks

A postback URL gets every event about an order without subscribing to events — all of the ones above except order.status_changed. Set a default under Account → Webhooks & postbacks (or PUT /v1/postback-settings), give a sales-channel connection its own, or send postbackUrl with an API order to override both for that order. It must be a public https:// address (anything else is a 422). Postbacks are signed with your store's postback secret, which is separate from each webhook subscription's secret; rotate it with POST /v1/postback-settings/rotate-secret (the new secret is shown once).

Order with its own postback URL
curl -X POST https://api.weprintlocal.co.uk/v1/orders -H "Authorization: Bearer $WPL_API_KEY" \
  -H "Idempotency-Key: my-order-1001" -H "Content-Type: application/json" \
  -d '{"externalReference":"1001","postbackUrl":"https://example.com/weprintlocal/postbacks",
       "lines":[{"quoteId":"$QUOTE_ID","externalLineId":"A-1","files":{ "Cover": "$COVER_FILE_ID", "Text": "$TEXT_FILE_ID" }}],
       "customer":{"name":"Jo Bloggs","email":"jo@example.com"},
       "deliveryAddress":{"name":"Jo Bloggs","line1":"1 High St","city":"Leeds","postcode":"LS1 1AA","countryCode":"GB"}}'

Every body is { id, event, createdUtc, data }. The id is unique to that delivery and stays the same on every retry and when you press "Send again", so store the ids you have handled and ignore repeats. The headers are X-WPL-Event, X-WPL-Delivery (the same id) and X-WPL-Signature. Events may arrive out of order after a retry: use createdUtc and the status in data, not arrival order.

order.received — Sent once, as soon as the order is created: its reference, the channel it came through and the day it must be dispatched by. externalLineId is your own line id, if you sent one.

order.received (JSON)
{
  "id": "5b0f6a5e-3c1d-4f7a-9d1e-2a6c8b7e4f10",
  "event": "order.received",
  "createdUtc": "2026-11-10T09:14:03.120Z",
  "data": {
    "orderReference": "WPL-7Q2M9X4KCB",
    "externalReference": "1001",
    "isTest": false,
    "channel": "API",
    "dispatchBy": "2026-11-13",
    "customerName": "Jo Bloggs",
    "items": [
      {
        "lineNo": 1,
        "externalLineId": "A-1",
        "quoteId": "3f7e2a10-8c4b-4e0e-9a51-7d2b6c1e9f33"
      }
    ]
  }
}

order.item_status_changed — One item moved on. status is accepted, in_production, shipped, delivered, cancelled or failed; reprint is true when the item is a reprint; shipment is filled in once the item has shipped (otherwise null).

order.item_status_changed (JSON)
{
  "id": "c2d4e6f8-1a3b-4c5d-8e7f-9a0b1c2d3e4f",
  "event": "order.item_status_changed",
  "createdUtc": "2026-11-12T15:42:10.004Z",
  "data": {
    "orderReference": "WPL-7Q2M9X4KCB",
    "externalReference": "1001",
    "isTest": false,
    "item": {
      "lineNo": 1,
      "externalLineId": "A-1",
      "productCode": "SQ-HARDBACK-BOOK",
      "quantity": 2,
      "status": "shipped",
      "reprint": false,
      "shipment": {
        "carrier": "Royal Mail",
        "trackingNumber": "RM123456785GB",
        "trackingUrl": "https://www.royalmail.com/track-your-item#/tracking-results/RM123456785GB",
        "shippedUtc": "2026-11-12T15:41:57.000Z"
      }
    }
  }
}

order.shipped (with shipments) — The whole order has been despatched. shipments has one entry per item with its carrier and tracking. Status events (accepted, in_production, shipped, delivered, cancelled …) all have this shape.

order.shipped (JSON)
{
  "id": "9e8d7c6b-5a4f-4e3d-a2c1-b0a9f8e7d6c5",
  "event": "order.shipped",
  "createdUtc": "2026-11-12T15:42:11.310Z",
  "data": {
    "orderReference": "WPL-7Q2M9X4KCB",
    "externalReference": "1001",
    "isTest": false,
    "previousStatus": "InProduction",
    "status": "Shipped",
    "paymentStatus": "Paid",
    "shipments": [
      {
        "lineNo": 1,
        "externalLineId": "A-1",
        "carrier": "Royal Mail",
        "trackingNumber": "RM123456785GB",
        "trackingUrl": "https://www.royalmail.com/track-your-item#/tracking-results/RM123456785GB",
        "shippedUtc": "2026-11-12T15:41:57.000Z"
      },
      {
        "lineNo": 2,
        "externalLineId": "A-2",
        "carrier": "DPD",
        "trackingNumber": "15501234567890",
        "trackingUrl": null,
        "shippedUtc": "2026-11-12T15:40:02.000Z"
      }
    ],
    "updatedUtc": "2026-11-12T15:42:11.000Z"
  }
}

order.delayed — The printer told us an item will be late. cause is their reason in plain words; dispatchBy is the new day it should be dispatched by.

order.delayed (JSON)
{
  "id": "1f2e3d4c-5b6a-4798-8a7b-6c5d4e3f2a1b",
  "event": "order.delayed",
  "createdUtc": "2026-11-11T11:05:44.870Z",
  "data": {
    "orderReference": "WPL-7Q2M9X4KCB",
    "externalReference": "1001",
    "isTest": false,
    "item": {
      "lineNo": 1,
      "externalLineId": "A-1"
    },
    "cause": "Waiting for cover board stock",
    "dispatchBy": "2026-11-17"
  }
}

Verify the signature. X-WPL-Signature: t=<unix>,v1=<hex> where v1 is the HMAC-SHA256 of t + "." + body with the secret. Check it against the raw body, and reject old timestamps.

Node.js — verify and de-duplicate
import { createHmac, timingSafeEqual } from "node:crypto";

// Use the raw request body (exactly the bytes received), not re-serialised JSON.
export function verify(rawBody, header, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return parts.v1?.length === expected.length &&
    timingSafeEqual(Buffer.from(parts.v1, "hex"), Buffer.from(expected, "hex"));
}

// Express: app.post("/postbacks", express.raw({ type: "application/json" }), (req, res) => {
//   if (!verify(req.body.toString("utf8"), req.get("x-wpl-signature") ?? "", process.env.WPL_POSTBACK_SECRET)) return res.sendStatus(401);
//   const event = JSON.parse(req.body);
//   if (alreadySeen(event.id)) return res.sendStatus(200);   // a retry or "Send again": ignore
//   remember(event.id); handle(event); res.sendStatus(200);
// });
PHP — verify and de-duplicate
<?php
function wpl_verify(string $rawBody, string $header, string $secret, int $tolerance = 300): bool {
    $parts = [];
    foreach (explode(',', $header) as $pair) {
        [$k, $v] = array_pad(explode('=', $pair, 2), 2, '');
        $parts[$k] = $v;
    }
    $t = (int)($parts['t'] ?? 0);
    if ($t === 0 || abs(time() - $t) > $tolerance) return false;
    $expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret);
    return hash_equals($expected, $parts['v1'] ?? '');
}

$raw = file_get_contents('php://input');
if (!wpl_verify($raw, $_SERVER['HTTP_X_WPL_SIGNATURE'] ?? '', getenv('WPL_POSTBACK_SECRET'))) {
    http_response_code(401);
    exit;
}
$event = json_decode($raw, true);
// De-duplicate on $event['id'], then handle $event['event'] and $event['data'].
http_response_code(200);

Retries. Answer with any 2xx within 10 seconds. Anything else (or no answer) is tried again after 30 seconds, then 1, 2, 4, 8 minutes and so on, doubling up to at most 6 hours apart, for up to 7 days. After that the delivery is marked Failed. A delivery to a webhook you paused or deleted is Dropped. Any delivery can be sent again.

Delivery log. Every webhook and postback we have sent, newest first, with each attempt (time, status code, error, time taken) and the exact body. Filter by status (Pending, Retrying, Delivered, Failed, Dropped), event and orderReference; page and pageSize (up to 100) page through it.

Terminal
curl "https://api.weprintlocal.co.uk/v1/webhook-deliveries?status=Failed&pageSize=20" -H "Authorization: Bearer $WPL_API_KEY"
curl "https://api.weprintlocal.co.uk/v1/webhook-deliveries?orderReference=$ORDER_REFERENCE" -H "Authorization: Bearer $WPL_API_KEY"

# Send one again now (same body and id, freshly signed) → 202
curl -X POST https://api.weprintlocal.co.uk/v1/webhook-deliveries/$DELIVERY_ID/redeliver -H "Authorization: Bearer $WPL_API_KEY"

# Default postback URL (null clears it) and its signing secret
curl https://api.weprintlocal.co.uk/v1/postback-settings -H "Authorization: Bearer $WPL_API_KEY"
curl -X PUT https://api.weprintlocal.co.uk/v1/postback-settings -H "Authorization: Bearer $WPL_API_KEY" -H "Content-Type: application/json" -d '{"url":"https://example.com/weprintlocal/postbacks"}'
curl -X POST https://api.weprintlocal.co.uk/v1/postback-settings/rotate-secret -H "Authorization: Bearer $WPL_API_KEY"

JavaScript

Node.js / server-side
const res = await fetch("https://api.weprintlocal.co.uk/v1/quotes", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.WPL_API_KEY}`,
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    "productCode": "SQ-HARDBACK-BOOK",
    "quantity": 1,
    "options": {
      "pages": "24"
    },
    "deliveryServiceCode": "STANDARD"
  })
});
const quote = await res.json();
console.log(quote.total, quote.currency);

Full reference

Every endpoint, field and error is described in the OpenAPI document — import it into Postman or generate a client: download openapi.json. Errors always return { code, message, fieldErrors, correlationId } — quote the correlationId if you contact us.

API reference

Merchant keys (wpl_test_/wpl_live_) call the store, quote, file, order, sandbox, intake and webhook endpoints. Printers in our network use a wpp_live_ key for the job endpoints.

Store

GET /v1/storeThe store named by the caller (theme, currency, support contact)
GET /v1/productsProducts this store can sell, with availability from its price books
GET /v1/products/{productCode}A product with its options and delivery services
GET /v1/storefront/hosts/{host}The store served on a custom domain (storefront only)

Quotes

POST /v1/quotesPrice a configuration. Quotes are immutable and expire; add one to a basket or order to buy it.
GET /v1/quotes/{quoteId}A quote issued to this store

Baskets

POST /v1/basketsCreate a basket. The returned token is its only credential: keep it server-side (e.g. an HttpOnly cookie) and send it as X-Basket-Token.
GET /v1/baskets/{basketId}The basket with totals, per-item issues and whether it can be checked out.
POST /v1/baskets/{basketId}/itemsAdd a quote to the basket, optionally with its artwork.
PUT /v1/baskets/{basketId}/items/{itemId}/filesAttach (or replace) artwork on a basket item.
POST /v1/baskets/{basketId}/items/{itemId}/requoteRe-price an item (e.g. after its quote expired) with the same configuration; artwork is kept.
DELETE /v1/baskets/{basketId}/items/{itemId}Remove an item from the basket.

Files

POST /v1/files/uploadsStep 1: request a short-lived upload grant for one artwork file (role as the product defines, e.g. Cover, Text, Artwork).
PUT /v1/files/uploads/{fileId}/contentStep 2: stream the file bytes (Content-Type application/pdf, image/png, image/jpeg or application/octet-stream) with the grant in X-Upload-Token.
GET /v1/files/{fileId}Upload and artwork-check status of a file.

Orders

GET /v1/ordersYour store's orders, newest first, with search and filters (merchant key, or signed-in store staff).
POST /v1/ordersPlace an order from a ready basket, or directly from quotes (API). Repeats with the same basket, Idempotency-Key or externalReference return the existing order with 200.
GET /v1/orders/exportDownload your orders as CSV (same filters as the list; up to 5,000 rows, one row per line).
GET /v1/orders/{orderReference}Order status, artwork, timeline, shipment tracking, issues and refunds. Shoppers (via the store) send X-Order-Token or X-Basket-Token.
POST /v1/orders/{orderReference}/cancelCancel an order before any printer has accepted it. Nothing is refunded automatically: refunds are agreed between you and the printer. Shoppers can only cancel unpaid orders.
POST /v1/orders/{orderReference}/holdPut an order on hold: nothing goes to a printer until you release it. Only before a printer accepts it.
POST /v1/orders/{orderReference}/releaseRelease a held order or confirm a draft, so it goes on to production.
PUT /v1/orders/{orderReference}/delivery-addressChange the delivery address (UK only) until a printer accepts the order. The order is re-routed to the best printer for the new address.
GET /v1/orders/{orderReference}/issuesProblems reported on this order and how they were resolved
POST /v1/orders/{orderReference}/issuesReport a problem with a dispatched item (within 30 days): damaged, misprinted, wrong item, missing items or lost in transit. The printer who made it sees the report and answers it (a reprint, a refund or a reply); we carry the messages.
GET /v1/orders/{orderReference}/notesNotes on the order shared between your team and ours
POST /v1/orders/{orderReference}/notesAdd a note to the order (visible to your team and ours)
GET /v1/orders/{orderReference}/activityEverything that happened to the order, newest first
PUT /v1/orders/{orderReference}/tagsReplace the order's tags (short labels to group and find orders)
POST /v1/orders/{orderReference}/lines/{lineNo}/cancelCancel one item before a printer has started it; the rest of the order carries on. Nothing is refunded automatically.
PUT /v1/orders/{orderReference}/lines/{lineNo}/printerChoose the printer for one item (printerCode), or send null to let us choose one near the delivery address. Use it when the printer you named declined (order.printer_declined), or before a printer has the job.
POST /v1/orders/{orderReference}/duplicatePlace the same items, artwork and address again as a new order, at today's prices (on account). hold: start it on hold to check first.
GET /v1/orders/{orderReference}/files/{fileId}Download one of the order's artwork files
GET /v1/order-viewsSaved order views for your store
POST /v1/order-viewsSave the current filters as a named view
DELETE /v1/order-views/{id}Delete a saved view

Sandbox

POST /v1/sandbox/jobs/{jobReference}/advanceAdvance a sandbox job (test keys only) to InProduction, Shipped or Delivered

Intake

POST /v1/intake/ordersSubmit orders in the canonical format; each is accepted, rejected (with issues) or a duplicate
POST /v1/intake/csvUpload a CSV of orders (one row per line; rows sharing order_ref form one order). Columns: order_ref, customer_name, customer_email, phone, address1, address2, city, region, postcode, country, product_code | sku, quantity, delivery_service, artwork_url, cover_url, opt:<option>…
GET /v1/intake/recordsRecent inbound orders and what happened to them (newest first)
GET /v1/intake/records/{recordId}One inbound order with its original payload
POST /v1/intake/records/{recordId}/replayProcess a rejected order again (after fixing SKU mappings, products or the connection)
GET /v1/intake/connectionsYour sales-channel connections
POST /v1/intake/connectionsConnect a Shopify shop, WooCommerce site or any JSON source; returns its inbound webhook URL
PATCH /v1/intake/connections/{connectionId}Rename, reconfigure or switch off a connection
DELETE /v1/intake/connections/{connectionId}Disconnect (the URL stops accepting orders; history is kept)
PUT /v1/intake/connections/{connectionId}/secretSet or rotate the signing secret (paste Shopify's / WooCommerce's, or omit `secret` to generate one for JSON sources — shown once)
GET /v1/intake/sku-mappingsYour SKU → product mappings
POST /v1/intake/sku-mappingsMap a marketplace SKU to a product and fixed options
PATCH /v1/intake/sku-mappings/{mappingId}Change a SKU mapping
DELETE /v1/intake/sku-mappings/{mappingId}Remove a SKU mapping
POST /v1/intake/sku-mappings/importImport SKU mappings from CSV (columns: sku, product_code, opt:<option>…); existing SKUs are updated

Intake webhooks

POST /v1/intake/shopify/{token}Shopify orders/create webhook (X-Shopify-Hmac-Sha256)
POST /v1/intake/woocommerce/{token}WooCommerce order.created webhook (X-WC-Webhook-Signature)
POST /v1/intake/json/{token}Any JSON order, mapped by the connection’s field mapping (X-WPL-Signature when a secret is set)

Webhooks

GET /v1/webhooksYour webhook subscriptions.
POST /v1/webhooksSubscribe a URL to order events. The signing secret is returned once.
PATCH /v1/webhooks/{id}Change a subscription’s URL, events, or pause it.
DELETE /v1/webhooks/{id}Delete a subscription.
POST /v1/webhooks/{id}/rotate-secretIssue a new signing secret (returned once); the old one stops working immediately.
POST /v1/webhooks/{id}/pingSend a signed webhook.ping event to the subscription to test your endpoint.
GET /v1/webhook-deliveriesEvery webhook and postback sent for your store, newest first, with each attempt
POST /v1/webhook-deliveries/{id}/redeliverSend a delivery again now (same body and id, freshly signed)
GET /v1/postback-settingsWhere your order postbacks go and whether a signing secret exists
PUT /v1/postback-settingsSet (or clear) the default postback URL for your orders
POST /v1/postback-settings/rotate-secretIssue a new postback signing secret (returned once)

Printer portal

GET /v1/printer/meYour printer profile, capacity and status
PATCH /v1/printer/mePause or resume new offers, change capacity or contact details
GET /v1/printer/jobsYour jobs, newest first
GET /v1/printer/jobs/{jobReference}One of your jobs: options, artwork links, delivery, due date, history
GET /v1/printer/jobs/{jobReference}/files/{role}Download artwork for a job you hold (while offered or in progress)
GET /v1/printer/issuesProblems reported on items you made (open ones by default)
GET /v1/printer/jobs/{jobReference}/issuesProblems reported on this job
POST /v1/printer/jobs/{jobReference}/issues/{issueReference}/resolveAnswer a problem on your job: Reprint (a new job for the affected quantity, offered to you), Refund (you have refunded the merchant yourself; nothing is paid through weprintlocal) or Reject (with your reason). The merchant sees your note.
POST /v1/printer/jobs/{jobReference}/acceptAccept an offered job
POST /v1/printer/jobs/{jobReference}/rejectDecline an offer (or hand back an accepted job before production); it's re-routed
POST /v1/printer/jobs/{jobReference}/statusReport progress: InProduction, Shipped (tracking required unless untracked), Delivered or Failed