Docs
PrintWisp API

PrintWisp API

Put the PrintWisp designer on your own site, record the orders it sells and fetch the print-ready files. HTTPS and JSON, a secret key, webhooks for what happens later.

Getting started

Keys: test and live

A key belongs to a store. Make it in the PrintWisp portal on a Custom Website (API) store's Overview → API keys: it opens only that store's products and sizes, in its branding and currency, and its orders are that store's. A connected shop (the WooCommerce plugin) receives its keys when it connects. A key made under Organisation → API keys & webhooks belongs to no store.

A test key (sk_test_…) opens the designer and records test orders only. A live key (sk_live_…) records real orders, once your organisation is live and the store is Open. A key is shown once, and afterwards only by its first characters.

Keep the key on your server. Nothing in a browser ever needs it: the designer link your server receives is what the page opens.

sk_test_…   test orders only, any time
sk_live_…   real orders, once the organisation is live and the store is Open

Authentication

Send the key as a Bearer token on every call except GET /v1/file, GET /v1/preview, GET /v1/brand-logo, GET /v1/brand-favicon, POST /v1/connect, POST /v1/provision, GET /v1/picture/o/{organisation}/p/{product}, GET /v1/media, GET /v1/openapi.json, which carry their own credential or are public. Requests and answers are JSON (Content-Type: application/json; charset=utf-8); answers are not cached (Cache-Control: no-store).

No key, or a wrong one: 401 unauthorized, “Send Authorization: Bearer <secret key>. Keys are made in PrintWisp, on your store's Overview → API keys.”

Base URL: https://printwisp.com/v1.

curl "https://printwisp.com/v1/events?after=evt_7c2e9a1d4b8f3065&limit=50" \
  -H "Authorization: Bearer $PRINTWISP_KEY"
<?php
$ch = curl_init('https://printwisp.com/v1/events?after=evt_7c2e9a1d4b8f3065&limit=50');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . getenv('PRINTWISP_KEY'), 'Content-Type: application/json'],
]);
$answer = json_decode(curl_exec($ch), true);
// Node (your server). The key never reaches a browser.
const r = await fetch('https://printwisp.com/v1/events?after=evt_7c2e9a1d4b8f3065&limit=50', {
  headers: { Authorization: 'Bearer ' + process.env.PRINTWISP_KEY },
});
const answer = await r.json();
import os, requests

r = requests.get(
    "https://printwisp.com/v1/events?after=evt_7c2e9a1d4b8f3065&limit=50",
    headers={"Authorization": f"Bearer {os.environ['PRINTWISP_KEY']}"},
)
answer = r.json()

10-minute quickstart

Product → designer → a first test order, in six calls. Open the quickstart →

Concepts

Organisation and stores

An organisation is the business that pays PrintWisp: it owns the catalogue, the users, the billing and the print output. Under it sit any number of stores: a PrintWisp Store (hosted cart and checkout), a connected shop (WooCommerce, Shopify, …) or Custom Website (API), a store for a site you run yourself. Each store has its own products offered, branding, currency, webhooks and orders.

GET /v1/orders answers with the store an order came from: {"type": "printwisp_store" | "connection", "id", "name", "platform"}, or null.

store, as GET /v1/orders returns itCopy
{ "type": "printwisp_store", "id": "harbour-shop", "name": "Harbour Prints Shop" }
{ "type": "connection", "id": "int_2b7c9e0d4a1f6835", "name": "Harbour Prints", "platform": "woocommerce" }

Products and sizes (codes)

A product has a code (canvas) and one or more sizes, each with its own code built from the product's: canvas-40x30. The merchant edits every code in the portal; your server keys on them. A session names the size to design on as profile (its code, or its id prf_…), or the product as product when it has one size or a default size.

GET /v1/products lists what the key's store offers: products → sizes with codes, names and prices, and options with their values' codes. The store's Products tab shows the same codes, and Export codes (CSV) downloads them.

GET /v1/productsCopy
{
  "store": { "code": "web-au", "name": "Harbour Prints AU", "currency": "AUD" },
  "products": [
    {
      "code": "canvas",
      "name": "Photo canvas",
      "sizes": [
        {
          "code": "canvas-40x30",
          "name": "40 × 30 cm",
          "price": "89.00",
          "currency": "AUD",
          "packed_kg": 2.431,
          "packed_kg_extra": 2.431
        },
        {
          "code": "canvas-60x40",
          "name": "60 × 40 cm",
          "price": "149.00",
          "currency": "AUD",
          "packed_kg": 4.576,
          "packed_kg_extra": 4.576
        }
      ],
      "options": [
        {
          "code": "edge",
          "name": "Edge",
          "values": [
            { "code": "mirror", "name": "Mirrored", "price_delta": "0.00" },
            { "code": "white", "name": "White", "price_delta": "0.00" }
          ]
        }
      ]
    }
  ]
}

Designs and signatures

A design (dsn_…) is what a shopper makes in the designer. With every save the designer hands your page the design id and its signature sig: an HMAC of the design id under your organisation's signing key, which never leaves PrintWisp. Keep both on your cart line.

The signature is the proof: POST /v1/orders accepts a line that names a design with its sig; GET /v1/preview?design=&sig= returns the design's picture to whoever holds the pair. A design is frozen as it was bought when the order is recorded and cannot be reopened afterwards.

What the designer sends your pageCopy
designer.on('design:saved', function (p) {
  // p.design  "dsn_…"   the design id
  // p.sig     its signature: keep both on the cart line
});

Hand-offs

A hand-off (hnd_…) is PrintWisp's own record that a design went into a cart it filled: a PrintWisp Store's cart, the WooCommerce plugin, or an Add-to-cart connector of a connected shop. It freezes the price and the parts at that moment. An order line names either its handoff, or its design + sig when your own cart held the design. One is enough.

Two ways to name a lineCopy
{"line_ref":"1","qty":1,"handoff":"hnd_6e4b1a9c3d7f2058"}
{"line_ref":"2","qty":1,"design":"dsn_5c2a9e01b7d4f638","sig":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}
{"line_ref":"3","qty":2,"sku":"FR-OAK-A3-BLK"}

Test mode

Orders posted with a test key are test orders (ord_t…, lines oln_t…): kept in their own tables, never charged, shown in the portal while the Test mode switch is on, and removed with Delete test data. Their webhooks carry "livemode": false, GET /v1/orders answers "livemode": false, and a test key reads only test orders, lines and events (a live key only real ones).

Print files

Each order line gets a print-ready PDF. print_status moves queued → rendering → ready, or failed with an error. When it is ready, the design.print_ready webhook brings a signed link and GET /v1/print?line= answers one: url + expires_at, good for 10 minutes, pointing at GET /v1/file?l=&e=&s=, which needs no key.

A ready lineCopy
{
  "line": "oln_41aa8c3e9d2b7f16",
  "order_ref": "WEB-10021",
  "design": "dsn_5c2a9e01b7d4f638",
  "print_status": "ready",
  "components": [],
  "fields": [],
  "url": "https://printwisp.com/v1/file?l=oln_41aa8c3e9d2b7f16&e=1759640400&s=9f2c4e1a7b3d8f05c6e2a9d1b4f7c3e8a5d2b9f6c1e4a7d3b8f5c2e9a6d1b4f7",
  "expires_at": "2026-10-05T04:51:00+00:00"
}
Endpoint reference

Every route

Each route has its own page. The whole API is also one machine-readable document, GET /v1/openapi.json (OpenAPI 3.1): these pages are drawn from it.

