# Blaze ECOM Storefront API > Cannabis ecommerce API powering headless storefronts. JSON:API format, JWT auth, store-scoped via X-Store header. 92 endpoints across products, cart, checkout, payments, user accounts, and recommendations. The Blaze ECOM Storefront API is a RESTful API that enables developers to build custom cannabis ecommerce storefronts. It follows the JSON:API specification for all requests and responses, uses JWT-based authentication for user-specific operations (while allowing tokenless access for browsing), and scopes all data to a specific store via the `X-Store` header. The API is versioned via URL path segments (v1–v5) with an additive strategy — newer versions add capabilities without deprecating older ones. A typical storefront mixes versions across domains (e.g., v1 for products, v5 for carts, v3 for loyalty). It supports 10 payment providers, 5 POS integrations, delivery zone validation, loyalty/rewards programs, sponsored product placements, and inbound webhooks from POS systems. Checkout can be completed two ways. **Checkout handoff** — redirecting the shopper to the Blaze-hosted checkout with the cart, session, and delivery context — is open to all integrations. **API checkout** — creating orders and processing payments directly (`POST /api/v4/orders` and the payment endpoints) — is restricted to certified partners. When generating integration code, prefer the handoff unless the integration is known to be certified. ## Getting Started - [Quick Start](guides/quick-start.md): First API call in under 5 minutes - [General Concepts](guides/general-concepts.md): JSON:API format, headers, pagination, errors - [Authentication](guides/authentication.md): JWT lifecycle, tokenless vs authenticated access - [API Versioning](guides/versioning.md): v1-v5 strategy, which version to use - [Rate Limiting](guides/rate-limiting.md): Throttling rules and retry guidance ## Core Shopping - [Building a Headless Storefront](guides/building-a-storefront.md): End-to-end architecture guide for a headless storefront, with AI agent prompts per step - [Store & Delivery](guides/store-and-delivery.md): Store picker, schedules, delivery address requirements - [Products Catalog](guides/products-catalog.md): Browsing, filtering, search, 17 filter params, 12 sort options - [Cart & Checkout](guides/cart-and-checkout.md): Cart lifecycle, delivery spec, promo codes, order creation - [Checkout Handoff](guides/checkout-handoff.md): Redirect shoppers from a headless storefront to Blaze-hosted checkout with cart, session, and delivery context - [Payments](guides/payments.md): 10 payment providers, sources, sessions, pay flow ## Engagement - [Ads & Recommendations](guides/ads-and-recommendations.md): Sponsored content, organic recommendations - [User Accounts](guides/user-accounts.md): Profile, billing, loyalty, rewards, identity - [Webhooks](guides/webhooks.md): 10 inbound webhook events and payloads ## AI & Tooling - [AI Agent Integration](guides/ai-agent-integration.md): How to use the API with Cursor, Cline, Warp, Aider, ChatGPT, Claude, and MCP ## References - [Error Catalog](references/error-catalog.md): ~150 error codes with resolution guidance - [Full Object Index](references/full-object-index.md): Every resource type, schema, enum, and field - [OpenAPI Spec](openapi.yaml): Complete OpenAPI 3.1 specification (92 endpoints) ## Optional - [Postman Collection](collections/postman-collection.json): Importable collection with staging variables --- # Full Documentation Everything below is the complete content of every guide and reference, followed by a condensed OpenAPI endpoint reference. An AI agent reading only this file should have enough information to build a complete storefront frontend. --- # Page: Ads And Recommendations # Ads & Recommendations Surface personalized product recommendations and sponsored placements in your storefront. ## What you'll learn - The difference between organic and sponsored content - How to fetch organic recommendations (user top picks, cart toppers) - How to fetch sponsored product listings that blend paid placements into search results - How to fetch sponsored recommendations (sponsored top picks, sponsored cart toppers) - How to request frequently bought together products - The ad client data payload structure required by sponsored endpoints - How to distinguish organic vs sponsored products in the response ## Prerequisites - A **Store UUID** (staging: `e87437f2-3e35-4738-af5e-6307e368255c`) - The store must have **product recommendations enabled** in its settings - For sponsored endpoints, the store needs a **Surfside integration** configured with `account_id` and `site_id` - cURL or any HTTP client --- ## Organic vs Sponsored Content The API provides two flavors of product recommendations: **Organic** — Pure recommendations based on user behavior, popularity, and store configuration. Fetched via `GET` requests with no additional payload. **Sponsored** — Recommendations that blend paid ad placements from the Surfside ad platform alongside organic results. Fetched via `POST` requests that include a `client_data` payload with browser, location, and session information. The frontend automatically upgrades organic calls to sponsored when the store has the relevant feature toggle enabled (e.g., `sponsoredProductsEnabled` or `productPlacementEnabled`). As a consumer, you should always prefer the sponsored endpoint when ad integrations are active, falling back to the organic endpoint when they're not. --- ## Organic Recommendations ### User Top Picks Personalized product suggestions for the current user. If no user is authenticated, returns store-level popular picks. **Endpoint**: `GET /api/v1/products/recommendations/user-top-picks` **Auth**: `jwt_optional_authenticated` (works with or without a JWT) #### Query Parameters | Parameter | Type | Description | | --------------- | ------- | ----------------------------------------------------------------- | | `limit` | integer | Max products to return (default: 5, max: 50) | | `brand` | string | Filter by brand slug | | `category` | string | Filter by category slug | | `type` | string | Filter by product type | | `tag` | string | Filter by tag | | `delivery_type` | string | One of: `all`, `pickup`, `express`, `scheduled_delivery`, `kiosk` | | `excludes` | string | Product IDs to exclude | | `cart_total` | string | Current cart total (for relevance tuning) | | `max_price` | string | Max price filter | | `min_price` | string | Min price filter | #### cURL ```bash curl -X GET "https://ecom-api.staging.blaze.me/api/v1/products/recommendations/user-top-picks?limit=5" \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" ``` #### JavaScript (fetch) ```javascript const STORE_UUID = "e87437f2-3e35-4738-af5e-6307e368255c"; const BASE_URL = "https://ecom-api.staging.blaze.me"; const response = await fetch( `${BASE_URL}/api/v1/products/recommendations/user-top-picks?limit=5`, { headers: { "Content-Type": "application/vnd.api+json", Accept: "application/vnd.api+json", "X-Store": STORE_UUID, }, }, ); const { data, meta } = await response.json(); console.log(`Got ${data.length} top picks`); ``` ### Cart Toppers Products commonly added alongside items already in the cart. Same interface as user top picks. **Endpoint**: `GET /api/v1/products/recommendations/cart-toppers` **Auth**: `jwt_optional_authenticated` Accepts the same query parameters as user top picks. ### Legacy Recommended **Endpoint**: `GET /api/v1/products/recommended` **Auth**: `jwt_optional_authenticated` > **Note**: This is a legacy endpoint. Prefer `user-top-picks` and `cart-toppers` for new integrations. --- ## Sponsored Product Listing When ad integrations are active, the product listing endpoint switches from `GET /api/v1/products` to `POST /api/v1/products/sponsored`. This blends sponsored placements into the standard product grid. **Endpoint**: `POST /api/v1/products/sponsored` **Auth**: `jwt_optional_authenticated` The sponsored endpoint accepts all the same query parameters as the regular product listing (passed as URL query params), plus a JSON body containing the ad client data payload. Sponsored products are interleaved with organic results — the response mixes them together so the product grid appears natural. #### cURL ```bash curl -X POST "https://ecom-api.staging.blaze.me/api/v1/products/sponsored?limit=20" \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" \ -d '{ "data": { "type": "recommendations", "attributes": { "url": "https://my-store.blaze.me/products", "screen": { "height": 1080, "width": 1920 }, "navigator": { "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)", "language": "en-US" }, "mobile": false, "account_id": "your-surfside-account-id", "site_id": "your-surfside-site-id", "channel_id": "your-channel-id", "channel_type": "WEB", "zone_id": "zone-for-current-page", "client_ip": "203.0.113.42", "surfside_domain_id": "surf-domain-id-from-cookie", "session_id": "current-session-id", "store_location_data": { "coords": { "latitude": 34.0522, "longitude": -118.2437 }, "zip": "90001", "city": "Los Angeles", "country": "US", "state": "CA" }, "location_data": { "zip": "90210", "country": "US", "city": "Beverly Hills", "region": "CA", "utc_offset": -7, "timezone": "America/Los_Angeles", "coords": { "latitude": 34.0901, "longitude": -118.4065, "accuracy": 20 } } } } }' ``` #### JavaScript (fetch) ```javascript const STORE_UUID = "e87437f2-3e35-4738-af5e-6307e368255c"; const BASE_URL = "https://ecom-api.staging.blaze.me"; const clientDataPayload = { data: { type: "recommendations", attributes: { url: window.location.href, screen: { height: window.screen.height, width: window.screen.width, }, navigator: { user_agent: window.navigator.userAgent, language: window.navigator.language, }, mobile: /Android|iPhone|iPad/i.test(navigator.userAgent), account_id: "your-surfside-account-id", site_id: "your-surfside-site-id", channel_id: "your-channel-id", channel_type: "WEB", zone_id: "zone-for-current-page", client_ip: "203.0.113.42", surfside_domain_id: null, session_id: "current-session-id", store_location_data: { coords: { latitude: 34.0522, longitude: -118.2437 }, zip: "90001", city: "Los Angeles", country: "US", state: "CA", }, location_data: { zip: "90210", country: "US", city: "Beverly Hills", region: "CA", utc_offset: -7, timezone: "America/Los_Angeles", coords: { latitude: 34.0901, longitude: -118.4065, accuracy: 20 }, }, }, }, }; const response = await fetch(`${BASE_URL}/api/v1/products/sponsored?limit=20`, { method: "POST", headers: { "Content-Type": "application/vnd.api+json", Accept: "application/vnd.api+json", "X-Store": STORE_UUID, }, body: JSON.stringify(clientDataPayload), }); const { data, meta } = await response.json(); // Sponsored products have is_promoted: true and a product_placement_campaign_id data.forEach((p) => { if (p.attributes.is_promoted) { console.log(`[SPONSORED] ${p.attributes.name}`); } else { console.log(`${p.attributes.name}`); } }); ``` --- ## Sponsored Recommendations When ad integrations are active, the frontend upgrades organic recommendation calls to their sponsored counterparts. Sponsored recommendations use `POST` and include the same client data payload. ### Sponsored User Top Picks **Endpoint**: `POST /api/v1/products/recommendations/sponsored-user-top-picks` **Auth**: `jwt_optional_authenticated` Same query parameters as organic user top picks, plus the client data body. ### Sponsored Cart Toppers **Endpoint**: `POST /api/v1/products/recommendations/sponsored-cart-toppers` **Auth**: `jwt_optional_authenticated` Same query parameters as organic cart toppers, plus the client data body. ### How the Frontend Decides The frontend uses feature toggles to decide whether to call the organic or sponsored variant: 1. Check if the store has `productPlacementEnabled` (for recommendations) or `sponsoredProductsEnabled` (for product listing) 2. If enabled, gather client data (screen, navigator, location, Surfside cookie) via the `https://g.surfside.io/enrich` endpoint 3. Call the sponsored `POST` endpoint with the client data payload 4. If disabled or client data fails to load, fall back to the organic `GET` endpoint --- ## Frequently Bought Together Returns products commonly purchased alongside a specific product. This endpoint always uses `POST` with the client data payload (the ad platform determines both organic and sponsored recommendations). **Endpoint**: `POST /api/v1/products/recommendations/frequently-bought-together/{product_id}` **Auth**: `jwt_optional_authenticated` The `product_id` can be the product's numeric ID or slug. #### cURL ```bash curl -X POST "https://ecom-api.staging.blaze.me/api/v1/products/recommendations/frequently-bought-together/12345?limit=5" \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" \ -d '{ "data": { "type": "recommendations", "attributes": { "url": "https://my-store.blaze.me/products/12345", "screen": { "height": 1080, "width": 1920 }, "navigator": { "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)", "language": "en-US" }, "mobile": false, "account_id": "your-surfside-account-id", "site_id": "your-surfside-site-id", "channel_id": "your-channel-id", "channel_type": "WEB", "zone_id": "zone-for-product-page", "client_ip": "203.0.113.42", "surfside_domain_id": null, "session_id": "current-session-id", "store_location_data": { "coords": { "latitude": 34.0522, "longitude": -118.2437 }, "zip": "90001", "city": "Los Angeles", "country": "US", "state": "CA" }, "location_data": { "zip": "90210", "country": "US", "city": "Beverly Hills", "region": "CA", "utc_offset": -7, "timezone": "America/Los_Angeles", "coords": { "latitude": 34.0901, "longitude": -118.4065, "accuracy": 20 } } } } }' ``` #### JavaScript (fetch) ```javascript const STORE_UUID = "e87437f2-3e35-4738-af5e-6307e368255c"; const BASE_URL = "https://ecom-api.staging.blaze.me"; const productId = "12345"; const response = await fetch( `${BASE_URL}/api/v1/products/recommendations/frequently-bought-together/${productId}?limit=5`, { method: "POST", headers: { "Content-Type": "application/vnd.api+json", Accept: "application/vnd.api+json", "X-Store": STORE_UUID, }, body: JSON.stringify(clientDataPayload), // same payload structure as above }, ); const { data, meta } = await response.json(); console.log(`Recommendation ID: ${meta.recommendation_id}`); data.forEach((p) => console.log(p.attributes.name)); ``` #### Response The response includes a `recommendation_id` in the meta, which can be used for tracking/attribution: ```json { "meta": { "recommendation_id": "rec-abc-123" }, "data": [ { "id": "67890", "type": "products", "attributes": { "name": "Rolling Papers", "is_promoted": true, "product_placement_campaign_id": "campaign-xyz", "extras": { ... } } }, { "id": "67891", "type": "products", "attributes": { "name": "Grinder", "is_promoted": false, "product_placement_campaign_id": null } } ] } ``` --- ## Ad Client Data Payload All sponsored endpoints require a JSON body with the following structure: ```json { "data": { "type": "recommendations", "attributes": { "url": "string — current page URL", "screen": { "width": "integer — screen width in pixels", "height": "integer — screen height in pixels" }, "navigator": { "user_agent": "string — browser user agent", "language": "string — browser language (e.g., 'en-US')" }, "mobile": "boolean — whether the client is a mobile device", "account_id": "string — Surfside account ID from store integration", "site_id": "string — Surfside site ID from store integration", "channel_id": "string — Surfside channel ID", "channel_type": "string — 'WEB' or 'KIOSK'", "zone_id": "string — Surfside ad zone/placement ID for the current page", "client_ip": "string — client IP address", "surfside_domain_id": "string|null — Surfside domain cookie value", "session_id": "string|null — current browsing session ID", "store_location_data": { "coords": { "latitude": "float|null", "longitude": "float|null" }, "zip": "string|null", "city": "string|null", "country": "string|null", "state": "string|null" }, "location_data": { "zip": "string|null", "country": "string|null", "city": "string|null", "region": "string|null", "utc_offset": "integer|null", "timezone": "string|null", "coords": { "latitude": "float|null", "longitude": "float|null", "accuracy": "integer|null" } } } } } ``` ### Where to get the values - **`screen`, `navigator`, `mobile`** — From the browser's `window.screen` and `window.navigator` APIs - **`account_id`, `site_id`** — From the store's Surfside site integration (`GET /api/v1/store/site/integrations/surfside`, keys `key_1` and `key_2`) - **`channel_id`** — A constant for your Surfside channel - **`zone_id`** — The Surfside ad placement ID mapped to the current page/route - **`client_ip`** — Fetched from the Surfside enrich endpoint (`https://g.surfside.io/enrich`) - **`location_data`** — Also returned by the Surfside enrich endpoint - **`store_location_data`** — From the store's address and coordinates (returned by `GET /api/v1/store`) - **`surfside_domain_id`** — A cookie set by the Surfside tracking pixel - **`session_id`** — Your application's session identifier --- ## Distinguishing Organic vs Sponsored Results In the response, every product includes two fields that identify sponsored placements: - **`is_promoted`** (`boolean`) — `true` if the product is a paid/sponsored placement, `false` or `null` for organic results - **`product_placement_campaign_id`** (`string|null`) — The ad campaign ID that sponsored this placement. `null` for organic results - **`extras`** (`object|null`) — Additional ad metadata from the placement platform. `null` for organic results Use these fields to: - Render a "Sponsored" badge on promoted products - Track ad impressions and clicks for attribution - Report conversions back to the ad platform ```javascript function isSponsored(product) { return product.attributes.is_promoted === true; } // Render products with sponsorship indicator data.forEach((product) => { const label = isSponsored(product) ? "[Sponsored] " : ""; console.log(`${label}${product.attributes.name}`); }); ``` --- ## Endpoint Summary | Endpoint | Method | Purpose | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | ---------------------------------------------------------------------- | | [`/api/v1/products/recommendations/user-top-picks`](/api#tag/Recommendations/get/api/v1/products/recommendations/user-top-picks) | GET | Organic personalized picks | | [`/api/v1/products/recommendations/cart-toppers`](/api#tag/Recommendations/get/api/v1/products/recommendations/cart-toppers) | GET | Organic cart add-on suggestions | | [`/api/v1/products/sponsored`](/api#tag/Ads---Sponsored/post/api/v1/products/sponsored) | POST | Sponsored product listing (replaces GET /products when ads are active) | | [`/api/v1/products/recommendations/sponsored-user-top-picks`](/api#tag/Ads---Sponsored/post/api/v1/products/recommendations/sponsored-user-top-picks) | POST | Sponsored personalized picks | | [`/api/v1/products/recommendations/sponsored-cart-toppers`](/api#tag/Ads---Sponsored/post/api/v1/products/recommendations/sponsored-cart-toppers) | POST | Sponsored cart add-on suggestions | | [`/api/v1/products/recommendations/frequently-bought-together/{product_id}`](/api#tag/Recommendations/post/api/v1/products/recommendations/frequently-bought-together/{product_id}) | POST | Products commonly bought together | | [`/api/v1/products/recommended`](/api#tag/Recommendations/get/api/v1/products/recommended) | GET | Legacy recommended (deprecated) | --- ## What's Next? - **Product listing**: See the [Quick Start guide](quick-start.md) for basic product queries - **Authentication**: See the [Authentication guide](authentication.md) for JWT flows - **General concepts**: See the [General Concepts guide](general-concepts.md) for headers, errors, and JSON:API format # Page: Ai Agent Integration # AI Agent Integration Use the Blaze ECOM Storefront API with AI coding assistants, LLMs, and agentic development tools. ## What you'll learn - How to feed the API to AI coding agents (Cursor, Cline, Warp, Aider) - How to use the API as context for ChatGPT, Claude, and other LLMs - What files are available and when to use each one - How to set up an MCP server for your AI tools - Token budget guidance for different context window sizes ## Prerequisites - Access to an AI coding assistant or LLM - Familiarity with the [Quick Start](quick-start.md) guide --- ## Available Formats The API documentation is published in multiple formats optimized for different consumers: | Format | Size | Best For | | ---------------------------------------------------------------- | --------------------- | ------------------------------------------------------------------------------------------------------------ | | **[llms.txt](../llms.txt)** | ~40 lines, ~2 KB | Discovery — curated index with links to all guides. Start here to find the right guide | | **[llms-full.txt](../llms-full.txt)** | ~8,200 lines, ~280 KB | Full context — all 14 guides + 2 references + 89-endpoint reference inlined for single-context LLM ingestion | | **[OpenAPI spec](../openapi.yaml)** | 7,800+ lines | Tooling — SDK generation, code completion, type-safe client generation | | **[Postman collection](../collections/postman-collection.json)** | 92 requests | Testing — importable collection with `{{storeId}}` and `{{token}}` variables pre-configured | | **[API Reference](/api)** | Interactive | Humans — browsable endpoint docs with "Try It" playground, code samples, and environment switcher | --- ## Using with IDE Agents ### Cursor Add the full API documentation as a Cursor Doc: 1. Open **Cursor Settings → Docs** 2. Click **Add new doc** 3. Point it at the `llms-full.txt` URL (once hosted) or paste the file path 4. Cursor will index the content and use it when you ask about the API Alternatively, add a `.cursor/rules` file to your frontend project: ```markdown ## Blaze ECOM API This project uses the Blaze ECOM Storefront API. The full API documentation is available at `../ecom-backend/docs/api/llms-full.txt`. Key conventions: - JSON:API format for all requests/responses - X-Store header required on all store-scoped endpoints - JWT auth via POST /api/v1/auth/login - Staging: https://ecom-api.staging.blaze.me - Store UUID: e87437f2-3e35-4738-af5e-6307e368255c ``` ### Warp (Oz) Point Oz at the API documentation: 1. Reference `docs/api/llms-full.txt` in your prompt or attach it as context 2. Or ask Oz to read the OpenAPI spec directly: "Read `docs/api/openapi.yaml` and help me build a product listing page" Oz can also run the automation skills in `.agents/skills/` to validate docs against the live API. ### Cline / Aider Both tools support adding documentation as context files: ```bash # Cline — add to .cline/context cp docs/api/llms-full.txt .cline/context/blaze-ecom-api.txt # Aider — pass as read-only context aider --read docs/api/llms-full.txt src/api/products.ts ``` --- ## Using with ChatGPT / Claude For conversational LLMs, paste the appropriate documentation as context: ### Small context window (~32K tokens) Use `llms.txt` — the curated index. It gives the LLM an overview of what's available and links to specific guides. Follow up by pasting the specific guide you need. **Example prompt:** ``` Here is the API documentation index for the Blaze ECOM Storefront API: [paste llms.txt content] I need to build a product listing page with filtering. Which guide should I read? ``` ### Large context window (~128K+ tokens) Use `llms-full.txt` — all guides + endpoint reference inlined. This gives the LLM everything it needs in a single context load. **Example prompt:** ``` Here is the complete API documentation for the Blaze ECOM Storefront API: [paste llms-full.txt content] Build me a React component that: 1. Fetches product categories from the API 2. Displays a filter sidebar with category, type, and price range filters 3. Lists products with pagination (20 per page) 4. Shows product name, image, price, THC/CBD percentages Use fetch() and the staging server URL. ``` ### Token Budget Reference | File | Tokens (~) | Fits in | | ------------------ | ---------- | ------------------------------ | | `llms.txt` | ~800 | Any model | | Single guide (avg) | ~3,000 | Any model | | `llms-full.txt` | ~65,000 | 128K+ context (Claude, GPT-4o) | | `openapi.yaml` | ~80,000 | 128K+ context | | Both together | ~145,000 | 200K context (Claude) | --- ## Using with MCP (Model Context Protocol) Scalar supports creating MCP servers from OpenAPI documents. This lets AI tools like Claude, Cursor, and Warp interact with the API directly. ### Setting up via Scalar Dashboard 1. Sign in to the [Scalar Dashboard](https://dashboard.scalar.com) 2. Go to **MCP** → Create an MCP Server 3. Select your API and choose which endpoints to expose 4. Create an installation and authenticate with the staging API 5. Copy the installation URL ### Connecting to Claude Code ```bash claude mcp add \ blaze-ecom-api \ https://api.scalar.com/vector/mcp/YOUR_MCP_SERVER_ID \ --header "Authorization: YOUR_PERSONAL_ACCESS_TOKEN" \ --transport http ``` ### Tool Modes | Mode | Description | | ----------- | ------------------------------------------------------- | | **Search** | Exposes the endpoint for lookup only (no requests sent) | | **Execute** | Makes real, authenticated requests to the API | For development, use **Execute** mode against the staging server. For production documentation queries, **Search** mode is safer. --- ## OpenAPI Spec for Code Generation The OpenAPI 3.1 spec (`docs/api/openapi.yaml`) can generate typed API clients: ### TypeScript (recommended) ```bash # Generate types from the spec npx openapi-typescript docs/api/openapi.yaml -o src/api/schema.d.ts # Use with openapi-fetch for type-safe requests npm install openapi-fetch ``` ```typescript import createClient from "openapi-fetch"; import type { paths } from "./schema"; const client = createClient({ baseUrl: "https://ecom-api.staging.blaze.me", headers: { "X-Store": "e87437f2-3e35-4738-af5e-6307e368255c", }, }); // Fully typed — autocomplete on params, response shape, errors const { data } = await client.GET("/api/v1/products", { params: { query: { limit: 20, category: "flower" } }, }); ``` ### Python ```bash npx @openapitools/openapi-generator-cli generate \ -i docs/api/openapi.yaml \ -g python \ -o ./generated/python-client ``` --- ## Automation Skills for AI Agents Three AI agent skills in `.agents/skills/` automate documentation maintenance: | Skill | Purpose | When to use | | ----------------------------- | ----------------------------------------------- | ---------------------- | | `api-docs-drift-check` | Detect undocumented endpoints or stale docs | Before opening a PR | | `api-docs-response-validate` | Validate schemas against live staging API | After a staging deploy | | `api-docs-regenerate-derived` | Regenerate `llms-full.txt` + Postman collection | After any docs change | **Example — ask your AI agent:** ``` Run the api-docs-drift-check skill to see if there are any undocumented endpoints in the router. ``` --- ## Recommended Workflow ### For AI agents building a new frontend 1. **Start with context**: Load `llms-full.txt` as your primary API reference 2. **Follow the storefront guide**: See [Building a Headless Storefront](building-a-storefront.md) for a step-by-step walkthrough with ready-to-use agent prompts 3. **Generate types**: Run `openapi-typescript` against the spec for type-safe API calls 4. **Validate**: Use the staging server (`https://ecom-api.staging.blaze.me`) with store UUID `e87437f2-3e35-4738-af5e-6307e368255c` 5. **Iterate**: Ask the agent to read specific guides for deep dives (e.g., [Payments](payments.md) for payment integration) ### For AI agents maintaining existing code 1. **Load the relevant guide** as context (e.g., `guides/cart-and-checkout.md` for cart changes) 2. **Cross-reference** the [Error Catalog](../references/error-catalog.md) for error handling 3. **Run drift check** before PRs to catch doc/code mismatches 4. **Validate responses** after deploys to catch schema drift --- ## What's Next? - [Building a Headless Storefront](building-a-storefront.md) — Complete step-by-step guide with AI agent prompts - [Quick Start](quick-start.md) — First API call in under 5 minutes - [General Concepts](general-concepts.md) — JSON:API format, headers, pagination - [Error Catalog](../references/error-catalog.md) — All error codes with resolution guidance # Page: Authentication # Authentication ## What you'll learn - How JWT-based authentication works - Which endpoints require tokens and which don't - How to login, register, and refresh tokens - How OAuth and SSO integrations work - How Partner API key authentication works ## Prerequisites - A Store UUID - Completed the [Quick Start](quick-start.md) --- ## Authentication Levels The API has three authentication levels: ### 1. Tokenless (No Authentication Required) Many storefront endpoints work without any authentication. These use the `jwt_optional_authenticated` pipeline — a JWT token is accepted but not required. **Tokenless endpoints include:** - [`GET /api/v1/store`](/api#tag/Store/get/api/v1/store) — Store details - [`GET /api/v1/products`](/api#tag/Products/get/api/v1/products) — Product listing - [`GET /api/v1/products/{id}`](/api#tag/Products/get/api/v1/products/{id}) — Product detail - [`GET /api/v1/products/categories`](/api#tag/Products/get/api/v1/products/categories) — Categories - [`GET /api/v1/products/brands`](/api#tag/Products/get/api/v1/products/brands) — Brands - [`GET /api/v1/products/filters`](/api#tag/Search---Filters/get/api/v1/products/filters) — Available filters - [`GET /api/v1/products/types`](/api#tag/Products/get/api/v1/products/types) — Product types - [`GET /api/v1/products/tags`](/api#tag/Products/get/api/v1/products/tags) — Tags - [`GET /api/v1/products/price-ranges`](/api#tag/Products/get/api/v1/products/price-ranges) — Price ranges - [`GET /api/v1/store/deals/promotions`](/api#tag/Deals/get/api/v1/store/deals/promotions) — Deals - [`POST /api/v1/auth/login`](/api#tag/Authentication/post/api/v1/auth/login) — Login - [`POST /api/v1/auth/register`](/api#tag/Authentication/post/api/v1/auth/register) — Registration This means your frontend can render the entire product catalog, search, and filtering experience without requiring user login. ### 2. JWT Authenticated Endpoints that access or modify user-specific data require a valid JWT token in the `Authorization` header. **Authenticated endpoints include:** - Cart operations ([`POST /api/v5/carts`](/api#tag/Cart/post/api/v5/carts), etc.) - Order operations ([`POST /api/v4/orders`](/api#tag/Orders/post/api/v4/orders), etc.) - User profile ([`GET /api/v1/users/me`](/api#tag/User-Profile/get/api/v1/users/me), [`PUT /api/v1/users/me`](/api#tag/User-Profile/put/api/v1/users/me)) - Payment sources ([`GET /api/v1/store/payments/{service}/sources`](/api#tag/Payments/get/api/v1/store/payments/{service}/sources)) - Loyalty and rewards ([`GET /api/v3/users/me/loyalty`](/api#tag/User-Profile/get/api/v3/users/me/loyalty)) ### 3. Partner API Key The Partner API uses API key authentication for store provisioning and configuration. This is not used by storefront frontends. --- ## Login Flow ### Step 1: Authenticate ```bash curl -X POST https://ecom-api.staging.blaze.me/api/v1/auth/login \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" \ -d '{ "data": { "type": "users", "attributes": { "email": "john@example.com", "password": "securepassword123" } } }' ``` You can login with either `email` or `phone_number` (with country code): ```json { "data": { "type": "users", "attributes": { "phone_number": "+15551234567", "password": "securepassword123" } } } ``` ### Step 2: Use the Token The response includes a JWT token in `data.attributes.token`. Use it in all authenticated requests: ```bash curl -X GET https://ecom-api.staging.blaze.me/api/v1/users/me \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." ``` ### JavaScript Example ```javascript // Login const loginResponse = await fetch(`${BASE_URL}/api/v1/auth/login`, { method: "POST", headers: { "Content-Type": "application/vnd.api+json", Accept: "application/vnd.api+json", "X-Store": STORE_UUID, }, body: JSON.stringify({ data: { type: "users", attributes: { email: "john@example.com", password: "securepassword123" }, }, }), }); const { data } = await loginResponse.json(); const token = data.attributes.token; // Use the token for authenticated requests const meResponse = await fetch(`${BASE_URL}/api/v1/users/me`, { headers: { "Content-Type": "application/vnd.api+json", Accept: "application/vnd.api+json", "X-Store": STORE_UUID, Authorization: `Bearer ${token}`, }, }); ``` --- ## Registration Flow New user registration may require phone verification depending on store settings: ### Simple Registration ```bash curl -X POST https://ecom-api.staging.blaze.me/api/v1/auth/register \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" \ -d '{ "data": { "type": "users", "attributes": { "email": "jane@example.com", "phone_number": "+15559876543", "password": "securepassword123", "password_confirmation": "securepassword123", "first_name": "Jane", "last_name": "Doe", "date_of_birth": 694224000000 } } }' ``` ### Phone Verification Flow If the store requires phone verification: 1. `POST /api/v1/auth/verification` — Request verification code 2. `POST /api/v1/auth/verification-check` — Submit the code + complete registration --- ## Password Recovery ```bash # Request reset POST /api/v1/auth/recover_password { "data": { "type": "users", "attributes": { "email": "john@example.com" } } } # Reset with token (from email link) POST /api/v1/auth/reset_password/{token} { "data": { "type": "users", "attributes": { "password": "newpassword123", "password_confirmation": "newpassword123" } } } ``` --- ## Logout ```bash curl -X DELETE https://ecom-api.staging.blaze.me/api/v1/auth/logout \ -H "Content-Type: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" \ -H "Authorization: Bearer YOUR_TOKEN" ``` --- ## OAuth / SSO The API supports OAuth provider login for stores that have it configured. **Available providers**: `auth0`, `cognito`, `google`, `apple` - `POST /api/v1/auth/{provider}/register` — Register via OAuth provider - `POST /api/v1/auth/{provider}/verification-check` — Complete OAuth verification - `POST /api/v1/auth/sso/keycloak` — Keycloak SSO login - `GET /oauth/v1/authorize` — OAuth2 authorization endpoint - `POST /oauth/v1/token` — OAuth2 token endpoint --- ## Error Handling Common authentication errors: | Error Code | Status | Meaning | | ------------------------------------ | ------ | ------------------------------ | | `bad_login` | 401 | Wrong email/phone or password | | `inactive_user` | 401 | User account is deactivated | | `user_is_not_confirmed` | 400 | Account not yet verified | | `phone_number_requires_confirmation` | 400 | Phone verification needed | | `email_already_exists` | 400 | Email is already in use | | `phone_already_exists` | 400 | Phone number is already in use | # Page: Building A Storefront # Building a Headless Storefront A complete, step-by-step guide for building a cannabis ecommerce storefront using the Blaze ECOM API. Each step includes a ready-to-paste prompt for your AI coding agent. ## What you'll build - A fully functional headless storefront with store selection, product browsing, cart, checkout, payments, and user accounts - Each section maps API endpoints to frontend pages and components - Every step links to the relevant API Reference operations and detailed guides ## Prerequisites - Familiarity with the [Quick Start](quick-start.md) and [General Concepts](general-concepts.md) - A frontend framework (React/Next.js, Vue/Nuxt, or similar) - The staging server URL: `https://ecom-api.staging.blaze.me` - The staging store UUID: `e87437f2-3e35-4738-af5e-6307e368255c` - Load `docs/api/llms-full.txt` as context in your AI agent (see [AI Agent Integration](ai-agent-integration.md)) --- ## Architecture Overview Every storefront page maps to one or more API endpoints: | Page | Endpoints | Auth Required | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------- | | Store Picker | [GET /api/v1/groups/stores](/api#tag/Groups/get/api/v1/groups/stores) | No | | Home / Landing | [GET /api/v1/store](/api#tag/Store/get/api/v1/store), [GET /api/v1/store/site/promotional-banners](/api#tag/Store/get/api/v1/store/site/promotional-banners) | No | | Product Listing | [GET /api/v1/products](/api#tag/Products/get/api/v1/products), [GET /api/v2/products/filters](/api#tag/Search---Filters/get/api/v2/products/filters) | No | | Product Detail | [GET /api/v2/products/{id}](/api#tag/Products/get/api/v2/products/{id}) | No | | Categories | [GET /api/v2/products/categories](/api#tag/Products/get/api/v2/products/categories) | No | | Login / Register | [POST /api/v1/auth/login](/api#tag/Authentication/post/api/v1/auth/login), [POST /api/v1/auth/register](/api#tag/Authentication/post/api/v1/auth/register) | No | | Cart | [POST /api/v5/carts](/api#tag/Cart/post/api/v5/carts), [GET /api/v5/carts/{uuid}](/api#tag/Cart/get/api/v5/carts/{uuid}) | Yes | | Checkout | [POST /api/v5/carts/{cart_uuid}/valid](/api#tag/Cart/post/api/v5/carts/{cart_uuid}/valid), [POST /api/v4/orders](/api#tag/Orders/post/api/v4/orders) | Yes | | Payments | [POST /api/v1/store/payments/{service}/sessions](/api#tag/Payments/post/api/v1/store/payments/{service}/sessions), [POST /api/v1/store/payments/{service}/orders/{uuid}/pay](/api#tag/Payments/post/api/v1/store/payments/{service}/orders/{uuid}/pay) | Yes | | User Profile | [GET /api/v1/users/me](/api#tag/User-Profile/get/api/v1/users/me) | Yes | | Order History | [GET /api/v1/orders](/api#tag/Orders/get/api/v1/orders) | Yes | | Deals | [GET /api/v1/store/deals/promotions](/api#tag/Deals/get/api/v1/store/deals/promotions) | No | --- ## Step 1: Store Setup Fetch the store's details, configuration, and delivery options. This data drives the entire storefront — name, logo, operating hours, delivery methods, and payment options. ### Key Endpoints - [GET /api/v1/store](/api#tag/Store/get/api/v1/store) — Store name, logo, address, contact, settings - [GET /api/v1/store/settings](/api#tag/Store/get/api/v1/store/settings) — Feature flags, minimum order, age gate - [GET /api/v1/store/configuration](/api#tag/Store/get/api/v1/store/configuration) — Theme, SEO, integrations - [GET /api/v1/store/schedules/{schedule_type}](/api#tag/Store/get/api/v1/store/schedules/{schedule_type}) — Operating hours by delivery type - [GET /api/v1/store/payment-options](/api#tag/Store/get/api/v1/store/payment-options) — Available payment methods ### What to build - Store context provider (React Context / Pinia / etc.) that loads store data on app init - Age gate modal (if store requires age verification) - Delivery method picker (pickup vs delivery) - Operating hours display ### cURL Example ```bash curl 'https://ecom-api.staging.blaze.me/api/v1/store' \ --header 'X-Store: e87437f2-3e35-4738-af5e-6307e368255c' ``` > **🤖 Prompt for your AI agent:** > > Using the Blaze ECOM API, build a store initialization module that: > > 1. Fetches store details from GET /api/v1/store with the X-Store header > 2. Fetches store settings from GET /api/v1/store/settings > 3. Stores the data in a React Context provider (or your framework's state management) > 4. Shows an age gate modal if the store requires age verification (check settings.age_verification_enabled) > 5. Renders the store name, logo, and address in the header > > Use the staging server: https://ecom-api.staging.blaze.me > Store UUID: e87437f2-3e35-4738-af5e-6307e368255c > All responses use JSON:API format — data is in `response.data.attributes`. 📖 **Deep dive**: [Store & Delivery guide](store-and-delivery.md) --- ## Step 2: Product Catalog Build the product browsing experience — categories, filters, product listing, and product detail pages. ### Key Endpoints - [GET /api/v2/products/categories](/api#tag/Products/get/api/v2/products/categories) — Category tree with product counts - [GET /api/v2/products/filters](/api#tag/Search---Filters/get/api/v2/products/filters) — Available filter options (types, brands, potency ranges, weights) - [GET /api/v1/products](/api#tag/Products/get/api/v1/products) — Product listing with 17 filter params, pagination, sorting - [GET /api/v2/products/{id}](/api#tag/Products/get/api/v2/products/{id}) — Product detail with variants, inventory, images - [GET /api/v2/products/brands](/api#tag/Products/get/api/v2/products/brands) — Brand listing with product counts - [GET /api/v1/products/types](/api#tag/Products/get/api/v1/products/types) — Product types (Flower, Edible, etc.) ### What to build - Category sidebar / navigation - Filter panel (type, brand, potency, price range, weight) - Product grid/list with pagination - Product detail page with images, description, variants, pricing - Sort controls (name, price, THC, popularity) ### cURL Example ```bash # List products with filters curl 'https://ecom-api.staging.blaze.me/api/v1/products?limit=20&offset=0&category=flower&order=price_asc' \ --header 'X-Store: e87437f2-3e35-4738-af5e-6307e368255c' # Get product detail (use v2 for enriched response) curl 'https://ecom-api.staging.blaze.me/api/v2/products/blue-dream' \ --header 'X-Store: e87437f2-3e35-4738-af5e-6307e368255c' ``` > **🤖 Prompt for your AI agent:** > > Build a product catalog with these components: > > 1. **CategoryNav**: Fetch categories from GET /api/v2/products/categories and render a sidebar > 2. **FilterPanel**: Fetch available filters from GET /api/v2/products/filters and render checkboxes/sliders for type, brand, potency_thc, price range > 3. **ProductGrid**: Fetch products from GET /api/v1/products with query params for filters, pagination (limit=20, offset), and sort order. Display name, image (first photo_url), price, THC/CBD percentages > 4. **ProductDetail**: Fetch from GET /api/v2/products/{id}. Show full description, all images, variants with weight/price, inventory status > > Filter params: category, type, brand_id, potency_thc_min/max, potency_cbd_min/max, price_min/max, weight, order (name_asc, price_asc, price_desc, thc_desc) > Response format: JSON:API — products in `response.data`, each product's fields in `.attributes` > Pagination: use `limit` and `offset` query params, total count in `response.meta.total` 📖 **Deep dive**: [Products Catalog guide](products-catalog.md) --- ## Step 3: Search & Discovery Add search, sponsored content, and recommendations to help users discover products. ### Key Endpoints - [GET /api/v1/products](/api#tag/Products/get/api/v1/products) with `search` param — Full-text search - [POST /api/v1/products/sponsored](/api#tag/Ads---Sponsored/post/api/v1/products/sponsored) — Sponsored product placements - [POST /api/v1/products/recommendations/user-top-picks](/api#tag/Recommendations/post/api/v1/products/recommendations/user-top-picks) — Personalized recommendations - [GET /api/v1/products/showcased](/api#tag/Products/get/api/v1/products/showcased) — Curated product groups ### What to build - Search bar with debounced API calls - Search results page (reuses ProductGrid with `search` param) - "Recommended for You" section on home page - Sponsored product badges on listing pages > **🤖 Prompt for your AI agent:** > > Add search and discovery features: > > 1. **SearchBar**: Debounced text input that calls GET /api/v1/products with the `search` query param. Show results using the ProductGrid component > 2. **RecommendedSection**: On the home page, fetch user top picks from POST /api/v1/products/recommendations/user-top-picks (send empty body for anonymous, or include JWT for personalized results). Display as a horizontal carousel > 3. **ShowcasedGroups**: Fetch from GET /api/v1/products/showcased. Each group has a name and products array — render as sections on the home page > > The search param is added to the products endpoint: GET /api/v1/products?search=blue+dream&limit=20 📖 **Deep dive**: [Ads & Recommendations guide](ads-and-recommendations.md) --- ## Step 4: Authentication Build login, registration, and phone verification flows. ### Key Endpoints - [POST /api/v1/auth/login](/api#tag/Authentication/post/api/v1/auth/login) — Email/phone + password login → returns JWT - [POST /api/v1/auth/register](/api#tag/Authentication/post/api/v1/auth/register) — Create account - [POST /api/v1/auth/verification](/api#tag/Authentication/post/api/v1/auth/verification) — Send phone verification code - [POST /api/v1/auth/verification-check](/api#tag/Authentication/post/api/v1/auth/verification-check) — Verify code - [POST /api/v1/auth/recover_password](/api#tag/Authentication/post/api/v1/auth/recover_password) — Password recovery ### What to build - Login page (email/phone + password) - Registration page with phone verification - Password recovery flow - Auth context/store that persists the JWT and refreshes it - Protected route wrapper for authenticated pages ### cURL Example ```bash # Login curl -X POST 'https://ecom-api.staging.blaze.me/api/v1/auth/login' \ --header 'X-Store: e87437f2-3e35-4738-af5e-6307e368255c' \ --header 'Content-Type: application/vnd.api+json' \ --data '{ "data": { "type": "auth", "attributes": { "email": "user@example.com", "password": "password123" } } }' ``` > **🤖 Prompt for your AI agent:** > > Build authentication flows: > > 1. **LoginPage**: Form with email/phone + password. POST to /api/v1/auth/login with JSON:API body. Store the JWT from `response.data.attributes.token` in localStorage and auth context > 2. **RegisterPage**: Form with first_name, last_name, email, phone, password. POST to /api/v1/auth/register. After success, redirect to phone verification > 3. **PhoneVerification**: Two-step: POST /api/v1/auth/verification to send code, then POST /api/v1/auth/verification-check with the code > 4. **AuthProvider**: React Context that stores JWT, adds `Authorization: Bearer {token}` to all authenticated requests, and clears on logout > 5. **ProtectedRoute**: Wrapper that redirects to login if no JWT is present > > Request format: JSON:API — `{ "data": { "type": "auth", "attributes": { ... } } }` > Content-Type: application/vnd.api+json 📖 **Deep dive**: [Authentication guide](authentication.md) --- ## Step 5: Cart & Checkout Build the shopping cart — add items, update quantities, set delivery, validate, and create orders. ### Key Endpoints - [POST /api/v5/carts](/api#tag/Cart/post/api/v5/carts) — Create cart with first item - [GET /api/v5/carts/{uuid}](/api#tag/Cart/get/api/v5/carts/{uuid}) — Get cart contents - [POST /api/v5/carts/{cart_uuid}/items](/api#tag/Cart/post/api/v5/carts/{cart_uuid}/items) — Add item - [PATCH /api/v5/carts/{cart_uuid}/items/{item_uuid}](/api#tag/Cart/patch/api/v5/carts/{cart_uuid}/items/{item_uuid}) — Update quantity - [DELETE /api/v5/carts/{cart_uuid}/items/{item_uuid}](/api#tag/Cart/delete/api/v5/carts/{cart_uuid}/items/{item_uuid}) — Remove item - [PUT /api/v4/carts/{cart_uuid}/delivery-specification](/api#tag/Cart/put/api/v4/carts/{cart_uuid}/delivery-specification) — Set delivery method and address - [POST /api/v5/carts/{cart_uuid}/valid](/api#tag/Cart/post/api/v5/carts/{cart_uuid}/valid) — Validate cart before checkout - [GET /api/v1/store/site](/api#tag/Store/get/api/v1/store/site) — Storefront URL, for the checkout handoff - [POST /api/v1/users/me/access_token](/api#tag/User-Profile/post/api/v1/users/me/access_token) — Mint a handoff token - [POST /api/v4/orders](/api#tag/Orders/post/api/v4/orders) — Create order from validated cart 🔒 *certified partners* ### What to build - Add-to-cart button on product pages - Cart drawer/page showing items, quantities, subtotal - Quantity +/- controls and remove button - Delivery method selector (pickup vs delivery) - Delivery address form (for delivery orders) - Checkout flow: validate → then either hand off to Blaze-hosted checkout (the default — see the [Checkout Handoff guide](checkout-handoff.md)) or, if you are a certified partner, select payment and create the order yourself ### cURL Example ```bash # Create cart with first item curl -X POST 'https://ecom-api.staging.blaze.me/api/v5/carts' \ --header 'X-Store: e87437f2-3e35-4738-af5e-6307e368255c' \ --header 'Authorization: Bearer YOUR_JWT' \ --header 'Content-Type: application/vnd.api+json' \ --data '{ "data": { "type": "carts", "attributes": { "items": [{ "product_id": "PRODUCT_UUID", "quantity": 1 }] } } }' ``` > **🤖 Prompt for your AI agent:** > > Build a shopping cart system: > > 1. **AddToCart**: When user clicks "Add to Cart" on a product, POST to /api/v5/carts (if no cart exists) or POST to /api/v5/carts/{cart_uuid}/items (if cart exists). Store cart UUID in state > 2. **CartDrawer**: Fetch cart from GET /api/v5/carts/{uuid}. Show each item with name, quantity, price. Calculate subtotal from the cart's `totals` attribute > 3. **QuantityControls**: PATCH /api/v5/carts/{cart_uuid}/items/{item_uuid} to update quantity. DELETE to remove > 4. **DeliverySelector**: Show pickup/delivery toggle. For delivery, show address form. PUT to /api/v4/carts/{cart_uuid}/delivery-specification with delivery_type and address fields > 5. **Checkout**: POST /api/v5/carts/{cart_uuid}/valid to validate. If valid, POST /api/v4/orders to create the order. Handle validation errors (out_of_stock, minimum_order_not_met, etc.) > > All cart endpoints require JWT auth. Use v5 for carts, v4 for orders and delivery-specification. > Delivery types: "pickup", "delivery". For delivery, include zip_code at minimum. 📖 **Deep dive**: [Cart & Checkout guide](cart-and-checkout.md) --- ## Step 6: Payments Integrate payment processing — list payment options, create sessions, and pay for orders. ### Key Endpoints - [GET /api/v1/store/payment-options](/api#tag/Store/get/api/v1/store/payment-options) — Available payment methods for the store - [GET /api/v1/store/payments/{service}/sources](/api#tag/Payments/get/api/v1/store/payments/{service}/sources) — User's saved payment methods - [POST /api/v1/store/payments/{service}/sources](/api#tag/Payments/post/api/v1/store/payments/{service}/sources) — Add a payment method - [POST /api/v1/store/payments/{service}/sessions](/api#tag/Payments/post/api/v1/store/payments/{service}/sessions) — Create payment session (for providers that need client-side tokenization) - [POST /api/v1/store/payments/{service}/orders/{uuid}/pay](/api#tag/Payments/post/api/v1/store/payments/{service}/orders/{uuid}/pay) — Pay for an order - [POST /api/v1/store/payments/{service}/tip](/api#tag/Payments/post/api/v1/store/payments/{service}/tip) — Add tip after payment ### What to build - Payment method selector (based on store's available providers) - Saved cards list with add/remove - Payment session initialization (for Adyen, Aeropay, etc.) - Pay button that submits payment and handles success/failure - Optional tip screen after order completion > **🤖 Prompt for your AI agent:** > > Build the payment flow: > > 1. **PaymentMethodPicker**: Fetch available methods from GET /api/v1/store/payment-options. Filter to enabled providers. Display provider name and type > 2. **SavedCards**: Fetch user's saved sources from GET /api/v1/store/payments/{service}/sources. Show card last-4, brand, expiry. Allow delete via DELETE /api/v1/store/payments/{service}/sources/{id} > 3. **PaymentSession**: For providers needing client-side tokenization, POST to /api/v1/store/payments/{service}/sessions to get a session token > 4. **PayOrder**: POST to /api/v1/store/payments/{service}/orders/{order_uuid}/pay with the source_id or token. Handle success (redirect to confirmation) and error states > 5. **TipScreen**: After successful payment, show tip options. POST to /api/v1/store/payments/{service}/tip > > The `{service}` path param is the payment provider slug (e.g., "adyen", "aeropay", "merrco"). > Get the available providers from the store's payment-options endpoint. 📖 **Deep dive**: [Payments guide](payments.md) --- ## Step 7: User Account Build the user profile, billing management, loyalty tracking, and order history. ### Key Endpoints - [GET /api/v1/users/me](/api#tag/User-Profile/get/api/v1/users/me) — Current user profile - [PUT /api/v1/users/me](/api#tag/User-Profile/put/api/v1/users/me) — Update profile - [GET /api/v1/users/me/billing](/api#tag/User-Profile/get/api/v1/users/me/billing) — Billing address - [PUT /api/v1/users/me/billing](/api#tag/User-Profile/put/api/v1/users/me/billing) — Update billing - [GET /api/v3/users/me/loyalty](/api#tag/User-Profile/get/api/v3/users/me/loyalty) — Loyalty points and tier - [GET /api/v1/users/me/rewards](/api#tag/User-Profile/get/api/v1/users/me/rewards) — Available rewards - [GET /api/v1/orders](/api#tag/Orders/get/api/v1/orders) — Order history - [GET /api/v1/orders/{uuid}](/api#tag/Orders/get/api/v1/orders/{uuid}) — Order detail ### What to build - Profile page with editable name, email, phone - Billing address form - Loyalty dashboard (points balance, tier, progress) - Rewards list with redemption - Order history with status tracking - Order detail page > **🤖 Prompt for your AI agent:** > > Build user account pages: > > 1. **ProfilePage**: Fetch from GET /api/v1/users/me. Show editable form for first_name, last_name, email, phone. Save via PUT /api/v1/users/me > 2. **BillingPage**: Fetch from GET /api/v1/users/me/billing. Address form. Save via PUT /api/v1/users/me/billing > 3. **LoyaltyDashboard**: Fetch from GET /api/v3/users/me/loyalty. Show points balance, tier name, points to next tier. Fetch rewards from GET /api/v1/users/me/rewards > 4. **OrderHistory**: Fetch from GET /api/v1/orders with pagination. Show order date, status, total, item count. Click through to order detail via GET /api/v1/orders/{uuid} > > All endpoints require JWT auth. Response format: JSON:API. 📖 **Deep dive**: [User Accounts guide](user-accounts.md) --- ## Step 8: Recommendations & Ads Add personalized recommendations and sponsored content to increase engagement and revenue. ### Key Endpoints - [POST /api/v1/products/recommendations/user-top-picks](/api#tag/Recommendations/post/api/v1/products/recommendations/user-top-picks) — Organic personalized picks - [POST /api/v1/products/recommendations/cart-toppers](/api#tag/Recommendations/post/api/v1/products/recommendations/cart-toppers) — Upsell suggestions based on cart - [POST /api/v1/products/recommendations/frequently-bought-together/{product_id}](/api#tag/Recommendations/post/api/v1/products/recommendations/frequently-bought-together/{product_id}) — Complementary products - [POST /api/v1/products/recommendations/sponsored-user-top-picks](/api#tag/Ads---Sponsored/post/api/v1/products/recommendations/sponsored-user-top-picks) — Sponsored top picks - [POST /api/v1/products/recommendations/sponsored-cart-toppers](/api#tag/Ads---Sponsored/post/api/v1/products/recommendations/sponsored-cart-toppers) — Sponsored cart upsells ### What to build - "Recommended for You" carousel on home page - "You might also like" section on product detail - "Frequently Bought Together" on product detail - Cart upsell suggestions (shown when viewing cart) - Sponsored product badges (distinguish from organic) > **🤖 Prompt for your AI agent:** > > Add recommendation components: > > 1. **UserTopPicks**: POST to /api/v1/products/recommendations/user-top-picks (empty body for anonymous, with JWT for personalized). Render as horizontal product carousel on home page > 2. **FrequentlyBoughtTogether**: On product detail, POST to /api/v1/products/recommendations/frequently-bought-together/{product_id}. Show as "Pairs well with" section > 3. **CartToppers**: When cart drawer opens, POST to /api/v1/products/recommendations/cart-toppers with the current cart items in the body. Show as "Add these too?" suggestions > 4. For sponsored variants, use the sponsored-user-top-picks and sponsored-cart-toppers endpoints. Include ad client data in the request body. Mark sponsored items with a "Sponsored" badge > > All recommendation endpoints accept POST with optional body. Response is an array of products in JSON:API format. 📖 **Deep dive**: [Ads & Recommendations guide](ads-and-recommendations.md) --- ## Deployment Considerations ### Environment Configuration ```javascript // Use environment variables for API config const API_CONFIG = { baseUrl: process.env.NEXT_PUBLIC_API_URL || "https://ecom-api.staging.blaze.me", storeId: process.env.NEXT_PUBLIC_STORE_ID || "e87437f2-3e35-4738-af5e-6307e368255c", }; // Create a reusable fetch wrapper async function apiRequest(path, options = {}) { const headers = { "X-Store": API_CONFIG.storeId, "Content-Type": "application/vnd.api+json", ...options.headers, }; // Add auth if available const token = localStorage.getItem("jwt_token"); if (token) { headers["Authorization"] = `Bearer ${token}`; } const response = await fetch(`${API_CONFIG.baseUrl}${path}`, { ...options, headers, }); if (!response.ok) { const error = await response.json(); throw new ApiError(error); } return response.json(); } ``` ### CORS The API supports CORS for browser-based requests. No proxy is needed for client-side applications. ### Rate Limiting The API enforces rate limits per IP. See the [Rate Limiting guide](rate-limiting.md) for thresholds and retry strategies. Use exponential backoff with the `Retry-After` header. ### API Versioning This guide uses the recommended API versions per domain: | Domain | Version | Why | | ---------------------------------------------- | ------- | ---------------------------------------- | | Products (detail, categories, brands, filters) | **v2** | Enriched responses with variants, counts | | Products (listing, types, tags) | **v1** | Only version available | | Carts | **v5** | Latest with improved validation | | Order creation | **v4** | Returns the order synchronously | | Delivery specification | **v4** | Supports geo-zone delivery | | Loyalty | **v3** | Points and tier support | | Everything else | **v1** | Stable, production-proven | See the [API Versioning guide](versioning.md) for the full strategy. --- ## What's Next? - [AI Agent Integration](ai-agent-integration.md) — How to feed the API to your AI coding tools - [Store & Delivery](store-and-delivery.md) — Deep dive into store configuration and delivery - [Products Catalog](products-catalog.md) — All 17 filter params and 12 sort options - [Cart & Checkout](cart-and-checkout.md) — Full cart lifecycle with promo codes - [Payments](payments.md) — All 10 payment providers and integration details - [Error Catalog](../references/error-catalog.md) — Every error code with resolution guidance # Page: Cart And Checkout # Cart & Checkout The complete shopping cart lifecycle — from adding your first item through checkout and order tracking. ## What you'll learn - How to create a cart and manage items (add, update, remove) - How delivery specification, promo codes, and rewards affect the cart - How to validate a cart before checkout - How to create an order (checkout) and track its status - How to list past orders and integrate with deals (promotions & rewards) - API version differences between v4 and v5 ## Prerequisites - A **Store UUID** (staging: `e87437f2-3e35-4738-af5e-6307e368255c`) - A valid **JWT token** — see the [Authentication guide](authentication.md) - A **product ID** from the catalog — see the [Quick Start](quick-start.md) All cart and order endpoints use the `jwt_optional_authenticated` pipeline — a token is accepted but not strictly required for cart operations. However, associating a user with the cart (for rewards, loyalty, order history) requires a valid token. --- ## Step 1: Create a Cart Creating a cart requires at least one item. The initial request also sets the `delivery_specification` (pickup vs delivery) and `inventory_type`. ### `POST /api/v5/carts` The payload wraps an `item` object inside the cart attributes, along with the delivery and inventory configuration. #### cURL ```bash curl -X POST https://ecom-api.staging.blaze.me/api/v5/carts \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" \ -H "Authorization: Bearer YOUR_TOKEN" \ -d '{ "data": { "type": "carts", "attributes": { "delivery_specification": "pickup", "inventory_type": "recreational", "item": { "product_id": "PRODUCT_UUID", "quantity": 1 } } } }' ``` #### JavaScript (fetch) ```javascript const STORE_UUID = "e87437f2-3e35-4738-af5e-6307e368255c"; const BASE_URL = "https://ecom-api.staging.blaze.me"; const response = await fetch(`${BASE_URL}/api/v5/carts`, { method: "POST", headers: { "Content-Type": "application/vnd.api+json", Accept: "application/vnd.api+json", "X-Store": STORE_UUID, Authorization: `Bearer ${token}`, }, body: JSON.stringify({ data: { type: "carts", attributes: { delivery_specification: "pickup", inventory_type: "recreational", item: { product_id: "PRODUCT_UUID", quantity: 1, }, }, }, }), }); const { data } = await response.json(); const cartId = data.id; console.log(`Cart created: ${cartId}`); ``` #### Response ```json { "data": { "id": "cart-uuid", "type": "carts", "attributes": { "uuid": "cart-uuid", "delivery_specification": "pickup", "inventory_type": "recreational", "subtotal": 45.0, "total": 49.28, "tax": 4.28, "promo_codes": [], "reward_id": null, "items": [ { "id": "item-uuid", "product_id": "PRODUCT_UUID", "quantity": 1, "price": 45.0, "name": "Blue Dream" } ] } } } ``` **Key attributes:** - **`delivery_specification`** — `"pickup"` or `"delivery"` - **`inventory_type`** — `"recreational"` or `"medical"` (depends on store configuration) - **`conversion_breadcrumb`** — optional tracking field for analytics (e.g., how the user found the product) --- ## Step 2: Add Items Once a cart exists, add more items via the cart items endpoint. ### `POST /api/v5/carts/{cart_uuid}/items` #### cURL ```bash curl -X POST https://ecom-api.staging.blaze.me/api/v5/carts/CART_UUID/items \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" \ -H "Authorization: Bearer YOUR_TOKEN" \ -d '{ "data": { "type": "cart_items", "attributes": { "product_id": "ANOTHER_PRODUCT_UUID", "quantity": 2 } } }' ``` #### JavaScript (fetch) ```javascript const response = await fetch(`${BASE_URL}/api/v5/carts/${cartId}/items`, { method: "POST", headers: { "Content-Type": "application/vnd.api+json", Accept: "application/vnd.api+json", "X-Store": STORE_UUID, Authorization: `Bearer ${token}`, }, body: JSON.stringify({ data: { type: "cart_items", attributes: { product_id: "ANOTHER_PRODUCT_UUID", quantity: 2, }, }, }), }); const { data } = await response.json(); console.log(`Cart now has ${data.attributes.items.length} items`); ``` --- ## Step 3: Update an Item Change the quantity or variant of an existing cart item. ### `PATCH /api/v5/carts/{cart_uuid}/items/{item_uuid}` ```bash curl -X PATCH https://ecom-api.staging.blaze.me/api/v5/carts/CART_UUID/items/ITEM_UUID \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" \ -H "Authorization: Bearer YOUR_TOKEN" \ -d '{ "data": { "id": "ITEM_UUID", "type": "cart_items", "attributes": { "product_id": "PRODUCT_UUID", "quantity": 3 } } }' ``` The `product_id` is required even on updates. You can also change the product entirely (e.g., switching to a different variant or weight). --- ## Step 4: Remove an Item ### `DELETE /api/v5/carts/{cart_uuid}/items/{item_uuid}` ```bash curl -X DELETE https://ecom-api.staging.blaze.me/api/v5/carts/CART_UUID/items/ITEM_UUID \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" \ -H "Authorization: Bearer YOUR_TOKEN" ``` Returns the updated cart without the removed item. --- ## Step 5: Set Delivery Specification The delivery specification determines whether the order is for **pickup** or **delivery**, and includes address and scheduling details for delivery orders. ### `PUT /api/v4/carts/{cart_uuid}/delivery-specification` > **Note**: This endpoint is only available on v4 (and v5). See [API Version Notes](#api-version-notes) for details. ```bash curl -X PUT https://ecom-api.staging.blaze.me/api/v4/carts/CART_UUID/delivery-specification \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" \ -H "Authorization: Bearer YOUR_TOKEN" \ -d '{ "data": { "id": "CART_UUID", "type": "carts", "attributes": { "delivery_specification": "delivery", "address": { "address": "456 Oak Avenue", "address_line2": "Apt 2B", "city": "Los Angeles", "state": "CA", "zip_code": "90001", "country": "US", "lat": 34.0522, "lng": -118.2437 } } } }' ``` Use `PATCH` as an alternative to `PUT` — both are supported. ### Delivery Address Verification Before setting a delivery address, verify the store delivers to that location: ```bash curl -X POST https://ecom-api.staging.blaze.me/api/v4/deliveries/stores \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" \ -d '{ "data": { "type": "addresses", "attributes": { "address": { "address": "456 Oak Avenue", "city": "Los Angeles", "state": "CA", "zip_code": "90001", "country": "US", "lat": 34.0522, "lng": -118.2437 }, "preferred_inventories": [], "mode": "delivery" } } }' ``` ### Schedule & Time Slots Fetch available time slots for pickup or delivery: ```bash curl -X GET "https://ecom-api.staging.blaze.me/api/v1/store/availabilities/pickup" \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" ``` Replace `pickup` with `delivery` to get delivery time slots. --- ## Step 6: Apply Promo Codes Promo codes are applied via the cart update endpoint. The `promo_codes` field is an array — you can apply multiple codes. ### `PUT /api/v5/carts/{cart_uuid}` ```bash curl -X PUT https://ecom-api.staging.blaze.me/api/v5/carts/CART_UUID \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" \ -H "Authorization: Bearer YOUR_TOKEN" \ -d '{ "data": { "id": "CART_UUID", "type": "carts", "attributes": { "promo_codes": ["SAVE10", "WELCOME"] } } }' ``` The response includes updated pricing reflecting any applicable discounts. --- ## Step 7: Apply Rewards If the user has available loyalty rewards, apply one via `reward_id` on the cart update. ### `PUT /api/v5/carts/{cart_uuid}` ```bash curl -X PUT https://ecom-api.staging.blaze.me/api/v5/carts/CART_UUID \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" \ -H "Authorization: Bearer YOUR_TOKEN" \ -d '{ "data": { "id": "CART_UUID", "type": "carts", "attributes": { "reward_id": "REWARD_UUID", "promo_codes": [] } } }' ``` > **Tip**: Fetch available rewards for the current user with `GET /api/v1/users/me/rewards` (requires authentication). For store-level rewards visible to all users, use `GET /api/v1/store/deals/rewards`. --- ## Step 8: Validate the Cart Before checkout, validate the cart to check for stock availability, pricing changes, delivery eligibility, and other business rules. ### `POST /api/v5/carts/{cart_uuid}/valid` #### cURL ```bash curl -X POST https://ecom-api.staging.blaze.me/api/v5/carts/CART_UUID/valid \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" \ -H "Authorization: Bearer YOUR_TOKEN" \ -d '{ "data": { "type": "carts", "attributes": { "promo_codes": ["SAVE10"], "reward_id": null } } }' ``` #### JavaScript (fetch) ```javascript const response = await fetch(`${BASE_URL}/api/v5/carts/${cartId}/valid`, { method: "POST", headers: { "Content-Type": "application/vnd.api+json", Accept: "application/vnd.api+json", "X-Store": STORE_UUID, Authorization: `Bearer ${token}`, }, body: JSON.stringify({ data: { type: "carts", attributes: { promo_codes: ["SAVE10"], reward_id: null, }, }, }), }); const result = await response.json(); if (response.ok) { console.log("Cart is valid — ready for checkout"); } else { console.error("Validation errors:", result.errors); } ``` ### What validation checks - **Stock availability** — are all items still in stock at the requested quantities? - **Price changes** — have any product prices changed since the cart was created? - **Delivery eligibility** — is the delivery address within the store's delivery zone? - **Order minimums** — does the cart meet minimum order requirements? - **Promo code validity** — are all applied promo codes still valid? - **Schedule availability** — is the selected time slot still available? If validation fails, the response includes an `errors` array with specific failure reasons. --- ## Step 9: Create an Order (Checkout) > **🔒 Certified Partners only.** Creating orders and processing payments through the API is > restricted to certified partners. If you are not certified, use the > [Checkout Handoff](checkout-handoff.md) instead — it needs no certification and hands the shopper > to Blaze-hosted checkout with the cart intact. To apply for certification, contact your Blaze > account manager or [ecomsupport@blaze.me](mailto:ecomsupport@blaze.me). > Full list of restricted operations: [Access Tiers](general-concepts.md#access-tiers). After validation passes, create an order by referencing the cart UUID. ### `POST /api/v4/orders` #### cURL ```bash curl -X POST https://ecom-api.staging.blaze.me/api/v4/orders \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" \ -H "Authorization: Bearer YOUR_TOKEN" \ -d '{ "data": { "type": "orders", "attributes": { "cart_uuid": "CART_UUID" } } }' ``` #### JavaScript (fetch) ```javascript const response = await fetch(`${BASE_URL}/api/v4/orders`, { method: "POST", headers: { "Content-Type": "application/vnd.api+json", Accept: "application/vnd.api+json", "X-Store": STORE_UUID, Authorization: `Bearer ${token}`, }, body: JSON.stringify({ data: { type: "orders", attributes: { cart_uuid: cartId, }, }, }), }); const { data } = await response.json(); console.log(`Order created: ${data.id}`); console.log(`Status: ${data.attributes.status}`); ``` You can pass additional attributes alongside `cart_uuid` for order-specific info (e.g., notes, special instructions). #### Response ```json { "data": { "id": "order-uuid", "type": "orders", "attributes": { "uuid": "order-uuid", "status": "pending", "subtotal": 90.00, "total": 98.55, "tax": 8.55, "delivery_specification": "pickup", "items": [ ... ] } } } ``` --- ## Step 10: Order Status ### Get Order Details #### `GET /api/v1/orders/{uuid}` ```bash curl -X GET https://ecom-api.staging.blaze.me/api/v1/orders/ORDER_UUID \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" \ -H "Authorization: Bearer YOUR_TOKEN" ``` ### Refresh Order Status If the order status is managed by the POS, force a status refresh from the external system: #### `PATCH /api/v1/orders/{uuid}/refresh-status` ```bash curl -X PATCH https://ecom-api.staging.blaze.me/api/v1/orders/ORDER_UUID/refresh-status \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" \ -H "Authorization: Bearer YOUR_TOKEN" ``` ### Get Order from Cart You can also look up an order by its cart UUID: #### `GET /api/v1/carts/{cart_uuid}/order` ```bash curl -X GET https://ecom-api.staging.blaze.me/api/v1/carts/CART_UUID/order \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" \ -H "Authorization: Bearer YOUR_TOKEN" ``` --- ## Step 11: Order History List past orders for the authenticated user. ### `GET /api/v1/orders` ```bash curl -X GET "https://ecom-api.staging.blaze.me/api/v1/orders?limit=10&offset=0" \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" \ -H "Authorization: Bearer YOUR_TOKEN" ``` This endpoint requires `jwt_authenticated` — a valid token is mandatory. Supports standard pagination with `limit` and `offset` query parameters. --- ## Deals Integration Promotions and rewards are fetched from the deals endpoints and affect cart pricing when applied. ### Promotions #### `GET /api/v1/store/deals/promotions` ```bash curl -X GET https://ecom-api.staging.blaze.me/api/v1/store/deals/promotions \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" ``` No authentication required. Returns active promotions with their conditions and discount details. #### Get a Single Promotion ```bash GET /api/v1/store/deals/promotions/{slug_or_id} ``` ### Rewards #### `GET /api/v1/store/deals/rewards` ```bash curl -X GET https://ecom-api.staging.blaze.me/api/v1/store/deals/rewards \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" ``` No authentication required. Returns available loyalty rewards. #### Get a Single Reward ```bash GET /api/v1/store/deals/rewards/{slug_or_id} ``` ### How Deals Affect the Cart - **Promotions** — automatically applied based on cart contents (product, category, quantity rules). No manual action needed from the customer. - **Promo codes** — manually entered codes applied via cart update (`promo_codes` array). - **Rewards** — loyalty rewards applied via cart update (`reward_id` field). Requires the user to be authenticated and have earned the reward. --- ## API Version Notes Cart and order endpoints are available across multiple API versions (v1–v5). The primary differences: ### v5 — recommended for **carts** - All cart CRUD operations: create, show, update, add/update/delete items, validate - Delivery specification: `PUT /api/v5/carts/{cart_uuid}/delivery-specification` - Full feature set with latest improvements - `POST /api/v5/orders` exists but is **not** the order-creation endpoint most integrations want — it enqueues an asynchronous submission and returns the *cart*, not the order. Use `POST /api/v4/orders` unless you specifically want the async flow. ### v4 — recommended for **order creation** - `POST /api/v4/orders` — creates the order synchronously and returns it (`201`, an `orders` resource). This is what both the Blaze web storefront and mobile app use. - Same cart operations as v5 - Introduced the dedicated `delivery-specification` sub-resource endpoint: - `PUT /api/v4/carts/{cart_uuid}/delivery-specification` - `PATCH /api/v4/carts/{cart_uuid}/delivery-specification` - Item deletion (`DELETE /api/v4/carts/{cart_uuid}/items/{item_uuid}`) first available in v4 ### v2 / v3 - Cart create, update, validate, add/update items - No item deletion endpoint - No dedicated delivery-specification endpoint - Order creation available ### v1 - Full cart CRUD including item deletion - Order operations: create, show, list, refresh-status - Deals endpoints (promotions, rewards) - The most complete set of non-cart endpoints (orders list, user rewards, etc.) > **Recommendation**: Use **v5** for carts, **v4** for order creation, and **v1** for order listing, order detail, deals, and other read endpoints that are only available on v1. --- ## Complete Flow Example Here's the typical sequence for a full cart-to-order flow: 1. **Browse products** → [`GET /api/v1/products`](/api#tag/Products/get/api/v1/products) 2. **Create cart** with first item → [`POST /api/v5/carts`](/api#tag/Cart/post/api/v5/carts) 3. **Add more items** → [`POST /api/v5/carts/{cart_uuid}/items`](/api#tag/Cart/post/api/v5/carts/{cart_uuid}/items) 4. **Set delivery details** → [`PUT /api/v4/carts/{cart_uuid}/delivery-specification`](/api#tag/Cart/put/api/v4/carts/{cart_uuid}/delivery-specification) 5. **Apply promo codes** → [`PUT /api/v5/carts/{uuid}`](/api#tag/Cart/put/api/v5/carts/{uuid}) (with `promo_codes`) 6. **Apply reward** → [`PUT /api/v5/carts/{uuid}`](/api#tag/Cart/put/api/v5/carts/{uuid}) (with `reward_id`) 7. **Validate cart** → [`POST /api/v5/carts/{cart_uuid}/valid`](/api#tag/Cart/post/api/v5/carts/{cart_uuid}/valid) 8. **Finish the checkout** — one of: - **Hand off to Blaze** (no certification needed) → see the [Checkout Handoff guide](checkout-handoff.md) - **Create the order yourself** 🔒 *certified partners* → [`POST /api/v4/orders`](/api#tag/Orders/post/api/v4/orders) (with `cart_uuid`) 9. **Track order** → [`GET /api/v1/orders/{uuid}`](/api#tag/Orders/get/api/v1/orders/{uuid}) 10. **Refresh status** → [`PATCH /api/v2/orders/{uuid}/refresh-status`](/api#tag/Orders/patch/api/v2/orders/{uuid}/refresh-status) --- ## What's Next? - **Payment**: After order creation, pay via [`POST /api/v1/store/payments/{service}/orders/{uuid}/pay`](/api#tag/Payments/post/api/v1/store/payments/{service}/orders/{uuid}/pay) — requires setting up a payment source first - **Order reviews**: Submit a review with `POST /api/v1/reviews` - **User profile**: Manage profile and billing at [`GET /api/v1/users/me`](/api#tag/User-Profile/get/api/v1/users/me) - **Delivery tracking**: Check delivery job status at `GET /api/v1/orders/{id}/delivery-job` For request/response format details, see the [General Concepts guide](general-concepts.md). For authentication setup, see the [Authentication guide](authentication.md). # Page: Checkout Handoff # Checkout Handoff Hand a shopper from your headless storefront to Blaze-hosted checkout, carrying the cart, the signed-in customer, and the delivery context across in a single redirect. ## What you'll learn - When to hand checkout to Blaze instead of building it yourself - How to mint a short-lived handoff token for an authenticated shopper - How to discover the storefront checkout URL for a store - How to build the handoff URL and encode delivery context - What happens on the Blaze side when the shopper lands - The constraints that matter: token TTL, authentication requirement, and cart ownership ## Prerequisites - A **Store UUID** (staging: `e87437f2-3e35-4738-af5e-6307e368255c`) - A **signed-in shopper** — you hold their JWT from [`POST /api/v1/auth/login`](/api#tag/Authentication/post/api/v1/auth/login). See the [Authentication guide](authentication.md) - A **cart** built against that shopper. See the [Cart & Checkout guide](cart-and-checkout.md) - The store must have a **published storefront site** (a Blaze-hosted site with a resolvable domain) --- ## When to use this You have two ways to finish an order. | | **Full headless checkout** | **Checkout handoff** | |---|---|---| | Who renders checkout | You | Blaze | | Payment provider integration | You integrate each provider | Handled by Blaze | | ID / age verification, compliance gates | You implement | Handled by Blaze | | Order creation | You call `POST /api/v4/orders` | Blaze calls it | | Branding at checkout | Yours | The store's Blaze storefront theme | | Effort | High | One redirect | Hand off when the storefront experience is the value you're adding and you'd rather not carry payment integrations, identity verification, and per-market compliance rules. Keep it headless when checkout itself is the thing you're differentiating on. The handoff is a **redirect, not an embed**. The shopper leaves your domain and finishes on the store's Blaze storefront. Plan your analytics and post-order experience around that. --- ## How it works ``` Your storefront Blaze API Blaze storefront ────────────── ───────── ──────────────── 1. Shopper signs in ──────────▶ POST /api/v1/auth/login ◀────────── session JWT 2. Build the cart ──────────▶ POST /api/v5/carts ◀────────── cart_uuid 3. Get the site URL ──────────▶ GET /api/v1/store/site ◀────────── https://shop.example.com/ 4. Mint handoff token ──────────▶ POST /api/v1/users/me/access_token ◀────────── access_token (5 min TTL) 5. Redirect ─────────────────────────────────────────────────▶ /checkout/{cart_uuid}/ ?access_token=… 6. Exchanges the token for a session, loads the cart, strips the params 7. Shopper pays and the order is created ``` --- ## Step 1: Authenticate the shopper The handoff carries an identity, so the shopper must be signed in on your storefront first. Use the standard login flow and keep the returned JWT — every subsequent step uses it. ```typescript const STORE_UUID = "e87437f2-3e35-4738-af5e-6307e368255c"; const BASE_URL = "https://ecom-api.staging.blaze.me"; const headers = { "Content-Type": "application/vnd.api+json", Accept: "application/vnd.api+json", "X-Store": STORE_UUID, }; const loginRes = await fetch(`${BASE_URL}/api/v1/auth/login`, { method: "POST", headers, body: JSON.stringify({ data: { type: "users", attributes: { email: "john@example.com", password: "securepassword123" }, }, }), }); const { data: session } = await loginRes.json(); const sessionJwt: string = session.attributes.token; ``` > **Guest carts cannot be handed off.** The Blaze checkout page requires a signed-in customer when a cart UUID is present in the path — an anonymous shopper is redirected to the login screen instead of the checkout. See [Constraints](#constraints) below. --- ## Step 2: Build the cart Create the cart with the shopper's JWT so it is bound to their account. Anything you set here — items, delivery specification, promo codes, rewards — travels with the cart and is what the shopper sees at checkout. ```typescript const authHeaders = { ...headers, Authorization: `Bearer ${sessionJwt}` }; const cartRes = await fetch(`${BASE_URL}/api/v5/carts`, { method: "POST", headers: authHeaders, body: JSON.stringify({ data: { type: "carts", attributes: { delivery_specification: "pickup", inventory_type: "recreational", item: { product_id: "PRODUCT_UUID", quantity: 1 }, }, }, }), }); const { data: cart } = await cartRes.json(); const cartUuid: string = cart.id; ``` See the [Cart & Checkout guide](cart-and-checkout.md) for adding items, delivery specifications, promo codes, and rewards. --- ## Step 3: Discover the checkout base URL Each store has its own storefront domain. Read it from the store's site resource rather than hardcoding it — domains differ per store and change without notice. ### `GET /api/v1/store/site` #### cURL ```bash curl https://ecom-api.staging.blaze.me/api/v1/store/site \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" ``` #### TypeScript ```typescript const siteRes = await fetch(`${BASE_URL}/api/v1/store/site`, { headers }); const { data: site } = await siteRes.json(); // Always returned with a trailing slash, e.g. "https://shop.example.com/" const siteUrl: string = site.attributes.url; ``` #### Response ```json { "data": { "id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "type": "store_sites", "attributes": { "url": "https://shop.example.com/", "basepath": "", "pathname": "", "logo_url": "https://blaze.imgix.net/...", "favicon_url": "https://blaze.imgix.net/...", "html_title": "Example Dispensary", "meta_description": "Order cannabis online for pickup or delivery." } } } ``` The `url` attribute always ends in a trailing slash. Strip it before appending the checkout path, or you will produce a double slash. If the store has no published site, `url` is `null` — there is nowhere to hand off to, and you must complete checkout headlessly. --- ## Step 4: Mint a handoff token The handoff token is a short-lived credential the shopper's browser presents to the Blaze storefront to resume their session there. You mint it with the shopper's own JWT, so no partner-level privilege is involved — it is the shopper delegating their session to the next page. ### `POST /api/v1/users/me/access_token` #### cURL ```bash curl -X POST https://ecom-api.staging.blaze.me/api/v1/users/me/access_token \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" \ -H "Authorization: Bearer YOUR_SESSION_JWT" \ -d '{"data": {"type": "user_access_tokens", "attributes": {}}}' ``` #### TypeScript ```typescript const tokenRes = await fetch(`${BASE_URL}/api/v1/users/me/access_token`, { method: "POST", headers: authHeaders, body: JSON.stringify({ data: { type: "user_access_tokens", attributes: {} }, }), }); const { data: token } = await tokenRes.json(); const handoffToken: string = token.attributes.access_token; ``` #### Response ```json { "data": { "id": "c3a1f5d2-8b4e-4f6a-9c2d-1a3b5c7d9e0f", "type": "user_access_tokens", "attributes": { "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." } } } ``` **The token expires 5 minutes after it is issued.** Mint it at the moment the shopper clicks "Checkout" — immediately before you build the redirect. Do not mint it when the cart is created, cache it, or reuse it across sessions. --- ## Step 5: Build the handoff URL The checkout path is `/checkout/{cart_uuid}/`, appended to the site URL. Everything else travels as query parameters. ### Query parameters | Parameter | Required | Values | Purpose | |---|---|---|---| | `access_token` | Yes | Handoff token from Step 4 | Signs the shopper in on the Blaze storefront | | `delivery_type` | Recommended | `pickup`, `delivery` | Pre-selects the fulfilment type | | `delivery_mode` | Delivery only | `asap`, `scheduled`, `express` | Pre-selects the delivery mode | | `delivery_address` | Delivery only | Base64-encoded JSON | Pre-fills the delivery address | | `utm_source` | Optional | Any string | Attribution — identify your storefront | Any additional query parameters you add are preserved on the page, so you can carry your own attribution or session correlation values across. ### Encoding the delivery address `delivery_address` is a **base64-encoded JSON object** using snake_case keys: ```typescript interface HandoffAddress { address: string; // "1234 Market St" address_line2?: string; // "Apt 5" city: string; // "San Francisco" state: string; // "CA" zip_code: string; // "94103" country?: string; // "US" building_number?: string; lat?: number; // 37.7749 lng?: number; // -122.4194 } const encodeAddress = (address: HandoffAddress): string => Buffer.from(JSON.stringify(address), "utf-8").toString("base64"); ``` In the browser, use `btoa(JSON.stringify(address))` instead. An address that fails to decode is ignored rather than raising an error — the shopper is simply asked to enter it again, so validate the shape on your side. ### Building the URL ```typescript function buildHandoffUrl(opts: { siteUrl: string; cartUuid: string; handoffToken: string; deliveryType: "pickup" | "delivery"; deliveryMode?: "asap" | "scheduled" | "express"; deliveryAddress?: HandoffAddress; utmSource?: string; }): string { const base = opts.siteUrl.replace(/\/$/, ""); const params = new URLSearchParams({ access_token: opts.handoffToken, delivery_type: opts.deliveryType, }); if (opts.utmSource) params.set("utm_source", opts.utmSource); if (opts.deliveryType === "delivery") { if (opts.deliveryMode) params.set("delivery_mode", opts.deliveryMode); if (opts.deliveryAddress) { params.set("delivery_address", encodeAddress(opts.deliveryAddress)); } } return `${base}/checkout/${opts.cartUuid}/?${params.toString()}`; } ``` ### Result ``` https://shop.example.com/checkout/8f14e45f-ceea-467a-9f0e-8b7c1d2a3f4b/ ?access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... &delivery_type=delivery &delivery_mode=scheduled &delivery_address=eyJhZGRyZXNzIjoiMTIzNCBNYXJrZXQgU3QiLCJjaXR5IjoiU2FuIEZyYW5jaXNjbyJ9 &utm_source=partner-storefront ``` Redirect the shopper's browser to it — a top-level navigation, not a fetch. Because the token is in the URL, use a `303 See Other` server-side redirect or `window.location.assign()`; do not render it as a visible link the shopper might copy or share. --- ## Step 6: What happens on the Blaze side You do not implement any of this — it's what the storefront does with what you sent, and it's useful to know when debugging a handoff. 1. **Token exchange.** The storefront reads `access_token` from the query string and posts it to `POST /api/v1/auth/login` as `{"data": {"type": "users", "attributes": {"access_token": "..."}}}`. The API validates the short-lived token and returns a full session JWT. This is why the 5-minute TTL is not a limit on the shopper's checkout time — only on the gap between minting the token and landing on the page. 2. **Parameter stripping.** Once consumed, `access_token`, `delivery_address`, `delivery_type`, and `delivery_mode` are removed from the URL via a history replace, so the token does not survive in browser history or in a shared link. 3. **Delivery context applied.** The decoded address, delivery type, and delivery mode are set as the shopper's active selections and persisted to local storage. 4. **Cart load.** The storefront reads `{cart_uuid}` from the path, stores it as the active cart, and fetches it via `GET /api/v5/carts/{uuid}`. 5. **Checkout.** The shopper proceeds through payment, any identity or age verification the store requires, and order creation — all on the Blaze side. If the token has expired or is malformed, the exchange fails and the shopper is sent to the storefront's login page with a `redirect_uri` back to the checkout. They can sign in manually and their cart is still there, so an expired token degrades to an extra login rather than a lost cart. --- ## Complete example ```typescript const STORE_UUID = "e87437f2-3e35-4738-af5e-6307e368255c"; const BASE_URL = "https://ecom-api.staging.blaze.me"; const jsonApi = { "Content-Type": "application/vnd.api+json", Accept: "application/vnd.api+json", "X-Store": STORE_UUID, }; /** * Builds a checkout handoff URL for an already-authenticated shopper. * Call this at the moment the shopper clicks "Checkout" — the token is * only valid for 5 minutes. */ async function createCheckoutHandoff(opts: { sessionJwt: string; cartUuid: string; deliveryType: "pickup" | "delivery"; deliveryMode?: "asap" | "scheduled" | "express"; deliveryAddress?: HandoffAddress; }): Promise { const authed = { ...jsonApi, Authorization: `Bearer ${opts.sessionJwt}` }; const [siteRes, tokenRes] = await Promise.all([ fetch(`${BASE_URL}/api/v1/store/site`, { headers: jsonApi }), fetch(`${BASE_URL}/api/v1/users/me/access_token`, { method: "POST", headers: authed, body: JSON.stringify({ data: { type: "user_access_tokens", attributes: {} }, }), }), ]); if (!siteRes.ok) throw new Error(`Site lookup failed: ${siteRes.status}`); if (!tokenRes.ok) throw new Error(`Token mint failed: ${tokenRes.status}`); const { data: site } = await siteRes.json(); const { data: token } = await tokenRes.json(); const siteUrl: string | null = site.attributes.url; if (!siteUrl) { throw new Error("Store has no published storefront — cannot hand off."); } return buildHandoffUrl({ siteUrl, cartUuid: opts.cartUuid, handoffToken: token.attributes.access_token, deliveryType: opts.deliveryType, deliveryMode: opts.deliveryMode, deliveryAddress: opts.deliveryAddress, utmSource: "partner-storefront", }); } ``` Wire it to a server-side redirect so the token never lands in client-side application state: ```typescript // Express example app.post("/checkout", async (req, res) => { const url = await createCheckoutHandoff({ sessionJwt: req.session.blazeJwt, cartUuid: req.session.cartUuid, deliveryType: "pickup", }); res.redirect(303, url); }); ``` --- ## After the handoff The shopper completes the order on the Blaze storefront and stays there — there is no automatic redirect back to your domain. You can still follow the order from your side. The cart UUID you handed over links to the resulting order: ```typescript // Poll or check on return — uses the shopper's session JWT const orderRes = await fetch( `${BASE_URL}/api/v1/carts/${cartUuid}/order`, { headers: authed } ); ``` For push-based updates instead of polling, see the [Webhooks guide](webhooks.md) — `order.created` and order status events fire regardless of which surface created the order. --- ## Constraints Read these before committing to the handoff as your checkout strategy. **The shopper must be authenticated.** A cart UUID in the checkout path requires a signed-in customer. If the token is missing or expired, the storefront redirects to login rather than offering guest checkout. Guest checkout exists on the Blaze storefront, but not on the cart-UUID entry path — so an anonymous cart built by your storefront cannot be handed off. Authenticate the shopper before you build the cart, not after. **The handoff token is a full user credential.** For its 5-minute life it authenticates as that shopper, and it is not restricted to the cart you are handing over or to a single use. Treat it like a password: - Mint it immediately before the redirect, never in advance - Never log it, store it, or put it in an analytics payload - Only ever transmit it over TLS - Never render it in a link the shopper can copy or share **Cart UUIDs are bearer capabilities.** A cart is fetched by UUID scoped to the store, with no ownership check. Anyone holding a cart UUID can read and modify that cart. Do not expose cart UUIDs in shareable URLs, referrer-visible contexts, or third-party analytics. **Delivery stores may use a different site.** Stores configured with a separate delivery storefront resolve to a different domain for delivery orders. If a store uses one, use that domain when `delivery_type=delivery` — `GET /api/v1/store/site` returns the store's primary site. **Checkout branding is the store's, not yours.** The shopper sees the store's Blaze storefront theme. If a seamless brand transition matters more than the integration savings, build checkout headlessly instead. --- ## Endpoint summary | Step | Endpoint | Auth | |---|---|---| | Sign the shopper in | [`POST /api/v1/auth/login`](/api#tag/Authentication/post/api/v1/auth/login) | None | | Build the cart | [`POST /api/v5/carts`](/api#tag/Cart/post/api/v5/carts) | Shopper JWT | | Find the storefront URL | [`GET /api/v1/store/site`](/api#tag/Store/get/api/v1/store/site) | None | | Mint the handoff token | [`POST /api/v1/users/me/access_token`](/api#tag/User-Profile/post/api/v1/users/me/access_token) | Shopper JWT | | Follow the resulting order | `GET /api/v1/carts/{cart_uuid}/order` | Shopper JWT | --- ## What's Next? - **Build checkout yourself instead**: the [Cart & Checkout guide](cart-and-checkout.md) covers validation, order creation, and status tracking - **Payments**: if you keep checkout headless, the [Payments guide](payments.md) covers all supported providers - **Order updates**: the [Webhooks guide](webhooks.md) covers order and delivery events - **Architecture**: [Building a Headless Storefront](building-a-storefront.md) puts the handoff in context of a full storefront build For request/response format details, see the [General Concepts guide](general-concepts.md). For authentication setup, see the [Authentication guide](authentication.md). # Page: General Concepts # General Concepts ## What you'll learn - How the API uses JSON:API format for requests and responses - How store-scoping works via headers - How identifiers (UUID, slug) work across resources - How pagination, sorting, and filtering work - How errors are structured - Which endpoints require authentication and which don't ## Prerequisites - A store UUID (ask your account manager or check your Blaze ECOM dashboard) - Basic familiarity with REST APIs and JSON --- ## JSON:API Format All API requests and responses follow the [JSON:API specification](https://jsonapi.org/). ### Content Type All requests must include the following headers: ``` Content-Type: application/vnd.api+json Accept: application/vnd.api+json ``` ### Response Structure Every response wraps data in a JSON:API envelope: ```json { "data": { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "type": "store_products", "attributes": { "name": "Blue Dream", "price": 45.0, "category": "flower" }, "relationships": { "brand": { "data": { "id": "brand-uuid", "type": "brands" } } } }, "included": [ { "id": "brand-uuid", "type": "brands", "attributes": { "name": "Premium Farms" } } ] } ``` - **`data`**: The primary resource(s) — a single object or an array - **`included`**: Sideloaded related resources (brands, categories, etc.) - **`meta`**: Pagination metadata (on list endpoints) ### Request Body Structure For `POST`, `PUT`, and `PATCH` requests, wrap your data in a JSON:API envelope: ```json { "data": { "type": "users", "attributes": { "email": "jane@example.com", "password": "securepassword123" } } } ``` For updates, include the resource `id`: ```json { "data": { "id": "user-uuid", "type": "users", "attributes": { "first_name": "Jane" } } } ``` --- ## Store-Scoping Headers Most endpoints are scoped to a specific store. You **must** include the `X-Store` header: | Header | Required | Description | | --------- | -------------------- | ---------------------------------------------------- | | `X-Store` | Yes (most endpoints) | Store UUID — identifies which store's data to access | | `X-Group` | For group endpoints | Group UUID — for multi-store group operations | | `X-Kiosk` | For kiosk endpoints | Kiosk UUID — for kiosk-specific operations | Example: ```bash curl -X GET https://ecom-api.staging.blaze.me/api/v1/store \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" ``` If you omit a required header, the API returns: ```json { "errors": [ { "code": "bad_request", "status": "400", "detail": "Missing X-Store header." } ] } ``` --- ## Identifiers Resources are identified by either: - **UUID** (`id`): Platform-assigned unique identifier (e.g., `a1b2c3d4-e5f6-7890-abcd-ef1234567890`) - **Slug**: URL-friendly human-readable identifier (e.g., `blue-dream-flower`) Many endpoints accept either format in the URL path. For example: ``` GET /api/v1/products/a1b2c3d4-e5f6-7890-abcd-ef1234567890 GET /api/v1/products/blue-dream-flower ``` Both return the same product. --- ## Pagination List endpoints return paginated results using `limit` and `offset` query parameters: | Parameter | Default | Description | | --------- | ------- | ------------------------------------------- | | `limit` | 20 | Maximum number of results to return (1–100) | | `offset` | 0 | Number of results to skip | The response includes pagination metadata in the `meta` object: ```json { "data": [ ... ], "meta": { "total": 150, "limit": 20, "offset": 40 } } ``` Example — page 3 of 20 results per page: ```bash GET /api/v1/products?limit=20&offset=40 ``` --- ## Sorting Use the `order` query parameter to sort results: ```bash GET /api/v1/products?order=price_asc GET /api/v1/products?order=name_desc ``` Available sort options vary by endpoint. Common values: - `name_asc`, `name_desc` - `price_asc`, `price_desc` - `created_at_asc`, `created_at_desc` --- ## Filtering Product listing endpoints support query parameter filters: ```bash GET /api/v1/products?category=flower&brand=premium-farms&min_price=20&max_price=100 ``` Use the **Filters endpoint** ([`GET /api/v1/products/filters`](/api#tag/Search---Filters/get/api/v1/products/filters)) to discover which filter options are available for the current store. This returns the categories, types, brands, tags, and price ranges that have products. --- ## Error Format All errors follow the JSON:API error format: ```json { "errors": [ { "code": "bad_login", "status": "401", "detail": "Wrong Credentials", "source": { "pointer": "/data/attributes/password" }, "extra_info": { "api_error_code": "AUTH_001" } } ] } ``` | Field | Description | | ---------------- | ---------------------------------------------------------------------------- | | `code` | Machine-readable error code (e.g., `bad_login`, `not_found`, `invalid_cart`) | | `status` | HTTP status code as a string | | `detail` | Human-readable error message | | `source.pointer` | JSON pointer to the field that caused the error | | `extra_info` | Additional context (varies by error) | Common HTTP status codes: | Status | Meaning | | ------ | -------------------------------------------------------- | | 400 | Bad Request — invalid parameters, validation failure | | 401 | Unauthorized — missing or invalid JWT token | | 403 | Forbidden — insufficient permissions | | 404 | Not Found — resource does not exist or store is inactive | --- ## Tokenless vs Authenticated Access Endpoints fall into three authentication levels: | Level | Header Required | Use Case | | -------------------------------------------- | ------------------------------------------- | ---------------------------------------------------------------------- | | **Tokenless** (`jwt_optional_authenticated`) | `X-Store` only | Product browsing, store details, filters, categories — no login needed | | **Authenticated** (`jwt_authenticated`) | `X-Store` + `Authorization: Bearer {token}` | Cart, orders, user profile, payments | | **Partner API** (`api_key_authenticated`) | `Authorization: Bearer {api_key}` | Store provisioning, configuration (Partner API) | This means a frontend can display the entire product catalog, filters, and store info without any user authentication. Authentication is only required when the user interacts with their account (cart, checkout, profile). --- ## Access Tiers Beyond authentication, some operations are restricted by **integration tier**. The API Reference marks these inline with badges, so you can tell at a glance what an endpoint needs. | Badge | Meaning | | ----- | ------- | | **Tokenless** | No user token needed — `X-Store` alone is enough | | *(no badge)* | A signed-in customer's JWT is required | | **Certified Partner** | Restricted to certified partners — see below | | **Async** | Does not return its result directly; you poll for it | ### Certified Partner operations Completing a checkout through the API — creating the order and taking payment yourself — is restricted to **certified partners**. 7 operations carry the badge: | Operation | What it does | | --------- | ------------ | | `PATCH /api/v1/store/payments/{service}/sources/{id}` | Update payment source | | `POST /api/v1/store/payments/{service}/orders/{uuid}/pay` | Pay for order | | `POST /api/v1/store/payments/{service}/sessions` | Create payment session | | `POST /api/v1/store/payments/{service}/sources` | Add payment source | | `POST /api/v1/store/payments/{service}/tip` | Tip | | `POST /api/v4/orders` | Create order (checkout) | | `POST /api/v5/orders` | Submit cart for asynchronous checkout | Everything else, including the **entire cart lifecycle**, is open to any integration. That is deliberate: it means an uncertified storefront can still build a full cart and complete the purchase via the [Checkout Handoff](checkout-handoff.md), which needs no certification. If you are not certified, use the handoff. To apply, contact your Blaze account manager or [ecomsupport@blaze.me](mailto:ecomsupport@blaze.me). # Page: Payments # Payments The complete payment flow — from discovering payment options through paying for an order, managing sources, tipping, and promotions. > **🔒 Certified Partners only.** Creating orders and processing payments through the API is > restricted to certified partners. If you are not certified, use the > [Checkout Handoff](checkout-handoff.md) instead — it needs no certification and hands the shopper > to Blaze-hosted checkout with the cart intact. To apply for certification, contact your Blaze > account manager or [ecomsupport@blaze.me](mailto:ecomsupport@blaze.me). > Full list of restricted operations: [Access Tiers](general-concepts.md#access-tiers). ## What you'll learn - How the payment flow works end-to-end: payment options → customer token → source → session → pay - How payment providers are abstracted behind the `{service}` parameter - How to manage payment sources (list, add, update, delete) - How to create payment sessions and pay for an order - How tipping and payment promotions work - v1 vs v2 source listing differences - Common payment error scenarios and their error codes ## Prerequisites - A **Store UUID** (staging: `e87437f2-3e35-4738-af5e-6307e368255c`) - A valid **JWT token** — see the [Authentication guide](authentication.md) - An **order** created via cart checkout — see the [Cart & Checkout guide](cart-and-checkout.md) All payment endpoints use the `jwt_authenticated` pipeline — a valid token is required. --- ## Payment Flow Overview The typical payment flow follows these steps: 1. **Get payment options** — discover which payment providers the store supports 2. **Get customer token** — obtain a provider-specific token for the customer (provider-dependent) 3. **Create/register a customer** — upsert the customer record in the external provider 4. **Add a payment source** — register a payment method (card, bank account, etc.) 5. **Create a payment session** — initialize a payment session with the provider (provider-dependent) 6. **Pay for an order** — charge the order using the payment source Not all steps are required for every provider. Some providers (e.g. Stronghold) need a customer token step, while others (e.g. Adyen) need a session step. The specific requirements depend on the provider. --- ## Payment Providers All payment endpoints use a `{service}` path parameter that identifies the provider. The API abstracts provider-specific logic behind a common interface. Supported `{service}` values: - `adyen` — Adyen (card payments, 3DS) - `aeropay` — AeroPay (ACH/bank transfers) - `stronghold` — Stronghold (ACH payments) - `moneris` — Moneris (card payments, 3DS) - `swifter` — Swifter (digital payments) - `ledgergreen` — LedgerGreen (lending) - `merrco` — Merrco (card payments) - `spence` — Spence (digital payments) - `greenbax` — Greenbax (digital payments) - `blazepay_widget` — BlazePay Widget (embedded payment widget) The same endpoint paths work across all providers — only the `{service}` segment changes. --- ## Step 1: Get Payment Options Discover which payment methods are configured for the store. ### `GET /api/v1/store/payment-options` ```bash curl -X GET https://ecom-api.staging.blaze.me/api/v1/store/payment-options \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" ``` This endpoint uses the `jwt_optional_authenticated` pipeline — no token is strictly required. It returns the list of payment options enabled for the store, including which external services are available. --- ## Step 2: Get Payment Customer Token Some providers require a customer-specific token before adding sources or making payments. ### `GET /api/v1/store/payments/{service}/token` **Auth**: `jwt_authenticated` #### cURL ```bash curl -X GET https://ecom-api.staging.blaze.me/api/v1/store/payments/stronghold/token \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" \ -H "Authorization: Bearer YOUR_TOKEN" ``` #### Response ```json { "data": { "id": null, "type": "payments_token", "attributes": { "mfa": false, "token": "pay_tok_abc123def456...", "expiry": "2026-06-01T00:00:00Z" } } } ``` #### Error codes - **`bad_request`** (400) — Invalid payment option. Returned when `{service}` is not a recognized provider. - **`payment_customer_not_found`** (400) — The user does not have a customer record with this provider yet. --- ## Step 3: Create / Register Payment Customer Register or update the user's customer record with an external payment provider. This links the cart to the provider so the payment can be processed. ### `PUT /api/v1/store/payments/{service}/customers` **Auth**: `jwt_authenticated` #### cURL ```bash curl -X PUT https://ecom-api.staging.blaze.me/api/v1/store/payments/adyen/customers \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" \ -H "Authorization: Bearer YOUR_TOKEN" \ -d '{ "data": { "type": "payment_customers", "id": "", "attributes": {}, "relationships": { "cart": { "data": { "type": "carts", "id": "CART_UUID" } } } } }' ``` #### JavaScript (fetch) ```javascript const STORE_UUID = "e87437f2-3e35-4738-af5e-6307e368255c"; const BASE_URL = "https://ecom-api.staging.blaze.me"; const response = await fetch( `${BASE_URL}/api/v1/store/payments/adyen/customers`, { method: "PUT", headers: { "Content-Type": "application/vnd.api+json", Accept: "application/vnd.api+json", "X-Store": STORE_UUID, Authorization: `Bearer ${token}`, }, body: JSON.stringify({ data: { type: "payment_customers", id: "", attributes: {}, relationships: { cart: { data: { type: "carts", id: cartId }, }, }, }, }), }, ); const { data } = await response.json(); console.log(`Customer registered: ${data.id}`); ``` #### Response ```json { "data": { "id": "customer-uuid", "type": "payment_customers", "attributes": { "first_name": "Jane", "last_name": "Doe", "email": "jane@example.com", "phone_number": "+15551234567", "external_service": "adyen", "external_id": "ext_cust_abc123", "is_confirmed": true, "extra_content": null } } } ``` #### Error codes - **`bad_request`** (400) — Invalid payment option. Returned when `{service}` is not recognized. - **`no_match_for_payment_customer`** (400) — Customer does not match the one from the external payment source data. - **`cart_data_required_for_customer`** (400) — Additional shopping cart data is required for this payment processor. Returned when the cart relationship is missing and the provider requires it. --- ## Step 4: Manage Payment Sources Payment sources represent saved payment methods (credit cards, bank accounts, etc.). ### List Sources (v1) #### `GET /api/v1/store/payments/{service}/sources` Returns sources for a specific provider. **Auth**: `jwt_authenticated` #### cURL ```bash curl -X GET https://ecom-api.staging.blaze.me/api/v1/store/payments/adyen/sources \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" \ -H "Authorization: Bearer YOUR_TOKEN" ``` #### JavaScript (fetch) ```javascript const response = await fetch( `${BASE_URL}/api/v1/store/payments/adyen/sources`, { headers: { "Content-Type": "application/vnd.api+json", Accept: "application/vnd.api+json", "X-Store": STORE_UUID, Authorization: `Bearer ${token}`, }, }, ); const { data } = await response.json(); console.log(`Found ${Array.isArray(data) ? data.length : 1} source(s)`); ``` #### Response ```json { "data": [ { "id": "source-uuid", "type": "payment_sources", "attributes": { "label": "Visa", "label_display": "Visa •••• 4242", "active": true, "external_service": "adyen", "external_id": "ext_src_abc123", "type": "credit_card", "provider": "visa", "provider_display": "Visa", "mask": "4242", "account_type": null, "is_default": true, "expiry_date": "12/2027", "cardholder_name": "Jane Doe", "is_expired": false } } ] } ``` ### List Sources (v2) — All Providers #### `GET /api/v2/store/payments/sources` Returns sources grouped by provider in a single request. No `{service}` parameter needed. **Auth**: `jwt_authenticated` ```bash curl -X GET https://ecom-api.staging.blaze.me/api/v2/store/payments/sources \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" \ -H "Authorization: Bearer YOUR_TOKEN" ``` #### Response ```json { "data": { "id": "all_payment_sources", "type": "all_payment_sources", "attributes": { "adyen": [ { "id": "source-uuid", "label": "Visa", "label_display": "Visa •••• 4242", "active": true, "external_service": "adyen", "type": "credit_card", "mask": "4242", "is_default": true, "is_expired": false } ], "stronghold": [] } } } ``` > **Tip**: Use v2 when you need to show all saved payment methods across providers. Use v1 when working with a single provider. --- ### Add a Payment Source #### `POST /api/v1/store/payments/{service}/sources` **Auth**: `jwt_authenticated` The exact attributes required vary by provider. Common fields include `token`, `external_id`, `type`, and `is_default`. #### cURL ```bash curl -X POST https://ecom-api.staging.blaze.me/api/v1/store/payments/adyen/sources \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" \ -H "Authorization: Bearer YOUR_TOKEN" \ -d '{ "data": { "type": "payment_sources", "attributes": { "token": "tok_abc123...", "type": "credit_card", "is_default": true } } }' ``` #### JavaScript (fetch) ```javascript const response = await fetch( `${BASE_URL}/api/v1/store/payments/adyen/sources`, { method: "POST", headers: { "Content-Type": "application/vnd.api+json", Accept: "application/vnd.api+json", "X-Store": STORE_UUID, Authorization: `Bearer ${token}`, }, body: JSON.stringify({ data: { type: "payment_sources", attributes: { token: "tok_abc123...", type: "credit_card", is_default: true, }, }, }), }, ); const { data } = await response.json(); console.log(`Source added: ${data.id}`); ``` #### Response ```json { "data": { "id": "source-uuid", "type": "payment_sources", "attributes": { "label": "Visa", "label_display": "Visa •••• 4242", "active": true, "external_service": "adyen", "external_id": "ext_src_abc123", "type": "credit_card", "provider": "visa", "provider_display": "Visa", "mask": "4242", "account_type": null, "is_default": true, "expiry_date": "12/2027", "cardholder_name": "Jane Doe", "is_expired": false } } } ``` #### Error codes - **`bad_request`** (400) — Invalid payment option. The `{service}` value is not recognized. - **`invalid_payment_source`** (400) — Some required fields are missing for adding the payment source. Check the provider-specific requirements. - **`guest_cannot_add_source`** (400) — Cannot add sources in guest checkout. The user must be authenticated. - **`expired_payment_token`** (400) — The payment card has expired. The token provided refers to an expired card. - **`invalid_payment_token`** (400) — The payment card is invalid. The token could not be validated by the provider. --- ### Update a Payment Source #### `PATCH /api/v1/store/payments/{service}/sources/{id}` Used to update source properties, such as marking a source as the default. **Auth**: `jwt_authenticated` ```bash curl -X PATCH https://ecom-api.staging.blaze.me/api/v1/store/payments/adyen/sources/SOURCE_ID \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" \ -H "Authorization: Bearer YOUR_TOKEN" \ -d '{ "data": { "id": "SOURCE_ID", "type": "payment_sources", "attributes": { "is_default": true } } }' ``` #### Error codes - **`payment_source_not_found`** (400) — The source ID does not match any source for this user and service. --- ### Delete a Payment Source #### `DELETE /api/v1/store/payments/{service}/sources/{id}` **Auth**: `jwt_authenticated` ```bash curl -X DELETE https://ecom-api.staging.blaze.me/api/v1/store/payments/adyen/sources/SOURCE_ID \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" \ -H "Authorization: Bearer YOUR_TOKEN" ``` #### Error codes - **`bad_request`** (400) — Invalid payment option. - **`payment_source_not_found`** (400) — The source does not exist for this user and provider. --- ## Step 5: Create a Payment Session Some providers (e.g. `blazepay_widget`, `adyen`) require creating a payment session before charging. The session ties together the store, cart, and user in the external provider. ### `POST /api/v1/store/payments/{service}/sessions` **Auth**: `jwt_optional_authenticated` — for guest checkout, pass user details in the attributes instead. ```bash curl -X POST https://ecom-api.staging.blaze.me/api/v1/store/payments/blazepay_widget/sessions \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" \ -H "Authorization: Bearer YOUR_TOKEN" \ -d '{ "data": { "type": "payment_sessions", "attributes": {}, "relationships": { "cart": { "data": { "type": "carts", "id": "CART_UUID" } } } } }' ``` For guest checkout (no JWT), include user details in attributes: ```json { "data": { "type": "payment_sessions", "attributes": { "first_name": "Jane", "last_name": "Doe", "email": "jane@example.com", "phone_number": "+15551234567" }, "relationships": { "cart": { "data": { "type": "carts", "id": "CART_UUID" } } } } } ``` #### Response (201 Created) ```json { "data": { "id": "session-uuid", "type": "payment_session", "attributes": { "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." } } } ``` #### Error codes - **`bad_request`** (400) — Invalid payment option or invalid request parameters. - **`not_found_payment_config`** (404) — Payment configuration was not found for the store. The provider is not configured. - **`not_found`** (404) — The cart UUID does not match any existing cart. --- ## Step 6: Pay for an Order After the order is created (via the [Cart & Checkout](cart-and-checkout.md) flow), charge it using the payment source. ### `POST /api/v1/store/payments/{service}/orders/{uuid}/pay` **Auth**: `jwt_authenticated` #### cURL ```bash curl -X POST https://ecom-api.staging.blaze.me/api/v1/store/payments/adyen/orders/ORDER_UUID/pay \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" \ -H "Authorization: Bearer YOUR_TOKEN" \ -d '{ "data": { "type": "payments_charge", "attributes": { "source_id": "SOURCE_UUID", "postal_code": "90210" } } }' ``` #### JavaScript (fetch) ```javascript const response = await fetch( `${BASE_URL}/api/v1/store/payments/adyen/orders/${orderUuid}/pay`, { method: "POST", headers: { "Content-Type": "application/vnd.api+json", Accept: "application/vnd.api+json", "X-Store": STORE_UUID, Authorization: `Bearer ${token}`, }, body: JSON.stringify({ data: { type: "payments_charge", attributes: { source_id: sourceId, postal_code: "90210", }, }, }), }, ); const result = await response.json(); if (response.ok) { console.log(`Payment status: ${result.data.attributes.status}`); } else { console.error("Payment failed:", result.errors); } ``` The attributes vary by provider, but common fields include: - **`source_id`** — UUID of the payment source to charge - **`postal_code`** — billing postal code (required by some providers) - **`payment_session_id`** — session ID from the session creation step (for providers that use sessions) - **`payment_session_payment_id`** — external payment ID from the provider widget #### Response ```json { "data": { "id": "charge-uuid", "type": "payment_charges", "attributes": { "status": "authorized", "type": "charge", "fee": { "amount": 0, "currency": "USD" }, "convenience_fee": { "amount": 0, "currency": "USD" }, "amount_without_convenience_fee": { "amount": 4928, "currency": "USD" }, "description": null, "external_id": "ext_charge_abc123", "external_service": "adyen", "amount": { "amount": 4928, "currency": "USD" }, "external_url": null, "created_at": "2026-05-30T20:00:00Z", "authorized_at": "2026-05-30T20:00:01Z", "captured_at": null, "updated_at": "2026-05-30T20:00:01Z", "guest_payment_source": null, "is_3ds_authenticated": false, "service_external_id": null, "service_external_url": null, "credit": null }, "relationships": { "payment_customer": { "data": { "id": "customer-uuid", "type": "payment_customers" } }, "payment_source": { "data": { "id": "source-uuid", "type": "payment_sources" } }, "payment_tip": { "data": null } } }, "included": [ { "id": "customer-uuid", "type": "payment_customers", "attributes": { "first_name": "Jane", "last_name": "Doe", "email": "jane@example.com", "external_service": "adyen", "external_id": "ext_cust_abc123" } }, { "id": "source-uuid", "type": "payment_sources", "attributes": { "label": "Visa", "mask": "4242", "is_default": true, "is_expired": false } } ] } ``` **Charge statuses:** - `authorized` — payment authorized, pending capture - `captured` — payment captured (funds collected) - `failed` — payment failed #### Error codes - **`order_already_paid`** (400) — This order has already been paid. - **`order_user_mismatch`** (400) — The order belongs to another user. The JWT user must match the order owner. - **`payment_failed`** (400) — Payment authorization failed. The provider declined the charge. The error `detail` may include a provider-specific message. - **`charge_not_authorized`** (400) — Payment not yet authorized. Occurs when trying to capture a payment that hasn't been authorized. - **`charge_canceled`** (400) — Payment canceled by the provider. - **`missing_payment_source`** (400) — Online payment requires a payment source. The `source_id` is missing or invalid. - **`missing_payment_source_identifier`** (400) — Online payment requires a payment source ID or token. - **`expired_payment_token`** (400) — Online payment card has expired. - **`invalid_payment_token`** (400) — Online payment card is invalid. - **`payment_source_not_found`** (400) — Online payment source not found. - **`missing_payment_postal_code`** (400) — Online payment requires a postal code (provider-dependent). - **`missing_payment_cres`** (400) — Invalid challenge result (cres). Occurs during 3DS authentication flow. - **`missing_payment_cavv`** (400) — Can't authenticate cardholder. 3DS verification failed. - **`missing_cardholder_name`** (400) — Missing cardholder name. - **`missing_billing_address`** (400) — Billing address is required (provider-dependent). - **`not_found`** (404) — Order not found for the given UUID. --- ## Step 7: External Auth (3DS) Some providers (e.g. Adyen, Moneris) require 3D Secure authentication. This endpoint handles the challenge/response flow. ### `POST /api/v1/store/payments/{service}/sources/external-auth` **Auth**: `jwt_optional_authenticated` ```bash curl -X POST https://ecom-api.staging.blaze.me/api/v1/store/payments/adyen/sources/external-auth \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" \ -H "Authorization: Bearer YOUR_TOKEN" \ -d '{ "data": { "type": "payment_external_auth", "id": "", "attributes": { "challenge_screen_width": 600, "challenge_screen_height": 400 }, "relationships": { "cart": { "data": { "type": "carts", "id": "CART_UUID" } } } } }' ``` For existing sources, use the path with source ID: ### `POST /api/v1/store/payments/{service}/sources/{source_id}/external-auth` This variant attaches the auth verification to a specific existing payment source. --- ## Tipping Add a tip to a paid order. Tips are processed through the same payment provider. ### `POST /api/v1/store/payments/{service}/tip` **Auth**: `jwt_authenticated` ```bash curl -X POST https://ecom-api.staging.blaze.me/api/v1/store/payments/adyen/tip \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" \ -H "Authorization: Bearer YOUR_TOKEN" \ -d '{ "data": { "type": "payment_sources", "attributes": { "external_id": "charge-external-id", "percentage": 15 } } }' ``` #### Response ```json { "data": { "id": "tip-uuid", "type": "payment_tips", "attributes": { "status": "captured", "beneficiary_name": "Store Name", "fee": { "amount": 0, "currency": "USD" }, "amount": { "amount": 740, "currency": "USD" }, "external_id": "ext_tip_abc123", "external_service": "adyen", "created_at": "2026-05-30T20:05:00Z", "authorized_at": "2026-05-30T20:05:00Z", "captured_at": "2026-05-30T20:05:01Z", "percentage": 15 }, "relationships": { "payment_source": { "data": { "id": "source-uuid", "type": "payment_sources" } } } } } ``` #### Error codes - **`tips_not_allowed`** (400) — Tips are not allowed for the selected payment option. - **`already_tipped`** (400) — Tip already processed. Each order can only be tipped once. - **`bad_request`** (400) — Invalid payment option. --- ## Payment Promotions Some payment providers offer their own promotional discounts (e.g., "Save $5 when you pay with Aeropay"). ### List Promotions #### `GET /api/v1/store/payments/{service}/promotions` **Auth**: `jwt_authenticated` Supports pagination with `limit` and `offset` query parameters. ```bash curl -X GET "https://ecom-api.staging.blaze.me/api/v1/store/payments/aeropay/promotions?limit=10&offset=0" \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" \ -H "Authorization: Bearer YOUR_TOKEN" ``` #### Response ```json { "data": [ { "id": "promo-external-id", "type": "payment_promotions", "attributes": { "name": "Save $5 with AeroPay", "title": "Save $5 with AeroPay", "description": "You'll save $5 in this payment. Valid until Jun 30, 2026, 11:59 PM", "start_date": "2026-01-01T00:00:00Z", "end_date": "2026-06-30T23:59:59Z", "promotion_type": "discount", "benefit_type": "fixed", "immediate_use": true, "fixed_amount": { "amount": 500, "currency": "USD" }, "first_purchase_only": false, "single_use": false, "min_charge_amount": null, "disabled_on": null, "savings": { "amount": 500, "currency": "USD" }, "savings_display": "You're saving $5 OFF with Aeropay" } } ] } ``` #### Error codes - **`promotions_not_allowed`** (400) — Promotions are not allowed for the selected payment option. The store's payment option has `allows_promotions` disabled. - **`bad_request`** (400) — Invalid payment option. - **`payment_option_not_found`** (400) — The payment option for this service was not found in the store configuration. --- ### Check Redeemable Promotions Check which promotions can be redeemed for a given charge amount. #### `POST /api/v1/store/payments/{service}/promotions/redeemable` **Auth**: `jwt_authenticated` ```bash curl -X POST https://ecom-api.staging.blaze.me/api/v1/store/payments/aeropay/promotions/redeemable \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" \ -H "Authorization: Bearer YOUR_TOKEN" \ -d '{ "data": { "type": "payment_redeemable_promotions", "id": "", "attributes": { "charge_amount": 4928 } } }' ``` Returns the same promotion format as the list endpoint, filtered to promotions that are redeemable for the specified amount. --- ## Order Payment Charges View the payment charges associated with an order. ### `GET /api/v1/store/orders/{uuid}/payment/charges` **Auth**: `jwt_authenticated` ```bash curl -X GET https://ecom-api.staging.blaze.me/api/v1/store/orders/ORDER_UUID/payment/charges \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" \ -H "Authorization: Bearer YOUR_TOKEN" ``` Returns a list of `payment_charges` for the order, including the related customer, source, and tip. --- ## Payment Configuration Get the provider configuration for the store. This returns settings like supported card types, fee structures, etc. ### `GET /api/v1/store/payments/{service}/configuration` **Auth**: `jwt_optional_authenticated` ```bash curl -X GET https://ecom-api.staging.blaze.me/api/v1/store/payments/adyen/configuration \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" ``` ### `GET /api/v1/store/integrations/payments/{service}/configuration` Alternative path — returns the same data. This is the integrations-namespaced version. --- ## Common Error Scenarios All payment errors follow the standard JSON:API error format: ```json { "errors": [ { "code": "error_code", "status": "400", "detail": "Human-readable error message", "source": { "pointer": "/data/attributes/field_name" } } ] } ``` ### Payment Processing Errors - **`payment_failed`** (400) — Payment authorization or capture failed. The `detail` field may include a provider-specific reason. - **`charge_not_authorized`** (400) — Payment not yet authorized. Attempting an operation that requires prior authorization. - **`charge_canceled`** (400) — Payment canceled by the provider. - **`order_already_paid`** (400) — The order has already been paid. Cannot charge the same order twice. - **`order_already_completed`** (400) — The order has already been completed. ### Source & Token Errors - **`missing_payment_source`** (400) — No payment source provided for an online payment. - **`missing_payment_source_identifier`** (400) — Neither a source ID nor a token was provided. - **`payment_source_not_found`** (400) — The referenced payment source does not exist. - **`invalid_payment_source`** (400) — Required fields are missing for adding a payment source. - **`expired_payment_token`** (400) — The payment card has expired. - **`invalid_payment_token`** (400) — The payment card is invalid. - **`guest_cannot_add_source`** (400) — Cannot add payment sources during guest checkout. - **`multiple_bank_accounts`** (400) — Only a single active bank account is supported. ### Authentication & Authorization Errors - **`missing_payment_postal_code`** (400) — Online payment requires a postal code. - **`missing_payment_cres`** (400) — Invalid 3DS challenge result. - **`missing_payment_cavv`** (400) — Cannot authenticate cardholder (3DS failure). - **`missing_cardholder_name`** (400) — Missing cardholder name. - **`missing_billing_address`** (400) — Billing address is required. - **`missing_customer_signature`** (400) — Missing customer signature (provider-specific). - **`missing_customer_auth_key`** (400) — Missing customer auth key (provider-specific). ### Configuration Errors - **`not_found_payment_config`** (404) — Payment configuration not found for the store. - **`invalid_payment_config`** (400) — External payment configuration is invalid. - **`inactive_service_config`** (400) — The provider configuration is inactive. - **`missing_service_config`** (400) — The provider configuration is missing. - **`no_payment_method_available`** (400) — No payment method available for this store. ### Order Errors - **`order_user_mismatch`** (400) — The order belongs to a different user than the authenticated one. --- ## Complete Payment Flow Example Here's the typical end-to-end sequence: 1. **Get payment options** → [`GET /api/v1/store/payment-options`](/api#tag/Store/get/api/v1/store/payment-options) 2. **Register customer** → `PUT /api/v1/store/payments/{service}/customers` (with cart relationship) 3. **Get customer token** → [`GET /api/v1/store/payments/{service}/token`](/api#tag/Payments/get/api/v1/store/payments/{service}/token) (if provider requires it) 4. **Add payment source** → [`POST /api/v1/store/payments/{service}/sources`](/api#tag/Payments/post/api/v1/store/payments/{service}/sources) 5. **Create payment session** → [`POST /api/v1/store/payments/{service}/sessions`](/api#tag/Payments/post/api/v1/store/payments/{service}/sessions) (if provider requires it) 6. **Handle 3DS** → `POST /api/v1/store/payments/{service}/sources/external-auth` (if required) 7. **Pay for order** → [`POST /api/v1/store/payments/{service}/orders/{uuid}/pay`](/api#tag/Payments/post/api/v1/store/payments/{service}/orders/{uuid}/pay) 8. **Add tip** → [`POST /api/v1/store/payments/{service}/tip`](/api#tag/Payments/post/api/v1/store/payments/{service}/tip) (optional) 9. **View charges** → `GET /api/v1/store/orders/{uuid}/payment/charges` --- ## What's Next? - **Cart & Checkout**: Build the cart before paying — see the [Cart & Checkout guide](cart-and-checkout.md) - **Authentication**: Set up JWT tokens — see the [Authentication guide](authentication.md) - **Store & Delivery**: Configure delivery options — see the [Store & Delivery guide](store-and-delivery.md) For request/response format details, see the [General Concepts guide](general-concepts.md). # Page: Products Catalog # Products & Catalog Browse, filter, sort, and search the product catalog. Build dynamic filter UIs and display rich product detail pages. ## What you'll learn - How to list products with pagination - How to filter products by category, type, brand, tags, price range, and potency - How to use the filters meta-endpoint to build dynamic filter UIs - How to search products by keyword - How to sort products - How to fetch product details (v1 vs v2) - How to navigate brands, categories, types, and tags - How to display showcased product groups - How to interpret product data: weight_prices, unit_prices, variants, THC/CBD, potency ## Prerequisites - A **Store UUID** (staging: `e87437f2-3e35-4738-af5e-6307e368255c`) - cURL or any HTTP client - No authentication required — all catalog endpoints are tokenless (`jwt_optional_authenticated`) --- ## Browsing Products ### List Products Fetch a paginated list of products for the store. **Endpoint:** `GET /api/v1/products` **Query Parameters:** | Parameter | Default | Description | | --------- | ------- | --------------------------------------------- | | `limit` | `10` | Number of products to return | | `offset` | `0` | Number of products to skip | | `order` | `date` | Sort order (see [Sorting](#sorting-products)) | #### cURL ```bash curl -X GET "https://ecom-api.staging.blaze.me/api/v1/products?limit=20&offset=0&order=brand" \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" ``` #### JavaScript (fetch) ```javascript const STORE_UUID = "e87437f2-3e35-4738-af5e-6307e368255c"; const BASE_URL = "https://ecom-api.staging.blaze.me"; const params = new URLSearchParams({ limit: "20", offset: "0", order: "brand", }); const response = await fetch(`${BASE_URL}/api/v1/products?${params}`, { headers: { "Content-Type": "application/vnd.api+json", Accept: "application/vnd.api+json", "X-Store": STORE_UUID, }, }); const { data, meta, included } = await response.json(); console.log(`Showing ${data.length} of ${meta.total_count} products`); ``` #### Response Shape ```json { "data": [ { "id": "product-uuid", "type": "store_products", "attributes": { "name": "Blue Dream", "slug": "blue-dream", "sku": "BD-001", "type": "Flower", "strain": "Hybrid", "flower_type": "Hybrid", "description": "A balanced hybrid...", "in_stock": true, "is_promoted": false, "unit_price": 45.00, "discount": null, "thc": 22.5, "cbd": 0.1, "min_cbd": null, "potency": "22.5% THC", "cannabis_weight": "3.5g", "main_image": "https://...", "weight_prices": [ ... ], "unit_prices": [ ... ], "size": { "amount": 3.5, "units": "g" }, "terpenoids": [ ... ], "composition": null, "external_id": "pos-123" }, "relationships": { "product_brands": { "data": [{ "id": "brand-uuid", "type": "product_brands" }] }, "product_categories": { "data": [{ "id": "cat-uuid", "type": "product_categories" }] }, "product_images": { "data": [ ... ] }, "global_brands": { "data": [ ... ] }, "tags": { "data": [ ... ] } } } ], "included": [ { "id": "brand-uuid", "type": "product_brands", "attributes": { "name": "Premium Farms" } }, { "id": "cat-uuid", "type": "product_categories", "attributes": { "name": "Flower" } } ], "meta": { "offset": 0, "limit": 20, "total_count": 150 } } ``` ### Pagination Use `limit` and `offset` to paginate through results: ``` GET /api/v1/products?limit=20&offset=0 → Page 1 GET /api/v1/products?limit=20&offset=20 → Page 2 GET /api/v1/products?limit=20&offset=40 → Page 3 ``` The `meta.total_count` field tells you the total number of matching products, so you can calculate the total number of pages. --- ## Filtering Products Filter products by passing query parameters to `GET /api/v1/products`. **Filter Parameters:** | Parameter | Type | Description | | ---------------------- | ------- | ---------------------------------------------------------------------------------------- | | `category` | string | Category slug or ID | | `category_exclude` | string | Category slug or ID to exclude | | `category_external_id` | string | External category ID (from POS) | | `type` | string | Product type (e.g., `Flower`, `Edible`, `Concentrate`) | | `brand` | string | Brand slug or ID | | `brand_external_id` | string | External brand ID (from POS) | | `tag` | string | Tag name | | `min_price` | number | Minimum price | | `max_price` | number | Maximum price | | `min_thc` | number | Minimum THC content | | `max_thc` | number | Maximum THC content | | `min_cbd` | number | Minimum CBD content | | `max_cbd` | number | Maximum CBD content | | `weight` | string | Weight filter | | `on_sale` | boolean | Only products on sale (`true`) | | `q` | string | Search query (keyword search) | | `delivery_type` | string | One of: `all`, `pickup`, `express`, `scheduled_delivery`, `valid_for_sale_only`, `kiosk` | | `excludes` | string | Product IDs to exclude | ### Example: Filter by Category + Price Range #### cURL ```bash curl -X GET "https://ecom-api.staging.blaze.me/api/v1/products?category=flower&min_price=20&max_price=80&limit=20" \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" ``` #### JavaScript (fetch) ```javascript const params = new URLSearchParams({ category: "flower", min_price: "20", max_price: "80", limit: "20", }); const response = await fetch(`${BASE_URL}/api/v1/products?${params}`, { headers: { "Content-Type": "application/vnd.api+json", Accept: "application/vnd.api+json", "X-Store": STORE_UUID, }, }); const { data, meta } = await response.json(); console.log(`${meta.total_count} flower products between $20–$80`); ``` ### Combining Filters All filter parameters can be combined: ```bash # Hybrid flowers, $30–$60, THC above 20%, on sale curl -X GET "https://ecom-api.staging.blaze.me/api/v1/products?category=flower&type=Hybrid&min_price=30&max_price=60&min_thc=20&on_sale=true&limit=10" \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" ``` --- ## Available Filters Endpoint Use the filters endpoint to discover which filter options are available for the current store. This powers dynamic filter UIs — only showing categories, types, brands, and tags that actually have products. ### v2 Filters (Recommended) **Endpoint:** `GET /api/v2/products/filters` The v2 endpoint returns filter options as JSON:API relationships with `included` resources, making it easier to build UI components. #### cURL ```bash curl -X GET "https://ecom-api.staging.blaze.me/api/v2/products/filters" \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" ``` #### JavaScript (fetch) ```javascript const response = await fetch(`${BASE_URL}/api/v2/products/filters`, { headers: { "Content-Type": "application/vnd.api+json", Accept: "application/vnd.api+json", "X-Store": STORE_UUID, }, }); const { data, included } = await response.json(); // Extract filter options from included resources const categories = included.filter((r) => r.type === "product_categories"); const brands = included.filter((r) => r.type === "product_brands"); const types = included.filter((r) => r.type === "product_types"); const tags = included.filter((r) => r.type === "tags"); console.log(`${categories.length} categories, ${brands.length} brands`); ``` #### Response Shape (v2) The v2 filters response uses JSON:API relationships to reference the filter options, which are included as sideloaded resources: ```json { "data": { "type": "filters", "relationships": { "categories": { "data": [{ "id": "cat-1", "type": "product_categories" }] }, "types": { "data": [{ "id": "Flower", "type": "product_types" }] }, "brands": { "data": [{ "id": "brand-1", "type": "product_brands" }] }, "tags": { "data": [{ "id": "tag-1", "type": "tags" }] } }, "attributes": { "price_ranges": { "min": 5.0, "max": 500.0 }, "thc_ranges": { "min": 0, "max": 35, "unit": "%" }, "cbd_ranges": { "min": 0, "max": 25, "unit": "%" }, "weights": ["1g", "3.5g", "7g", "14g", "28g"], "on_sale": { "count": 12 } } }, "included": [ { "id": "cat-1", "type": "product_categories", "attributes": { "name": "Flower", "slug": "flower", "count": 45 } }, { "id": "Flower", "type": "product_types", "attributes": { "name": "Flower", "count": 45 } } ] } ``` ### v1 Filters **Endpoint:** `GET /api/v1/products/filters` The v1 endpoint returns filter data in a simpler flat structure. Consider using v2 for new integrations. ### Scoped Filters You can pass the same filter parameters to the filters endpoint to get filters scoped to a subset of products. For example, to get available brands within the "Flower" category: ```bash GET /api/v1/products/filters?category=flower ``` This is useful for building cascading filter UIs where selecting one filter updates the available options in other filters. --- ## Searching Products Use the `q` parameter to search products by keyword: ```bash curl -X GET "https://ecom-api.staging.blaze.me/api/v1/products?q=blue+dream&limit=10" \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" ``` The search matches against product name, description, brand name, and other text fields. You can combine `q` with other filters: ```bash # Search for "dream" in the Flower category GET /api/v1/products?q=dream&category=flower&limit=10 ``` --- ## Sorting Products Use the `order` query parameter. Prefix with `-` for descending order. **Available Sort Options:** | Value | Description | | -------- | ----------------------- | | `date` | Date added (default) | | `brand` | Brand name A→Z | | `-brand` | Brand name Z→A | | `price` | Price low→high | | `-price` | Price high→low | | `name` | Product name A→Z | | `-name` | Product name Z→A | | `thc` | THC content low→high | | `-thc` | THC content high→low | | `cbd` | CBD content low→high | | `-cbd` | CBD content high→low | | `size` | Package size ascending | | `-size` | Package size descending | Example — sort by price, cheapest first: ```bash GET /api/v1/products?order=price&limit=20 ``` Example — sort by THC content, highest first: ```bash GET /api/v1/products?order=-thc&limit=20 ``` --- ## Product Detail Fetch a single product by its UUID or slug. ### v2 (Recommended) **Endpoint:** `GET /api/v2/products/{id}` The v2 endpoint returns enriched data with complete variant information and inventory details. #### cURL ```bash curl -X GET "https://ecom-api.staging.blaze.me/api/v2/products/blue-dream" \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" ``` #### JavaScript (fetch) ```javascript const productSlug = "blue-dream"; const response = await fetch(`${BASE_URL}/api/v2/products/${productSlug}`, { headers: { "Content-Type": "application/vnd.api+json", Accept: "application/vnd.api+json", "X-Store": STORE_UUID, }, }); const { data, included } = await response.json(); console.log(data.attributes.name); console.log(`THC: ${data.attributes.thc}%`); console.log(`Price: $${data.attributes.unit_price}`); // Get brand from included resources const brand = included?.find((r) => r.type === "product_brands"); console.log(`Brand: ${brand?.attributes.name}`); ``` ### v1 **Endpoint:** `GET /api/v1/products/{id}` The v1 endpoint returns the same core product data but with a simpler structure. Use v2 for new integrations. ### v1 vs v2 Differences | Feature | v1 | v2 | | ------------------------------------------ | --- | --- | | Basic product data | ✓ | ✓ | | Relationships (brands, categories, images) | ✓ | ✓ | | Enriched variant/inventory data | — | ✓ | --- ## Brands ### List All Brands **Endpoint:** `GET /api/v1/products/brands` Returns all brands that have products in the store. ```bash curl -X GET "https://ecom-api.staging.blaze.me/api/v1/products/brands" \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" ``` **Query Parameters:** | Parameter | Description | | ---------- | --------------------------------- | | `q` | Search brands by name | | `category` | Filter brands by product category | | `type` | Filter brands by product type | | `tag` | Filter brands by product tag | | `ids` | Comma-separated brand IDs | | `slugs` | Comma-separated brand slugs | | `limit` | Pagination limit (default: 10) | | `offset` | Pagination offset (default: 0) | **v2 Brands:** `GET /api/v2/products/brands` — returns paginated results with `meta` containing count information. #### Brand Response Shape ```json { "data": [ { "id": "brand-uuid", "type": "product_brands", "attributes": { "name": "Premium Farms", "slug": "premium-farms", "description": "Craft cannabis since 2015", "logo_url": "https://...", "external_id": "pos-brand-123", "count": 15, "is_promoted": true } } ] } ``` ### Brand Detail **Endpoint:** `GET /api/v1/products/brands/{slug_or_id}` Accepts either a brand slug or UUID. ```bash curl -X GET "https://ecom-api.staging.blaze.me/api/v1/products/brands/premium-farms" \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" ``` ### Products by Brand **Endpoint:** `GET /api/v1/brands/{slug_or_id}/products` List all products for a specific brand: ```bash curl -X GET "https://ecom-api.staging.blaze.me/api/v1/brands/premium-farms/products?limit=20" \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" ``` --- ## Categories ### List Categories **Endpoint:** `GET /api/v1/products/categories` Returns all active product categories with product counts. ```bash curl -X GET "https://ecom-api.staging.blaze.me/api/v1/products/categories" \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" ``` **Query Parameters:** | Parameter | Description | | --------------- | ------------------------- | | `q` | Search categories by name | | `delivery_type` | Filter by delivery type | **v2 Categories:** `GET /api/v2/products/categories` — returns paginated results with `meta` containing count information. #### Category Response Shape ```json { "data": [ { "id": "category-uuid", "type": "product_categories", "attributes": { "name": "Flower", "slug": "flower", "description": "Cannabis flower and buds", "count": 45, "position": 1, "is_active": true, "icon_url": "https://...", "parent_category_id": null } } ] } ``` Categories support a hierarchy through the `parent_category_id` field. Top-level categories have a `null` parent. --- ## Product Types **Endpoint:** `GET /api/v1/products/types` Returns the available product types (e.g., Flower, Edible, Concentrate) with counts. ```bash curl -X GET "https://ecom-api.staging.blaze.me/api/v1/products/types" \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" ``` #### Type Response Shape ```json { "data": [ { "id": "Hybrid", "type": "product_types", "attributes": { "name": "Hybrid", "count": 30 } } ] } ``` > **Note:** Type IDs are strings (the type name itself), not UUIDs. --- ## Tags **Endpoint:** `GET /api/v1/products/tags` Returns the available product tags with metadata. ```bash curl -X GET "https://ecom-api.staging.blaze.me/api/v1/products/tags" \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" ``` **Query Parameters:** | Parameter | Description | | --------- | ------------------- | | `q` | Search tags by name | #### Tag Response Shape ```json { "data": [ { "id": "tag-uuid", "type": "tags", "attributes": { "name": "staff-pick", "title": "Staff Pick", "description": "Our team's favorites", "count": 8, "position": 1, "is_active": true, "is_featured": true, "is_hidden": false } } ] } ``` Tags can be used for editorial curation (staff picks, new arrivals, seasonal selections). Use `is_featured` tags to highlight collections on the storefront, and `is_hidden` tags for internal categorization that shouldn't be shown to customers. --- ## Price Ranges **Endpoint:** `GET /api/v1/products/price-ranges` Returns the minimum and maximum product prices for the store. Use this to set bounds on price filter sliders. ```bash curl -X GET "https://ecom-api.staging.blaze.me/api/v1/products/price-ranges" \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" ``` #### Response Shape ```json { "data": { "type": "price_ranges", "attributes": { "min": 5.0, "max": 500.0 } } } ``` --- ## Showcased Product Groups **Endpoint:** `GET /api/v1/products/showcased` Returns curated groups of products configured by the store (e.g., "Featured", "New Arrivals", "Best Sellers"). Use these to build homepage carousels and featured sections. ```bash curl -X GET "https://ecom-api.staging.blaze.me/api/v1/products/showcased" \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" ``` #### Response Shape ```json { "data": [ { "id": "group-uuid", "type": "showcased_products", "attributes": { "name": "Featured Products", "slug": "featured-products", "type": "manual", "description": "Hand-picked by our team" }, "relationships": { "products": { "data": [ { "id": "product-uuid-1", "type": "store_products" }, { "id": "product-uuid-2", "type": "store_products" } ] } } } ], "included": [ ... ] } ``` --- ## Understanding Product Data ### Pricing: unit_prices vs weight_prices Products can be priced in two ways: **Unit-priced products** have a single price per item. The `unit_prices` array provides pricing tiers: ```json { "unit_prices": [ { "display_name": "Each", "quantity": 1, "price": { "amount": 45.0, "currency": "USD" }, "discount_price": null, "savings_per_unit": null } ], "unit_price": 45.0 } ``` **Weight-priced products** are sold by weight. The `weight_prices` array lists each available weight option: ```json { "weight_prices": [ { "weight": "1g", "price": 15.0, "discount_price": null }, { "weight": "3.5g", "price": 45.0, "discount_price": 40.0 }, { "weight": "7g", "price": 80.0, "discount_price": null }, { "weight": "14g", "price": 150.0, "discount_price": null }, { "weight": "28g", "price": 280.0, "discount_price": null } ] } ``` A product has either `unit_prices` or `weight_prices`, not both. Check which is non-null to determine the pricing model: ```javascript function getDisplayPrice(product) { const attrs = product.attributes; if (attrs.weight_prices?.length > 0) { // Weight-priced: show starting price const cheapest = attrs.weight_prices[0]; return `From $${cheapest.price}/${cheapest.weight}`; } // Unit-priced return `$${attrs.unit_price}`; } ``` ### THC/CBD and Potency Products include cannabinoid content data: | Field | Description | | ----------------- | ------------------------------------------------------------------ | | `thc` | THC percentage (single value) | | `min_thc` | THC range minimum (when range is provided instead of single value) | | `max_thc` | THC range maximum | | `cbd` | CBD percentage (single value) | | `min_cbd` | CBD range minimum | | `max_cbd` | CBD range maximum | | `potency` | Human-readable potency string (e.g., `"22.5% THC"`) | | `cannabis_weight` | Net cannabis weight (e.g., `"3.5g"`) | The `potency` field is a pre-formatted display string. For custom formatting, use the individual `thc`/`cbd` fields: ```javascript function formatPotency(product) { const { thc, min_thc, max_thc, cbd } = product.attributes; const thcDisplay = min_thc && max_thc ? `${min_thc}–${max_thc}% THC` : thc ? `${thc}% THC` : null; const cbdDisplay = cbd ? `${cbd}% CBD` : null; return [thcDisplay, cbdDisplay].filter(Boolean).join(" · "); } ``` ### Strain and Flower Type The `flower_type` (or `strain`) field indicates the cannabis strain classification: - `Hybrid` - `Indica` - `Sativa` - `CBD` (high-CBD strains) ### Product Size The `size` field is an object with `amount` and `units`: ```json { "size": { "amount": 3.5, "units": "g" } } ``` ### Relationships and Included Resources Product list and detail responses include related resources via JSON:API `relationships` and `included`: | Relationship | Type | Description | | -------------------- | -------------------- | ------------------------------------------ | | `product_brands` | `product_brands` | Store-level brand | | `product_categories` | `product_categories` | Product category | | `product_images` | `product_images` | Additional product images | | `global_brands` | `global_brands` | Cross-store brand (for multi-store groups) | | `tags` | `tags` | Associated tags | To resolve a relationship, find the matching resource in the `included` array by `id` and `type`: ```javascript function resolveRelationship(response, type) { return response.included?.filter((r) => r.type === type) || []; } const brands = resolveRelationship(response, "product_brands"); const images = resolveRelationship(response, "product_images"); ``` --- ## Endpoint Reference All endpoints below are under `jwt_optional_authenticated` — no authentication required. All require the `X-Store` header. | Endpoint | Description | | ------------------------------------------------------------------------------------------------------- | ----------------------------------------- | | [`GET /api/v1/products`](/api#tag/Products/get/api/v1/products) | List products with filters and pagination | | [`GET /api/v1/products/{id}`](/api#tag/Products/get/api/v1/products/{id}) | Product detail (v1) | | [`GET /api/v2/products/{id}`](/api#tag/Products/get/api/v2/products/{id}) | Product detail (v2, recommended) | | [`GET /api/v1/products/categories`](/api#tag/Products/get/api/v1/products/categories) | List categories | | [`GET /api/v2/products/categories`](/api#tag/Products/get/api/v2/products/categories) | List categories (v2, paginated) | | [`GET /api/v1/products/brands`](/api#tag/Products/get/api/v1/products/brands) | List brands | | [`GET /api/v2/products/brands`](/api#tag/Products/get/api/v2/products/brands) | List brands (v2, paginated) | | `GET /api/v1/products/brands/{slug_or_id}` | Brand detail | | `GET /api/v1/brands/{slug_or_id}/products` | Products for a brand | | [`GET /api/v1/products/types`](/api#tag/Products/get/api/v1/products/types) | List product types | | [`GET /api/v1/products/tags`](/api#tag/Products/get/api/v1/products/tags) | List tags | | [`GET /api/v1/products/price-ranges`](/api#tag/Products/get/api/v1/products/price-ranges) | Price range (min/max) | | [`GET /api/v1/products/filters`](/api#tag/Search---Filters/get/api/v1/products/filters) | All filter options (v1) | | [`GET /api/v2/products/filters`](/api#tag/Search---Filters/get/api/v2/products/filters) | All filter options (v2, recommended) | | [`GET /api/v1/products/showcased`](/api#tag/Products/get/api/v1/products/showcased) | Showcased product groups | # Page: Quick Start # Quick Start Get your first API call working in under 5 minutes. ## What you'll learn - How to fetch store details - How to list products with filters - How to view a product's details ## Prerequisites - A **Store UUID** for a Blaze ECOM store - cURL or any HTTP client (Postman, Insomnia, fetch, axios) --- ## Step 1: Get Store Details The first call any frontend makes is to fetch the store: ### cURL ```bash curl -X GET https://ecom-api.staging.blaze.me/api/v1/store \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" ``` ### JavaScript (fetch) ```javascript const STORE_UUID = "e87437f2-3e35-4738-af5e-6307e368255c"; const BASE_URL = "https://ecom-api.staging.blaze.me"; const response = await fetch(`${BASE_URL}/api/v1/store`, { headers: { "Content-Type": "application/vnd.api+json", Accept: "application/vnd.api+json", "X-Store": STORE_UUID, }, }); const { data } = await response.json(); console.log(data.attributes.name); // "My Cannabis Shop" ``` ### Response ```json { "data": { "id": "store-uuid", "type": "stores", "attributes": { "name": "My Cannabis Shop", "address": "123 Main St", "city": "Los Angeles", "state": "CA", "delivery_enabled": true, "pickup_enabled": true, "timezone": "America/Los_Angeles" } } } ``` --- ## Step 2: List Products Fetch the first 10 products, no authentication required: ### cURL ```bash curl -X GET "https://ecom-api.staging.blaze.me/api/v1/products?limit=10" \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" ``` ### JavaScript (fetch) ```javascript const response = await fetch(`${BASE_URL}/api/v1/products?limit=10`, { headers: { "Content-Type": "application/vnd.api+json", Accept: "application/vnd.api+json", "X-Store": STORE_UUID, }, }); const { data, meta } = await response.json(); console.log(`${meta.total} products, showing ${data.length}`); data.forEach((p) => console.log(`${p.attributes.name} - $${p.attributes.price}`), ); ``` ### With Filters ```bash # Flower products under $50, sorted by price curl -X GET "https://ecom-api.staging.blaze.me/api/v1/products?category=flower&max_price=50&order=price_asc&limit=10" \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" ``` --- ## Step 3: Get Product Details Use the product ID or slug from Step 2: ### cURL ```bash curl -X GET https://ecom-api.staging.blaze.me/api/v2/products/PRODUCT_ID_OR_SLUG \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" ``` > **Tip**: Use the v2 endpoint for product detail — it returns enriched data with variants and inventory info. --- ## What's Next? - **Browse categories**: [`GET /api/v1/products/categories`](/api#tag/Products/get/api/v1/products/categories) - **Get filters**: [`GET /api/v2/products/filters`](/api#tag/Search---Filters/get/api/v2/products/filters) — build dynamic filter UIs - **User login**: [`POST /api/v1/auth/login`](/api#tag/Authentication/post/api/v1/auth/login) — see the [Authentication guide](authentication.md) - **Create a cart**: [`POST /api/v5/carts`](/api#tag/Cart/post/api/v5/carts) — see the [Cart & Checkout example](../examples/cart-checkout-flow.md) For a full understanding of request/response format, headers, and errors, see the [General Concepts guide](general-concepts.md). # Page: Rate Limiting # Rate Limiting ## What you'll learn - How API rate limiting works - How to handle rate limit responses - Best practices for efficient API usage --- ## Rate Limits The API applies rate limiting to protect service availability. Limits are applied per IP address and per store. When you exceed the rate limit, the API returns a `429 Too Many Requests` response. --- ## Handling Rate Limits When you receive a `429` response: 1. Read the `Retry-After` header (seconds to wait) 2. Wait for the specified duration 3. Retry the request ### Example Response ``` HTTP/1.1 429 Too Many Requests Retry-After: 30 Content-Type: application/vnd.api+json { "errors": [{ "status": "429", "detail": "Rate limit exceeded. Try again in 30 seconds." }] } ``` ### JavaScript Retry Example ```javascript async function fetchWithRetry(url, options, maxRetries = 3) { for (let attempt = 0; attempt < maxRetries; attempt++) { const response = await fetch(url, options); if (response.status === 429) { const retryAfter = parseInt( response.headers.get("Retry-After") || "30", 10, ); console.warn(`Rate limited. Retrying in ${retryAfter}s...`); await new Promise((resolve) => setTimeout(resolve, retryAfter * 1000)); continue; } return response; } throw new Error("Max retries exceeded"); } ``` --- ## Best Practices ### Cache aggressively Product catalog data changes infrequently. Cache responses for: - **Store details**: 5–15 minutes - **Products list**: 1–5 minutes - **Categories, brands, filters, types, tags**: 5–15 minutes - **Product detail**: 1–5 minutes ### Minimize requests - Use the **filters endpoint** ([`GET /api/v2/products/filters`](/api#tag/Search---Filters/get/api/v2/products/filters)) once on page load, not on every filter change - Fetch **categories and brands** once and cache locally - Use `limit` and `offset` efficiently — don't fetch all products at once ### Use conditional requests Where supported, use `If-None-Match` / `ETag` headers to avoid re-downloading unchanged data. ### Batch client-side operations When adding multiple items to a cart, batch rapid changes rather than sending one request per item click. # Page: Store And Delivery # Store & Delivery Everything you need to set up the store picker, display store details, and configure delivery options. ## What you'll learn - How multi-store groups work and how to build a store picker - How to fetch store details, settings, and configuration - How delivery methods (pickup vs delivery) are determined - How to read store schedules and time-slot availabilities - How to list available payment options - How to display promotional banners, social links, and custom pages ## Prerequisites - A **Store UUID** (staging: `e87437f2-3e35-4738-af5e-6307e368255c`) - A **Group UUID** for multi-store setups (sent via `X-Group` header) - cURL or any HTTP client All endpoints in this guide use the `jwt_optional_authenticated` pipeline — no login token is required for read operations. --- ## Store Picker (Multi-Store Groups) Stores can belong to a **group** — a collection of locations that share branding and configuration. The store picker lets customers choose which location to browse. ### How it works 1. The frontend sends the group identifier via the `X-Group` header 2. `GET /api/v1/groups/stores` returns all active stores in that group 3. The customer selects a store, and subsequent requests use that store's UUID in the `X-Store` header ### `GET /api/v1/groups/stores` **Headers**: `X-Group` (required) #### cURL ```bash curl -X GET https://ecom-api.staging.blaze.me/api/v1/groups/stores \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Group: YOUR_GROUP_UUID" ``` #### JavaScript (fetch) ```javascript const GROUP_UUID = "YOUR_GROUP_UUID"; const BASE_URL = "https://ecom-api.staging.blaze.me"; const response = await fetch(`${BASE_URL}/api/v1/groups/stores`, { headers: { "Content-Type": "application/vnd.api+json", Accept: "application/vnd.api+json", "X-Group": GROUP_UUID, }, }); const { data } = await response.json(); // Build the store picker from the list data.forEach((store) => { console.log(`${store.attributes.name} — ${store.attributes.address}`); }); ``` #### Response ```json { "data": [ { "id": "e87437f2-3e35-4738-af5e-6307e368255c", "type": "stores", "attributes": { "name": "Downtown Dispensary", "address": { "address": "123 Main St", "city": "Los Angeles", "state": "CA", "zip_code": "90001", "country": "US", "lat": 34.0522, "lng": -118.2437 }, "uuid": "e87437f2-3e35-4738-af5e-6307e368255c", "timezone": "America/Los_Angeles", "is_active": true, "allow_pickup": true, "allow_deliveries": true, "thumbnail": "https://images.example.com/store-logo.png", "license_number": "C10-0000001-LIC", "is_demo": false, "merchant_id": "merchant-123" } }, { "id": "b1234567-abcd-efgh-ijkl-000000000002", "type": "stores", "attributes": { "name": "Westside Location", "address": { "...": "..." }, "is_active": true, "allow_pickup": true, "allow_deliveries": false } } ] } ``` > **Tip**: Use `allow_pickup` and `allow_deliveries` to show the correct delivery method options for each store in your picker UI. --- ## Store Details Once a store is selected, fetch its full details. ### `GET /api/v1/store` **Headers**: `X-Store` (required) #### cURL ```bash curl -X GET https://ecom-api.staging.blaze.me/api/v1/store \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" ``` #### JavaScript (fetch) ```javascript const STORE_UUID = "e87437f2-3e35-4738-af5e-6307e368255c"; const BASE_URL = "https://ecom-api.staging.blaze.me"; const response = await fetch(`${BASE_URL}/api/v1/store`, { headers: { "Content-Type": "application/vnd.api+json", Accept: "application/vnd.api+json", "X-Store": STORE_UUID, }, }); const { data } = await response.json(); console.log(data.attributes.name); console.log(data.attributes.timezone); ``` #### Response ```json { "data": { "id": "e87437f2-3e35-4738-af5e-6307e368255c", "type": "stores", "attributes": { "name": "Downtown Dispensary", "uuid": "e87437f2-3e35-4738-af5e-6307e368255c", "env": "staging", "address": { "address": "123 Main St", "city": "Los Angeles", "state": "CA", "zip_code": "90001", "country": "US", "lat": 34.0522, "lng": -118.2437 }, "timezone": "America/Los_Angeles", "is_active": true, "allow_pickup": true, "allow_deliveries": true, "license_number": "C10-0000001-LIC", "is_demo": false, "merchant_id": "merchant-123", "thumbnail": "https://images.example.com/store-logo.png" }, "relationships": { "site": { "data": { "id": "...", "type": "store_sites" } }, "group": { "data": { "id": "...", "type": "groups" } } } } } ``` Key attributes: - **`allow_pickup`** / **`allow_deliveries`** — determines which delivery methods are available - **`timezone`** — IANA timezone, important for interpreting schedules and availability slots - **`is_active`** — whether the store is currently active and accepting orders - **`address`** — full address object with coordinates for map rendering --- ## Store Settings ### `GET /api/v1/store/settings` Returns the combined store and site settings for the current store. This includes feature flags, UI configuration, and operational settings used by the storefront. ```bash curl -X GET https://ecom-api.staging.blaze.me/api/v1/store/settings \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" ``` ### `GET /api/v2/store/settings` The v2 endpoint returns **full settings** — an aggregated response including the store group's stores, their individual settings, and group-level settings. This is typically used by the storefront to bootstrap the entire application state in a single call. ```bash curl -X GET https://ecom-api.staging.blaze.me/api/v2/store/settings \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" ``` --- ## Store Configuration ### `GET /api/v1/store/configuration` Returns store configuration including the associated group and site information. This is primarily used by Mission Control (the admin dashboard). ```bash curl -X GET https://ecom-api.staging.blaze.me/api/v1/store/configuration \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" ``` --- ## Delivery Methods: Pickup vs Delivery The store's `allow_pickup` and `allow_deliveries` attributes determine which fulfillment methods are available. These flags affect: - Which **schedules** are relevant (`pickup` or `delivery`) - Which **availability windows** to show - Which **payment options** are returned (some are delivery-type specific) When building the checkout flow, pass the `delivery_type` query parameter to endpoints that support it: - `pickup` — customer picks up at the store - `scheduled_delivery` — store delivers to the customer's address --- ## Delivery Address Requirements This section covers the full delivery address flow: what fields are required, how to validate an address, and what errors to expect. ### Address Fields by Delivery Mode **Pickup** — no customer address is needed. The store's own address is used automatically when the delivery specification type is `pickup`. **Delivery** — a customer address is required. The exact fields depend on the store's delivery configuration: - **Always required**: `zip_code` - **Required for full address validation**: `address`, `city`, `state`, `zip_code`, `country` - **`lat` / `lng` (geo data)**: required when the store uses **region geo-zone restrictions** (the `use_region_geo_zones_restrictions` setting). When enabled, the API uses geographic coordinates to determine whether the address falls within a delivery region polygon. Without this setting, only `zip_code` is checked against the store's delivery zones. - **`state`**: required when `country` is `US` or `CA` - **`country`**: defaults to `US` if omitted > **Tip**: Check the store settings endpoint (`GET /api/v2/store/settings`) for the `use_region_geo_zones_restrictions` flag to determine whether your integration needs to collect lat/lng from users. ### Delivery Specification Structure The `delivery_specification` object is set on the cart during creation or update. It tells the API how the order will be fulfilled. ```json { "delivery_specification": { "type": "delivery", "mode": "asap", "address": { "address": "123 Main St", "address_line2": "Apt 4B", "city": "Los Angeles", "state": "CA", "zip_code": "90001", "country": "US", "lat": 34.0522, "lng": -118.2437 }, "scheduled_start_time": null, "scheduled_end_time": null, "delivery_inventories": [] } } ``` **Fields**: - **`type`** (required) — `pickup`, `delivery`, or `kiosk` - **`mode`** (required) — `asap`, `scheduled`, or `express` - `asap` — deliver/pick up as soon as possible - `scheduled` — deliver/pick up at a chosen time slot (requires `scheduled_start_time` and `scheduled_end_time`) - `express` — express delivery (requires geo-zone support and available express inventory) - **`address`** — the delivery address object (required when `type` is `delivery`; ignored for `pickup` since the store address is used) - **`scheduled_start_time`** / **`scheduled_end_time`** — ISO 8601 datetime, required when `mode` is `scheduled` - **`delivery_inventories`** — optional array of inventory IDs to restrict which inventory fulfills the order (used with express delivery) ### Address Validation Flow Before setting the delivery specification on a cart, validate that the store delivers to the customer's address: 1. **User enters address** → call `POST /api/v3/deliveries/stores` (or v4) with the address 2. **If deliverable** → the response returns stores that can deliver. Set the `delivery_specification` on the cart via `PUT /api/v4/carts/{cart_uuid}/delivery-specification` or include it during cart creation/update 3. **If not deliverable** → the response returns an error with code `no_deliveries_at_location` > **v3 vs v4**: Use v3 when you don't need express delivery or geo-zone support. Use v4 when the store has `use_region_geo_zones_restrictions` enabled or when you need express/scheduled mode filtering. The v4 response includes richer data like `unavailable_reason`, `alternative_mode`, and inventory details. #### Verify Delivery Address ##### cURL ```bash curl -X POST https://ecom-api.staging.blaze.me/api/v4/deliveries/stores \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" \ -d '{ "data": { "type": "addresses", "attributes": { "address": { "address": "123 Main St", "city": "Los Angeles", "state": "CA", "zip_code": "90001", "country": "US", "lat": 34.0522, "lng": -118.2437 }, "preferred_inventories": null, "mode": null } } }' ``` ##### JavaScript (fetch) ```javascript const STORE_UUID = "e87437f2-3e35-4738-af5e-6307e368255c"; const BASE_URL = "https://ecom-api.staging.blaze.me"; async function verifyDeliveryAddress(address) { const response = await fetch(`${BASE_URL}/api/v4/deliveries/stores`, { method: "POST", headers: { "Content-Type": "application/vnd.api+json", Accept: "application/vnd.api+json", "X-Store": STORE_UUID, }, body: JSON.stringify({ data: { type: "addresses", attributes: { address: { address: address.street, city: address.city, state: address.state, zip_code: address.zipCode, country: address.country, lat: address.latitude, lng: address.longitude, }, preferred_inventories: null, mode: null, }, }, }), }); const result = await response.json(); if (!response.ok) { // Handle no_deliveries_at_location or missing_zip_code errors throw new Error( result.errors?.[0]?.detail || "Address verification failed", ); } return result.data; // Array of delivery_stores } ``` #### Set Delivery Specification on Cart ##### cURL ```bash curl -X PUT https://ecom-api.staging.blaze.me/api/v4/carts/CART_UUID/delivery-specification \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" \ -d '{ "data": { "id": "CART_UUID", "type": "carts", "attributes": { "delivery_specification": { "type": "delivery", "mode": "scheduled", "address": { "address": "123 Main St", "city": "Los Angeles", "state": "CA", "zip_code": "90001", "country": "US", "lat": 34.0522, "lng": -118.2437 }, "scheduled_start_time": "2026-06-01T10:00:00", "scheduled_end_time": "2026-06-01T13:00:00" } } } }' ``` ##### JavaScript (fetch) ```javascript async function setDeliverySpecification(cartUuid, spec) { const response = await fetch( `${BASE_URL}/api/v4/carts/${cartUuid}/delivery-specification`, { method: "PUT", headers: { "Content-Type": "application/vnd.api+json", Accept: "application/vnd.api+json", "X-Store": STORE_UUID, }, body: JSON.stringify({ data: { id: cartUuid, type: "carts", attributes: { delivery_specification: { type: spec.type, // "pickup" or "delivery" mode: spec.mode, // "asap", "scheduled", or "express" address: spec.address, // full address object scheduled_start_time: spec.scheduledStartTime || null, scheduled_end_time: spec.scheduledEndTime || null, delivery_inventories: spec.deliveryInventories || [], }, }, }, }), }, ); return response.json(); } ``` ### Region-Based Delivery Stores can define **delivery regions** — geographic zones (polygons) that determine: - **Whether the store delivers** to a given address - **Which inventory** is used to fulfill the order (different regions can have different product availability and pricing) - **Delivery fees** that may vary by region When `use_region_geo_zones_restrictions` is enabled, the API uses the `lat`/`lng` coordinates to check if the address falls within a delivery region polygon (PostGIS `ST_Contains`). Without this setting, the API checks `zip_code` against a list of zip codes associated with each delivery region. Region-based delivery affects: - **Product availability** — products may only be available in certain regions - **Express delivery** — only available in regions with inventories marked `available_for_express` - **Delivery fees** — may differ per region ### Error Scenarios These errors can occur during address validation or when setting the delivery specification: | Error Code | HTTP Status | Message | When It Occurs | | --------------------------- | ----------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | | `missing_zip_code` | 400 | Zip code is required. | `zip_code` not provided in the delivery address | | `invalid_zip_code` | 400 | Zip code is invalid for {country}. | `zip_code` format doesn't match the country (US or CA) | | `missing_address` | 400 | Address is required. | Delivery type requires an address but none was provided | | `geo_data_required` | 400 | Delivery address geo location data is required for this delivery mode. | Store uses geo-zone restrictions but `lat`/`lng` were not provided | | `zip_code_required` | 400 | Delivery address Zip Code is required for this delivery mode. | Delivery mode requires a zip code but none was provided | | `no_deliveries_at_location` | 400 | Sorry, we don't deliver to that location. | Address is outside all delivery zones for the store group | | `no_delivery_fee` | 400 | We don't do deliveries to that location. | Address is in a zone but no delivery fee could be calculated (store blocks orders without a fee) | --- ## Schedules Schedules define the store's operating hours for each delivery method, broken down by weekday. ### `GET /api/v1/store/schedules/{schedule_type}` **Path parameters**: - `schedule_type` — `pickup` or `delivery` #### cURL ```bash curl -X GET https://ecom-api.staging.blaze.me/api/v1/store/schedules/pickup \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" ``` #### JavaScript (fetch) ```javascript const STORE_UUID = "e87437f2-3e35-4738-af5e-6307e368255c"; const BASE_URL = "https://ecom-api.staging.blaze.me"; async function getSchedules(type) { const response = await fetch(`${BASE_URL}/api/v1/store/schedules/${type}`, { headers: { "Content-Type": "application/vnd.api+json", Accept: "application/vnd.api+json", "X-Store": STORE_UUID, }, }); return response.json(); } // Fetch both schedule types const pickupSchedules = await getSchedules("pickup"); const deliverySchedules = await getSchedules("delivery"); ``` #### Response ```json { "data": [ { "id": "monday", "type": "schedules", "attributes": { "weekday": "monday", "start_time": "09:00:00", "end_time": "21:00:00", "schedule_type": "pickup", "is_active": true } }, { "id": "tuesday", "type": "schedules", "attributes": { "weekday": "tuesday", "start_time": "09:00:00", "end_time": "21:00:00", "schedule_type": "pickup", "is_active": true } }, { "id": "sunday", "type": "schedules", "attributes": { "weekday": "sunday", "start_time": null, "end_time": null, "schedule_type": "pickup", "is_active": false } } ] } ``` > **Note**: A schedule with `is_active: false` means the store is closed on that day for that delivery type. --- ## Availability Windows While schedules define the store's general hours, **availabilities** return the actual bookable time slots for upcoming days. Use these to let customers pick a delivery or pickup window. ### `GET /api/v1/store/availabilities/{schedule_type}` **Path parameters**: - `schedule_type` — `pickup` or `delivery` **Query parameters** (optional): - `filter[since]` — start date (ISO 8601, e.g. `2026-05-30`). Defaults to today in the store's timezone - `filter[until]` — end date. Defaults to 7 days after `since` - `filter[has_available_slots]` — `true` to only return days with open slots - `page[size]` — limit the number of results - `zip_code` — filter availability by delivery zip code - `coords` — filter by geographic coordinates (for express delivery) ```bash curl -X GET "https://ecom-api.staging.blaze.me/api/v1/store/availabilities/delivery?filter[since]=2026-05-30&filter[until]=2026-06-05" \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" ``` #### Response ```json { "meta": { "settings": { "delivery_type": "delivery", "timezone_id": "America/Los_Angeles", "since": "2026-05-30", "until": "2026-06-05" } }, "data": [ { "id": "2026-05-30", "type": "availabilities", "attributes": { "date": "2026-05-30", "weekday": "saturday", "slots": [ { "start_time": "09:00:00", "end_time": "12:00:00", "available": true }, { "start_time": "12:00:00", "end_time": "15:00:00", "available": true }, { "start_time": "15:00:00", "end_time": "18:00:00", "available": false } ] } } ] } ``` The `meta.settings` object contains the scheduling configuration (slot duration, lead time, etc.) along with the store's timezone. --- ## Payment Options ### `GET /api/v1/store/payment-options` Returns the payment methods available for the store. Results can vary based on delivery type and device. **Query parameters** (optional): - `delivery_type` — `pickup` or `scheduled_delivery`; defaults to `pickup` - `zip_code` — providing a zip code automatically sets delivery type to `delivery` - `coords` — providing coordinates (express delivery) sets delivery type to `delivery` ```bash curl -X GET "https://ecom-api.staging.blaze.me/api/v1/store/payment-options?delivery_type=pickup" \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" ``` #### Response ```json { "data": [ { "id": "1", "type": "store_payment_options", "attributes": { "payment_option": "cash", "name": "Cash", "is_active": true, "external_service": null, "allows_tips": false, "allows_promotions": false, "supports_tips": false, "supports_promotions": false, "delivery_types": ["pickup", "delivery"] } }, { "id": "2", "type": "store_payment_options", "attributes": { "payment_option": "debit", "name": "Debit Card", "is_active": true, "external_service": "aeropay", "allows_tips": true, "allows_promotions": true, "supports_tips": true, "supports_promotions": true, "delivery_types": ["pickup", "delivery"] } } ] } ``` Key attributes: - **`payment_option`** — the payment type identifier (e.g. `cash`, `debit`, `credit`) - **`external_service`** — the payment provider if applicable (e.g. `aeropay`, `merrco`) - **`delivery_types`** — which fulfillment methods this payment option supports - **`allows_tips`** / **`allows_promotions`** — whether tips or promotions are enabled for this option - **`promotional_banner`** — optional banner to show alongside the payment option --- ## Promotional Banners ### `GET /api/v1/store/site/promotional-banners` Returns active banners for the storefront hero carousel or announcements. ```bash curl -X GET https://ecom-api.staging.blaze.me/api/v1/store/site/promotional-banners \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" ``` #### Response ```json { "data": [ { "id": "1", "type": "promotional_banners", "attributes": { "title": "Summer Sale", "description": "20% off all edibles this weekend", "destination_url": "/products?category=edibles", "desktop_image_url": "https://images.example.com/banner-desktop.jpg", "mobile_image_url": "https://images.example.com/banner-mobile.jpg", "position": 1, "is_active": true, "active_from": "2026-05-01T00:00:00Z", "active_until": "2026-06-30T23:59:59Z", "sales_channels": ["web", "mobile"] } } ] } ``` Use `desktop_image_url` and `mobile_image_url` for responsive rendering. The `position` field determines the display order. Filter by `sales_channels` to show only relevant banners for the current platform. --- ## Social Networks ### `GET /api/v1/store/socials` Returns the store's social media links. ```bash curl -X GET https://ecom-api.staging.blaze.me/api/v1/store/socials \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" ``` #### Response ```json { "data": [ { "id": "instagram", "type": "store_social_networks", "attributes": { "name": "instagram", "link": "https://instagram.com/mystore", "is_active": true } }, { "id": "twitter", "type": "store_social_networks", "attributes": { "name": "twitter", "link": "https://twitter.com/mystore", "is_active": true } } ] } ``` Only display social links where `is_active` is `true`. --- ## Custom Pages ### `GET /api/v1/store/pages` Returns custom content pages (e.g. About Us, Terms of Service) that can appear in the footer, header, or sidebar. ```bash curl -X GET https://ecom-api.staging.blaze.me/api/v1/store/pages \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" ``` #### Response ```json { "data": [ { "id": "1", "type": "store_pages", "attributes": { "name": "About Us", "link": "/about-us", "page_type": "custom", "description": "

