Design to order
The whole path from a customer’s design to a product on its way: the designer commits a design with the publishable key, and the merchant’s server approves it, prints it and orders it with the secret key, or with the integration key of a connected store (Connecting a store). Each step is reported as an event to the merchant’s webhook endpoints, shown after the step that emits it.
1. The customer commits a design
The designer runs in the customer’s browser, on the merchant’s storefront, with
the tenant’s publishable key. The customer fills in a published template, and
the designer commits what they made with POST /v1/designs:
{
"templateId": "tshirt-basic@2",
"values": { "name": { "content": "Alexandra" } },
"blankId": "blank_tshirt",
"placementId": "front"
}
templateId is the published template version the customer filled in.
values holds what they entered, keyed by the id of the template layer each
value is for, with that layer’s properties inside (a text layer’s content,
for instance). blankId and placementId name the product and the spot on it
the design is for; send both or neither. A design is checked against its
template when it is committed, and a design that breaks one of the template’s
rules is refused with constraint_violation.
{
"designId": "6f1c8a2b-3d4e-4f5a-8b6c-7d8e9f0a1b2c",
"printFloorDpi": 150,
"lowResolution": []
}
The design is now committed, and cannot change. lowResolution lists the
pictures that print below printFloorDpi at the design’s size, the minimum
print resolution the template sets, else your workspace’s. It is a warning,
never a refusal. POST /v1/validate and POST /v1/proofs answer the same
two fields, so a customer can be told before the design is committed. Hand its designId to the
merchant’s own server with the customer’s cart, the way the storefront already
passes an order along.
To show the committed design on its product in the cart, at checkout, on
order pages and in emails, the merchant’s server fetches
GET /v1/designs/{id}/mockup.png: the design drawn on the blank’s photo, as
GET /t/{id}/mockup.png draws the template, in the view the customer
previewed it in unless the request names another. Pass the design’s
proofId as v and the image can be kept for good.
{
"id": "1790000000000-0192f5a4-7c1e-4d3a-9b6e-2f4c8a1d5e70",
"type": "design.committed",
"createdAt": 1790000000000,
"payload": {
"designId": "6f1c8a2b-3d4e-4f5a-8b6c-7d8e9f0a1b2c",
"templateId": "tshirt-basic@2",
"apiKeyId": "3b2d9c1e-5a4f-4e7d-8c6b-1a0f9e8d7c6b"
}
}
A fulfilment payload, the order data a printshop needs (a material, a
shipping method), can only be attached by the secret or integration key. A
server that needs one commits the design itself: the designer renders a proof
with POST /v1/proofs, and the server commits it with POST /v1/designs,
sending the proof’s proofToken and the fulfilmentPayload.
2. The merchant’s server approves it
Once the customer has paid, the merchant’s server approves the design with
POST /v1/designs/{id}/approve and its secret or integration key. With no
body it approves now:
{ "status": "approved" }
{
"id": "1790000060000-5d7e1f2a-3b4c-4d5e-8f60-718293a4b5c6",
"type": "design.approved",
"createdAt": 1790000060000,
"payload": {
"designId": "6f1c8a2b-3d4e-4f5a-8b6c-7d8e9f0a1b2c",
"approvedVia": "explicit"
}
}
A server that commits the design itself, and has already taken payment, can
approve it in the same call: "approve": true on POST /v1/designs commits it
approved, so it never waits between two calls, and design.approved follows
design.committed with approvedVia set to explicit. It needs the grant
that approves (a secret or integration key holds it); a publishable key
sending it is refused with forbidden.
To give the customer a window to change their mind, send afterDays:
{ "afterDays": 14 }
{ "status": "scheduled" }
The design stays committed, and design.approval_scheduled carries the time it
will be approved. When that time comes, design.approved follows with
approvedVia set to deadline, unless the design was rejected first with
POST /v1/designs/{id}/reject, which emits design.rejected.
{
"id": "1790000060000-9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
"type": "design.approval_scheduled",
"createdAt": 1790000060000,
"payload": {
"designId": "6f1c8a2b-3d4e-4f5a-8b6c-7d8e9f0a1b2c",
"approveAfter": 1791209660000
}
}
Correcting a design before it is ordered
A design that has not been ordered can be corrected with
PATCH /v1/designs/{id}. values takes the commit’s shape and is applied on
top of the design as it is; expectedRevision is the revision you read:
{
"values": { "namn": { "content": "Merry Christmas" } },
"expectedRevision": 1
}
{
"revision": 2,
"edited": true,
"proofId": "prf_9c1e2d3f4a5b6c7d8e9f0a1b",
"printFloorDpi": 150,
"lowResolution": []
}
The design is back to committed, its approval cleared, and
design.edited says so. The correction renders a proof of what the design
now says, proofId, which becomes the design’s proof: show it to your
customer with GET /v1/designs/{id}/proof, and a proof page shows it too.
Values that leave the document as it is write nothing: the answer is
edited: false with the current revision and proof, and the approval
stands. Approve it again as above; send expectedRevision
with the approval to be refused revision_changed if someone corrected it
since you read it. A print made before the correction is refused
print_superseded when ordered: print the corrected design again once it is
approved.
{
"id": "1790000090000-3b4c5d6e-7f8a-4b9c-8d0e-1f2a3b4c5d6e",
"type": "design.edited",
"createdAt": 1790000090000,
"payload": {
"designId": "6f1c8a2b-3d4e-4f5a-8b6c-7d8e9f0a1b2c",
"revision": 2
}
}
Once any print of the design has an order, the correction is refused
not_editable. POST /v1/designs/{id}/duplicate makes a new committed design
from it instead, with duplicatedFrom naming the source, and emits
design.duplicated. The duplicate waits for you to correct and approve it.
{ "designId": "7a2d9b3c-4e5f-4a6b-9c7d-8e9f0a1b2c3d" }
When the customer asks for changes
A customer on a deciding proof page can ask for changes, and your own page can
relay a customer’s request with POST /v1/designs/{id}/request-changes:
{ "message": "Could the name be larger?", "expectedRevision": 1 }
The design stays committed, a scheduled approval is cancelled, and
design.changes_requested carries the request’s words. expectedRevision is
the revision whose proof the customer was shown; a request about a design
corrected since is refused revision_changed. Correct the design, or decide
it, and the request is closed; GET /v1/designs/{id}/review answers the open
one. The correction’s proof is what the customer then decides.
{
"id": "1790000095000-4c5d6e7f-8a9b-4c0d-9e1f-2a3b4c5d6e7f",
"type": "design.changes_requested",
"createdAt": 1790000095000,
"payload": {
"designId": "6f1c8a2b-3d4e-4f5a-8b6c-7d8e9f0a1b2c",
"revision": 1,
"message": "Could the name be larger?"
}
}
Sending the customer back to your store to edit
A proof page you hand your customer can send them back to your store once they have asked for changes, so your store opens the design for them to correct: see Sending the customer back to the store.
3. Print the approved design
POST /v1/prints makes the print files for an approved design:
{ "designId": "6f1c8a2b-3d4e-4f5a-8b6c-7d8e9f0a1b2c" }
It answers 202 as soon as the print is reserved, with its Location, and
renders it after the answer. The print reads created while it renders:
{ "printId": "c4d5e6f7-a8b9-4c0d-9e1f-2a3b4c5d6e7f", "status": "created" }
Read it with GET /v1/prints/{id} until it is completed, or wait for
print.completed, before you order it. To be answered once it has rendered
instead, send Prefer: wait: the answer is then a 201 with the print and its
job ticket, which says what to make and how to cut it. The print’s ticket says
nothing about any one order: how many copies, your reference and who it ships
to are each order’s, added when it is placed.
{
"printId": "c4d5e6f7-a8b9-4c0d-9e1f-2a3b4c5d6e7f",
"jobTicket": {
"product": {
"sku": "6f1c8a2b-3d4e-4f5a-8b6c-7d8e9f0a1b2c",
"tenantId": "acme"
},
"artwork": { "templateId": "tshirt-basic@2", "name": "Front" },
"printArea": {
"widthMm": 90,
"heightMm": 50,
"bleedMm": 3,
"cutShape": { "type": "path", "d": "M 0 0 L 90 0 L 90 50 L 0 50 Z" }
},
"artifact": {
"file": "PR-2026-000123_6f1c8a2b.pdf",
"format": "pdf",
"colorSpace": "DeviceCMYK",
"iccProfile": "ISOcoated_v2_eci.icc",
"effectiveDpi": 300,
"includesBleed": true,
"trimMarks": false,
"spotColors": [],
"spotOutput": "process"
},
"manifest": {
"docHash": "9f2c4e1a",
"templateVersion": "tshirt-basic@2",
"rendererVersion": "0.1.0",
"fontSetHash": "a1b2c3d4",
"iccProfile": "ISOcoated_v2_eci.icc",
"renderedAt": "2026-09-21T14:01:40.000Z"
}
}
}
Cut to printArea: it is the geometry the print file was rendered against.
GET /v1/prints/{id} reads the print again later.
Printing emits print.created when the print is reserved and
print.completed when its files are ready. A print that could not be rendered
reads failed, with its failure, and emits print.failed instead; with
Prefer: wait the request answers 500 with render_failed. A print still
rendering ten minutes after it started is failed too.
{
"id": "1790000100000-2b3c4d5e-6f70-4812-9a3b-4c5d6e7f8091",
"type": "print.completed",
"createdAt": 1790000100000,
"payload": { "printId": "c4d5e6f7-a8b9-4c0d-9e1f-2a3b4c5d6e7f" }
}
4. Submit the order
POST /v1/prints/{id}/orders sends a completed print to the printshop its
design was committed to, and the order records that connection as
printshopConnectionId, with your name for it as printshopName. The body
is optional, and every field in it is:
quantity— how many copies; one when left out. It belongs to the order, not the print, so one print can be ordered again in another quantity.externalRef— your own reference for the order, such as your order or line id. It is not unique, andGET /v1/orders?externalRef=finds every order under it.recipientandshippingAddress— who the order is for and where it ships. The country is a two-letter ISO code. They are never written to logs.variant— the ref of the blank’s variant the shopper chose, such as White / M (GET /v1/blanks/{id}/variantslists them). A blank with one variant needs none; one with several refuses an order naming none (variant_required), and a variant of another blank or a retired one is refused too.
The first four reach the printshop on the job ticket, and a retry of a failed
order carries the same ones and the same variant. The printshop is sent what
it needs for the variant, such as Printful’s catalogue variant id, and the
order keeps a copy of the variant as it was placed (variant).
{
"quantity": 3,
"externalRef": "ORDER-1042",
"variant": "v_3f9a1c2b7d4e8a01",
"recipient": { "name": "Alice Morgan", "email": "[email protected]" },
"shippingAddress": {
"line1": "1 High Street",
"postalCode": "SW1A 2AA",
"city": "London",
"country": "GB"
}
}
{
"id": "e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b",
"tenantId": "acme",
"printId": "c4d5e6f7-a8b9-4c0d-9e1f-2a3b4c5d6e7f",
"designId": "6f1c8a2b-3d4e-4f5a-8b6c-7d8e9f0a1b2c",
"printIds": [
"c4d5e6f7-a8b9-4c0d-9e1f-2a3b4c5d6e7f"
],
"prints": [
{
"printId": "c4d5e6f7-a8b9-4c0d-9e1f-2a3b4c5d6e7f",
"designId": "6f1c8a2b-3d4e-4f5a-8b6c-7d8e9f0a1b2c",
"designRevision": 1,
"placementId": "front",
"printedAs": "Front",
"spotOutput": "process",
"status": "completed",
"placementFields": []
}
],
"quantity": 3,
"externalRef": "ORDER-1042",
"recipient": {
"name": "Alice Morgan",
"email": "[email protected]"
},
"shippingAddress": {
"line1": "1 High Street",
"postalCode": "SW1A 2AA",
"city": "London",
"country": "GB"
},
"adapter": "printful",
"shipTo": null,
"printshopConnectionId": "0b1c2d3e-4f5a-4b6c-8d7e-9f0a1b2c3d4e",
"printshopName": "Printful",
"spotOutput": "process",
"externalOrderRef": "PF-889123",
"status": "queued",
"lastError": null,
"failureKind": null,
"hold": null,
"tracking": null,
"submittedAt": 1790000120000,
"lastCheckedAt": null,
"updatedAt": 1790000121000,
"replacesOrderId": null,
"replacedByOrderId": null,
"personalDataErasedAt": null,
"blankId": "blank_classic_tee",
"variantRef": "v_3f9a1c2b7d4e8a01",
"variant": {
"ref": "v_3f9a1c2b7d4e8a01",
"options": {
"Color": "White",
"Size": "M"
},
"fields": [
{
"name": "catalogVariantId",
"value": "4012",
"source": "own"
}
]
},
"printshopAsPlaced": {
"name": "Printful",
"adapter": "printful"
},
"delivery": null,
"blank": {
"name": "Classic tee",
"optionAxes": [
"Color",
"Size"
],
"variants": 12
}
}
A printshop that turns the order down is still a 201: the order is recorded
with status: "failed" and its reason in lastError, and order.failed is
emitted. A failed order that never reached the printshop can be submitted
again with POST /v1/orders/{id}/retry.
{
"id": "1790000120000-7c8d9e0f-1a2b-4c3d-8e4f-5a6b7c8d9e0f",
"type": "order.submitted",
"createdAt": 1790000120000,
"payload": {
"orderId": "e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b",
"printId": "c4d5e6f7-a8b9-4c0d-9e1f-2a3b4c5d6e7f",
"adapter": "printful",
"quantity": 3,
"externalRef": "ORDER-1042",
"externalOrderRef": null,
"replacesOrderId": null
}
}
From here the order moves on as the printshop reports it (queued,
printed, shipped), and each move is an order.status_changed. Read the
order and its history at any time with GET /v1/orders/{id}.
{
"id": "1790086400000-0f1e2d3c-4b5a-4968-8776-655443322110",
"type": "order.status_changed",
"createdAt": 1790086400000,
"payload": {
"orderId": "e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b",
"printId": "c4d5e6f7-a8b9-4c0d-9e1f-2a3b4c5d6e7f",
"status": "shipped",
"previousStatus": "printed"
}
}
An order whose submission timed out has the status outcome_unknown: the
printshop may have accepted it, and may be making it now. It is not a failure,
and submitting it again could make the product twice. Ask the printshop what
happened first, then settle it with what it told you. A secret key, a
connected store’s integration key or a studio member holding orders:update
may do this, and a reason is required:
- The printshop does not have it: mark it
failed.POST /v1/orders/{id}/retrythen submits it again. - The printshop has it: mark it
submitted, with the printshop’s own reference for it when you were given one, so its status is followed from then on.
{
"status": "failed",
"reason": "The printshop has no order under this number."
}
An order in outcome_unknown that the printshop has given its reference for
can also be cancelled, with POST /v1/orders/{id}/cancel, as an order in
submitted or queued can.
When your customer asks you to erase their personal data, see Erasing personal data.
Questions? Write to [email protected].