RouteAuthWhat it does
POST/v1/sessions keyA design link for one shopper, to open on your page.
GET/v1/orders?order=|?ref= keyOne order with its store, shopper, totals and lines.
POST/v1/orders keyRecord a paid order; its print files are queued.
POST/v1/quote keyWhat a design (or a ready product, by SKU) costs, priced by PrintWisp.
GET/v1/events?after=&limit= keyWhat happened since an event: the catch-up for webhooks.
GET/v1/print?line=&test= keyA line's print status and, when ready, a 10-minute link.
GET/v1/file?l=&e=&s= signed linkThe PDF itself, for whoever holds a fresh signed link.
GET/v1/preview?design=&sig= design + sigThe design's picture (image/jpeg) for a cart line.
GET/v1/brand-logo?t=&s=&v= noneA store's logo for the designer bar (image/png, public).
GET/v1/brand-favicon?t=&s=&v= noneA store's favicon: the browser tab icon of its pages and designer (image/png, public).
POST/v1/connect one-time codeThe WooCommerce plugin swaps the approved code for its key.
POST/v1/provision nonePrintWisp's Shopify app sets a new shop up, with no sign-in.
POST/v1/login-link keyA one-time link that signs the organisation's owner in to the PrintWisp portal.
GET/v1/products keyThe store's catalogue with codes, names, prices and options.
GET/v1/picture/o/{organisation}/p/{product} noneA product's own picture, public, for a shop platform to copy.
GET/v1/catalogue?offered= keyEvery product as the store sees it, offered or not.
POST/v1/catalogue keyOffer products in the store, or stop offering them.
GET/v1/billing keyThe organisation's plan and this month's fees, for an app that bills through its platform.
POST/v1/billing keyThe shop chose a plan in its platform, or fees are now on its platform bill.
GET/v1/designs?product=&size= keyThe "Start from a design" cards for one product in the key's store.
GET/v1/media?t=&f=&size=&sig= noneA picture or font a Design's pages are drawn with (signed, public).
DELETE/v1/sessions/{session}/memory keyWhat ✕ does for a guest: this shopper's ref no longer reopens the design.
GET/v1/projects?customer_ref= keyA signed-in customer's designs: what "My designs" lists.
GET/v1/projects/{project}?customer_ref= keyOne design of the customer's.
PATCH/v1/projects/{project} keyRename a design.
DELETE/v1/projects/{project}?customer_ref= keyDelete a design from My designs.
POST/v1/projects/{project}/copy keyA copy of a design, with its pictures: Make a copy, Order again.
POST/v1/customers/claim keyProof by mailbox: designs a guest emailed to this customer's verified address join their account.
GET/v1/openapi.json noneThis OpenAPI document: one source of truth for the docs.

POST/v1/sessions

A design link for one shopper, to open on your page. Parameters, answers and errors →

curl -X POST "https://printwisp.com/v1/sessions" \
  -H "Authorization: Bearer $PRINTWISP_KEY" \
  -H "Content-Type: application/json" \
  -d '{"profile":"canvas-40x30","origin":"https://shop.example","customer_ref":"cust_8812","qty":1}'

# 201
{
  "session": "dss_3f9c1a7e5b2d8c40",
  "token": "7b1e5c0a9d3f2e8b4c6a1d7e9f0b3c5a2e4d6f8a1b3c5e7d",
  "url": "https://printwisp.com/d/7b1e5c0a9d3f2e8b4c6a1d7e9f0b3c5a2e4d6f8a1b3c5e7d",
  "expires_at": "2026-10-06T06:12:00+00:00",
  "origin": "https://shop.example",
  "product": "canvas",
  "profile": "prf_9a21c4d8e0f17b35",
  "design": null
}
<?php
$ch = curl_init('https://printwisp.com/v1/sessions');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => json_encode([
      'profile' => 'canvas-40x30',
      'origin' => 'https://shop.example',
      'customer_ref' => 'cust_8812',
      'qty' => 1
    ]),
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . getenv('PRINTWISP_KEY'), 'Content-Type: application/json'],
]);
$answer = json_decode(curl_exec($ch), true);
// Node (your server). The key never reaches a browser.
const r = await fetch('https://printwisp.com/v1/sessions', {
  method: 'POST',
  headers: { Authorization: 'Bearer ' + process.env.PRINTWISP_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({ profile: 'canvas-40x30', origin: 'https://shop.example', customer_ref: 'cust_8812', qty: 1 }),
});
const answer = await r.json();
import os, requests

r = requests.post(
    "https://printwisp.com/v1/sessions",
    json={
      "profile": "canvas-40x30",
      "origin": "https://shop.example",
      "customer_ref": "cust_8812",
      "qty": 1
    },
    headers={"Authorization": f"Bearer {os.environ['PRINTWISP_KEY']}"},
)
answer = r.json()

GET/v1/orders

One order with its store, shopper, totals and lines. Parameters, answers and errors →

curl "https://printwisp.com/v1/orders?ref=WEB-10021" \
  -H "Authorization: Bearer $PRINTWISP_KEY"

# 200
{
  "order": "ord_7d1e4b9a2c6f0583",
  "order_ref": "WEB-10021",
  "livemode": true,
  "store": null,
  "source": "api",
  "status": null,
  "placed_at": "2026-10-05T04:31:00+00:00",
  "customer": { "name": "Jane Citizen", "email": "jane@example.com" },
  "shipping": { "name": null, "address": null, "method": null, "tracking": null },
  "totals": { "subtotal": null, "shipping": null, "tax": null, "total": null, "currency": null },
  "lines": [
    {
      "line": "oln_41aa8c3e9d2b7f16",
      "line_ref": "1",
      "design": "dsn_5c2a9e01b7d4f638",
      "sku": null,
      "qty": 1,
      "unit_price": null,
      "line_amount": "89.00",
      "currency": "AUD",
      "print_status": "ready",
      "components": [
        {
          "slot": "paper",
          "name": "Paper",
          "value": "Lustre 170 gsm",
          "code": "lustre_170",
          "ref": null,
          "chosen_by": "shop",
          "price": null
        }
      ],
      "fields": []
    }
  ]
}
<?php
$ch = curl_init('https://printwisp.com/v1/orders?ref=WEB-10021');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . getenv('PRINTWISP_KEY'), 'Content-Type: application/json'],
]);
$answer = json_decode(curl_exec($ch), true);
// Node (your server). The key never reaches a browser.
const r = await fetch('https://printwisp.com/v1/orders?ref=WEB-10021', {
  headers: { Authorization: 'Bearer ' + process.env.PRINTWISP_KEY },
});
const answer = await r.json();
import os, requests

r = requests.get(
    "https://printwisp.com/v1/orders?ref=WEB-10021",
    headers={"Authorization": f"Bearer {os.environ['PRINTWISP_KEY']}"},
)
answer = r.json()

POST/v1/orders

Record a paid order; its print files are queued. Parameters, answers and errors →

curl -X POST "https://printwisp.com/v1/orders" \
  -H "Authorization: Bearer $PRINTWISP_KEY" \
  -H "Content-Type: application/json" \
  -d '{"order_ref":"WEB-10021","placed_at":"2026-10-05T04:31:00Z","currency":"AUD","customer":{"name":"Jane Citizen","email":"jane@example.com"},"lines":[{"line_ref":"1","qty":1,"amount":"89.00","design":"dsn_5c2a9e01b7d4f638","sig":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}]}'

# 201
{
  "order": "ord_7d1e4b9a2c6f0583",
  "order_ref": "WEB-10021",
  "created": true,
  "lines": [
    {
      "line": "oln_41aa8c3e9d2b7f16",
      "line_ref": "1",
      "design": "dsn_5c2a9e01b7d4f638",
      "qty": 1,
      "print_status": "queued",
      "components": [],
      "fields": []
    }
  ]
}
<?php
$ch = curl_init('https://printwisp.com/v1/orders');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => json_encode([
      'order_ref' => 'WEB-10021',
      'placed_at' => '2026-10-05T04:31:00Z',
      'currency' => 'AUD',
      'customer' => ['name' => 'Jane Citizen', 'email' => 'jane@example.com'],
      'lines' => [
        [
          'line_ref' => '1',
          'qty' => 1,
          'amount' => '89.00',
          'design' => 'dsn_5c2a9e01b7d4f638',
          'sig' => 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
        ]
      ]
    ]),
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . getenv('PRINTWISP_KEY'), 'Content-Type: application/json'],
]);
$answer = json_decode(curl_exec($ch), true);
// Node (your server). The key never reaches a browser.
const r = await fetch('https://printwisp.com/v1/orders', {
  method: 'POST',
  headers: { Authorization: 'Bearer ' + process.env.PRINTWISP_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    order_ref: 'WEB-10021',
    placed_at: '2026-10-05T04:31:00Z',
    currency: 'AUD',
    customer: { name: 'Jane Citizen', email: 'jane@example.com' },
    lines: [
      {
        line_ref: '1',
        qty: 1,
        amount: '89.00',
        design: 'dsn_5c2a9e01b7d4f638',
        sig: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'
      }
    ]
  }),
});
const answer = await r.json();
import os, requests