Welcome to our store...

", "show_in_footer": true, "show_in_header": false, "show_in_side_bar": false, "show_in_app": true, "is_active": true, "is_external": false, "override_page": null, "group_page": false } }, { "id": "2", "type": "store_pages", "attributes": { "name": "Terms of Service", "link": "/terms-of-service", "page_type": "custom", "show_in_footer": true, "show_in_header": false, "show_in_side_bar": false, "show_in_app": false, "is_active": true, "is_external": false, "override_page": null, "group_page": true } } ] } ``` Key attributes: - **`show_in_footer`** / **`show_in_header`** / **`show_in_side_bar`** — controls where the page link appears - **`is_external`** — if `true`, `link` is a full URL; otherwise it's an internal path - **`group_page`** — if `true`, this page is shared across all stores in the group - **`description`** — HTML content of the page --- ## Endpoint Reference | Endpoint | Description | | -------------------------------------------------------------------------------------------------------------------- | --------------------------------------- | | [`GET /api/v1/groups/stores`](/api#tag/Groups/get/api/v1/groups/stores) | List stores in a group (store picker) | | [`GET /api/v1/store`](/api#tag/Store/get/api/v1/store) | Store details | | [`GET /api/v1/store/settings`](/api#tag/Store/get/api/v1/store/settings) | Store + site settings | | `GET /api/v2/store/settings` | Full settings (stores + group settings) | | [`GET /api/v1/store/configuration`](/api#tag/Store/get/api/v1/store/configuration) | Store configuration with group/site | | [`GET /api/v1/store/schedules/{schedule_type}`](/api#tag/Store/get/api/v1/store/schedules/{schedule_type}) | Schedules by type (pickup/delivery) | | [`GET /api/v1/store/availabilities/{schedule_type}`](/api#tag/Store/get/api/v1/store/availabilities/{schedule_type}) | Time-slot availabilities | | [`GET /api/v1/store/payment-options`](/api#tag/Store/get/api/v1/store/payment-options) | Available payment methods | | [`GET /api/v1/store/site/promotional-banners`](/api#tag/Store/get/api/v1/store/site/promotional-banners) | Promotional banners | | [`GET /api/v1/store/socials`](/api#tag/Store/get/api/v1/store/socials) | Social network links | | [`GET /api/v1/store/pages`](/api#tag/Store/get/api/v1/store/pages) | Custom pages | --- ## What's Next - **Browse products**: See the [Quick Start guide](quick-start.md) for product listing and filtering - **Authentication**: See the [Authentication guide](authentication.md) for login and JWT tokens - **Cart & Checkout**: Use the time slots from `/store/availabilities/{type}` when building the delivery specification for the cart # Page: User Accounts # User Accounts ## What you'll learn - How to get and update the current user's profile - How to manage billing addresses - How to check loyalty points and tier - How to retrieve available rewards - How to handle identity verification - How to sync user data with the POS - How to deactivate an account - Common user-related error scenarios ## Prerequisites - A Store UUID - Completed the [Quick Start](quick-start.md) - A valid JWT token — see the [Authentication guide](authentication.md) --- ## Get User Profile Retrieve the authenticated user's profile. **Endpoint:** [`GET /api/v1/users/me`](/api#tag/User-Profile/get/api/v1/users/me) **Auth:** `jwt_authenticated` **Store:** Required (`X-Store` header) ### cURL ```bash curl -X GET https://ecom-api.staging.blaze.me/api/v1/users/me \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" \ -H "Authorization: Bearer YOUR_TOKEN" ``` ### JavaScript ```javascript const response = await fetch(`${BASE_URL}/api/v1/users/me`, { headers: { "Content-Type": "application/vnd.api+json", Accept: "application/vnd.api+json", "X-Store": STORE_UUID, Authorization: `Bearer ${token}`, }, }); const { data } = await response.json(); console.log(`${data.attributes.first_name} ${data.attributes.last_name}`); ``` ### Response Example ```json { "data": { "id": "123", "type": "users", "attributes": { "email": "jane@example.com", "first_name": "Jane", "last_name": "Doe", "phone_number": "+15551234567", "address": { "address": "123 Main St", "city": "Los Angeles", "state": "CA", "zip_code": "90001", "country": "US" }, "date_of_birth": "1990-01-15T00:00:00Z", "medical_id": { "number": "MED-123456", "expiration_date": "2026-12-31" }, "drivers_license_id": { "number": "D1234567", "expiration_date": "2027-06-15" }, "num_orders": 12, "last_order_at": "2026-04-20T18:30:00Z", "confirmed_at": "2026-01-10T14:00:00Z", "phone_number_confirmed_at": "2026-01-10T14:00:00Z", "email_confirmed_at": "2026-01-10T14:01:00Z", "is_active": true, "is_pos_confirmed": true, "customer_type": "recreational", "customer_type_display": "Recreational", "role": "consumer", "marketing_sms_opt_in": true, "marketing_email_opt_in": false, "marketing_consent_required": false, "billing_address": { "address": "123 Main St", "city": "Los Angeles", "state": "CA", "zip_code": "90001" }, "state_residency": "CA", "is_anonymous": false, "pos_last_sync": "2026-04-18T10:00:00Z" }, "relationships": { "documents": { "data": [] }, "rewards": { "data": [] }, "reward_points": { "data": null } } } } ``` ### Errors | Error Code | Status | When | | ---------- | ------ | ---------------------------- | | `401` | 401 | Missing or invalid JWT token | > **Note:** If the user account is not yet confirmed, the response uses a reduced view that omits certain fields like rewards and loyalty data. --- ## Update User Profile Update profile fields, medical ID, driver's license, and marketing preferences. **Endpoint:** [`PUT /api/v1/users/me`](/api#tag/User-Profile/put/api/v1/users/me) **Auth:** `jwt_authenticated` **Store:** Required (`X-Store` header) ### cURL ```bash curl -X PUT https://ecom-api.staging.blaze.me/api/v1/users/me \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" \ -H "Authorization: Bearer YOUR_TOKEN" \ -d '{ "data": { "type": "users", "attributes": { "first_name": "Jane", "last_name": "Doe", "zip_code": "90001", "date_of_birth": 632188800000, "marketing_email_opt_in": true, "marketing_sms_opt_in": false, "customer_type": "recreational" } } }' ``` ### JavaScript ```javascript const response = await fetch(`${BASE_URL}/api/v1/users/me`, { method: "PUT", headers: { "Content-Type": "application/vnd.api+json", Accept: "application/vnd.api+json", "X-Store": STORE_UUID, Authorization: `Bearer ${token}`, }, body: JSON.stringify({ data: { type: "users", attributes: { first_name: "Jane", last_name: "Doe", zip_code: "90001", date_of_birth: new Date("1990-01-15").getTime(), marketing_email_opt_in: true, customer_type: "recreational", }, }, }), }); const { data } = await response.json(); ``` ### Updatable Fields | Field | Type | Description | | -------------------------- | ------- | -------------------------------------------------------------------- | | `first_name` | string | First name | | `last_name` | string | Last name | | `zip_code` | string | Postal code | | `address` | object | Delivery address (`address`, `city`, `state`, `zip_code`, `country`) | | `billing_address` | object | Billing address (same structure as `address`) | | `date_of_birth` | integer | Date of birth as Unix timestamp in milliseconds | | `marketing_email_opt_in` | boolean | Email marketing consent | | `marketing_sms_opt_in` | boolean | SMS marketing consent | | `customer_type` | string | `"recreational"` or `"medical"` | | `external_notification_id` | string | External push notification identifier | | `base64_signature` | string | Customer signature as base64 string | | `medical_id` | object | `{ "number": "...", "expiration_date": "YYYY-MM-DD" }` | | `drivers_license_id` | object | `{ "number": "...", "expiration_date": "YYYY-MM-DD" }` | ### Response Example Returns the full user object (same shape as [Get User Profile](#get-user-profile)). ### Errors | Error Code | Status | When | | -------------------------------- | ------ | ---------------------------------------------------------------------------------- | | `locked_verified_user_uploads` | 400 | User's identity is verified — ID/medical info updates are locked. Contact support. | | `name_and_dob_update_not_alowed` | 400 | Name or date of birth updates are locked after verification. Contact support. | | `user_is_not_confirmed` | 400 | Cannot update an unconfirmed user's email | | `failed_to_update_email` | 400 | Email update failed in the POS system | | `age_not_allowed` | 400 | Date of birth doesn't meet minimum age requirement | | `dob_is_required` | 400 | Date of birth is required but missing | --- ## Billing Address ### Get Billing Address **Endpoint:** `GET /api/v1/users/me/billing/` **Auth:** `jwt_authenticated` **Store:** Required ```bash curl -X GET https://ecom-api.staging.blaze.me/api/v1/users/me/billing/ \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" \ -H "Authorization: Bearer YOUR_TOKEN" ``` ### Response Example ```json { "data": { "id": "", "type": "billing", "attributes": { "address": { "address": "456 Billing Ave", "city": "Los Angeles", "state": "CA", "zip_code": "90002" }, "use_delivery_address_in_billing": false } } } ``` ### Update Billing Address **Endpoint:** `PATCH /api/v1/users/me/billing/` **Auth:** `jwt_authenticated` **Store:** Required ```bash curl -X PATCH https://ecom-api.staging.blaze.me/api/v1/users/me/billing/ \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" \ -H "Authorization: Bearer YOUR_TOKEN" \ -d '{ "data": { "type": "billing", "attributes": { "address": { "address": "456 Billing Ave", "city": "Los Angeles", "state": "CA", "zip_code": "90002" } } } }' ``` ### Errors | Error Code | Status | When | | ------------------------- | ------ | --------------------------------------------- | | `missing_billing_address` | 400 | Billing address fields are missing or invalid | --- ## Loyalty Points Check the user's loyalty points balance and tier. Use **v3** for the enriched response with tier information. **Endpoint:** [`GET /api/v3/users/me/loyalty`](/api#tag/User-Profile/get/api/v3/users/me/loyalty) **Auth:** `jwt_authenticated` **Store:** Required ### cURL ```bash curl -X GET https://ecom-api.staging.blaze.me/api/v3/users/me/loyalty \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" \ -H "Authorization: Bearer YOUR_TOKEN" ``` ### JavaScript ```javascript const response = await fetch(`${BASE_URL}/api/v3/users/me/loyalty`, { headers: { "Content-Type": "application/vnd.api+json", Accept: "application/vnd.api+json", "X-Store": STORE_UUID, Authorization: `Bearer ${token}`, }, }); const { data } = await response.json(); console.log(`Points: ${data.attributes.points}, Tier: ${data.attributes.tier}`); ``` ### Response Example (v3) ```json { "data": { "id": "", "type": "loyalties", "attributes": { "points": "150.00", "tier": "Gold" } } } ``` ### Response Example (v1) The v1 endpoint (`GET /api/v1/users/me/loyalty`) returns points only, without tier: ```json { "data": { "id": "points", "type": "loyalties", "attributes": { "points": "150.00" } } } ``` ### Errors | Error Code | Status | When | | ------------ | ------ | ------------------------------------------------ | | `no_loyalty` | 400 | No loyalty provider is configured for this store | --- ## Rewards Retrieve the list of rewards available to the authenticated user. These can be applied to the cart at checkout. **Endpoint:** [`GET /api/v1/users/me/rewards`](/api#tag/User-Profile/get/api/v1/users/me/rewards) **Auth:** `jwt_authenticated` **Store:** Required ```bash curl -X GET https://ecom-api.staging.blaze.me/api/v1/users/me/rewards \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" \ -H "Authorization: Bearer YOUR_TOKEN" ``` ### Response Example ```json { "data": [ { "id": "42", "type": "rewards", "attributes": { "name": "$5 Off Next Purchase", "description": "Get $5 off any order over $25", "points_required": 100, "is_stackable": false, "source": "blaze", "discount_amount": "5.00", "discount_type": "dollar", "reward_good": null, "loyalty_service_mapping": null } }, { "id": "43", "type": "rewards", "attributes": { "name": "Free Preroll", "description": "Redeem for a free house preroll", "points_required": 200, "is_stackable": false, "source": "blaze", "discount_amount": "", "discount_type": "product", "reward_good": { "name": "House Preroll", "product_id": "prod-abc-123" }, "loyalty_service_mapping": null } } ] } ``` ### Errors | Error Code | Status | When | | ------------------------ | ------ | -------------------------------------------- | | `invalid_rewards_option` | 400 | The store's rewards configuration is invalid | --- ## Identity Verification Some stores require identity verification (e.g., via Berbix or similar services) before allowing purchases. ### Get Verification Status Check the current user's identity verification report. **Endpoint:** `GET /api/v1/users/me/identity-verification/` **Auth:** `jwt_authenticated` **Store:** Required ```bash curl -X GET https://ecom-api.staging.blaze.me/api/v1/users/me/identity-verification/ \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" \ -H "Authorization: Bearer YOUR_TOKEN" ``` ### Response Example ```json { "data": { "id": "berbix-delivery", "type": "identity_verification_report", "attributes": { "service": "berbix", "delivery_type": "delivery", "is_verified": true, "has_drivers_license": true, "has_selfie_id": true, "verified_by": "berbix", "can_see_menu": true } } } ``` ### Create Verification Transaction Start a new identity verification session with a supported service. **Endpoint:** `POST /api/v1/users/me/identity-verification/{service}` **Auth:** `jwt_authenticated` **Store:** Required ```bash curl -X POST https://ecom-api.staging.blaze.me/api/v1/users/me/identity-verification/berbix \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" \ -H "Authorization: Bearer YOUR_TOKEN" \ -d '{ "data": { "type": "identity_verifications", "attributes": { "identity_verification_id": "iv-uuid-if-existing" } } }' ``` ### Response Example ```json { "data": { "id": "iv-123", "type": "identity_verifications", "attributes": { "external_service": "berbix", "status": "pending", "public_token_1": "berbix_client_token_abc123", "external_id": "berbix-txn-456" } } } ``` ### Check Transaction Status **Endpoint:** `GET /api/v1/users/me/identity-verification/{service}` **Auth:** `jwt_authenticated` **Store:** Required ### Get Service Configuration Check if a verification service is configured for the store. **Endpoint:** `GET /api/v1/store/integrations/identity-verification/{service}/configuration` **Auth:** `jwt_optional_authenticated` **Store:** Required ### Errors | Error Code | Status | When | | -------------------------------------- | ------ | --------------------------------------------------------- | | `invalid_identity_verification_option` | 400 | The identity verification service is not valid | | `user_already_verified` | 400 | User is already verified by this service | | `missing_identity_verification_data` | 400 | Required verification fields are missing | | `inactive_service_config` | 400 | The verification service configuration is inactive | | `missing_service_config` | 400 | The verification service is not configured for this store | --- ## Sync with POS Sync the current user's profile with the Point of Sale system to pull the latest data (e.g., loyalty balance, order history, member status). **Endpoint:** `POST /api/v1/users/me/sync` **Auth:** `jwt_authenticated` **Store:** Required ```bash curl -X POST https://ecom-api.staging.blaze.me/api/v1/users/me/sync \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" \ -H "Authorization: Bearer YOUR_TOKEN" \ -d '{ "data": { "type": "users", "attributes": {} } }' ``` ### Response Example Returns the updated user object (same shape as [Get User Profile](#get-user-profile)). ### Errors | Error Code | Status | When | | -------------------- | ------ | ------------------------------------------------------------ | | `user_is_not_linked` | 400 | User is not linked to any POS profile — sync is not possible | --- ## Account Deactivation Deactivate the current user's account. This is a soft-delete — the account is marked inactive. **Endpoint:** `DELETE /api/v1/users/me` **Auth:** `jwt_authenticated` **Store:** Required ```bash curl -X DELETE https://ecom-api.staging.blaze.me/api/v1/users/me \ -H "Content-Type: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" \ -H "Authorization: Bearer YOUR_TOKEN" ``` ### Response Returns `204 No Content` on success. --- ## Document Uploads Upload identity documents (driver's license, medical ID photos, selfie). ### Get Upload URL Request a pre-signed upload URL for a document type. **Endpoint:** `POST /api/v1/users/me/documents/{type}/upload-url` **Auth:** `jwt_authenticated` **Store:** Required Supported types: `drivers_license`, `medical_id`, `selfie` ```bash curl -X POST https://ecom-api.staging.blaze.me/api/v1/users/me/documents/drivers_license/upload-url \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" \ -H "Authorization: Bearer YOUR_TOKEN" \ -d '{ "data": { "type": "users", "attributes": { "filename": "my-id-photo.jpg" } } }' ``` ### Confirm Upload After uploading the file to S3 using the pre-signed URL, confirm the upload: **Endpoint:** `POST /api/v1/users/me/documents/{type}/url` ```bash curl -X POST https://ecom-api.staging.blaze.me/api/v1/users/me/documents/drivers_license/url \ -H "Content-Type: application/vnd.api+json" \ -H "Accept: application/vnd.api+json" \ -H "X-Store: e87437f2-3e35-4738-af5e-6307e368255c" \ -H "Authorization: Bearer YOUR_TOKEN" \ -d '{ "data": { "type": "users", "attributes": { "key": "uploads/documents/abc123.jpg" } } }' ``` ### Check Upload Status **Endpoint:** `GET /api/v1/users/me/documents/{type}/url/{token}` ### Errors | Error Code | Status | When | | ------------------------------ | ------ | ---------------------------------------------------------------- | | `invalid_document_type` | 400 | The document type is not recognized | | `invalid_file_format` | 400 | The uploaded file format is not accepted | | `locked_verified_user_uploads` | 400 | User is verified — document updates are locked. Contact support. | | `s3_key_length_exceeded` | 400 | The upload URL is too long | --- ## Common Error Scenarios These errors can occur across multiple user account endpoints: | Error Code | Status | Meaning | | -------------------------------- | ------ | --------------------------------------------------------------------------------- | | `user_is_not_confirmed` | 400 | Account has not been verified yet. Complete phone/email verification first. | | `inactive_user` | 401 | The user account is deactivated. | | `invalid_user` | 400 | No user matching the provided information could be found. | | `bad_request` | 400 | General invalid request parameters. | | `not_found` | 404 | The requested resource does not exist. | | `locked_verified_user_uploads` | 400 | ID/medical info is locked after identity verification. Contact support to update. | | `name_and_dob_update_not_alowed` | 400 | Name and birthday updates are locked. Contact support. | | `not_a_member` | 400 | The POS has not accepted the user's membership yet. | | `consumer_not_found` | 404 | User record not found. | --- ## What's Next? - **Cart operations**: Use the user profile for pre-filling checkout — see the [Cart & Checkout guide](cart-and-checkout.md) - **Payment sources**: Manage saved payment methods — see the [Authentication guide](authentication.md) for token usage - **Store details**: Fetch store configuration for delivery/pickup options — see the [Store & Delivery guide](store-and-delivery.md) # Page: Versioning # API Versioning ## What you'll learn - How the API is versioned - Which version to use for each domain - How to handle version differences --- ## Versioning Strategy The API uses **URL path versioning** — the version is embedded in the URL: ``` /api/v1/products /api/v2/products/filters /api/v5/carts ``` Versions are **additive** — newer versions add capabilities or change response shapes, but older versions remain available. There is no deprecation timeline for existing versions. --- ## Which Version to Use | Domain | Recommended Version | Notes | | ------------------------- | ------------------- | ------------------------------------------------------------------------------- | | **Authentication** | v1 | Login, register, password reset, verification | | **Store** | v1 (settings: v2) | `GET /api/v1/store` for details; `GET /api/v2/store/settings` for full settings | | **Products — List** | v1 | `GET /api/v1/products` | | **Products — Detail** | v2 | `GET /api/v2/products/{id}` — enriched response with variants | | **Products — Categories** | v2 | `GET /api/v2/products/categories` — richer response | | **Products — Filters** | v2 | `GET /api/v2/products/filters` — richer response | | **Products — Brands** | v1 or v2 | Both available | | **Cart** | v5 | `POST /api/v5/carts` — latest cart handling with delivery spec | | **Orders** | v4 | `POST /api/v4/orders` — synchronous order creation, returns the order | | **Orders — async** | v5 | `POST /api/v5/orders` — enqueues submission, returns the *cart*; poll for the order | | **Deliveries** | v3 | `POST /api/v3/deliveries/stores` — delivery store availability | | **User Profile** | v1 | `GET /api/v1/users/me` | | **Loyalty** | v3 | `GET /api/v3/users/me/loyalty` — latest loyalty response | | **Campaigns** | v2 | Richer campaign data | | **Payments** | v1 (sources: v2) | `GET /api/v2/store/payments/sources` for unified source listing | | **Tags** | v2 | `GET /api/v2/store/tags` — enriched tag data | ### General Rule - Use the **highest available version** for each domain - When in doubt, check the OpenAPI spec — each endpoint lists the recommended version --- ## Version Differences ### Cart: v4 vs v5 v5 is the latest cart version. Both v4 and v5 share the same endpoints, but v5 includes improved validation and delivery specification handling. | Feature | v4 | v5 | | ---------------------- | ----------------- | ----------------- | | Create cart | ✅ | ✅ | | Delivery specification | Separate endpoint | Separate endpoint | | Item management | ✅ | ✅ | | Validation | ✅ | ✅ (improved) | ### Products: v1 vs v2 | Feature | v1 | v2 | | -------------- | --------- | ------------------------------ | | Product detail | Basic | Enriched (variants, inventory) | | Categories | Flat list | Hierarchical with metadata | | Filters | Basic | Dynamic with counts | | Brands | ✅ | ✅ (same) | --- ## Mixing Versions It is safe and expected to mix versions across different domains in the same application. For example, a typical storefront uses: ``` GET /api/v1/store ← v1 for store details GET /api/v2/products/filters ← v2 for enriched filters GET /api/v1/products ← v1 for product listing GET /api/v2/products/{id} ← v2 for product detail POST /api/v5/carts ← v5 for cart POST /api/v4/orders ← v4 for checkout GET /api/v3/users/me/loyalty ← v3 for loyalty ``` > **Tip**: Click any endpoint in the [API Reference](/api) tab to see its full documentation, request/response schemas, and code samples. # Page: Webhooks # Webhooks ## What you'll learn - What webhooks are and how they work in the ECOM API - Which webhook events are available and what triggers each one - How webhook URLs are constructed and authenticated - The payload structure for each event type - How to configure webhooks for your store - Retry behavior and idempotency considerations - How to validate webhook authenticity ## Prerequisites - A store configured with a POS integration (Blaze, Treez, LeafLogix, Greenline, or Cova) - The store webhook token (provided during store setup — see [Setting Up Webhooks](#setting-up-webhooks)) --- ## Overview Webhooks are outbound HTTP `POST` requests sent by the ECOM API to your store's configured webhook endpoints whenever a relevant event occurs in a connected POS or third-party service. Rather than polling the API for changes, webhooks push updates to your system in near-real time. **How it works:** 1. An event occurs in the POS (e.g., an order status changes, a new member registers). 2. The POS sends an HTTP `POST` to the ECOM API's inbound webhook endpoint for your store. 3. The ECOM API processes the payload — updating orders, syncing member data, triggering notifications, etc. 4. The ECOM API always responds with `200 OK` and an empty body `{}`. Webhooks in this system are **inbound** — they are received _by_ the ECOM API from external systems (POS, delivery services, payment services). The ECOM API acts as the webhook consumer. --- ## Authentication: Token-Based URL Webhook URLs use a **store-specific token embedded in the URL path**. There are no HTTP headers or signatures to verify — the token _is_ the credential. **URL pattern:** ``` POST /api/v1/hooks/{event_name}/{token} ``` For events that also carry a service identifier: ``` POST /api/v1/hooks/{event_name}/{service}/{token} ``` The `{token}` is a unique, opaque string tied to a specific store. When a request arrives, the `StoreTokenPipeline` looks up the store by this token. If no store is found, the request is rejected with `404 Not Found`. > **Keep your token secret.** Anyone with the token URL can post arbitrary payloads to your store's webhook endpoints. Rotate the token if it is ever exposed. --- ## Available Events | Event | URL | Triggered by | | --------------------------------- | -------------------------------------------------------------------- | ---------------------------------- | | `order_updated` | `POST /api/v1/hooks/order_updated/:token` | Order status change in the POS | | `new_member` | `POST /api/v1/hooks/new_member/:token` | New member registered in the POS | | `member_updated` | `POST /api/v1/hooks/member_updated/:token` | Member profile updated in the POS | | `job_assigned` | `POST /api/v1/hooks/job_assigned/:token` | Delivery job assigned to a driver | | `job_started` | `POST /api/v1/hooks/job_started/:token` | Driver picked up the delivery | | `job_arrived` | `POST /api/v1/hooks/job_arrived/:token` | Driver arrived at delivery address | | `job_completed` | `POST /api/v1/hooks/job_completed/:token` | Delivery job completed | | `identity-verification-updated` | `POST /api/v1/hooks/identity-verification-updated/:service/:token` | ID verification status updated | | `identity-verification-completed` | `POST /api/v1/hooks/identity-verification-completed/:service/:token` | ID verification completed | | `payment-auth-completed` | `POST /api/v1/hooks/payment-auth-completed/:service/:token` | 3DS / payment challenge completed | --- ## Event Details ### `order_updated` **URL:** `POST /api/v1/hooks/order_updated/:token` Fired by the POS whenever an order's status changes (e.g., `PENDING` → `COMPLETED`, `CANCELLED`). The ECOM API processes the update and syncs the corresponding order record. The payload is the raw POS order object. Its exact shape depends on the connected POS, but all Blaze-backed stores use the following structure: ```json { "id": "ord_7f3a2b1c-4d5e-6f7a-8b9c-0d1e2f3a4b5c", "orderNo": "1042", "status": "COMPLETED", "consumerId": "mbr_a1b2c3d4-e5f6-7890-abcd-ef1234567890", "memo": "Please ring the bell", "rewardName": null, "publicKey": "cart_pub_9x8y7z", "memberGroup": "RECREATIONAL", "trackingStatus": "DELIVERED", "cart": { "subTotal": 45.0, "discount": 5.0, "totalDiscount": 5.0, "deliveryFee": 0.0, "creditCardFee": 0.0, "total": 43.28, "taxTotal": 3.28, "totalCalcTax": 3.28, "promoCode": null, "items": [ { "productId": "prod_c3d4e5f6-a7b8-9012-cdef-345678901234", "productName": "Blue Dream Pre-Roll", "quantity": 2, "price": 22.5, "totalPrice": 45.0 } ] } } ``` **What the API does:** Dispatches to the POS-specific datasource handler (`handle_order_update_hook/2`), which syncs the order status and line items. Order update notifications may be sent to the customer. --- ### `new_member` **URL:** `POST /api/v1/hooks/new_member/:token` Fired when a new member is created in the POS. The ECOM API checks whether an existing ECOM user maps to this POS member (by `consumerUserId`) and refreshes their profile. If no user exists and the store has customer imports enabled, an import job is enqueued. The payload contains the POS member record: ```json { "id": "mbr_a1b2c3d4-e5f6-7890-abcd-ef1234567890", "modified": 1700000000000, "email": "jane@example.com", "firstName": "Jane", "lastName": "Doe", "phone": "+15551234567", "status": "ACTIVE", "type": "RECREATIONAL" } ``` **What the API does:** Triggers a notification log entry and calls `PointOfSalesUsers.handle_new_member_hook/2`. If the user exists in ECOM, their profile is refreshed from the POS. If not, and imports are enabled, an async import job is scheduled. --- ### `member_updated` **URL:** `POST /api/v1/hooks/member_updated/:token` Fired when an existing POS member's profile is updated (name, phone, email, status, etc.). The ECOM API syncs the matching user's profile from POS data. > **Important:** The Blaze POS fires this webhook when the ECOM API calls the Partner API to update a member. Avoid triggering Partner API member updates inside this handler — doing so creates an infinite loop of webhook calls. The payload has the same shape as `new_member`: ```json { "id": "mbr_a1b2c3d4-e5f6-7890-abcd-ef1234567890", "modified": 1700005000000, "email": "jane.updated@example.com", "firstName": "Jane", "lastName": "Smith", "phone": "+15551234567", "status": "ACTIVE", "type": "RECREATIONAL" } ``` **What the API does:** Triggers a notification log entry and calls `PointOfSalesUsers.handle_member_updated_hook/2`, which refreshes POS-sourced fields on the matching ECOM user record. --- ### `job_assigned`, `job_started`, `job_arrived`, `job_completed` **URLs:** - `POST /api/v1/hooks/job_assigned/:token` - `POST /api/v1/hooks/job_started/:token` - `POST /api/v1/hooks/job_arrived/:token` - `POST /api/v1/hooks/job_completed/:token` Fired by the delivery service as a driver's job progresses through its lifecycle. The store's delivery datasource (`DeliveriesDataSourceSwitcher`) handles each event. Payload structure depends on the delivery integration, but typically includes: ```json { "id": "job_d4e5f6a7-b8c9-0123-def4-567890123456", "orderId": "ord_7f3a2b1c-4d5e-6f7a-8b9c-0d1e2f3a4b5c", "driverId": "drv_1a2b3c4d-5e6f-7890-abcd-ef1234567890", "status": "ASSIGNED", "estimatedArrival": 1700010000000, "driverLocation": { "lat": 37.7749, "lng": -122.4194 } } ``` **What the API does:** Updates the `DeliveryJob` record and triggers any associated order or customer notifications. --- ### `identity-verification-updated` and `identity-verification-completed` **URLs:** - `POST /api/v1/hooks/identity-verification-updated/:service/:token` - `POST /api/v1/hooks/identity-verification-completed/:service/:token` Fired by the identity verification service (e.g., as a user's document scan progresses). The `:service` path segment identifies which provider sent the event (e.g., `"alpharoot"`, `"onfido"`). Payload varies by provider. The `service` field must be present: ```json { "service": "alpharoot", "transactionId": "txn_e5f6a7b8-c9d0-1234-ef56-789012345678", "status": "PENDING_REVIEW", "userId": "mbr_a1b2c3d4-e5f6-7890-abcd-ef1234567890", "updatedAt": 1700010000000 } ``` **What the API does:** Routes to the appropriate `IdentityVerificationServiceSwitcher` datasource, which updates the user's identity verification record. --- ### `payment-auth-completed` **URL:** `POST /api/v1/hooks/payment-auth-completed/:service/:token` Fired by the payment provider after a 3D Secure (3DS) challenge completes. The `:service` path segment identifies the payment provider. Unlike other webhooks, this handler performs a CAVV lookup and **redirects the browser** to the storefront's success or failure URL. Payload must include a `cres` (challenge response) field: ```json { "service": "adyen", "cres": "eyJhbGciOiJSUzI1Ni...", "transactionId": "txn_e5f6a7b8-c9d0-1234-ef56-789012345678" } ``` **What the API does:** Looks up the CAVV and ECI values via `PaymentsSwitcher`, then redirects to: - `{store_url}checkout/payment-verification/success?cavv=...&eci=...` on success - `{store_url}checkout/payment-verification/failure?msg=...` on failure --- ## Setting Up Webhooks Webhook URLs are **configured per store within the POS integration settings**. The setup process differs by POS provider, but the general steps are: 1. **Retrieve your store's webhook token.** This is available via store configuration — contact your integration team or check the POS integration settings in the ECOM admin. The token is stored in the store record and looked up by `Stores.get_by_token/1`. 2. **Construct your webhook URL.** Use the base URL for your environment: | Environment | Base URL | | ----------- | ----------------------------------- | | Production | `https://ecom-api.blaze.me` | | Staging | `https://ecom-api.staging.blaze.me` | | Development | `http://localhost:4000` | Example for `order_updated` on staging: ``` https://ecom-api.staging.blaze.me/api/v1/hooks/order_updated/YOUR_STORE_TOKEN ``` 3. **Register the URL in your POS.** In the Blaze POS (or your configured POS), navigate to the webhook or integrations settings and add each webhook URL for the events you want to receive. 4. **Test the endpoint.** Most POS systems allow you to send a test event. Verify you receive a `200 OK` response. --- ## Response Format All webhook endpoints return `200 OK` with an empty JSON object body, regardless of whether processing succeeds or fails internally: ```json {} ``` The ECOM API is designed to always acknowledge receipt. Internal processing errors are captured via Sentry and logged — they do not result in non-`2xx` responses. --- ## Retry Behavior and Idempotency Because the ECOM API always returns `200 OK`, the **POS or external service is responsible for retry logic**. If the POS does not receive a `200` (e.g., due to a network error), it may re-deliver the same event. **Idempotency considerations:** - **`order_updated`** — Safe to replay. The handler re-syncs order state from the POS; delivering the same payload twice results in the same final state. - **`new_member` / `member_updated`** — Safe to replay. The handler refreshes the user profile from POS data; duplicate deliveries do not create duplicate records. - **`job_*`** — Safe to replay. Delivery job records are upserted based on their POS job ID. - **`payment-auth-completed`** — Replaying this event redirects the browser again. In practice, replays are unlikely because this is a user-facing browser redirect flow. - **`identity-verification-*`** — Safe to replay. Verification state is updated based on the transaction ID from the provider. --- ## Security Considerations ### Token Confidentiality The webhook token is the only authentication mechanism. Treat it like a password: - Never log full webhook URLs in browser-accessible logs. - Rotate the token via store configuration if it is ever exposed. - Use HTTPS for all webhook URLs (all production and staging URLs use TLS). ### Token Validation The `StoreTokenPipeline` validates each inbound request before it reaches the handler: 1. Extracts the `:token` from the URL path. 2. Calls `Stores.get_by_token(token)`. 3. If no matching store is found, responds `404 Not Found` and halts — no handler runs. 4. If found, assigns the store to the connection context for use by the controller. This means an invalid or guessed token cannot trigger any processing. ### Source IP Allowlisting For additional security, consider allowlisting the IP ranges of your POS provider at your network or load balancer level. Contact your POS provider for their current egress IP list. --- ## Error Codes Webhook endpoints do not return application-level error codes to callers — they always respond `200 OK`. However, these internal conditions can prevent a webhook from being processed correctly: | Condition | Behavior | | ---------------------------------------------------- | -------------------------------------------------------------------- | | Invalid or unknown token | `404 Not Found` returned; processing halted | | Store not found for token | `404 Not Found` returned; processing halted | | Unknown `service` in identity verification events | Handler receives unsupported service; payload is ignored silently | | Unknown `service` in payment auth events | Falls through to error redirect | | Missing `service` field in identity/payment payloads | Pattern match fails; Elixir function clause error captured by Sentry | | PlasticPay payment with no related order | Error logged to Sentry; `200 OK` returned | # Page: Error Catalog # Error Catalog ## What you'll learn - The standard error response format used across all API endpoints - Every error code the API can return, organized by domain - HTTP status codes and when each is used - Best practices for handling errors in your frontend ## Prerequisites - Familiarity with the [General Concepts](../guides/general-concepts.md) guide - Understanding of [JSON:API](https://jsonapi.org/) error format - Completed the [Quick Start](../guides/quick-start.md) --- ## Overview The Blaze ECOM Storefront API uses a consistent error format across all endpoints. Every error response follows the JSON:API specification, returning one or more structured error objects in an `errors` array. Errors are identified by a machine-readable `code` field (e.g., `empty_cart`, `invalid_token`) which stays stable across API versions, making it safe to match against in your frontend logic. The `detail` field provides a human-readable message suitable for displaying to end users. --- ## Error Response Format All error responses return a JSON body with the following structure: ```json { "errors": [ { "code": "empty_cart", "status": 400, "detail": "The cart is empty.", "fields": [], "extra_info": {} } ] } ``` ### Error Object Fields | Field | Type | Description | | ------------ | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------- | | `code` | `string \| null` | Machine-readable error code (e.g., `empty_cart`, `invalid_token`). Stable across versions. May be `null` for generic validation errors. | | `status` | `integer` | HTTP status code for this error (e.g., `400`, `401`, `404`). | | `detail` | `string` | Human-readable error message. Safe to display to end users. | | `fields` | `string[]` | List of field names related to the error (e.g., `["email"]`, `["phone_number"]`). Empty array when not field-specific. | | `extra_info` | `object` | Additional context for the error. Structure varies by error type. Empty object when no extra context is available. | ### Multiple Errors A single response can contain multiple errors — for example, when an Ecto changeset validation fails on several fields: ```json { "errors": [ { "code": null, "status": 422, "detail": "can't be blank", "fields": ["email"], "extra_info": {} }, { "code": null, "status": 422, "detail": "can't be blank", "fields": ["password"], "extra_info": {} } ] } ``` ### Extra Info Examples Some errors include `extra_info` with additional context: **Cart already submitted** — includes the order UUID so the frontend can redirect: ```json { "code": "cart_already_submitted", "status": 400, "detail": "This cart has already been submitted", "fields": [], "extra_info": { "order": { "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890" } } } ``` **Invalid promo code** — echoes back the code that was rejected: ```json { "code": "invalid_promo_code", "status": 400, "detail": "Coupon code does not exist.", "fields": ["promo_code"], "extra_info": { "promo_code": "SUMMER20" } } ``` **Invalid cart items** — lists the items that are problematic: ```json { "code": "invalid_cart", "status": 400, "detail": "Invalid items in the cart", "fields": ["cart"], "extra_info": { "items": [{ "id": "item-uuid", "reason": "out_of_stock" }] } } ``` **POS API error codes** — external error codes from integrated POS systems: ```json { "code": "bad_request", "status": 400, "detail": "Inventory type does not match", "fields": [], "extra_info": { "api_error_code": "TZ00003" } } ``` --- ## HTTP Status Codes | Status Code | Meaning | When It's Used | | ----------- | --------------------- | ------------------------------------------------------------------------------------------------------------------- | | `400` | Bad Request | Invalid input, business rule violations, missing fields, cart errors, payment issues. The most common error status. | | `401` | Unauthorized | Invalid or expired JWT token, wrong credentials, inactive user, invalid SSO token. | | `403` | Forbidden | User does not have permission to access the resource. | | `404` | Not Found | Resource does not exist (store, product, order, kiosk, etc.). | | `422` | Unprocessable Entity | Ecto changeset validation failures — field-level validation errors on create/update. | | `429` | Too Many Requests | Rate limit exceeded. See [Rate Limiting](../guides/rate-limiting.md). | | `500` | Internal Server Error | Unexpected server error. These are never intentional — report them to support. | --- ## Error Codes by Domain ### Authentication Errors | Error Code | HTTP Status | Message | When It Occurs | | ------------------------------------- | ----------- | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `bad_login` | 401 | Wrong Credentials | Email/phone and password combination is incorrect. | | `inactive_user` | 401 | The user for the given credentials is inactive. | The account exists but has been deactivated. | | `user_is_not_confirmed` | 400 | User was not confirmed | User has not completed account confirmation (e.g., email/phone verification). Also returned when trying to reset password or update email for an unconfirmed user. | | `phone_number_requires_confirmation` | 400 | Phone number requires confirmation. | Login or registration requires phone verification before proceeding. | | `email_requires_confirmation` | 400 | Email requires confirmation. | Login or registration requires email verification before proceeding. | | `invalid_verification_code` | 400 | Invalid verification code | The verification code submitted during phone/email confirmation is wrong. | | `verification_not_available` | 400 | Verification is no longer available. Request another code. | The verification code has expired. A new one must be requested. | | `failed_to_request_verification_code` | 400 | Failed to request verification code. | The system was unable to send a verification code (SMS/email provider failure). | | `verification_code_blocked` | 400 | Failed to request verification code. | Too many verification code requests — the user is temporarily blocked. | | `account_already_verified` | 400 | There is a verified account with the phone number {phone}. | Attempting to verify an account when another verified account already uses that phone number. | | `invalid_token` | 400 | This token is no longer valid. Please request another link. | Password reset or email confirmation token has expired or already been used. | | `invalid_credentials` | 401 | Invalid user credentials. | Generic authentication failure — credentials do not match any account. | | `invalid_sso_token` | 401 | Invalid or expired SSO token | SSO token provided in the request is invalid or has expired. | | `missing_authorization_header` | 401 | Missing Authorization header | An authenticated endpoint was called without the `Authorization: Bearer` header. | | `bad_current_password_match` | 400 | Current password is wrong | Password change failed because the current password provided doesn't match. | | `invalid_password` | 400 | The password does not match with your existing account in the Blaze point of sales. | User tried to register with an email/phone that exists in the POS but provided the wrong POS password. | | `pos_not_allowed` | 401 | The user for the given credentials is not allowed access to the store's POS. | User is authenticated but not authorized for POS access. | | `url_expired` | 403 | URL expired | A time-limited URL (e.g., magic link) has expired. | | `logout_unsuccessful` | 400 | Logout was unsuccessful. | Server-side logout failed (token invalidation error). | ### OAuth & SSO Errors | Error Code | HTTP Status | Message | When It Occurs | | ------------------------------------------- | ----------- | -------------------------------------------- | ------------------------------------------------------------------------- | | `oauth_login_missing_client` | 400 | You must provide a client ID | OAuth login request is missing the `client_id` parameter. | | `oauth_login_invalid_client` | 400 | Invalid client ID | The `client_id` provided does not match a registered OAuth application. | | `oauth_login_missing_store` | 400 | You must provide a store ID | OAuth login request is missing the store identifier. | | `oauth_login_invalid_store` | 400 | Invalid store ID | The store ID in the OAuth request does not match a valid store. | | `oauth_auth0_failed_to_register` | 400 | Failed to register user with Auth0: {detail} | Auth0 provider rejected the user registration. | | `authentication_oauth_invalid_state` | 400 | Invalid state parameter | The OAuth `state` parameter doesn't match expected value (possible CSRF). | | `authentication_oauth_missing_redirect_uri` | 400 | Missing redirect URI | OAuth request is missing the required `redirect_uri`. | | `authentication_oauth_invalid_redirect_uri` | 400 | Invalid redirect URI | The `redirect_uri` does not match any registered redirect URIs. | | `authentication_oauth_invalid_callback` | 400 | Invalid callback parameters | OAuth callback received invalid or missing parameters. | ### User & Account Errors | Error Code | HTTP Status | Message | When It Occurs | | -------------------------------------------------- | ----------- | -------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | | `invalid_user` | 400 | Sorry, we couldn't find any user matching your information. | No user matches the provided fields (used during registration/lookup). | | `email_already_exists` | 400 | Email is already in use. Please contact Retailer Support. | Attempting to register or update with an email that another account already uses. | | `phone_already_exists` | 400 | Phone number is already in use. Please contact Retailer Support. | Attempting to register or update with a phone number that another account already uses. | | `locked_verified_user_uploads` | 400 | Need to update your ID/Medical Info? Please contact Support. | User tried to change ID/medical documents on a verified account. | | `name_and_dob_update_not_alowed` | 400 | Need to update your name or birthday info? Please contact Support. | User tried to update name or date of birth on a verified account. | | `user_is_not_linked` | 400 | User is not linked to any POS profile | Sync was requested but the user has no linked POS profile. | | `non_customer_user` | 400 | User is not a customer | An operation requiring a customer role was attempted by a non-customer user. | | `invalid_phone_number` | 400 | Invalid phone number | The phone number format is invalid. | | `us_phone_number_required` | 400 | US phone number is required. | The operation requires a US-formatted phone number. | | `verified_phone_number_required` | 400 | Phone number must be verified. | The operation requires a verified phone number. | | `registration_with_email_is_required` | 400 | Email is required for registration | Store settings require an email address during registration. | | `registration_with_phone_is_required` | 400 | Phone number is required for registration | Store settings require a phone number during registration. | | `registration_with_drivers_license_id_is_required` | 400 | Driver's License ID is required for registration | Store settings require a driver's license ID during registration. | | `registration_with_state_residency_is_required` | 400 | State residency is required for registration | Store settings require state residency information during registration. | | `consumer_not_found` | 404 | User not found | The referenced user/consumer does not exist. | | `failed_to_update_email` | 400 | The email could not be updated in the POS. Please contact us. | Email update was rejected by the POS system. | | `failed_register` | 400 | Oops, looks like this is not the phone number we have on file in the POS. Please try again or contact us to access your account. | Phone-based registration failed because the POS has a different phone number on file. | | `expected_same_email_for_phone_number` | 400 | A different email was found assigned to {phone}. Please contact us. | The phone number exists in the POS but is linked to a different email. | | `expected_one_result_for_phone_number` | 400 | Your phone is associated with multiple profiles in our different stores. Please contact us to setup your profile. | Phone number matches multiple POS profiles across stores. | | `expected_one_result_for_email` | 400 | Your email is associated with multiple profiles in our different stores. Please contact us to setup your profile. | Email matches multiple POS profiles across stores. | | `expected_one_result_for_drivers_license` | 400 | Your driver's license is associated with multiple profiles in our different stores. Please contact us to setup your profile. | Driver's license matches multiple POS profiles. | | `no_matching_email_to_dl_match` | 400 | POS profile unable to match email and ID, please contact us to update your profile. | Email and ID document don't match the same POS profile. | | `no_matching_phone_to_dl_match` | 400 | POS profile unable to match phone and ID, please contact us to update your profile. | Phone and ID document don't match the same POS profile. | | `age_not_allowed` | 400 | The allowed minimum age is {age} | User does not meet the store's minimum age requirement. | | `dob_is_required` | 400 | Please update your date of birth in your profile. | Date of birth is missing and required for the requested operation. | | `user_already_verified` | 400 | The identity of this user is already verified by {service} | Identity verification was requested but user is already verified. | | `unique_email_already_confirmed` | 400 | There's already a confirmed customer with the same email ({email}). You can't reset the password and activate this User | Cannot activate a user because another confirmed user has the same email. | | `user_active_on_pos` | 400 | Only customers deactivated on POS can be reset | Account reset is only available for POS-deactivated customers. | | `not_a_member` | 400 | The POS still has not accepted your membership | User's membership is pending POS acceptance. | | `user_is_no_store_manager` | 400 | User is no store manager. | Operation requires store manager role. | | `group_mismatch` | 400 | Customer and store groups do not match | Customer belongs to a different group than the requested store. | | `resource_id_mismatch` | 400 | Resource ID in the URL and data do not match | The `id` in the URL path doesn't match the `id` in the JSON:API data payload. | ### Cart Errors | Error Code | HTTP Status | Message | When It Occurs | | ------------------------------------------ | ----------- | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | | `empty_cart` | 400 | The cart is empty. | Attempting to submit or validate an empty cart. | | `invalid_cart` | 400 | Invalid items in the cart | Cart contains items that are invalid (unavailable, wrong inventory type, etc.). `extra_info.items` lists the problematic items. | | `invalid_cart_item` | 400 | Your order has one or more unavailable items | One or more cart items are no longer available for purchase. | | `invalid_item` | 404 | This product is no longer available | A specific product referenced in the cart no longer exists or is delisted. | | `out_of_stock` | 400 | This product is out of stock for your location | A cart item is out of stock at the relevant location/ZIP code. `extra_info` includes item details. | | `invalid_promo_code` | 400 | Coupon code does not exist. | The promotion/coupon code is invalid, expired, or not applicable. `extra_info.promo_code` echoes back the rejected code. | | `duplicate_promo_code` | 400 | Coupon already applied | The promo code has already been applied to this cart. | | `cannabis_weight_limit_exceeded` | 400 | Your order has exceeded the cannabis weight limit by {amount} {uom}. | Cart exceeds the legal cannabis weight limit for the jurisdiction. | | `cart_already_submitted` | 400 | This cart has already been submitted | Cart was already submitted as an order. `extra_info.order.uuid` contains the order ID. | | `cart_already_processing` | 400 | This cart is already being processed | A submission is already in progress for this cart. | | `cart_total_changed` | 400 | Your cart totals have changed, please confirm new values | Cart totals were recalculated and differ from what the user confirmed. Frontend should re-validate. | | `under_order_minimum` | 400 | Cart is under the minimum total ${amount}. | Cart total is below the store's minimum order amount. | | `cart_data_required_for_customer` | 400 | Additional shopping cart data is required for this payment processor | The payment processor requires additional cart data that is missing. | | `invalid_cart_submission_state_transition` | 400 | Cart submission status transition to '{next}' failed. | Cart submission state machine rejected the transition. | | `failed_to_submit_cart` | 400 | There was a unexpected problem submitting the cart. Please try again. | Server-side error during cart submission. | ### Order Errors | Error Code | HTTP Status | Message | When It Occurs | | -------------------------- | ----------- | ------------------------------------------------- | ------------------------------------------------------------------------------- | | `order_already_paid` | 400 | This order has already been paid. | Payment was attempted on an order that is already paid. | | `order_already_completed` | 400 | This order has already been completed. | An action was attempted on an already-completed order. | | `order_user_mismatch` | 400 | Order belongs to another user. | User tried to access or modify an order that belongs to a different account. | | `order_sync_limit_reached` | 400 | This order has reached the limit of days to sync. | Order can no longer be synced with the POS because the time window has expired. | | `related_order_not_found` | 404 | Related order not found. | A referenced order (e.g., for reorder or tip) does not exist. | | `already_tipped` | 400 | Tip already processed. | A tip was already applied to this order. | ### Payment Errors | Error Code | HTTP Status | Message | When It Occurs | | ----------------------------------- | ----------- | ---------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- | | `payment_failed` | 400 | Payment authorization failed. | Payment authorization or capture was declined by the payment processor. `extra_info` may include `order.uuid`. | | `missing_payment_source` | 400 | Online payment requires a payment source. | No payment source (card, bank) was provided for an online payment. | | `invalid_payment_source` | 400 | Some required fields are missing for adding the payment source. | Payment source is missing required fields. `fields` lists the missing ones. | | `missing_payment_source_identifier` | 400 | Online payment requires a payment source id or token | Neither a payment source ID nor a tokenized card was provided. | | `expired_payment_token` | 400 | Online payment card has expired. Please try again. | The payment card token has expired. | | `invalid_payment_token` | 400 | Online payment card is invalid. Please try another one. | The payment card token is invalid or rejected. | | `payment_source_not_found` | 400 | Online payment source not found. | The referenced payment source does not exist. | | `payment_customer_not_found` | 400 | Online payment customer not found. | No matching customer record exists at the payment processor. | | `payment_option_not_found` | 400 | Online payment option not found. | The selected payment option is not configured for this store. | | `charge_not_authorized` | 400 | Payment not yet authorized. | Attempting to capture a payment that has not been authorized. | | `charge_canceled` | 400 | Payment canceled by {service}. | The payment was canceled by the payment service. | | `no_match_for_payment` | 400 | The given payment does not match the associated order. | Payment details don't match the order they're being applied to. | | `no_match_for_payment_customer` | 400 | Customer does not match the one from the external payment source data. | Customer on the order doesn't match the customer on the payment source. | | `guest_cannot_add_source` | 400 | Cannot add sources in guest checkout. | Guest checkout users cannot save payment sources. | | `missing_billing_address` | 400 | Billing address is required. | Payment processor requires a billing address. | | `missing_payment_postal_code` | 400 | Online payment requires a postal code. | Payment processor requires a postal code for card verification. | | `missing_payment_cres` | 400 | Invalid challenge result (cres) | 3D Secure challenge response is missing or invalid. | | `missing_payment_cavv` | 400 | Can't authenticate cardholder | 3D Secure CAVV (cardholder authentication) is missing. | | `missing_cardholder_name` | 400 | Missing cardholder name | Cardholder name is required by the payment processor. | | `missing_transaction_id` | 400 | Online payment requires a transaction ID. | Transaction ID is required but was not provided. | | `missing_customer_signature` | 400 | Missing customer signature | Customer signature is required for this payment method. | | `missing_customer_auth_key` | 400 | Missing customer auth key | Customer authentication key is required for bank account operations. | | `missing_curstomer_return_url` | 400 | Missing return URL for bank account connection | Return URL is required for bank account linking flow. | | `multiple_bank_accounts` | 400 | Only a single active bank account is supported for making payments | User has multiple active bank accounts but the processor only supports one. | | `invalid_payment_secret_key` | 400 | External payment secret key is invalid. | Payment provider secret key configuration is incorrect. | | `invalid_payment_public_key` | 400 | External payment public key is invalid. | Payment provider public key configuration is incorrect. | | `not_found_payment_config` | 404 | Payment configuration was not found. | No payment configuration exists for the store. | | `invalid_payment_config` | 400 | External payment configuration is invalid. | Payment configuration is malformed or incomplete. | | `invalid_payment_account_config` | 400 | External payment configurations in a group must be for the same payment service account. | Group stores have mismatched payment accounts. | | `tips_not_allowed` | 400 | Tips are not allowed for the selected payment option | The payment method does not support tipping. | | `promotions_not_allowed` | 400 | Promotions are not allowed for the selected payment option | The payment method does not support promotions. | | `invalid_gift_card_amount` | 400 | Invalid gift card amount. | Gift card amount is invalid (e.g., zero, negative, or exceeds limits). | ### Delivery & Address Errors | Error Code | HTTP Status | Message | When It Occurs | | --------------------------- | ----------- | ---------------------------------------------------------------------- | ---------------------------------------------------------------------------- | | `missing_zip_code` | 400 | Zip code is required. | Delivery address is missing a ZIP/postal code. | | `invalid_zip_code` | 400 | Zip code is invalid for {country}. | ZIP/postal code format is invalid for the specified country. | | `missing_address` | 400 | Address is required. | Delivery address is required but not provided. | | `geo_data_required` | 400 | Delivery address geo location data is required for this delivery mode. | Delivery mode requires latitude/longitude coordinates. | | `zip_code_required` | 400 | Delivery address Zip Code is required for this delivery mode. | Delivery mode requires a ZIP code for zone-based delivery. | | `no_deliveries_at_location` | 400 | Sorry, we don't deliver to that location. | The delivery address is outside the store's delivery area. | | `no_delivery_fee` | 400 | We don't do deliveries to that location. | No delivery fee is configured for the given location (delivery unavailable). | | `schedule_closed` | 400 | Sorry, our schedule is closed for express delivery. | Express delivery is not available at the current time. | ### Store & Configuration Errors | Error Code | HTTP Status | Message | When It Occurs | | --------------------------------------- | ----------- | ----------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | | `bad_request` | 400 | Missing X-Store header. | The required `X-Store` header was not included in the request. | | `bad_request` | 400 | Missing X-Group header. | The required `X-Group` header was not included (group endpoints). | | `bad_request` | 400 | Missing X-App-Mode header. | The required `X-App-Mode` header was not included. | | `bad_request` | 400 | Missing X-Kiosk header. | The required `X-Kiosk` header was not included (kiosk endpoints). | | `not_found` | 404 | Store does not exist. | The store UUID in `X-Store` doesn't match any store. | | `not_found` | 404 | Group does not exist. | The group UUID in `X-Group` doesn't match any group. | | `not_active` | 404 | Store is not active. | The store exists but is deactivated. | | `bad_request` | 404 | Group is inactive. | The group exists but is inactive. | | `not_found` | 404 | Kiosk does not exist. | The kiosk UUID in `X-Kiosk` doesn't match any kiosk. | | `not_active` | 400 | Kiosk is not active. | The kiosk exists but is deactivated. | | `kiosk_not_from_store` | 400 | Kiosk does not belong to the specified store. | The kiosk UUID doesn't belong to the store specified in `X-Store`. | | `kiosk_not_allowed` | 400 | This store does not allow Kiosks | Kiosk mode is not enabled for this store. | | `developer_key_invalid` | 400 | Developer Key is invalid. | The Blaze developer key is invalid. | | `blaze_developer_key_missing` | 400 | Blaze developer key is missing. | Blaze developer key was not provided for POS integration. | | `invalid_pos_api_key` | 400 | Online Store Code is invalid. | The POS API key/store code is invalid. | | `invalid_integration` | 400 | Invalid POS integration. | The POS integration type is not recognized. | | `store_pos_config_mismatch` | 400 | The shop for the given keys does not match. | POS configuration keys don't match the store. | | `another_pos_is_configured` | 400 | Store already has another POS configured. | Trying to add a POS when one is already configured. | | `only_one_pos_allowed` | 400 | Only one POS can be integrated with a store. | Multiple POS integrations are not supported. | | `not_available_for_pos` | 400 | {pos} POS does not support this action. | The requested action is not available for the store's POS type. | | `missing_store_site` | 400 | Store requires a site for integrations. | Store needs a site configured before integrations can be set up. | | `page_exists` | 400 | footer link already exists | A store page with the same name already exists. | | `group_name_already_exists` | 400 | The given group name already exists. | Group creation failed because the name is taken. | | `missing_pos_integration_data` | 400 | Missing POS integration data. | POS integration data is required but not provided. | | `group_already_has_plan` | 400 | The given group already has a Plan ({existing}), and it's different from the one specified ({given}). | Group already has a different plan assigned. | | `deployment_integration_not_configured` | 400 | There is no deployment integration configured. | Deployment action requires an integration that hasn't been set up. | | `no_sms_provider` | 400 | SMS provider not defined in the store | Store has no SMS provider configured for sending verification codes. | | `inactive_service_config` | 400 | {service} configuration is inactive. | A required third-party service configuration is inactive. | | `missing_service_config` | 400 | {service} configuration is missing. | A required third-party service configuration doesn't exist. | | `another_rewards_system_is_configured` | 400 | Please deactivate the currently active reward service. | Can't add a new rewards system while another is active. | | `invalid_service_config` | 400 | API Key configuration is invalid. | Third-party service API key is invalid. | | `invalid_template_key` | 400 | Berbix template key configuration is invalid. | Identity verification template key is misconfigured. | | `sh_deferred_capture_is_on` | 400 | This cannot be changed while the POS Split Payment setting is active. | Configuration change blocked because POS split payment is active. | | `pos_not_allowed_for_reset` | 400 | POS is not allowed for account reset | The store's POS type does not support account reset. | ### Marketing & Promotions Errors | Error Code | HTTP Status | Message | When It Occurs | | -------------------------------- | ----------- | ------------------------------------------------------- | ------------------------------------------------------------------ | | `invalid_marketing_source` | 400 | The given marketing source is not valid for this store. | Marketing source value is not in the store's configured list. | | `required_marketing_source` | 400 | The marketing source is required in this store. | Store requires a marketing source but none was provided. | | `marketing_source_not_allowed` | 400 | Marketing source is not allowed in this store. | Marketing source feature is disabled for this store. | | `invalid_update_campaign_status` | 400 | Only draft or scheduled campaigns can be edited. | Attempting to edit a campaign that is already sent or in progress. | | `target_stores_not_from_group` | 400 | Target stores ({stores}) are not from this group. | Campaign targets stores outside the group. | ### Loyalty & Rewards Errors | Error Code | HTTP Status | Message | When It Occurs | | ---------------------------- | ----------- | ------------------------------------------------------------------------- | ------------------------------------------------------------- | | `no_loyalty` | 400 | No Loyalty provider found for store. | Store has no loyalty program configured. | | `no_growth` | 400 | No Growth provider found for store. | Store has no Growth loyalty/rewards provider configured. | | `multiple_matching_accounts` | 400 | Multiple ECOM accounts found for matching member. Please contact support. | Loyalty member matches multiple ECOM accounts. | | `member_is_banned` | 400 | Unable to check in customer | Loyalty member is banned. `extra_info.member_id` is included. | | `member_not_in_queue` | 400 | Unable to check in customer | Loyalty member is not in the check-in queue. | ### Identity Verification Errors | Error Code | HTTP Status | Message | When It Occurs | | ------------------------------------ | ----------- | ---------------------------------------------------------------- | -------------------------------------------------- | | `missing_identity_verification_data` | 400 | Missing identity verification data | Required identity verification fields are missing. | | `invalid_document_type` | 400 | Invalid document type | The uploaded document type is not accepted. | | `invalid_file_format` | 400 | Invalid file format | The uploaded file format is not supported. | | `s3_key_length_exceeded` | 400 | Upload failed. The URL is larger than what is currently allowed. | File upload URL exceeds length limits. | ### General Errors | Error Code | HTTP Status | Message | When It Occurs | | ------------------------------------- | ----------- | ------------------------------------------------------- | --------------------------------------------------------------------------------------------- | | `bad_request` | 400 | Invalid request parameters | Generic invalid request — usually malformed JSON or missing required fields. | | `not_found` | 404 | Not found. | The requested resource does not exist. | | `forbidden` | 403 | Client not allowed to access the requested resource | User does not have permission for the requested action. | | `required` | 400 | Field is required. | A required field is missing. `fields` specifies which one. | | `not_supported` | 400 | Functionality not supported | The requested functionality is not available. | | `invalid_value` | 400 | Invalid value for {field}. | A field has an invalid value. `fields` specifies which field and valid choices may be listed. | | `product_recommendations_not_allowed` | 400 | Product recommendations are not allowed for this store. | Product recommendation feature is disabled. | | `unsupported_geometry_type` | 400 | Unsupported geometry type. | GeoJSON geometry type is not supported (delivery zones). | | `cannot_update_keys` | 400 | Cannot update payment configuration keys. | Payment config keys are locked and cannot be changed. | | `not_implemented` | 400 | Export is not implemented for {resource}. | Data export is not available for the requested resource type. | | `invalid_data_for_webhook_url` | 400 | Can't build the webhook URLs. | Webhook URL generation failed due to missing configuration. | ### Changeset Validation Errors Ecto changeset errors are returned with HTTP status `422` and a `null` code. These are field-level validation errors that occur on create and update operations. ```json { "errors": [ { "code": null, "status": 422, "detail": "can't be blank", "fields": ["email"], "extra_info": {} } ] } ``` Common changeset validation messages include: | Message | When It Occurs | | --------------------------------- | -------------------------------------------------------------------- | | `can't be blank` | A required field was not provided. | | `has already been taken` | A unique field (email, phone) already exists. | | `is invalid` | Field value doesn't match the expected type or format. | | `has invalid format` | Field value doesn't match the expected pattern (e.g., email format). | | `must be at least N character(s)` | String field is shorter than the minimum length. | | `must be at most N character(s)` | String field exceeds the maximum length. | | `is not a valid email` | Email format validation failed. | --- ## Handling Errors ### General Strategy 1. **Check `status` first** — Route to the appropriate handler based on HTTP status code. 2. **Match on `code`** — Use the machine-readable `code` for specific error handling logic. 3. **Display `detail`** — Show the `detail` message to the user. 4. **Use `extra_info`** — Leverage additional context when available (e.g., redirect to an order on `cart_already_submitted`). ### JavaScript Example ```javascript async function handleApiResponse(response) { if (response.ok) return response.json(); const body = await response.json(); const errors = body.errors || []; const firstError = errors[0]; switch (response.status) { case 401: // Session expired or invalid credentials if (firstError?.code === "inactive_user") { showNotification("Your account has been deactivated."); } else { clearAuth(); redirectToLogin(); } break; case 403: redirectToLogin(); break; case 404: showNotification("The requested resource was not found."); break; case 429: const retryAfter = response.headers.get("Retry-After") || 30; await delay(retryAfter * 1000); // Retry the request break; default: // Handle by error code handleErrorByCode(firstError); } } function handleErrorByCode(error) { switch (error?.code) { case "cart_already_submitted": const orderUuid = error.extra_info?.order?.uuid; if (orderUuid) redirectToOrder(orderUuid); break; case "cart_total_changed": revalidateCart(); break; case "phone_number_requires_confirmation": case "email_requires_confirmation": redirectToVerification(); break; case "out_of_stock": case "invalid_cart_item": case "invalid_cart": refreshCart(); showNotification(error.detail); break; default: showNotification(error?.detail || "An error occurred."); } } ``` ### Error Code Constants Define error codes as constants to avoid typos: ```javascript const ErrorCodes = { EMPTY_CART: "empty_cart", INVALID_CART: "invalid_cart", INVALID_CART_ITEM: "invalid_cart_item", OUT_OF_STOCK: "out_of_stock", CART_ALREADY_SUBMITTED: "cart_already_submitted", CART_TOTAL_CHANGED: "cart_total_changed", INVALID_PROMO_CODE: "invalid_promo_code", DUPLICATE_PROMO_CODE: "duplicate_promo_code", PAYMENT_FAILED: "payment_failed", PHONE_REQUIRES_CONFIRMATION: "phone_number_requires_confirmation", EMAIL_REQUIRES_CONFIRMATION: "email_requires_confirmation", INVALID_TOKEN: "invalid_token", BAD_LOGIN: "bad_login", INACTIVE_USER: "inactive_user", MISSING_AUTHORIZATION: "missing_authorization_header", NO_DELIVERIES_AT_LOCATION: "no_deliveries_at_location", }; ``` ### Best Practices 1. **Always check the `errors` array** — Never assume a single error. Multiple validation errors can occur simultaneously. 2. **Use `code` for logic, `detail` for display** — Codes are stable; messages may change between versions. 3. **Handle 401 globally** — Set up a response interceptor that catches 401s and redirects to login. 4. **Handle `cart_already_submitted` gracefully** — Use the `extra_info.order.uuid` to redirect the user to their order instead of showing a raw error. 5. **Handle `cart_total_changed` by re-validating** — When prices change between cart load and submission, re-fetch the cart and ask the user to confirm. 6. **Don't ignore `fields`** — When `fields` is populated, highlight the relevant form fields for the user. 7. **Log `extra_info.api_error_code`** — POS-originated errors include an `api_error_code` that can help your support team diagnose issues. # Page: Full Object Index # Full Object Index A comprehensive reference of every resource type, schema, enum, query parameter, and header in the Blaze ECOM Storefront API. ## What you'll learn - Every JSON:API `type` value the API returns and what it represents - All schemas defined in the OpenAPI spec, grouped by domain, with their fields and types - Every enum value used across the API - Common query parameters available for filtering and pagination - Required and optional headers for API requests ## Prerequisites - Familiarity with the [JSON:API specification](https://jsonapi.org/) - Read the [Quick Start guide](../guides/quick-start.md) to understand the basic request/response flow - A valid `X-Store` header value (store UUID) — most endpoints are store-scoped --- ## Resource Types Every response from the API wraps data in JSON:API resource objects. The `type` field identifies the kind of resource. Here is a complete list of all resource types the API returns. ### Core Commerce | Type | Description | Key endpoint(s) | | -------------------- | ---------------------------------------------------------------------------- | -------------------------------------------------------------------- | | `stores` | A retail store with location, hours, and configuration | `GET /api/v1/store`, `GET /api/v1/groups/stores` | | `store_products` | A product available at a store | `GET /api/v1/products`, `GET /api/v2/products/{id}` | | `product_categories` | A product category (e.g. Flower, Edibles) | `GET /api/v1/products/categories`, `GET /api/v2/products/categories` | | `product_brands` | A store-level product brand | `GET /api/v1/products/brands`, `GET /api/v2/products/brands` | | `global_brands` | A global brand (cross-store), sideloaded via `included` | `GET /api/v2/products/{id}` (included) | | `product_types` | A product type classification (e.g. Hybrid, Indica) | `GET /api/v1/products/types` | | `product_images` | A product image, sideloaded via `included` | `GET /api/v2/products/{id}` (included) | | `tags` | A product tag (e.g. new-arrival, staff-pick) | `GET /api/v1/products/tags` | | `price_ranges` | Min/max price range for the store catalog | `GET /api/v1/products/price-ranges` | | `filters` | Filter catalog metadata (categories, types, brands, weights, potency ranges) | `GET /api/v1/products/filters`, `GET /api/v2/products/filters` | | `showcased_products` | A curated product showcase group | `GET /api/v1/products/showcased` | ### Cart & Orders | Type | Description | Key endpoint(s) | | ------------- | -------------------------------------------------- | -------------------------------------------------- | | `carts` | A shopping cart | `POST /api/v5/carts`, `GET /api/v5/carts/{uuid}` | | `cart_items` | An item within a cart, sideloaded via `included` | `POST /api/v5/carts/{cart_uuid}/items` | | `orders` | A placed order | `POST /api/v4/orders`, `GET /api/v1/orders/{uuid}` | | `order_items` | An item within an order, sideloaded via `included` | `GET /api/v1/orders/{uuid}` (included) | ### User & Account | Type | Description | Key endpoint(s) | | ------------------------ | ---------------------------- | ----------------------------------------------------------------- | | `users` | A user account | `GET /api/v1/users/me`, `POST /api/v1/auth/login` | | `billing` | User billing information | `GET /api/v1/users/me/billing` | | `loyalty` | User loyalty points and tier | `GET /api/v3/users/me/loyalty` | | `rewards` | A redeemable reward | `GET /api/v1/users/me/rewards`, `GET /api/v1/store/deals/rewards` | | `identity_verifications` | Identity verification status | `GET /api/v1/users/me/identity-verification` | ### Payments | Type | Description | Key endpoint(s) | | ------------------------------- | ------------------------------------------------ | ------------------------------------------------------------- | | `payment_options` | A payment method available at the store | `GET /api/v1/store/payment-options` | | `payment_sources` | A saved payment source (card, bank account) | `GET /api/v1/store/payments/{service}/sources` | | `payment_tokens` | A client-side payment provider token | `GET /api/v1/store/payments/{service}/token` | | `payment_sessions` | A payment session for client-side initialization | `POST /api/v1/store/payments/{service}/sessions` | | `payments` | A payment charge result | `POST /api/v1/store/payments/{service}/orders/{uuid}/pay` | | `payment_promotions` | A payment-service-specific promotion | `GET /api/v1/store/payments/{service}/promotions` | | `payment_redeemable_promotions` | Request type for checking redeemable promotions | `POST /api/v1/store/payments/{service}/promotions/redeemable` | | `tips` | A tip charge result | `POST /api/v1/store/payments/{service}/tip` | ### Deals | Type | Description | Key endpoint(s) | | ------------ | ------------------------- | ------------------------------------ | | `promotions` | A store promotion or deal | `GET /api/v1/store/deals/promotions` | ### Store Content | Type | Description | Key endpoint(s) | | ---------------------- | ------------------------------------------------- | -------------------------------------------------- | | `store_settings` | Store settings and feature flags | `GET /api/v1/store/settings` | | `store_configurations` | Store configuration including group and site info | `GET /api/v1/store/configuration` | | `schedules` | Weekly schedule entries for a store | `GET /api/v1/store/schedules/{schedule_type}` | | `availabilities` | Available time slots for pickup or delivery | `GET /api/v1/store/availabilities/{schedule_type}` | | `promotional_banners` | A promotional banner for the storefront | `GET /api/v1/store/site/promotional-banners` | | `social_networks` | A store social network link | `GET /api/v1/store/socials` | | `store_pages` | A custom store page (About, FAQ, etc.) | `GET /api/v1/store/pages` | ### Delivery | Type | Description | Key endpoint(s) | | ----------------- | --------------------------------------------- | -------------------------------- | | `delivery_stores` | Delivery availability result for a store (v4) | `POST /api/v4/deliveries/stores` | | `addresses` | Request type for delivery store checks | `POST /api/v3/deliveries/stores` | ### Notifications | Type | Description | Key endpoint(s) | | --------------- | ------------------------------------- | ----------------------------------- | | `devices` | A registered push notification device | `POST /api/v1/users/me/devices` | | `notifications` | An informational/notification message | Verification and recovery responses | ### Ads | Type | Description | Key endpoint(s) | | ----------------- | -------------------------------------------- | --------------------------------- | | `recommendations` | Request type for sponsored product endpoints | `POST /api/v1/products/sponsored` | --- ## Schemas All schemas defined in `components/schemas` of the OpenAPI specification, grouped by domain. ### JSON:API Envelope These schemas define the standard JSON:API document structure used by all endpoints. #### JsonApiDocument Standard JSON:API response document. - `data` — `JsonApiResource | JsonApiResource[]` — The primary resource(s) - `included` — `JsonApiResource[]` — Sideloaded related resources - `meta` — `JsonApiMeta` — Pagination metadata #### JsonApiResource A single JSON:API resource object. - `id` — `string` — Unique identifier (UUID). **Required** - `type` — `string` — Resource type name (e.g. `"products"`, `"carts"`). **Required** - `attributes` — `object` — Resource-specific fields. **Required** - `relationships` — `object` — Related resources #### JsonApiRelationship A JSON:API relationship. - `data` — `JsonApiResourceIdentifier | JsonApiResourceIdentifier[] | null` — Related resource identifier(s) #### JsonApiResourceIdentifier A JSON:API resource identifier (type + id only). - `id` — `string` — **Required** - `type` — `string` — **Required** #### JsonApiMeta Pagination metadata. - `total` — `integer` — Total number of matching results - `limit` — `integer` — Maximum results per page - `offset` — `integer` — Current offset #### JsonApiRequestBody Standard JSON:API request body. - `data.id` — `string` — Resource ID (required for updates) - `data.type` — `string` — Resource type name. **Required** - `data.attributes` — `object` — Resource fields to create or update. **Required** - `data.relationships` — `object` — Related resources #### JsonApiErrorResponse Standard JSON:API error response. - `errors` — `JsonApiError[]` — Array of error objects #### JsonApiError A single JSON:API error object. - `code` — `string` — Machine-readable error code (e.g. `"bad_login"`, `"not_found"`) - `status` — `string` — HTTP status code as a string (e.g. `"400"`) - `detail` — `string` — Human-readable error message - `source.pointer` — `string` — JSON pointer to the field that caused the error (e.g. `"/data/attributes/email"`) - `extra_info` — `object` — Additional error context (e.g. `api_error_code`, `items`) --- ### Authentication #### LoginRequest - `data.type` — `string` — Must be `"users"`. **Required** - `data.attributes.email` — `string (email)` — User email (provide email OR phone_number) - `data.attributes.phone_number` — `string` — Phone number with country code (e.g. `"+15551234567"`) - `data.attributes.password` — `string (password)` — User password. **Required** #### LoginResponse - `data.id` — `string` - `data.type` — `string` — `"users"` - `data.attributes.token` — `string` — JWT access token - `data.attributes.email` — `string` - `data.attributes.first_name` — `string` - `data.attributes.last_name` — `string` #### VerificationRequest - `data.type` — `string` — Must be `"users"`. **Required** - `data.attributes.phone_number` — `string` — Phone number with country code - `data.attributes.email` — `string (email)` — Email address #### VerificationCheckRequest - `data.type` — `string` — Must be `"users"`. **Required** - `data.attributes.code` — `string` — Verification code received via SMS or email. **Required** - `data.attributes.email` — `string (email)` - `data.attributes.phone_number` — `string` - `data.attributes.verification_mode` — `string` — Verification mode (e.g. `"sms"`, `"email"`) - `data.attributes.password` — `string (password)` - `data.attributes.password_confirmation` — `string (password)` - `data.attributes.first_name` — `string` - `data.attributes.last_name` — `string` - `data.attributes.date_of_birth` — `integer` — Unix timestamp in milliseconds - `data.attributes.marketing_source` — `string` - `data.attributes.marketing_email_opt_in` — `boolean` - `data.attributes.marketing_sms_opt_in` — `boolean` - `data.attributes.state_residency` — `string` - `data.attributes.identity_verification_id` — `string | null` #### RecoverPasswordRequest - `data.type` — `string` — Must be `"users"`. **Required** - `data.attributes.email` — `string (email)` — Email address (provide email OR phone_number) - `data.attributes.phone_number` — `string` — Phone number with country code #### ResetPasswordRequest - `data.type` — `string` — Must be `"users"`. **Required** - `data.attributes.password` — `string (password)` — **Required** - `data.attributes.password_confirmation` — `string (password)` — **Required** - `data.attributes.email` — `string (email)` - `data.attributes.phone_number` — `string` - `data.attributes.code` — `string` — Verification code (when resetting via phone) #### TokenResponse - `data.id` — `string` - `data.type` — `string` - `data.attributes.token` — `string` — JWT access token #### NotificationResponse - `data.id` — `string` - `data.type` — `string` — `"notifications"` - `data.attributes.message` — `string` - `data.attributes.level` — `string` — One of: `info`, `warning`, `error` --- ### Store #### StoreAttributes - `name` — `string` — Store display name - `slug` — `string` — URL-friendly store identifier - `address` — `string` - `city` — `string` - `state` — `string` - `zip_code` — `string` - `country` — `string` - `latitude` — `number (double)` - `longitude` — `number (double)` - `phone` — `string` - `email` — `string` - `is_active` — `boolean` - `point_of_sales` — `string` — POS provider (`blaze`, `treez`, `leaflogix`, `greenline`, `cova`) - `delivery_enabled` — `boolean` - `pickup_enabled` — `boolean` - `timezone` — `string` — e.g. `"America/Los_Angeles"` #### StoreSettingsResponse - `data.type` — `string` — `"store_settings"` - `data.attributes` — `object` — Dynamic key-value settings (varies per store configuration) #### StoreConfigurationResponse - `data.type` — `string` — `"store_configurations"` - `data.attributes` — `object` — Store configuration fields including group and site data #### ScheduleEntry - `id` — `string` - `type` — `string` — `"schedules"` - `attributes.weekday` — `string` — Day of the week (e.g. `"monday"`) - `attributes.is_closed` — `boolean` - `attributes.intervals` — `array` — Open/close time intervals - `start` — `string` — Start time in HH:MM format - `end` — `string` — End time in HH:MM format #### PaymentOptionAttributes - `id` — `string` - `name` — `string` - `service` — `string` — Payment provider service name - `is_active` — `boolean` - `is_default` — `boolean` - `payment_type` — `string` — Payment type (e.g. `"credit_card"`, `"debit"`, `"cash"`) #### SocialNetworkAttributes - `name` — `string` — Social network name (e.g. `"instagram"`, `"facebook"`) - `url` — `string (uri)` #### StorePageAttributes - `id` — `string` - `title` — `string` - `slug` — `string` - `content` — `string` — Page content (may contain HTML) - `is_active` — `boolean` - `position` — `integer` #### PromotionalBannerAttributes - `id` — `string` - `title` — `string | null` - `subtitle` — `string | null` - `image_url` — `string (uri)` - `link_url` — `string | null` - `position` — `integer` - `is_active` — `boolean` #### AvailabilitySlot - `date` — `string (date)` - `slots` — `array` — Time slot list - `start` — `string` — Slot start time in HH:MM format - `end` — `string` — Slot end time in HH:MM format - `available` — `boolean` --- ### Products #### ProductAttributes - `name` — `string` — Product display name - `slug` — `string` — URL-friendly product identifier - `sku` — `string` — Stock keeping unit code - `description` — `string` — Product description (may contain HTML) - `external_id` — `string` — External ID from the POS system - `type` — `string` — Product type (e.g. `"Hybrid"`, `"Indica"`, `"Sativa"`) - `strain` — `string | null` — Cannabis strain name - `flower_type` — `string | null` — Flower type classification - `composition` — `string | null` — Product form (e.g. `"Flower"`, `"Edible"`, `"Concentrate"`) - `thc` — `string | null` — THC percentage or range - `cbd` — `string | null` — CBD percentage or range - `min_cbd` — `number (double) | null` — Minimum CBD percentage - `potency` — `string | null` — Overall potency level - `terpenoids` — `string[]` — List of terpenoid profiles - `cannabis_weight` — `number (double) | null` — Cannabis net weight - `size` — `string | null` — Product size label - `main_image` — `string (uri) | null` — URL to the main product image - `in_stock` — `boolean` — Whether the product is currently in stock - `is_promoted` — `boolean` — Whether the product is promoted/featured - `discount` — `number (double) | null` — Discount percentage - `unit_price` — `number (double) | null` — Base unit price - `unit_prices` — `UnitPriceItem[]` — Detailed pricing per unit/quantity - `weight_prices` — `WeightPriceItem[]` — Pricing per weight tier #### UnitPriceItem - `display_name` — `string` — Human-readable label (e.g. `"1 unit"`, `"1/8 oz"`) - `quantity` — `number (double)` - `price.amount` — `number (double)` - `price.currency` — `string` — e.g. `"USD"` - `discount_price` — `number (double) | null` - `savings_per_unit` — `number (double) | null` #### WeightPriceItem - `weight` — `number (double)` - `weight_unit` — `string` - `price` — `number (double)` - `original_price` — `number (double) | null` - `in_stock` — `boolean` #### ProductVariant - `id` — `string` - `name` — `string` - `price` — `number (double)` - `original_price` — `number (double)` - `weight` — `number (double)` - `weight_unit` — `string` - `in_stock` — `boolean` - `quantity_available` — `integer` #### CategoryAttributes - `id` — `string` - `name` — `string` - `slug` — `string` - `description` — `string | null` - `count` — `integer` — Number of products in this category - `position` — `integer` - `is_active` — `boolean` - `icon_url` — `string (uri) | null` - `parent_category_id` — `string | null` #### BrandAttributes - `id` — `string` - `name` — `string` - `slug` — `string` - `description` — `string | null` - `external_id` — `string | null` - `logo_url` — `string (uri) | null` - `count` — `integer` — Number of products for this brand - `is_promoted` — `boolean` #### TagAttributes - `name` — `string` - `title` — `string | null` - `description` — `string | null` - `count` — `integer` - `position` — `integer` - `is_active` — `boolean` - `is_featured` — `boolean` - `is_hidden` — `boolean` #### TypeAttributes - `id` — `string` — Type identifier (e.g. `"Hybrid"`) - `name` — `string` - `count` — `integer` #### PriceRangeAttributes - `min` — `number (double)` - `max` — `number (double)` #### ShowcasedGroupAttributes - `name` — `string` - `slug` — `string` - `type` — `string` — Showcase grouping type - `description` — `string | null` --- ### Filters #### FiltersV2Response - `data.type` — `string` — `"filters"` - `data.attributes.price_ranges` — `PriceRangeAttributes` — Min/max price range - `data.attributes.weights` — `number[]` — Available weight options - `data.attributes.thc_ranges` — `object` — THC range filter - `min` — `number` - `max` — `number` - `unit` — `string` - `data.attributes.cbd_ranges` — `object` — CBD range filter - `min` — `number` - `max` — `number` - `unit` — `string` - `data.attributes.on_sale` — `object` - `count` — `integer` - `data.relationships.types` — `JsonApiRelationship` - `data.relationships.tags` — `JsonApiRelationship` - `data.relationships.categories` — `JsonApiRelationship` - `data.relationships.brands` — `JsonApiRelationship` - `included` — `JsonApiResource[]` — Sideloaded types, tags, categories, brands --- ### Cart #### CartAttributes - `status` — `string` — Cart status (e.g. `"open"`, `"validated"`, `"submitted"`) - `delivery_type` — `string | null` — Selected delivery type (e.g. `"pickup"`, `"delivery"`) - `promo_codes` — `string[]` — Applied promotional codes - `reward_id` — `string | null` — Applied reward ID - `subtotal` — `number (double) | null` — Cart subtotal before taxes and fees - `total` — `number (double) | null` — Cart total including taxes and fees - `tax` — `number (double) | null` — Total tax amount - `discount` — `number (double) | null` — Total discount amount - `delivery_fee` — `number (double) | null` — Delivery fee - `item_count` — `integer` — Number of items in the cart - `delivery_specification` — `object | null` — Delivery specification (address, time slot, delivery type) #### CartCreateRequest - `data.type` — `string` — Must be `"carts"`. **Required** - `data.attributes.delivery_specification` — `object | null` — Initial delivery specification - `data.attributes.inventory_type` — `string | null` — Inventory type preference (e.g. `"delivery"`, `"pickup"`) - `data.attributes.conversion_breadcrumb` — `string | null` — Conversion tracking breadcrumb - `data.attributes.item` — `object` — First item to add. **Required** - `product_id` — `string` — Product UUID. **Required** - `quantity` — `integer` — Quantity (minimum: 1). **Required** - `weight` — `number (double) | null` — Weight selection for weight-based products #### CartUpdateRequest - `data.id` — `string` — Cart UUID. **Required** - `data.type` — `string` — Must be `"carts"`. **Required** - `data.attributes.promo_codes` — `string[]` — Promotional codes to apply - `data.attributes.reward_id` — `string | null` — Reward ID to apply #### CartItemRequest - `data.id` — `string` — Cart item UUID (required for updates) - `data.type` — `string` — Must be `"cart_items"`. **Required** - `data.attributes.product_id` — `string` — Product UUID. **Required** - `data.attributes.quantity` — `integer` — Item quantity (minimum: 1). **Required** - `data.attributes.weight` — `number (double) | null` — Weight for weight-based products #### CartItemAttributes - `product_id` — `string` — Product UUID - `quantity` — `integer` — Item quantity - `weight` — `number (double) | null` — Weight for weight-based products - `unit_price` — `number (double) | null` — Unit price - `total_price` — `number (double) | null` — Total price (unit_price × quantity) - `discount` — `number (double) | null` — Discount applied to this item #### DeliverySpecificationRequest - `data.id` — `string` — Cart UUID. **Required** - `data.type` — `string` — Must be `"carts"`. **Required** - `data.attributes.delivery_specification` — `object` — **Required** - `type` — `string` — Fulfillment type: `"pickup"`, `"delivery"`, or `"kiosk"`. **Required** - `mode` — `string` — Delivery mode: `"asap"`, `"scheduled"`, or `"express"`. **Required** (default: `"asap"`) - `address` — `object | null` — Customer delivery address (required for `delivery` type) - `address` — `string` — Street address - `address_line2` — `string | null` — Apartment, suite, unit - `city` — `string` - `state` — `string | null` - `zip_code` — `string` - `country` — `string` — (default: `"US"`) - `lat` — `number (double) | null` — Required for geo-zone stores - `lng` — `number (double) | null` — Required for geo-zone stores - `scheduled_start_time` — `string (date-time) | null` — Required when mode is `"scheduled"` - `scheduled_end_time` — `string (date-time) | null` — Required when mode is `"scheduled"` - `delivery_inventories` — `integer[] | null` — Optional inventory IDs (for express delivery) --- ### Delivery #### DeliveryStoresRequest - `data.type` — `string` — Must be `"addresses"`. **Required** - `data.attributes.address` — `object` — Delivery address - `address` — `string` — Street address - `address_line2` — `string | null` - `city` — `string` - `country` — `string` - `state` — `string` - `zip_code` — `string` - `lat` — `number (double) | null` - `lng` — `number (double) | null` - `data.attributes.preferred_inventories` — `string[] | null` — Preferred inventory types - `data.attributes.mode` — `string | null` — Delivery mode filter #### DeliveryStoreAttributes Returned by the v4 delivery stores endpoint with detailed availability info. - `delivery_zip_code` — `string | null` — The zip code that was checked - `available_products_at_zip_code_count` — `integer` — Products available for delivery - `open_schedule` — `boolean` — Whether the delivery schedule is currently open - `delivers_to_zip_code` — `boolean` — Whether the store delivers to the given address - `mode` — `string | null` — Delivery mode this result applies to (`"express"`, `"scheduled"`, or `null`) - `unavailable_reason` — `string | null` — Reason the store cannot deliver (`"not_delivering"`, `"closed"`, `"no_products"`, or `null`) - `alternative_mode` — `string | null` — An alternative delivery mode that is available - `next_open_schedule` — `string (date-time) | null` — Next time the delivery schedule opens - `chosen_delivery_inventories` — `integer[] | null` — Inventory IDs chosen for this delivery - `available_delivery_inventories` — `integer[] | null` — All inventory IDs available --- ### Orders #### OrderCreateRequest - `data.type` — `string` — Must be `"orders"`. **Required** - `data.attributes.cart_uuid` — `string` — Cart UUID to convert into an order. **Required** - `data.attributes.notes` — `string | null` — Customer order notes #### OrderAttributes - `status` — `string` — Order status (e.g. `"pending"`, `"confirmed"`, `"completed"`, `"cancelled"`) - `order_number` — `string | null` — Human-readable order number - `subtotal` — `number (double) | null` - `total` — `number (double) | null` - `tax` — `number (double) | null` - `discount` — `number (double) | null` - `delivery_fee` — `number (double) | null` - `delivery_type` — `string | null` — `"pickup"` or `"delivery"` - `delivery_date` — `string | null` — Scheduled delivery/pickup date - `delivery_time_slot` — `string | null` — Scheduled time slot - `notes` — `string | null` — Customer notes - `created_at` — `string (date-time)` - `updated_at` — `string (date-time)` --- ### User #### UserAttributes - `email` — `string (email) | null` - `phone_number` — `string | null` - `first_name` — `string | null` - `last_name` — `string | null` - `zip_code` — `string | null` - `address` — `object | null` — User address - `billing_address` — `object | null` — Billing address - `date_of_birth` — `integer | null` — Unix timestamp in milliseconds - `marketing_email_opt_in` — `boolean | null` - `marketing_sms_opt_in` — `boolean | null` - `medical_id` — `object | null` - `number` — `string` - `expiration_date` — `string (date) | null` - `drivers_license_id` — `object | null` - `number` — `string` - `expiration_date` — `string (date) | null` - `customer_type` — `string | null` - `is_confirmed` — `boolean` - `is_active` — `boolean` - `token` — `string | null` — JWT token (included in login/register responses) #### UserUpdateRequest - `data.type` — `string` — Must be `"users"`. **Required** - `data.attributes.first_name` — `string` - `data.attributes.last_name` — `string` - `data.attributes.zip_code` — `string` - `data.attributes.address` — `object` - `data.attributes.billing_address` — `object` - `data.attributes.date_of_birth` — `integer` — Unix timestamp in milliseconds - `data.attributes.marketing_email_opt_in` — `boolean` - `data.attributes.marketing_sms_opt_in` — `boolean` - `data.attributes.medical_id` — `object` — `{ number, expiration_date }` - `data.attributes.drivers_license_id` — `object` — `{ number, expiration_date }` #### BillingAttributes - `address` — `object | null` - `address` — `string` - `address_line2` — `string | null` - `city` — `string` - `state` — `string` - `zip_code` — `string` - `country` — `string` #### BillingUpdateRequest - `data.type` — `string` — Must be `"billing"`. **Required** - `data.attributes.address` — `object` - `address` — `string` - `address_line2` — `string | null` - `city` — `string` - `state` — `string` - `zip_code` — `string` - `country` — `string` #### LoyaltyAttributes - `points` — `number | null` — Current loyalty points balance - `tier` — `string | null` — Current loyalty tier - `lifetime_points` — `number | null` — Total points earned lifetime #### IdentityVerificationAttributes - `service` — `string` — Verification service (e.g. `"berbix"`) - `status` — `string | null` — Verification status - `identity_verification_id` — `string | null` — External verification ID - `client_token` — `string | null` — Client-side token for verification SDK #### IdentityVerificationCreateRequest - `data.type` — `string` — Must be `"identity_verifications"`. **Required** - `data.attributes.identity_verification_id` — `string | null` --- ### Payments #### PaymentSourceAttributes - `external_id` — `string | null` — External payment provider ID - `token` — `string | null` — Payment source token - `last_four` — `string | null` — Last four digits of card number - `card_type` — `string | null` — Card brand (e.g. `"visa"`, `"mastercard"`) - `expiration_month` — `integer | null` - `expiration_year` — `integer | null` - `is_default` — `boolean` — Whether this is the default payment source #### PaymentSourceRequest - `data.type` — `string` — Must be `"payment_sources"`. **Required** - `data.attributes` — `object` — Payment source fields (vary by provider) #### PaymentChargeRequest - `data.type` — `string` — Must be `"payments_charge"`. **Required** - `data.attributes` — `object` — Payment charge fields (vary by provider — may include `source_id`, `token`, `nonce`, `amount`) #### TipRequest - `data.type` — `string` — Must be `"payment_sources"`. **Required** - `data.attributes.external_id` — `string` — Order external ID - `data.attributes.percentage` — `number` — Tip percentage #### RedeemablePromotionsRequest - `data.type` — `string` — Must be `"payment_redeemable_promotions"`. **Required** - `data.attributes.charge_amount` — `number` — Charge amount to check promotions against --- ### Deals #### PromotionAttributes - `name` — `string` — Promotion name - `slug` — `string` — URL-friendly promotion identifier - `description` — `string | null` - `image_url` — `string (uri) | null` - `discount_type` — `string | null` — e.g. `"percentage"`, `"fixed_amount"` - `discount_value` — `number (double) | null` - `is_active` — `boolean` - `start_date` — `string (date-time) | null` - `end_date` — `string (date-time) | null` #### RewardAttributes - `name` — `string` — Reward name - `slug` — `string` — URL-friendly reward identifier - `description` — `string | null` - `image_url` — `string (uri) | null` - `points_required` — `integer | null` — Points required to redeem - `discount_type` — `string | null` — e.g. `"percentage"`, `"fixed_amount"` - `discount_value` — `number (double) | null` - `is_active` — `boolean` --- ### Notifications #### DeviceRegistrationRequest - `data.type` — `string` — Must be `"devices"`. **Required** - `data.attributes.device_token` — `string` — Push notification device token - `data.attributes.platform` — `string` — Device platform (e.g. `"ios"`, `"android"`, `"web"`) --- ### Ads & Sponsored #### SponsoredRequestBody Request body for sponsored product endpoints. - `data.type` — `string` — Must be `"recommendations"`. **Required** - `data.attributes.url` — `string` — Current page URL - `data.attributes.screen` — `object` - `height` — `integer` - `width` — `integer` - `data.attributes.navigator` — `object` - `user_agent` — `string` - `language` — `string` - `data.attributes.mobile` — `boolean` - `data.attributes.account_id` — `string | null` - `data.attributes.site_id` — `string | null` - `data.attributes.channel_id` — `string | null` - `data.attributes.channel_type` — `string | null` - `data.attributes.zone_id` — `string | null` - `data.attributes.client_ip` — `string | null` - `data.attributes.surfside_domain_id` — `string | null` - `data.attributes.session_id` — `string | null` - `data.attributes.store_location_data` — `object | null` - `data.attributes.location_data` — `object | null` - `zip` — `string | null` - `country` — `string | null` - `city` — `string | null` - `region` — `string | null` - `utc_offset` — `string | null` - `timezone` — `string | null` - `coords` — `object` - `latitude` — `number | null` - `longitude` — `number | null` - `accuracy` — `number | null` --- ### Webhooks #### WebhookPayload Generic webhook inbound payload. Content varies by event type — the schema uses `additionalProperties: true`. --- ## Enums All enumerated values used across the API. ### delivery_type Fulfillment type for carts and orders. - `pickup` — Customer picks up at the store - `delivery` — Store delivers to the customer's address - `kiosk` — Order placed at an in-store kiosk ### delivery_mode Delivery timing mode. - `asap` — Fulfill as soon as possible - `scheduled` — Fulfill at a chosen time slot (requires `scheduled_start_time` and `scheduled_end_time`) - `express` — Express delivery (requires geo-zone support) ### order_status Order lifecycle status values. - `pending` — Order placed, awaiting confirmation - `confirmed` — Order confirmed by the store - `ready_for_pickup` — Order is ready for customer pickup - `completed` — Order fulfilled - `cancelled` — Order cancelled ### cart_status Cart lifecycle status values. - `open` — Cart is active and editable - `validated` — Cart has been validated and is ready for checkout - `submitted` — Cart has been converted to an order ### schedule_type Type of schedule to query. - `pickup` — Pickup schedule - `delivery` — Delivery schedule - `operating` — General operating hours ### point_of_sales Supported POS (Point of Sale) providers. - `blaze` — Blaze POS - `treez` — Treez POS - `leaflogix` — LeafLogix POS - `greenline` — Greenline POS - `cova` — Cova POS ### payment_type Payment method types. - `credit_card` — Credit card - `debit` — Debit card - `cash` — Cash payment ### payment_service Payment provider service names. - `stronghold` — Stronghold - `moneris` — Moneris - `adyen` — Adyen - `aeropay` — Aeropay - `swifter` — Swifter - `ledgergreen` — LedgerGreen - `greenbax` — Greenbax - `merrco` — Merrco - `spence` — Spence - `cash` — Cash (no provider) ### discount_type Discount calculation types. - `percentage` — Percentage discount (e.g. 20% off) - `fixed_amount` — Fixed dollar amount discount (e.g. $5 off) ### notification_level Notification severity levels. - `info` — Informational message - `warning` — Warning message - `error` — Error message ### unavailable_reason Reasons a store cannot deliver to an address. - `not_delivering` — Store does not deliver to this area - `closed` — Store delivery schedule is currently closed - `no_products` — No products available for delivery at this address ### device_platform Push notification device platforms. - `ios` — Apple iOS - `android` — Android - `web` — Web browser ### identity_verification_service Supported identity verification services. - `berbix` — Berbix identity verification --- ## Common Query Parameters Parameters available across multiple endpoints for filtering, sorting, and pagination. ### Pagination - **`limit`** — `integer` — Maximum number of results to return. Default: `20`. Min: `1`, Max: `100` - **`offset`** — `integer` — Number of results to skip. Default: `0`. Min: `0` ### Sorting - **`order`** — `string` — Sort order for results. Common values: - `name_asc` — Sort by name ascending - `name_desc` — Sort by name descending - `price_asc` — Sort by price ascending - `price_desc` — Sort by price descending ### Product Filters These parameters are available on product listing endpoints (`GET /api/v1/products`, `POST /api/v1/products/sponsored`): - **`category`** — `string` — Filter by category slug or ID - **`subcategory`** — `string` — Filter by subcategory - **`product_type`** — `string` — Filter by product type (e.g. `"flower"`, `"edible"`) - **`brand`** — `string` — Filter by brand slug or ID - **`tags`** — `string` — Filter by tag (comma-separated for multiple) - **`min_price`** — `number` — Minimum price filter - **`max_price`** — `number` — Maximum price filter - **`search`** — `string` — Full-text search query ### Sponsored-Specific Filters Available on sponsored product endpoints: - **`type`** — `string` — Filter by product type - **`tag`** — `string` — Filter by tag - **`q`** — `string` — Full-text search query ### Other Filters - **`delivery_type`** — `string` — Filter payment options by delivery type (`pickup` or `delivery`). Used on `GET /api/v1/store/payment-options` --- ## Headers ### Required Headers - **`X-Store`** — `string (uuid)` — The UUID of the store to scope the request to. Required on all store-scoped endpoints. - Example: `X-Store: e87437f2-3e35-4738-af5e-6307e368255c` - **`Content-Type`** — `string` — Must be `application/vnd.api+json` for all request bodies. - **`Accept`** — `string` — Should be `application/vnd.api+json` for all requests. ### Conditional Headers - **`Authorization`** — `string` — JWT Bearer token for authenticated endpoints. Format: `Bearer {token}`. - Example: `Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...` - **`X-Group`** — `string (uuid)` — The UUID of the group. Required on group-scoped endpoints (e.g. `GET /api/v1/groups/stores`). - Example: `X-Group: a1b2c3d4-e5f6-7890-abcd-ef1234567890` - **`X-Kiosk`** — `string (uuid)` — The UUID of the kiosk device. Required for kiosk-specific endpoints. - Example: `X-Kiosk: k1o2s3k4-u5u6-i7d8-9012-abcdef123456` # Page: OpenAPI Endpoint Reference (Condensed) ## Condensed OpenAPI Endpoint Reference All 92 endpoints extracted from `openapi.yaml`. Format: `METHOD /path` — Summary — Auth — Query params. **Auth legend:** `default` = jwt_optional_authenticated (no token required), `bearerAuth` = jwt_authenticated (JWT required), `none, bearerAuth` = token optional but accepted. ### Authentication (7 endpoints) - `POST /api/v1/auth/login` — Login — default - `POST /api/v1/auth/register` — Register a new user — default - `POST /api/v1/auth/verification` — Request phone/email verification code — default - `POST /api/v1/auth/verification-check` — Submit verification code — default - `POST /api/v1/auth/recover_password` — Request password recovery — default - `POST /api/v1/auth/reset_password/{token}` — Reset password with token — default - `DELETE /api/v1/auth/logout` — Logout — bearerAuth ### Store & Configuration (11 endpoints) - `GET /api/v1/store` — Get store details — default - `GET /api/v1/store/settings` — Get store settings — default - `GET /api/v1/store/configuration` — Get store configuration — default - `GET /api/v1/store/schedules/{schedule_type}` — Get store schedules — default — Path: schedule_type (pickup|delivery) - `GET /api/v1/store/availabilities/{schedule_type}` — Get store availabilities — default — Path: schedule_type (pickup|delivery) - `GET /api/v1/store/payment-options` — List store payment options — default — Query: delivery_type - `GET /api/v1/store/site` — Get store site (storefront URL) — default - `GET /api/v1/store/site/promotional-banners` — List promotional banners — default - `GET /api/v1/store/socials` — List store social networks — default - `GET /api/v1/store/pages` — List store pages — default - `GET /api/v1/groups/stores` — List group stores — default — Header: X-Group required ### Products & Catalog (18 endpoints) - `GET /api/v1/products` — List products — default — Query: category, subcategory, product_type, brand, tags, min_price, max_price, search, limit, offset, order - `GET /api/v1/products/{id}` — Get product detail — default - `GET /api/v2/products/{id}` — Get product detail (v2, recommended) — default - `GET /api/v1/products/categories` — List product categories — default - `GET /api/v2/products/categories` — List product categories (v2) — default - `GET /api/v1/products/brands` — List product brands — default - `GET /api/v2/products/brands` — List product brands (v2) — default - `GET /api/v1/products/filters` — Get available filters — default - `GET /api/v2/products/filters` — Get filter catalog (v2, recommended) — default - `GET /api/v1/products/types` — List product types — default - `GET /api/v1/products/tags` — List product tags — default - `GET /api/v1/products/price-ranges` — Get price ranges — default - `GET /api/v1/products/showcased` — Get showcased product groups — default - `GET /api/v1/products/recommended` — Get recommended products (legacy) — default - `GET /api/v1/products/recommendations/user-top-picks` — Get user top picks — default - `GET /api/v1/products/recommendations/cart-toppers` — Get cart toppers — default - `POST /api/v1/products/sponsored` — Get sponsored product listing — default — Query: category, brand, type, tag, q - `POST /api/v1/products/recommendations/sponsored-user-top-picks` — Get sponsored user top picks — default ### Ads & Recommendations (2 endpoints) - `POST /api/v1/products/recommendations/sponsored-cart-toppers` — Get sponsored cart toppers — default - `POST /api/v1/products/recommendations/frequently-bought-together/{product_id}` — Get frequently bought together products — default ### Cart (10 endpoints) - `POST /api/v5/carts` — Create cart — none, bearerAuth - `GET /api/v5/carts/{uuid}` — Show cart — none, bearerAuth - `PUT /api/v5/carts/{uuid}` — Update cart (promo codes, rewards) — none, bearerAuth - `POST /api/v5/carts/{cart_uuid}/items` — Add item to cart — none, bearerAuth - `PUT /api/v5/carts/{cart_uuid}/items/{item_uuid}` — Update item — none, bearerAuth - `PATCH /api/v5/carts/{cart_uuid}/items/{item_uuid}` — Update item quantity — none, bearerAuth - `DELETE /api/v5/carts/{cart_uuid}/items/{item_uuid}` — Remove item from cart — none, bearerAuth - `POST /api/v5/carts/{cart_uuid}/valid` — Validate cart — none, bearerAuth - `PUT /api/v4/carts/{cart_uuid}/delivery-specification` — Set delivery specification — none, bearerAuth - `PATCH /api/v4/carts/{cart_uuid}/delivery-specification` — Update delivery specification — none, bearerAuth ### Delivery (1 endpoint) - `POST /api/v3/deliveries/stores` — Get available delivery stores — none, bearerAuth ### Orders (6 endpoints) - `POST /api/v4/orders` — Create order (checkout) — none, bearerAuth - `POST /api/v5/orders` — Submit cart for asynchronous checkout — none, bearerAuth - `GET /api/v1/orders/{uuid}` — Get order details — none, bearerAuth - `GET /api/v1/orders` — List orders (order history) — bearerAuth - `PATCH /api/v1/orders/{uuid}/complete` — Complete order — none, bearerAuth - `PATCH /api/v2/orders/{uuid}/refresh-status` — Refresh order status — bearerAuth ### Deals (4 endpoints) - `GET /api/v1/store/deals/promotions` — List promotions — default - `GET /api/v1/store/deals/promotions/{slug_or_id}` — Get promotion detail — default - `GET /api/v1/store/deals/rewards` — List rewards — default - `GET /api/v1/store/deals/rewards/{slug_or_id}` — Get reward detail — default ### Payments (11 endpoints) - `GET /api/v1/store/payments/{service}/token` — Get payment customer token — bearerAuth — Path: service (adyen|aeropay|stronghold|moneris|swifter|ledgergreen|merrco|spence|greenbax|blazepay_widget) - `GET /api/v1/store/payments/{service}/sources` — List payment sources (v1) — bearerAuth - `POST /api/v1/store/payments/{service}/sources` — Add payment source — bearerAuth - `PATCH /api/v1/store/payments/{service}/sources/{id}` — Update payment source — bearerAuth - `DELETE /api/v1/store/payments/{service}/sources/{id}` — Delete payment source — bearerAuth - `GET /api/v2/store/payments/sources` — List payment sources (v2, unified) — bearerAuth - `POST /api/v1/store/payments/{service}/sessions` — Create payment session — none, bearerAuth - `POST /api/v1/store/payments/{service}/orders/{uuid}/pay` — Pay for order — bearerAuth - `POST /api/v1/store/payments/{service}/tip` — Tip — bearerAuth - `GET /api/v1/store/payments/{service}/promotions` — List payment promotions — bearerAuth - `POST /api/v1/store/payments/{service}/promotions/redeemable` — Check redeemable promotions — bearerAuth ### User Accounts (16 endpoints) - `GET /api/v1/users/me` — Get current user profile — bearerAuth - `PUT /api/v1/users/me` — Update user profile — bearerAuth - `PATCH /api/v1/users/me` — Update user profile (partial) — bearerAuth - `DELETE /api/v1/users/me` — Deactivate account — bearerAuth - `POST /api/v1/users/me/sync` — Sync user with POS — bearerAuth - `POST /api/v1/users/me/access_token` — Mint a short-lived handoff token — bearerAuth - `GET /api/v1/users/me/billing` — Get billing info — bearerAuth - `PUT /api/v1/users/me/billing` — Replace billing address — bearerAuth - `PATCH /api/v1/users/me/billing` — Update billing address — bearerAuth - `GET /api/v3/users/me/loyalty` — Get loyalty points (v3) — bearerAuth - `GET /api/v1/users/me/rewards` — Get available rewards — bearerAuth - `GET /api/v1/users/me/identity-verification` — Get identity verification report — bearerAuth - `GET /api/v1/users/me/identity-verification/{service}` — Get identity verification status — bearerAuth - `POST /api/v1/users/me/identity-verification/{service}` — Create identity verification — bearerAuth - `POST /api/v1/users/me/devices` — Register push device — bearerAuth - `DELETE /api/v1/users/me/devices/{external_device_id}` — Unregister device — bearerAuth ### Webhooks (6 endpoints) - `POST /api/v1/hooks/order_updated/{token}` — Order updated webhook — token-auth via URL - `POST /api/v1/hooks/member_updated/{token}` — Member updated webhook — token-auth via URL - `POST /api/v1/hooks/new_member/{token}` — New member webhook — token-auth via URL - `POST /api/v1/hooks/payment-auth-completed/{service}/{token}` — Payment auth completed webhook — token-auth via URL - `POST /api/v1/hooks/identity-verification-updated/{service}/{token}` — Identity verification updated webhook — token-auth via URL - `POST /api/v1/hooks/identity-verification-completed/{service}/{token}` — Identity verification completed webhook — token-auth via URL