📡 Public REST API

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.

Your two keys

Every workspace gets two keys, visible in your workspace under Integration:

KeyPrefixUse
Embed keyfk_…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 secretsk_…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.

Authentication

Three mechanisms exist, depending on the endpoint:

Public endpoints (no auth)

EndpointDescription
GET /api/plansActive SaaS plans with their features_json.
GET /api/categoriesSports / product categories: slug, name, icon, image, color, sort_order.
GET /api/catalog3D 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/publicPlatform counters: { "tenants": …, "orders": …, "designs": … }.
POST /api/contactContact form. Body requires name, email, message.

GET /api/resolve — SKU → 3D product

Maps an external shop reference (Shopify / WooCommerce / custom SKU) to one of your 3D products. Used by embed.js auto-detection and the SDK.

Query paramDescription
embed_keyRequired. Your fk_ key.
external_skuShop SKU (checked against imported external products, then against products.external_sku).
external_idShop product ID.
external_urlProduct 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.

Widget endpoints (embed key required)

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).

EndpointDescription
GET /api/embed/bootstrapEverything 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/trackWidget analytics event. Body: { "type": "…", "data": { … } } (type required).
curl -H "X-Embed-Key: fk_xxx" https://printwearai.com/api/embed/bootstrap

Saved configs & orders (session required)

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):

EndpointDescription
GET /api/configsList my saved configurations (without heavy fields).
GET /api/configs/{id}One config with its full config_json.
POST /api/configsSave 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/ordersCreate 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/ordersList my orders.
POST /api/customer/orders/{id}/checkout(Re)create a Stripe Checkout session for an unpaid order; returns { "url": … }.

Workspace endpoints (staff session)

Available to your team from the app (Orders screen uses them):

EndpointDescription
GET /api/ordersOrders of your workspace (filter with ?status=, max 200).
GET /api/orders/{id}Order detail with items and addresses.
PUT /api/orders/{id}/statusUpdate status (draft, confirmed, paid, in_production, shipped, delivered, cancelled). Fires order.updated.
GET /api/orders/{id}/items/{iid}/patron.pdfDownload the production pattern as a CMYK PDF (ISO Coated v2 / FOGRA39 output intent) — the print file.
GET /api/orders/{id}/items/{iid}/patron.svgDownload the production pattern SVG of an order item — see Production exports.
GET /api/orders/{id}/exportFull production JSON of an order (items, sizes, config snapshots, addresses).

Errors

Every error is JSON with a single error field (occasionally extra fields):

{ "error": "Product not found" }
StatusMeaning
400Invalid or missing parameter.
401Not authenticated (no/expired session).
402Plan limit — feature not in your plan, or monthly order quota reached.
403Invalid key or insufficient role.
404Resource not found (or unknown workspace).
429Rate limit exceeded — see below.
503Dependent service unavailable (e.g. merchant payments not configured).

Rate limiting

Widget endpoints are rate-limited per embed key on a sliding window:

EndpointLimit
GET /api/embed/bootstrap60 requests / minute
GET /api/embed/design/{id}120 requests / minute
POST /api/embed/track600 requests / minute
POST /api/export/print-ready60 requests / minute

When exceeded, the API returns 429 with a Retry-After header and body { "error": "Too many requests, please try again later" }.

Going further