r = requests.post(
    "https://printwisp.com/v1/orders",
    json={
      "order_ref": "WEB-10021",
      "placed_at": "2026-10-05T04:31:00Z",
      "currency": "AUD",
      "customer": { "name": "Jane Citizen", "email": "jane@example.com" },
      "lines": [
        {
          "line_ref": "1",
          "qty": 1,
          "amount": "89.00",
          "design": "dsn_5c2a9e01b7d4f638",
          "sig": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
        }
      ]
    },
    headers={"Authorization": f"Bearer {os.environ['PRINTWISP_KEY']}"},
)
answer = r.json()

POST/v1/quote

What a design (or a ready product, by SKU) costs, priced by PrintWisp. Parameters, answers and errors →

curl -X POST "https://printwisp.com/v1/quote" \
  -H "Authorization: Bearer $PRINTWISP_KEY" \
  -H "Content-Type: application/json" \
  -d '{"design":"dsn_5c2a9e01b7d4f638","sig":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","qty":1}'

# 200
{
  "handoff": "hnd_6e4b1a9c3d7f2058",
  "handoff_sig": "9b1c…",
  "design": "dsn_5c2a9e01b7d4f638",
  "unit_price": "585.00",
  "currency": "AUD",
  "qty": 1,
  "sku": "pl-hc-10x10",
  "pages": 40,
  "product": "pl-hc-10x10",
  "product_name": "Premium Lay-flat 10x10in Square",
  "choices": { "Cover finish": "Matte Lamination" }
}
<?php
$ch = curl_init('https://printwisp.com/v1/quote');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => json_encode([
      'design' => 'dsn_5c2a9e01b7d4f638',
      'sig' => 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
      'qty' => 1
    ]),
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . getenv('PRINTWISP_KEY'), 'Content-Type: application/json'],
]);
$answer = json_decode(curl_exec($ch), true);
// Node (your server). The key never reaches a browser.
const r = await fetch('https://printwisp.com/v1/quote', {
  method: 'POST',
  headers: { Authorization: 'Bearer ' + process.env.PRINTWISP_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({
    design: 'dsn_5c2a9e01b7d4f638',
    sig: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
    qty: 1
  }),
});
const answer = await r.json();
import os, requests

r = requests.post(
    "https://printwisp.com/v1/quote",
    json={
      "design": "dsn_5c2a9e01b7d4f638",
      "sig": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
      "qty": 1
    },
    headers={"Authorization": f"Bearer {os.environ['PRINTWISP_KEY']}"},
)
answer = r.json()

GET/v1/events

What happened since an event: the catch-up for webhooks. Parameters, answers and errors →

curl "https://printwisp.com/v1/events?after=evt_7c2e9a1d4b8f3065&limit=50" \
  -H "Authorization: Bearer $PRINTWISP_KEY"

# 200
{
  "events": [
    {
      "id": "evt_8a0d3c7e1f5b2946",
      "type": "order.created",
      "created": "2026-10-05T04:31:02+00:00",
      "data": { "order": "ord_7d1e4b9a2c6f0583", "order_ref": "WEB-10021" }
    },
    {
      "id": "evt_9b1e4d8f2a6c3057",
      "type": "design.print_ready",
      "created": "2026-10-05T04:31:40+00:00",
      "data": { "order": "ord_7d1e4b9a2c6f0583", "line": "oln_41aa8c3e9d2b7f16" }
    }
  ],
  "has_more": false
}
<?php
$ch = curl_init('https://printwisp.com/v1/events?after=evt_7c2e9a1d4b8f3065&limit=50');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . getenv('PRINTWISP_KEY'), 'Content-Type: application/json'],
]);
$answer = json_decode(curl_exec($ch), true);
// Node (your server). The key never reaches a browser.
const r = await fetch('https://printwisp.com/v1/events?after=evt_7c2e9a1d4b8f3065&limit=50', {
  headers: { Authorization: 'Bearer ' + process.env.PRINTWISP_KEY },
});
const answer = await r.json();
import os, requests

r = requests.get(
    "https://printwisp.com/v1/events?after=evt_7c2e9a1d4b8f3065&limit=50",
    headers={"Authorization": f"Bearer {os.environ['PRINTWISP_KEY']}"},
)
answer = r.json()

GET/v1/print

A line's print status and, when ready, a 10-minute link. Parameters, answers and errors →

curl "https://printwisp.com/v1/print?line=oln_41aa8c3e9d2b7f16" \
  -H "Authorization: Bearer $PRINTWISP_KEY"

# 200
{
  "line": "oln_41aa8c3e9d2b7f16",
  "order_ref": "WEB-10021",
  "design": "dsn_5c2a9e01b7d4f638",
  "print_status": "ready",
  "components": [],
  "fields": [],
  "url": "https://printwisp.com/v1/file?l=oln_41aa8c3e9d2b7f16&e=1759640400&s=9f2c4e1a7b3d8f05c6e2a9d1b4f7c3e8a5d2b9f6c1e4a7d3b8f5c2e9a6d1b4f7",
  "expires_at": "2026-10-05T04:51:00+00:00"
}
<?php
$ch = curl_init('https://printwisp.com/v1/print?line=oln_41aa8c3e9d2b7f16');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . getenv('PRINTWISP_KEY'), 'Content-Type: application/json'],
]);
$answer = json_decode(curl_exec($ch), true);
// Node (your server). The key never reaches a browser.
const r = await fetch('https://printwisp.com/v1/print?line=oln_41aa8c3e9d2b7f16', {
  headers: { Authorization: 'Bearer ' + process.env.PRINTWISP_KEY },
});
const answer = await r.json();
import os, requests

r = requests.get(
    "https://printwisp.com/v1/print?line=oln_41aa8c3e9d2b7f16",
    headers={"Authorization": f"Bearer {os.environ['PRINTWISP_KEY']}"},
)
answer = r.json()

GET/v1/file

The PDF itself, for whoever holds a fresh signed link. Parameters, answers and errors →

curl "https://printwisp.com/v1/file?l=oln_41aa8c3e9d2b7f16&e=1759640400&s=9f2c4e1a7b3d8f05c6e2a9d1b4f7c3e8a5d2b9f6c1e4a7d3b8f5c2e9a6d1b4f7" \
  -o WEB-10021-1.pdf
<?php
file_put_contents('WEB-10021-1.pdf', file_get_contents('https://printwisp.com/v1/file?l=oln_41aa8c3e9d2b7f16&e=1759640400&s=9f2c4e1a7b3d8f05c6e2a9d1b4f7c3e8a5d2b9f6c1e4a7d3b8f5c2e9a6d1b4f7'));
// Node
const r = await fetch('https://printwisp.com/v1/file?l=oln_41aa8c3e9d2b7f16&e=1759640400&s=9f2c4e1a7b3d8f05c6e2a9d1b4f7c3e8a5d2b9f6c1e4a7d3b8f5c2e9a6d1b4f7');
require('fs').writeFileSync('WEB-10021-1.pdf', Buffer.from(await r.arrayBuffer()));
import requests

r = requests.get(
    "https://printwisp.com/v1/file?l=oln_41aa8c3e9d2b7f16&e=1759640400&s=9f2c4e1a7b3d8f05c6e2a9d1b4f7c3e8a5d2b9f6c1e4a7d3b8f5c2e9a6d1b4f7",
)
open("WEB-10021-1.pdf", "wb").write(r.content)

GET/v1/preview

The design's picture (image/jpeg) for a cart line. Parameters, answers and errors →

curl "https://printwisp.com/v1/preview?design=dsn_5c2a9e01b7d4f638&sig=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" \
  -o design.jpg
<?php
file_put_contents('design.jpg', file_get_contents('https://printwisp.com/v1/preview?design=dsn_5c2a9e01b7d4f638&sig=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855'));
// Node
const r = await fetch('https://printwisp.com/v1/preview?design=dsn_5c2a9e01b7d4f638&sig=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855');
require('fs').writeFileSync('design.jpg', Buffer.from(await r.arrayBuffer()));
import requests

r = requests.get(
    "https://printwisp.com/v1/preview?design=dsn_5c2a9e01b7d4f638&sig=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
)
open("design.jpg", "wb").write(r.content)

GET/v1/brand-favicon

A store's favicon: the browser tab icon of its pages and designer (image/png, public). Parameters, answers and errors →

GET /v1/brand-faviconCopy
curl "https://printwisp.com/v1/brand-favicon?t=ten_7c0a1b2e9d4f&v=3" \
  -o favicon.png
