> ## Documentation Index
> Fetch the complete documentation index at: https://opencals.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Verify customer email

> Confirm customer email ownership using the verification token and return auth tokens



## OpenAPI

````yaml /api-reference/openapi.json post /storefront/auth/verify-email
openapi: 3.0.0
info:
  title: Opencals API
  description: >
    # Opencals Storefront API


    Build custom booking storefronts with the Opencals Storefront API. Browse
    services, check real-time availability, manage carts, process payments, and
    complete bookings — all programmatically.


    ---


    ## Getting Started


    ### 1. Create an Account


    Sign up at [app.opencals.com](https://app.opencals.com) and set up your
    store with services, staff members, locations, and schedules.


    ### 2. Generate a Storefront API Key


    Navigate to **Settings > API Keys** in your dashboard. Create a new
    **Storefront API Key**. Each key is scoped to your store and starts with
    `sfk_`.


    ### 3. Make Your First Request


    ```bash

    curl -H "X-Api-Key: sfk_your_key_here" \
      https://api.opencals.com/customer/products
    ```


    This returns a paginated list of your store's active services.


    ---


    ## Authentication


    ### Storefront API Key


    All requests require a **Storefront API Key** in the `X-Api-Key` header:


    ```

    X-Api-Key: sfk_your_key_here

    ```


    This key identifies your store. It should be kept server-side (e.g., in
    environment variables) and never exposed to the browser.


    ### Customer Authentication


    Some endpoints require a logged-in customer (viewing appointments, managing
    profile). Customers authenticate via credentials and receive JWT tokens:


    ```http

    POST /auth/customer/sign-in

    X-Api-Key: sfk_your_key_here

    Content-Type: application/json


    {
      "email": "customer@example.com",
      "password": "securepassword"
    }

    ```


    **Response:**

    ```json

    {
      "accessToken": "eyJhbGciOi...",
      "refreshToken": "eyJhbGciOi..."
    }

    ```


    Include the customer token alongside the API key for authenticated requests:

    ```

    Authorization: Bearer <customer_access_token>

    X-Api-Key: sfk_your_key_here

    ```


    ### Customer Auth Endpoints


    | Method | Endpoint | Description |

    |--------|----------|-------------|

    | `POST` | `/auth/customer/sign-up` | Register a new customer (sends
    verification email) |

    | `POST` | `/auth/customer/sign-in` | Sign in with email + password |

    | `POST` | `/auth/customer/oauth` | Sign in/register via OAuth provider
    (e.g., Google) |

    | `POST` | `/auth/customer/verify-email` | Verify email with token from
    verification email |

    | `POST` | `/auth/customer/request-email-verification` | Resend verification
    email |

    | `POST` | `/auth/customer/request-password-reset` | Send password reset
    email |

    | `POST` | `/auth/customer/reset-password` | Reset password with token |

    | `GET`  | `/auth/customer/refresh` | Refresh tokens (pass refresh token as
    Bearer) |


    ---


    ## Booking Flows


    The booking flow depends on your product configuration. Below are the common
    patterns.


    ### Standard Booking (Single Time Slot)


    The most common flow — customer picks a service, selects a date/time, and
    books.


    ```

    1. GET  /customer/products                              → Browse services

    2. GET  /customer/products/:id/current-availabilities    → Get available
    time slots for a date
          ?date=2026-06-15&timezone=America/New_York
    3. POST /customer/appointments                           → Create
    appointment with chosen slot
          { slot: { productId, fromDate, fromTime, toDate, toTime }, numberOfAttendees: 1 }
    4. POST /customer/cart                                   → Create or get
    cart

    5. POST /customer/cart/items                             → Add appointment
    to cart
          { appointmentId: "..." }
    6. POST /customer/cart/checkout/start                    → Start checkout
    with payment provider
          { provider: "stripe" }
    7. POST /customer/cart/checkout/save-customer            → Save customer
    info
          { customer: { email, firstName, lastName } }
    8. POST /customer/cart/checkout/submit                   → Complete booking
    & capture payment

    ```


    ### Custom Duration / Date Range Booking


    When a product has **custom duration** enabled (e.g., car rentals, venue
    bookings), customers select a date range instead of a single time slot.


    **How to detect:** The product's `isCustomDuration` field is `true`.


    ```

    1. GET /customer/products/:id/current-availability-ranges   → Get available
    date ranges
          ?timezone=America/New_York
    2. UI shows a date range picker instead of individual slots

    3. POST /customer/appointments
          { slot: { productId, fromDate: "2026-06-15", fromTime: "09:00:00",
                    toDate: "2026-06-18", toTime: "17:00:00" } }
    4. Continue with cart → checkout flow as above

    ```


    ### Group Appointments


    When a product supports multiple attendees (`maxAttendees > 1`), display an
    attendees selector.


    ```

    POST /customer/appointments

    {
      "slot": { ... },
      "numberOfAttendees": 4
    }

    ```


    ### Location-First Flow


    When a store has multiple locations, let the customer pick a location first,
    then filter services and availability by location.


    ```

    1. GET /customer/locations                                → List all
    locations

    2. User selects a location

    3. GET /customer/products?locationId=<locationId>         → Products at that
    location

    4. GET /customer/products/:id/current-availabilities
          ?date=2026-06-15&locationId=<locationId>            → Slots at that location
    5. POST /customer/appointments
          { slot: { ..., locationId: "<locationId>" } }
    ```


    ### Staff-First Flow


    When staff member selection is important (e.g., hair salons), let customers
    choose their preferred staff member.


    ```

    1. GET /customer/staff-members                            → List staff
    members

    2. User selects a staff member

    3. GET /customer/products/:id/current-availabilities
          ?date=2026-06-15&staffMemberId=<staffMemberId>      → Slots with that staff member
    4. POST /customer/appointments
          { slot: { ..., staffMemberId: "<staffMemberId>" } }
    ```


    ### Combined Flow (Location + Staff + Service)


    All filters can be combined:


    ```

    GET /customer/products/:id/current-availabilities
        ?date=2026-06-15
        &locationId=<locationId>
        &staffMemberId=<staffMemberId>
        &timezone=America/New_York
    ```


    ---


    ## Product Configuration Flags


    Products have configuration fields that determine which UI elements to
    display. Check these fields when rendering your booking interface:


    | Field | Type | UI Behavior |

    |-------|------|-------------|

    | `isCustomDuration` | `boolean` | `true` → show date range picker; `false`
    → show single time slot picker |

    | `maxAttendees` | `number` | `> 1` → show attendees selector; `1` → hide it
    |

    | `skipCheckout` | `boolean` | `true` → skip payment step, book directly;
    `false` → normal checkout |

    | `fixedStartTimes` / `fixedEndTimes` | `string[]` | If present, only these
    specific times are bookable |

    | `locations` | `Location[]` | If populated, show location selector |

    | `staffMembers` | `StaffMember[]` | If populated, show staff member
    selector |

    | `price` | `number` | Service price; `0` = free |

    | `minPrice` / `maxPrice` | `number` | Price range for variable-priced
    services |

    | `duration` | `number` | Service duration in seconds |

    | `minDuration` / `maxDuration` | `number` | Duration range for custom
    duration services |

    | `images` | `Image[]` | Product images for gallery display |

    | `description` | `string` | Product description (may contain markdown) |


    ---


    ## Endpoint Reference


    ### Products


    | Method | Endpoint | Auth | Description |

    |--------|----------|------|-------------|

    | `GET` | `/customer/products` | API key | List all active products
    (paginated) |

    | `GET` | `/customer/products/:id` | API key | Get product by ID |

    | `GET` | `/customer/products/slug/:slug` | API key | Get product by URL
    slug |

    | `GET` | `/customer/products/:id/current-availabilities` | API key | Get
    bookable time slots for a date |

    | `GET` | `/customer/products/:id/current-availability-ranges` | API key |
    Get available date ranges (for custom duration) |

    | `GET` | `/customer/products/:id/nearest-availability` | API key | Find
    next available slot |


    **Availability query parameters:**


    | Parameter | Required | Description |

    |-----------|----------|-------------|

    | `date` | Yes (for current-availabilities) | Date in `YYYY-MM-DD` format |

    | `locationId` | No | Filter by location |

    | `staffMemberId` | No | Filter by staff member |

    | `timezone` | No | Timezone for date/time calculations (e.g.,
    `America/New_York`) |


    ### Product Collections


    | Method | Endpoint | Auth | Description |

    |--------|----------|------|-------------|

    | `GET` | `/customer/product-collections` | API key | List all collections
    (paginated) |

    | `GET` | `/customer/product-collections/slug/:slug` | API key | Get
    collection by slug |


    ### Locations


    | Method | Endpoint | Auth | Description |

    |--------|----------|------|-------------|

    | `GET` | `/customer/locations` | API key | List all locations (paginated) |

    | `GET` | `/customer/locations/:id` | API key | Get location by ID |

    | `GET` | `/customer/locations/slug/:slug` | API key | Get location by slug
    |


    ### Staff Members


    | Method | Endpoint | Auth | Description |

    |--------|----------|------|-------------|

    | `GET` | `/customer/staff-members` | API key | List all staff members
    (paginated) |

    | `GET` | `/customer/staff-members/slug/:slug` | API key | Get staff member
    by slug |


    ### Cart


    All cart endpoints use the `X-Cart-Id` header to identify the cart.


    | Method | Endpoint | Auth | Description |

    |--------|----------|------|-------------|

    | `POST` | `/customer/cart` | API key | Create or retrieve a cart |

    | `GET` | `/customer/cart` | API key | Get current cart |

    | `POST` | `/customer/cart/items` | API key | Add an appointment to the cart
    |

    | `DELETE` | `/customer/cart/items/:itemId` | API key | Remove item from
    cart |

    | `POST` | `/customer/cart/extend` | API key | Extend cart expiration (5
    min) |


    **Adding items to cart:**

    ```json

    POST /customer/cart/items

    X-Cart-Id: <cart_id>


    { "appointmentId": "appointment-uuid-here" }

    ```


    ### Add-Ons


    Add-ons are optional extras customers can attach to a cart item at checkout
    (equipment rentals, upgrades, accessories, etc.). Add-ons are scoped to
    products — only add-ons assigned to the booked service appear.


    **Storefront (listing):**


    | Method | Endpoint | Auth | Description |

    |--------|----------|------|-------------|

    | `GET` | `/storefront/add-ons` | API key | List add-ons (filter by
    `productId`) |

    | `GET` | `/storefront/add-ons/:id` | API key | Get add-on by ID |

    | `GET` | `/storefront/add-ons/slug/:slug` | API key | Get add-on by slug |

    | `GET` | `/storefront/products/:id/add-ons` | API key | List add-ons for a
    specific product |


    **Cart (selection):**


    All cart add-on endpoints require the `X-Cart-Id` header.


    | Method | Endpoint | Auth | Description |

    |--------|----------|------|-------------|

    | `POST` | `/storefront/cart/add-ons` | API key | Add an add-on to a cart
    item |

    | `PATCH` | `/storefront/cart/add-ons/:id` | API key | Update add-on
    quantity |

    | `DELETE` | `/storefront/cart/add-ons/:id` | API key | Remove add-on from
    cart |


    **Adding an add-on to a cart item:**

    ```json

    POST /storefront/cart/add-ons

    X-Cart-Id: <cart_id>


    {
      "cartItemId": "uuid-of-cart-item",
      "addOnId": "uuid-of-add-on",
      "quantity": 1
    }

    ```


    The system validates that the add-on is assigned to the cart item's product
    and that `quantity` does not exceed `maxQuantity` (if set). For
    duration-multiplied add-ons, quantity is auto-set to match the service
    duration units — send any value and it will be overridden.


    **Add-on object (storefront response):**

    ```json

    {
      "id": "addon-uuid",
      "slug": "deep-conditioning",
      "title": "Deep Conditioning Treatment",
      "description": "Intensive moisture treatment added to any color or cut service.",
      "price": 20.00,
      "taxable": true,
      "durationMultiplied": false,
      "maxQuantity": 1,
      "status": "active",
      "imageId": "img-uuid-or-null"
    }

    ```


    **Pricing notes:**

    - `price` — unit price. For duration-multiplied add-ons, multiply by
    duration units to get the total.

    - `durationMultiplied: true` — quantity is auto-set to the number of
    base-duration units booked (useful for per-day or per-hour charges).

    - Prices are snapshotted at cart/order creation. Later catalog price changes
    do not affect existing orders.


    **In the order response**, add-ons appear as `addOnLineItems` on each line
    item:

    ```json

    {
      "lineItems": [
        {
          "id": "...",
          "appointmentId": "...",
          "addOnLineItems": [
            {
              "id": "...",
              "addOnId": "...",
              "title": "Deep Conditioning Treatment",
              "quantity": 1,
              "originalUnitPrice": 20.00,
              "unitTaxAmount": 3.33,
              "totalPrice": 20.00
            }
          ]
        }
      ]
    }

    ```


    ### Checkout


    All checkout endpoints require the `X-Cart-Id` header.


    | Method | Endpoint | Auth | Description |

    |--------|----------|------|-------------|

    | `POST` | `/customer/cart/checkout/start` | API key | Start checkout
    (initialize payment) |

    | `POST` | `/customer/cart/checkout/save-customer` | API key | Save customer
    info to cart |

    | `GET` | `/customer/cart/checkout/questions/:language` | API key | Get
    checkout questions |

    | `POST` | `/customer/cart/checkout/save-answers` | API key | Save checkout
    question answers |

    | `POST` | `/customer/cart/checkout/submit` | API key | Submit checkout
    (capture payment, create order) |


    **Start checkout:**

    ```json

    POST /customer/cart/checkout/start

    X-Cart-Id: <cart_id>


    {
      "provider": "stripe",
      "customer": {
        "email": "john@example.com",
        "firstName": "John",
        "lastName": "Smith"
      }
    }

    ```


    The `provider` field accepts: `stripe`, `cash`, or other configured payment
    providers.


    **Submit checkout response** includes order details and optionally
    auto-generated customer auth tokens:

    ```json

    {
      "order": { "id": "...", "name": "#1001", ... },
      "tokens": {
        "accessToken": "eyJ...",
        "refreshToken": "eyJ..."
      }
    }

    ```


    ### Appointments (Customer Authenticated)


    | Method | Endpoint | Auth | Description |

    |--------|----------|------|-------------|

    | `POST` | `/customer/appointments` | API key (+ optional customer token) |
    Create appointment |

    | `GET` | `/customer/appointments` | Customer token | List customer
    appointments (paginated) |

    | `GET` | `/customer/appointments/:id` | Customer token | Get appointment
    details |

    | `PUT` | `/customer/appointments/:id/reschedule` | Customer token |
    Reschedule appointment |

    | `PUT` | `/customer/appointments/:id/cancel` | Customer token | Cancel
    appointment |

    | `POST` | `/customer/appointments/:id/feedback` | Customer token | Submit
    feedback |

    | `GET` | `/customer/appointments/preferences/booking-history` | Customer
    token | Get booking preferences |


    ### Orders (Customer Authenticated)


    | Method | Endpoint | Auth | Description |

    |--------|----------|------|-------------|

    | `GET` | `/customer/orders` | Customer token | List customer orders
    (paginated) |

    | `GET` | `/customer/orders/:id` | Customer token | Get order details |


    ### Customer Profile (Customer Authenticated)


    | Method | Endpoint | Auth | Description |

    |--------|----------|------|-------------|

    | `GET` | `/customer/profile` | Customer token | Get customer profile |

    | `PUT` | `/customer/profile` | Customer token | Update customer profile |

    | `PUT` | `/customer/password` | Customer token | Change password |


    ### Store


    | Method | Endpoint | Auth | Description |

    |--------|----------|------|-------------|

    | `GET` | `/customer/stores/public-settings` | API key | Get store public
    settings |


    **Public settings response includes:**

    - `name` — store display name

    - `domain` — store domain

    - `currency` — store currency code (e.g., `USD`)

    - `settings` — general store settings (timezone, locale, etc.)

    - `storefrontSettings` — presentation config (logo, colors, banners, etc.)

    - `contactInfo` — store contact information

    - `features` — enabled plan features


    ### Payment


    | Method | Endpoint | Auth | Description |

    |--------|----------|------|-------------|

    | `GET` | `/customer/payment/providers` | API key | List available payment
    providers |

    | `GET` | `/customer/payment/settings` | API key | Get public payment
    settings |


    ### Images


    | Method | Endpoint | Auth | Description |

    |--------|----------|------|-------------|

    | `GET` | `/customer/images/:id` | API key | Get image metadata by ID |


    ### Checkout Questions


    | Method | Endpoint | Auth | Description |

    |--------|----------|------|-------------|

    | `GET` | `/customer/checkout-questions/:productId/translations/:language` |
    API key | Get checkout questions for a product |


    ### Feedback Questions


    | Method | Endpoint | Auth | Description |

    |--------|----------|------|-------------|

    | `GET` | `/customer/feedback-questions/:productId/translations/:language` |
    Customer token | Get feedback questions for a product |


    ---


    ## Checkout Flow Detail


    ### Step 1: Create Appointment


    Before adding to a cart, create the appointment with the selected time slot:


    ```json

    POST /customer/appointments

    {
      "slot": {
        "productId": "product-uuid",
        "fromDate": "2026-06-15",
        "fromTime": "09:00:00",
        "toDate": "2026-06-15",
        "toTime": "10:00:00",
        "locationId": "location-uuid-or-null",
        "staffMemberId": "staff-uuid-or-null"
      },
      "numberOfAttendees": 1
    }

    ```


    All date/time values are in **UTC**. Convert from the user's timezone before
    sending.


    ### Step 2: Create Cart & Add Items


    ```json

    POST /customer/cart

    → Returns { "id": "cart-uuid", ... }


    POST /customer/cart/items

    X-Cart-Id: cart-uuid

    { "appointmentId": "appointment-uuid" }

    ```


    Carts expire automatically. Use `POST /customer/cart/extend` to add 5
    minutes.


    ### Step 3: Start Checkout


    ```json

    POST /customer/cart/checkout/start

    X-Cart-Id: cart-uuid

    {
      "provider": "stripe",
      "customer": { "email": "john@example.com", "firstName": "John", "lastName": "Smith" }
    }

    ```


    Response includes payment provider details (e.g., Stripe `clientSecret` for
    Elements).


    ### Step 4: Save Customer Info


    If not provided in start, save customer details separately:


    ```json

    POST /customer/cart/checkout/save-customer

    X-Cart-Id: cart-uuid

    {
      "customer": { "email": "john@example.com", "firstName": "John", "lastName": "Smith" }
    }

    ```


    For logged-in customers, pass `{ "customer": { "customerId":
    "existing-customer-uuid" } }`.


    ### Step 5: Checkout Questions (Optional)


    If the store has checkout questions configured:


    ```

    GET /customer/cart/checkout/questions/en

    X-Cart-Id: cart-uuid

    → Returns array of questions with types (text, select, checkbox, etc.)


    POST /customer/cart/checkout/save-answers

    X-Cart-Id: cart-uuid

    { "answers": [{ "questionId": "...", "value": "..." }] }

    ```


    ### Step 6: Submit Checkout


    ```json

    POST /customer/cart/checkout/submit

    X-Cart-Id: cart-uuid

    {}

    ```


    Response includes the created order and optionally customer auth tokens for
    auto-login.


    ---


    ## Payment Integration


    ### Available Providers


    Fetch configured payment providers for your store:


    ```

    GET /customer/payment/providers

    → [{ "name": "stripe", "displayName": "Credit Card", ... }]

    ```


    ### Stripe Integration


    1. Get Stripe publishable key from `GET /customer/payment/settings`

    2. Start checkout with `provider: "stripe"` — response includes
    `clientSecret`

    3. Use Stripe Elements / Payment Element with the `clientSecret`

    4. After Stripe confirms payment client-side, call `POST /checkout/submit`


    ### Cash / Pay at Venue


    Start checkout with `provider: "cash"`. No payment processing needed —
    submit directly.


    ### Redirect-Based Payments


    Some providers redirect the customer to an external page. The start response
    includes a `redirectUrl`. After payment, the customer is redirected back to
    your `thankYouUrl`.


    ---


    ## Store Settings


    Use `GET /customer/stores/public-settings` to retrieve store configuration
    for your template:


    - **Store name** → display in header/navbar

    - **Logo** → from `storefrontSettings`

    - **Currency** → format prices correctly (e.g., `USD`, `EUR`)

    - **Timezone** → convert availability times to/from the store's timezone

    - **Contact info** → display in footer or contact page

    - **Features** → check which features are enabled (e.g., customer portal)


    ---


    ## Rate Limiting


    Requests are rate-limited per API key:


    | Category | Limit |

    |----------|-------|

    | General | 100 requests / 60 seconds |

    | Authentication | 20 requests / 10 seconds |

    | Password reset | 5 requests / hour |

    | Registration | 10 requests / minute |


    When rate-limited, you'll receive a `429 Too Many Requests` response.


    ---


    ## Response Format


    All responses use **camelCase** property names:


    ```json

    {
      "id": "abc-123",
      "title": "Haircut",
      "maxAttendees": 1,
      "duration": 3600,
      "staffMembers": [
        { "id": "staff-1", "firstName": "John", "lastName": "Doe" }
      ]
    }

    ```


    ### Pagination


    List endpoints return paginated collections:


    ```json

    {
      "data": [ ... ],
      "meta": {
        "total": 42,
        "page": 1,
        "limit": 10,
        "totalPages": 5
      }
    }

    ```


    Use `?page=2&limit=20` query parameters to paginate.


    ---


    ## Error Responses


    ```json

    {
      "statusCode": 400,
      "message": "Validation failed",
      "errors": [
        { "property": "email", "messages": ["Email is required"] }
      ]
    }

    ```


    | Status | Meaning |

    |--------|---------|

    | 400 | Validation error |

    | 401 | Invalid or missing API key / customer token |

    | 404 | Resource not found |

    | 429 | Rate limit exceeded |


    ---


    ## Timezone Handling


    - All date/time values in API requests and responses are in **UTC**

    - Pass the `timezone` query parameter to availability endpoints to get slots
    converted to a specific timezone

    - Use the store's timezone from public settings as the default display
    timezone

    - Convert user-selected times to UTC before creating appointments


    ---


    ## Webhooks (Coming Soon)


    Webhook support for real-time notifications is planned. Events will include:


    - `product.created`, `product.updated`, `product.deleted`

    - `staff_member.created`, `staff_member.updated`, `staff_member.deleted`

    - `location.created`, `location.updated`, `location.deleted`

    - `store.settings_updated`

    - `collection.updated`


    Use webhooks to invalidate cached data in your deployed storefronts.
  version: '1.0'
  contact: {}
servers:
  - url: https://api.opencals.com
security:
  - Storefront API key: []
tags: []
paths:
  /storefront/auth/verify-email:
    post:
      tags:
        - Auth
      summary: Verify customer email
      description: >-
        Confirm customer email ownership using the verification token and return
        auth tokens
      operationId: auth_verifyEmail
      parameters: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/VerifyEmail'
      responses:
        '200':
          description: ''
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TokensResponse'
components:
  schemas:
    VerifyEmail:
      type: object
      properties:
        token:
          type: string
          description: Email verification token from email link
      required:
        - token
    TokensResponse:
      type: object
      properties:
        accessToken:
          type: string
          description: JWT access token for authenticating API requests
          example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
        refreshToken:
          type: string
          description: JWT refresh token for obtaining new access tokens
          example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
      required:
        - accessToken
        - refreshToken
  securitySchemes:
    Storefront API key:
      type: apiKey
      in: header
      name: x-api-key
      description: Storefront API key (sfk_...)
      x-default: sfk_your_api_key

````