Personalized orders
Send different artwork with every order — names, photos or generated designs — by URL or uploaded file, without creating a product first.
For products that are different for every customer (a name on the back of a jersey, a pet photo on a mug, a generated poster), order a catalog variant with inline designs instead of a SKU. You do not need to create a product first.
{
"catalog_variant_id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3",
"quantity": 1,
"designs": [
{ "placement": "front", "layers": [{ "file_id": "file_01k8x3y1q3w5e7r9t1y3u5i7o9" }] },
{ "placement": "back", "layers": [{ "url": "https://cdn.example.com/personalized/order-1042-back.png" }] }
]
}catalog_variant_id— the exact color/size fromGET /catalog/products/{id}(variants[].id).designs— the same format as product designs: one entry per placement, layers withfile_idorurl, optionalpositionandopacity. Everyrequiredplacement must have artwork.
Inline items are validated like products (artwork type, size, resolution) and priced like products — the estimate includes extra placements.
Two ways to send the artwork
Upload first (file_id) |
Send a URL (url) |
|
|---|---|---|
| How | POST /files (multipart or { "url": … }) → use the file_… id |
Put the public image URL in the layer |
| When it is downloaded | During the upload call | While we create the order |
| Errors surface | On upload (file_invalid, file_too_large, download_failed, download_not_an_image, …) |
On order creation: a 400 (download_failed, download_not_an_image, file_invalid, file_download_failed, …) with param = items[i].designs[j].layers[k].url |
| Best for | Retries, large files, reusing artwork | Simplicity — one request |
URLs must be publicly reachable over http(s) (no login, no short-lived signed URL that might expire before we fetch it) and return the image itself. A link to a web page — a file host's share page, for example — fails with download_not_an_image: use the host's direct download link instead.
- We follow up to 5 redirects, and retry once when the host answers with a temporary error (a dropped connection or HTTP 429, 502, 503, 504) — so a single CDN hiccup doesn't fail your order.
- Images are checked before they are decoded: at most 30,000 px on a side and 250 megapixels in total (
file_too_large). - Photos with an EXIF rotation are stored upright, and CMYK images are converted to RGB, so what you see in the preview is what is printed.
- Downloading the same image again (for example when you retry an order) reuses the file we already stored instead of keeping a copy.
Example: a name on the back
Generate the personalized image in your backend (any image library), host it or upload it, then order:
curl -X POST https://swaglists.com/api/v1/orders \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: store-order-2087" \
-d '{
"external_id": "2087",
"recipient": { "name": "Sam Lee", "address1": "9 Elm St", "city": "Austin", "state_code": "TX", "postal_code": "78701", "country_code": "US" },
"items": [{
"catalog_variant_id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3",
"quantity": 1,
"external_line_id": "line-1",
"designs": [
{ "placement": "front", "layers": [{ "file_id": "file_01k8x3y1q3w5e7r9t1y3u5i7o9" }] },
{ "placement": "back", "layers": [{ "url": "https://cdn.example.com/names/sam-lee-23.png",
"position": { "x": 0.1, "y": 0.1, "width": 0.8, "height": 0.3 } }] }
]
}]
}'const res = await fetch("https://swaglists.com/api/v1/orders", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": `store-order-${storeOrder.id}`,
},
body: JSON.stringify({
external_id: String(storeOrder.id),
recipient,
items: [
{
catalog_variant_id: "cv_01k8w1a2s7d9f1g3h5j7k9m1n3",
quantity: 1,
designs: [
{ placement: "front", layers: [{ file_id: LOGO_FILE_ID }] },
{ placement: "back", layers: [{ url: personalizedImageUrl }] }, // centered & fitted when no position
],
},
],
}),
});order = requests.post(
"https://swaglists.com/api/v1/orders",
headers={"Authorization": f"Bearer {os.environ['API_KEY']}", "Idempotency-Key": f"store-order-{store_order_id}"},
json={
"external_id": str(store_order_id),
"recipient": recipient,
"items": [{
"catalog_variant_id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3",
"quantity": 1,
"designs": [
{"placement": "front", "layers": [{"file_id": LOGO_FILE_ID}]},
{"placement": "back", "layers": [{"url": personalized_image_url}]},
],
}],
},
).json()The position above makes the name box 80% of the area wide and 30% high, near the top — make the image's aspect ratio match the box (see keeping the aspect ratio) or omit position to fit it automatically.
Mixing personalized and regular items
One order can contain both SKU items and inline items:
"items": [
{ "sku": "CA-TEE-BLK-M", "quantity": 2 },
{ "catalog_variant_id": "cv_01k8w1a2s7d9f1g3h5j7k9m1n3", "quantity": 1, "designs": [ … ] }
]Tips
- Price check: send the same body to
POST /orders/estimatefirst, or show your customers a fixed price per product and placement. - Resolution: generate images at the placement's
recommended_width_px(300 DPI); low-resolution artwork is rejected below 75 DPI. - Storefront personalization fields: Shopify line item
propertiesand WooCommerce item meta carry the customer's input — render the image from them in your webhook handler, then send the order. See the Shopify recipe. - Inline items show
product_id: nullandsku: nullon the order, anddesignsis exactly what will be printed. - Previews: a few seconds after the order is created,
mockup_urlshows your design on the product, rendered from the order's owndesigns(it never changes when you later edit a product). Until it is rendered — and in theorder.createdwebhook, which is built when the order is created —mockup_urlis the catalog photo of the variant. Fetch the order again (GET /orders/{id}) to get the rendered image;mockup_urllinks don't expire.