<?php
file_put_contents('favicon.png', file_get_contents('https://printwisp.com/v1/brand-favicon?t=ten_7c0a1b2e9d4f&v=3'));
// Node
const r = await fetch('https://printwisp.com/v1/brand-favicon?t=ten_7c0a1b2e9d4f&v=3');
require('fs').writeFileSync('favicon.png', Buffer.from(await r.arrayBuffer()));
import requests

r = requests.get(
    "https://printwisp.com/v1/brand-favicon?t=ten_7c0a1b2e9d4f&v=3",
)
open("favicon.png", "wb").write(r.content)

POST/v1/connect

The WooCommerce plugin swaps the approved code for its key. Parameters, answers and errors →

curl -X POST "https://printwisp.com/v1/connect" \
  -H "Content-Type: application/json" \
  -d '{"code":"0f3a9c1e5b7d2f4a6c8e0b2d4f6a8c1e3b5d7f9a2c4e6b8d","site":"https://harbourprints.com.au"}'

# 200
{
  "ok": true,
  "key": "sk_live_…",
  "keys": { "test": "sk_test_…", "live": "sk_live_…" },
  "webhook_secret": "whsec_…",
  "studio_url": "https://printwisp.com",
  "integration": "int_2b7c9e0d4a1f6835",
  "store": { "id": "ten_harbour", "name": "Harbour Prints", "code": "harbour-woo", "status": "draft" }
}
<?php
$ch = curl_init('https://printwisp.com/v1/connect');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => json_encode([
      'code' => '0f3a9c1e5b7d2f4a6c8e0b2d4f6a8c1e3b5d7f9a2c4e6b8d',
      'site' => 'https://harbourprints.com.au'
    ]),
    CURLOPT_HTTPHEADER     => ['Content-Type: application/json'],
]);
$answer = json_decode(curl_exec($ch), true);
// Node
const r = await fetch('https://printwisp.com/v1/connect', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    code: '0f3a9c1e5b7d2f4a6c8e0b2d4f6a8c1e3b5d7f9a2c4e6b8d',
    site: 'https://harbourprints.com.au'
  }),
});
const answer = await r.json();
import requests

r = requests.post(
    "https://printwisp.com/v1/connect",
    json={
      "code": "0f3a9c1e5b7d2f4a6c8e0b2d4f6a8c1e3b5d7f9a2c4e6b8d",
      "site": "https://harbourprints.com.au"
    },
)
answer = r.json()

POST/v1/provision

PrintWisp's Shopify app sets a new shop up, with no sign-in. Parameters, answers and errors →

POST /v1/provisionCopy
curl -X POST "https://printwisp.com/v1/provision" \
  -H "Content-Type: application/json"
<?php
$ch = curl_init('https://printwisp.com/v1/provision');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => json_encode(null),
    CURLOPT_HTTPHEADER     => ['Content-Type: application/json'],
]);
$answer = json_decode(curl_exec($ch), true);
// Node
const r = await fetch('https://printwisp.com/v1/provision', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
});
const answer = await r.json();
import requests

r = requests.post(
    "https://printwisp.com/v1/provision",
)
answer = r.json()

GET/v1/products

The store's catalogue with codes, names, prices and options. Parameters, answers and errors →

curl "https://printwisp.com/v1/products" \
  -H "Authorization: Bearer $PRINTWISP_KEY"

# 200
{
  "store": { "code": "web-au", "name": "Harbour Prints AU", "currency": "AUD" },
  "products": [
    {
      "code": "canvas",
      "name": "Photo canvas",
      "sizes": [
        {
          "code": "canvas-40x30",
          "name": "40 × 30 cm",
          "price": "89.00",
          "currency": "AUD",
          "packed_kg": 2.431,
          "packed_kg_extra": 2.431
        },
        {
          "code": "canvas-60x40",
          "name": "60 × 40 cm",
          "price": "149.00",
          "currency": "AUD",
          "packed_kg": 4.576,
          "packed_kg_extra": 4.576
        }
      ],
      "options": [
        {
          "code": "edge",
          "name": "Edge",
          "values": [
            { "code": "mirror", "name": "Mirrored", "price_delta": "0.00" },
            { "code": "white", "name": "White", "price_delta": "0.00" }
          ]
        }
      ]
    }
  ]
}
<?php
$ch = curl_init('https://printwisp.com/v1/products');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . getenv('PRINTWISP_KEY'), 'Content-Type: application/json'],
]);
$answer = json_decode(curl_exec($ch), true);
// Node (your server). The key never reaches a browser.
const r = await fetch('https://printwisp.com/v1/products', {
  headers: { Authorization: 'Bearer ' + process.env.PRINTWISP_KEY },
});
const answer = await r.json();
import os, requests

r = requests.get(
    "https://printwisp.com/v1/products",
    headers={"Authorization": f"Bearer {os.environ['PRINTWISP_KEY']}"},
)
answer = r.json()

GET/v1/picture/o/{organisation}/p/{product}

A product's own picture, public, for a shop platform to copy. Parameters, answers and errors →

GET /v1/picture/o/{organisation}/p/{product}Copy
curl "https://printwisp.com/v1/picture/o/{organisation}/p/{product}" \
  -o file
<?php
file_put_contents('file', file_get_contents('https://printwisp.com/v1/picture/o/{organisation}/p/{product}'));
// Node
const r = await fetch('https://printwisp.com/v1/picture/o/{organisation}/p/{product}');
require('fs').writeFileSync('file', Buffer.from(await r.arrayBuffer()));
import requests

r = requests.get(
    "https://printwisp.com/v1/picture/o/{organisation}/p/{product}",
)
open("file", "wb").write(r.content)

GET/v1/catalogue

Every product as the store sees it, offered or not. Parameters, answers and errors →

curl "https://printwisp.com/v1/catalogue" \
  -H "Authorization: Bearer $PRINTWISP_KEY"

# 200
{
  "store": { "code": "web-au", "name": "Harbour Prints AU", "currency": "AUD" },
  "products": [
    {
      "code": "layflat",
      "name": "Lay-flat photobook",
      "listed": false,
      "sizes": [{ "code": "layflat-8x8", "name": "8 x 8 in", "price": "345.00", "currency": "AUD" }],
      "unpriced_sizes": ["12 x 12 in"],
      "from_price": "345.00",
      "currency": "AUD",
      "quantity_priced": false,
      "blocked": null
    }
  ]
}
<?php
$ch = curl_init('https://printwisp.com/v1/catalogue');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . getenv('PRINTWISP_KEY'), 'Content-Type: application/json'],
]);
$answer = json_decode(curl_exec($ch), true);
// Node (your server). The key never reaches a browser.
const r = await fetch('https://printwisp.com/v1/catalogue', {
  headers: { Authorization: 'Bearer ' + process.env.PRINTWISP_KEY },
});
const answer = await r.json();
import os, requests

r = requests.get(
    "https://printwisp.com/v1/catalogue",
    headers={"Authorization": f"Bearer {os.environ['PRINTWISP_KEY']}"},
)
answer = r.json()

POST/v1/catalogue

Offer products in the store, or stop offering them. Parameters, answers and errors →

POST /v1/catalogueCopy
curl -X POST "https://printwisp.com/v1/catalogue" \
  -H "Authorization: Bearer $PRINTWISP_KEY" \
  -H "Content-Type: application/json" \
  -d '{"codes":["layflat"],"listed":true}'

