{
    "openapi": "3.1.0",
    "info": {
        "title": "PrintWisp API",
        "version": "1",
        "summary": "Put the PrintWisp designer on your own site, record the orders it sells and fetch the print-ready files.",
        "description": "HTTPS and JSON, a secret key, webhooks for what happens later. Written by hand from the Studio module's code (controllers/V1Controller.php, services/Webhooks.php, public/js/embed.js, models/OrderRepo.php); the docs pages at /docs are drawn from this file, and the suite checks that every route here exists and every route of the API is here.",
        "contact": {
            "email": "hello@printwisp.com"
        }
    },
    "servers": [
        {
            "url": "https://printwisp.com/v1",
            "description": "The API on this PrintWisp host."
        }
    ],
    "security": [
        {
            "bearer": []
        }
    ],
    "tags": [
        {
            "name": "Designer",
            "description": "Design links for one shopper."
        },
        {
            "name": "My designs",
            "description": "A signed-in customer's designs, emailed links, and proof by mailbox."
        },
        {
            "name": "Orders",
            "description": "Paid orders and their print files."
        },
        {
            "name": "Events",
            "description": "What happened: the catch-up for webhooks."
        },
        {
            "name": "Signed links",
            "description": "Routes that carry their own credential in the link and take no key."
        },
        {
            "name": "Connect",
            "description": "The WooCommerce plugin's Connect."
        },
        {
            "name": "Catalogue",
            "description": "Products and sizes."
        },
        {
            "name": "Billing",
            "description": "The organisation's plan and fees, for an app that bills through its platform."
        },
        {
            "name": "Spec",
            "description": "This document."
        }
    ],
    "paths": {
        "/sessions": {
            "post": {
                "operationId": "createSession",
                "x-slug": "sessions",
                "tags": [
                    "Designer"
                ],
                "summary": "A design link for one shopper, to open on your page.",
                "description": "Your server asks, with the key, when a shopper presses Design yours; the browser only ever sees the answer. The answer is an ordinary design link that names one size and at most one design (the shopper can reach nothing else), may be shown in a frame by `origin` and by no other site, and lasts `hours`: 24 by default when framed, 30 days when not framed (then it is a link to email or open on its own, and may not be framed at all). The store is the key's store; for a key with no store, the connected store named in `connection`, else the Your website store whose site address has the host of `origin`. A store key opens only the products and sizes its store offers. A Paused store opens no designers; a Draft store opens them with a test key, or with its `preview_key`.",
                "requestBody": {
                    "required": true,
                    "description": "JSON, or form fields.",
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/SessionRequest"
                            }
                        }
                    }
                },
                "x-example": {
                    "body": {
                        "profile": "canvas-40x30",
                        "origin": "https://shop.example",
                        "customer_ref": "cust_8812",
                        "qty": 1
                    }
                },
                "x-example-bodies": [
                    {
                        "body": {
                            "product": "canvas"
                        },
                        "note": "one-size product, a link to email, 30 days"
                    },
                    {
                        "body": {
                            "profile": "canvas-40x30",
                            "origin": "https://shop.example"
                        },
                        "note": "framed on your page, 24 hours"
                    },
                    {
                        "body": {
                            "profile": "canvas-40x30",
                            "origin": "https://shop.example",
                            "customer_ref": "cust_8812",
                            "design": "dsn_5c2a9e01b7d4f638"
                        },
                        "note": "the same shopper reopens a design"
                    },
                    {
                        "body": {
                            "link": "9f2c…64 hex…e01b",
                            "origin": "https://shop.example",
                            "customer_ref": "wp-user-12",
                            "signed_in": true,
                            "customer_email": "sarah.t@gmail.com"
                        },
                        "note": "an emailed link, opened on your page (answers link_email_is_customers)"
                    }
                ],
                "responses": {
                    "201": {
                        "description": "The link and what it names. Give your page the `url`; keep `session` to tie the design to your own records. `profile` is the size's id and `product` the product's code; `design` is the reopened design or null.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Session"
                                },
                                "example": {
                                    "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
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "403": {
                        "description": "The store is a Draft and the key is a live key: send the store's `preview_key`, or use a test key.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "examples": {
                                    "store_draft": {
                                        "value": {
                                            "error": "store_draft",
                                            "message": "This store is a Draft: send its preview_key to open designers until it is Open."
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "Nothing with that code or id.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "examples": {
                                    "product_not_found": {
                                        "value": {
                                            "error": "product_not_found",
                                            "message": "No product with that code."
                                        }
                                    },
                                    "profile_not_found": {
                                        "value": {
                                            "error": "profile_not_found",
                                            "message": "No size with that id."
                                        }
                                    },
                                    "design_not_found": {
                                        "value": {
                                            "error": "design_not_found",
                                            "message": "No design with that id."
                                        }
                                    },
                                    "design_not_customers": {
                                        "summary": "A design reopens only under the customer_ref it was made with (or that joined it by proof, or opened its live emailed link).",
                                        "value": {
                                            "error": "design_not_found",
                                            "message": "No design with that id for this customer."
                                        }
                                    },
                                    "link_not_found": {
                                        "summary": "`link` is not an emailed link of this store.",
                                        "value": {
                                            "error": "link_not_found",
                                            "message": "No such link here."
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "405": {
                        "description": "Any method but POST.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "examples": {
                                    "method_not_allowed": {
                                        "value": {
                                            "error": "method_not_allowed",
                                            "message": "POST only."
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "410": {
                        "description": "`link` no longer opens its design: a newer one was sent, or it was ordered or deleted (`reason`).",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "examples": {
                                    "link_gone": {
                                        "value": {
                                            "error": "link_gone",
                                            "reason": "replaced",
                                            "message": "A newer link was sent for this design."
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "409": {
                        "description": "The key's store is Paused.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "examples": {
                                    "store_paused": {
                                        "value": {
                                            "error": "store_paused",
                                            "message": "This store is paused: it opens no designers until it is set to Open or Draft."
                                        }
                                    },
                                    "account_paused": {
                                        "value": {
                                            "error": "account_paused",
                                            "message": "This PrintWisp account is paused: the free trial ended. Add a card under Organisation → Plan & billing to switch it back on. Nothing was deleted."
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "The request names no size, or one that does not fit, or one the key's store does not offer.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "examples": {
                                    "wrong_product": {
                                        "value": {
                                            "error": "invalid_session",
                                            "message": "That size is not one of that product's."
                                        }
                                    },
                                    "several_sizes": {
                                        "summary": "The product has several sizes and no default; the message lists their ids.",
                                        "value": {
                                            "error": "invalid_session",
                                            "message": "This product comes in 2 sizes. Say which with profile: prf_9a21c4d8e0f17b35, prf_0b3e7f2a9c4d5618."
                                        }
                                    },
                                    "no_sizes": {
                                        "value": {
                                            "error": "invalid_session",
                                            "message": "This product has no size to design on yet."
                                        }
                                    },
                                    "nothing_named": {
                                        "value": {
                                            "error": "invalid_session",
                                            "message": "Say which product (its code) or which size (profile)."
                                        }
                                    },
                                    "bad_origin": {
                                        "value": {
                                            "error": "invalid_session",
                                            "message": "origin must be the web address of the page that shows the designer, like https://shop.example -- scheme and host, no path."
                                        }
                                    },
                                    "bad_qty": {
                                        "value": {
                                            "error": "invalid_session",
                                            "message": "qty must be a whole number, 1 or more."
                                        }
                                    },
                                    "bad_hours": {
                                        "value": {
                                            "error": "invalid_session",
                                            "message": "hours must be a whole number from 1 to 720."
                                        }
                                    },
                                    "other_size": {
                                        "value": {
                                            "error": "invalid_session",
                                            "message": "That design was made on another size."
                                        }
                                    },
                                    "ordered": {
                                        "value": {
                                            "error": "invalid_session",
                                            "message": "That design has been ordered and cannot be changed. Start a new one."
                                        }
                                    },
                                    "bad_price": {
                                        "value": {
                                            "error": "invalid_session",
                                            "message": "price is the unit price your page shows, a number like 89.00."
                                        }
                                    },
                                    "no_connection": {
                                        "summary": "The message names the connection you sent.",
                                        "value": {
                                            "error": "invalid_session",
                                            "message": "No such connection: int_2b7c9e0d4a1f6835."
                                        }
                                    },
                                    "product_not_offered": {
                                        "summary": "A store key: the message names the store's code.",
                                        "value": {
                                            "error": "product_not_offered",
                                            "message": "The store web-au does not offer that product. Add it on the Products tab of that store."
                                        }
                                    },
                                    "size_not_offered": {
                                        "summary": "A store key: the message names the store's code.",
                                        "value": {
                                            "error": "size_not_offered",
                                            "message": "The store web-au does not offer that size. Offer it on the Products tab of that store."
                                        }
                                    },
                                    "ready_product": {
                                        "summary": "A ready product (a stocked item) has no designer: quote and order it by SKU.",
                                        "value": {
                                            "error": "ready_product",
                                            "message": "That product is ready to ship: it has no designer. Quote it with POST /v1/quote {sku, qty} and order it with lines [{sku, qty}]."
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        },
        "/orders": {
            "post": {
                "operationId": "createOrder",
                "x-slug": "orders-post",
                "tags": [
                    "Orders"
                ],
                "summary": "Record a paid order; its print files are queued.",
                "description": "Every line must name a design this organisation issued: a `handoff`, or a `design` with its `sig`. Nothing is recorded when any line is wrong. Each design is frozen as bought and its print file queued; the `order.created` event fires once. Posting the same `order_ref` again answers 200 with the existing order and `\"created\": false`: a retry never prints twice. Where an order came from is PrintWisp's to say: an order posted here is always recorded with `source` `api`, whatever the body says. A test key (`sk_test_…`) records a test order (`ord_t…`, `oln_t…`): kept apart from real orders, never charged, removed with Delete test data. A live key records a real order only once the organisation is live and, for a store key, only while the store is Open.",
                "requestBody": {
                    "required": true,
                    "description": "JSON, or form fields.",
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/OrderRequest"
                            }
                        }
                    }
                },
                "x-example": {
                    "body": {
                        "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"
                            }
                        ]
                    }
                },
                "responses": {
                    "201": {
                        "description": "Created.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/OrderRecorded"
                                },
                                "example": {
                                    "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": []
                                        }
                                    ]
                                }
                            }
                        }
                    },
                    "200": {
                        "description": "That order_ref was already recorded for this store: the existing order, `created: false`.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/OrderRecorded"
                                },
                                "example": {
                                    "order": "ord_7d1e4b9a2c6f0583",
                                    "order_ref": "WEB-10021",
                                    "created": false,
                                    "lines": [
                                        {
                                            "line": "oln_41aa8c3e9d2b7f16",
                                            "line_ref": "1",
                                            "design": "dsn_5c2a9e01b7d4f638",
                                            "qty": 1,
                                            "print_status": "ready",
                                            "components": [],
                                            "fields": []
                                        }
                                    ]
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "405": {
                        "$ref": "#/components/responses/OrdersMethod"
                    },
                    "409": {
                        "description": "A live key cannot record a real order yet: use a test key until the organisation is live and the store is Open.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "examples": {
                                    "organisation_not_live": {
                                        "value": {
                                            "error": "organisation_not_live",
                                            "message": "Your organisation is not live yet: record orders with a test key (sk_test_...) until it is."
                                        }
                                    },
                                    "store_not_open": {
                                        "value": {
                                            "error": "store_not_open",
                                            "message": "This store is not Open: record its orders with a test key (sk_test_...) until it is."
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "The order is refused; the message names the line that is wrong.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "examples": {
                                    "no_ref": {
                                        "value": {
                                            "error": "invalid_order",
                                            "message": "order_ref is required: the shop's order number."
                                        }
                                    },
                                    "no_lines": {
                                        "value": {
                                            "error": "invalid_order",
                                            "message": "An order needs at least one line."
                                        }
                                    },
                                    "too_many": {
                                        "value": {
                                            "error": "invalid_order",
                                            "message": "At most 200 lines in one order."
                                        }
                                    },
                                    "bad_qty": {
                                        "value": {
                                            "error": "invalid_order",
                                            "message": "Line 1: qty must be a positive whole number."
                                        }
                                    },
                                    "bad_amount": {
                                        "value": {
                                            "error": "invalid_order",
                                            "message": "Line 1: amount must be a number, 0 or more."
                                        }
                                    },
                                    "no_handoff": {
                                        "value": {
                                            "error": "invalid_order",
                                            "message": "Line 1: no such hand-off hnd_6e4b1a9c3d7f2058."
                                        }
                                    },
                                    "handoff_not_in_cart": {
                                        "value": {
                                            "error": "invalid_order",
                                            "message": "Line 1: hand-off hnd_6e4b1a9c3d7f2058 never reached a cart."
                                        }
                                    },
                                    "bad_design": {
                                        "value": {
                                            "error": "invalid_order",
                                            "message": "Line 1: design dsn_5c2a9e01b7d4f638 is not one this shop issued, or its signature is wrong."
                                        }
                                    },
                                    "nothing_named": {
                                        "value": {
                                            "error": "invalid_order",
                                            "message": "Line 1: name a handoff, or a design with its sig."
                                        }
                                    },
                                    "no_connection": {
                                        "value": {
                                            "error": "invalid_order",
                                            "message": "No such connection: int_2b7c9e0d4a1f6835."
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            },
            "get": {
                "operationId": "getOrder",
                "x-slug": "orders-get",
                "tags": [
                    "Orders"
                ],
                "summary": "One order with its store, shopper, totals and lines.",
                "description": "One order, as the `order.created` webhook sends it: by PrintWisp's id (`order`) or by your order number (`ref`). A test key reads test orders and a live key real ones. A connected shop's key sees only its own shop's orders; any other key sees every order of the organisation. Money is a string with two decimals; `null` where the order did not carry it (an order you post keeps its own totals, so `totals` is null and each line's `line_amount` is what you sent).",
                "parameters": [
                    {
                        "name": "order",
                        "in": "query",
                        "description": "PrintWisp's order id (`ord_…`). Give `order` or `ref`.",
                        "schema": {
                            "type": "string"
                        }
                    },
                    {
                        "name": "ref",
                        "in": "query",
                        "description": "Your order number, the `order_ref` you posted.",
                        "schema": {
                            "type": "string"
                        }
                    },
                    {
                        "name": "test",
                        "in": "query",
                        "description": "`1` reads a test order with a live key (one posted with `\"test\": true`). A test key always reads test orders.",
                        "schema": {
                            "type": "string",
                            "enum": [
                                "1"
                            ]
                        }
                    }
                ],
                "x-example": {
                    "query": {
                        "ref": "WEB-10021"
                    }
                },
                "responses": {
                    "200": {
                        "description": "The order.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Order"
                                },
                                "example": {
                                    "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": []
                                        }
                                    ]
                                }
                            }
                        }
                    },
                    "400": {
                        "description": "Neither `order` nor `ref`.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "examples": {
                                    "missing_order": {
                                        "value": {
                                            "error": "missing_order",
                                            "message": "Send ?order=ord_… or ?ref=<order number>."
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "404": {
                        "description": "No such order for this key. The message repeats what you sent.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "examples": {
                                    "order_not_found": {
                                        "value": {
                                            "error": "order_not_found",
                                            "message": "No order WEB-10021 for this key."
                                        }
                                    },
                                    "other_mode": {
                                        "summary": "An order id of the other mode: a test order asked with a live key.",
                                        "value": {
                                            "error": "order_not_found",
                                            "message": "No order ord_t3a9c1e7b5d2f804 for this key (a live key reads real orders)."
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "405": {
                        "$ref": "#/components/responses/OrdersMethod"
                    }
                }
            }
        },
        "/quote": {
            "post": {
                "operationId": "createQuote",
                "x-slug": "quote-post",
                "tags": [
                    "Orders"
                ],
                "summary": "What a design (or a ready product, by SKU) costs, priced by PrintWisp.",
                "description": "For a shop whose own cart takes the design (the WooCommerce plugin): send the `design` and the `sig` the designer gave the page. PrintWisp checks the design is printable and prices it as it is -- the pages of a book, the options chosen -- always with PrintWisp's own price, whatever the store's Price comes from says. The price is frozen as a hand-off, in the shop's cart: charge `unit_price` per copy and report the order with the `handoff` on its line, so what is paid is what PrintWisp quoted.\n\nA READY product (sold as `ready`: a stocked item, no designer): send `{sku, qty}` (or `{variant, qty}` with the variant's code) and no design. The answer is `type: \"ready\"` with the store's `unit_price`, the `variant`, its `barcode`, `available` (false when a product that stops selling at 0 has fewer than `qty` left, with a `message`) and `stock`. Nothing is frozen: report the order with `{sku, qty}` on the line.",
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "properties": {
                                    "design": {
                                        "type": "string",
                                        "example": "dsn_5c2a9e01b7d4f638",
                                        "description": "With `sig`, for a personalised product."
                                    },
                                    "sig": {
                                        "type": "string",
                                        "description": "The designer's signature for the design."
                                    },
                                    "sku": {
                                        "type": "string",
                                        "example": "FR-OAK-A3-BLK",
                                        "description": "A ready product's variant SKU, instead of design + sig."
                                    },
                                    "variant": {
                                        "type": "string",
                                        "example": "oak-frame-a3-black",
                                        "description": "Or the variant's code."
                                    },
                                    "qty": {
                                        "type": "integer",
                                        "minimum": 1,
                                        "default": 1
                                    }
                                }
                            }
                        }
                    }
                },
                "x-example": {
                    "body": {
                        "design": "dsn_5c2a9e01b7d4f638",
                        "sig": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
                        "qty": 1
                    }
                },
                "responses": {
                    "200": {
                        "description": "Priced and frozen.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "handoff": {
                                            "type": "string"
                                        },
                                        "handoff_sig": {
                                            "type": "string"
                                        },
                                        "design": {
                                            "type": "string"
                                        },
                                        "unit_price": {
                                            "type": [
                                                "string",
                                                "null"
                                            ],
                                            "description": "Per copy, in the store's currency, tax as the store's prices are entered."
                                        },
                                        "currency": {
                                            "type": "string"
                                        },
                                        "qty": {
                                            "type": "integer"
                                        },
                                        "sku": {
                                            "type": "string"
                                        },
                                        "pages": {
                                            "type": [
                                                "integer",
                                                "null"
                                            ]
                                        },
                                        "product": {
                                            "type": "string"
                                        },
                                        "product_name": {
                                            "type": "string"
                                        },
                                        "choices": {
                                            "type": "object",
                                            "description": "What was chosen, as the customer saw it."
                                        }
                                    }
                                },
                                "example": {
                                    "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"
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "404": {
                        "description": "No such design, or no ready product with that SKU or variant code.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "examples": {
                                    "design_not_found": {
                                        "value": {
                                            "error": "design_not_found",
                                            "message": "No design dsn_5c2a9e01b7d4f638."
                                        }
                                    },
                                    "sku_not_found": {
                                        "summary": "A ready product: the message repeats the SKU (or variant code) you sent.",
                                        "value": {
                                            "error": "sku_not_found",
                                            "message": "No ready product with the SKU FR-OAK-A3-BLK."
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "405": {
                        "description": "POST only.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "example": {
                                    "error": "method_not_allowed",
                                    "message": "POST {design, sig, qty} or {sku, qty}."
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "The design cannot be priced.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "examples": {
                                    "bad_design": {
                                        "value": {
                                            "error": "bad_design",
                                            "message": "Send the design and the signature the designer gave you."
                                        }
                                    },
                                    "not_sold": {
                                        "value": {
                                            "error": "not_sold",
                                            "message": "This design is not linked to a product the shop sells."
                                        }
                                    },
                                    "cannot_price": {
                                        "value": {
                                            "error": "cannot_price",
                                            "message": "The design cannot be printed yet: Page 3: a photograph is below the resolution floor."
                                        }
                                    },
                                    "invalid_quote": {
                                        "summary": "A ready product: a qty that is not a whole number from 1, or an extra the product does not have.",
                                        "value": {
                                            "error": "invalid_quote",
                                            "message": "qty must be a whole number, 1 or more."
                                        }
                                    },
                                    "not_offered": {
                                        "summary": "A store key: the store does not offer that ready product. The message names the store's code and the SKU.",
                                        "value": {
                                            "error": "not_offered",
                                            "message": "The store web-au does not offer FR-OAK-A3-BLK. Offer it on the Products tab of that store."
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        },
        "/events": {
            "get": {
                "operationId": "listEvents",
                "x-slug": "events",
                "tags": [
                    "Events"
                ],
                "summary": "What happened since an event: the catch-up for webhooks.",
                "description": "The organisation's events after the one named, oldest first: the catch-up for anything a webhook missed. The list is organisation-wide for any key; a test key lists the test events (`\"livemode\": false` in `data`) and a live key the real ones. `has_more` says whether to call again with the last id. Pings from Send a test are not listed.",
                "parameters": [
                    {
                        "name": "after",
                        "in": "query",
                        "description": "An event id (`evt_…`); empty starts from the first event.",
                        "schema": {
                            "type": "string"
                        }
                    },
                    {
                        "name": "limit",
                        "in": "query",
                        "description": "How many, 1 to 200.",
                        "schema": {
                            "type": "integer",
                            "minimum": 1,
                            "maximum": 200,
                            "default": 50
                        }
                    }
                ],
                "x-example": {
                    "query": {
                        "after": "evt_7c2e9a1d4b8f3065",
                        "limit": 50
                    }
                },
                "responses": {
                    "200": {
                        "description": "The events.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/EventList"
                                },
                                "example": {
                                    "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
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "404": {
                        "description": "`after` names no event.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "examples": {
                                    "not_found": {
                                        "value": {
                                            "error": "not_found",
                                            "message": "No event with that id."
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        },
        "/print": {
            "get": {
                "operationId": "getPrint",
                "x-slug": "print",
                "tags": [
                    "Orders"
                ],
                "summary": "A line's print status and, when ready, a 10-minute link.",
                "description": "A test line (`oln_t…`) answers only to a test key, a real line only to a live key. `print_status` moves `queued` → `rendering` → `ready`, or `failed` with an `error`. When it is `ready` the answer adds a signed `url` with its `expires_at`, good for 10 minutes, pointing at `GET /v1/file`. `components` says what the line is made of; `fields` holds the shopper's answers.",
                "parameters": [
                    {
                        "name": "line",
                        "in": "query",
                        "required": true,
                        "description": "The order line (`oln_…`).",
                        "schema": {
                            "type": "string"
                        }
                    },
                    {
                        "name": "test",
                        "in": "query",
                        "description": "`1` reads a test order line with a live key.",
                        "schema": {
                            "type": "string",
                            "enum": [
                                "1"
                            ]
                        }
                    }
                ],
                "x-example": {
                    "query": {
                        "line": "oln_41aa8c3e9d2b7f16"
                    }
                },
                "responses": {
                    "200": {
                        "description": "The line's print status.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/PrintStatus"
                                },
                                "example": {
                                    "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"
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "404": {
                        "description": "No such line in this organisation for this key's mode.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "examples": {
                                    "not_found": {
                                        "value": {
                                            "error": "not_found",
                                            "message": "No order line with that id."
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        },
        "/file": {
            "get": {
                "operationId": "getFile",
                "x-slug": "file",
                "tags": [
                    "Signed links"
                ],
                "summary": "The PDF itself, for whoever holds a fresh signed link.",
                "description": "The print-ready PDF, for a link that is fresh and unaltered: `l` the line, `e` the expiry (unix), `s` the signature. No key: take the link from `GET /v1/print` or the `design.print_ready` webhook. Answers `Content-Disposition: attachment`.",
                "security": [],
                "parameters": [
                    {
                        "name": "l",
                        "in": "query",
                        "required": true,
                        "description": "The line (`oln_…`).",
                        "schema": {
                            "type": "string"
                        }
                    },
                    {
                        "name": "e",
                        "in": "query",
                        "required": true,
                        "description": "When the link expires (unix seconds).",
                        "schema": {
                            "type": "integer"
                        }
                    },
                    {
                        "name": "s",
                        "in": "query",
                        "required": true,
                        "description": "The link's signature.",
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "x-example": {
                    "query": {
                        "l": "oln_41aa8c3e9d2b7f16",
                        "e": 1759640400,
                        "s": "9f2c4e1a7b3d8f05c6e2a9d1b4f7c3e8a5d2b9f6c1e4a7d3b8f5c2e9a6d1b4f7"
                    },
                    "save": "WEB-10021-1.pdf"
                },
                "responses": {
                    "200": {
                        "description": "The PDF.",
                        "content": {
                            "application/pdf": {
                                "schema": {
                                    "type": "string",
                                    "format": "binary"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "Expired, altered, or not ready: one plain-text answer for every reason. Ask `GET /v1/print` for a fresh link.",
                        "content": {
                            "text/plain": {
                                "schema": {
                                    "type": "string"
                                },
                                "example": "This link has expired or is not valid. Ask the shop for a fresh one."
                            }
                        }
                    }
                }
            }
        },
        "/preview": {
            "get": {
                "operationId": "getPreview",
                "x-slug": "preview",
                "tags": [
                    "Signed links"
                ],
                "summary": "The design's picture (image/jpeg) for a cart line.",
                "description": "The picture of a design for your cart line, to whoever holds `design` + `sig`: the same pair the designer gave your page. No key. Cached privately for 60 seconds (`Cache-Control: private, max-age=60`), because the picture changes with the design. One plain-text 404 for every refusal, so ids cannot be probed.",
                "security": [],
                "parameters": [
                    {
                        "name": "design",
                        "in": "query",
                        "required": true,
                        "description": "The design (`dsn_` + 16 hex).",
                        "schema": {
                            "type": "string"
                        }
                    },
                    {
                        "name": "sig",
                        "in": "query",
                        "required": true,
                        "description": "The design's signature, as the designer handed it to your page.",
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "x-example": {
                    "query": {
                        "design": "dsn_5c2a9e01b7d4f638",
                        "sig": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
                    },
                    "save": "design.jpg",
                    "img": true
                },
                "responses": {
                    "200": {
                        "description": "The picture.",
                        "content": {
                            "image/jpeg": {
                                "schema": {
                                    "type": "string",
                                    "format": "binary"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "No design, wrong signature, or nothing drawn yet: one answer for all three.",
                        "content": {
                            "text/plain": {
                                "schema": {
                                    "type": "string"
                                },
                                "example": "No picture for that design."
                            }
                        }
                    }
                }
            }
        },
        "/brand-logo": {
            "get": {
                "operationId": "getBrandLogo",
                "x-slug": "brand-logo",
                "tags": [
                    "Signed links"
                ],
                "summary": "A store's logo for the designer bar (image/png, public).",
                "description": "The logo for the designer's top bar. `t` is the organisation's id; `s` a store's id when that store has its own logo, else the organisation's logo is answered; `v` a version that changes with every upload, so the image is cached for a year (`Cache-Control: public, max-age=31536000, immutable`). Public on purpose, with `Access-Control-Allow-Origin: *`.",
                "security": [],
                "parameters": [
                    {
                        "name": "t",
                        "in": "query",
                        "required": true,
                        "description": "The organisation's id.",
                        "schema": {
                            "type": "string"
                        }
                    },
                    {
                        "name": "s",
                        "in": "query",
                        "description": "A store's id, for the logo of a store with its own branding.",
                        "schema": {
                            "type": "string"
                        }
                    },
                    {
                        "name": "v",
                        "in": "query",
                        "description": "The logo's version.",
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "x-example": {
                    "query": {
                        "t": "ten_7c0a1b2e9d4f",
                        "v": "3"
                    },
                    "save": "logo.png"
                },
                "responses": {
                    "200": {
                        "description": "The logo.",
                        "content": {
                            "image/png": {
                                "schema": {
                                    "type": "string",
                                    "format": "binary"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "No logo uploaded.",
                        "content": {
                            "text/plain": {
                                "schema": {
                                    "type": "string"
                                },
                                "example": "No logo."
                            }
                        }
                    }
                }
            }
        },
        "/brand-favicon": {
            "get": {
                "operationId": "getBrandFavicon",
                "x-slug": "brand-favicon",
                "tags": [
                    "Signed links"
                ],
                "summary": "A store's favicon: the browser tab icon of its pages and designer (image/png, public).",
                "description": "The browser tab icon of a store's pages and designer. `t` is the organisation's id; `s` a store's id when that store has its own favicon, else the organisation's favicon is answered; `v` a version that changes with every upload, so the image is cached for a year (`Cache-Control: public, max-age=31536000, immutable`). Public on purpose, with `Access-Control-Allow-Origin: *`.",
                "security": [],
                "parameters": [
                    {
                        "name": "t",
                        "in": "query",
                        "required": true,
                        "description": "The organisation's id.",
                        "schema": {
                            "type": "string"
                        }
                    },
                    {
                        "name": "s",
                        "in": "query",
                        "description": "A store's id, for the favicon of a store with its own branding.",
                        "schema": {
                            "type": "string"
                        }
                    },
                    {
                        "name": "v",
                        "in": "query",
                        "description": "The favicon's version.",
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "x-example": {
                    "query": {
                        "t": "ten_7c0a1b2e9d4f",
                        "v": "3"
                    },
                    "save": "favicon.png"
                },
                "responses": {
                    "200": {
                        "description": "The favicon.",
                        "content": {
                            "image/png": {
                                "schema": {
                                    "type": "string",
                                    "format": "binary"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "No favicon uploaded.",
                        "content": {
                            "text/plain": {
                                "schema": {
                                    "type": "string"
                                },
                                "example": "No favicon."
                            }
                        }
                    }
                }
            }
        },
        "/connect": {
            "post": {
                "operationId": "connect",
                "x-slug": "connect",
                "tags": [
                    "Connect"
                ],
                "summary": "The WooCommerce plugin swaps the approved code for its key.",
                "description": "The second half of the WooCommerce plugin's Connect: the shop's server swaps the one-time `code` its owner approved for the shop's key and webhook secret, once, within ten minutes, for that `site` only. Your own site does not need this: make the key in the portal.",
                "security": [],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/ConnectRequest"
                            }
                        }
                    }
                },
                "x-example": {
                    "body": {
                        "code": "0f3a9c1e5b7d2f4a6c8e0b2d4f6a8c1e3b5d7f9a2c4e6b8d",
                        "site": "https://harbourprints.com.au"
                    }
                },
                "responses": {
                    "200": {
                        "description": "The shop's keys and webhook secret, shown this once. `key` is the live key (kept for plugins made before test keys); `keys` holds the test and the live key. `store.id` is the organisation's id; `store.name`, `store.code` and `store.status` are the connected store's.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Connected"
                                },
                                "example": {
                                    "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"
                                    }
                                }
                            }
                        }
                    },
                    "400": {
                        "description": "The code is used, expired, or made for another site.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "examples": {
                                    "used_or_expired": {
                                        "value": {
                                            "error": "invalid_code",
                                            "message": "That connection code is not valid any more. Press Connect again."
                                        }
                                    },
                                    "other_site": {
                                        "value": {
                                            "error": "invalid_code",
                                            "message": "That code was made for another site."
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "405": {
                        "description": "Any method but POST.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "examples": {
                                    "method_not_allowed": {
                                        "value": {
                                            "error": "method_not_allowed",
                                            "message": "POST only."
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        },
        "/provision": {
            "post": {
                "operationId": "provision",
                "x-slug": "provision",
                "tags": [
                    "Connect"
                ],
                "security": [],
                "summary": "PrintWisp's Shopify app sets a new shop up, with no sign-in.",
                "description": "Called only by PrintWisp's own Shopify app, the first time it opens in a shop with no PrintWisp link. PrintWisp confirms the request by calling the app back (at a host it already trusts) with `state`; the app says yes only for a check value it made for that shop moments before. The shop gets its own organisation (billed through Shopify), a draft store, the connection and starter products -- or, already connected, the same organisation with fresh keys. One shop, one organisation; a second call is answered the same way. The answer is the same as POST /v1/connect.",
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "required": [
                                    "platform",
                                    "shop",
                                    "state",
                                    "app"
                                ],
                                "properties": {
                                    "platform": {
                                        "type": "string",
                                        "enum": [
                                            "shopify"
                                        ]
                                    },
                                    "shop": {
                                        "type": "string",
                                        "example": "harbour-prints.myshopify.com"
                                    },
                                    "state": {
                                        "type": "string",
                                        "description": "The app's one-time check value (48 hex characters)."
                                    },
                                    "app": {
                                        "type": "string",
                                        "example": "https://app.printwisp.com"
                                    },
                                    "shop_name": {
                                        "type": "string"
                                    },
                                    "email": {
                                        "type": "string",
                                        "format": "email"
                                    },
                                    "currency": {
                                        "type": "string",
                                        "example": "AUD"
                                    }
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "The shop's keys and webhook secret, as POST /v1/connect answers.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Connected"
                                }
                            }
                        }
                    },
                    "400": {
                        "description": "Not JSON, not a Shopify shop, or no check value.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "examples": {
                                    "invalid_request": {
                                        "value": {
                                            "error": "invalid_request",
                                            "message": "That is not a Shopify shop address."
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "403": {
                        "description": "The app could not be asked, or did not confirm the request.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "examples": {
                                    "unknown_app": {
                                        "value": {
                                            "error": "unknown_app",
                                            "message": "That app address is not one PrintWisp trusts."
                                        }
                                    },
                                    "not_confirmed": {
                                        "value": {
                                            "error": "not_confirmed",
                                            "message": "PrintWisp's Shopify app did not confirm this request."
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "405": {
                        "description": "Not a POST.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "examples": {
                                    "method_not_allowed": {
                                        "value": {
                                            "error": "method_not_allowed",
                                            "message": "POST only."
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "409": {
                        "description": "The same shop is being set up at this moment.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "examples": {
                                    "busy": {
                                        "value": {
                                            "error": "busy",
                                            "message": "PrintWisp is busy setting this shop up. Try again in a moment."
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "500": {
                        "description": "The set-up failed; nothing half-made is kept.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "examples": {
                                    "provision_failed": {
                                        "value": {
                                            "error": "provision_failed",
                                            "message": "PrintWisp could not set this shop up just now. Try again in a minute."
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        },
        "/login-link": {
            "post": {
                "operationId": "loginLink",
                "x-slug": "login-link",
                "tags": [
                    "Connect"
                ],
                "summary": "A one-time link that signs the organisation's owner in to the PrintWisp portal.",
                "description": "Used by PrintWisp's own Shopify app for \"Open in PrintWisp\", so the merchant lands signed in to their own organisation instead of a sign-in page. The link signs in the organisation's active owner, opens `to` (a path under /app; anything else opens /app), lasts two minutes and works once. Never made for PrintWisp's master organisation or a platform user. A used or expired link signs nobody in.",
                "requestBody": {
                    "required": false,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "properties": {
                                    "to": {
                                        "type": "string",
                                        "example": "/app/catalogue",
                                        "description": "Where the portal opens: a path starting with /app."
                                    }
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "The link, to open in the merchant's browser within two minutes.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object",
                                    "properties": {
                                        "ok": {
                                            "type": "boolean"
                                        },
                                        "url": {
                                            "type": "string",
                                            "example": "https://printwisp.com/app/login-link?t=3f9c..."
                                        },
                                        "to": {
                                            "type": "string",
                                            "example": "/app"
                                        },
                                        "expires_in": {
                                            "type": "integer",
                                            "example": 120
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "403": {
                        "description": "Not made for this organisation (PrintWisp's own, or a platform user).",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "examples": {
                                    "not_allowed": {
                                        "value": {
                                            "error": "not_allowed",
                                            "message": "Sign-in links are not made for this organisation."
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "405": {
                        "description": "Not a POST.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "examples": {
                                    "method_not_allowed": {
                                        "value": {
                                            "error": "method_not_allowed",
                                            "message": "POST /v1/login-link only."
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "409": {
                        "description": "The organisation has no active owner who can sign in.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "examples": {
                                    "no_owner": {
                                        "value": {
                                            "error": "no_owner",
                                            "message": "This organisation has no owner who can sign in yet. Sign in to PrintWisp, or choose \"Forgot password\" with your store's email."
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        },
        "/products": {
            "get": {
                "operationId": "listProducts",
                "x-slug": "products",
                "tags": [
                    "Catalogue"
                ],
                "summary": "The store's catalogue with codes, names, prices and options.",
                "description": "What the key's store offers: each product with its offered sizes (code, name, the store's price in its currency) and its options with their values' codes, so your server can map its own products to PrintWisp's without copying codes by hand. The size's `code` is what you send as `profile`. A key with no store lists nothing: use a store's key.",
                "x-example": {
                    "query": []
                },
                "responses": {
                    "200": {
                        "description": "The store and its products. Money is a string with two decimals, as in GET /v1/orders; `price` is null for a size with no price. With a key that belongs to no store: `store` null, `products` empty, and a `message`.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Catalogue"
                                },
                                "example": {
                                    "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.5759999999999996,
                                                    "packed_kg_extra": 4.5759999999999996
                                                }
                                            ],
                                            "options": [
                                                {
                                                    "code": "edge",
                                                    "name": "Edge",
                                                    "values": [
                                                        {
                                                            "code": "mirror",
                                                            "name": "Mirrored",
                                                            "price_delta": "0.00"
                                                        },
                                                        {
                                                            "code": "white",
                                                            "name": "White",
                                                            "price_delta": "0.00"
                                                        }
                                                    ]
                                                }
                                            ]
                                        }
                                    ]
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "405": {
                        "description": "Any method but GET.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "examples": {
                                    "method_not_allowed": {
                                        "value": {
                                            "error": "method_not_allowed",
                                            "message": "GET /v1/products only."
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        },
        "/picture/o/{organisation}/p/{product}": {
            "get": {
                "operationId": "productPicture",
                "x-slug": "picture",
                "tags": [
                    "Catalogue"
                ],
                "security": [],
                "summary": "A product's own picture, public, for a shop platform to copy.",
                "description": "The product's own picture (JPEG, PNG or WebP; never the drawing PrintWisp shows when a product has none), with no key: product pictures are public. GET /v1/products and GET /v1/catalogue give this address as each product's `image` (null when it has no picture), and PrintWisp's Shopify app hands it to Shopify as the product's media. A picture over 2400 px or 5 MB is sent as a 2400 px WebP copy. Cached for a day.",
                "parameters": [
                    {
                        "name": "organisation",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string",
                            "example": "ten_3f9c0a1b2c3d"
                        },
                        "description": "The organisation's id."
                    },
                    {
                        "name": "product",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string",
                            "example": "canvas"
                        },
                        "description": "The product's code."
                    }
                ],
                "responses": {
                    "200": {
                        "description": "The picture.",
                        "content": {
                            "image/jpeg": [],
                            "image/png": [],
                            "image/webp": []
                        }
                    },
                    "404": {
                        "description": "No such product, or it has no picture of its own (plain text)."
                    }
                }
            }
        },
        "/catalogue": {
            "get": {
                "operationId": "getCatalogue",
                "x-slug": "catalogue",
                "tags": [
                    "Catalogue"
                ],
                "summary": "Every product as the store sees it, offered or not.",
                "description": "The whole catalogue for the key's store: each product, whether the store offers it (`listed`), its priced sizes, the names of sizes that have no price yet, its lowest price, whether it is priced by quantity tiers (which a Shopify variant cannot hold), and `blocked` -- why it cannot be offered yet (no size, no price, nowhere to deliver), or null. What a platform app shows in a products picker. A key with no store is refused. With `offered=1`, only the products on sale -- offered in this store or in at least one of the organisation's open stores -- and `not_offered` counts the products left out (drafts, retired copies, products only a draft store lists).",
                "parameters": [
                    {
                        "name": "offered",
                        "in": "query",
                        "description": "`1`: only products offered in this store or in an open store of the organisation; the response adds `not_offered`, how many were left out.",
                        "schema": {
                            "type": "string",
                            "enum": [
                                "1"
                            ]
                        }
                    }
                ],
                "x-example": {
                    "query": []
                },
                "responses": {
                    "200": {
                        "description": "The store and every product.",
                        "content": {
                            "application/json": {
                                "example": {
                                    "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
                                        }
                                    ]
                                }
                            }
                        }
                    },
                    "400": {
                        "description": "The key belongs to no store.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "examples": {
                                    "no_store": {
                                        "value": {
                                            "error": "no_store",
                                            "message": "This key belongs to no store: use a store's key."
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            },
            "post": {
                "operationId": "setCatalogueListed",
                "x-slug": "catalogue-list",
                "tags": [
                    "Catalogue"
                ],
                "summary": "Offer products in the store, or stop offering them.",
                "description": "Send product codes and `listed`: true offers them in the key's store (with all their current sizes), false stops offering them. The same as ticking them on the store's Products tab in PrintWisp. Unknown codes come back in `unknown` and nothing is guessed. Send `{\"currency\": \"USD\", \"fx_rate\": 0.66}` instead (a connected shop's store only) and the store sells in that currency, its prices converted from the catalogue's at that rate (how many of it one of the catalogue's currency is; the catalogue's own currency needs none). The answer is the store with its currency and rate.",
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "example": {
                                "codes": [
                                    "layflat",
                                    "canvas"
                                ],
                                "listed": true
                            }
                        }
                    }
                },
                "x-example": {
                    "body": {
                        "codes": [
                            "layflat"
                        ],
                        "listed": true
                    }
                },
                "responses": {
                    "200": {
                        "description": "What changed, and any codes that are not products of this organisation.",
                        "content": {
                            "application/json": {
                                "example": {
                                    "changed": [
                                        "layflat"
                                    ],
                                    "unknown": []
                                }
                            }
                        }
                    },
                    "400": {
                        "description": "No codes sent, or the key belongs to no store.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "examples": {
                                    "invalid_request": {
                                        "value": {
                                            "error": "invalid_request",
                                            "message": "Send {\"codes\": [\"product-code\", ...], \"listed\": true}."
                                        }
                                    },
                                    "no_store": {
                                        "value": {
                                            "error": "no_store",
                                            "message": "This key belongs to no store: use a store's key."
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "A currency PrintWisp cannot sell in, a missing rate, or a store that is not a connected shop's.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "examples": {
                                    "invalid_request": {
                                        "value": {
                                            "error": "invalid_request",
                                            "message": "PrintWisp cannot sell in JPY yet."
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        },
        "/billing": {
            "get": {
                "operationId": "getBilling",
                "x-slug": "billing",
                "tags": [
                    "Billing"
                ],
                "summary": "The organisation's plan and this month's fees, for an app that bills through its platform.",
                "description": "For a platform app that bills its shop through the platform (the Shopify app): the organisation's billing `provider` (`stripe` or `shopify`), `status`, `trial_ends_at`, the `plan` with its fee tiers, and `month` -- this calendar month's designer sales and fees per currency. `unreported` lists the fees not yet put on the shop's platform bill, oldest first (always empty unless the organisation is billed through Shopify): report each as usage, then mark them with POST /v1/billing.",
                "x-example": {
                    "query": []
                },
                "responses": {
                    "200": {
                        "description": "The organisation's billing.",
                        "content": {
                            "application/json": {
                                "example": {
                                    "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.100000000000001,
                                            "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"
                                        }
                                    ]
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "405": {
                        "description": "GET or POST only.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "example": {
                                    "error": "method_not_allowed",
                                    "message": "GET or POST /v1/billing."
                                }
                            }
                        }
                    }
                }
            },
            "post": {
                "operationId": "updateBilling",
                "x-slug": "billing-update",
                "tags": [
                    "Billing"
                ],
                "summary": "The shop chose a plan in its platform, or fees are now on its platform bill.",
                "description": "Send one of two things. `subscription` -- the shop chose or changed a plan in Shopify (`plan` is a PrintWisp plan code; `status` as Shopify gives it): the organisation moves onto Shopify billing and Stripe lets go of it, so it is never billed twice; the answer is the billing as GET /v1/billing gives it. An exempt organisation stays exempt. `reported` -- those fee ids are now on the shop's Shopify bill (`ref` is your usage record); the answer says how many were marked.",
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "properties": {
                                    "subscription": {
                                        "type": "object",
                                        "properties": {
                                            "plan": {
                                                "type": "string",
                                                "example": "growth"
                                            },
                                            "status": {
                                                "type": "string",
                                                "example": "active"
                                            },
                                            "trial_ends_at": {
                                                "type": [
                                                    "string",
                                                    "null"
                                                ],
                                                "format": "date-time"
                                            },
                                            "shop": {
                                                "type": "string"
                                            },
                                            "id": {
                                                "type": "string"
                                            }
                                        }
                                    },
                                    "reported": {
                                        "type": "object",
                                        "properties": {
                                            "ids": {
                                                "type": "array",
                                                "items": {
                                                    "type": "integer"
                                                }
                                            },
                                            "ref": {
                                                "type": "string"
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                },
                "x-example": {
                    "body": {
                        "reported": {
                            "ids": [
                                812
                            ],
                            "ref": "gid://shopify/AppUsageRecord/5566778899"
                        }
                    }
                },
                "responses": {
                    "200": {
                        "description": "With `reported`: how many fees were marked. With `subscription`: the billing, as GET /v1/billing gives it.",
                        "content": {
                            "application/json": {
                                "example": {
                                    "marked": 1
                                }
                            }
                        }
                    },
                    "400": {
                        "description": "Neither `subscription` nor `reported` was sent.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "examples": {
                                    "invalid_request": {
                                        "value": {
                                            "error": "invalid_request",
                                            "message": "Send {\"subscription\": {...}} or {\"reported\": {\"ids\": [...], \"ref\": \"...\"}}."
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "422": {
                        "description": "The subscription names no plan PrintWisp has.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "examples": {
                                    "unknown_plan": {
                                        "summary": "The message repeats the plan code you sent.",
                                        "value": {
                                            "error": "invalid_request",
                                            "message": "Unknown plan: growth-2025"
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        },
        "/designs": {
            "get": {
                "operationId": "listDesigns",
                "x-slug": "designs",
                "tags": [
                    "Catalogue"
                ],
                "summary": "The \"Start from a design\" cards for one product in the key's store.",
                "description": "The Designs the key's store shows on one product's page (its own list, or the catalogue list it follows), in order: each Design's `code` (send it as `design` to POST /v1/sessions and the designer opens with every page already set), the name this store's shoppers read, its description and group, `fit` (`fits`, or `changes` when a page uses this size's plain layout), and its pages drawn as SVG (cover first) with the store's logo; the pictures and fonts inside load from signed /v1/media addresses. `design_yours` is the product's start-from-scratch button. Cached privately for five minutes.",
                "parameters": [
                    {
                        "name": "product",
                        "in": "query",
                        "required": false,
                        "description": "The product's code (or send only `size`: the product is that size's).",
                        "schema": {
                            "type": "string"
                        }
                    },
                    {
                        "name": "size",
                        "in": "query",
                        "description": "A size's code: the pages are drawn on it (default: the product's default size).",
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "x-example": {
                    "query": {
                        "product": "calendar2027-a4-v2"
                    }
                },
                "responses": {
                    "200": {
                        "description": "The cards.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object"
                                },
                                "example": {
                                    "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 ...>"
                                            }
                                        }
                                    ]
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "No product with that code.",
                        "content": {
                            "application/json": {
                                "example": {
                                    "error": "product_not_found",
                                    "message": "No product with that code."
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "The key's store does not offer that product.",
                        "content": {
                            "application/json": {
                                "example": {
                                    "error": "product_not_offered",
                                    "message": "The store web-au does not offer that product. Add it on the Products tab of that store."
                                }
                            }
                        }
                    },
                    "405": {
                        "description": "Not a GET.",
                        "content": {
                            "application/json": {
                                "example": {
                                    "error": "method_not_allowed",
                                    "message": "GET /v1/designs only."
                                }
                            }
                        }
                    }
                }
            }
        },
        "/media": {
            "get": {
                "operationId": "getMedia",
                "x-slug": "media",
                "tags": [
                    "Signed links"
                ],
                "summary": "A picture or font a Design's pages are drawn with (signed, public).",
                "description": "The library picture (a light copy) or font that an SVG page from GET /v1/designs names, to whoever holds the signed address; the addresses come only from GET /v1/designs. Library pictures and the organisation's fonts only, never a shopper's photo. One plain-text 404 for every refusal.",
                "security": [],
                "parameters": [
                    {
                        "name": "t",
                        "in": "query",
                        "required": true,
                        "description": "The organisation's id.",
                        "schema": {
                            "type": "string"
                        }
                    },
                    {
                        "name": "f",
                        "in": "query",
                        "required": true,
                        "description": "The picture (`ast_`) or font face (`fnt_`).",
                        "schema": {
                            "type": "string"
                        }
                    },
                    {
                        "name": "size",
                        "in": "query",
                        "description": "`screen` (1600 px, the default) or `tile`.",
                        "schema": {
                            "type": "string"
                        }
                    },
                    {
                        "name": "sig",
                        "in": "query",
                        "required": true,
                        "description": "The signature GET /v1/designs put in the address.",
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "x-example": {
                    "query": {
                        "t": "ten_7c0a1b2e9d4f",
                        "f": "ast_c41c3bf6c1bfd5d5",
                        "size": "screen",
                        "sig": "0f1e2d3c4b5a69788796a5b4c3d2e1f0"
                    },
                    "save": "picture.webp",
                    "img": true
                },
                "responses": {
                    "200": {
                        "description": "The file.",
                        "content": {
                            "image/webp": {
                                "schema": {
                                    "type": "string",
                                    "format": "binary"
                                }
                            },
                            "font/ttf": {
                                "schema": {
                                    "type": "string",
                                    "format": "binary"
                                }
                            }
                        }
                    },
                    "404": {
                        "description": "Wrong signature, or no such file.",
                        "content": {
                            "text/plain": {
                                "schema": {
                                    "type": "string"
                                },
                                "example": "Not found."
                            }
                        }
                    }
                }
            }
        },
        "/sessions/{session}/memory": {
            "delete": {
                "operationId": "forgetSession",
                "x-slug": "sessions-memory",
                "tags": [
                    "My designs"
                ],
                "summary": "What ✕ does for a guest: this shopper's ref no longer reopens the design.",
                "description": "The designer calls it itself when a guest closes it (Close · Delete project, or Close after the link was emailed); your server may call it too, say when a guest signs in or out. The session's `customer_ref` stops reopening or listing the session's design, and its open designer links on that design stop working. The design itself stays with the shop: its emailed link, if one was sent, still opens it anywhere (`link` says whether there is one). A guest's design is deleted 7 days after its last change, link or not. Clear your page's own tab memory too (embed.js does it on close).",
                "parameters": [
                    {
                        "name": "session",
                        "in": "path",
                        "required": true,
                        "description": "The session id (`dss_…`) POST /v1/sessions answered.",
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "x-example": {
                    "path": {
                        "session": "dss_3f9c1a7e5b2d8c40"
                    }
                },
                "responses": {
                    "200": {
                        "description": "Forgotten. `forgotten`: how many of the shopper's sessions on that design; `link`: an emailed link still opens it.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object"
                                },
                                "example": {
                                    "forgotten": 2,
                                    "link": true
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "404": {
                        "description": "No such session for this key.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "examples": {
                                    "session_not_found": {
                                        "value": {
                                            "error": "session_not_found",
                                            "message": "No such designer session."
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        },
        "/projects": {
            "get": {
                "operationId": "listProjects",
                "x-slug": "projects",
                "tags": [
                    "My designs"
                ],
                "summary": "A signed-in customer's designs: what \"My designs\" lists.",
                "description": "Every design listed for `customer_ref`, newest change first: the ones it made (in the key's store), and the ones that joined it by proof (`joined: \"email\"`, POST /v1/customers/claim). Each with its picture (`cover`, a signed address), name, product and size, `pages`, `updated_at` and `status`: `in_progress`, `in_cart` (handed to a cart, not ordered) or `ordered` with `order_ref`. `link` says when its private link was emailed (to a masked address) and whether the one reminder is still to go. `note: \"saved_by_email\"` is set on the first list after a design joined, so you can tag it once (\"saved by email before you signed in\"). Never a deleted design, never a merchant's Design. A guest's designs are never listed under an account just because they signed in on the same browser.",
                "parameters": [
                    {
                        "name": "customer_ref",
                        "in": "query",
                        "required": true,
                        "description": "Your signed-in customer, as you send it to POST /v1/sessions (`wp-user-12`, `shopify-customer-6061…`, your own id).",
                        "schema": {
                            "type": "string",
                            "maxLength": 190
                        }
                    }
                ],
                "x-example": {
                    "query": {
                        "customer_ref": "wp-user-12"
                    }
                },
                "responses": {
                    "200": {
                        "description": "The list.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object"
                                },
                                "example": {
                                    "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"
                                        }
                                    ]
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "405": {
                        "description": "Not a GET.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "examples": {
                                    "method_not_allowed": {
                                        "value": {
                                            "error": "method_not_allowed",
                                            "message": "GET /v1/projects lists; change one at /v1/projects/{project}."
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "No customer_ref.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "examples": {
                                    "no_customer_ref": {
                                        "value": {
                                            "error": "invalid_request",
                                            "message": "Say whose designs with customer_ref: your signed-in customer's id, as you send it to POST /v1/sessions."
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        },
        "/projects/{project}": {
            "get": {
                "operationId": "getProject",
                "x-slug": "projects-one",
                "tags": [
                    "My designs"
                ],
                "summary": "One design of the customer's.",
                "description": "The same row as the list, for one design the customer made or that joined their account.",
                "parameters": [
                    {
                        "name": "project",
                        "in": "path",
                        "required": true,
                        "description": "The design's id (`dsn_…`) from the list.",
                        "schema": {
                            "type": "string"
                        }
                    },
                    {
                        "name": "customer_ref",
                        "in": "query",
                        "required": true,
                        "description": "Your signed-in customer, as you send it to POST /v1/sessions (`wp-user-12`, `shopify-customer-6061…`, your own id).",
                        "schema": {
                            "type": "string",
                            "maxLength": 190
                        }
                    }
                ],
                "x-example": {
                    "path": {
                        "project": "dsn_5c2a9e01b7d4f638"
                    },
                    "query": {
                        "customer_ref": "wp-user-12"
                    }
                },
                "responses": {
                    "200": {
                        "description": "The design.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object"
                                },
                                "example": {
                                    "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"
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "404": {
                        "description": "No design with that id for this customer (or it was deleted).",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "examples": {
                                    "project_not_found": {
                                        "value": {
                                            "error": "project_not_found",
                                            "message": "No design with that id for this customer."
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "No customer_ref.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "examples": {
                                    "no_customer_ref": {
                                        "value": {
                                            "error": "invalid_request",
                                            "message": "Say whose designs with customer_ref: your signed-in customer's id, as you send it to POST /v1/sessions."
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            },
            "patch": {
                "operationId": "renameProject",
                "x-slug": "projects-rename",
                "tags": [
                    "My designs"
                ],
                "summary": "Rename a design.",
                "description": "Up to 120 characters. An ordered design keeps the name it was ordered with.",
                "parameters": [
                    {
                        "name": "project",
                        "in": "path",
                        "required": true,
                        "description": "The design's id (`dsn_…`) from the list.",
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "required": [
                                    "customer_ref",
                                    "name"
                                ],
                                "properties": {
                                    "customer_ref": {
                                        "type": "string",
                                        "description": "Your signed-in customer."
                                    },
                                    "name": {
                                        "type": "string",
                                        "maxLength": 120,
                                        "description": "The new name."
                                    }
                                }
                            }
                        }
                    }
                },
                "x-example": {
                    "path": {
                        "project": "dsn_5c2a9e01b7d4f638"
                    },
                    "body": {
                        "customer_ref": "wp-user-12",
                        "name": "Our Story, 2026"
                    }
                },
                "responses": {
                    "200": {
                        "description": "Renamed: the design as the list shows it.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object"
                                },
                                "example": {
                                    "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
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "404": {
                        "description": "No design with that id for this customer (or it was deleted).",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "examples": {
                                    "project_not_found": {
                                        "value": {
                                            "error": "project_not_found",
                                            "message": "No design with that id for this customer."
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "409": {
                        "description": "Ordered.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "examples": {
                                    "project_ordered": {
                                        "value": {
                                            "error": "project_ordered",
                                            "message": "An ordered design keeps the name it was ordered with."
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "No name, or too long.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "examples": {
                                    "no_name": {
                                        "value": {
                                            "error": "invalid_request",
                                            "message": "Give the design a name."
                                        }
                                    },
                                    "too_long": {
                                        "value": {
                                            "error": "invalid_request",
                                            "message": "A design's name is up to 120 characters."
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            },
            "delete": {
                "operationId": "deleteProject",
                "x-slug": "projects-delete",
                "tags": [
                    "My designs"
                ],
                "summary": "Delete a design from My designs.",
                "description": "Refused while the design is in the cart (remove it from the cart first) and always once it is ordered. A design the customer made is withdrawn: its emailed link and every designer link to it stop working, and the store's retention removes it for good. A design that joined the account by email is only taken off the list (`how: \"removed\"`): it is still the guest's who emailed it.",
                "parameters": [
                    {
                        "name": "project",
                        "in": "path",
                        "required": true,
                        "description": "The design's id (`dsn_…`) from the list.",
                        "schema": {
                            "type": "string"
                        }
                    },
                    {
                        "name": "customer_ref",
                        "in": "query",
                        "required": true,
                        "description": "Your signed-in customer, as you send it to POST /v1/sessions (`wp-user-12`, `shopify-customer-6061…`, your own id).",
                        "schema": {
                            "type": "string",
                            "maxLength": 190
                        }
                    }
                ],
                "x-example": {
                    "path": {
                        "project": "dsn_5c2a9e01b7d4f638"
                    },
                    "query": {
                        "customer_ref": "wp-user-12"
                    }
                },
                "responses": {
                    "200": {
                        "description": "Deleted (`how`: `deleted` or `removed`).",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object"
                                },
                                "example": {
                                    "deleted": true,
                                    "project": "dsn_5c2a9e01b7d4f638",
                                    "how": "deleted"
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "404": {
                        "description": "No design with that id for this customer (or it was deleted).",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "examples": {
                                    "project_not_found": {
                                        "value": {
                                            "error": "project_not_found",
                                            "message": "No design with that id for this customer."
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "409": {
                        "description": "In the cart, or ordered.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "examples": {
                                    "project_in_cart": {
                                        "value": {
                                            "error": "project_in_cart",
                                            "message": "This design is in the cart. Remove it from the cart first, then delete it."
                                        }
                                    },
                                    "project_ordered": {
                                        "value": {
                                            "error": "project_ordered",
                                            "message": "An ordered design is kept as it was printed: it cannot be deleted."
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "No customer_ref.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "examples": {
                                    "no_customer_ref": {
                                        "value": {
                                            "error": "invalid_request",
                                            "message": "Say whose designs with customer_ref: your signed-in customer's id, as you send it to POST /v1/sessions."
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        },
        "/projects/{project}/copy": {
            "post": {
                "operationId": "copyProject",
                "x-slug": "projects-copy",
                "tags": [
                    "My designs"
                ],
                "summary": "A copy of a design, with its pictures: Make a copy, Order again.",
                "description": "A new design with the same pages and pictures, the customer's own; the original is untouched (an ordered one stays as printed). Open it with POST /v1/sessions `design` and the same `customer_ref`.",
                "parameters": [
                    {
                        "name": "project",
                        "in": "path",
                        "required": true,
                        "description": "The design's id (`dsn_…`) from the list.",
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "required": [
                                    "customer_ref"
                                ],
                                "properties": {
                                    "customer_ref": {
                                        "type": "string",
                                        "description": "Your signed-in customer."
                                    },
                                    "name": {
                                        "type": "string",
                                        "maxLength": 120,
                                        "description": "A name for the copy (default: the same name)."
                                    }
                                }
                            }
                        }
                    }
                },
                "x-example": {
                    "path": {
                        "project": "dsn_5c2a9e01b7d4f638"
                    },
                    "body": {
                        "customer_ref": "wp-user-12"
                    }
                },
                "responses": {
                    "201": {
                        "description": "The copy, as the list shows it.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object"
                                },
                                "example": {
                                    "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
                                    }
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "404": {
                        "description": "No design with that id for this customer (or it was deleted).",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "examples": {
                                    "project_not_found": {
                                        "value": {
                                            "error": "project_not_found",
                                            "message": "No design with that id for this customer."
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "405": {
                        "description": "Not a POST.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "examples": {
                                    "method_not_allowed": {
                                        "value": {
                                            "error": "method_not_allowed",
                                            "message": "POST /v1/projects/{project}/copy."
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "No customer_ref.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "examples": {
                                    "no_customer_ref": {
                                        "value": {
                                            "error": "invalid_request",
                                            "message": "Say whose designs with customer_ref: your signed-in customer's id, as you send it to POST /v1/sessions."
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        },
        "/customers/claim": {
            "post": {
                "operationId": "claimDesigns",
                "x-slug": "customers-claim",
                "tags": [
                    "My designs"
                ],
                "summary": "Proof by mailbox: designs a guest emailed to this customer's verified address join their account.",
                "description": "Call it on every sign-in, with the signed-in customer's email, ONLY when your platform verified it (`verified: true`): Shopify customer accounts, a sign-in code. WooCommerce does not verify addresses, so a WooCommerce shop never claims; its customers join a design by opening its emailed link while signed in. A guest who pressed ✕ › Email me the link and typed that same address owns the mailbox the link went to: those designs (in the key's store) are listed under `customer_ref` from now on, tagged once in GET /v1/projects (`note: \"saved_by_email\"`). Idempotent. This and opening the emailed link while signed in are the only ways a guest's design joins an account; there is no merge call, because a browser is not a person (a shared computer's next user would inherit the last guest's designs).",
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "type": "object",
                                "required": [
                                    "customer_ref",
                                    "email",
                                    "verified"
                                ],
                                "properties": {
                                    "customer_ref": {
                                        "type": "string",
                                        "description": "Your signed-in customer (never a guest ref)."
                                    },
                                    "email": {
                                        "type": "string",
                                        "description": "Their verified email address."
                                    },
                                    "verified": {
                                        "type": "boolean",
                                        "description": "true: you verified that this customer owns the address (Shopify customer accounts, a sign-in code). Required. WooCommerce does not verify addresses: never claim there."
                                    }
                                }
                            }
                        }
                    }
                },
                "x-example": {
                    "body": {
                        "customer_ref": "wp-user-12",
                        "email": "sarah.t@gmail.com",
                        "verified": true
                    }
                },
                "responses": {
                    "200": {
                        "description": "`claimed`: how many designs joined now (0 when they already had).",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object"
                                },
                                "example": {
                                    "customer_ref": "wp-user-12",
                                    "claimed": 1
                                }
                            }
                        }
                    },
                    "401": {
                        "$ref": "#/components/responses/Unauthorized"
                    },
                    "405": {
                        "description": "Not a POST.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "examples": {
                                    "method_not_allowed": {
                                        "value": {
                                            "error": "method_not_allowed",
                                            "message": "POST /v1/customers/claim."
                                        }
                                    }
                                }
                            }
                        }
                    },
                    "422": {
                        "description": "Missing fields, or a guest ref.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "examples": {
                                    "missing": {
                                        "value": {
                                            "error": "invalid_request",
                                            "message": "Send {\"customer_ref\": \"<your signed-in customer>\", \"email\": \"<their verified address>\"}."
                                        }
                                    },
                                    "guest": {
                                        "value": {
                                            "error": "invalid_request",
                                            "message": "customer_ref is a guest: only a signed-in customer with a verified address can claim designs."
                                        }
                                    },
                                    "unverified": {
                                        "value": {
                                            "error": "invalid_request",
                                            "message": "Send \"verified\": true, and only when you have verified that this customer owns the address (WooCommerce does not verify addresses: do not claim there)."
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        },
        "/openapi.json": {
            "get": {
                "operationId": "getOpenApi",
                "x-slug": "openapi",
                "tags": [
                    "Spec"
                ],
                "summary": "This OpenAPI document: one source of truth for the docs.",
                "description": "This document, read-only and public: no key. Cached for five minutes (`Cache-Control: public, max-age=300`). The docs pages are drawn from it, and the suite checks it against the API's routes.",
                "security": [],
                "x-example": {
                    "query": [],
                    "save": "openapi.json"
                },
                "responses": {
                    "200": {
                        "description": "The OpenAPI 3.1 document.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "type": "object"
                                }
                            }
                        }
                    },
                    "405": {
                        "description": "Any method but GET.",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/Error"
                                },
                                "examples": {
                                    "method_not_allowed": {
                                        "value": {
                                            "error": "method_not_allowed",
                                            "message": "GET only."
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    },
    "webhooks": {
        "order.created": {
            "post": {
                "operationId": "orderCreated",
                "summary": "A new order is placed",
                "description": "POST /v1/orders with a new order_ref, or a PrintWisp Store checkout. Once per order. `data` is everything GET /v1/orders answers, plus `retrieve`: the URL to fetch it again. A test order's events carry `\"livemode\": false` in `data`.",
                "parameters": [
                    {
                        "$ref": "#/components/parameters/StudioSignature"
                    },
                    {
                        "$ref": "#/components/parameters/StudioEvent"
                    },
                    {
                        "$ref": "#/components/parameters/StudioDelivery"
                    }
                ],
                "requestBody": {
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/Event"
                            },
                            "example": {
                                "id": "evt_8a0d3c7e1f5b2946",
                                "type": "order.created",
                                "created": "2026-10-05T04:31:02+00:00",
                                "data": {
                                    "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": "queued",
                                            "components": [],
                                            "fields": []
                                        }
                                    ],
                                    "retrieve": "https://printwisp.com/v1/orders?order=ord_7d1e4b9a2c6f0583"
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "2XX": {
                        "description": "Received. Answer within 10 seconds."
                    }
                }
            }
        },
        "design.print_ready": {
            "post": {
                "operationId": "printReady",
                "summary": "A print file is ready to download",
                "description": "A line's print file is ready. `file` is a signed link made when the message is sent, not when the event happened, so a retry hours later still gets a fresh 10-minute link.",
                "parameters": [
                    {
                        "$ref": "#/components/parameters/StudioSignature"
                    },
                    {
                        "$ref": "#/components/parameters/StudioEvent"
                    },
                    {
                        "$ref": "#/components/parameters/StudioDelivery"
                    }
                ],
                "requestBody": {
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/Event"
                            },
                            "example": {
                                "id": "evt_9b1e4d8f2a6c3057",
                                "type": "design.print_ready",
                                "created": "2026-10-05T04:31:40+00:00",
                                "data": {
                                    "order": "ord_7d1e4b9a2c6f0583",
                                    "order_ref": "WEB-10021",
                                    "line": "oln_41aa8c3e9d2b7f16",
                                    "line_ref": "1",
                                    "design": "dsn_5c2a9e01b7d4f638",
                                    "sku": null,
                                    "qty": 1,
                                    "item_no": "VEL00001234",
                                    "components": [],
                                    "fields": [],
                                    "file": {
                                        "url": "https://printwisp.com/v1/file?l=oln_41aa8c3e9d2b7f16&e=1759640400&s=9f2c4e1a7b3d8f05c6e2a9d1b4f7c3e8a5d2b9f6c1e4a7d3b8f5c2e9a6d1b4f7",
                                        "expires_at": "2026-10-05T04:51:00+00:00"
                                    }
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "2XX": {
                        "description": "Received."
                    }
                }
            }
        },
        "design.print_failed": {
            "post": {
                "operationId": "printFailed",
                "summary": "A print file could not be made",
                "description": "The same data as design.print_ready, without `file`, plus `error`.",
                "parameters": [
                    {
                        "$ref": "#/components/parameters/StudioSignature"
                    },
                    {
                        "$ref": "#/components/parameters/StudioEvent"
                    },
                    {
                        "$ref": "#/components/parameters/StudioDelivery"
                    }
                ],
                "requestBody": {
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/Event"
                            },
                            "example": {
                                "id": "evt_0c2f5e9a3b7d4168",
                                "type": "design.print_failed",
                                "created": "2026-10-05T04:31:40+00:00",
                                "data": {
                                    "order": "ord_7d1e4b9a2c6f0583",
                                    "order_ref": "WEB-10021",
                                    "line": "oln_41aa8c3e9d2b7f16",
                                    "line_ref": "1",
                                    "design": "dsn_5c2a9e01b7d4f638",
                                    "sku": null,
                                    "qty": 1,
                                    "components": [],
                                    "fields": [],
                                    "error": "why it could not be made"
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "2XX": {
                        "description": "Received."
                    }
                }
            }
        },
        "ping": {
            "post": {
                "operationId": "ping",
                "summary": "Send a test",
                "description": "You pressed Send a test on an endpoint in the portal. Goes to that one endpoint only, and is not listed by GET /v1/events.",
                "parameters": [
                    {
                        "$ref": "#/components/parameters/StudioSignature"
                    },
                    {
                        "$ref": "#/components/parameters/StudioEvent"
                    },
                    {
                        "$ref": "#/components/parameters/StudioDelivery"
                    }
                ],
                "requestBody": {
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/Event"
                            },
                            "example": {
                                "id": "evt_1d3a6f0b4c8e5279",
                                "type": "ping",
                                "created": "2026-10-05T04:30:00+00:00",
                                "data": {
                                    "message": "A test from Studio."
                                }
                            }
                        }
                    }
                },
                "responses": {
                    "2XX": {
                        "description": "Received."
                    }
                }
            }
        }
    },
    "components": {
        "securitySchemes": {
            "bearer": {
                "type": "http",
                "scheme": "bearer",
                "bearerFormat": "sk_test_ or sk_live_ + 40 hex",
                "description": "A secret key, sent as `Authorization: Bearer sk_test_…` or `sk_live_…`. Made in the portal on a Your website store's Overview → API keys (a store key), or under Organisation → API keys & webhooks (a key with no store). A test key records test orders only; a live key records real orders once the organisation is live and the store is Open."
            }
        },
        "parameters": {
            "StudioSignature": {
                "name": "Studio-Signature",
                "in": "header",
                "required": true,
                "description": "t=<unix>,v1=<hex HMAC-SHA256(secret, t + \".\" + body)>",
                "schema": {
                    "type": "string"
                }
            },
            "StudioEvent": {
                "name": "Studio-Event",
                "in": "header",
                "required": true,
                "description": "The event type.",
                "schema": {
                    "type": "string"
                }
            },
            "StudioDelivery": {
                "name": "Studio-Delivery",
                "in": "header",
                "required": true,
                "description": "The delivery id (`whd_…`).",
                "schema": {
                    "type": "string"
                }
            }
        },
        "responses": {
            "Unauthorized": {
                "description": "No key, or a wrong one.",
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/Error"
                        },
                        "examples": {
                            "unauthorized": {
                                "value": {
                                    "error": "unauthorized",
                                    "message": "Send Authorization: Bearer <secret key>. Keys are made in PrintWisp, on your store's Overview → API keys."
                                }
                            }
                        }
                    }
                }
            },
            "OrdersMethod": {
                "description": "Any method but GET or POST.",
                "content": {
                    "application/json": {
                        "schema": {
                            "$ref": "#/components/schemas/Error"
                        },
                        "examples": {
                            "method_not_allowed": {
                                "value": {
                                    "error": "method_not_allowed",
                                    "message": "GET or POST."
                                }
                            }
                        }
                    }
                }
            }
        },
        "schemas": {
            "Error": {
                "type": "object",
                "required": [
                    "error",
                    "message"
                ],
                "properties": {
                    "error": {
                        "type": "string",
                        "description": "A stable code."
                    },
                    "message": {
                        "type": "string",
                        "description": "A sentence you can show a developer."
                    }
                }
            },
            "SessionRequest": {
                "type": "object",
                "properties": {
                    "profile": {
                        "type": "string",
                        "description": "The size to design on: its code (`canvas-40x30`) or its id (`prf_…`). Give `profile` or `product`; when both are given they must agree."
                    },
                    "product": {
                        "type": "string",
                        "description": "The product's code (or id). Enough on its own when the product has one size or a default size; otherwise the answer is 422 invalid_session listing the sizes to choose from."
                    },
                    "origin": {
                        "type": "string",
                        "description": "The web address of the page that shows the designer: scheme and host, no path (`https://shop.example`). The link may be framed there only, for 24 hours by default. Without it the link cannot be framed and lasts 30 days."
                    },
                    "customer_ref": {
                        "type": "string",
                        "maxLength": 190,
                        "description": "Your own id for the shopper. Lets the same shopper reopen the design later with `design`. A signed-in customer's designs are listed under it (GET /v1/projects)."
                    },
                    "signed_in": {
                        "type": "boolean",
                        "description": "Is `customer_ref` a signed-in customer (true) or a guest (false)? Read from the ref when not sent: `wp-user-`, `shopify-customer-` are signed in. A guest gets the designer's ✕ \"Keep this design?\" sheet; a signed-in shopper is never asked (it is in My designs)."
                    },
                    "customer_email": {
                        "type": "string",
                        "description": "A signed-in customer's VERIFIED email. With `link`: answers `link_email_is_customers`, and when it is the link's address the design joins their account (proof by mailbox)."
                    },
                    "link": {
                        "type": "string",
                        "description": "An emailed link's token (64 hex), which your page read from `#pw_link=` (embed.js `link()`): opens the design that link names. A browser that did not make it is offered \"Make my own copy\"; a signed-in customer under another address, \"Add to My designs\" (a copy). 404 link_not_found, or 410 link_gone once it was replaced, ordered or deleted."
                    },
                    "page": {
                        "type": "string",
                        "description": "Your product page showing the designer (on `origin`). A link emailed from this design then opens there, with `#pw_link=<token>`, rather than on PrintWisp's own designer page."
                    },
                    "customer_name": {
                        "type": "string",
                        "maxLength": 190,
                        "description": "The shopper's name, kept with the session."
                    },
                    "qty": {
                        "type": "integer",
                        "minimum": 1,
                        "maximum": 100000,
                        "description": "The quantity the designer starts with."
                    },
                    "hours": {
                        "type": "integer",
                        "minimum": 1,
                        "maximum": 720,
                        "description": "How long the link lasts. Default 24 when framed, 30 days when not."
                    },
                    "design": {
                        "type": "string",
                        "description": "Reopen a design (`dsn_…`): it must have been made under the same `customer_ref`, on the same size, and not yet ordered (an ordered design is frozen as printed). Or a Design's code from GET /v1/designs: the designer opens with every page already set, as this store offers that Design on the product; a code it does not offer is ignored."
                    },
                    "connection": {
                        "type": "string",
                        "description": "For a key with no store: the connected store (`int_…`) the designer's Add to cart should go to. A store's own key needs none."
                    },
                    "price": {
                        "type": [
                            "number",
                            "string"
                        ],
                        "description": "The unit price your page shows, a number like `89.00`. When the store's prices come from both your page and PrintWisp and must agree, the designer refuses Add to cart while PrintWisp's price differs from it."
                    },
                    "preview_key": {
                        "type": "string",
                        "description": "While the store is a Draft: the store's preview key, for a live key (a test key needs none). A Paused store opens no designers."
                    },
                    "metadata": {
                        "type": "object",
                        "maxProperties": 20,
                        "additionalProperties": {
                            "type": [
                                "string",
                                "number",
                                "boolean",
                                "null"
                            ]
                        },
                        "description": "Anything your server wants back with Add to cart: up to 20 keys (letters, digits, `_`; up to 40 characters), each a string (up to 500 characters), number, true/false or null. Kept with the session and sent in the Add-to-cart JSON as `{{metadata.<key>}}`. A Custom Website passes its own cart id here (`{\"cart\": \"c_8f3c41\"}`), so PrintWisp's server-to-server Add to cart knows whose cart it is.",
                        "example": {
                            "cart": "c_8f3c41"
                        }
                    }
                }
            },
            "Session": {
                "type": "object",
                "properties": {
                    "session": {
                        "type": "string",
                        "description": "The session's id (`dss_…`)."
                    },
                    "token": {
                        "type": "string",
                        "description": "48 hex characters; the link's secret."
                    },
                    "url": {
                        "type": "string",
                        "description": "The design link to open with embed.js."
                    },
                    "expires_at": {
                        "type": "string",
                        "format": "date-time"
                    },
                    "origin": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "The one site that may frame the link, or null."
                    },
                    "product": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "The product's code."
                    },
                    "profile": {
                        "type": "string",
                        "description": "The size's id (`prf_…`)."
                    },
                    "design": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "The reopened design, or null."
                    },
                    "metadata": {
                        "type": [
                            "object",
                            "null"
                        ],
                        "description": "The metadata you sent, as kept."
                    }
                }
            },
            "OrderLineRequest": {
                "type": "object",
                "examples": [
                    {
                        "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"
                    }
                ],
                "properties": {
                    "handoff": {
                        "type": "string",
                        "description": "A hand-off (`hnd_…`) from a cart PrintWisp filled. Give `handoff`, or `design` + `sig`."
                    },
                    "design": {
                        "type": "string",
                        "description": "The design (`dsn_…`), with its `sig`, when your own cart held it."
                    },
                    "sig": {
                        "type": "string",
                        "description": "The design's signature, as the designer handed it to your page."
                    },
                    "line_ref": {
                        "type": "string",
                        "maxLength": 100,
                        "description": "Your id for the line; l1, l2 … when missing."
                    },
                    "qty": {
                        "type": "integer",
                        "minimum": 1,
                        "maximum": 100000,
                        "default": 1
                    },
                    "amount": {
                        "type": [
                            "number",
                            "string"
                        ],
                        "description": "What the shopper paid for the line, a number 0 or more: the base of PrintWisp's per-order fee."
                    },
                    "currency": {
                        "type": "string",
                        "description": "3 letters; the order's `currency` when missing."
                    },
                    "extras": {
                        "type": "array",
                        "items": {
                            "type": "string"
                        },
                        "description": "Ready lines: the codes of the extras ticked (a product's `extras`, e.g. [\"gift-wrap\"]); each adds its price when PrintWisp prices the line."
                    },
                    "sku": {
                        "type": "string",
                        "maxLength": 160,
                        "description": "Your SKU, for a design + sig line. On its own (no handoff, no design): a READY product's variant SKU -- a line with nothing to print, priced by PrintWisp from the store unless you send `unit_price`."
                    },
                    "variant": {
                        "type": "string",
                        "maxLength": 100,
                        "description": "A ready product's variant code, instead of `sku`."
                    },
                    "unit_price": {
                        "type": [
                            "number",
                            "string"
                        ],
                        "description": "Ready lines only: your own price per item, when your shop priced it."
                    }
                }
            },
            "OrderRequest": {
                "type": "object",
                "required": [
                    "order_ref",
                    "lines"
                ],
                "properties": {
                    "order_ref": {
                        "type": "string",
                        "maxLength": 100,
                        "description": "Your own order number. Unique within a store: each store numbers its own orders, and the orders that belong to no store share one numbering."
                    },
                    "lines": {
                        "type": "array",
                        "minItems": 1,
                        "maxItems": 200,
                        "items": {
                            "$ref": "#/components/schemas/OrderLineRequest"
                        }
                    },
                    "placed_at": {
                        "type": "string",
                        "format": "date-time",
                        "description": "When the shopper paid; now when missing."
                    },
                    "test": {
                        "type": "boolean",
                        "description": "true records a test order (no fee, never counted) even with a live key -- for an order your platform marks as a test. It never turns a test key's order into a real one."
                    },
                    "customer": {
                        "type": "object",
                        "description": "`{name, email}`; other keys are dropped.",
                        "properties": {
                            "name": {
                                "type": "string"
                            },
                            "email": {
                                "type": "string"
                            }
                        }
                    },
                    "currency": {
                        "type": "string",
                        "description": "The default for every line's `amount`."
                    },
                    "fx": {
                        "type": "object",
                        "description": "The exchange rate your platform used for amounts in a currency other than your catalogue's: `{base, currency, rate, stale}` -- how many `currency` one `base` (your catalogue's currency) is; `stale: true` for a last known rate rather than the day's. PrintWisp converts those lines at it for its fee when it is believable (within 20% of the store's own rate), else at the store's own rate. Optional: an order is never refused for it.",
                        "properties": {
                            "base": {
                                "type": "string"
                            },
                            "currency": {
                                "type": "string"
                            },
                            "rate": {
                                "type": "number"
                            },
                            "stale": {
                                "type": "boolean"
                            }
                        }
                    },
                    "connection": {
                        "type": "string",
                        "description": "For an account key: a connected store's id (`int_…`). A store's own key needs none."
                    }
                }
            },
            "Component": {
                "type": "object",
                "properties": {
                    "slot": {
                        "type": "string"
                    },
                    "name": {
                        "type": "string"
                    },
                    "value": {
                        "type": "string"
                    },
                    "code": {
                        "type": "string"
                    },
                    "ref": {
                        "type": [
                            "string",
                            "null"
                        ]
                    },
                    "chosen_by": {
                        "type": "string"
                    },
                    "price": {
                        "type": [
                            "string",
                            "number",
                            "null"
                        ]
                    }
                }
            },
            "Adjustment": {
                "type": "object",
                "description": "A photo's filter and levels on one picture box. The print file is made with them applied.",
                "properties": {
                    "page": {
                        "type": "integer",
                        "description": "The page (surface), from 1."
                    },
                    "box": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "The picture box's code."
                    },
                    "filter": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "The filter, by the product's code for it; null for none."
                    },
                    "filter_key": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "enum": [
                            "bw",
                            "sepia",
                            "warm",
                            "cool",
                            "vintage",
                            "hc_bw",
                            "vivid",
                            "matte",
                            null
                        ],
                        "description": "PrintWisp's own name for the filter, whatever the product calls it."
                    },
                    "levels": {
                        "type": "array",
                        "items": {
                            "type": "object",
                            "properties": {
                                "code": {
                                    "type": "string",
                                    "description": "The product's code for the level."
                                },
                                "key": {
                                    "type": "string",
                                    "enum": [
                                        "brightness",
                                        "contrast",
                                        "saturation",
                                        "exposure",
                                        "warmth",
                                        "shadows",
                                        "highlights"
                                    ]
                                },
                                "value": {
                                    "type": "integer",
                                    "minimum": -100,
                                    "maximum": 100
                                }
                            }
                        }
                    }
                },
                "examples": [
                    {
                        "page": 1,
                        "box": "picture",
                        "filter": "mono",
                        "filter_key": "bw",
                        "levels": [
                            {
                                "code": "contrast",
                                "key": "contrast",
                                "value": 20
                            }
                        ]
                    }
                ]
            },
            "Field": {
                "type": "object",
                "properties": {
                    "code": {
                        "type": "string"
                    },
                    "question": {
                        "type": "string"
                    },
                    "answer": {
                        "type": "string"
                    },
                    "value": {
                        "type": "string"
                    }
                }
            },
            "OrderRecorded": {
                "type": "object",
                "properties": {
                    "order": {
                        "type": "string"
                    },
                    "order_ref": {
                        "type": "string"
                    },
                    "created": {
                        "type": "boolean"
                    },
                    "lines": {
                        "type": "array",
                        "items": {
                            "type": "object",
                            "properties": {
                                "line": {
                                    "type": "string"
                                },
                                "line_ref": {
                                    "type": "string"
                                },
                                "type": {
                                    "type": "string",
                                    "enum": [
                                        "print",
                                        "ready"
                                    ]
                                },
                                "design": {
                                    "type": [
                                        "string",
                                        "null"
                                    ]
                                },
                                "sku": {
                                    "type": [
                                        "string",
                                        "null"
                                    ]
                                },
                                "qty": {
                                    "type": "integer"
                                },
                                "unit_price": {
                                    "type": [
                                        "string",
                                        "null"
                                    ]
                                },
                                "print_status": {
                                    "type": "string",
                                    "enum": [
                                        "queued",
                                        "rendering",
                                        "ready",
                                        "failed",
                                        "none"
                                    ],
                                    "description": "`none` for a ready line: nothing to print."
                                },
                                "components": {
                                    "type": "array",
                                    "items": {
                                        "$ref": "#/components/schemas/Component"
                                    }
                                },
                                "fields": {
                                    "type": "array",
                                    "items": {
                                        "$ref": "#/components/schemas/Field"
                                    }
                                },
                                "adjustments": {
                                    "type": "array",
                                    "items": {
                                        "$ref": "#/components/schemas/Adjustment"
                                    },
                                    "description": "Print lines: each photo's filter and levels, by the product's own codes (Product > Basics > Photo filters and levels); [] when none."
                                }
                            }
                        }
                    }
                }
            },
            "Order": {
                "type": "object",
                "properties": {
                    "order": {
                        "type": "string"
                    },
                    "order_ref": {
                        "type": "string"
                    },
                    "livemode": {
                        "type": "boolean",
                        "description": "false for a test order (recorded with a test key, or a test checkout)."
                    },
                    "store": {
                        "type": [
                            "object",
                            "null"
                        ],
                        "description": "Where the order came from: `{type: \"printwisp_store\", id, name}` (id is the store's code) or `{type: \"connection\", id, name, platform}`; null for an order posted with a key of a Your website store or a key with no store, and no connection.",
                        "examples": [
                            {
                                "type": "printwisp_store",
                                "id": "harbour-shop",
                                "name": "Harbour Prints Shop"
                            },
                            {
                                "type": "connection",
                                "id": "int_2b7c9e0d4a1f6835",
                                "name": "Harbour Prints",
                                "platform": "woocommerce"
                            }
                        ]
                    },
                    "source": {
                        "type": "string",
                        "description": "`api` for an order posted to POST /v1/orders; `store` for a PrintWisp Store checkout."
                    },
                    "status": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "Set by the portal on PrintWisp Store orders; null for orders you post."
                    },
                    "placed_at": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "format": "date-time"
                    },
                    "customer": {
                        "type": "object",
                        "properties": {
                            "name": {
                                "type": [
                                    "string",
                                    "null"
                                ]
                            },
                            "email": {
                                "type": [
                                    "string",
                                    "null"
                                ]
                            }
                        }
                    },
                    "shipping": {
                        "type": "object",
                        "properties": {
                            "name": {
                                "type": [
                                    "string",
                                    "null"
                                ]
                            },
                            "address": {
                                "type": [
                                    "object",
                                    "null"
                                ]
                            },
                            "method": {
                                "type": [
                                    "string",
                                    "null"
                                ]
                            },
                            "tracking": {
                                "type": [
                                    "string",
                                    "null"
                                ]
                            }
                        }
                    },
                    "totals": {
                        "type": "object",
                        "properties": {
                            "subtotal": {
                                "type": [
                                    "string",
                                    "null"
                                ]
                            },
                            "shipping": {
                                "type": [
                                    "string",
                                    "null"
                                ]
                            },
                            "tax": {
                                "type": [
                                    "string",
                                    "null"
                                ]
                            },
                            "total": {
                                "type": [
                                    "string",
                                    "null"
                                ]
                            },
                            "currency": {
                                "type": [
                                    "string",
                                    "null"
                                ]
                            }
                        }
                    },
                    "lines": {
                        "type": "array",
                        "items": {
                            "type": "object",
                            "properties": {
                                "line": {
                                    "type": "string"
                                },
                                "line_ref": {
                                    "type": "string"
                                },
                                "type": {
                                    "type": "string",
                                    "enum": [
                                        "print",
                                        "ready"
                                    ],
                                    "description": "`print`: a design and its print file. `ready`: a stocked item to pick by `sku` -- `design` null, `print_status` none."
                                },
                                "product": {
                                    "type": [
                                        "string",
                                        "null"
                                    ],
                                    "description": "The product's code."
                                },
                                "name": {
                                    "type": [
                                        "string",
                                        "null"
                                    ]
                                },
                                "size": {
                                    "type": [
                                        "string",
                                        "null"
                                    ],
                                    "description": "Print lines: the size's code."
                                },
                                "variant": {
                                    "type": [
                                        "string",
                                        "null"
                                    ],
                                    "description": "Ready lines: the variant's code."
                                },
                                "variant_label": {
                                    "type": [
                                        "string",
                                        "null"
                                    ],
                                    "description": "Ready lines: e.g. A3 · Black."
                                },
                                "variant_name": {
                                    "type": [
                                        "string",
                                        "null"
                                    ],
                                    "description": "Ready lines: the variant's own name as the shopper saw it, frozen at the sale (e.g. Snuggle Double); null when it had none."
                                },
                                "extras": {
                                    "type": "array",
                                    "description": "Ready lines: the extras ticked (tickboxes with their own price), frozen at the sale; empty when none.",
                                    "items": {
                                        "type": "object",
                                        "properties": {
                                            "code": {
                                                "type": "string"
                                            },
                                            "name": {
                                                "type": "string"
                                            },
                                            "value": {
                                                "type": "string",
                                                "description": "The Yes value's code."
                                            },
                                            "price": {
                                                "type": [
                                                    "string",
                                                    "null"
                                                ],
                                                "description": "What it added to one item, in the line's currency."
                                            }
                                        }
                                    }
                                },
                                "barcode": {
                                    "type": [
                                        "string",
                                        "null"
                                    ],
                                    "description": "Ready lines: the variant's barcode (GTIN/EAN). Print lines: the item number printed on the book's barcode (same as `item_no`)."
                                },
                                "item_no": {
                                    "type": [
                                        "string",
                                        "null"
                                    ],
                                    "description": "Print lines: what the book's Code 128 barcode reads -- the store's barcode prefix (0-4 of A-Z 0-9) and the line's number padded to 8 digits, e.g. VEL00001234. Null for a ready line and for a line recorded before item numbers existed (its barcode carries `line`).",
                                    "examples": [
                                        "VEL00001234"
                                    ]
                                },
                                "design": {
                                    "type": [
                                        "string",
                                        "null"
                                    ]
                                },
                                "sku": {
                                    "type": [
                                        "string",
                                        "null"
                                    ]
                                },
                                "qty": {
                                    "type": "integer"
                                },
                                "unit_price": {
                                    "type": [
                                        "string",
                                        "null"
                                    ]
                                },
                                "line_amount": {
                                    "type": [
                                        "string",
                                        "null"
                                    ]
                                },
                                "currency": {
                                    "type": [
                                        "string",
                                        "null"
                                    ]
                                },
                                "print_status": {
                                    "type": "string",
                                    "enum": [
                                        "queued",
                                        "rendering",
                                        "ready",
                                        "failed",
                                        "none"
                                    ]
                                },
                                "components": {
                                    "type": "array",
                                    "items": {
                                        "$ref": "#/components/schemas/Component"
                                    }
                                },
                                "fields": {
                                    "type": "array",
                                    "items": {
                                        "$ref": "#/components/schemas/Field"
                                    }
                                },
                                "adjustments": {
                                    "type": "array",
                                    "items": {
                                        "$ref": "#/components/schemas/Adjustment"
                                    },
                                    "description": "Print lines: each photo's filter and levels, by the product's own codes (Product > Basics > Photo filters and levels); [] when none."
                                }
                            }
                        }
                    }
                }
            },
            "Event": {
                "type": "object",
                "required": [
                    "id",
                    "type",
                    "created",
                    "data"
                ],
                "properties": {
                    "id": {
                        "type": "string",
                        "description": "`evt_…`; a retry of the same delivery carries the same id."
                    },
                    "type": {
                        "type": "string",
                        "enum": [
                            "order.created",
                            "design.print_ready",
                            "design.print_failed",
                            "ping"
                        ]
                    },
                    "created": {
                        "type": "string",
                        "format": "date-time"
                    },
                    "data": {
                        "type": "object"
                    }
                }
            },
            "EventList": {
                "type": "object",
                "properties": {
                    "events": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/Event"
                        }
                    },
                    "has_more": {
                        "type": "boolean"
                    }
                }
            },
            "PrintStatus": {
                "type": "object",
                "properties": {
                    "line": {
                        "type": "string"
                    },
                    "order_ref": {
                        "type": "string"
                    },
                    "design": {
                        "type": "string"
                    },
                    "item_no": {
                        "type": [
                            "string",
                            "null"
                        ],
                        "description": "What the book's barcode reads: the store's barcode prefix and the line's number padded to 8 digits (e.g. VEL00001234); null for a line recorded before item numbers existed."
                    },
                    "print_status": {
                        "type": "string",
                        "enum": [
                            "queued",
                            "rendering",
                            "ready",
                            "failed"
                        ]
                    },
                    "components": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/Component"
                        }
                    },
                    "fields": {
                        "type": "array",
                        "items": {
                            "$ref": "#/components/schemas/Field"
                        }
                    },
                    "url": {
                        "type": "string",
                        "description": "When ready: the signed link to GET /v1/file."
                    },
                    "expires_at": {
                        "type": "string",
                        "format": "date-time",
                        "description": "When ready: 10 minutes from now."
                    },
                    "error": {
                        "type": "string",
                        "description": "When failed: why."
                    }
                }
            },
            "ConnectRequest": {
                "type": "object",
                "required": [
                    "code",
                    "site"
                ],
                "properties": {
                    "code": {
                        "type": "string",
                        "description": "The one-time code (48 hex) the shop's browser received after its owner approved."
                    },
                    "site": {
                        "type": "string",
                        "description": "The shop's address; must have the host the code was made for."
                    },
                    "shop_name": {
                        "type": "string",
                        "description": "The shop's own name; names the store until the owner renames it."
                    },
                    "shop_currency": {
                        "type": "string",
                        "description": "The shop's currency, 3 letters."
                    },
                    "prices_include_tax": {
                        "type": "boolean",
                        "description": "Whether the shop's prices include tax."
                    }
                }
            },
            "Catalogue": {
                "type": "object",
                "properties": {
                    "store": {
                        "type": [
                            "object",
                            "null"
                        ],
                        "properties": {
                            "code": {
                                "type": "string"
                            },
                            "name": {
                                "type": "string"
                            },
                            "currency": {
                                "type": "string"
                            }
                        }
                    },
                    "message": {
                        "type": "string",
                        "description": "Only for a key with no store: what to use instead."
                    },
                    "products": {
                        "type": "array",
                        "items": {
                            "type": "object",
                            "properties": {
                                "code": {
                                    "type": "string"
                                },
                                "name": {
                                    "type": "string"
                                },
                                "sell_as": {
                                    "type": "string",
                                    "enum": [
                                        "personalised",
                                        "ready"
                                    ],
                                    "description": "`personalised`: opens the designer (sizes). `ready`: a stocked item -- order its `variants` by `sku`."
                                },
                                "variants": {
                                    "type": "array",
                                    "description": "Ready products only.",
                                    "items": {
                                        "type": "object",
                                        "properties": {
                                            "code": {
                                                "type": "string"
                                            },
                                            "sku": {
                                                "type": "string"
                                            },
                                            "barcode": {
                                                "type": [
                                                    "string",
                                                    "null"
                                                ]
                                            },
                                            "name": {
                                                "type": "string"
                                            },
                                            "options": {
                                                "type": "object",
                                                "description": "Option code -> value code, e.g. {\"size\": \"a3\", \"colour\": \"black\"}."
                                            },
                                            "price": {
                                                "type": [
                                                    "string",
                                                    "null"
                                                ]
                                            },
                                            "currency": {
                                                "type": "string"
                                            },
                                            "stock": {
                                                "type": "object",
                                                "properties": {
                                                    "tracked": {
                                                        "type": "boolean"
                                                    },
                                                    "on_hand": {
                                                        "type": [
                                                            "integer",
                                                            "null"
                                                        ]
                                                    },
                                                    "available": {
                                                        "type": "boolean"
                                                    }
                                                }
                                            },
                                            "title": {
                                                "type": [
                                                    "string",
                                                    "null"
                                                ],
                                                "description": "The variant's own name for shoppers; null = the product name and its option values."
                                            },
                                            "description": {
                                                "type": [
                                                    "string",
                                                    "null"
                                                ],
                                                "description": "The variant's own description, which replaces the product's; null = the product's."
                                            },
                                            "compare_at_price": {
                                                "type": [
                                                    "string",
                                                    "null"
                                                ],
                                                "description": "Shown struck through: the variant's (else the product's) compare-at price in the store's currency, only when higher than price."
                                            },
                                            "extra_kg": {
                                                "type": [
                                                    "number",
                                                    "null"
                                                ],
                                                "description": "Weight this variant adds to the product's weight_kg."
                                            },
                                            "weight_kg": {
                                                "type": [
                                                    "number",
                                                    "null"
                                                ],
                                                "description": "The variant's shipping weight: the product's weight_kg + extra_kg."
                                            }
                                        }
                                    }
                                },
                                "compare_at_price": {
                                    "type": [
                                        "string",
                                        "null"
                                    ],
                                    "description": "Ready products: the product's compare-at price (catalogue currency)."
                                },
                                "weight_kg": {
                                    "type": [
                                        "number",
                                        "null"
                                    ],
                                    "description": "Ready products: the product's shipping weight; each variant adds its extra_kg."
                                },
                                "extras": {
                                    "type": "array",
                                    "description": "Ready products: tickboxes with their own price that make no variants (e.g. Gift wrap). Send the codes ticked as a ready order line's `extras`.",
                                    "items": {
                                        "type": "object",
                                        "properties": {
                                            "code": {
                                                "type": "string"
                                            },
                                            "name": {
                                                "type": "string"
                                            },
                                            "display": {
                                                "type": "string",
                                                "enum": [
                                                    "checkbox"
                                                ]
                                            },
                                            "value": {
                                                "type": "string",
                                                "description": "The Yes value's code."
                                            },
                                            "price": {
                                                "type": [
                                                    "string",
                                                    "null"
                                                ]
                                            },
                                            "currency": {
                                                "type": "string"
                                            }
                                        }
                                    }
                                },
                                "sizes": {
                                    "type": "array",
                                    "items": {
                                        "type": "object",
                                        "properties": {
                                            "code": {
                                                "type": "string",
                                                "description": "The size's code: send it as `profile`."
                                            },
                                            "name": {
                                                "type": "string"
                                            },
                                            "price": {
                                                "type": [
                                                    "string",
                                                    "null"
                                                ]
                                            },
                                            "currency": {
                                                "type": "string"
                                            },
                                            "packed_kg": {
                                                "type": [
                                                    "number",
                                                    "null"
                                                ],
                                                "description": "Packed shipping weight of one copy in kg, as the store's delivery weighs it (null: needs no delivery, or no weight)."
                                            },
                                            "packed_kg_extra": {
                                                "type": [
                                                    "number",
                                                    "null"
                                                ],
                                                "description": "Packed kg each further copy adds -- less than packed_kg when copies nest (posters in one tube)."
                                            }
                                        }
                                    }
                                },
                                "options": {
                                    "type": "array",
                                    "items": {
                                        "type": "object",
                                        "properties": {
                                            "code": {
                                                "type": "string"
                                            },
                                            "name": {
                                                "type": "string"
                                            },
                                            "display": {
                                                "type": "string",
                                                "enum": [
                                                    "buttons",
                                                    "list",
                                                    "swatch",
                                                    "image"
                                                ],
                                                "description": "Ready products: how the option is drawn (buttons, a dropdown, colour swatches, pictures)."
                                            },
                                            "values": {
                                                "type": "array",
                                                "items": {
                                                    "type": "object",
                                                    "properties": {
                                                        "code": {
                                                            "type": "string"
                                                        },
                                                        "name": {
                                                            "type": "string"
                                                        },
                                                        "price_delta": {
                                                            "type": [
                                                                "string",
                                                                "number",
                                                                "null"
                                                            ]
                                                        },
                                                        "swatch": {
                                                            "type": [
                                                                "string",
                                                                "null"
                                                            ],
                                                            "description": "Ready products: the value's colour, #RRGGBB."
                                                        }
                                                    }
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            },
            "Connected": {
                "type": "object",
                "properties": {
                    "ok": {
                        "type": "boolean"
                    },
                    "key": {
                        "type": "string"
                    },
                    "keys": {
                        "type": "object",
                        "properties": {
                            "test": {
                                "type": [
                                    "string",
                                    "null"
                                ]
                            },
                            "live": {
                                "type": "string"
                            }
                        }
                    },
                    "webhook_secret": {
                        "type": "string"
                    },
                    "studio_url": {
                        "type": "string"
                    },
                    "integration": {
                        "type": "string"
                    },
                    "store": {
                        "type": "object",
                        "properties": {
                            "id": {
                                "type": "string"
                            },
                            "name": {
                                "type": "string"
                            },
                            "code": {
                                "type": [
                                    "string",
                                    "null"
                                ]
                            },
                            "status": {
                                "type": [
                                    "string",
                                    "null"
                                ]
                            }
                        }
                    }
                }
            }
        }
    },
    "x-webhook-signature": {
        "header": "Studio-Signature",
        "format": "t=<unix>,v1=<hex HMAC-SHA256(secret, t + \".\" + body)>",
        "tolerance_seconds": 300,
        "timeout_seconds": 10,
        "connect_timeout_seconds": 5,
        "headers": [
            "Content-Type: application/json",
            "User-Agent: Studio-Webhooks/1",
            "Studio-Event: <type>",
            "Studio-Delivery: whd_…",
            "Studio-Signature: t=…,v1=…"
        ],
        "retry_after_seconds": [
            60,
            300,
            1800,
            7200,
            21600,
            86400
        ],
        "max_attempts": 7,
        "disable_after_failures": 20,
        "lease_seconds": 600,
        "test_vector": {
            "secret": "whsec_test_docs_only",
            "t": 1759640400,
            "body": "{\"id\":\"evt_1d3a6f0b4c8e5279\",\"type\":\"ping\",\"created\":\"2026-10-05T04:30:00+00:00\",\"data\":{\"message\":\"A test from Studio.\"}}",
            "header": "t=1759640400,v1=c27782f34bbf98bccb8ca5839107b7cc82f620038ea22912d7464cc9c03faae5"
        },
        "verify": {
            "PHP": "<?php\nfunction verify(string $secret, string $body, string $header, int $tolerance = 300): bool {\n    $t = null; $sigs = [];\n    foreach (explode(',', $header) as $part) {\n        [$k, $v] = array_pad(explode('=', trim($part), 2), 2, null);\n        if ($k === 't' && ctype_digit((string)$v)) { $t = (int)$v; }\n        if ($k === 'v1') { $sigs[] = $v; }\n    }\n    if ($t === null || !$sigs || abs(time() - $t) > $tolerance) { return false; }\n    $want = hash_hmac('sha256', $t . '.' . $body, $secret);\n    foreach ($sigs as $sig) { if (hash_equals($want, (string)$sig)) { return true; } }\n    return false;\n}",
            "JavaScript": "// Node: read the RAW body; a re-serialised body never matches.\nconst crypto = require('crypto');\nfunction verify(secret, rawBody, header, tolerance = 300) {\n  let t = null; const sigs = [];\n  for (const part of String(header).split(',')) {\n    const i = part.indexOf('='); const k = part.slice(0, i).trim(); const v = part.slice(i + 1).trim();\n    if (k === 't' && /^\\d+$/.test(v)) t = Number(v);\n    if (k === 'v1') sigs.push(v);\n  }\n  if (t === null || !sigs.length || Math.abs(Date.now() / 1000 - t) > tolerance) return false;\n  const want = crypto.createHmac('sha256', secret).update(t + '.' + rawBody).digest('hex');\n  return sigs.some(s => s.length === want.length && crypto.timingSafeEqual(Buffer.from(s), Buffer.from(want)));\n}",
            "Python": "import hmac, hashlib, time\n\ndef verify(secret: str, body: bytes, header: str, tolerance: int = 300) -> bool:\n    t, sigs = None, []\n    for part in header.split(\",\"):\n        k, _, v = part.strip().partition(\"=\")\n        if k == \"t\" and v.isdigit():\n            t = int(v)\n        if k == \"v1\":\n            sigs.append(v)\n    if t is None or not sigs or abs(time.time() - t) > tolerance:\n        return False\n    want = hmac.new(secret.encode(), f\"{t}.\".encode() + body, hashlib.sha256).hexdigest()\n    return any(hmac.compare_digest(want, s) for s in sigs)",
            "cURL": "# Recompute a signature by hand to check your code:\nprintf '%s.%s' \"$T\" \"$BODY\" | openssl dgst -sha256 -hmac \"$WEBHOOK_SECRET\"\n# must equal the v1= value when $T is the t= value and $BODY the exact raw body"
        },
        "handler": {
            "PHP": "\n$body = file_get_contents('php://input');\nif (!verify($secret, $body, $_SERVER['HTTP_STUDIO_SIGNATURE'] ?? '')) { http_response_code(400); exit; }\n$event = json_decode($body, true);          // { id, type, created, data }\n// handle $event['type'] … then answer 2xx within 10 seconds\nhttp_response_code(200);",
            "JavaScript": "\napp.post('/hooks/printwisp', express.raw({ type: 'application/json' }), (req, res) => {\n  if (!verify(process.env.PRINTWISP_WEBHOOK_SECRET, req.body, req.get('Studio-Signature'))) return res.sendStatus(400);\n  const event = JSON.parse(req.body);          // { id, type, created, data }\n  res.sendStatus(200);                           // answer first, work later\n});",
            "Python": "\n# Flask\n@app.post(\"/hooks/printwisp\")\ndef hook():\n    if not verify(SECRET, request.get_data(), request.headers.get(\"Studio-Signature\", \"\")):\n        return \"\", 400\n    event = request.get_json()        # { id, type, created, data }\n    return \"\", 200"
        }
    },
    "x-embed": {
        "script": "https://printwisp.com/studio/js/embed.js",
        "global": "StudioEmbed.open(options)",
        "options": [
            {
                "name": "url",
                "type": "string",
                "required": true,
                "description": "The session's url from POST /v1/sessions; http(s) only."
            },
            {
                "name": "container",
                "type": "selector | element",
                "description": "Open inside this box instead of the full-screen overlay (min-height 560 px)."
            },
            {
                "name": "title",
                "type": "string",
                "description": "The product name in the overlay's bar on a phone, and the frame's title."
            },
            {
                "name": "height",
                "type": "CSS length",
                "description": "The frame's height inside a container (default 100%)."
            },
            {
                "name": "remember",
                "type": "string",
                "description": "A 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."
            },
            {
                "name": "rememberAddress",
                "type": "boolean",
                "description": "With remember: false keeps the design id out of the page's address (#pw=); it is still kept in this browser (default true)."
            },
            {
                "name": "guest",
                "type": "boolean",
                "description": "With 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."
            }
        ],
        "statics": [
            {
                "name": "StudioEmbed.reopen(key, options?)",
                "description": "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."
            },
            {
                "name": "StudioEmbed.remembered(key)",
                "description": "{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."
            },
            {
                "name": "StudioEmbed.forget(key)",
                "description": "Drops everything kept for the key."
            },
            {
                "name": "StudioEmbed.forgetAll()",
                "description": "Drops every design this browser kept, for every key. Call it when a shopper signs in or out."
            },
            {
                "name": "StudioEmbed.link()",
                "description": "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."
            },
            {
                "name": "StudioEmbed.version",
                "description": "4 since link(), forgetAll() and the forget message (My designs, 25 Oct 2026); 3 since remember and reopen (24 Oct 2026)."
            }
        ],
        "methods": [
            {
                "name": "design",
                "description": "The design id, after the first save."
            },
            {
                "name": "sig",
                "description": "The design's signature, after the first save."
            },
            {
                "name": "frame",
                "description": "The iframe."
            },
            {
                "name": "on(type, fn)",
                "description": "Listen to a message."
            },
            {
                "name": "off(type, fn)",
                "description": "Stop listening."
            },
            {
                "name": "save()",
                "description": "A Promise for the next design:saved; rejects with {code: \"timeout\"} after 15 s, or {code: \"closed\"}."
            },
            {
                "name": "close()",
                "description": "Overlay only: closes it and fires close."
            },
            {
                "name": "destroy()",
                "description": "Removes the frame and every listener."
            }
        ],
        "messages": [
            {
                "type": "ready",
                "payload": "{design, profile, title, chrome?}",
                "when": "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."
            },
            {
                "type": "design:saved",
                "payload": "{design, sig, name, valid}",
                "when": "After every save, including the one before Add to cart."
            },
            {
                "type": "add-to-cart",
                "payload": "{design, sig, qty, options}",
                "variant": "built-in payload",
                "when": "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."
            },
            {
                "type": "add-to-cart",
                "payload": "<your JSON template>",
                "variant": "message connector",
                "when": "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."
            },
            {
                "type": "cart:navigate",
                "payload": "{url}",
                "when": "A store with a server connector: its cart answered with a page to go to; embed.js sends the shopper there itself."
            },
            {
                "type": "cart:form",
                "payload": "{action, method, fields}",
                "when": "A store with a form connector: its cart wants a form POST; embed.js submits it itself."
            },
            {
                "type": "error",
                "payload": "{code: \"save_failed\", message}",
                "when": "A save failed: the one error the designer sends the page; also the rejection of save()."
            },
            {
                "type": "close",
                "payload": "{design, saved, signed_in?, forgot?}",
                "when": "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."
            },
            {
                "type": "forget",
                "payload": "{design, link}",
                "when": "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."
            }
        ]
    }
}