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

  1. Open your Pixel Wrangler app in Shopify Admin
  2. Navigate to Settings
  3. Find the Artwork Export API Key section
  4. Click Generate API Key
  5. 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

ParameterTypeRequiredDescription
shopstringYesYour Shopify domain (e.g., mystore.myshopify.com)
formatstringNourls (recommended), json, or zip (default)
orderIdsstringNo*Comma-separated Shopify order GIDs
orderNumbersstringNo*Comma-separated order numbers (e.g., 1001,1002)
startDatestringNo*Filter from this date, YYYY-MM-DD
endDatestringNo*Filter to this date, YYYY-MM-DD
limitnumberNourls/json page size (max 1000, default 1000)
cursorstringNourls/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 for urls/json — the cursor + page limit bound the work, so you can sync from the beginning. startDate then 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-vN filename 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 as urls, 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 a 302 redirect to a presigned S3 URL. Convenient for small, one-off downloads; not for large or continuous pulls — use urls.

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:

  1. Request a page (omit cursor to start from the oldest, or pass startDate as a floor).
  2. Download each files[].url directly from S3 (in parallel).
  3. Persist nextCursor only after the page's downloads succeed, then request the next page with &cursor=<nextCursor>.
  4. Stop when nextCursor is null. 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).
  • nextCursor is null when 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 urls and json list every sku/lineItem for 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.zip

If the window exceeds 500 files you'll get 400 with a message to use format=urls.


Limits & Constraints

LimitValueApplies toNotes
Maximum date range30 dayszip onlyurls/json page via cursor instead — no range cap
Maximum files500zip onlyLarger → use urls
Page size (limit)1000urls/jsonDefault 1000; follow nextCursor for the rest
Presigned URL expiry1 hourallRe-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

StatusMeaning
302ZIP ready — redirect to a presigned S3 URL (format=zip)
200Success (urls, json)
400Bad request — missing/invalid params, date range > 30 days, or > 500 files for zip
401Unauthorized — missing or invalid API key
404No orders/files match
500Server error
{ "error": "Description of the error" }

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
done

Python

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 run
  • 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