# 200
{ "changed": ["layflat"], "unknown": [] }
<?php
$ch = curl_init('https://printwisp.com/v1/catalogue');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => json_encode(['codes' => ['layflat'], 'listed' => true]),
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . getenv('PRINTWISP_KEY'), 'Content-Type: application/json'],
]);
$answer = json_decode(curl_exec($ch), true);
// Node (your server). The key never reaches a browser.
const r = await fetch('https://printwisp.com/v1/catalogue', {
  method: 'POST',
  headers: { Authorization: 'Bearer ' + process.env.PRINTWISP_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({ codes: ['layflat'], listed: true }),
});
const answer = await r.json();
import os, requests

r = requests.post(
    "https://printwisp.com/v1/catalogue",
    json={ "codes": ["layflat"], "listed": True },
    headers={"Authorization": f"Bearer {os.environ['PRINTWISP_KEY']}"},
)
answer = r.json()

GET/v1/billing

The organisation's plan and this month's fees, for an app that bills through its platform. Parameters, answers and errors →

curl "https://printwisp.com/v1/billing" \
  -H "Authorization: Bearer $PRINTWISP_KEY"

# 200
{
  "provider": "shopify",
  "status": "active",
  "trial_ends_at": null,
  "shop": "harbour-prints.myshopify.com",
  "subscription": "gid://shopify/AppSubscription/1029384756",
  "plan": {
    "code": "growth",
    "name": "Growth",
    "monthly_price": "29.00",
    "currency": "AUD",
    "fee_percent": "3.00",
    "tiers": []
  },
  "month": { "AUD": { "sales": 1170, "fees": 35.1, "lines": 4 } },
  "unreported": [
    {
      "id": 812,
      "order_ref": "#1042",
      "sales": "345.00",
      "currency": "AUD",
      "fee_percent": "3.00",
      "fee": "10.35",
      "placed_at": "2026-10-05T04:31:02+00:00"
    }
  ]
}
<?php
$ch = curl_init('https://printwisp.com/v1/billing');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . getenv('PRINTWISP_KEY'), 'Content-Type: application/json'],
]);
$answer = json_decode(curl_exec($ch), true);
// Node (your server). The key never reaches a browser.
const r = await fetch('https://printwisp.com/v1/billing', {
  headers: { Authorization: 'Bearer ' + process.env.PRINTWISP_KEY },
});
const answer = await r.json();
import os, requests

r = requests.get(
    "https://printwisp.com/v1/billing",
    headers={"Authorization": f"Bearer {os.environ['PRINTWISP_KEY']}"},
)
answer = r.json()

POST/v1/billing

The shop chose a plan in its platform, or fees are now on its platform bill. Parameters, answers and errors →

curl -X POST "https://printwisp.com/v1/billing" \
  -H "Authorization: Bearer $PRINTWISP_KEY" \
  -H "Content-Type: application/json" \
  -d '{"reported":{"ids":[812],"ref":"gid://shopify/AppUsageRecord/5566778899"}}'

# 200
{ "marked": 1 }
<?php
$ch = curl_init('https://printwisp.com/v1/billing');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => json_encode(['reported' => ['ids' => [812], 'ref' => 'gid://shopify/AppUsageRecord/5566778899']]),
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . getenv('PRINTWISP_KEY'), 'Content-Type: application/json'],
]);
$answer = json_decode(curl_exec($ch), true);
// Node (your server). The key never reaches a browser.
const r = await fetch('https://printwisp.com/v1/billing', {
  method: 'POST',
  headers: { Authorization: 'Bearer ' + process.env.PRINTWISP_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({ reported: { ids: [812], ref: 'gid://shopify/AppUsageRecord/5566778899' } }),
});
const answer = await r.json();
import os, requests

r = requests.post(
    "https://printwisp.com/v1/billing",
    json={ "reported": { "ids": [812], "ref": "gid://shopify/AppUsageRecord/5566778899" } },
    headers={"Authorization": f"Bearer {os.environ['PRINTWISP_KEY']}"},
)
answer = r.json()

GET/v1/designs

The "Start from a design" cards for one product in the key's store. Parameters, answers and errors →

curl "https://printwisp.com/v1/designs?product=calendar2027-a4-v2" \
  -H "Authorization: Bearer $PRINTWISP_KEY"

# 200
{
  "product": "calendar2027-a4-v2",
  "size": "calendar2027-a4-v2",
  "store": "photiles",
  "design_yours": { "label": "Design yours", "starts_on": "plain_pages" },
  "designs": [
    {
      "code": "calendar2027-rose",
      "name": "Rose stripes",
      "description": "Plum and rose stripes, a split year in white and teal.",
      "collection": "Patterns",
      "type": "layouts",
      "fit": "fits",
      "aspect": 1.4028,
      "pages": { "cover": "<svg ...>", "opening": "<svg ...>", "back": "<svg ...>" }
    }
  ]
}
<?php
$ch = curl_init('https://printwisp.com/v1/designs?product=calendar2027-a4-v2');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . getenv('PRINTWISP_KEY'), 'Content-Type: application/json'],
]);
$answer = json_decode(curl_exec($ch), true);
// Node (your server). The key never reaches a browser.
const r = await fetch('https://printwisp.com/v1/designs?product=calendar2027-a4-v2', {
  headers: { Authorization: 'Bearer ' + process.env.PRINTWISP_KEY },
});
const answer = await r.json();
import os, requests

r = requests.get(
    "https://printwisp.com/v1/designs?product=calendar2027-a4-v2",
    headers={"Authorization": f"Bearer {os.environ['PRINTWISP_KEY']}"},
)
answer = r.json()

GET/v1/media

A picture or font a Design's pages are drawn with (signed, public). Parameters, answers and errors →

curl "https://printwisp.com/v1/media?t=ten_7c0a1b2e9d4f&f=ast_c41c3bf6c1bfd5d5&size=screen&sig=0f1e2d3c4b5a69788796a5b4c3d2e1f0" \
  -o picture.webp
<?php
file_put_contents('picture.webp', file_get_contents('https://printwisp.com/v1/media?t=ten_7c0a1b2e9d4f&f=ast_c41c3bf6c1bfd5d5&size=screen&sig=0f1e2d3c4b5a69788796a5b4c3d2e1f0'));
// Node
const r = await fetch('https://printwisp.com/v1/media?t=ten_7c0a1b2e9d4f&f=ast_c41c3bf6c1bfd5d5&size=screen&sig=0f1e2d3c4b5a69788796a5b4c3d2e1f0');
require('fs').writeFileSync('picture.webp', Buffer.from(await r.arrayBuffer()));
import requests

r = requests.get(
    "https://printwisp.com/v1/media?t=ten_7c0a1b2e9d4f&f=ast_c41c3bf6c1bfd5d5&size=screen&sig=0f1e2d3c4b5a69788796a5b4c3d2e1f0",
)
open("picture.webp", "wb").write(r.content)

DELETE/v1/sessions/{session}/memory

What ✕ does for a guest: this shopper's ref no longer reopens the design. Parameters, answers and errors →

DELETE /v1/sessions/{session}/memoryCopy
curl -X DELETE "https://printwisp.com/v1/sessions/dss_3f9c1a7e5b2d8c40/memory" \
  -H "Authorization: Bearer $PRINTWISP_KEY"

# 200
{ "forgotten": 2, "link": true }
<?php
$ch = curl_init('https://printwisp.com/v1/sessions/dss_3f9c1a7e5b2d8c40/memory');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'DELETE',
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . getenv('PRINTWISP_KEY'), 'Content-Type: application/json'],
]);
$answer = json_decode(curl_exec($ch), true);
// Node (your server). The key never reaches a browser.
const r = await fetch('https://printwisp.com/v1/sessions/dss_3f9c1a7e5b2d8c40/memory', {
  method: 'DELETE',
  headers: { Authorization: 'Bearer ' + process.env.PRINTWISP_KEY },
});
const answer = await r.json();
import os, requests

r = requests.delete(
    "https://printwisp.com/v1/sessions/dss_3f9c1a7e5b2d8c40/memory",
    headers={"Authorization": f"Bearer {os.environ['PRINTWISP_KEY']}"},
)
answer = r.json()

GET/v1/projects

A signed-in customer's designs: what "My designs" lists. Parameters, answers and errors →

curl "https://printwisp.com/v1/projects?customer_ref=wp-user-12" \
  -H "Authorization: Bearer $PRINTWISP_KEY"

# 200
{
  "customer_ref": "wp-user-12",
  "projects": [
    {
      "id": "dsn_5c2a9e01b7d4f638",
      "name": "Our Story",
      "cover": "https://printwisp.com/v1/preview?design=dsn_5c2a9e01b7d4f638&sig=3e1f…&v=1761300720",
      "product": { "code": "petite", "name": "The Petite" },
      "size": { "code": "petite-8x6", "name": "8×6 in Landscape" },
      "pages": 20,
      "updated_at": "2026-10-24T06:32:00+00:00",
      "status": "in_progress",
      "order_ref": null,
      "link": { "emailed_at": "2026-10-24T06:32:00+00:00", "to": "s…h.t@gmail.com", "reminder": false },
      "joined": "email",
      "note": "saved_by_email"
    }
  ]
}
<?php
$ch = curl_init('https://printwisp.com/v1/projects?customer_ref=wp-user-12');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . getenv('PRINTWISP_KEY'), 'Content-Type: application/json'],
]);
$answer = json_decode(curl_exec($ch), true);
// Node (your server). The key never reaches a browser.
const r = await fetch('https://printwisp.com/v1/projects?customer_ref=wp-user-12', {
  headers: { Authorization: 'Bearer ' + process.env.PRINTWISP_KEY },
});
const answer = await r.json();
import os, requests

