Everything the widget, the SDK and your integrations talk to. Base URL: https://printwearai.com/api. All endpoints exchange JSON (Content-Type: application/json) unless stated otherwise.
Every workspace gets two keys, visible in your workspace under Integration:
| Key | Prefix | Use |
|---|---|---|
| Embed key | fk_… | Public. Identifies your workspace on widget/catalog endpoints. Safe to ship in frontend code — it only grants read access to your published catalog and widget bootstrap data. |
| API secret | sk_… | Secret. Generated at signup, shown only to workspace owners/admins (hidden for members). Keep it server-side only. Note: the endpoints on this page do not currently accept the sk_ key as a request credential — authenticated endpoints use your app session (see below). The secret is reserved for server-to-server features; treat it like a password anyway. |
Both keys can be rotated at any time (workspace admin session required): POST /api/settings/rotate-embed-key and POST /api/settings/rotate-api-secret. Rotating invalidates the previous key immediately.
Three mechanisms exist, depending on the endpoint:
X-Embed-Key: fk_xxx (recommended), or append ?embed_key=fk_xxx to the URL. The query-string form works but leaks the key into Referer headers, proxy logs and browser history, so the API answers it with a X-Embed-Key-Warning response header. Use the header in new code.pwai_session, or the same session token passed as Authorization: Bearer <token>.| Endpoint | Description |
|---|---|
GET /api/plans | Active SaaS plans with their features_json. |
GET /api/categories | Sports / product categories: slug, name, icon, image, color, sort_order. |
GET /api/catalog | 3D catalog. With an embed key (or a session) you get global products plus products exclusive to your workspace; without one, only the global published catalog. Each item includes zones_config, default_sizes, default_tiers, nb_designs, nb_patterns. |
GET /api/stats/public | Platform counters: { "tenants": …, "orders": …, "designs": … }. |
POST /api/contact | Contact form. Body requires name, email, message. |
GET /api/resolve — SKU → 3D productMaps an external shop reference (Shopify / WooCommerce / custom SKU) to one of your 3D products. Used by embed.js auto-detection and the SDK.
| Query param | Description |
|---|---|
embed_key | Required. Your fk_ key. |
external_sku | Shop SKU (checked against imported external products, then against products.external_sku). |
external_id | Shop product ID. |
external_url | Product page URL (path-based fallback match). |
curl "https://printwearai.com/api/resolve?embed_key=fk_xxx&external_sku=MY-SKU-42"
{
"product_slug": "tshirt-classic-v1",
"product_name": "Classic T-shirt",
"glb_path": "/models/tshirt-classic-v1.glb",
"thumbnail": "/uploads/…/thumb.webp",
"price_base": 24.9,
"zones_config": { … },
"sizes": ["S","M","L","XL"],
"external_sku": "MY-SKU-42",
"external_id": ""
}
Errors: 400 missing embed_key, 403 invalid key, 404 no 3D product linked to that reference.
These power the embedded configurator. They are CORS-enabled and require a plan that includes embedding — Free/Essentiel workspaces get a 402 and should use the hosted page instead (see Embed widget).
| Endpoint | Description |
|---|---|
GET /api/embed/bootstrap | Everything the widget needs in one call: tenant_slug, brand (name, logo, colors, currency, VAT, shipping, fulfillment mode) and products, each with its attached designs and patterns. |
GET /api/embed/design/{id} | Full SVG data for one design. |
POST /api/embed/track | Widget analytics event. Body: { "type": "…", "data": { … } } (type required). |
curl -H "X-Embed-Key: fk_xxx" https://printwearai.com/api/embed/bootstrap
End customers who create an account can save designs and place orders. These endpoints need a logged-in session plus workspace context (embed key or session tenant):
| Endpoint | Description |
|---|---|
GET /api/configs | List my saved configurations (without heavy fields). |
GET /api/configs/{id} | One config with its full config_json. |
POST /api/configs | Save a config. Requires product_id + config_json; optional name, patron_svg, thumbnail_b64. Fires the config.saved webhook. |
PUT /api/configs/{id} / DELETE /api/configs/{id} | Update / delete my config. |
POST /api/customer/orders | Create an order from items[] (product_id, quantity, sizes_breakdown, optional config_id, patron_svg, config_snapshot). Prices are recomputed server-side from the product tiers. If the merchant has Stripe payments enabled, the order is created as pending_payment and the response includes a checkout_url; otherwise it is confirmed directly. Fires order.created. |
GET /api/customer/orders | List my orders. |
POST /api/customer/orders/{id}/checkout | (Re)create a Stripe Checkout session for an unpaid order; returns { "url": … }. |
Available to your team from the app (Orders screen uses them):
| Endpoint | Description |
|---|---|
GET /api/orders | Orders of your workspace (filter with ?status=, max 200). |
GET /api/orders/{id} | Order detail with items and addresses. |
PUT /api/orders/{id}/status | Update status (draft, confirmed, paid, in_production, shipped, delivered, cancelled). Fires order.updated. |
GET /api/orders/{id}/items/{iid}/patron.pdf | Download the production pattern as a CMYK PDF (ISO Coated v2 / FOGRA39 output intent) — the print file. |
GET /api/orders/{id}/items/{iid}/patron.svg | Download the production pattern SVG of an order item — see Production exports. |
GET /api/orders/{id}/export | Full production JSON of an order (items, sizes, config snapshots, addresses). |
Every error is JSON with a single error field (occasionally extra fields):
{ "error": "Product not found" }
| Status | Meaning |
|---|---|
400 | Invalid or missing parameter. |
401 | Not authenticated (no/expired session). |
402 | Plan limit — feature not in your plan, or monthly order quota reached. |
403 | Invalid key or insufficient role. |
404 | Resource not found (or unknown workspace). |
429 | Rate limit exceeded — see below. |
503 | Dependent service unavailable (e.g. merchant payments not configured). |
Widget endpoints are rate-limited per embed key on a sliding window:
| Endpoint | Limit |
|---|---|
GET /api/embed/bootstrap | 60 requests / minute |
GET /api/embed/design/{id} | 120 requests / minute |
POST /api/embed/track | 600 requests / minute |
POST /api/export/print-ready | 60 requests / minute |
When exceeded, the API returns 429 with a Retry-After header and body { "error": "Too many requests, please try again later" }.
order.created, order.updated, config.saved… instead of polling.