# SaaSTARTER Storefront API > API for building storefronts and ecommerce experiences. ## Authentication Session-based via cookie `better-auth.session_token`. Sign in: POST /api/auth/sign-in/email { email, password } Endpoints marked [public] do not require authentication. ## Blog Posts ### GET /api/blogs Retrieve a list of Blog Posts [public] Parameters: page (number, query, optional): limit (number, query, optional): depth (number, query, optional): locale (string, query, optional): fallback-locale (string, query, optional): sort (string, query, optional): where (string, query, optional): Responses: 200: List of Blog Posts ### GET /api/blogs/{id} Find a Blog Post by ID [public] Responses: 200: Blog Post object 404: Blog Post not found ## FAQs ### GET /api/faqs Retrieve a list of FAQs [public] Parameters: page (number, query, optional): limit (number, query, optional): depth (number, query, optional): locale (string, query, optional): fallback-locale (string, query, optional): sort (string, query, optional): where (string, query, optional): Responses: 200: List of FAQs ### GET /api/faqs/{id} Find a FAQ by ID [public] Responses: 200: FAQ object 404: FAQ not found ## Media ### GET /api/media Retrieve a list of Media [public] Parameters: page (number, query, optional): limit (number, query, optional): depth (number, query, optional): locale (string, query, optional): fallback-locale (string, query, optional): sort (string, query, optional): where (string, query, optional): Responses: 200: List of Media ### GET /api/media/{id} Find a Media by ID [public] Responses: 200: Media object 404: Media not found ## Reviews ### GET /api/reviews List product reviews [public] Returns a paginated list of approved reviews for a given product, along with aggregate statistics (average rating, star-rating breakdown). Parameters: productId (string, query, required): Numeric ID of the product to fetch reviews for page (string, query, optional): Page number for pagination (defaults to 1) limit (string, query, optional): Number of reviews per page (max 50, defaults to 10) sort (string, query, optional): Sort order for reviews Responses: 200: Paginated reviews with aggregate statistics 400: Validation error — missing productId 500: Internal server error ### POST /api/reviews Create a product review [auth required] 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. Request body (JSON): productId: integer [min: 1, max: 2147483647] (required) — Numeric ID of the product being reviewed (e.g. 42) rating: integer [min: 1, max: 5] (required) — Star rating from 1 to 5 (e.g. 4) title: string [min: 1, max: 200] (required) — Short title for the review (e.g. "Great quality product") body: string [min: 1, max: 5000] (required) — Full text of the review (e.g. "I have been using this for a month and the build quality is excellent. Highly recommended.") Responses: 201: Review created successfully (status: pending) 400: Validation error — missing or invalid fields 401: Unauthorized — authentication required 404: Authenticated user not found in Payload users collection 409: Conflict — user has already reviewed this product 500: Internal server error ### GET /api/reviews/{id} Find a Review by ID [public] Responses: 200: Review object 404: Review not found ### POST /api/reviews/{reviewId}/helpful Mark a review as helpful [auth required] Increments the helpful vote count on a review. Requires authentication. The review must exist; otherwise a 404 is returned. Parameters: reviewId (string, path, required): Numeric ID of the review to mark as helpful Responses: 200: Helpful count incremented successfully 401: Unauthorized — authentication required 404: Review not found 500: Internal server error ## addresses ### GET /api/addresses Retrieve a list of addresses [auth required] Parameters: page (number, query, optional): limit (number, query, optional): depth (number, query, optional): locale (string, query, optional): fallback-locale (string, query, optional): sort (string, query, optional): where (string, query, optional): Responses: 200: List of addresses ### POST /api/addresses Create a new addresses [auth required] Parameters: depth (number, query, optional): locale (string, query, optional): Request body (JSON): customer: string (optional) — ID of the users title: string | null (optional) firstName: string | null (optional) lastName: string | null (optional) company: string | null (optional) addressLine1: string | null (optional) addressLine2: string | null (optional) city: string | null (optional) state: string | null (optional) postalCode: string | null (optional) country: string [enum: US, GB, CA, AU, AT, BE, BR, BG, CY, CZ, DK, EE, FI, FR, DE, GR, HK, HU, IN, IE, IT, JP, LV, LT, LU, MY, MT, MX, NL, NZ, NO, PL, PT, RO, SG, SK, SI, ES, SE, CH] (required) phone: string | null (optional) Responses: 201: addresses object ### GET /api/addresses/{id} Find a addresses by ID [auth required] Responses: 200: addresses object 404: addresses not found ### PATCH /api/addresses/{id} Update a addresses [auth required] Request body (JSON): customer: string (optional) — ID of the users title: string | null (optional) firstName: string | null (optional) lastName: string | null (optional) company: string | null (optional) addressLine1: string | null (optional) addressLine2: string | null (optional) city: string | null (optional) state: string | null (optional) postalCode: string | null (optional) country: string [enum: US, GB, CA, AU, AT, BE, BR, BG, CY, CZ, DK, EE, FI, FR, DE, GR, HK, HU, IN, IE, IT, JP, LV, LT, LU, MY, MT, MX, NL, NZ, NO, PL, PT, RO, SG, SK, SI, ES, SE, CH] (optional) phone: string | null (optional) Responses: 200: addresses object 404: addresses not found ### DELETE /api/addresses/{id} Delete a addresses [auth required] Responses: 200: addresses object 404: addresses not found ## variants ### GET /api/variants Retrieve a list of variants [public] Parameters: page (number, query, optional): limit (number, query, optional): depth (number, query, optional): locale (string, query, optional): fallback-locale (string, query, optional): sort (string, query, optional): where (string, query, optional): Responses: 200: List of variants ### GET /api/variants/{id} Find a variants by ID [public] Responses: 200: variants object 404: variants not found ## variantTypes ### GET /api/variantTypes Retrieve a list of variantTypes [public] Parameters: page (number, query, optional): limit (number, query, optional): depth (number, query, optional): locale (string, query, optional): fallback-locale (string, query, optional): sort (string, query, optional): where (string, query, optional): Responses: 200: List of variantTypes ### GET /api/variantTypes/{id} Find a variantTypes by ID [public] Responses: 200: variantTypes object 404: variantTypes not found ## variantOptions ### GET /api/variantOptions Retrieve a list of variantOptions [public] Parameters: page (number, query, optional): limit (number, query, optional): depth (number, query, optional): locale (string, query, optional): fallback-locale (string, query, optional): sort (string, query, optional): where (string, query, optional): Responses: 200: List of variantOptions ### GET /api/variantOptions/{id} Find a variantOptions by ID [public] Responses: 200: variantOptions object 404: variantOptions not found ## products ### GET /api/products Retrieve a list of products [public] Parameters: page (number, query, optional): limit (number, query, optional): depth (number, query, optional): locale (string, query, optional): fallback-locale (string, query, optional): sort (string, query, optional): where (string, query, optional): Responses: 200: List of products ### GET /api/products/{id} Find a products by ID [public] Responses: 200: products object 404: products not found ## carts ### GET /api/carts Retrieve a list of carts [auth required] Parameters: page (number, query, optional): limit (number, query, optional): depth (number, query, optional): locale (string, query, optional): fallback-locale (string, query, optional): sort (string, query, optional): where (string, query, optional): Responses: 200: List of carts ### POST /api/carts Create a new carts [auth required] Parameters: depth (number, query, optional): locale (string, query, optional): Request body (JSON): items: array | null (optional) secret: string | null (optional) customer: string (optional) — ID of the users purchasedAt: string | null (optional) status: string | null [enum: active, purchased, abandoned] (optional) subtotal: number | null (optional) currency: string | null [enum: USD] (optional) discountCode: string | null (optional) — Applied discount code for this cart Responses: 201: carts object ### GET /api/carts/{id} Find a carts by ID [auth required] Responses: 200: carts object 404: carts not found ### PATCH /api/carts/{id} Update a carts [auth required] Request body (JSON): items: array | null (optional) secret: string | null (optional) customer: string (optional) — ID of the users purchasedAt: string | null (optional) status: string | null [enum: active, purchased, abandoned] (optional) subtotal: number | null (optional) currency: string | null [enum: USD] (optional) discountCode: string | null (optional) — Applied discount code for this cart Responses: 200: carts object 404: carts not found ### DELETE /api/carts/{id} Delete a carts [auth required] Responses: 200: carts object 404: carts not found ## Orders ### GET /api/orders List the authenticated user's orders [auth required] 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). Parameters: page (integer, query, optional): Page number for pagination (defaults to 1) limit (integer, query, optional): Number of orders per page (defaults to 10, max 50) Responses: 200: Paginated order list 401: Not authenticated 500: Internal server error ## orders ### GET /api/orders/{id} Find a orders by ID [auth required] Responses: 200: orders object 404: orders not found ## Contact ### POST /api/contact Submit contact form [public] Accepts a contact form submission and stores it in the Payload CMS `contact-form-submissions` collection. No authentication is required. Request body (JSON): name: string [min: 1, max: 150, pattern: ^[\p{L}\p{N}\s\-'.]+$] (required) — Full name of the person submitting the form (e.g. "Jane Doe") email: string [min: 5, max: 320, format: email] (required) — Contact email address (e.g. "jane@example.com") subject: string [min: 1, max: 200] (required) — Subject line for the contact message (e.g. "Partnership inquiry") message: string [min: 1, max: 5000] (required) — Body of the contact message (e.g. "Hi, I would love to discuss a potential partnership. Please let me know a good time to connect.") Responses: 201: Contact form submitted successfully 400: Validation error — missing or invalid fields 500: Internal server error Dashboard: Payload Admin > Contact Form Submissions ## Newsletter ### POST /api/newsletter Subscribe to newsletter [public] 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. Request body (JSON): email: string [min: 5, max: 320, format: email] (required) — Email address to subscribe to the newsletter (e.g. "subscriber@example.com") Responses: 200: Successfully subscribed (or already subscribed) 400: Validation error — invalid email address 500: Internal server error Dashboard: Payload Admin > Newsletter Subscribers ## Wishlist ### GET /api/wishlist Get user wishlist [auth required] 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. Responses: 200: Wishlist items retrieved successfully 401: Unauthorized — authentication required 500: Internal server error ### POST /api/wishlist Add item to wishlist [auth required] Adds a product (and optionally a specific variant) to the authenticated user's wishlist. Returns 409 if the product is already in the wishlist. Request body (JSON): productId: integer [min: 1, max: 2147483647] (required) — Numeric ID of the product to add to the wishlist (e.g. 42) variantId: integer [min: 1, max: 2147483647] (optional) — Optional numeric ID of a specific product variant (e.g. 7) Responses: 201: Item added to wishlist 400: Validation error — missing productId 401: Unauthorized — authentication required 404: Authenticated user not found in Payload users collection 409: Conflict — product is already in the wishlist 500: Internal server error ### DELETE /api/wishlist/{itemId} Remove item from wishlist [auth required] 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. Parameters: itemId (string, path, required): Numeric ID of the wishlist item to remove Responses: 200: Wishlist item removed successfully 401: Unauthorized — authentication required 403: Forbidden — wishlist item belongs to another user 404: Authenticated user not found in Payload users collection 500: Internal server error ### GET /api/wishlist/check/{productId} Check if product is in wishlist [auth required] 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. Parameters: productId (string, path, required): Numeric ID of the product to check Responses: 200: Wishlist check result ## Cart ### POST /api/cart/apply-discount Apply a discount code to a cart [auth required] 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. Request body (JSON): code: string [min: 1, max: 50] (required) — The discount code to apply to the cart (e.g. "SAVE20") cartId: integer [min: 1, max: 2147483647] (required) — The ID of the cart to apply the discount to (e.g. 42) secret: string [min: 1, max: 255] (optional) — Cart secret for guest users who are not authenticated but own the cart (e.g. "a1b2c3d4-e5f6-7890-abcd-ef1234567890") Responses: 200: Discount applied successfully 400: Validation error or invalid discount code 403: Not authorised to modify this cart 404: Cart not found or already purchased 500: Internal server error Dashboard: Use from the cart page by entering a discount code and clicking Apply. ### POST /api/cart/remove-discount Remove a discount code from a cart [auth required] Removes any previously applied discount code from the specified cart. The caller must own the cart (via session) or provide the cart secret. Request body (JSON): cartId: integer [min: 1, max: 2147483647] (required) — The ID of the cart to remove the discount from (e.g. 42) secret: string [min: 1, max: 255] (optional) — Cart secret for guest users who are not authenticated but own the cart (e.g. "a1b2c3d4-e5f6-7890-abcd-ef1234567890") Responses: 200: Discount removed successfully 400: Missing or invalid cart ID 403: Not authorised to modify this cart 404: Cart not found or already purchased 500: Internal server error Dashboard: Use from the cart page by clicking the remove discount button next to the applied code. ## Discounts ### POST /api/discount/validate Validate a discount code [public] 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. Request body (JSON): code: string [min: 1, max: 50] (required) — The discount code to validate (e.g. "WELCOME10") customerEmail: string [min: 3, max: 320, format: email] (optional) — Customer email for per-customer usage limit checks (e.g. "jane@example.com") subtotal: integer [min: 0, max: 99999999] (optional) — Cart subtotal in cents for minimum order and discount calculation (e.g. 15000) Responses: 200: Validation result. Both valid and invalid codes return 200; check the `valid` field. 400: Missing or invalid request body 429: Rate limit exceeded (10 requests per minute per IP) 500: Internal server error Rate limit: 10 requests per 60s ## Payments ### POST /api/payment-amount Calculate final payment amount with optional discount [auth required] 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. Request body (JSON): paymentIntentId: string [min: 1, max: 255] (required) — The Stripe PaymentIntent ID (e.g. "pi_3Oc0X2Abc123def456") discountCode: string [min: 1, max: 50] (optional) — Optional discount code to apply to the payment (e.g. "SAVE20") Responses: 200: Payment amount calculated successfully 400: Missing paymentIntentId, invalid payment state, or discount error 403: PaymentIntent does not belong to the authenticated user 429: Rate limit exceeded (20 requests per minute per IP) 500: Internal server error Rate limit: 20 requests per 60s ## Search ### GET /api/search Full-text search across products and blogs [public] 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. Parameters: q (string, query, required): Full-text search query limit (integer, query, optional): Maximum number of results per category (default 10, max 20) locale (string, query, optional): Locale for blog content localization Responses: 200: Search results containing matched products and blog posts 500: Internal server error ### GET /api/search/suggest Autocomplete search suggestions [public] 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. Parameters: q (string, query, required): Prefix text for autocomplete suggestions limit (integer, query, optional): Maximum number of suggestions (default 5, max 10) Responses: 200: Autocomplete suggestions 500: Internal server error ## Recommendations ### GET /api/recommendations/related/{productId} Get related products [public] 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. Parameters: productId (string, path, required): The numeric ID of the product to find related products for limit (integer, query, optional): Maximum number of related products (default 6, max 12) locale (string, query, optional): Locale for product content localization Responses: 200: Related products list 500: Internal server error ## Documentation ### GET /api/openapi.json OpenAPI 3.1 specification (JSON) [public] 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. Responses: 200: OpenAPI 3.1 JSON specification ### GET /llms.txt LLM-friendly API reference (plain text) [public] 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. Responses: 200: Plain-text API reference ### GET /to-humans.md Human-readable API reference (Markdown) [public] 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. Responses: 200: Markdown API reference