r = requests.get(
    "https://printwisp.com/v1/projects?customer_ref=wp-user-12",
    headers={"Authorization": f"Bearer {os.environ['PRINTWISP_KEY']}"},
)
answer = r.json()

GET/v1/projects/{project}

One design of the customer's. Parameters, answers and errors →

GET /v1/projects/{project}Copy
curl "https://printwisp.com/v1/projects/dsn_5c2a9e01b7d4f638?customer_ref=wp-user-12" \
  -H "Authorization: Bearer $PRINTWISP_KEY"

# 200
{
  "project": {
    "id": "dsn_5c2a9e01b7d4f638",
    "name": "Our Story",
    "cover": "https://printwisp.com/v1/preview?design=dsn_5c2a9e01b7d4f638&sig=3e1f…&v=1761300720",
    "product": { "code": "petite", "name": "The Petite" },
    "size": { "code": "petite-8x6", "name": "8×6 in Landscape" },
    "pages": 20,
    "updated_at": "2026-10-24T06:32:00+00:00",
    "status": "in_progress",
    "order_ref": null,
    "link": { "emailed_at": "2026-10-24T06:32:00+00:00", "to": "s…h.t@gmail.com", "reminder": false },
    "joined": "email",
    "note": "saved_by_email"
  }
}
<?php
$ch = curl_init('https://printwisp.com/v1/projects/dsn_5c2a9e01b7d4f638?customer_ref=wp-user-12');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . getenv('PRINTWISP_KEY'), 'Content-Type: application/json'],
]);
$answer = json_decode(curl_exec($ch), true);
// Node (your server). The key never reaches a browser.
const r = await fetch('https://printwisp.com/v1/projects/dsn_5c2a9e01b7d4f638?customer_ref=wp-user-12', {
  headers: { Authorization: 'Bearer ' + process.env.PRINTWISP_KEY },
});
const answer = await r.json();
import os, requests

r = requests.get(
    "https://printwisp.com/v1/projects/dsn_5c2a9e01b7d4f638?customer_ref=wp-user-12",
    headers={"Authorization": f"Bearer {os.environ['PRINTWISP_KEY']}"},
)
answer = r.json()

PATCH/v1/projects/{project}

Rename a design. Parameters, answers and errors →

PATCH /v1/projects/{project}Copy
curl -X PATCH "https://printwisp.com/v1/projects/dsn_5c2a9e01b7d4f638" \
  -H "Authorization: Bearer $PRINTWISP_KEY" \
  -H "Content-Type: application/json" \
  -d '{"customer_ref":"wp-user-12","name":"Our Story, 2026"}'

# 200
{
  "project": {
    "id": "dsn_5c2a9e01b7d4f638",
    "name": "Our Story, 2026",
    "cover": "https://printwisp.com/v1/preview?design=dsn_5c2a9e01b7d4f638&sig=3e1f…&v=1761300720",
    "product": { "code": "petite", "name": "The Petite" },
    "size": { "code": "petite-8x6", "name": "8×6 in Landscape" },
    "pages": 20,
    "updated_at": "2026-10-24T06:32:00+00:00",
    "status": "in_progress",
    "order_ref": null,
    "link": { "emailed_at": "2026-10-24T06:32:00+00:00", "to": "s…h.t@gmail.com", "reminder": false },
    "joined": null,
    "note": null
  }
}
<?php
$ch = curl_init('https://printwisp.com/v1/projects/dsn_5c2a9e01b7d4f638');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'PATCH',
    CURLOPT_POSTFIELDS     => json_encode(['customer_ref' => 'wp-user-12', 'name' => 'Our Story, 2026']),
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . getenv('PRINTWISP_KEY'), 'Content-Type: application/json'],
]);
$answer = json_decode(curl_exec($ch), true);
// Node (your server). The key never reaches a browser.
const r = await fetch('https://printwisp.com/v1/projects/dsn_5c2a9e01b7d4f638', {
  method: 'PATCH',
  headers: { Authorization: 'Bearer ' + process.env.PRINTWISP_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({ customer_ref: 'wp-user-12', name: 'Our Story, 2026' }),
});
const answer = await r.json();
import os, requests

r = requests.patch(
    "https://printwisp.com/v1/projects/dsn_5c2a9e01b7d4f638",
    json={ "customer_ref": "wp-user-12", "name": "Our Story, 2026" },
    headers={"Authorization": f"Bearer {os.environ['PRINTWISP_KEY']}"},
)
answer = r.json()

DELETE/v1/projects/{project}

Delete a design from My designs. Parameters, answers and errors →

DELETE /v1/projects/{project}Copy
curl -X DELETE "https://printwisp.com/v1/projects/dsn_5c2a9e01b7d4f638?customer_ref=wp-user-12" \
  -H "Authorization: Bearer $PRINTWISP_KEY"

# 200
{ "deleted": true, "project": "dsn_5c2a9e01b7d4f638", "how": "deleted" }
<?php
$ch = curl_init('https://printwisp.com/v1/projects/dsn_5c2a9e01b7d4f638?customer_ref=wp-user-12');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'DELETE',
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . getenv('PRINTWISP_KEY'), 'Content-Type: application/json'],
]);
$answer = json_decode(curl_exec($ch), true);
// Node (your server). The key never reaches a browser.
const r = await fetch('https://printwisp.com/v1/projects/dsn_5c2a9e01b7d4f638?customer_ref=wp-user-12', {
  method: 'DELETE',
  headers: { Authorization: 'Bearer ' + process.env.PRINTWISP_KEY },
});
const answer = await r.json();
import os, requests

r = requests.delete(
    "https://printwisp.com/v1/projects/dsn_5c2a9e01b7d4f638?customer_ref=wp-user-12",
    headers={"Authorization": f"Bearer {os.environ['PRINTWISP_KEY']}"},
)
answer = r.json()

POST/v1/projects/{project}/copy

A copy of a design, with its pictures: Make a copy, Order again. Parameters, answers and errors →

POST /v1/projects/{project}/copyCopy
curl -X POST "https://printwisp.com/v1/projects/dsn_5c2a9e01b7d4f638/copy" \
  -H "Authorization: Bearer $PRINTWISP_KEY" \
  -H "Content-Type: application/json" \
  -d '{"customer_ref":"wp-user-12"}'

# 201
{
  "project": {
    "id": "dsn_0e4b7c2d9a1f5386",
    "name": "Our Story",
    "cover": "https://printwisp.com/v1/preview?design=dsn_5c2a9e01b7d4f638&sig=3e1f…&v=1761300720",
    "product": { "code": "petite", "name": "The Petite" },
    "size": { "code": "petite-8x6", "name": "8×6 in Landscape" },
    "pages": 20,
    "updated_at": "2026-10-24T06:32:00+00:00",
    "status": "in_progress",
    "order_ref": null,
    "link": null,
    "joined": null,
    "note": null
  }
}
<?php
$ch = curl_init('https://printwisp.com/v1/projects/dsn_5c2a9e01b7d4f638/copy');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => json_encode(['customer_ref' => 'wp-user-12']),
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . getenv('PRINTWISP_KEY'), 'Content-Type: application/json'],
]);
$answer = json_decode(curl_exec($ch), true);
// Node (your server). The key never reaches a browser.
const r = await fetch('https://printwisp.com/v1/projects/dsn_5c2a9e01b7d4f638/copy', {
  method: 'POST',
  headers: { Authorization: 'Bearer ' + process.env.PRINTWISP_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({ customer_ref: 'wp-user-12' }),
});
const answer = await r.json();
import os, requests

r = requests.post(
    "https://printwisp.com/v1/projects/dsn_5c2a9e01b7d4f638/copy",
    json={ "customer_ref": "wp-user-12" },
    headers={"Authorization": f"Bearer {os.environ['PRINTWISP_KEY']}"},
)
answer = r.json()

POST/v1/customers/claim

Proof by mailbox: designs a guest emailed to this customer's verified address join their account. Parameters, answers and errors →

POST /v1/customers/claimCopy
curl -X POST "https://printwisp.com/v1/customers/claim" \
  -H "Authorization: Bearer $PRINTWISP_KEY" \
  -H "Content-Type: application/json" \
  -d '{"customer_ref":"wp-user-12","email":"sarah.t@gmail.com","verified":true}'

# 200
{ "customer_ref": "wp-user-12", "claimed": 1 }
<?php
$ch = curl_init('https://printwisp.com/v1/customers/claim');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_POSTFIELDS     => json_encode(['customer_ref' => 'wp-user-12', 'email' => 'sarah.t@gmail.com', 'verified' => true]),
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . getenv('PRINTWISP_KEY'), 'Content-Type: application/json'],
]);
$answer = json_decode(curl_exec($ch), true);
// Node (your server). The key never reaches a browser.
const r = await fetch('https://printwisp.com/v1/customers/claim', {
  method: 'POST',
  headers: { Authorization: 'Bearer ' + process.env.PRINTWISP_KEY, 'Content-Type': 'application/json' },
  body: JSON.stringify({ customer_ref: 'wp-user-12', email: 'sarah.t@gmail.com', verified: true }),
});
const answer = await r.json();
import os, requests

