In five steps
- Ask the studio for a partner account: you receive a key and your terms — on account or prepaid, a credit limit, a discount.
- Upload each picture (
POST /uploads) and keep the id it answers. - Price the order (
POST /quote): each line, the shipping options, VAT, the total — and whether it would go straight into production. - Place it (
POST /orders) with your own reference and anIdempotency-Key. It is priced again here; sendexpected_totalso that a price that moved is refused, never charged. - Follow it on your webhook, or ask
GET /orders?updated_since=now and then.
A partner account, a key, a question: info@xyz-prints.com.
Authentication
Every call carries your partner key in the header Authorization: Bearer <key>. The key reaches this API only, and only your own orders and files. Keep it on your server — never in a browser or an app. The studio mints and revokes keys: ask for a new one if it leaks. It can also hold your key to the addresses your servers call from, so a key that leaks is worth nothing elsewhere.
curl \
-H "Authorization: Bearer $XYZ_KEY" \
https://www.xyz-prints.com/api/v1/partnerMoney is EUR with VAT included, as the storefront shows it; the rate follows the delivery country and your VAT number, checked with VIES. Prices are always computed here — your system never computes money.
Files
Send each picture once, as the multipart/form-data field file. Formats: tiff, jpg, png, pdf, psd, heic, bmp. Up to 2048 MB a file; a single request carries up to 8 MB, so send anything larger in chunks. A PDF gives one id per page. A file not ordered within 72 hours is removed.
curl \
-H "Authorization: Bearer $XYZ_KEY" \
-F "file=@harbour-at-dusk.tif" \
https://www.xyz-prints.com/api/v1/partner/uploads201 — Answer
{
"files": [
{
"id": "k3Fq9w2LxP0aZt7c",
"name": "harbour-at-dusk.tif",
"bytes": 88213504,
"width_px": 7200,
"height_px": 9000,
"dpi": 300,
"dpi_assumed": false,
"colour": {
"mode": "RGB",
"bit_depth": 16,
"profile": true,
"assigned": null
},
"expires_at": "2026-09-28T10:00:00.000Z"
}
]
}In chunks: cut the file into parts of one size (the last may be shorter) and POST each one as multipart with session (your id for the file: up to 30 letters, digits, - and _), index (from 0), total, chunk_size, size (the whole file, in bytes), name and chunk. Each part answers 202; the last answers 201 with the file.
The answer gives the picture’s pixels, the resolution it declares and its colour profile. The resolution on the print depends on the size ordered: the quote warns below 100 DPI, which the studio calls a fair print.
What can be ordered
GET /catalogue lists today’s ids for each kind of line. Below, the same list, read live.
curl \
-H "Authorization: Bearer $XYZ_KEY" \
https://www.xyz-prints.com/api/v1/partner/catalogueFine-art prints kind: "print"
5682 | Hahnemühle German Etching 310g | roll, up to 110 × 1200 cm · 310 g/m² · mounts: pvc-4mm-white, pvc-5mm-white, dibond-3mm, museum-board-1-6mm, museum-board-2-2mm, pvc-5mm-black |
5683 | Hahnemühle Museum Etching 350g | roll, up to 110 × 1200 cm · 350 g/m² · mounts: pvc-4mm-white, pvc-5mm-white, dibond-3mm, museum-board-1-6mm, museum-board-2-2mm, pvc-5mm-black |
5684 | Hahnemühle PhotoRag 308g | roll, up to 110 × 1200 cm · 308 g/m² · mounts: pvc-4mm-white, pvc-5mm-white, dibond-3mm, museum-board-1-6mm, museum-board-2-2mm, pvc-5mm-black |
5698 | Hahnemühle PhotoRag Baryta 315g | roll, up to 110 × 1200 cm · 315 g/m² · mounts: pvc-4mm-white, pvc-5mm-white, dibond-3mm, museum-board-1-6mm, museum-board-2-2mm, pvc-5mm-black |
5685 | Hahnemühle William Turner 310g | roll, up to 110 × 1200 cm · 310 g/m² · mounts: pvc-4mm-white, pvc-5mm-white, dibond-3mm, museum-board-1-6mm, museum-board-2-2mm, pvc-5mm-black |
5686 | Canson Infinity Edition Etching Rag 310g | roll, up to 110 × 1200 cm · 310 g/m² · mounts: pvc-4mm-white, pvc-5mm-white, dibond-3mm, museum-board-1-6mm, museum-board-2-2mm, pvc-5mm-black |
5687 | Canson Infinity Platine Fibre Rag 310g | roll, up to 110 × 1200 cm · 310 g/m² · mounts: pvc-4mm-white, pvc-5mm-white, dibond-3mm, museum-board-1-6mm, museum-board-2-2mm, pvc-5mm-black |
5937 | Canson Infinity Baryta Photographique 310g | roll, up to 110 × 1200 cm · 310 g/m² · mounts: pvc-4mm-white, pvc-5mm-white, dibond-3mm, museum-board-1-6mm, museum-board-2-2mm, pvc-5mm-black |
5934 | Canson Infinity Rag Photographique II 310g | roll, up to 61 × 1200 cm · 310 g/m² · mounts: pvc-4mm-white, pvc-5mm-white, dibond-3mm, museum-board-1-6mm, museum-board-2-2mm, pvc-5mm-black |
5688 | Hahnemühle Photo Pearl 310g | roll, up to 110 × 1200 cm · 310 g/m² · mounts: pvc-4mm-white, pvc-5mm-white, dibond-3mm, museum-board-1-6mm, museum-board-2-2mm, pvc-5mm-black |
5689 | Hahnemühle Photo Luster 260g | roll, up to 110 × 1200 cm · 260 g/m² · mounts: pvc-4mm-white, pvc-5mm-white, dibond-3mm, museum-board-1-6mm, museum-board-2-2mm, pvc-5mm-black |
5871 | Canson Infinity PhotoGloss Premium RC 270g | roll, up to 61 × 1200 cm · 270 g/m² · mounts: pvc-4mm-white, pvc-5mm-white, dibond-3mm, museum-board-1-6mm, museum-board-2-2mm, pvc-5mm-black |
5690 | XYZ White Velvet 270g | roll, up to 110 × 1200 cm · 270 g/m² · mounts: pvc-4mm-white, pvc-5mm-white, dibond-3mm, museum-board-1-6mm, museum-board-2-2mm, pvc-5mm-black |
5691 | Hahnemühle Studio Enhanced 210g | roll, up to 110 × 1200 cm · 210 g/m² · mounts: pvc-4mm-white, pvc-5mm-white, dibond-3mm, museum-board-1-6mm, museum-board-2-2mm, pvc-5mm-black |
5692 | Hahnemühle Art Canvas Smooth 370g | roll, up to 110 × 1200 cm · 370 g/m² |
5872 | Canson Museum ProCanvas WC | roll, up to 110 × 1200 cm · 385 g/m² |
5699 | Awagami Murakumo Kozo Select Natural 42g | roll, up to 91 × 1200 cm · 42 g/m² · mounts: pvc-4mm-white, pvc-5mm-white, dibond-3mm, museum-board-1-6mm, museum-board-2-2mm, pvc-5mm-black |
5694 | Awagami Kozo White 70g | roll, up to 110 × 1200 cm · 70 g/m² · mounts: pvc-4mm-white, pvc-5mm-white, dibond-3mm, museum-board-1-6mm, museum-board-2-2mm, pvc-5mm-black |
5702 | Transparent film 165µ | roll, up to 61 × 1200 cm · 100 g/m² · mounts: pvc-4mm-white, pvc-5mm-white, dibond-3mm, museum-board-1-6mm, museum-board-2-2mm, pvc-5mm-black |
5696 | Fedrigoni Arena White Smooth 350g | sheets 51 × 72 51 × 72, 72 × 102 cm 72 × 102 cm · 350 g/m² · double-sided · mounts: pvc-4mm-white, pvc-5mm-white, dibond-3mm, museum-board-1-6mm, museum-board-2-2mm, pvc-5mm-black |
5697 | Fedrigoni Arena White Rough 300g | sheets 51 × 72 51 × 72, 72 × 102 cm 72 × 102 cm · 300 g/m² · double-sided · mounts: pvc-4mm-white, pvc-5mm-white, dibond-3mm, museum-board-1-6mm, museum-board-2-2mm, pvc-5mm-black |
Options
A “print” line
Every line has kind and qty (1–999); these are the rest.
| Field | Type | What it is |
|---|---|---|
paper_idrequired | integer | The paper, by its id in the catalogue. |
width_cmrequired | number | Width of the print as it hangs, in cm. On a cut-sheet paper, one of its sheet sizes (either way round). |
height_cmrequired | number | Height of the print as it hangs, in cm. |
filerequired | string | The picture: an upload id from POST /uploads. A PDF gives one id per page. |
fit | "fit" | "fill" default "fit" | fit = the whole picture, as large as it goes, with paper showing where the shapes differ. fill = the print covered edge to edge, the picture cropped about its centre. |
border_cm | number default 0 | White border all round, in cm. A cut sheet always keeps at least its unprintable margin. |
rotate | 0 | 90 | 180 | 270 default 0 | Turn the picture clockwise before placing it, in degrees. |
crop | object | Keep part of the picture: {"x", "y", "w", "h"} as fractions 0–1 of its width and height (after its EXIF orientation). Cut before fit or fill. |
back_file | string | Double-sided, on a cut-sheet paper that prints both faces: the back’s upload id. Same fit, border and rotation. |
back_blank | boolean default false | Double-sided with a blank back (the odd last page of a document). |
mount | string | Mount the print on a board, by id (options.mounts). Not with double-sided. |
subframe | string | A hanging subframe behind the mount, by id (options.subframes). Needs mount. |
subframe_pct | number default 60 | The subframe’s size as a percentage of the print (the catalogue gives the range). |
frame | string | Frame the print, by id (options.frames). Not with double-sided. |
glazing | string | The frame’s glazing, by id (options.glazings), or "none". Default: the first offered. |
matt | string | A passe-partout inside the frame, by id (options.matts). |
matt_cm | number default 5 | The passe-partout’s width all round, in cm (0–20). |
note | string | A word for the studio about this print (up to 500 characters). |
Example
{
"kind": "print",
"qty": 2,
"paper_id": 101,
"width_cm": 40,
"height_cm": 50,
"file": "upl_…",
"fit": "fill",
"border_cm": 2
}Products kind: "product"
gift-card-50 | Gift card — €50 | €50.00 |
film-scan-35mm | Film scanning — 35 mm frame | €6.00 |
A “product” line
Every line has kind and qty (1–999); these are the rest.
| Field | Type | What it is |
|---|---|---|
productrequired | string | The item, by its slug (or SKU) in the catalogue. |
variant | string | The option, by its id in the catalogue — required when the item has options. |
note | string | A word for the studio about this line (up to 500 characters). |
Example
{
"kind": "product",
"qty": 3,
"product": "the-slug"
}Price it
POST /quote takes the body an order takes, reference aside, and writes nothing. It answers each line’s price, the shipping options, VAT and the total, warnings about the artwork (a low resolution, a crop), and would_be: the state the order would start in under your terms today.
curl \
-H "Authorization: Bearer $XYZ_KEY" \
-H "Content-Type: application/json" \
-d @order.json \
https://www.xyz-prints.com/api/v1/partner/quoteorder.json — Example
{
"lines": [
{
"kind": "print",
"qty": 2,
"paper_id": 101,
"width_cm": 40,
"height_cm": 50,
"file": "k3Fq9w2LxP0aZt7c",
"fit": "fill",
"border_cm": 2
}
],
"delivery": {
"mode": "ship"
},
"ship_to": {
"first_name": "Ana",
"last_name": "Silva",
"address_1": "Rua das Flores 12",
"city": "Porto",
"postcode": "4050-262",
"country": "PT",
"phone": "+351 912 345 678"
},
"note": "Gift — no invoice in the parcel."
}Answer
{
"quote": {
"currency": "EUR",
"lines": [
{
"kind": "print",
"name": "Hahnemühle Photo Rag 308",
"qty": 2,
"unit_price": 54.12,
"total": 108.24,
"vat_rate": 23,
"details": [
{
"name": "File",
"value": "harbour-at-dusk.tif"
},
{
"name": "Sheet",
"value": "40 × 50 cm"
},
{
"name": "Image",
"value": "36 × 46 cm"
}
]
}
],
"warnings": [
{
"line": 0,
"code": "cropped",
"message": "fill cuts 3.2% of the picture away to cover the print; send fit to keep all of it."
}
],
"delivery": {
"mode": "ship",
"service": "ctt_express",
"label": "CTT Expresso",
"options": [
{
"id": "ctt_express",
"label": "CTT Expresso",
"price": 12.05,
"days": [
1,
2
]
}
]
},
"totals": {
"goods": 108.24,
"discount": 0,
"shipping": 12.05,
"vat": 22.49,
"total": 120.29
},
"vat": {
"rate": 23,
"exempt": false,
"reason": null,
"number": "PT509999999",
"status": "valid_domestic"
},
"would_be": {
"state": "in_production",
"holds": []
}
}
}Place it
POST /orders needs an Idempotency-Key header: a retry with the same key answers the first result and never places a second order. Your reference is unique as well — a second order carrying it is refused and the first one comes back. Send expected_total, the total /quote gave you, and a price that changed meanwhile is refused (409 price_changed) instead of charged.
curl \
-H "Authorization: Bearer $XYZ_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: SHOP-10482-1" \
-d @order.json \
https://www.xyz-prints.com/api/v1/partner/ordersorder.json — Example
{
"reference": "SHOP-10482",
"lines": [
{
"kind": "print",
"qty": 2,
"paper_id": 101,
"width_cm": 40,
"height_cm": 50,
"file": "k3Fq9w2LxP0aZt7c",
"fit": "fill",
"border_cm": 2
}
],
"delivery": {
"mode": "ship"
},
"ship_to": {
"first_name": "Ana",
"last_name": "Silva",
"address_1": "Rua das Flores 12",
"city": "Porto",
"postcode": "4050-262",
"country": "PT",
"phone": "+351 912 345 678"
},
"note": "Gift — no invoice in the parcel.",
"expected_total": 120.29
}201 — Answer
{
"order": {
"number": 6214,
"reference": "SHOP-10482",
"state": "in_production",
"holds": [],
"status": "processing",
"created_at": "2026-09-25T10:04:11.000Z",
"updated_at": "2026-09-25T10:04:11.000Z",
"currency": "EUR",
"payment": {
"terms": "account",
"state": "unpaid",
"paid_at": null
},
"totals": {
"goods": 108.24,
"discount": 0,
"shipping": 12.05,
"vat": 22.49,
"total": 120.29,
"refunded": 0
},
"vat": {
"rate": 23,
"exempt": false,
"reason": null,
"number": "PT509999999"
},
"delivery": {
"mode": "ship",
"service": "CTT Expresso",
"ship_to": {
"first_name": "Ana",
"last_name": "Silva",
"address_1": "Rua das Flores 12",
"city": "Porto",
"postcode": "4050-262",
"country": "PT",
"phone": "+351 912 345 678"
},
"tracking": null
},
"invoice": null,
"note": "Gift — no invoice in the parcel.",
"lines": [
{
"kind": "print",
"name": "Hahnemühle Photo Rag 308",
"qty": 2,
"unit_price": 54.12,
"total": 108.24,
"vat_rate": 23,
"details": [
{
"name": "File",
"value": "harbour-at-dusk.tif"
},
{
"name": "Sheet",
"value": "40 × 50 cm"
},
{
"name": "Image",
"value": "36 × 46 cm"
}
],
"file": "k3Fq9w2LxP0aZt7c"
}
]
},
"warnings": [
{
"line": 0,
"code": "cropped",
"message": "fill cuts 3.2% of the picture away to cover the print; send fit to keep all of it."
}
]
}ship_to is the recipient: first_name and last_name, or company; address_1, city, postcode and country (two letters); a phone and an e-mail help the carrier. Your own company is the one invoiced — the studio keeps it on your account. delivery.service picks one of the shipping options /quote gave; without it, the first. The recipient hears nothing from the studio unless you ask for it.
Payment and acceptance
On account: an order that passes every check goes straight into production. It is invoiced to you and paid by bank transfer against the invoice; the studio records the payment.
Prepaid: the order waits for its money. payment.pay_url is its payment page (card, MB WAY, PayPal…); paid, it goes into production by itself.
A hold keeps an order out of production until the studio releases it; holds says why:
review— Your account is set so that a person looks at every order first.credit_limit— This order takes what you owe on account past your credit limit.no_credit_limit— Your account is on credit, but no credit limit has been agreed yet — the studio sets one, then releases the order.
The state of an order
| State | Meaning |
|---|---|
awaiting_payment | Prepaid terms: waiting for its money through `payment.pay_url`. Paid, it goes into production by itself. |
held | Kept out of production until the studio releases it — see `holds`. |
in_production | Accepted: being printed, finished and packed. |
shipped | Handed to the carrier; `delivery.tracking` when there is a number. |
completed | Delivered or collected, and closed. |
cancelled | Cancelled; nothing more happens. |
refunded | Refunded in full. |
failed | A payment failed and the order stopped. |
An order that is held, or still waiting for its payment, can be cancelled: POST /orders/{number}/cancel. Once it is in production, write to the studio.
Follow it
GET /orders/{number} reads one order; GET /orders?reference= finds yours by your reference. To keep your system in step without a webhook, ask GET /orders?updated_since=<the last updated_at you saw>: every order changed since, oldest change first.
curl \
-H "Authorization: Bearer $XYZ_KEY" \
"https://www.xyz-prints.com/api/v1/partner/orders?updated_since=2026-09-25T10:04:11.000Z"Webhooks
Give the studio an https address and every change to your orders is POSTed there as JSON, with the order as it stands. Answer with any 2xx within 8 seconds. Each message is sent once: if you were down, GET /orders?updated_since= catches you up. Messages can arrive out of order — compare data.order.updated_at.
| Event | When |
|---|---|
order.created | The order was placed — `order.state` says whether it went into production, was held, or waits for payment. |
order.accepted | A held or prepaid order went into production. |
order.paid | Its payment was recorded. |
order.shipped | It left the studio; `tracking` when there is a number. |
order.ready | Ready to collect at the studio. |
order.invoiced | The invoice was issued; `invoice` is its number. |
order.completed | Closed. |
order.cancelled | Cancelled. |
order.refunded | Refunded. |
order.updated | Anything else about it changed (`from` and `to` for its status). |
The studio’s “Send a test” posts the event ping, signed the same way, with no order.
order.shipped — Example
{
"id": "evt_6f1c0a9b2d3e4f5a6b7c8d9e",
"event": "order.shipped",
"created_at": "2026-09-27T15:31:02.000Z",
"data": {
"tracking": "EA123456789PT",
"order": {
"number": 6214,
"reference": "SHOP-10482",
"state": "shipped",
"holds": [],
"status": "processing",
"created_at": "2026-09-25T10:04:11.000Z",
"updated_at": "2026-09-25T10:04:11.000Z",
"currency": "EUR",
"payment": {
"terms": "account",
"state": "unpaid",
"paid_at": null
},
"totals": {
"goods": 108.24,
"discount": 0,
"shipping": 12.05,
"vat": 22.49,
"total": 120.29,
"refunded": 0
},
"vat": {
"rate": 23,
"exempt": false,
"reason": null,
"number": "PT509999999"
},
"delivery": {
"mode": "ship",
"service": "CTT Expresso",
"ship_to": {
"first_name": "Ana",
"last_name": "Silva",
"address_1": "Rua das Flores 12",
"city": "Porto",
"postcode": "4050-262",
"country": "PT",
"phone": "+351 912 345 678"
},
"tracking": "EA123456789PT"
},
"invoice": null,
"note": "Gift — no invoice in the parcel.",
"lines": [
{
"kind": "print",
"name": "Hahnemühle Photo Rag 308",
"qty": 2,
"unit_price": 54.12,
"total": 108.24,
"vat_rate": 23,
"details": [
{
"name": "File",
"value": "harbour-at-dusk.tif"
},
{
"name": "Sheet",
"value": "40 × 50 cm"
},
{
"name": "Image",
"value": "36 × 46 cm"
}
],
"file": "k3Fq9w2LxP0aZt7c"
}
]
}
}
}Every message is signed: X-WebOffice-Signature: t=<unix time>,v1=<HMAC-SHA256 of “t.body” under your signing secret, in hex>. Check it against the raw body, and refuse a t more than five minutes away. The studio gives you the secret and can rotate it.
import crypto from 'node:crypto'
// raw: the request body exactly as it arrived (a string, before any JSON parsing)
// header: the X-WebOffice-Signature header; secret: your signing secret (whsec_…)
export function verified(raw, header, secret) {
const parts = Object.fromEntries(String(header).split(',').map((kv) => kv.trim().split('=')))
const t = Number(parts.t)
if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > 300) return false
const want = crypto.createHmac('sha256', secret).update(t + '.' + raw).digest('hex')
const got = String(parts.v1 || '')
return got.length === want.length && crypto.timingSafeEqual(Buffer.from(got), Buffer.from(want))
}Errors
An error answers an HTTP status and { error: { code, message, fields } }; fields names each problem the way the body spells it (lines[0].width_cm). Codes are stable, messages are for people.
422 — Example
{
"error": {
"code": "invalid_lines",
"message": "Hahnemühle Photo Rag 308: That size is too large for this paper — the largest print it can make is 111.8 × 1500 cm.",
"fields": [
{
"field": "lines[0].width_cm",
"code": "size_not_available",
"message": "Hahnemühle Photo Rag 308: That size is too large for this paper — the largest print it can make is 111.8 × 1500 cm."
}
]
}
}| Code | Meaning |
|---|---|
unauthorised | No key, or not a partner key (401). |
partner_inactive | The key’s partner is switched off, or the brand no longer takes partner orders (403). |
address_not_allowed | Your account only takes calls from the addresses the studio listed, and this is not one (403). |
rate_limited | Past a limit — calls, lines priced, orders an hour, or bad keys from one address: wait the Retry-After seconds (429). |
internal | Something failed on our side; nothing you sent was wrong. Try again (500). |
storage_full | Too many of your files are waiting for an order: order them or let them expire first (507). |
unknown_call | No such call (404). |
invalid_query | A query parameter is not what the call takes — updated_since is an ISO 8601 date-time (400). |
invalid_body | The body is not what the call takes; `fields` names each problem (422). |
invalid_lines | A line cannot be made as asked — an unknown paper, a size it cannot take, a finishing it does not offer, a file that is not yours; `fields` names the line and the field (422). |
no_shipping | No carrier takes this order to this address, or its freight is quoted by hand (422). |
unknown_service | delivery.service is not one of the options for this order (422). |
destination_refused | The studio does not ship to that country (422). |
pickup_not_offered | Collection at the studio is not offered (422). |
idempotency_key_required | POST /orders needs an Idempotency-Key header (400). |
in_progress | Another request with this Idempotency-Key is still running (409). |
duplicate_reference | An order already carries this reference; it comes back as `order` (409). |
price_changed | The total is not your expected_total; the current `total` comes back — price it again (409). |
paused | The studio is not taking orders until `until` (503). |
unknown_order | No order of yours with that number (404). |
not_cancellable | The order is already in production, shipped or settled (409). |
unknown_file | No upload of yours with that id — expired, or never sent (404). |
no_file | The upload carried no file (400). |
format_not_accepted | That file type is not accepted (415). |
too_large | A file, a single upload request or an order body is too large: chunk the file, or send fewer lines (413). |
unreadable | The file could not be read as a picture or a PDF (415). |
upload_failed | A chunk did not fit the upload it claimed to belong to (400). |
Per key, every ten minutes: 600 calls, 2000 lines priced (a quote or an order counts its lines) and 8192 MB of uploads. Per account: 120 orders an hour, 20 GB of files waiting for an order, and a body of up to 1024 KB. Past a limit the answer is 429 with Retry-After (507 for the files). Calls with a bad key are throttled per address.
Every call
GET /partner
Who the key belongs to: your terms, what you owe on account, the limits.
GET /partner/catalogue
What can be ordered today, kind by kind: the ids a line names.
POST /partner/uploads
Send a file. Up to the single-request limit as multipart field `file`; larger files in chunks. A PDF becomes one file per page.
no_file format_not_accepted too_large unreadable upload_failed storage_full unauthorised partner_inactive address_not_allowed rate_limited unknown_call internal
GET /partner/uploads/{id}
A file you sent: its pixels, resolution and colour, and until when it is kept.
unknown_file unauthorised partner_inactive address_not_allowed rate_limited unknown_call internal
POST /partner/quote
Price an order without placing it: lines, shipping options, VAT, total — and whether it would go straight into production.
too_large invalid_body invalid_lines no_shipping unknown_service destination_refused pickup_not_offered unauthorised partner_inactive address_not_allowed rate_limited unknown_call internal
POST /partner/orders
Place an order. Priced here, then accepted into production, held, or left waiting for its payment — by your terms.
Idempotency-Key(required) — Your unique key for this attempt (8–200 characters). A retry with the same key returns the first answer and never places a second order.
idempotency_key_required in_progress too_large invalid_body invalid_lines duplicate_reference price_changed no_shipping unknown_service destination_refused pickup_not_offered paused unauthorised partner_inactive address_not_allowed rate_limited unknown_call internal
GET /partner/orders
Your orders, newest first — or every order changed since a moment, oldest change first, to keep your system in step.
?updated_since=— ISO 8601 date-time: orders changed after it, oldest change first. Keep the lastupdated_atand ask again.?reference=— Your own reference: the one order that carries it.?limit=— 1–100, default 50. `more: true` says there is another page.
invalid_query unauthorised partner_inactive address_not_allowed rate_limited unknown_call internal
GET /partner/orders/{number}
One of your orders by its number.
unknown_order unauthorised partner_inactive address_not_allowed rate_limited unknown_call internal
POST /partner/orders/{number}/cancel
Cancel an order that is held or still waiting for its payment. Once in production, write to the studio.
unknown_order not_cancellable unauthorised partner_inactive address_not_allowed rate_limited unknown_call internal
GET /partner/openapi.json
This description as OpenAPI 3.1, for your code generator. Add ?brand=<slug> for the line fields of a brand.
The same, as OpenAPI 3.1 for a code generator: https://www.xyz-prints.com/api/v1/partner/openapi.json?brand=xyz-prints
