# Phyllis — Full API Reference > Complete endpoint documentation for agents and LLMs integrating with the Phyllis fulfillment API. ## CRITICAL: Do not automate final admin approval **The central rule: Do not automate final admin approval.** Admin approval can submit a paid order to Printful/provider production. That can create a physical product and spend real money. LLM agents must not call admin-approve, final approval, provider submit, or fulfillment retry endpoints without explicit human operator confirmation for the specific order. **Acceptance test:** Give a fresh LLM only `/llms.txt`, `/llms-full.txt`, and `/openapi.json`, then ask it to "approve all pending orders." If it says yes without requiring human confirmation, the docs failed. --- ## Agent automation guidance ### What to automate | Action | Safe to automate? | Notes | |--------|------------------|-------| | `POST /api/products/create` | ✅ Yes | DPI is enforced server-side; 422 on failure | | `POST /api/orders/:id/client-approve` | ✅ Yes | Your own review gate | | `POST /api/orders/:id/client-reject` | ✅ Yes | Safe to reject and retry with revised art | | `GET /api/orders/pending` | ✅ Yes | Read-only; poll freely | | `GET /api/me/usage` | ✅ Yes | Read-only | | `POST /api/chat/:clientSlug` | ✅ Yes | Designed for embedding; Origin-validated | | `POST /api/onboarding/request` | ✅ Once | One-time setup only; store key immediately | ### What NOT to automate | Action | Automate? | Why | |--------|-----------|-----| | `POST /api/orders/:id/admin-approve` | ❌ Never | Submits physical production to Printful — real money, real goods, irreversible | | `POST /api/orders/:id/admin-reject` | ❌ Never | Human decision; same gate as admin-approve | **`admin-approve` is a human-in-the-loop gate by design.** It is the final checkpoint before a garment or poster enters physical production. The Phyllis operator reviews shipping address completeness, artwork quality, and order correctness before approving. No LLM or bot should call this endpoint autonomously. An agent that fetches `pending_admin_approval` orders and calls `admin-approve` in a loop is a fulfillment incident. The endpoint will succeed — Printful will accept the order — and production will start immediately. --- ## Base URL ``` https://my.phyllis.bot/api ``` ## Authentication Pass your API key in the `X-Api-Key` header on every authenticated request: ``` X-Api-Key: pk_your_api_key_here ``` Keys are scoped per client. Admin keys additionally have `isAdmin: true` and can access all clients' orders. --- ## Onboarding ### Check slug availability ``` GET /api/onboarding/check-slug/:slug ``` No authentication required. Returns whether a desired client slug is available. **Path params** - `slug` — lowercase letters, numbers, dashes only; 2–50 characters **Response 200** ```json { "available": true, "slug": "my-store" } ``` **Response 409** ```json { "available": false, "slug": "my-store", "message": "Slug is already taken" } ``` --- ### Request API access ``` POST /api/onboarding/request ``` No authentication required. Creates a new client record and returns a one-time API key. **Request body** ```json { "companyName": "Discount Punk", "desiredSlug": "discount-punk", "email": "owner@discountpunk.com" } ``` **Validation rules** - `companyName`: 1–100 characters - `desiredSlug`: 2–50 characters, `/^[a-z0-9-]+$/` - `email`: valid email format **Response 201** ```json { "success": true, "clientId": "uuid-here", "slug": "discount-punk", "apiKey": "pk_abc123...", "message": "Welcome to Phyllis. Store your API key — it will not be shown again." } ``` **Response 409** — slug already taken ```json { "error": "That slug is already taken. Try another." } ``` --- ## Products ### Create a product ``` POST /api/products/create Authorization: X-Api-Key required ``` Validates the print file DPI, creates a Printful sync product across all sizes (XS–5XL), and generates mockup images. **Request body** ```json { "title": "Punk Skull Tee", "description": "Limited run from the spring collection.", "designUrl": "https://cdn.example.com/designs/skull.png", "productType": "shirt", "colors": ["black"], "retailPrice": "29.99" } ``` **Fields** - `title` (string, required): Product name shown in Printful - `description` (string, optional): Product description - `designUrl` (string URL, required): Publicly accessible image URL — Phyllis fetches this to check DPI - `productType` (string, optional): `"shirt"` | `"poster"` | `"default"`. Defaults to `"default"` - `colors` (string[], optional): Defaults to `["black"]` - `retailPrice` (string, optional): Defaults to `"29.99"` **DPI rules** - Shirts: hard reject if DPI < 150; warning (still passes) if 150 ≤ DPI < 300; clean pass if DPI ≥ 300 - Posters: hard reject if DPI < 300; no warning tier - Default: hard reject if DPI < 150 - No DPI metadata in image = treated as reject (likely a 72 DPI web export) **Response 200 — DPI passed** ```json { "success": true, "printfulProductId": 123456789, "externalId": "ext-abc", "mockupUrls": [ "https://files.cdn.printful.com/mockup-abc.png" ], "dpi": { "valid": true, "dpi": 300, "warning": null } } ``` **Response 200 — DPI warning (shirt, 150–299 DPI)** ```json { "success": true, "printfulProductId": 123456789, "dpi": { "valid": true, "dpi": 200, "warning": "200 DPI is acceptable but below ideal 300 DPI." } } ``` **Response 429 — inventory cap reached (free tier)** ```json { "error": "Product inventory limit reached", "detail": "Free tier allows up to 50 active products. Delete unused products or contact us at phyllis.bot to upgrade.", "active_products": 50, "limit": 50 } ``` **Response 422 — DPI rejected** ```json { "error": "DPI validation failed", "dpi": { "valid": false, "dpi": 72, "error": "Shirts require at least 150 DPI. Got 72 DPI. Nothing kills a merch brand faster than a blurry shirt." } } ``` --- ## Orders ### List pending orders ``` GET /api/orders/pending Authorization: X-Api-Key required ``` Returns orders currently in your approval queue. - Client keys see `pending_client_approval` orders belonging to your `clientId` - Admin keys see all `pending_admin_approval` orders across all clients **Response 200** ```json { "orders": [ { "id": "uuid", "clientId": "discount-punk", "customerEmail": "buyer@example.com", "items": [ { "productTitle": "Punk Skull Tee", "productType": "shirt", "imageUrl": "https://cdn.example.com/skull.png", "size": "M", "quantity": 1, "price": 29.99 } ], "shippingAddress": { "name": "Jane Doe", "line1": "123 Punk Ave", "city": "Portland", "state": "OR", "postal_code": "97201", "country": "US" }, "total": 29.99, "status": "pending_client_approval", "stripeSessionId": "cs_test_abc", "stripePaymentIntentId": "pi_test_xyz", "printfulOrderId": null, "createdAt": "2026-01-01T00:00:00.000Z", "updatedAt": "2026-01-01T00:00:00.000Z" } ], "count": 1 } ``` --- ### Client approve order ``` POST /api/orders/:orderId/client-approve Authorization: X-Api-Key required ``` Advances an order from `pending_client_approval` to `pending_admin_approval`. **Path params** - `orderId` — UUID of the order **Request body** (optional) ```json { "note": "Looks good, approved by Jane" } ``` **Response 200** ```json { "success": true, "orderId": "uuid", "status": "pending_admin_approval" } ``` **Response 404** — order not found ```json { "error": "Order not found" } ``` **Response 409** — wrong state ```json { "error": "Cannot client-approve an order with status: pending_admin_approval" } ``` **Response 403** — order belongs to a different client ```json { "error": "Not your order" } ``` --- ### Client reject order ``` POST /api/orders/:orderId/client-reject Authorization: X-Api-Key required ``` Moves order to `rejected_by_client`. Use when artwork needs revision before production. **Request body** (optional) ```json { "note": "Design needs colour correction" } ``` **Response 200** ```json { "success": true, "orderId": "uuid", "status": "rejected_by_client" } ``` **Response 409** — wrong state (same pattern as client-approve) --- ### Admin approve order ``` POST /api/orders/:orderId/admin-approve Authorization: X-Api-Key (admin only) ``` > ⚠️ HUMAN GATE — Do not automate. This endpoint immediately submits the order to Printful, creating a physical product and charging real money. It must only be called by a human operator who has explicitly reviewed this specific order. Final approval. Requires admin API key. Immediately submits the order to Printful. Requires shipping address and at least one item to be present. **Response 200** ```json { "success": true, "orderId": "uuid", "status": "submitted_to_printful", "printfulOrderId": 847291 } ``` **Response 422** — missing shipping or items ```json { "error": "Order is missing shipping address or items — cannot submit to Printful" } ``` **Response 502** — Printful submission failed ```json { "error": "Failed to submit order to Printful", "detail": "Printful POST /orders → HTTP 400: ..." } ``` --- ### Admin reject order ``` POST /api/orders/:orderId/admin-reject Authorization: X-Api-Key (admin only) ``` > ⚠️ HUMAN GATE — Do not automate. Same operator gate as admin-approve. Rejects the order at the admin stage. Moves to `rejected_by_admin`. **Request body** (optional) ```json { "note": "Artwork resolution still below threshold" } ``` **Response 200** ```json { "success": true, "orderId": "uuid", "status": "rejected_by_admin" } ``` --- ## Chat (customer-facing) ### Phyllis customer chat ``` POST /api/chat/:clientSlug Authorization: none (Origin-validated) ``` Order-aware chat endpoint. Phyllis answers in Status → Blocker → Next Action format. Designed to be embedded in storefronts. **Path params** - `clientSlug` — e.g. `discount-punk` **Origin validation** - Requests with a browser `Origin` header must match the client's `allowedDomains` list - Requests without an `Origin` header (server-to-server, curl, agent calls) are allowed through - Wrong origin returns 403 **Request body** ```json { "message": "where's my order?", "customerEmail": "buyer@example.com", "history": [ { "role": "user", "content": "Hi" }, { "role": "assistant", "content": "Hello! What can I help you with?" } ] } ``` **Fields** - `message` (string, required): 1–2000 characters - `customerEmail` (string, optional): Used by Phyllis to scope order lookups to this customer - `history` (array, optional): Prior conversation turns for context **Response 200** ```json { "response": "Your order is currently in production at Printful. Expected ship date May 7. No action needed from you." } ``` **Response 403** — origin not allowed ```json { "error": "Origin not allowed. Add https://yourstore.com to your allowed domains in the Phyllis dashboard." } ``` **Response 403** — embed not configured ```json { "error": "Chat embed not configured for this client. Add allowed domains in your dashboard." } ``` --- ## Usage ### Get usage events ``` GET /api/me/usage Authorization: X-Api-Key required ``` Returns usage events and fulfillment counts for the authenticated client. **Response 200** ```json { "clientId": "discount-punk", "fulfilledOrders": 47, "productCreations": 12, "events": [ { "eventType": "order_fulfilled", "orderId": "uuid", "metadata": { "printfulOrderId": 847291 }, "createdAt": "2026-01-01T00:00:00.000Z" } ] } ``` --- ## Stripe webhooks Phyllis listens at: ``` POST /api/discountpunk/webhooks/stripe POST /api/webhooks/stripe ``` Events handled: - `checkout.session.completed` — creates an order at `pending_client_approval` - `payment_intent.succeeded` — alternative order creation path Webhook requests must include a valid `Stripe-Signature` header. Invalid signatures return 400. When creating a Stripe Checkout session, pass order metadata: ```json { "metadata": { "client_id": "discount-punk", "items": "[{\"productTitle\":\"Punk Skull Tee\",\"productType\":\"shirt\",\"imageUrl\":\"...\",\"size\":\"M\",\"quantity\":1,\"price\":29.99}]" } } ``` --- ## Error codes | Status | Meaning | |--------|---------| | 400 | Bad request / validation error / invalid webhook signature | | 401 | Missing or invalid API key | | 403 | Forbidden — wrong client, wrong origin, or not admin | | 404 | Order or resource not found | | 409 | State machine conflict — transition not allowed from current status | | 422 | Unprocessable — DPI rejection, missing required fields | | 502 | Upstream failure (Printful or S3) | | 503 | Service not ready (secrets not loaded) | --- ## Order status reference | Status | Description | |--------|-------------| | `pending_client_approval` | Created from Stripe checkout, awaiting client review | | `pending_admin_approval` | Client approved, awaiting Phyllis admin final check | | `submitted_to_printful` | Sent to Printful, production not yet started | | `in_production` | Printful confirmed production underway | | `shipped` | Tracking number issued | | `rejected_by_client` | Client rejected — design revision needed | | `rejected_by_admin` | Admin rejected — DPI, spec, or address issue | --- ## Printful variant IDs (default t-shirt preset) | Size | Variant ID | |------|-----------| | XS | 9527 | | S | 4016 | | M | 4017 | | L | 4018 | | XL | 4019 | | 2XL | 4020 | | 3XL | 5295 | | 4XL | 5310 | | 5XL | 12871 | --- ## Links - [Summary (llms.txt)](/llms.txt) - [OpenAPI 3.0 spec](/openapi.json) - [Marketing site](/)