WorldForge Studio

08 — Print

PDF compilation, artifacts, the print-shop directory, orders, reviews, and claim/verify.

Scopes: print:read for GETs, print:write for mutations, plus role checks (shop owner / platform admin) where noted.

Compile & artifacts

MethodPathNotes
POST/print/compileRender a stored artifact or an inline compiled sheet to PDF or PNG (see below)
GET/print/artifactsList stored artifacts (print_artifacts)
POST/print/artifactsStore an artifact
DELETE/print/artifacts/:idRemove

/print/compile is mounted at /print behind requireAuth (and is the one print route an API key with only print:read may call). Query: format=pdf|png (default pdf), pageSize=A4|A5|A6 (default A4); a bad query is 400 invalid_query. Body, strict, discriminated on source:

  • { "source": "artifact", "artifactId": "pa_…" } — renders the caller's own stored artifact. An unknown or someone else's artifact is 404 unknown_artifact.
  • { "source": "inline", "docId": "[A-Za-z0-9_-]+", "compiled": { pageMm: { w, h }, ops: [...] } } — ops are rect, circle, line, path and text vector ops with RGBA colours in 0–1.

A body that fails the schema is 400 invalid_compiled_sheet. Render failures: 400 unsupported_op, 400 invalid_color, 400 invalid_transform, otherwise 500 render_failed. Success is 200 with the bytes (application/pdf or image/png, non-JSON; use a raw fetch) and Content-Disposition: attachment; filename="sheet-<docId or artifactId>.<ext>". Every compile, successful or not, writes a print.compile audit row.

Directory — /print-shops

DB-backed (migration 0030), haversine-filtered:

MethodPathNotes
GET/print-shops?near=lat,lng&radius_km=10List / nearby search (distanceKm when filtered)
GET/print-shops/:idDetail
POST/print-shopsAdmin create — platform_admin role (user_roles), not a scope

Owner surface

MethodPathNotes
GET/print-shops/mineShops owned by the caller
PATCH/print-shops/:idEdit (owner only, via owner_user_id)
GET/print-shops/:id/ordersThe shop's orders, newest first: { orders: [{ id, pickupName, pageCount, mode, artifactId, qrPayload, status, createdAt }] }. Shop owner or platform admin; scope print-shop:write. 404 unknown_shop, 403 not_owner
PATCH/print-shops/:id/orders/:orderIdUpdate an order's status. Body, strict: { status: "ready" | "picked_up" }. Forward-only (pending → ready → picked_up, pending → picked_up allowed); the same status is a no-op, going back is 409 invalid_transition. 404 unknown_order. Answers { order: { id, status } }, plus notified when the customer was told the order is ready
POST/print-shops/:id/claim-tokenAdmin-issued one-shot claim token
POST/print-shops/:id/claimRedeem a token to become owner

Reviews & replies

MethodPathNotes
GET/print-shops/:id/reviews?limit=&offset=Public reviews, newest first, no auth. limit 1–50 (20), offset ≥ 0 (0), strict (400 invalid_query); 404 unknown_shop. { reviews: [{ id, stars, comment, author: "Verified customer", ownerReply, createdAt, updatedAt }], total, ratingAvg, ratingCount }. Only ratings with a comment are listed and comments a moderator hid are left out (their stars still count in ratingAvg/ratingCount); id is the order id
GET/print-shops/:id/reviews/allUnfiltered (owner)
POST/print-shops/:id/reviews/:orderId/replyOwner reply — idempotent POST
PATCH/print-shops/:id/reviews/:orderId/replyEdit the reply
DELETE/print-shops/:id/reviews/:orderId/replyRemove it
POST/print-shops/:id/reviews/:orderId/reportReport a review
POST/print-shops/:id/verifyOwner/admin verify
DELETE/print-shops/:id/verifyUnverify

Admin extras

MethodPathNotes
GET/print-shops/admin/shopsAdmin listing
GET/print-shops/admin/places/searchPlace search (Google Places proxy)
GET/print-shops/admin/places/:placeIdPlace detail
GET/print-shops/admin/reviews/reportedModeration queue
POST/print-shops/admin/reviews/:orderId/:actionhide / unhide / dismiss

Orders — /print-orders

MethodPathNotes
GET/print-ordersThe caller's orders, newest first, each with rating ({ stars, comment, updatedAt } or null)
POST/print-ordersPlace an order. Body, strict: shopId, artifactId (yours; the order copies its mode and world), pageCount (1–10000), pickupName (1–120), optional mode (gm|player|battle|poster|atlas, must equal the artifact's mode or 400 invalid_body / mode_mismatch). 404 with reason unknown_shop or unknown_artifact. 201 with the order (id po_…, status: "pending", qrPayload: "gas-print:v1:<id>")
GET/print-orders/:idDetail, with rating. Someone else's order is 404 not_found, never 403, so ids cannot be probed
PUT/print-orders/:id/ratingRate the shop for this order. Body, strict: { stars: 1–5, comment?: ≤1000 chars }. Only after pickup: 409 order_not_picked_up (with status). Re-PUT edits; changing the comment text clears a moderator hide and any open report. Answers { rating: { stars, comment, updatedAt } }
DELETE/print-orders/:id/ratingRetract your rating; 204, or 404 when there is none
GET/print-orders/:id/qrThe pickup code as JSON: { id, qrPayload }. Not an image; the server ships no QR codec and the client renders qrPayload (gas-print:v1:<orderId>) itself. Someone else's order is 404

All /print-orders routes need a resolvable caller (403 unresolved_caller otherwise) and only ever see the caller's own orders.

There is no /print-shops/:id/rating, /print-shops/:id/qr or /print-orders/:id/reviews: ratings and the QR belong to an order (/print-orders/:id/…), and a shop's reviews are GET /print-shops/:id/reviews.