r = requests.post(
    "https://printwisp.com/v1/customers/claim",
    json={ "customer_ref": "wp-user-12", "email": "sarah.t@gmail.com", "verified": True },
    headers={"Authorization": f"Bearer {os.environ['PRINTWISP_KEY']}"},
)
answer = r.json()

GET/v1/openapi.json

This OpenAPI document: one source of truth for the docs. Parameters, answers and errors →

GET /v1/openapi.jsonCopy
curl "https://printwisp.com/v1/openapi.json"
<?php
$answer = json_decode(file_get_contents('https://printwisp.com/v1/openapi.json'), true);
// Node
const r = await fetch('https://printwisp.com/v1/openapi.json');
const answer = await r.json();
import requests

r = requests.get(
    "https://printwisp.com/v1/openapi.json",
)
answer = r.json()
Webhooks

Webhooks

PrintWisp POSTs signed JSON to your server when an order is placed and when a print file is ready or failed. Endpoints are made in the portal: Organisation → API keys & webhooks (every store's events), or a PrintWisp Store's Webhooks tab (that store's). Events, payloads, signature and retries →

A deliveryCopy
POST /your/hook HTTP/1.1
Content-Type: application/json
User-Agent: Studio-Webhooks/1
Studio-Event: <type>
Studio-Delivery: whd_…
Studio-Signature: t=…,v1=…

{"id":"evt_1d3a6f0b4c8e5279","type":"ping","created":"2026-10-05T04:30:00+00:00","data":{"message":"A test from Studio."}}
Embedding

embed.js: options

<script src="https://printwisp.com/studio/js/embed.js"></script> gives your page StudioEmbed.open(options). It needs no key: it opens the url your server got from POST /v1/sessions, in a full-screen overlay or inside your own box. ES5, no dependencies.

ParameterTypeDescription
url requiredstringThe session's url from POST /v1/sessions; http(s) only.
containerselector | elementOpen inside this box instead of the full-screen overlay (min-height 560 px).
titlestringThe product name in the overlay's bar on a phone, and the frame's title.
heightCSS lengthThe frame's height inside a container (default 100%).
rememberstringA key of yours, say the product code: this tab keeps the open link so StudioEmbed.reopen(key) brings the same designer back after a refresh (embed.js 3). Left out: nothing is kept.
rememberAddressbooleanWith remember: false keeps the design id out of the page's address (#pw=); it is still kept in this browser (default true).
guestbooleanWith remember, for a shopper who is not signed in (My designs, embed 4): the tab keeps the open designer for a refresh or Back, and nothing else -- no design id in this browser or the address. Closing the designer forgets it.

The returned object: design (The design id, after the first save), sig (The design's signature, after the first save), frame (The iframe), on(type, fn) (Listen to a message), off(type, fn) (Stop listening), save() (A Promise for the next design:saved; rejects with {code: "timeout"} after 15 s, or {code: "closed"}), close() (Overlay only: closes it and fires close), destroy() (Removes the frame and every listener).

Messages are believed only from the designer's own window and origin, on a channel id made for this one frame and kept in the link's fragment, which never reaches a server. Nothing is ever posted to *.

Open the designerCopy
// the url your server got from POST /v1/sessions
const d = StudioEmbed.open({ url: session.url, title: 'Photo canvas 40 × 30 cm' });
// or inside your own box:
// StudioEmbed.open({ url: session.url, container: '#designer', height: '720px' });

Messages

EventPayloadWhen
ready{design, profile, title, chrome?}The designer has loaded (the overlay's spinner goes). title is the product name for the overlay's bar; chrome is 1 when the designer draws its own Close.
design:saved{design, sig, name, valid}After every save, including the one before Add to cart.
add-to-cart{design, sig, qty, options} (built-in payload)The shopper pressed Add to cart and the session has no Add-to-cart connector: your own cart takes the design; put design + sig on the line and post them with the order. options is null when the product has none.
add-to-cart<your JSON template> (message connector)The session's store has an Add-to-cart connector set to message: the payload is the store's own JSON template rendered for this design. Put the handoff on the line and post it with the order.
cart:navigate{url}A store with a server connector: its cart answered with a page to go to; embed.js sends the shopper there itself.
cart:form{action, method, fields}A store with a form connector: its cart wants a form POST; embed.js submits it itself.
error{code: "save_failed", message}A save failed: the one error the designer sends the page; also the rejection of save().
close{design, saved, signed_in?, forgot?}The overlay was closed (the designer's own Close, or close()). signed_in: the shopper is signed in, so the design is in My designs (show "Saved to My designs"). forgot: a guest closed it, so the page forgets it.
forget{design, link}A guest closed the designer (Close · Delete project, or Close after the link was emailed): embed.js forgets what this tab kept for the key; forget anything your page keeps about this design. link: an emailed link still opens it.

With cart:navigate and cart:form your page has nothing to do: embed.js already follows the answer. Listen to add-to-cart when your own cart holds the design. The store's Checkout tab shows the method in force.

d.on('add-to-cart', function (p) {
  // built-in payload: p.design, p.sig, p.qty, p.options
  addToMyCart({ design: p.design, sig: p.sig, qty: p.qty });
  d.close();
});
d.on('error', function (e) { console.warn(e.code, e.message); });

Keep the designer through a refresh

A shopper who refreshes the page, or whose phone reloads it, should come back to their designer, not a blank page. Two small things on your page do it (embed.js 3):

  1. When the page loads, call StudioEmbed.reopen('<product>'). If this tab had the designer open, the same one opens again, with everything in it: blank pages, photos added but not placed yet. It gives back the same object as open(), so attach your handlers to it again. It gives null when there is nothing to reopen.
  2. Pass remember: '<product>' when you open the designer.

Only this tab keeps the link, for up to 20 hours. Closing the designer, or adding to the cart, forgets it. Without remember nothing is kept, as before.

A saved design in a new tab. After each save the page's address carries the design id (#pw=dsn_...; a fragment, never sent to a server) and the browser keeps it. StudioEmbed.remembered('<product>') gives {design}: send it to your server, which passes it to POST /v1/sessions together with its own customer_ref: your signed-in customer's id, or for a guest a random id kept in a first-party HttpOnly cookie you set. PrintWisp reopens a design only for the customer_ref that made it. StudioEmbed.forget('<product>') drops everything kept.

StudioEmbed.reopen(key, options?): On page load: the designer this tab had open (less than 20 hours ago) opens again and the same object as open() comes back, so wire your handlers to it; null when there is none. options as for open(), plus cover: false (no Opening cover) and accent, label, timeout. StudioEmbed.remembered(key): {link?, design?}: the open link this tab keeps, and the last saved design id (from the address, or this browser). Send design to your server for POST /v1/sessions with your customer_ref. StudioEmbed.forget(key): Drops everything kept for the key. StudioEmbed.forgetAll(): Drops every design this browser kept, for every key. Call it when a shopper signs in or out. StudioEmbed.link(): The emailed link token from #pw_link= in the address (and removes it from the address), or an empty string. Pass it to POST /v1/sessions as link. StudioEmbed.version: 4 since link(), forgetAll() and the forget message (My designs, 25 Oct 2026); 3 since remember and reopen (24 Oct 2026).

Keep it through a refreshCopy
// 1. when the page loads
var d = StudioEmbed.reopen('canvas-40x30');
if (d) { wire(d); }

// 2. your Design button
button.onclick = async function () {
  var r = StudioEmbed.remembered('canvas-40x30');   // r.design after a save
  // your server: POST /v1/sessions {product, origin, qty, customer_ref, design: r.design}
  var s = await (await fetch('/printwisp/session?design=' + (r.design || ''))).json();
  wire(StudioEmbed.open({ url: s.url, remember: 'canvas-40x30' }));
};

// 3. your handlers, the same for both
function wire(d) {
  d.on('add-to-cart', function (p) { addToMyCart({ design: p.design, sig: p.sig, qty: p.qty }); d.close(); });
}
My designs

Saved designs: emailed links and My designs

Guests. Send signed_in: false (or a guest customer_ref) to POST /v1/sessions. When a guest presses ✕ with something in the design, the designer asks once "Keep this design?" and the store emails a private link, in the store's name. The link opens the design on any device; a browser that did not make it is offered "Make my own copy". Turn it off, or hide the reminder box, on the store's Designer tab.

Opening a link on your page. Send page (your product page) with each session: a link emailed from it then opens there, as #pw_link=<token>. Read it with StudioEmbed.link() and pass it to POST /v1/sessions as link, with your customer_ref. Without page the link opens on PrintWisp's own designer page.

Signed-in customers. Send signed_in: true. They are never asked at ✕; list their designs with GET /v1/projects?customer_ref= (rename, copy, delete on the same address). On every sign-in call POST /v1/customers/claim with their verified email: designs a guest emailed to that address join the account. Nothing else moves a guest design into an account: not signing in on the same browser.

No browser memory for guests. A guest's design lives only in the tab: keep the open designer in sessionStorage (embed.js remember with guest: true) so a refresh or Back brings it back, and nothing beyond the tab. ✕ is a close: the designer tells your page to forget it (forget message); the design stays with the shop (its Projects list, 7 days without a link); your server can do the same with DELETE /v1/sessions/{session}/memory. On sign-in and sign-out call StudioEmbed.forgetAll().

A link from the emailCopy
// on your product page, before anything else
var t = StudioEmbed.link();            // the #pw_link= token, removed from the address
if (t) {
  // your server: POST /v1/sessions {link: t, origin, page, customer_ref, signed_in, customer_email}
  var s = await (await fetch('/printwisp/session?link=' + t)).json();
  wire(StudioEmbed.open({ url: s.url, remember: 'canvas-40x30' }));
}

// sign-in and sign-out: this browser forgets every design it kept
StudioEmbed.forgetAll();
Custom Website

Add to cart: Server or Website

A Custom Website chooses on its Checkout tab how Add to cart reaches its cart. Server (server): PrintWisp's server posts the store's JSON to your cart's address -- the steps below. Website (message): your page's script receives the same JSON as the add-to-cart event (see Messages) and sends it to your server; price it there, never from the browser. Either way the JSON carries your metadata. Server, in three steps, no code on the page beyond embed.js:

  1. Open the designer with your cart id. POST /v1/sessions with metadata, any key-value pairs you want back: {"cart": "c_8f3c41"}. Up to 20 keys; each value a string (up to 500 characters), number, true/false or null.
  2. PrintWisp posts the store's JSON to your cart's address (the store's Checkout tab) when the shopper presses Add to cart. Your metadata is in it wherever the JSON says {{metadata.<key>}}; the starter JSON has "cart": "{{metadata.cart}}". It is signed: X-PrintWisp-Signature: t=<unix time>,v1=<hex hmac-sha256(signing secret, t + "." + body)>; refuse a request older than 5 minutes or with a wrong signature.
  3. Add the line and answer where the shopper goes next: {"redirect": "https://www.example.com/cart"} (the field the store's JSON setup names; redirect by default). The shopper's browser goes there. Keep properties.design and properties.signature on the line: you report them with the paid order (POST /v1/orders). Any non-2xx answer is shown to the shopper and nothing is added.

Because the cart id travels server to server, no claim link and no cookie are needed. The signing secret is on the store's Checkout tab, under Request Payload.

curl -X POST "https://printwisp.com/v1/sessions" \
  -H "Authorization: Bearer $PRINTWISP_KEY" -H "Content-Type: application/json" \
  -d '{"profile":"canvas-40x30","origin":"https://www.example.com","metadata":{"cart":"c_8f3c41"}}'
# 201 {"session":"dss_…","url":"…/d/…",…,"metadata":{"cart":"c_8f3c41"}}
POST /cart/printwisp-add HTTP/1.1
Content-Type: application/json
X-PrintWisp-Signature: t=1760000000,v1=5d1c…

{"sku":"canvas-40x30","quantity":1,"price":89.00,"currency":"AUD",
 "cart":"c_8f3c41",
 "properties":{"design":"dsn_…","signature":"…","handoff":"hnd_…","handoff_sig":"…"}}
HTTP/1.1 200 OK
Content-Type: application/json

{"redirect":"https://www.example.com/cart"}
Errors

Error codes

Every refusal is JSON with error (a stable code) and message (a sentence you can show a developer). The signed-link routes answer a plain-text 404 instead, one wording for every reason, so nothing can be probed.

HTTPerrorSent by
400invalid_code POST /v1/connect
400invalid_request POST /v1/provision · POST /v1/catalogue · POST /v1/billing
400missing_order GET /v1/orders
400no_store GET /v1/catalogue · POST /v1/catalogue
401unauthorized POST /v1/sessions · GET /v1/orders · POST /v1/orders · POST /v1/quote · GET /v1/events · GET /v1/print · POST /v1/login-link · GET /v1/products · GET /v1/billing · POST /v1/billing · DELETE /v1/sessions/{session}/memory · GET /v1/projects · GET /v1/projects/{project} · PATCH /v1/projects/{project} · DELETE /v1/projects/{project} · POST /v1/projects/{project}/copy · POST /v1/customers/claim
403not_allowed POST /v1/login-link
403not_confirmed POST /v1/provision
403store_draft POST /v1/sessions
403unknown_app POST /v1/provision
404design_not_found POST /v1/sessions · POST /v1/quote
404link_not_found POST /v1/sessions
404not_found GET /v1/events · GET /v1/print
404order_not_found GET /v1/orders
404product_not_found POST /v1/sessions
404profile_not_found POST /v1/sessions
404project_not_found GET /v1/projects/{project} · PATCH /v1/projects/{project} · DELETE /v1/projects/{project} · POST /v1/projects/{project}/copy
404session_not_found DELETE /v1/sessions/{session}/memory
404sku_not_found POST /v1/quote
405method_not_allowed POST /v1/sessions · GET /v1/orders · POST /v1/orders · POST /v1/connect · POST /v1/provision · POST /v1/login-link · GET /v1/products · GET /v1/projects · POST /v1/projects/{project}/copy · POST /v1/customers/claim · GET /v1/openapi.json
409account_paused POST /v1/sessions
409busy POST /v1/provision
409no_owner POST /v1/login-link
409organisation_not_live POST /v1/orders
409project_in_cart DELETE /v1/projects/{project}
409project_ordered PATCH /v1/projects/{project} · DELETE /v1/projects/{project}
409store_not_open POST /v1/orders
409store_paused POST /v1/sessions
410link_gone POST /v1/sessions
422bad_design POST /v1/quote
422cannot_price POST /v1/quote
422invalid_order POST /v1/orders
422invalid_quote POST /v1/quote
422invalid_request POST /v1/catalogue · POST /v1/billing · GET /v1/projects · GET /v1/projects/{project} · PATCH /v1/projects/{project} · DELETE /v1/projects/{project} · POST /v1/projects/{project}/copy · POST /v1/customers/claim
422invalid_session POST /v1/sessions
422not_offered POST /v1/quote
422not_sold POST /v1/quote
422product_not_offered POST /v1/sessions
422ready_product POST /v1/sessions
422size_not_offered POST /v1/sessions
500provision_failed POST /v1/provision

Each route's page lists its errors with their exact messages.

An errorCopy
HTTP/1.1 401 Unauthorized
Content-Type: application/json; charset=utf-8

{
  "error": "unauthorized",
  "message": "Send Authorization: Bearer <secret key>. Keys are made in PrintWisp, on your store's Overview → API keys."
}
SDKs

SDKs

PHP Coming soon composer require printwisp/printwisp-php is not published yet. It will wrap the routes above and verify webhooks. Until then the PHP examples use curl.

JavaScript and Python: no package is planned; the API is small enough for fetch and requests, as the examples show. The browser side is embed.js.