{"openapi":"3.1.0","info":{"title":"SaaSTARTER Storefront API","version":"1.0.0","description":"API for building storefronts and ecommerce experiences.\n\n## Quick Start\n\n1. **Authenticate** — `POST /api/auth/sign-in/email` with `{ email, password }` to get a session cookie, or browse as a guest\n2. **Browse products** — `GET /api/products` to list products, `GET /api/products/{id}` for details\n3. **Search** — `GET /api/search?q=keyword` for full-text search\n4. **Add to cart** — Use the cart endpoints to manage items\n5. **Apply discount** — `POST /api/cart/apply-discount` with a discount code\n6. **Checkout** — `POST /api/payment-amount` to calculate the final total, then complete payment via Stripe\n7. **View orders** — `GET /api/orders` to see order history\n\n## Authentication\n\nTwo authentication methods are supported:\n\n**Session Cookie** — Sign in via `POST /api/auth/sign-in/email` with `{ email, password }`. The `better-auth.session_token` cookie is set automatically.\n\n**API Key** — Pass an `x-api-key` header with a scoped API key. Create keys at `/account/developer` or via `POST /api/auth/api-key/create`. Keys are scoped to specific resources (products, cart, orders, reviews, wishlist).\n\n## Error Format\n\nAll errors return `{ error: string }`. Validation errors additionally include `{ details: { fieldErrors, formErrors } }` with per-field messages.","contact":{"name":"API Support","email":"support@yourdomain.com"},"license":{"name":"MIT"}},"servers":[{"url":"http://localhost:3000","description":"API Server"}],"externalDocs":{"description":"Human-readable API reference (Markdown)","url":"http://localhost:3000/to-humans.md"},"tags":[{"name":"Contact","description":"Contact form submissions."},{"name":"Newsletter","description":"Email newsletter subscriptions."},{"name":"Reviews","description":"Product reviews and ratings."},{"name":"Wishlist","description":"Customer product wishlists."},{"name":"Cart","description":"Shopping cart discount management."},{"name":"Discounts","description":"Discount code validation."},{"name":"Payments","description":"Payment amount calculation."},{"name":"Orders","description":"Customer order history."},{"name":"Search","description":"Product search and autocomplete."},{"name":"Recommendations","description":"Product recommendations and tracking."}],"paths":{"/api/contact":{"post":{"summary":"Submit contact form","description":"Accepts a contact form submission and stores it in the Payload CMS `contact-form-submissions` collection. No authentication is required.","tags":["Contact"],"responses":{"201":{"description":"Contact form submitted successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true,"description":"Indicates the submission was saved"}}}}}},"400":{"description":"Validation error — missing or invalid fields","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","email","subject","message"],"properties":{"name":{"type":"string","minLength":1,"maxLength":150,"pattern":"^[\\p{L}\\p{N}\\s\\-'.]+$","description":"Full name of the person submitting the form","example":"Jane Doe"},"email":{"type":"string","format":"email","minLength":5,"maxLength":320,"description":"Contact email address","example":"jane@example.com"},"subject":{"type":"string","minLength":1,"maxLength":200,"description":"Subject line for the contact message","example":"Partnership inquiry"},"message":{"type":"string","minLength":1,"maxLength":5000,"description":"Body of the contact message","example":"Hi, I would love to discuss a potential partnership. Please let me know a good time to connect."}}}}}},"x-dashboard-trigger":"Payload Admin > Contact Form Submissions"}},"/api/newsletter":{"post":{"summary":"Subscribe to newsletter","description":"Subscribes an email address to the newsletter. The address is stored in the Payload CMS `newsletter-subscribers` collection and optionally synced to a Resend audience when `RESEND_API_KEY` and `RESEND_AUDIENCE_ID` are configured. Duplicate emails are silently ignored.","tags":["Newsletter"],"responses":{"200":{"description":"Successfully subscribed (or already subscribed)","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true,"description":"Indicates the subscription was recorded"}}}}}},"400":{"description":"Validation error — invalid email address","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email","minLength":5,"maxLength":320,"description":"Email address to subscribe to the newsletter","example":"subscriber@example.com"}}}}}},"x-dashboard-trigger":"Payload Admin > Newsletter Subscribers"}},"/api/reviews":{"get":{"summary":"List product reviews","description":"Returns a paginated list of approved reviews for a given product, along with aggregate statistics (average rating, star-rating breakdown).","tags":["Reviews"],"responses":{"200":{"description":"Paginated reviews with aggregate statistics","content":{"application/json":{"schema":{"type":"object","properties":{"reviews":{"type":"array","maxItems":50,"items":{"type":"object"},"description":"Array of approved review documents"},"totalPages":{"type":"integer","minimum":0,"description":"Total number of pages","example":3},"page":{"type":"integer","minimum":1,"description":"Current page number","example":1},"totalDocs":{"type":"integer","minimum":0,"description":"Total number of approved reviews","example":28},"averageRating":{"type":"number","minimum":0,"maximum":5,"description":"Average rating rounded to one decimal place","example":4.3},"breakdown":{"type":"object","description":"Count of reviews per star rating","properties":{"1":{"type":"integer","minimum":0,"example":1},"2":{"type":"integer","minimum":0,"example":2},"3":{"type":"integer","minimum":0,"example":5},"4":{"type":"integer","minimum":0,"example":10},"5":{"type":"integer","minimum":0,"example":10}}}}}}}},"400":{"description":"Validation error — missing productId","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[],"parameters":[{"name":"productId","in":"query","required":true,"description":"Numeric ID of the product to fetch reviews for","schema":{"type":"string","minLength":1,"maxLength":20,"example":"42"}},{"name":"page","in":"query","required":false,"description":"Page number for pagination (defaults to 1)","schema":{"type":"string","minLength":1,"maxLength":10,"example":"1"}},{"name":"limit","in":"query","required":false,"description":"Number of reviews per page (max 50, defaults to 10)","schema":{"type":"string","minLength":1,"maxLength":3,"example":"10"}},{"name":"sort","in":"query","required":false,"description":"Sort order for reviews","schema":{"type":"string","enum":["-createdAt","helpful","highest","lowest"],"example":"-createdAt"}}]},"post":{"summary":"Create a product review","description":"Creates a new review for a product. Requires authentication. The review is created with `pending` status and must be approved by an admin before it appears publicly. Duplicate reviews per user per product are rejected (409). Verified purchase status is automatically detected from order history.","tags":["Reviews"],"responses":{"201":{"description":"Review created successfully (status: pending)","content":{"application/json":{"schema":{"type":"object","properties":{"review":{"type":"object","description":"The newly created review document"}}}}}},"400":{"description":"Validation error — missing or invalid fields","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"401":{"description":"Unauthorized — authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Authenticated user not found in Payload users collection","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflict — user has already reviewed this product","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"cookieAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["productId","rating","title","body"],"properties":{"productId":{"type":"integer","minimum":1,"maximum":2147483647,"description":"Numeric ID of the product being reviewed","example":42},"rating":{"type":"integer","minimum":1,"maximum":5,"description":"Star rating from 1 to 5","example":4},"title":{"type":"string","minLength":1,"maxLength":200,"description":"Short title for the review","example":"Great quality product"},"body":{"type":"string","minLength":1,"maxLength":5000,"description":"Full text of the review","example":"I have been using this for a month and the build quality is excellent. Highly recommended."}}}}}}}},"/api/reviews/{reviewId}/helpful":{"post":{"summary":"Mark a review as helpful","description":"Increments the helpful vote count on a review. Requires authentication. The review must exist; otherwise a 404 is returned.","tags":["Reviews"],"responses":{"200":{"description":"Helpful count incremented successfully","content":{"application/json":{"schema":{"type":"object","properties":{"helpfulCount":{"type":"integer","minimum":0,"description":"Updated helpful vote count","example":5}}}}}},"401":{"description":"Unauthorized — authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Review not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"cookieAuth":[]}],"parameters":[{"name":"reviewId","in":"path","required":true,"description":"Numeric ID of the review to mark as helpful","schema":{"type":"string","minLength":1,"maxLength":20,"example":"17"}}]}},"/api/wishlist":{"get":{"summary":"Get user wishlist","description":"Returns all wishlist items for the authenticated user, sorted by most recently added. Each item includes full product and variant relations (depth 2). Limited to 50 items.","tags":["Wishlist"],"responses":{"200":{"description":"Wishlist items retrieved successfully","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","maxItems":50,"items":{"type":"object"},"description":"Array of wishlist item documents with populated product/variant relations"}}}}}},"401":{"description":"Unauthorized — authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"cookieAuth":[]}]},"post":{"summary":"Add item to wishlist","description":"Adds a product (and optionally a specific variant) to the authenticated user's wishlist. Returns 409 if the product is already in the wishlist.","tags":["Wishlist"],"responses":{"201":{"description":"Item added to wishlist","content":{"application/json":{"schema":{"type":"object","properties":{"item":{"type":"object","description":"The newly created wishlist item document"}}}}}},"400":{"description":"Validation error — missing productId","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}}},"401":{"description":"Unauthorized — authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Authenticated user not found in Payload users collection","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Conflict — product is already in the wishlist","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"cookieAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["productId"],"properties":{"productId":{"type":"integer","minimum":1,"maximum":2147483647,"description":"Numeric ID of the product to add to the wishlist","example":42},"variantId":{"type":"integer","minimum":1,"maximum":2147483647,"description":"Optional numeric ID of a specific product variant","example":7}}}}}}}},"/api/wishlist/{itemId}":{"delete":{"summary":"Remove item from wishlist","description":"Deletes a specific wishlist item by its ID. Requires authentication and ownership verification — users can only remove their own wishlist items. Returns 403 if the item belongs to another user.","tags":["Wishlist"],"responses":{"200":{"description":"Wishlist item removed successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","example":true,"description":"Indicates the item was removed"}}}}}},"401":{"description":"Unauthorized — authentication required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Forbidden — wishlist item belongs to another user","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Authenticated user not found in Payload users collection","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"security":[{"cookieAuth":[]}],"parameters":[{"name":"itemId","in":"path","required":true,"description":"Numeric ID of the wishlist item to remove","schema":{"type":"string","minLength":1,"maxLength":20,"example":"123"}}]}},"/api/wishlist/check/{productId}":{"get":{"summary":"Check if product is in wishlist","description":"Checks whether a specific product is in the authenticated user's wishlist. If the user is not authenticated or not found, returns `{ inWishlist: false }` without an error. When the product is found in the wishlist, the response includes the `itemId` for convenient removal.","tags":["Wishlist"],"responses":{"200":{"description":"Wishlist check result","content":{"application/json":{"schema":{"type":"object","properties":{"inWishlist":{"type":"boolean","description":"Whether the product is in the user's wishlist","example":true},"itemId":{"oneOf":[{"type":"integer","minimum":1,"description":"ID of the matching wishlist item","example":123},{"type":"null"}],"description":"ID of the matching wishlist item, or null if not in the wishlist"}}}}}}},"security":[{"cookieAuth":[]}],"parameters":[{"name":"productId","in":"path","required":true,"description":"Numeric ID of the product to check","schema":{"type":"string","minLength":1,"maxLength":20,"example":"42"}}]}},"/api/cart/apply-discount":{"post":{"summary":"Apply a discount code to a cart","description":"Validates and applies a discount code to the specified cart. The caller must own the cart (via session) or provide the cart secret. The discount is validated against the `/api/discount/validate` endpoint before being persisted.","tags":["Cart"],"responses":{"200":{"description":"Discount applied successfully","content":{"application/json":{"schema":{"type":"object","required":["success","discount"],"properties":{"success":{"type":"boolean","enum":[true],"example":true},"discount":{"type":"object","required":["discountId","code","discountType","discountValue","maxDiscountAmount","discountAmount"],"properties":{"discountId":{"type":"integer","minimum":1,"description":"The internal ID of the discount code record","example":7},"code":{"type":"string","description":"The normalised discount code","example":"SAVE20"},"discountType":{"type":"string","enum":["percentage","flat"],"description":"Whether the discount is a percentage or flat amount","example":"percentage"},"discountValue":{"type":"number","minimum":0,"description":"The discount value (percentage points or cents)","example":20},"maxDiscountAmount":{"type":["number","null"],"minimum":0,"description":"Maximum discount cap in cents (percentage type only)","example":5000},"discountAmount":{"type":"number","minimum":0,"description":"Calculated discount amount in cents for the current cart","example":2000}}}}},"example":{"success":true,"discount":{"discountId":7,"code":"SAVE20","discountType":"percentage","discountValue":20,"maxDiscountAmount":5000,"discountAmount":2000}}}}},"400":{"description":"Validation error or invalid discount code","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"},"example":{"success":false,"error":"Discount code is required"}}}},"403":{"description":"Not authorised to modify this cart","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"success":false,"error":"Not authorized"}}}},"404":{"description":"Cart not found or already purchased","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"success":false,"error":"Cart not found"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"success":false,"error":"Failed to apply discount"}}}}},"security":[{"cookieAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["code","cartId"],"properties":{"code":{"type":"string","minLength":1,"maxLength":50,"description":"The discount code to apply to the cart","example":"SAVE20"},"cartId":{"type":"integer","minimum":1,"maximum":2147483647,"description":"The ID of the cart to apply the discount to","example":42},"secret":{"type":"string","minLength":1,"maxLength":255,"description":"Cart secret for guest users who are not authenticated but own the cart","example":"a1b2c3d4-e5f6-7890-abcd-ef1234567890"}}},"example":{"code":"SAVE20","cartId":42}}}},"x-dashboard-trigger":"Use from the cart page by entering a discount code and clicking Apply."}},"/api/cart/remove-discount":{"post":{"summary":"Remove a discount code from a cart","description":"Removes any previously applied discount code from the specified cart. The caller must own the cart (via session) or provide the cart secret.","tags":["Cart"],"responses":{"200":{"description":"Discount removed successfully","content":{"application/json":{"schema":{"type":"object","required":["success"],"properties":{"success":{"type":"boolean","enum":[true],"example":true}}},"example":{"success":true}}}},"400":{"description":"Missing or invalid cart ID","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"},"example":{"success":false,"error":"Cart ID is required"}}}},"403":{"description":"Not authorised to modify this cart","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"success":false,"error":"Not authorized"}}}},"404":{"description":"Cart not found or already purchased","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"success":false,"error":"Cart not found"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"success":false,"error":"Failed to remove discount"}}}}},"security":[{"cookieAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["cartId"],"properties":{"cartId":{"type":"integer","minimum":1,"maximum":2147483647,"description":"The ID of the cart to remove the discount from","example":42},"secret":{"type":"string","minLength":1,"maxLength":255,"description":"Cart secret for guest users who are not authenticated but own the cart","example":"a1b2c3d4-e5f6-7890-abcd-ef1234567890"}}},"example":{"cartId":42}}}},"x-dashboard-trigger":"Use from the cart page by clicking the remove discount button next to the applied code."}},"/api/discount/validate":{"post":{"summary":"Validate a discount code","description":"Validates a discount code by checking whether it exists, is active, is within its valid date range, has not exceeded its usage limits, and meets minimum order requirements. Optionally calculates the discount amount when a subtotal is provided. Rate limited to 10 requests per IP per minute.","tags":["Discounts"],"responses":{"200":{"description":"Validation result. Both valid and invalid codes return 200; check the `valid` field.","content":{"application/json":{"schema":{"oneOf":[{"type":"object","required":["valid","discountId","code","discountType","discountValue","maxDiscountAmount","discountAmount"],"properties":{"valid":{"type":"boolean","enum":[true]},"discountId":{"type":"integer","minimum":1,"description":"Internal ID of the discount code record","example":3},"code":{"type":"string","description":"The normalised discount code","example":"WELCOME10"},"discountType":{"type":"string","enum":["percentage","flat"],"example":"percentage"},"discountValue":{"type":"number","minimum":0,"description":"Discount value (percentage or cents)","example":10},"maxDiscountAmount":{"type":["number","null"],"minimum":0,"description":"Maximum discount cap in cents","example":null},"discountAmount":{"type":"number","minimum":0,"description":"Calculated discount amount in cents","example":1500}}},{"type":"object","required":["valid","error"],"properties":{"valid":{"type":"boolean","enum":[false]},"error":{"type":"string","description":"Human-readable error message","example":"Invalid discount code"}}}]},"examples":{"valid":{"summary":"Valid discount code","value":{"valid":true,"discountId":3,"code":"WELCOME10","discountType":"percentage","discountValue":10,"maxDiscountAmount":null,"discountAmount":1500}},"invalid":{"summary":"Invalid discount code","value":{"valid":false,"error":"Invalid discount code"}},"expired":{"summary":"Expired discount code","value":{"valid":false,"error":"This discount code has expired"}}}}}},"400":{"description":"Missing or invalid request body","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"},"example":{"valid":false,"error":"Discount code is required"}}}},"429":{"description":"Rate limit exceeded (10 requests per minute per IP)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"valid":false,"error":"Too many attempts. Please try again later."}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"valid":false,"error":"Failed to validate discount code"}}}}},"security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["code"],"properties":{"code":{"type":"string","minLength":1,"maxLength":50,"description":"The discount code to validate","example":"WELCOME10"},"customerEmail":{"type":"string","format":"email","minLength":3,"maxLength":320,"description":"Customer email for per-customer usage limit checks","example":"jane@example.com"},"subtotal":{"type":"integer","minimum":0,"maximum":99999999,"description":"Cart subtotal in cents for minimum order and discount calculation","example":15000}}},"example":{"code":"WELCOME10","customerEmail":"jane@example.com","subtotal":15000}}}},"x-rate-limit":{"window":"60s","max":10}}},"/api/payment-amount":{"post":{"summary":"Calculate final payment amount with optional discount","description":"Retrieves the current amount of a Stripe PaymentIntent and optionally applies a discount code. The PaymentIntent must still be in the `requires_payment_method` status. For authenticated users, ownership is verified via the Stripe customer. For guests, the PaymentIntent ID acts as authorization. Rate limited to 20 requests per IP per minute.","tags":["Payments"],"responses":{"200":{"description":"Payment amount calculated successfully","content":{"application/json":{"schema":{"type":"object","required":["amount","currency","discountAmount"],"properties":{"amount":{"type":"integer","minimum":0,"description":"Final payment amount in cents after any discount","example":8000},"currency":{"type":"string","minLength":3,"maxLength":3,"description":"Three-letter ISO currency code","example":"usd"},"discountAmount":{"type":"integer","minimum":0,"description":"Discount amount in cents","example":2000}}},"example":{"amount":8000,"currency":"usd","discountAmount":2000}}}},"400":{"description":"Missing paymentIntentId, invalid payment state, or discount error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"},"examples":{"missingId":{"summary":"Missing payment intent ID","value":{"error":"paymentIntentId is required"}},"alreadyInitiated":{"summary":"Payment already in progress","value":{"error":"Cannot modify payment amount after payment has been initiated","amount":10000,"currency":"usd"}}}}}},"403":{"description":"PaymentIntent does not belong to the authenticated user","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Not authorized"}}}},"429":{"description":"Rate limit exceeded (20 requests per minute per IP)","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Too many requests. Please try again later."}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Failed to retrieve payment amount"}}}}},"security":[{"cookieAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["paymentIntentId"],"properties":{"paymentIntentId":{"type":"string","minLength":1,"maxLength":255,"description":"The Stripe PaymentIntent ID","example":"pi_3Oc0X2Abc123def456"},"discountCode":{"type":"string","minLength":1,"maxLength":50,"description":"Optional discount code to apply to the payment","example":"SAVE20"}}},"example":{"paymentIntentId":"pi_3Oc0X2Abc123def456","discountCode":"SAVE20"}}}},"x-rate-limit":{"window":"60s","max":20}}},"/api/orders":{"get":{"summary":"List the authenticated user's orders","description":"Returns a paginated list of orders belonging to the authenticated user. Orders are matched by the internal user ID or the user's email and are sorted by creation date (newest first).","tags":["Orders"],"responses":{"200":{"description":"Paginated order list","content":{"application/json":{"schema":{"type":"object","required":["orders","totalPages","page"],"properties":{"orders":{"type":"array","maxItems":50,"items":{"type":"object","description":"Order object with depth-2 relations (customer, products, etc.)"},"description":"Array of order objects"},"totalPages":{"type":"integer","minimum":0,"description":"Total number of pages","example":3},"page":{"type":"integer","minimum":1,"description":"Current page number","example":1},"totalDocs":{"type":"integer","minimum":0,"description":"Total number of orders","example":27}}},"example":{"orders":[],"totalPages":3,"page":1,"totalDocs":27}}}},"401":{"description":"Not authenticated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Unauthorized"}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Failed to load orders"}}}}},"security":[{"cookieAuth":[]}],"parameters":[{"name":"page","in":"query","required":false,"description":"Page number for pagination (defaults to 1)","schema":{"type":"integer","minimum":1,"maximum":10000,"default":1,"example":1}},{"name":"limit","in":"query","required":false,"description":"Number of orders per page (defaults to 10, max 50)","schema":{"type":"integer","minimum":1,"maximum":50,"default":10,"example":10}}]}},"/api/search":{"get":{"summary":"Full-text search across products and blogs","description":"Searches for products using the SaaSignal search index and for blog posts using Payload CMS. Returns results from both collections. If no query is provided, returns empty arrays.","tags":["Search"],"responses":{"200":{"description":"Search results containing matched products and blog posts","content":{"application/json":{"schema":{"type":"object","properties":{"hits":{"type":"array","maxItems":20,"description":"Product search results (alias for products)","items":{"type":"object","properties":{"id":{"type":"string","example":"42"},"score":{"type":"number","example":0.95},"document":{"type":"object","properties":{"name":{"type":"string","example":"Wireless Noise-Cancelling Headphones"},"slug":{"type":"string","example":"wireless-noise-cancelling-headphones"},"description":{"type":"string","example":"Premium over-ear headphones with ANC"},"priceInUSD":{"type":"number","example":299.99},"category":{"type":"string","example":"electronics"},"imageUrl":{"type":"string","example":"https://example.com/images/headphones.jpg"}}}}}},"products":{"type":"array","maxItems":20,"description":"Product search results","items":{"$ref":"#/components/schemas/ProductHit"}},"blogs":{"type":"array","maxItems":5,"description":"Blog post search results","items":{"type":"object","properties":{"id":{"type":"string","example":"7"},"title":{"type":"string","example":"Top 10 Wireless Headphones of 2026"},"slug":{"type":"string","example":"top-10-wireless-headphones-2026"},"excerpt":{"type":"string","example":"Discover the best wireless headphones..."},"category":{"type":"string","example":"reviews"},"coverImageUrl":{"type":"string","example":"https://example.com/images/blog-cover.jpg"}}}}}},"example":{"hits":[{"id":"42","score":0.95,"document":{"name":"Wireless Noise-Cancelling Headphones","slug":"wireless-noise-cancelling-headphones","priceInUSD":299.99,"category":"electronics"}}],"products":[{"id":"42","score":0.95,"document":{"name":"Wireless Noise-Cancelling Headphones","slug":"wireless-noise-cancelling-headphones","priceInUSD":299.99,"category":"electronics"}}],"blogs":[{"id":"7","title":"Top 10 Wireless Headphones of 2026","slug":"top-10-wireless-headphones-2026","excerpt":"Discover the best wireless headphones...","category":"reviews"}]}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Search failed"}}}}},"security":[],"parameters":[{"name":"q","in":"query","required":true,"description":"Full-text search query","schema":{"type":"string","minLength":1,"maxLength":200,"example":"wireless headphones"}},{"name":"limit","in":"query","required":false,"description":"Maximum number of results per category (default 10, max 20)","schema":{"type":"integer","minimum":1,"maximum":20,"default":10,"example":10}},{"name":"locale","in":"query","required":false,"description":"Locale for blog content localization","schema":{"type":"string","enum":["en","ar","es"],"default":"en","example":"en"}}]}},"/api/search/suggest":{"get":{"summary":"Autocomplete search suggestions","description":"Returns prefix-based autocomplete suggestions from the SaaSignal search index. Use this to power a search-as-you-type UI. Returns an empty array when no prefix is provided.","tags":["Search"],"responses":{"200":{"description":"Autocomplete suggestions","content":{"application/json":{"schema":{"type":"object","properties":{"suggestions":{"type":"array","maxItems":10,"description":"Autocomplete suggestion results","items":{"type":"object","properties":{"text":{"type":"string","description":"Suggested search completion text","example":"wireless headphones"},"score":{"type":"number","description":"Relevance score","example":0.92}}}}}},"example":{"suggestions":[{"text":"wireless headphones","score":0.92},{"text":"wireless charger","score":0.87},{"text":"wireless mouse","score":0.81}]}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Suggest failed"}}}}},"security":[],"parameters":[{"name":"q","in":"query","required":true,"description":"Prefix text for autocomplete suggestions","schema":{"type":"string","minLength":1,"maxLength":200,"example":"wire"}},{"name":"limit","in":"query","required":false,"description":"Maximum number of suggestions (default 5, max 10)","schema":{"type":"integer","minimum":1,"maximum":10,"default":5,"example":5}}]}},"/api/recommendations/related/{productId}":{"get":{"summary":"Get related products","description":"Returns products related to the specified product using the SaaSignal ranking engine. Falls back to same-category products sorted by creation date if no ranking data is available yet.","tags":["Recommendations"],"responses":{"200":{"description":"Related products list","content":{"application/json":{"schema":{"type":"object","properties":{"products":{"type":"array","maxItems":12,"description":"List of related products","items":{"type":"object","properties":{"id":{"type":"integer","description":"Unique product identifier","example":15},"name":{"type":"string","description":"Product display name","example":"Bluetooth Speaker"},"slug":{"type":"string","description":"URL-safe product slug","example":"bluetooth-speaker"},"priceInUSD":{"type":"number","description":"Price in US dollars","example":79.99},"category":{"type":"string","description":"Product category","example":"electronics"}}}}}},"example":{"products":[{"id":15,"name":"Bluetooth Speaker","slug":"bluetooth-speaker","priceInUSD":79.99,"category":"electronics"},{"id":23,"name":"USB-C Audio Adapter","slug":"usb-c-audio-adapter","priceInUSD":19.99,"category":"electronics"}]}}}},"500":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"error":"Failed to fetch related products"}}}}},"security":[],"parameters":[{"name":"productId","in":"path","required":true,"description":"The numeric ID of the product to find related products for","schema":{"type":"string","minLength":1,"maxLength":20,"example":"42"}},{"name":"limit","in":"query","required":false,"description":"Maximum number of related products (default 6, max 12)","schema":{"type":"integer","minimum":1,"maximum":12,"default":6,"example":6}},{"name":"locale","in":"query","required":false,"description":"Locale for product content localization","schema":{"type":"string","enum":["en","ar","es"],"default":"en","example":"en"}}]}},"/api/openapi.json":{"get":{"summary":"OpenAPI 3.1 specification (JSON)","description":"Returns this API's full OpenAPI 3.1 specification as JSON. Use this to generate client SDKs, import into API tools (Postman, Insomnia), or power interactive documentation UIs.","tags":["Documentation"],"security":[],"responses":{"200":{"description":"OpenAPI 3.1 JSON specification","content":{"application/json":{"schema":{"type":"object"}}}}}}},"/llms.txt":{"get":{"summary":"LLM-friendly API reference (plain text)","description":"Returns the full API reference as structured plain text optimized for LLM context windows. Use this to feed API documentation into AI assistants, chatbots, or code generators.","tags":["Documentation"],"security":[],"responses":{"200":{"description":"Plain-text API reference","content":{"text/plain":{"schema":{"type":"string"}}}}}}},"/to-humans.md":{"get":{"summary":"Human-readable API reference (Markdown)","description":"Returns the full API reference as a Markdown document with table of contents, request/response tables, cURL examples, and error reference. Suitable for rendering in documentation sites or reading directly.","tags":["Documentation"],"security":[],"responses":{"200":{"description":"Markdown API reference","content":{"text/markdown":{"schema":{"type":"string"}}}}}}}},"components":{"schemas":{"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"Human-readable error message.","example":"Internal server error"}}},"ValidationError":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"Summary of the validation failure.","example":"Validation failed"},"details":{"type":"object","description":"Structured validation error details from Zod.","properties":{"fieldErrors":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}},"description":"Per-field error messages.","example":{"email":["Invalid email address"]}},"formErrors":{"type":"array","items":{"type":"string"},"description":"Form-level error messages.","example":[]}}}}},"SuccessResponse":{"type":"object","required":["success"],"properties":{"success":{"type":"boolean","example":true}}},"ProductHit":{"type":"object","properties":{"id":{"type":"string","description":"Unique product identifier from the search index"},"score":{"type":"number","description":"Relevance score from the search engine"},"document":{"type":"object","properties":{"name":{"type":"string","description":"Product display name"},"slug":{"type":"string","description":"URL-safe product slug"},"description":{"type":"string","description":"Product description"},"priceInUSD":{"type":"number","description":"Product price in US dollars"},"category":{"type":"string","description":"Product category slug"},"imageUrl":{"type":"string","description":"URL of the product thumbnail image"}}}}},"PaginatedResponse":{"type":"object","properties":{"docs":{"type":"array","items":{},"description":"Array of documents for the current page."},"totalDocs":{"type":"integer","description":"Total number of matching documents.","example":42},"totalPages":{"type":"integer","description":"Total number of pages.","example":5},"page":{"type":"integer","description":"Current page number (1-indexed).","example":1},"limit":{"type":"integer","description":"Maximum documents per page.","example":10},"hasNextPage":{"type":"boolean","example":true},"hasPrevPage":{"type":"boolean","example":false}}}},"securitySchemes":{"cookieAuth":{"type":"apiKey","in":"cookie","name":"better-auth.session_token","description":"Session cookie set by Better-Auth after login. Authenticate via POST /api/auth/sign-in/email or OAuth flow."},"apiKeyAuth":{"type":"apiKey","in":"header","name":"x-api-key","description":"API key for programmatic access. Create keys at /account/developer or via POST /api/auth/api-key/create. Keys are scoped — only endpoints matching the key's permissions are accessible."}}},"security":[{"cookieAuth":[]},{"apiKeyAuth":[]}]}