# SaaSTARTER Storefront API — API Reference

API for building storefronts and ecommerce experiences.

## Quick Start

1. **Authenticate** — `POST /api/auth/sign-in/email` with `{ email, password }` to get a session cookie, or browse as a guest
2. **Browse products** — `GET /api/products` to list products, `GET /api/products/{id}` for details
3. **Search** — `GET /api/search?q=keyword` for full-text search
4. **Add to cart** — Use the cart endpoints to manage items
5. **Apply discount** — `POST /api/cart/apply-discount` with a discount code
6. **Checkout** — `POST /api/payment-amount` to calculate the final total, then complete payment via Stripe
7. **View orders** — `GET /api/orders` to see order history

## Authentication

Two authentication methods are supported:

**Session Cookie** — Sign in via `POST /api/auth/sign-in/email` with `{ email, password }`. The `better-auth.session_token` cookie is set automatically.

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

## Error Format

All errors return `{ error: string }`. Validation errors additionally include `{ details: { fieldErrors, formErrors } }` with per-field messages.

## Table of Contents

- [Contact](#contact) — Contact form submissions.
- [Newsletter](#newsletter) — Email newsletter subscriptions.
- [Reviews](#reviews) — Product reviews and ratings.
- [Wishlist](#wishlist) — Customer product wishlists.
- [Cart](#cart) — Shopping cart discount management.
- [Discounts](#discounts) — Discount code validation.
- [Payments](#payments) — Payment amount calculation.
- [Orders](#orders) — Customer order history.
- [Search](#search) — Product search and autocomplete.
- [Recommendations](#recommendations) — Product recommendations and tracking.

## Contact
> Contact form submissions.

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

> **Dashboard:** Payload Admin > Contact Form Submissions

#### Request Body

| Field | Type | Required | Constraints | Description |
|-------|------|----------|-------------|-------------|
| `name` | string | Yes | minLength: 1, maxLength: 150, pattern | Full name of the person submitting the form |
| `email` | string | Yes | minLength: 5, maxLength: 320, format: email | Contact email address |
| `subject` | string | Yes | minLength: 1, maxLength: 200 | Subject line for the contact message |
| `message` | string | Yes | minLength: 1, maxLength: 5000 | Body of the contact message |

#### Responses

| Status | Description |
|--------|-------------|
| 201 | Contact form submitted successfully |
| 400 | Validation error — missing or invalid fields |
| 500 | Internal server error |

**cURL Example:**
```bash
curl -X POST "https://www.saastarter.saastemly.com/api/contact"
```

---

## Newsletter
> Email newsletter subscriptions.

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

> **Dashboard:** Payload Admin > Newsletter Subscribers

#### Request Body

| Field | Type | Required | Constraints | Description |
|-------|------|----------|-------------|-------------|
| `email` | string | Yes | minLength: 5, maxLength: 320, format: email | Email address to subscribe to the newsletter |

#### Responses

| Status | Description |
|--------|-------------|
| 200 | Successfully subscribed (or already subscribed) |
| 400 | Validation error — invalid email address |
| 500 | Internal server error |

**cURL Example:**
```bash
curl -X POST "https://www.saastarter.saastemly.com/api/newsletter"
```

---

## Reviews
> Product reviews and ratings.

### 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

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `productId` | query | string | Yes | Numeric ID of the product to fetch reviews for |
| `page` | query | string | No | Page number for pagination (defaults to 1) |
| `limit` | query | string | No | Number of reviews per page (max 50, defaults to 10) |
| `sort` | query | string | No | Sort order for reviews |

#### Responses

| Status | Description |
|--------|-------------|
| 200 | Paginated reviews with aggregate statistics |
| 400 | Validation error — missing productId |
| 500 | Internal server error |

**cURL Example:**
```bash
curl "https://www.saastarter.saastemly.com/api/reviews"
```

---

### 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

| Field | Type | Required | Constraints | Description |
|-------|------|----------|-------------|-------------|
| `productId` | integer | Yes | min: 1, max: 2147483647 | Numeric ID of the product being reviewed |
| `rating` | integer | Yes | min: 1, max: 5 | Star rating from 1 to 5 |
| `title` | string | Yes | minLength: 1, maxLength: 200 | Short title for the review |
| `body` | string | Yes | minLength: 1, maxLength: 5000 | Full text of the review |

#### Responses

| Status | Description |
|--------|-------------|
| 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 |

**cURL Example:**
```bash
curl -X POST "https://www.saastarter.saastemly.com/api/reviews"
```

---

### GET `/api/reviews/{id}`

**Find a Review by ID** ` PUBLIC `

#### Responses

| Status | Description |
|--------|-------------|
| 200 | Review object |
| 404 | Review not found |

**cURL Example:**
```bash
curl "https://www.saastarter.saastemly.com/api/reviews/{id}"
```

---

### 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

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `reviewId` | path | string | Yes | Numeric ID of the review to mark as helpful |

#### Responses

| Status | Description |
|--------|-------------|
| 200 | Helpful count incremented successfully |
| 401 | Unauthorized — authentication required |
| 404 | Review not found |
| 500 | Internal server error |

**cURL Example:**
```bash
curl -X POST "https://www.saastarter.saastemly.com/api/reviews/{reviewId}/helpful"
```

---

## Wishlist
> Customer product wishlists.

### 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

| Status | Description |
|--------|-------------|
| 200 | Wishlist items retrieved successfully |
| 401 | Unauthorized — authentication required |
| 500 | Internal server error |

**cURL Example:**
```bash
curl "https://www.saastarter.saastemly.com/api/wishlist"
```

---

### 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

| Field | Type | Required | Constraints | Description |
|-------|------|----------|-------------|-------------|
| `productId` | integer | Yes | min: 1, max: 2147483647 | Numeric ID of the product to add to the wishlist |
| `variantId` | integer | No | min: 1, max: 2147483647 | Optional numeric ID of a specific product variant |

#### Responses

| Status | Description |
|--------|-------------|
| 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 |

**cURL Example:**
```bash
curl -X POST "https://www.saastarter.saastemly.com/api/wishlist"
```

---

### 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

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `itemId` | path | string | Yes | Numeric ID of the wishlist item to remove |

#### Responses

| Status | Description |
|--------|-------------|
| 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 |

**cURL Example:**
```bash
curl -X DELETE "https://www.saastarter.saastemly.com/api/wishlist/{itemId}"
```

---

### 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

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `productId` | path | string | Yes | Numeric ID of the product to check |

#### Responses

| Status | Description |
|--------|-------------|
| 200 | Wishlist check result |

**cURL Example:**
```bash
curl "https://www.saastarter.saastemly.com/api/wishlist/check/{productId}"
```

---

## Cart
> Shopping cart discount management.

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

> **Dashboard:** Use from the cart page by entering a discount code and clicking Apply.

#### Request Body

| Field | Type | Required | Constraints | Description |
|-------|------|----------|-------------|-------------|
| `code` | string | Yes | minLength: 1, maxLength: 50 | The discount code to apply to the cart |
| `cartId` | integer | Yes | min: 1, max: 2147483647 | The ID of the cart to apply the discount to |
| `secret` | string | No | minLength: 1, maxLength: 255 | Cart secret for guest users who are not authenticated but own the cart |

**Example:**
```json
{
  "code": "SAVE20",
  "cartId": 42
}
```

#### Responses

| Status | Description |
|--------|-------------|
| 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 |

**cURL Example:**
```bash
curl -X POST "https://www.saastarter.saastemly.com/api/cart/apply-discount" \
  -H "Content-Type: application/json" \
  -d '{"code":"SAVE20","cartId":42}'
```

---

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

> **Dashboard:** Use from the cart page by clicking the remove discount button next to the applied code.

#### Request Body

| Field | Type | Required | Constraints | Description |
|-------|------|----------|-------------|-------------|
| `cartId` | integer | Yes | min: 1, max: 2147483647 | The ID of the cart to remove the discount from |
| `secret` | string | No | minLength: 1, maxLength: 255 | Cart secret for guest users who are not authenticated but own the cart |

**Example:**
```json
{
  "cartId": 42
}
```

#### Responses

| Status | Description |
|--------|-------------|
| 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 |

**cURL Example:**
```bash
curl -X POST "https://www.saastarter.saastemly.com/api/cart/remove-discount" \
  -H "Content-Type: application/json" \
  -d '{"cartId":42}'
```

---

## Discounts
> Discount code validation.

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

> **Rate Limit:** 10 requests per 60s

#### Request Body

| Field | Type | Required | Constraints | Description |
|-------|------|----------|-------------|-------------|
| `code` | string | Yes | minLength: 1, maxLength: 50 | The discount code to validate |
| `customerEmail` | string | No | minLength: 3, maxLength: 320, format: email | Customer email for per-customer usage limit checks |
| `subtotal` | integer | No | min: 0, max: 99999999 | Cart subtotal in cents for minimum order and discount calculation |

**Example:**
```json
{
  "code": "WELCOME10",
  "customerEmail": "jane@example.com",
  "subtotal": 15000
}
```

#### Responses

| Status | Description |
|--------|-------------|
| 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 |

**cURL Example:**
```bash
curl -X POST "https://www.saastarter.saastemly.com/api/discount/validate" \
  -H "Content-Type: application/json" \
  -d '{"code":"WELCOME10","customerEmail":"jane@example.com","subtotal":15000}'
```

---

## Payments
> Payment amount calculation.

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

> **Rate Limit:** 20 requests per 60s

#### Request Body

| Field | Type | Required | Constraints | Description |
|-------|------|----------|-------------|-------------|
| `paymentIntentId` | string | Yes | minLength: 1, maxLength: 255 | The Stripe PaymentIntent ID |
| `discountCode` | string | No | minLength: 1, maxLength: 50 | Optional discount code to apply to the payment |

**Example:**
```json
{
  "paymentIntentId": "pi_3Oc0X2Abc123def456",
  "discountCode": "SAVE20"
}
```

#### Responses

| Status | Description |
|--------|-------------|
| 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 |

**cURL Example:**
```bash
curl -X POST "https://www.saastarter.saastemly.com/api/payment-amount" \
  -H "Content-Type: application/json" \
  -d '{"paymentIntentId":"pi_3Oc0X2Abc123def456","discountCode":"SAVE20"}'
```

---

## Orders
> Customer order history.

### 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

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `page` | query | integer | No | Page number for pagination (defaults to 1) |
| `limit` | query | integer | No | Number of orders per page (defaults to 10, max 50) |

#### Responses

| Status | Description |
|--------|-------------|
| 200 | Paginated order list |
| 401 | Not authenticated |
| 500 | Internal server error |

**cURL Example:**
```bash
curl "https://www.saastarter.saastemly.com/api/orders"
```

---

## Search
> Product search and autocomplete.

### 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

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `q` | query | string | Yes | Full-text search query |
| `limit` | query | integer | No | Maximum number of results per category (default 10, max 20) |
| `locale` | query | string | No | Locale for blog content localization |

#### Responses

| Status | Description |
|--------|-------------|
| 200 | Search results containing matched products and blog posts |
| 500 | Internal server error |

**cURL Example:**
```bash
curl "https://www.saastarter.saastemly.com/api/search"
```

---

### 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

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `q` | query | string | Yes | Prefix text for autocomplete suggestions |
| `limit` | query | integer | No | Maximum number of suggestions (default 5, max 10) |

#### Responses

| Status | Description |
|--------|-------------|
| 200 | Autocomplete suggestions |
| 500 | Internal server error |

**cURL Example:**
```bash
curl "https://www.saastarter.saastemly.com/api/search/suggest"
```

---

## Recommendations
> Product recommendations and tracking.

### 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

| Name | In | Type | Required | Description |
|------|-----|------|----------|-------------|
| `productId` | path | string | Yes | The numeric ID of the product to find related products for |
| `limit` | query | integer | No | Maximum number of related products (default 6, max 12) |
| `locale` | query | string | No | Locale for product content localization |

#### Responses

| Status | Description |
|--------|-------------|
| 200 | Related products list |
| 500 | Internal server error |

**cURL Example:**
```bash
curl "https://www.saastarter.saastemly.com/api/recommendations/related/{productId}"
```

---

## Error Reference

All errors return a JSON object with an `error` field:
```json
{ "error": "Human-readable error message" }
```

Validation errors (400) additionally include structured details:
```json
{
  "error": "Validation failed",
  "details": {
    "fieldErrors": { "email": ["Invalid email address"] },
    "formErrors": []
  }
}
```
