Artwork Export API
Pull artwork files for fulfilment integrations — an incremental presigned-URL feed, JSON metadata, or a ZIP archive, by order, date range, or cursor.
The Artwork Export API lets you programmatically download artwork files generated by Pixel Wrangler — useful for fulfillment systems, automated downloads, or custom workflows.
Endpoint
GET /api/artwork-export
Authentication
Every request must include your API key as a bearer token:
Authorization: Bearer YOUR_API_KEY
Generating an API Key
- Open your Pixel Wrangler app in Shopify Admin
- Navigate to Settings
- Find the Artwork Export API Key section
- Click Generate API Key
- Copy and securely store the key
Security: Treat the key like a password. Never expose it in client-side code or public repositories.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
shop | string | Yes | Your Shopify domain (e.g., mystore.myshopify.com) |
format | string | No | urls (recommended), json, or zip (default) |
orderIds | string | No* | Comma-separated Shopify order GIDs |
orderNumbers | string | No* | Comma-separated order numbers (e.g., 1001,1002) |
startDate | string | No* | Filter from this date, YYYY-MM-DD |
endDate | string | No* | Filter to this date, YYYY-MM-DD |
limit | number | No | urls/json page size (max 1000, default 1000) |
cursor | string | No | urls/json opaque feed cursor — pass back the previous page's nextCursor |
* A filter is required for
zip(a bounded window, capped at 30 days) and optional forurls/json— the cursor + page limit bound the work, so you can sync from the beginning.startDatethen acts as an optional floor on completion time. Only completed artwork is returned.
Edited orders: if a design was edited after purchase, every format returns the current artwork (with its
-edited-vNfilename suffix). Superseded files are excluded automatically.
Choosing a format
urls(recommended) — an incremental feed of presigned S3 links, keyset-paginated by completion time. You download files individually and advance a cursor. Scales to any volume (peak season is just more pages) and is the right tool for bulk or continuous sync. Use this for production fulfillment.json— the same feed asurls, but file metadata grouped by file (SKUs, line items) instead of download links.zip— a single archive of a bounded window (≤ 500 files), built synchronously and returned as a302redirect to a presigned S3 URL. Convenient for small, one-off downloads; not for large or continuous pulls — useurls.
URLs format — incremental feed (recommended)
A keyset-paginated feed of completed artwork, ordered by completion time, returning presigned S3 URLs (valid 1 hour). No server-side zipping, so it scales to any volume — a peak day is just more pages.
Each page returns up to limit files plus an opaque nextCursor. To sync:
- Request a page (omit
cursorto start from the oldest, or passstartDateas a floor). - Download each
files[].urldirectly from S3 (in parallel). - Persist
nextCursoronly after the page's downloads succeed, then request the next page with&cursor=<nextCursor>. - Stop when
nextCursorisnull. Next run, resume from your saved cursor.
{
"count": 500,
"expiresInSeconds": 3600,
"nextCursor": "MjAyNi0wNi0xOFQwNjozMjo0OC44MDdafGFiYzEyMw",
"files": [
{
"id": "f1e2d3c4-…",
"orderNumber": "1001",
"skus": ["CUST-TEE-M-PK-CT01"],
"lineItemIds": ["gid://shopify/LineItem/111"],
"fileName": "CUST-TEE-BK-Front-a646836f.png",
"url": "https://…s3…?X-Amz-Signature=…",
"fileType": "png",
"expiresInSeconds": 3600
}
]
}fileName— the customizer filename (carries product + colour); the name to save the file as.skus/lineItemIds— the line-item SKU(s) for the file, as an array (a file can serve multiple line items). Route artwork from these; for "stripped" filenames that don't encode the product/colour, the SKU is the source of truth (you can rebuild the production filename from it).id— a stable file id; dedup on it, since page boundaries can re-send a file (safe to ignore).nextCursorisnullwhen you've caught up. It's an opaque token — store it as-is and pass it back; don't parse it.
JSON format — same feed, metadata only
The same incremental feed as urls (same cursor/nextCursor/id), but each entry is file metadata — all SKUs and line items that reference the file — instead of a download link.
{
"count": 4,
"nextCursor": null,
"files": [
{
"id": "f1e2d3c4-…",
"orderId": "gid://shopify/Order/1234567890",
"orderNumber": "1001",
"fileName": "CUSTOM-BK-File-1-a1b2c3d4.png",
"fileType": "png",
"skus": ["CUSTOM-BK-SM", "CUSTOM-BK-MD"],
"lineItemIds": ["gid://shopify/LineItem/111", "gid://shopify/LineItem/222"],
"url": "https://px-customiser-output-files.s3.ap-southeast-2.amazonaws.com/…"
}
]
}Multiple line items can share one artwork file (same design in different sizes); both
urlsandjsonlist everysku/lineItemfor the file.
ZIP format (synchronous, small archives)
format=zip builds the archive inline and 302-redirects to a presigned S3 URL. Your HTTP client must follow redirects. It's capped at 500 files and a bounded date range — for anything larger or for continuous sync, use the urls feed.
artwork-export-2026-06-07.zip
├── 1001/
│ ├── CUSTOM-BK-File-1-a1b2c3d4.png
│ └── CUSTOM-BK-File-2-a1b2c3d4.png
└── 1002/
└── CUSTOM-WH-File-1-e5f6g7h8.png
ARTWORK_EXPORT_API_KEY="replace-with-your-api-key"
curl -L --oauth2-bearer "${ARTWORK_EXPORT_API_KEY}" \
"https://your-app-url.com/api/artwork-export?shop=mystore.myshopify.com&startDate=2026-06-01&endDate=2026-06-07&format=zip" \
-o artwork.zipIf the window exceeds 500 files you'll get
400with a message to useformat=urls.
Limits & Constraints
| Limit | Value | Applies to | Notes |
|---|---|---|---|
| Maximum date range | 30 days | zip only | urls/json page via cursor instead — no range cap |
| Maximum files | 500 | zip only | Larger → use urls |
Page size (limit) | 1000 | urls/json | Default 1000; follow nextCursor for the rest |
| Presigned URL expiry | 1 hour | all | Re-request the page/zip to mint fresh URLs |
Large / peak-season pulls: use urls (the incremental feed), not zip. Server-side zipping of high-resolution artwork is memory- and time-bound (and gives no compression on already-compressed images); the feed downloads files directly from S3 in parallel and scales without limit.
Error Responses
| Status | Meaning |
|---|---|
302 | ZIP ready — redirect to a presigned S3 URL (format=zip) |
200 | Success (urls, json) |
400 | Bad request — missing/invalid params, date range > 30 days, or > 500 files for zip |
401 | Unauthorized — missing or invalid API key |
404 | No orders/files match |
500 | Server error |
{ "error": "Description of the error" }Integration Example — incremental sync (recommended)
Page the urls feed, download each file, advance the cursor. This is the production pattern for fulfillment.
Bash
ARTWORK_EXPORT_API_KEY="replace-with-your-api-key"
SHOP="mystore.myshopify.com"
BASE="https://your-app-url.com/api/artwork-export"
CURSOR="" # persist this between runs; start empty for a full sync
while :; do
URL="${BASE}?shop=${SHOP}&format=urls&limit=500"
[ -n "$CURSOR" ] && URL="${URL}&cursor=${CURSOR}"
PAGE=$(curl -sS --oauth2-bearer "${ARTWORK_EXPORT_API_KEY}" "$URL")
# download each file (parallel); skus[] tells you the product(s) to route to
printf '%s' "$PAGE" | jq -r '.files[] | [(.skus[0] // "unknown"), .fileName, .url] | @tsv' \
| while IFS=$'\t' read -r sku name url; do
mkdir -p "artwork/${sku}"
curl -sS --fail -o "artwork/${sku}/${name}" "$url"
done
CURSOR=$(printf '%s' "$PAGE" | jq -r '.nextCursor // empty')
[ -z "$CURSOR" ] && break # caught up — save CURSOR for next run
donePython
import requests
API_KEY = "YOUR_API_KEY"
SHOP = "mystore.myshopify.com"
BASE = "https://your-app-url.com/api/artwork-export"
headers = {"Authorization": f"Bearer {API_KEY}"}
cursor = None # persist between runs
while True:
params = {"shop": SHOP, "format": "urls", "limit": 500}
if cursor:
params["cursor"] = cursor
page = requests.get(BASE, params=params, headers=headers).json()
for f in page["files"]:
# f["skus"] / f["lineItemIds"] map the file to its product(s)
resp = requests.get(f["url"]) # presigned — no auth header
# ... write resp.content keyed by f["skus"][0] (or f["fileName"]), dedup on f["id"] ...
cursor = page.get("nextCursor")
if not cursor:
break # caught up — save cursor for next runRecommended practice
- Persist the cursor, advance it only after a page's downloads succeed, and resume from it next run — so you never miss or re-process an order, at any volume.
- Dedup on
files[].id— page boundaries can re-send a file. - Download within the hour (URL expiry); just re-request the page to get fresh URLs.
Support
If you hit issues, contact support with:
- Your shop domain
- The full request URL (API key redacted)
- The error response received
- Timestamp of the request