# Bagisto API — Full Documentation Generated from https://api-docs.bagisto.com. Each section below is one documentation page. --- # API Authentication Guide URL: /api/authentication # API Authentication Guide Bagisto provides multiple authentication methods to secure your API requests. Choose the authentication method based on the type of API access you need. ## Quick Authentication Overview | Your Use Case | Authentication Method | API Type | Read More | |---|---|---|---| | **Public data** (products, categories) | `X-STOREFRONT-KEY` header | Shop API | [Public APIs](#_1-public-apis-storefront) | | **Customer operations** (cart, orders, profile) | `X-STOREFRONT-KEY` + Bearer token | Shop API | [Customer APIs](#_2-customer-apis) | | **Admin operations** (manage products, inventory) | Bearer token (admin) | Admin API | [Admin APIs](#_3-admin-apis) | ## Authentication Architecture All Bagisto APIs are built on a secure, Laravel-native foundation: - **Laravel Sanctum** — Token-based authentication framework - **Secure Token Generation** — Cryptographically secure token creation - **Token Expiration** — Configurable token lifetime - **Rate Limiting** — Per-key rate limit protection - **HTTPS Required** — Enforced in production environments --- ## 1. Public APIs (Storefront) **Best for:** Reading public data (products, categories, prices) without user login. ### The Basics - **What you need:** `X-STOREFRONT-KEY` header - **What you get:** Read-only access to storefront data - **Who can use it:** Anyone (no login required) - **Perfect for:** Mobile apps, websites, third-party integrations ### What You Can Do Here are common things you can do with Public APIs: - 📦 Browse products and get detailed product information - 🏷️ View categories and subcategories - 🎨 Get product attributes and variations - 📄 Read CMS pages and content - 🌍 Get available countries and locales - 📮 Retrieve shipping and payment methods (available options) ### How to Use **1. Get your Storefront Key** ```bash php artisan bagisto-api:generate-key --name="Web Storefront" ``` You'll get something like: `pk_storefront_xxxxxxxxxxxxx` **2. Make a REST API request:** ```bash curl -X GET "https://your-domain.com/api/shop/products" \ -H "Content-Type: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxx" ``` **3. Or a GraphQL request:** ```bash curl -X POST "https://your-domain.com/api/graphql" \ -H "Content-Type: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxx" \ -d '{ "query": "query { products { id name price } }" }' ``` ### Key Facts - 🔓 **Read-only** — You can't modify data, only view it - 📊 **Cacheable** — Responses can be cached for better performance - ⚡ **Fast** — No database lookups for user data - 🚀 **Scalable** — Can handle high request volumes - 🔄 **Rate limited** — Default: 100 requests/minute per key (see [Rate Limiting Guide](./rate-limiting)) --- ## 2. Customer APIs **Best for:** Building customer-facing features (shopping cart, orders, profiles) after user login. ### The Basics - **What you need:** `X-STOREFRONT-KEY` header + Bearer token (from customer login) - **What you get:** Access to customer's personal data and ability to perform actions - **Who can use it:** Authenticated customers only - **Perfect for:** Mobile apps, customer portals, checkout flows ### How It Works (3 Steps) **Step 1: Customer logs in** ```bash curl -X POST "https://your-domain.com/api/shop/customer/login" \ -H "Content-Type: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxx" \ -d '{ "email": "john@example.com", "password": "SecurePass@123" }' ``` **You'll get back:** ```json { "id": 1, "name": "John Doe", "email": "john@example.com", "token": "IsInRbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "success": true, "message": "Login successful" } ``` **Step 2: Save the token** Store the token securely in your application. See the [Introduction Guide](./introduction.md) for recommended storage patterns. **Step 3: Use token in future requests** ```bash curl -X GET "https://your-domain.com/api/shop/customers/addresses" \ -H "Content-Type: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxx" \ -H "Authorization: Bearer IsInRbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." ``` ### What You Can Do Once authenticated, customers can: - 👤 View and edit their profile - 📍 Manage delivery addresses - 🛒 Add/remove items from cart - ❤️ Create and manage wishlists - 🛍️ Place orders - 📦 View order history and tracking - ⭐ Create product reviews - 🔄 Manage subscriptions (if enabled) ### Key Facts - 👤 **User-specific** — Each customer sees only their own data - 🔐 **Requires login** — Must authenticate first (Bearer token) - 📝 **Read & Write** — Can view and modify data - ⏱️ **Session-based** — Token expires after some time - 🚫 **Not cacheable** — Personal data shouldn't be cached - 🔄 **Requires both headers** — Need `X-STOREFRONT-KEY` AND `Authorization: Bearer` --- ## 3. Admin APIs **Best for:** Building admin dashboards to manage products, inventory, customers, and system settings. ::: tip Admin uses a pre-issued Integration token Admin clients authenticate with an **Integration token** generated from the **Integration** menu in the admin panel — there is no admin login. Send it as `Authorization: Bearer |` (no `X-STOREFRONT-KEY`). Admin **GraphQL** clients POST to `/api/admin/graphql`. See the [Admin Authentication](/api/graphql-api/admin/authentication) reference. ::: ### The Basics - **What you need:** A pre-issued Integration token (no login) - **What you get:** Full control over all store data - **Who can use it:** Admins (and sub-admins) holding a valid Integration token - **Perfect for:** Admin dashboards, inventory management, reporting tools ### How It Works (3 Steps) **Step 1: Generate an Integration token** In the admin panel, open the **Integration** menu and generate a token. A store owner can generate tokens here and share them with the sub-admins who need API access — each token is tied to a specific admin user and inherits that admin's permissions. The token is shown **once** — copy it. **Step 2: Save the token** Store the token securely in your application. See the [Introduction Guide](./introduction.md) for recommended storage patterns. **Step 3: Use the token in API requests** ```bash curl -X GET "https://your-domain.com/api/admin/products" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer |" ``` **JavaScript example:** ```javascript const token = ''; fetch('https://your-domain.com/api/admin/products', { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } }) .then(res => res.json()) .then(data => console.log(data)); ``` ### What You Can Do Admins have full control over: - 📦 Create, read, update, and delete products - 🏷️ Manage categories and product attributes - 📊 Manage inventory and stock levels - 👥 View and manage all customers - 🛍️ Process and manage orders - 📈 Generate reports and analytics - ⚙️ Configure system settings - 📮 Set up shipping and payment methods - 🔐 Manage admin users and permissions ### Key Facts - 🔑 **Admin-only** — Requires an admin Integration token (no Storefront Key needed) - 🔐 **Token-based** — Authenticate with a pre-issued Integration token (no login) - 📝 **Full CRUD** — Create, read, update, and delete everything - ⚙️ **System-wide** — Can affect all store data - 🚫 **Not cacheable** — Data changes frequently - 🔒 **Role-based** — What you can do depends on your admin role --- ## Authentication Summary Table **Quick reference — Which auth method for which API?** | API Type | Use Case | Headers Required | Login Needed | |----------|----------|------------------|---| | **Public** | Browse products, categories | `X-STOREFRONT-KEY` only | ❌ No | | **Customer** | Cart, orders, profile | `X-STOREFRONT-KEY` + `Authorization: Bearer` | ✅ Customer login | | **Admin** | Manage products, inventory | `Authorization: Bearer` only | Integration token | ### Optional Context Headers In addition to authentication headers, you can pass these optional headers to control the locale, currency, and channel context for the response data: | Header | Purpose | Example | Fallback | |--------|---------|---------|----------| | `X-LOCALE` | Return content in a specific locale | `fr` | Channel's default locale | | `X-CURRENCY` | Return pricing in a specific currency | `EUR` | Channel's base currency | | `X-CHANNEL` | Use a specific sales channel | `default` | Default channel | If these headers are omitted or contain a value that doesn't exist in the system, the API silently falls back to the default value. For more details, see the [GraphQL Introduction](/api/graphql-api/introduction#context-headers-x-locale-x-currency-x-channel). --- ## Common Patterns ### Public API Request ```bash # Just need the Storefront Key curl -X GET "https://your-domain.com/api/shop/products" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxx" ``` ### Customer API Request ```bash # Need BOTH Storefront Key AND Bearer token curl -X POST "https://your-domain.com/api/shop/customers/addresses" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxx" \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \ -d '{"address": "123 Main St"}' ``` ### Admin API Request ```bash # Only need the Bearer token (no Storefront Key) curl -X GET "https://your-domain.com/api/admin/products" \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." ``` --- ## Using Tokens in Requests All authenticated requests require the Bearer token in the `Authorization` header. How you pass the token depends on your platform: **Web/Frontend:** Use the token from your authentication state, cookie, or session storage. **Mobile Apps:** Retrieve the token from secure device storage. **Backend Services:** Use the token from environment variables or secure vaults. For detailed token storage and security guidance, see the [Introduction Guide](./introduction.md). --- ## Security Essentials ✅ **Do This:** - Use HTTPS for all requests (required in production) - Include the token in the `Authorization: Bearer` header - Validate token before making requests - Handle 401 errors by re-authenticating - Use strong passwords (12+ characters, mixed case, numbers, special chars) ❌ **Don't Do This:** - Don't hardcode tokens in source code - Don't log tokens or API keys - Don't send tokens in URL query parameters - Don't commit `.env` files to Git - Don't reuse the same token across environments - Don't ignore token expiration --- ## Troubleshooting Authentication Issues ### "Invalid API Key" Error **Problem:** Your Storefront Key is rejected. **Solution:** ```bash # 1. Double-check your key echo $BAGISTO_STOREFRONT_KEY # 2. Verify it's active php artisan bagisto-api:key:manage status --key="Your Key" # 3. Make sure header name is exactly correct # Should be: X-STOREFRONT-KEY (with hyphen, not underscore) ``` ### "Unauthorized" (401) Error **Problem:** Your Bearer token is invalid or expired. **Solution:** ```bash # 1. Check if token is still valid (tokens expire) # 2. Login again to get a new token curl -X POST "https://your-domain.com/api/shop/customer/login" \ -H "Content-Type: application/json" \ -d '{"email": "user@example.com", "password": "password"}' # 3. Use the new token in your Authorization header ``` ### "Forbidden" (403) Error **Problem:** You're authenticated but don't have permission. **Solution:** - If Customer API: Make sure you logged in as the customer - If Admin API: Make sure you logged in as an admin, not a customer - Check your admin role has permission for this endpoint ### Token Keeps Expiring **Problem:** You have to login repeatedly. **Solution:** - Implement automatic token refresh (if backend supports it) - Check token expiration time in the response - Store refresh token if provided - Consider longer-lived tokens for backend-to-backend communication --- ## Related Documentation - [API Key Management Guide](./storefront-api-key-management-guide.md) — How to generate and manage API keys - [REST API Guide](./rest-api/rest-api.md) — REST API endpoints - [GraphQL API Guide](./graphql-api/graphql-api.md) — GraphQL queries and mutations --- # Build with AI URL: /api/build-with-ai --- outline: false --- # Build with AI This documentation is built to be consumed by AI coding agents (Claude Code, Cursor, Windsurf, and others) as well as humans. If you're building an app on the Bagisto API — or extending the API itself — with an AI assistant, point it at the resources below. ## `llms.txt` — the machine-readable index A single file lists every documented endpoint with a one-line description and a link, so an agent can discover the whole API surface in one fetch: - **Index:** [`/llms.txt`](/llms.txt) — every Shop and Admin endpoint, grouped by transport and menu. - **Full content:** [`/llms-full.txt`](/llms-full.txt) — the complete documentation concatenated into one file for full ingestion. These follow the [llms.txt convention](https://llmstxt.org). An agent should fetch `llms.txt` first to map the surface, then open the specific endpoint page for the exact request and response shape. ## Agent skills The [`bagisto/api-agent-skills`](https://github.com/bagisto/bagisto-api) repository packages this knowledge as installable **agent skills** — reusable instructions that teach an agent how to install the stack, build apps on the API, and extend the API package. Install all skills into your agent: ```bash npx skills add bagisto/api-agent-skills ``` Install for a specific agent, or a single skill: ```bash npx skills add bagisto/api-agent-skills -a claude-code npx skills add bagisto/api-agent-skills --skill "bagisto-api-build-app" ``` | Skill | Use it to | |-------|-----------| | `bagisto-api-install` | Install Bagisto + the API package | | `bagisto-api-build-app` | Build a storefront / admin / mobile / custom UI on the API | | `bagisto-api-develop` | Add or extend an endpoint in the API package | | `bagisto-api-dev-cycle` | Run the package's tests, lint, and cache cycle | | `bagisto-api-code-review` | Review a change to the package | | `bagisto-api-git` | Branch / stage / prepare a PR | ## Optional: local docs MCP server The skills repo also ships an **optional** local [MCP](https://modelcontextprotocol.io) server that lets an agent search this documentation on demand — no hosting required. It indexes the API pages locally and exposes `search_api_docs`, `list_endpoints`, and `get_doc` tools. ```bash claude mcp add bagisto-api -- node /absolute/path/to/api-agent-skills/mcp/src/index.mjs ``` You don't need the MCP server to build with AI — `llms.txt` and `llms-full.txt` cover the same need statically. See the [`mcp/README.md`](https://github.com/bagisto/bagisto-api) in the skills repo for setup. ## How an agent should use these docs 1. Fetch [`/llms.txt`](/llms.txt) to discover the endpoint you need. 2. Open that endpoint's page for the exact request body, response shape, and errors — **never guess a payload**. 3. For end-to-end flows, follow the [Recipes](/api/recipes/) — ordered walkthroughs that chain real endpoint calls. 4. Mind the GraphQL rules: select **result fields** (`cartId`, `orderId`, `success`) on action mutations — not `id`; inputs are camelCase; the Shop and Admin GraphQL endpoints are separate. --- # GraphQL API Overview URL: /api/graphql-api # GraphQL API Overview Welcome to the Bagisto GraphQL API documentation. This comprehensive guide covers all available endpoints for both customer-facing (Shop) and administrative (Admin) operations. ## API Tiers Bagisto provides two distinct API tiers to serve different use cases: ### 🛍️ Shop API The Shop API is designed for customer-facing applications like mobile apps, storefronts, and third-party integrations. It provides read and write access to: - **Products** - Browse and search products, manage variants - **Categories** - Navigate category hierarchies - **Cart** - Create and manage shopping carts - **Customers** - Customer authentication and profile management - **Orders** - Order retrieval and tracking - **Checkout** - Complete checkout workflow - **Reviews** - Product reviews and ratings [Explore Shop API →](/api/graphql) ### 🔧 Admin API The Admin API is for administrative and management applications. Full control over: - **Products** - Complete product management with bulk operations - **Categories** - Category hierarchy and product assignment - **Orders** - Order fulfillment, shipments, invoices, and refunds - **Customers** - Customer administration and management - **Inventory** - Stock management across warehouses - **Promotions** - Coupons, discounts, and cart rules - **Attributes** - Product attributes and attribute sets - **Reports** - Sales, product, customer, and inventory analytics [Explore Admin API →](/api/graphql) ## Getting Started ### 1. Authentication Choose the appropriate authentication method for your use case: - **Guest Access** - For unauthenticated Shop API operations - **Customer Authentication** - For authenticated customer operations - **Admin Authentication** - For Admin API operations [Learn about Authentication →](/api/graphql-api/authentication) ### 2. Cursor Pagination Bagisto GraphQL API uses cursor-based pagination for efficient collection queries. This method is ideal for handling large datasets and provides a better user experience for infinite scrolling. #### Pagination Parameters - **`first`** - Number of items to retrieve from the start (positive integer) - **`after`** - Cursor to start retrieving items after (used for forward pagination) - **`last`** - Number of items to retrieve from the end (positive integer) - **`before`** - Cursor to start retrieving items before (used for backward pagination) - **`totalItems`** - Total count of items in the collection (returned in response) #### Example: Get First 10 Products ```graphql query { products(first: 10) { edges { node { id name price } cursor } pageInfo { hasNextPage hasPreviousPage startCursor endCursor } totalItems } } ``` #### Example: Get Next Page ```graphql query { products(first: 10, after: "cursor_from_previous_response") { edges { node { id name price } cursor } pageInfo { hasNextPage endCursor } totalItems } } ``` #### Example: Get Last 10 Products ```graphql query { products(last: 10) { edges { node { id name price } cursor } pageInfo { hasPreviousPage startCursor } totalItems } } ``` #### Example: Get Previous Page ```graphql query { products(last: 10, before: "cursor_from_response") { edges { node { id name price } cursor } pageInfo { hasPreviousPage startCursor } totalItems } } ``` [Learn more about Pagination →](/api/graphql-api/pagination) ### 3. Exploring the API Use the interactive GraphQL Playground to test queries and mutations: - Write and test queries in real-time - Explore the schema with built-in documentation - Get instant feedback on syntax errors - View response examples [Start with Playground Guide →](/api/graphql-api/playground) ### 4. Integration Integrate Bagisto with your applications using one of these languages: - **JavaScript/TypeScript** - React, Vue, Next.js, etc. - **Python** - Django, Flask, FastAPI - **PHP** - Laravel, WordPress, etc. - **Ruby** - Rails and other frameworks - **Go** - Go applications and services - **Java** - Spring Boot, Maven projects [View Integration Guides →](/api/graphql-api/integrations) ## Shop API Resources | Resource | Description | Key Operations | |----------|-------------|-----------------| | [Products](/api/graphql-api/shop/products) | Browse and search products | Get products, Search, Filter, Sort | | [Categories](/api/graphql-api/shop/categories) | Navigate category tree | Get categories, Get products by category | | [Attribute Options](/api/graphql-api/shop/attribute-options) | Product attribute values | Get options, Translations, Swatches | | [Cart](/api/graphql-api/shop/cart) | Manage shopping cart | Create, Add items, Update, Apply coupons | | [Customers](/api/graphql-api/shop/customers) | Customer authentication | Register, Login, Profile, Addresses | | [Orders](/api/graphql-api/shop/orders) | View and track orders | Get orders, Track shipments | | [Checkout](/api/graphql-api/shop/checkout) | Complete purchase flow | Billing address, Shipping, Payment | | [Reviews](/api/graphql-api/shop/reviews) | Product reviews | Get reviews, Create review | ## Admin API Resources | Resource | Description | Key Operations | |----------|-------------|-----------------| | [Products](/api/graphql-api/admin/products) | Full product management | Create, Update, Delete, Bulk operations | | [Categories](/api/graphql-api/admin/categories) | Category management | Create, Update, Assign products | | [Orders](/api/graphql-api/admin/orders) | Order fulfillment | Shipments, Invoices, Refunds, Comments | | [Customers](/api/graphql-api/admin/customers) | Customer administration | Create, Update, Manage addresses | | [Inventory](/api/graphql-api/admin/inventory) | Stock management | Update stock, Warehouses, Transfers | | [Promotions](/api/graphql-api/admin/promotions) | Promotions & discounts | Coupons, Cart rules, Discounts | | [Attributes](/api/graphql-api/admin/attributes) | Product attributes | Create attributes, Options, Sets | | [Reports](/api/graphql-api/admin/reports) | Business analytics | Sales, Products, Customers, Inventory | ## Best Practices Follow these recommendations for production deployments: - **Performance** - Use pagination, selective field queries - **Security** - Implement rate limiting, validate inputs - **Caching** - Cache frequently accessed data - **Error Handling** - Gracefully handle errors and edge cases - **Monitoring** - Track API usage and performance [Read Full Best Practices Guide →](/api/graphql-api/best-practices) ## Rate Limiting API endpoints are rate-limited to ensure fair usage: - **Standard Limits** - 1000 requests per hour per IP - **Burst Limits** - 100 requests per minute - **Webhook Delivery** - 10 retries with exponential backoff Response headers indicate current rate limit status: - `X-Rate-Limit-Limit` - Total requests allowed - `X-Rate-Limit-Remaining` - Requests remaining - `X-Rate-Limit-Reset` - Unix timestamp when limit resets ## Support & Resources - **Documentation** - [API docs](/) - **GraphQL Schema** - Available in Playground - **Community** - [Bagisto Forums](https://forums.bagisto.com/) - **GitHub** - [Bagisto Repository](https://github.com/bagisto/bagisto) ## Version Info - **API Version** - 1.0 - **GraphQL Spec** - June 2018 - **Last Updated** - Dec 2025 --- **Ready to get started?** Choose your starting point: - [Authentication Setup](/api/graphql-api/authentication) - Configure authentication - [Shop API](/api/graphql-api/shop/products) - Browse customer APIs - [Admin API](/api/graphql-api/admin/products) - Explore management APIs - [Playground](/api/graphql-api/playground) - Test APIs interactively --- # GraphQL API URL: /api/graphql-api # GraphQL API Bagisto's GraphQL API delivers a modern, flexible approach to e-commerce data access. Built on Laravel Lighthouse, it provides efficient querying capabilities perfect for headless commerce, mobile apps, and modern frontend frameworks. ## 🚀 Quick Navigation Choose your next step: | Documentation | Description | |---|---| | 📖 [Introduction](/api/graphql-api/introduction) | Get started with GraphQL basics and API overview | | 🔐 [Authentication](/api/graphql-api/authentication) | Learn all authentication methods and token management | | 🛍️ [Shop API](/api/graphql-api/shop-api) | Customer-facing e-commerce operations reference | | 👨‍💼 [Admin API](/api/graphql-api/admin-api) | Administrative operations and management reference | | 🎮 [Playground Guide](/api/graphql-api/playground) | Interactive testing with sample queries | | 💻 [Integration Guides](/api/graphql-api/integrations) | Code examples for multiple programming languages | | 💡 [Best Practices](/api/graphql-api/best-practices) | Performance, security, and testing best practices | ## 🌐 Live Playground Test queries instantly without any setup: 🎮 **[GraphQL Playground](https://api-demo.bagisto.com/api/graphiql)** - Interactive query builder with schema explorer ## Key Features ✨ **Developer Friendly** - Interactive GraphiQL playground - Auto-complete and schema documentation - Copy as cURL functionality - Real-time error reporting 🚀 **High Performance** - Request only the data you need - Cursor-based pagination - Query optimization tools - Caching support 🔒 **Secure** - Multiple authentication methods - Guest checkout support - Token-based security - Rate limiting 📱 **Mobile Ready** - Optimized for low bandwidth - Small payload sizes - Perfect for native apps ## What Can You Build? - 🛒 Headless storefronts and e-commerce sites - 📱 Mobile apps (iOS & Android) - 🔄 Third-party integrations and marketplaces - 📊 Analytics dashboards - ⚡ Progressive Web Apps (PWA) - 🤖 AI-powered shopping assistants ## Quick Start ### 1. Choose Your Path **For Building Customer-Facing Apps:** - Start with [Shop API Reference](/api/graphql-api/shop-api) - Learn [Authentication Methods](/api/graphql-api/authentication) **For Admin Dashboards:** - Start with [Admin API Reference](/api/graphql-api/admin-api) - Review [Permission Requirements](/api/graphql-api/admin-api#permission--role-management) **For Your Programming Language:** - Find your language in [Integration Guides](/api/graphql-api/integrations) - Copy-paste code examples and adapt ### 2. Test in Playground - Visit [GraphQL Playground](https://api-demo.bagisto.com/api/graphiql) - Try [Sample Queries](/api/graphql-api/playground#quick-start-queries) - Explore the [Schema](/api/graphql-api/playground#schema-explorer) ### 3. Implement in Your App - Follow the [Authentication Guide](/api/graphql-api/authentication) - Use examples from [Integration Guides](/api/graphql-api/integrations) - Apply [Best Practices](/api/graphql-api/best-practices) ## Documentation Structure ### Core Documentation 1. **[Introduction](/api/graphql-api/introduction)** - GraphQL fundamentals, setup, and endpoints 2. **[Authentication](/api/graphql-api/authentication)** - All auth methods (guest, customer, admin) 3. **[Shop API](/api/graphql-api/shop-api)** - Complete Shop API with all queries and mutations ### Advanced Documentation 4. **[Admin API](/api/graphql-api/admin-api)** - Admin operations for management tasks 5. **[Playground Guide](/api/graphql-api/playground)** - Interactive testing with sample queries 6. **[Integration Guides](/api/graphql-api/integrations)** - Code examples for: - JavaScript / Node.js / React / Next.js - Python / Django - PHP / Laravel - Ruby / Rails - Go - Java ### Best Practices 7. **[Best Practices](/api/graphql-api/best-practices)** - Performance optimization, security, testing, debugging ## Common Use Cases ### Building a Headless Storefront ``` 1. Get Products → [Shop API - Products](/api/graphql-api/shop-api#products) 2. Manage Cart → [Shop API - Shopping Cart](/api/graphql-api/shop-api#shopping-cart) 3. Checkout → [Shop API - Checkout](/api/graphql-api/shop-api#checkout) 4. Learn Auth → [Authentication Guide](/api/graphql-api/authentication) ``` ### Building a Mobile App ``` 1. Learn Guest Auth → [Authentication - Guest](/api/graphql-api/authentication#1-guest-checkout-authentication) 2. Browse Products → [Shop API - Products](/api/graphql-api/shop-api#products) 3. Integrate Language → [Integration Guides](/api/graphql-api/integrations) 4. Apply Best Practices → [Best Practices](/api/graphql-api/best-practices) ``` ### Building an Admin Dashboard ``` 1. Admin Login → [Authentication - Admin](/api/graphql-api/authentication#3-admin-authentication) 2. Manage Data → [Admin API Reference](/api/graphql-api/admin-api) 3. Optimize Performance → [Best Practices - Performance](/api/graphql-api/best-practices#performance-optimization) 4. Implement Testing → [Best Practices - Testing](/api/graphql-api/best-practices#testing) ``` ### Building a Third-Party Integration ``` 1. Choose Auth Method → [Authentication Guide](/api/graphql-api/authentication) 2. Decide Shop or Admin → [Shop API](/api/graphql-api/shop-api) or [Admin API](/api/graphql-api/admin-api) 3. Select Language → [Integration Guides](/api/graphql-api/integrations) 4. Handle Errors → [Best Practices - Error Handling](/api/graphql-api/best-practices#error-handling) ``` ## API Endpoints ::: warning Shop and Admin use DIFFERENT GraphQL endpoints (2026-05-28) The admin API was moved off the shared `/api/graphql` endpoint and onto a dedicated route. Always pick the right endpoint based on which API you're calling. ::: | Endpoint | Purpose | Authentication | |----------|---------|-----------------| | `POST /api/graphql` | **Shop** GraphQL API (public + customer) | `X-STOREFRONT-KEY` (+ `Authorization: Bearer` for customer ops) | | `POST /api/admin/graphql` | **Admin** GraphQL API | `Authorization: Bearer ` only — **no** storefront key | | `GET /api/graphiql` | Shop GraphiQL Playground | None | | `GET /api/admin/graphiql` | Admin GraphiQL Playground | None | | `GET /api/sandbox` | Apollo Sandbox UI | None | An admin Bearer token sent to `/api/graphql` is **rejected** with 401 — there is no back door. Admin clients must use `/api/admin/graphql`. ## Popular Queries ### Get Products ```graphql query { products(channel: "default", first: 10) { edges { node { id name price } } } } ``` [See more Shop API queries →](/api/graphql-api/shop-api#products) ### Customer Login ```graphql mutation { createLogin(input: { email: "user@example.com" password: "password" }) { accessToken } } ``` [See more Auth examples →](/api/graphql-api/authentication#2-customer-authentication) ### Create Order ```graphql mutation { createOrder(input: { cartId: "CART_ID" billingAddressId: "ADDRESS_ID" shippingMethod: "flatrate_flatrate" paymentMethod: "paypal" }) { order { id incrementId } } } ``` [See complete checkout flow →](/api/graphql-api/shop-api#checkout) ## Getting Help | Resource | Purpose | |----------|---------| | 🎮 [Live Playground](https://api-demo.bagisto.com/api/graphiql) | Test queries instantly | | 📚 [Documentation](/api/graphql-api/introduction) | Comprehensive guides | | 💬 [Community Forum](https://forums.bagisto.com) | Ask questions | | 🐛 [Issue Tracker](https://github.com/bagisto/bagisto/issues) | Report bugs | | 📧 [Contact Support](https://bagisto.com/en/contacts/) | Enterprise support | --- **Start Building Today!** 👉 **New to GraphQL?** Start with [Introduction](/api/graphql-api/introduction) 👉 **Ready to code?** Choose your language in [Integration Guides](/api/graphql-api/integrations) 👉 **Want to test?** Visit [GraphQL Playground](https://api-demo.bagisto.com/api/graphiql) --- # Admin API Reference URL: /api/graphql-api/admin-api # Admin API Reference The Bagisto Admin API provides comprehensive administrative endpoints for managing your e-commerce platform. All Admin API operations require authenticated admin access. ## Overview The Admin API allows administrators to: - Manage products, categories, and inventory - Handle customer accounts and addresses - Manage orders, shipments, and invoices - Configure system settings - Manage promotions and discounts - View reports and analytics ::: warning Authentication Required All Admin API requests require an admin **Integration token** sent as `Authorization: Bearer |`. See [Authentication](/api/graphql-api/admin/authentication). ::: ## Authentication Admin API requests authenticate with a pre-issued **Integration token** — there is no login mutation. Generate a token from the **Integration** menu in the admin panel (a store owner can generate tokens here and share them with the sub-admins who need API access), then send it on every request: ```bash Authorization: Bearer | ``` The admin GraphQL endpoint is `POST /api/admin/graphql`. See [Admin Authentication](/api/graphql-api/admin/authentication) for the full token lifecycle, IP allowlists, and rate limits. ## Products Manage product catalog including creation, updates, and deletion. ### Create Product ```graphql mutation { createProduct(input: { name: "New Product" sku: "SKU-123" type: "simple" attributeFamily: "default" description: "Product description" shortDescription: "Short desc" weight: 1.5 status: true channel: "default" prices: [ { channel: "default" price: 99.99 cost: 50.00 specialPrice: 79.99 specialPriceFrom: "2024-01-01" specialPriceTo: "2024-12-31" } ] images: [ { type: "image" path: "/path/to/image.jpg" } ] attributes: [ { code: "color" value: "red" } ] }) { product { id name sku status createdAt } } } ``` ### Get All Products ```graphql query { products( channel: "default" first: 50 filters: { status: [1] } ) { pageInfo { hasNextPage endCursor } edges { node { id name sku type status description price cost weight quantity createdAt updatedAt } } } } ``` ### Get Product by ID ```graphql query { adminProduct(id: "1") { id name sku type description shortDescription weight status quantity price cost attributeFamily { id code } attributes { edges { node { id code label value } } } images { edges { node { id type url path } } } inventories { edges { node { id warehouseId quantity reservedQuantity } } } prices { edges { node { id channel price cost specialPrice specialPriceFrom specialPriceTo } } } } } ``` ### Update Product ```graphql mutation { updateProduct(input: { id: "1" name: "Updated Product Name" description: "Updated description" price: 119.99 cost: 60.00 status: true weight: 2.0 attributes: [ { code: "color" value: "blue" } ] }) { product { id name price updatedAt } } } ``` ### Update Product Prices ```graphql mutation { updateProductPrices(input: { productId: "1" prices: [ { channel: "default" price: 129.99 cost: 70.00 specialPrice: 99.99 specialPriceFrom: "2024-02-01" specialPriceTo: "2024-02-28" } ] }) { product { id prices { edges { node { channel price specialPrice } } } } } } ``` ### Delete Product ```graphql mutation { deleteProduct(input: { id: "1" }) { status message } } ``` ### Bulk Update Products ```graphql mutation { bulkUpdateProducts(input: { productIds: ["1", "2", "3"] updates: { status: true weight: 1.5 } }) { status message updatedCount } } ``` ### Add Product Image ```graphql mutation { addProductImage(input: { productId: "1" type: "image" path: "/path/to/new-image.jpg" altText: "Product alt text" }) { image { id type url path } } } ``` ### Delete Product Image ```graphql mutation { deleteProductImage(input: { productId: "1" imageId: "1" }) { status message } } ``` ## Categories Manage product categories. ### Create Category ```graphql mutation { createCategory(input: { parentId: 1 name: "New Category" slug: "new-category" description: "Category description" imageUrl: "https://example.com/image.jpg" bannerUrl: "https://example.com/banner.jpg" metaTitle: "New Category - Meta Title" metaDescription: "Meta description" metaKeywords: "keywords" status: true displayMode: "products_only" position: 1 }) { category { id name slug status createdAt } } } ``` ### Get All Categories ```graphql query { adminCategories(first: 50) { edges { node { id name slug description status position parentId children { totalCount } } } } } ``` ### Update Category ```graphql mutation { updateCategory(input: { id: "2" name: "Updated Category" description: "Updated description" status: true position: 2 }) { category { id name status updatedAt } } } ``` ### Delete Category ```graphql mutation { deleteCategory(input: { id: "2" }) { status message } } ``` ## Inventory Management Manage stock and warehouse inventory. ### Get Inventory ```graphql query { inventory(productId: "1") { id productId warehouseId quantity reservedQuantity availableQuantity warehouse { id name code } } } ``` ### Update Inventory ```graphql mutation { updateInventory(input: { productId: "1" warehouseId: "1" quantity: 100 }) { inventory { id quantity availableQuantity updatedAt } } } ``` ### Get Warehouses ```graphql query { warehouses(first: 20) { edges { node { id name code address city state country zipCode phoneNumber } } } } ``` ### Create Warehouse ```graphql mutation { createWarehouse(input: { name: "New Warehouse" code: "WH-02" address: "456 Warehouse Ave" city: "Chicago" state: "IL" country: "US" zipCode: "60601" phoneNumber: "3125551234" }) { warehouse { id name code createdAt } } } ``` ## Customer Management Manage customer accounts and information. ### Get All Customers ```graphql query { adminCustomers(first: 50) { pageInfo { hasNextPage endCursor } edges { node { id firstName lastName email gender createdAt totalOrders totalSpent } } } } ``` ### Get Customer Details ```graphql query { adminCustomer(id: "1") { id firstName lastName email gender dateOfBirth phone status createdAt addresses { edges { node { id firstName lastName address city state country zipCode defaultBilling defaultShipping } } } orders { edges { node { id incrementId status grandTotal createdAt } } } } } ``` ### Create Customer ```graphql mutation { adminCreateCustomer(input: { firstName: "John" lastName: "Doe" email: "john@example.com" password: "SecurePassword123!" gender: "Male" dateOfBirth: "1990-05-15" status: true }) { customer { id firstName email createdAt } } } ``` ### Update Customer ```graphql mutation { adminUpdateCustomer(input: { id: "1" firstName: "Jane" lastName: "Smith" email: "jane@example.com" gender: "Female" status: true }) { customer { id firstName email updatedAt } } } ``` ### Delete Customer ```graphql mutation { adminDeleteCustomer(input: { id: "1" }) { status message } } ``` ### Add Customer Address ```graphql mutation { adminAddCustomerAddress(input: { customerId: "1" firstName: "John" lastName: "Doe" address: "123 Main St" city: "New York" state: "NY" country: "US" zipCode: "10001" phoneNumber: "2125551234" defaultBilling: true defaultShipping: false }) { address { id firstName address city defaultBilling } } } ``` ## Order Management Manage customer orders, shipments, and invoices. ### Get All Orders ```graphql query { adminOrders( first: 50 filters: { status: ["pending", "processing"] } ) { pageInfo { hasNextPage endCursor } edges { node { id incrementId status grandTotal itemsCount customer { id firstName lastName email } createdAt } } } } ``` ### Get Order Details ```graphql query { adminOrder(id: "1") { id incrementId status grandTotal subtotal taxTotal shippingTotal discountAmount couponCode customer { id firstName lastName email } billingAddress { firstName lastName address city state country zipCode } shippingAddress { firstName lastName address city state country zipCode } items { edges { node { id productId productName sku quantity price totalPrice invoicedQty shippedQty refundedQty } } } shipments { edges { node { id status carrierTitle trackNumber createdAt items { edges { node { id quantity } } } } } } invoices { edges { node { id incrementId status grandTotal createdAt } } } } } ``` ### Create Shipment ```graphql mutation { createShipment(input: { orderId: "1" items: [ { orderItemId: "1" quantity: 2 } ] carrier: "fedex" trackNumber: "123456789" }) { shipment { id incrementId status carrierTitle trackNumber createdAt } } } ``` ### Create Invoice ```graphql mutation { createInvoice(input: { orderId: "1" items: [ { orderItemId: "1" quantity: 2 } ] }) { invoice { id incrementId status grandTotal createdAt } } } ``` ### Create Refund ```graphql mutation { createRefund(input: { orderId: "1" items: [ { orderItemId: "1" quantity: 1 } ] adjustmentFee: 0 adjustmentNegativeFee: 0 }) { refund { id status grandTotal createdAt } } } ``` ### Update Order Status ```graphql mutation { updateOrderStatus(input: { orderId: "1" status: "processing" }) { order { id status updatedAt } } } ``` ### Add Order Comment ```graphql mutation { addOrderComment(input: { orderId: "1" comment: "Order is being processed" notifyCustomer: true }) { comment { id comment createdAt } } } ``` ## Promotions & Discounts Manage promotional campaigns and coupon codes. ### Get All Promotions ```graphql query { adminPromotions(first: 50) { edges { node { id name description ruleType status startDate endDate priority } } } } ``` ### Create Promotion ```graphql mutation { createPromotion(input: { name: "Summer Sale 2024" description: "Summer discount promotion" ruleType: "cart_rule" status: true startDate: "2024-06-01" endDate: "2024-08-31" conditions: [ { attribute: "subtotal" operator: "gt" value: "100" } ] actions: [ { type: "discount_percentage" value: "10" } ] priority: 1 }) { promotion { id name status createdAt } } } ``` ### Get All Coupons ```graphql query { adminCoupons(first: 50) { edges { node { id code description discountType discountValue usageLimit usageCount status startDate endDate } } } } ``` ### Create Coupon ```graphql mutation { createCoupon(input: { code: "SAVE20" description: "Save 20% on your purchase" discountType: "percentage" discountValue: "20" usageLimit: 100 usageLimitPerCustomer: 1 status: true startDate: "2024-01-01" endDate: "2024-12-31" minOrderAmount: "50" }) { coupon { id code discountValue status createdAt } } } ``` ## Attributes & Attribute Families Manage product attributes and families. ### Get All Attributes ```graphql query { adminAttributes(first: 50) { edges { node { id code label type required filterable configurable options { edges { node { id label value } } } } } } } ``` ### Create Attribute ```graphql mutation { createAttribute(input: { code: "brand" label: "Brand" type: "select" required: false filterable: true configurable: false options: [ { label: "Brand A", value: "brand-a" } { label: "Brand B", value: "brand-b" } ] }) { attribute { id code label type createdAt } } } ``` ### Get Attribute Families ```graphql query { adminAttributeFamilies(first: 50) { edges { node { id code name attributeGroups { edges { node { id name attributes { edges { node { code label } } } } } } } } } } ``` ## System Configuration Manage system settings and configuration. ### Get Configuration ```graphql query { configuration { general { storeName storeEmail taxIdentifier } catalog { productsPerPage defaultSort } checkout { guestCheckout reviewCheckout } shipping { origin freeShippingThreshold } } } ``` ### Update Configuration ```graphql mutation { updateConfiguration(input: { general: { storeName: "My Store" storeEmail: "store@example.com" } catalog: { productsPerPage: 20 defaultSort: "name" } }) { status message } } ``` ## Reports & Analytics Generate business reports and analytics. ### Get Sales Report ```graphql query { salesReport( startDate: "2024-01-01" endDate: "2024-12-31" granularity: "monthly" ) { periods { date orders totalRevenue avgOrderValue itemsSold } } } ``` ### Get Customer Report ```graphql query { customerReport( startDate: "2024-01-01" endDate: "2024-12-31" ) { newCustomers returningCustomers totalRevenue avgCustomerValue } } ``` ### Get Product Report ```graphql query { productReport( startDate: "2024-01-01" endDate: "2024-12-31" limit: 10 ) { products { productId productName unitsSold totalRevenue rating } } } ``` ## Permission & Role Management Manage admin roles and permissions. ### Get All Roles ```graphql query { adminRoles(first: 50) { edges { node { id name description permissions { edges { node { id name slug } } } } } } } ``` ### Create Role ```graphql mutation { createAdminRole(input: { name: "Product Manager" description: "Can manage products and categories" permissions: ["products.view", "products.create", "products.edit"] }) { role { id name permissions { edges { node { name } } } } } } ``` ### Get Admin Users ```graphql query { adminUsers(first: 50) { edges { node { id name email role { name } status createdAt } } } } ``` ### Create Admin User ```graphql mutation { createAdminUser(input: { name: "Product Manager" email: "pm@example.com" password: "SecurePassword123!" roleId: "2" status: true }) { admin { id name email role { name } createdAt } } } ``` ## Error Handling ### Permission Denied Error ```json { "errors": [ { "message": "Unauthenticated or insufficient permissions", "extensions": { "category": "authentication" } } ] } ``` ### Validation Error ```json { "errors": [ { "message": "Validation failed", "extensions": { "validation": { "name": ["The name field is required"], "sku": ["The SKU must be unique"] } } } ] } ``` --- **💡 Pro Tips:** - Use bulk operations for better performance - Cache frequently accessed data like categories and attributes - Implement proper error handling and retry logic - Monitor rate limits and implement backoff strategies **📚 Related Documentation:** - 🔐 [Authentication](/api/graphql-api/authentication) - 🛍️ [Shop API](/api/graphql-api/shop-api) - 💡 [Best Practices](/api/graphql-api/best-practices) --- # Admin API URL: /api/graphql-api/admin-coming-soon # Admin API ## Coming Soon The comprehensive Admin API documentation is coming soon. This section will include detailed guides for: - **Products**: Create, read, update, and delete products with full attribute management - **Categories**: Manage product categories and hierarchies - **Customers**: Customer management and administration - **Orders**: Order management, fulfillment, and tracking - **Attributes**: Attribute creation and configuration - **Inventory**: Stock management and warehouse operations - **Promotions**: Create and manage discounts and promotional campaigns - **Reports**: Access business intelligence and analytics reports - **Mutations**: Advanced operations for admin-level modifications Stay tuned for comprehensive documentation and examples! --- # Attributes URL: /api/graphql-api/admin/attributes --- outline: false examples: - id: get-all-attributes title: Get All Attributes description: Retrieve all product attributes from the admin panel. query: | # This page documents a legacy Shop GraphQL stub; the actual Admin API # query is `adminAttributes`. See: ./catalog/attributes/attributes-listing.md variables: | {} response: | {} --- # Attributes ## About The `attributes` admin query retrieves product attribute definitions and configurations. Use this query to: - Display attribute management interfaces - Build product attribute assignment forms - Retrieve attribute types and options - Manage attribute properties and validation rules - Export attribute metadata - Sync attributes with external systems - Build dynamic product forms based on attributes This query provides complete attribute definitions including type, options, validation, and visibility settings needed for attribute management. ## Arguments | Argument | Type | Description | |----------|------|-------------| | `first` | `Int` | Attributes per page (max: 100). Default: 20. | | `after` | `String` | Pagination cursor for forward pagination. | | `last` | `Int` | Attributes for backward pagination (max: 100). | | `before` | `String` | Pagination cursor for backward pagination. | | `type` | `[AttributeType!]` | Filter by type: `select`, `text`, `textarea`, `price`, `boolean`, `date`. | | `sortKey` | `AttributeSortKeys` | Sort by: `CODE`, `NAME`, `POSITION`. Default: `POSITION` | | `isFilterable` | `Boolean` | Filter by visibility in layered navigation. | | `isSearchable` | `Boolean` | Filter by searchability in catalog search. | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `edges` | `[AttributeEdge!]!` | Attribute edges with pagination. | | `edges.node` | `Attribute!` | Attribute object. | | `edges.node.id` | `ID!` | Attribute ID. | | `edges.node.code` | `String!` | Unique attribute code. | | `edges.node.name` | `String!` | Attribute display name. | | `edges.node.type` | `String!` | Attribute type (select, text, textarea, etc.). | | `edges.node.isRequired` | `Boolean!` | Whether attribute is mandatory for products. | | `edges.node.isFilterable` | `Boolean!` | Available in layered navigation. | | `edges.node.isSearchable` | `Boolean!` | Searchable in catalog search. | | `edges.node.isVisibleOnFront` | `Boolean!` | Visible on product pages. | | `edges.node.options` | `[AttributeOption!]` | Available options (for select/multiselect types). | | `edges.node.options.id` | `ID!` | Option ID. | | `edges.node.options.label` | `String!` | Option display label. | | `edges.node.options.value` | `String!` | Option value. | | `edges.node.validation` | `AttributeValidation` | Validation rules and constraints. | | `edges.node.position` | `Int!` | Display order. | | `nodes` | `[Attribute!]!` | Flattened attribute array. | | `pageInfo` | `PageInfo!` | Pagination metadata. | | `totalCount` | `Int!` | Total attributes. | --- # Admin Authentication URL: /api/graphql-api/admin/authentication --- outline: false examples: - id: admin-authenticated-query title: Authenticated Query description: Every Admin GraphQL request carries the admin Bearer token. This example reads the authenticated admin's own profile to confirm the token works. query: | query { readAdminProfile { id name email status } } variables: | {} response: | { "data": { "readAdminProfile": { "id": "/api/admin/admin_profiles/4", "name": "Admin User", "email": "admin@example.com", "status": "1" } } } --- # Admin Authentication The Bagisto Admin GraphQL API authenticates every request with a pre-issued **Integration token**. There is no login mutation — you generate a token once in the admin panel and send it on every request. ## Endpoint ``` POST /api/admin/graphql Authorization: Bearer | Content-Type: application/json ``` Admin GraphQL has its **own** endpoint, separate from the shop GraphQL endpoint (`POST /api/graphql`). The admin endpoint authenticates with the Bearer token **only** — the storefront key is not used here. An interactive playground is available at `GET /api/admin/graphiql`. ## How to authenticate 1. In the admin panel, open the **Integration** menu (`Admin → Integration`) and generate a token. 2. Copy the token the moment it is shown — it is displayed **once**. 3. Send it on every Admin GraphQL request as a Bearer token: ``` Authorization: Bearer | ``` ## About the token - The token belongs to a specific admin user and carries that admin's permissions. A request can never do more than the owning admin is allowed to do. - The plaintext format is `|`. Send it verbatim. - A token can be locked down with scoped **permissions**, an **IP allowlist**, an **expiry date**, and **rate limits** — see [Token security](#token-security) below. - Revoke or regenerate a token at any time from the same **Integration** menu. A revoked token stops working immediately. ## Token security Each Integration token can be locked down at generation time in the **Integration** menu. Four independent controls scope what a token can do: ### Permissions (ACL) A token is tied to one admin and **inherits that admin's role permissions** — it can never do more than its owner. When generating the token you choose a permission mode: - **All** — every action the owner's role allows. - **Custom** — a specific subset of permissions you select, frozen onto the token. - **Same as web** — always mirrors the owner's current role, so the token automatically follows any later changes to that role. A query or mutation for an action the token isn't permitted to perform fails with a **Forbidden** entry in the GraphQL `errors[]`. ### IP allowlist Optionally restrict a token to specific client IPs. Individual **IPv4** and **IPv6** addresses and **CIDR ranges** are all supported. Leave the allowlist empty to allow any IP. A request from an address that isn't on the list is rejected as **401 Unauthenticated**. (`127.0.0.1` is always allowed, for local development.) ### Expiry A token can have an **expiry date** (default: one year after generation) or be set to **never expire**. After the expiry date the token stops working (**401**). ### Rate limits Each token is throttled by two independent buckets: | Bucket | Default | |---|---| | Per minute | 60 requests | | Per day | 10,000 requests | Exceeding either limit returns **429 Too Many Requests**. **Unlimited rate limit** — when generating or editing the token, choose the **Unlimited** option for the per-minute and/or per-day limit. That removes the cap for that bucket; set **both** to Unlimited for a fully unthrottled token. ## Errors | Condition | Result | |---|---| | Missing / malformed / expired / revoked token, or client IP not on the token's allowlist | `401 Unauthenticated` | | Token valid but lacks permission for the action | GraphQL `errors[]` (Forbidden) | | Per-minute or per-day rate limit exceeded | `429 Too Many Requests` | ## Examples Use the interactive example on the right to see an authenticated request in GraphQL, cURL, Node.js, React, and PHP. --- # Attributes URL: /api/graphql-api/admin/catalog/attributes --- outline: false --- # Attributes Attributes are the fields a product can carry — `name`, `price`, `color`, `material`, and so on. The Attributes menu lists, creates, edits, and deletes them, and manages the selectable options of dropdown-style attributes. It mirrors the admin **Catalog → Attributes** screen. ## Attribute types The `type` (fixed at creation) decides how the field is captured and stored: | Type | Notes | |------|-------| | `text` / `textarea` | Free text / multi-line text. | | `price` / `boolean` | A decimal price / a yes-no flag. | | `date` / `datetime` | A date / a date-and-time. | | `image` / `file` | An uploaded image / file. | | `select` / `multiselect` / `checkbox` | A pick-list — these have **options** (see below). | ## Options (for select / multiselect / checkbox) Only `select`, `multiselect`, and `checkbox` attributes have options (e.g. a `color` attribute's Red / Green / Blue). Options are managed through their own mutations under the attribute, and each option carries its own per-locale translations. A `select` attribute can also drive **swatches** (`swatchType` = dropdown / color / image) used on the storefront. ## Configurable, filterable, system - **`isConfigurable`** — whether the attribute can be used as a variant-defining attribute for configurable products (e.g. colour × size). Only `select`-type attributes qualify. - **`isFilterable`** — whether it appears in storefront layered navigation. - **`isRequired` / `isUnique`** — validation on the product form. - **`valuePerLocale` / `valuePerChannel`** — whether the value can differ per locale / per channel. - **System attributes** (`isUserDefined = false`) are the built-in fields (sku, name, price, …). Their `code` and `type` are immutable and they cannot be deleted. ## Nested data is returned whole On the single-attribute query, `translations` and `options` (with their nested `translations`) are returned as **whole JSON** — query each as a bare field (`options`, not `options { … }`); the entire array comes back, and it resolves over GraphQL. ## Operations in this menu | Action | Operation | |--------|-----------| | [List attributes](/api/graphql-api/admin/catalog/attributes/attributes-listing) | `adminAttributes` query | | [Attribute detail](/api/graphql-api/admin/catalog/attributes/attributes-detail) | `adminAttribute(id:)` query | | [Create attribute](/api/graphql-api/admin/catalog/attributes/attributes-create) | `createAdminAttribute` mutation | | [Update attribute](/api/graphql-api/admin/catalog/attributes/attributes-update) | `updateAdminAttribute` mutation | | [Delete attribute](/api/graphql-api/admin/catalog/attributes/attributes-delete) | `deleteAdminAttribute` mutation | | [Mass delete](/api/graphql-api/admin/catalog/attributes/attributes-mass-delete) | `createAdminAttributeMassDelete` mutation | | [Attribute options (CRUD)](/api/graphql-api/admin/catalog/attributes/attribute-options) | `createAdminAttributeOption` / `updateAdminAttributeOption` / `deleteAdminAttributeOption` mutations | The single-attribute query embeds the full `translations` and `options` (with their translations) inline; the listing leaves those two heavy blocks out (fetch them by id). All Attributes operations require an admin Bearer token — see [Authentication](/api/graphql-api/admin/authentication). Reads require `catalog.attributes.view`; writes require the matching `catalog.attributes.create` / `.edit` / `.delete` permission. --- # Catalog Attribute Options — Create / Update / Delete URL: /api/graphql-api/admin/catalog/attributes/attribute-options --- outline: false examples: - id: admin-catalog-attribute-option-create title: Create Attribute Option description: Add a new option to a select/multiselect/checkbox attribute. Mirrors POST /api/admin/catalog/attributes/{attributeId}/options. query: | mutation CreateAttributeOption($input: createAdminAttributeOptionInput!, $attributeId: Int!) { createAdminAttributeOption(input: $input, attributeId: $attributeId) { adminAttribute { id _id } } } variables: | { "attributeId": 12, "input": { "adminName": "Wool", "sortOrder": 2, "translations": { "en": { "label": "Wool" }, "fr": { "label": "Laine" } } } } response: | { "data": { "createAdminAttributeOption": { "adminAttribute": { "id": "/api/admin/attributes/12", "_id": 12 } } } } - id: admin-catalog-attribute-option-update title: Update Attribute Option description: Partial update of one option. Mirrors PUT /api/admin/catalog/attributes/{attributeId}/options/{optionId}. query: | mutation UpdateAttributeOption($input: updateAdminAttributeOptionInput!, $attributeId: Int!, $optionId: Int!) { updateAdminAttributeOption(input: $input, attributeId: $attributeId, optionId: $optionId) { adminAttribute { id _id } } } variables: | { "attributeId": 12, "optionId": 45, "input": { "adminName": "Merino Wool", "sortOrder": 1, "translations": { "en": { "label": "Merino Wool" } } } } response: | { "data": { "updateAdminAttributeOption": { "adminAttribute": { "id": "/api/admin/attributes/12", "_id": 12 } } } } - id: admin-catalog-attribute-option-delete title: Delete Attribute Option description: Delete one option. Refused if products still reference the option. Mirrors DELETE /api/admin/catalog/attributes/{attributeId}/options/{optionId}. query: | mutation DeleteAttributeOption($input: deleteAdminAttributeOptionInput!) { deleteAdminAttributeOption(input: $input) { adminAttribute { id } } } variables: | { "input": { "id": "/api/admin/catalog/attributes/12/options/45" } } response: | { "data": { "deleteAdminAttributeOption": { "adminAttributeOption": { "id": "/api/admin/attribute_options/45" } } } } --- # Catalog Attribute Options — Create / Update / Delete GraphQL counterpart to the [REST attribute-options endpoints](/api/rest-api/admin/catalog/attributes/attribute-options). ## Operations | Operation | Type | Purpose | |-----------|------|---------| | `createAdminAttributeOption` | Mutation | Add an option to a `select`/`multiselect`/`checkbox` attribute | | `updateAdminAttributeOption` | Mutation | Partial update of an option | | `deleteAdminAttributeOption` | Mutation | Remove an option | All three mutations carry **extra args** alongside `input`: - `attributeId: Int!` — required on every mutation - `optionId: Int!` — required on update and delete ## Input — Create / Update | Field | Type | Notes | |-------|------|-------| | `admin_name` | `String` | Internal admin label (required on create) | | `sort_order` | `Int` | Display order | | `swatch_value` | `String` | Hex color / image path / display text depending on swatch type | | `translations` | `JSON` | Map of locale → `{ label }` | ## Errors | Condition | Message | |-----------|---------| | Attribute is not `select`/`multiselect`/`checkbox` | `This attribute type does not support options.` | | Delete refused — option in use | `This option is used by N product(s) and cannot be deleted.` | | Unknown id | `Attribute option not found.` | --- # Catalog Attribute — Create URL: /api/graphql-api/admin/catalog/attributes/attributes-create --- outline: false examples: - id: admin-catalog-attribute-create title: Create Attribute description: Create a new product attribute with optional translations and options. Mirrors the REST endpoint POST /api/admin/catalog/attributes. query: | mutation CreateAttribute($input: createAdminAttributeInput!) { createAdminAttribute(input: $input) { adminAttribute { id _id } } } variables: | { "input": { "code": "material", "adminName": "Material", "type": "select", "isFilterable": true, "translations": { "en": { "name": "Material" }, "fr": { "name": "Matière" } }, "options": [ { "adminName": "Cotton", "sortOrder": 1, "translations": { "en": { "label": "Cotton" } } } ] } } response: | { "data": { "createAdminAttribute": { "adminAttribute": { "id": "/api/admin/attributes/50", "_id": 50 } } } } --- # Catalog Attribute — Create Creates a new product attribute. Equivalent to [`POST /api/admin/catalog/attributes`](/api/rest-api/admin/catalog/attributes/attributes-create). ## Operation | Operation | Type | Purpose | |-----------|------|---------| | `createAdminAttribute` | Mutation | Create a new attribute | ## Input fields Same field set as the REST request body — `code`, `admin_name`, `type`, `swatch_type`, the boolean flag fields, `validation`, `default_value`, `position`, `translations`, `options`. See the [REST page](/api/rest-api/admin/catalog/attributes/attributes-create) for the full table. ## Notes - The mutation returns the attribute IRI (`id`) plus `_id`. For the full detail payload, follow up with the [`adminAttribute`](/api/graphql-api/admin/catalog/attributes/attributes-detail) query or the REST `GET /api/admin/catalog/attributes/{id}` endpoint. - Field names in the input are snake_case (`admin_name`, `is_filterable`) — the project's name-converter does NOT remap multi-word camelCase keys onto the create DTO. --- # Catalog Attribute — Delete URL: /api/graphql-api/admin/catalog/attributes/attributes-delete --- outline: false examples: - id: admin-catalog-attribute-delete title: Delete Attribute description: Deletes a user-defined attribute. Mirrors DELETE /api/admin/catalog/attributes/{id}. query: | mutation DeleteAttribute($input: deleteAdminAttributeInput!) { deleteAdminAttribute(input: $input) { adminAttribute { id } } } variables: | { "input": { "id": "/api/admin/attributes/50" } } response: | { "data": { "deleteAdminAttribute": { "adminAttribute": { "id": "/api/admin/attributes/50" } } } } --- # Catalog Attribute — Delete Deletes a user-defined attribute. Equivalent to [`DELETE /api/admin/catalog/attributes/{id}`](/api/rest-api/admin/catalog/attributes/attributes-delete). ::: tip Prerequisites The example uses an illustrative `id` value. Replace it with the id of a attribute that exists in your store — use the [`adminAttributes`](./attributes-listing.md) query to discover valid ids. ::: ## Operation | Operation | Type | Purpose | |-----------|------|---------| | `deleteAdminAttribute` | Mutation | Delete a user-defined attribute | ## Errors Each failure returns an `errors[]` entry: | Condition | Message | |-----------|---------| | System attribute (`is_user_defined = 0`) | `System attributes cannot be deleted.` | | Attribute is referenced by one or more attribute families | `Attribute is part of one or more attribute families. Remove it from those families first.` | | Unknown id | `Attribute not found.` | For bulk deletion, use [`createAdminAttributeMassDelete`](/api/graphql-api/admin/catalog/attributes/attributes-mass-delete). --- # Catalog Attribute — Detail (GraphQL) URL: /api/graphql-api/admin/catalog/attributes/attributes-detail --- outline: false examples: - id: admin-catalog-attribute-detail title: Attribute Detail (with translations and options) description: Fetch a single attribute by IRI including all locale translations and — for select/multiselect/checkbox types — all options with their own translations. query: | query AdminCatalogAttribute($id: ID!) { adminAttribute(id: $id) { id _id code type adminName isRequired isUnique valuePerLocale valuePerChannel isFilterable isConfigurable isVisibleOnFront isUserDefined swatchType position locale validation defaultValue isComparable enableWysiwyg regex createdAt updatedAt translations options } } variables: | { "id": "/api/admin/catalog/attributes/12" } response: | { "data": { "adminAttribute": { "id": "/api/admin/catalog/attributes/12", "_id": 12, "code": "color", "type": "select", "adminName": "Color", "isRequired": 0, "isUnique": 0, "valuePerLocale": 0, "valuePerChannel": 0, "isFilterable": 1, "isConfigurable": 1, "isVisibleOnFront": 1, "isUserDefined": 1, "swatchType": "color", "position": 5, "locale": "en", "validation": null, "defaultValue": null, "isComparable": 0, "enableWysiwyg": 0, "regex": null, "createdAt": "2026-01-12T08:15:00+00:00", "updatedAt": "2026-04-30T14:20:09+00:00", "translations": [ { "locale": "en", "name": "Color" }, { "locale": "fr", "name": "Couleur" } ], "options": [ { "id": 33, "adminName": "Red", "sortOrder": 1, "swatchValue": "#FF0000", "swatchValueUrl": null, "translations": [ { "locale": "en", "label": "Red" }, { "locale": "fr", "label": "Rouge" } ] }, { "id": 34, "adminName": "Blue", "sortOrder": 2, "swatchValue": "#0000FF", "swatchValueUrl": null, "translations": [ { "locale": "en", "label": "Blue" }, { "locale": "fr", "label": "Bleu" } ] } ] } } } --- # Catalog Attribute — Detail (GraphQL) GraphQL item query that returns a single attribute by its IRI, including the **full translations array** (every locale present in the database) and — for `select`, `multiselect`, and `checkbox` types — all **options** with their own locale translations. This is the query to call when an admin needs complete metadata for an attribute — e.g. when pre-populating the edit form in Catalog → Attributes. ## Operation | Operation | Type | |-----------|------| | `adminAttribute` | Query (item) | ## Authentication Every request must include an admin Bearer token: ``` Authorization: Bearer ``` Obtain a token via the [`createAdminLogin`](/api/graphql-api/admin/authentication) mutation. ## Arguments | Argument | Type | Required | Description | |----------|------|----------|-------------| | `id` | `ID!` | Yes | API Platform IRI of the attribute (e.g. `"/api/admin/catalog/attributes/12"`) | ::: tip Finding the IRI The IRI can be taken directly from `id` in any `adminAttributes` edge node, or constructed as `/api/admin/catalog/attributes/{numericId}`. ::: ## Fields | Field | Type | Description | |-------|------|-------------| | `id` | `ID` | API Platform IRI (e.g. `/api/admin/catalog/attributes/12`) | | `_id` | `Int` | Raw attribute ID | | `code` | `String` | Attribute code (e.g. `color`, `size`) | | `type` | `String` | Attribute type (e.g. `select`, `text`, `boolean`) | | `adminName` | `String` | Internal admin-facing label | | `isRequired` | `Int` | `1` = required on product forms | | `isUnique` | `Int` | `1` = value must be unique across products | | `valuePerLocale` | `Int` | `1` = separate value per store locale | | `valuePerChannel` | `Int` | `1` = separate value per channel | | `isFilterable` | `Int` | `1` = appears in layered navigation | | `isConfigurable` | `Int` | `1` = used as a configurable variant axis | | `isVisibleOnFront` | `Int` | `1` = shown on the storefront product page | | `isUserDefined` | `Int` | `1` = admin-created (not a system attribute) | | `swatchType` | `String` | Swatch mode (`color`, `image`, `text`); `null` for non-swatch types | | `position` | `Int` | Display order position | | `locale` | `String` | App locale used for top-level scalar fields | | `validation` | `String` | Validation rule string (e.g. `numeric`, `email`); `null` if none | | `defaultValue` | `String` | Default value; `null` if not configured | | `isComparable` | `Int` | `1` if shown in the storefront product-compare table, else `0` | | `enableWysiwyg` | `Int` | `1` if a rich-text editor is used for a `textarea` attribute, else `0` | | `regex` | `String` | Custom regex pattern, used when `validation` is `regex`; `null` otherwise | | `createdAt` | `String` | ISO 8601 creation timestamp | | `updatedAt` | `String` | ISO 8601 last-update timestamp | | `translations` | scalar (JSON array) | All locale translations — see shape below | | `options` | scalar (JSON array\|null) | Options for `select`/`multiselect`/`checkbox` types; `null` for other types | ### `translations` item shape `translations` is returned as a **plain JSON array** (scalar in GraphQL). Each element corresponds to one locale row in `attribute_translations`: | Key | Type | Description | |-----|------|-------------| | `locale` | string | Locale code (e.g. `en`, `fr`) | | `name` | string\|null | Locale-specific attribute display name | ### `options[]` item shape `options` is returned as a **plain JSON array** (scalar in GraphQL). Each element corresponds to one row in `attribute_options`: | Key | Type | Description | |-----|------|-------------| | `id` | integer | Option ID | | `adminName` | string | Internal admin label for the option | | `sortOrder` | integer | Display sort order | | `swatchValue` | string\|null | Swatch value (hex for `color`, path for `image`, text for `text`); `null` otherwise | | `swatchValueUrl` | string\|null | Full URL to the swatch image for `image` swatches; `null` for other types | | `translations` | array | Locale translations for this option (see below) | ### `options[].translations[]` item shape | Key | Type | Description | |-----|------|-------------| | `locale` | string | Locale code (e.g. `en`, `fr`) | | `label` | string\|null | Locale-specific display label for the option | ::: warning translations and options are returned whole `translations` and `options` (each option with its nested `translations`) are returned as **whole JSON** — query each as a bare field (`translations`, not `translations { … }`). The entire structure comes back, and they resolve over GraphQL. ::: ## Example Query ```graphql query AdminCatalogAttribute($id: ID!) { adminAttribute(id: $id) { id _id code type adminName isRequired isUnique valuePerLocale valuePerChannel isFilterable isConfigurable isVisibleOnFront isUserDefined swatchType position locale validation defaultValue createdAt updatedAt translations options } } ``` ```json { "id": "/api/admin/catalog/attributes/12" } ``` ## Example Response ```json { "data": { "adminAttribute": { "id": "/api/admin/catalog/attributes/12", "_id": 12, "code": "color", "type": "select", "adminName": "Color", "isRequired": 0, "isUnique": 0, "valuePerLocale": 0, "valuePerChannel": 0, "isFilterable": 1, "isConfigurable": 1, "isVisibleOnFront": 1, "isUserDefined": 1, "swatchType": "color", "position": 5, "locale": "en", "validation": null, "defaultValue": null, "isComparable": 0, "enableWysiwyg": 0, "regex": null, "createdAt": "2026-01-12T08:15:00+00:00", "updatedAt": "2026-04-30T14:20:09+00:00", "translations": [ { "locale": "en", "name": "Color" }, { "locale": "fr", "name": "Couleur" } ], "options": [ { "id": 33, "adminName": "Red", "sortOrder": 1, "swatchValue": "#FF0000", "swatchValueUrl": null, "translations": [ { "locale": "en", "label": "Red" }, { "locale": "fr", "label": "Rouge" } ] }, { "id": 34, "adminName": "Blue", "sortOrder": 2, "swatchValue": "#0000FF", "swatchValueUrl": null, "translations": [ { "locale": "en", "label": "Blue" }, { "locale": "fr", "label": "Bleu" } ] } ] } } } ``` ## Errors | Scenario | GraphQL `errors[]` | HTTP Status | |----------|-------------------|-------------| | Unknown ID | `"Attribute not found"` in `errors[]`, `data.adminAttribute: null` | `200` (GraphQL convention) | | Missing auth | `"Unauthenticated"` in `errors[]` | `200` | ## Notes - **`translations` and `options` are plain JSON scalars**, not typed GraphQL object lists. You access them as regular JSON arrays in the response. This avoids API Platform serializing nested objects as IRI strings instead of inline objects — a known behaviour when using nested DTO types in API Platform GraphQL. - **`options` is `null` for non-option types.** Only `select`, `multiselect`, and `checkbox` attributes have options. For all other types, `options` is `null`. - **`translations` contains every locale in the DB**, not just the current app locale. If the store has translations for `en`, `fr`, and `de`, all three entries are returned. Missing locale entries have `null` name. - **The `id` argument is the IRI**, not the numeric integer. Use the `_id` field from listing queries and construct `"/api/admin/catalog/attributes/{_id}"`, or pass the `id` field directly from a listing result. - **GraphQL and REST return identical data** for this query — both return `translations` and `options` as plain JSON arrays queried bare (no sub-selection), and every camelCase flag field (`isRequired`, `isConfigurable`, `adminName`, etc.) resolves over GraphQL. --- # Catalog Attributes — Datagrid Listing (GraphQL) URL: /api/graphql-api/admin/catalog/attributes/attributes-listing --- outline: false examples: - id: admin-catalog-attributes-list title: List Catalog Attributes (Datagrid) description: Cursor-paginated query returning the flat attribute list — the GraphQL equivalent of the admin Catalog → Attributes datagrid. Supports filtering by code, type, admin_name, boolean flags, and locale. query: | query AdminCatalogAttributes( $first: Int $after: String $type: String $isConfigurable: Int $locale: String ) { adminAttributes( first: $first after: $after type: $type isConfigurable: $isConfigurable locale: $locale ) { edges { cursor node { id _id code type adminName isRequired isUnique valuePerLocale valuePerChannel isFilterable isConfigurable isVisibleOnFront isUserDefined swatchType position locale createdAt updatedAt } } pageInfo { hasNextPage hasPreviousPage endCursor startCursor } totalCount } } variables: | { "first": 10, "type": "select", "isConfigurable": 1, "locale": "en" } response: | { "data": { "adminAttributes": { "edges": [ { "cursor": "MA==", "node": { "id": "/api/admin/catalog/attributes/12", "_id": 12, "code": "color", "type": "select", "adminName": "Color", "isRequired": 0, "isUnique": 0, "valuePerLocale": 0, "valuePerChannel": 0, "isFilterable": 1, "isConfigurable": 1, "isVisibleOnFront": 1, "isUserDefined": 1, "swatchType": "color", "position": 5, "locale": "en", "createdAt": "2026-01-12T08:15:00+00:00", "updatedAt": "2026-04-30T14:20:09+00:00" } } ], "pageInfo": { "hasNextPage": false, "hasPreviousPage": false, "endCursor": "MA==", "startCursor": "MA==" }, "totalCount": 1 } } } --- # Catalog Attributes — Datagrid Listing (GraphQL) Cursor-paginated GraphQL query that mirrors the Bagisto admin **Catalog → Attributes** datagrid. Returns the flat attribute list with filtering, sorting, and cursor pagination. ::: tip How this menu works For attribute types, options, and the configurable/filterable flags, see the [Attributes overview](/api/graphql-api/admin/catalog/attributes/). ::: ## Operation | Operation | Type | Pagination | |-----------|------|------------| | `adminAttributes` | Query | Cursor (`first` / `after`) | ::: tip Operation name API Platform derives the GraphQL operation name from the resource `shortName`. `AdminAttribute` has `shortName: 'AdminAttribute'`, so the collection query is `adminAttributes` (the `Catalog` segment is the API tag, not part of the name). The item query is `adminAttribute(id: ID!)`. ::: ## Authentication Every request must include an admin Bearer token: ``` Authorization: Bearer ``` Obtain a token via the [`createAdminLogin`](/api/graphql-api/admin/authentication) mutation. ## Arguments | Argument | Type | Description | Example | |----------|------|-------------|---------| | `first` | `Int` | Number of items per page (default `10`, max `50`) | `10` | | `after` | `String` | Cursor from a previous `pageInfo.endCursor` for keyset pagination | `"MA=="` | | `id` | `String` | Filter by attribute ID — single integer or comma-separated list (e.g. `"1"` or `"1,2"`) | `"12"` | | `code` | `String` | Partial attribute code match (SQL `LIKE %value%`) | `"color"` | | `type` | `String` | Exact attribute type filter | `"select"` | | `admin_name` | `String` | Partial admin name match (SQL `LIKE %value%`) | `"Color"` | | `is_required` | `Int` | Filter by is_required: `0` = no, `1` = yes | `1` | | `is_unique` | `Int` | Filter by is_unique: `0` = no, `1` = yes | `0` | | `is_filterable` | `Int` | Filter by is_filterable: `0` = no, `1` = yes | `1` | | `is_configurable` | `Int` | Filter by is_configurable: `0` = no, `1` = yes | `1` | | `is_visible_on_front` | `Int` | Filter by is_visible_on_front: `0` = no, `1` = yes | `1` | | `is_user_defined` | `Int` | Filter by is_user_defined: `0` = no, `1` = yes | `1` | | `locale` | `String` | Locale code for translation resolution (default: app locale) | `"en"` | | `sort` | `String` | Column to sort by (see Sorting section below) | `"id"` | | `order` | `String` | Sort direction: `"asc"` or `"desc"` (default `"desc"`) | `"desc"` | ::: tip extraArgs convention API Platform does not automatically expose filter arguments in the GraphQL schema for `QueryCollection` operations. This query uses `extraArgs` declared on the `QueryCollection` operation to surface all filter/sort arguments as first-class GraphQL arguments. Pass them directly alongside `first` and `after`. ::: ## Node Fields Each `edges[].node` object contains: | Field | Type | Description | |-------|------|-------------| | `id` | `ID` | API Platform IRI (e.g. `/api/admin/catalog/attributes/12`) | | `_id` | `Int` | Raw attribute ID | | `code` | `String` | Attribute code (e.g. `color`, `size`) | | `type` | `String` | Attribute type (e.g. `select`, `text`, `boolean`) | | `adminName` | `String` | Internal admin-facing label | | `isRequired` | `Int` | `1` = required on product forms | | `isUnique` | `Int` | `1` = value must be unique across products | | `valuePerLocale` | `Int` | `1` = separate value per store locale | | `valuePerChannel` | `Int` | `1` = separate value per channel | | `isFilterable` | `Int` | `1` = appears in layered navigation | | `isConfigurable` | `Int` | `1` = used as a configurable variant axis | | `isVisibleOnFront` | `Int` | `1` = shown on the storefront product page | | `isUserDefined` | `Int` | `1` = admin-created (not a system attribute) | | `swatchType` | `String` | Swatch mode (`color`, `image`, `text`); `null` for non-swatch types | | `position` | `Int` | Display order position | | `locale` | `String` | Locale code used for name resolution | | `createdAt` | `String` | ISO 8601 creation timestamp | | `updatedAt` | `String` | ISO 8601 last-update timestamp | ::: warning translations and options not available in listing `translations` and `options` are **not returned** in this query — they are only available in the item query `adminAttribute(id: ID!)`. Use the item query when you need per-locale display names or option lists for a specific attribute. ::: ## Example Query ```graphql query AdminCatalogAttributes( $first: Int $after: String $type: String $is_configurable: Int $locale: String ) { adminAttributes( first: $first after: $after type: $type is_configurable: $is_configurable locale: $locale ) { edges { cursor node { id _id code type adminName isRequired isUnique valuePerLocale valuePerChannel isFilterable isConfigurable isVisibleOnFront isUserDefined swatchType position locale createdAt updatedAt } } pageInfo { hasNextPage hasPreviousPage endCursor startCursor } totalCount } } ``` ```json { "first": 10, "type": "select", "is_configurable": 1, "locale": "en" } ``` ## Example Response ```json { "data": { "adminAttributes": { "edges": [ { "cursor": "MA==", "node": { "id": "/api/admin/catalog/attributes/12", "_id": 12, "code": "color", "type": "select", "adminName": "Color", "isRequired": 0, "isUnique": 0, "valuePerLocale": 0, "valuePerChannel": 0, "isFilterable": 1, "isConfigurable": 1, "isVisibleOnFront": 1, "isUserDefined": 1, "swatchType": "color", "position": 5, "locale": "en", "createdAt": "2026-01-12T08:15:00+00:00", "updatedAt": "2026-04-30T14:20:09+00:00" } } ], "pageInfo": { "hasNextPage": false, "hasPreviousPage": false, "endCursor": "MA==", "startCursor": "MA==" }, "totalCount": 1 } } } ``` ## Sorting Pass `sort` with the column name and `order` for direction. **Sortable columns:** | `sort` value | Sorts by | |---|---| | `id` | Attribute ID (default) | | `code` | Attribute code | | `admin_name` | Admin label | | `type` | Attribute type | | `position` | Display order position | ## Cursor Pagination - `first` controls the page size (default `10`, max `50`). - To fetch the next page, pass the `pageInfo.endCursor` value as `after` in the next request. - `totalCount` reflects the full count of matching attributes across all pages. ## Notes - **GraphQL and REST share identical filter and sort semantics** — the same results are returned over both transports. - **No automatic filter applied.** All attributes (system and user-defined) are returned by default. Pass `is_user_defined: 1` to restrict to admin-created attributes. - **Pass filter and sort arguments at the top level** of the query, alongside `first` and `after`. - **Every camelCase flag field resolves over GraphQL** — `isRequired`, `isConfigurable`, `isFilterable`, etc. all come back populated; GraphQL and REST return the same values. --- # Catalog Attribute — Mass Delete URL: /api/graphql-api/admin/catalog/attributes/attributes-mass-delete --- outline: false examples: - id: admin-catalog-attribute-mass-delete title: Mass Delete Attributes description: Delete a batch of user-defined attributes. The whole batch is pre-validated — if any id is a system attribute, no row is deleted. Mirrors POST /api/admin/catalog/attributes/mass-delete. query: | mutation MassDeleteAttributes($input: createAdminAttributeMassDeleteInput!) { createAdminAttributeMassDelete(input: $input) { adminAttributeMassDelete { id deleted message } } } variables: | { "input": { "indices": [24, 31] } } response: | { "data": { "createAdminAttributeMassDelete": { "adminAttributeMassDelete": { "id": "/api/admin/attribute_mass_deletes/1", "deleted": [24, 31], "message": "Attributes deleted successfully." } } } } --- # Catalog Attribute — Mass Delete Bulk-deletes a batch of user-defined attributes in a single mutation. Equivalent to [`POST /api/admin/catalog/attributes/mass-delete`](/api/rest-api/admin/catalog/attributes/attributes-mass-delete). ## Operation | Operation | Type | Purpose | |-----------|------|---------| | `createAdminAttributeMassDelete` | Mutation | Delete multiple user-defined attributes at once | ## Input | Field | Type | Notes | |-------|------|-------| | `indices` | `[Int!]!` | Attribute ids to delete | ## Notes - **All-or-nothing.** If any id is a system attribute, the whole batch fails with an `errors[]` entry `System attributes cannot be deleted.` — no row is touched. - Unknown ids are silently skipped — they do not appear in `deleted`. --- # Catalog Attribute — Update URL: /api/graphql-api/admin/catalog/attributes/attributes-update --- outline: false examples: - id: admin-catalog-attribute-update title: Update Attribute description: Update an existing attribute. `code` is immutable; `type` cannot change while product attribute values reference the attribute. Mirrors PUT /api/admin/catalog/attributes/{id}. query: | mutation UpdateAttribute($input: updateAdminAttributeInput!) { updateAdminAttribute(input: $input) { adminAttribute { id _id } } } variables: | { "input": { "id": "/api/admin/attributes/50", "code": "material", "adminName": "Material (updated)", "type": "select", "isFilterable": true, "translations": { "en": { "name": "Material (updated)" } } } } response: | { "data": { "updateAdminAttribute": { "adminAttribute": { "id": "/api/admin/attributes/50", "_id": 50 } } } } --- # Catalog Attribute — Update Updates an existing attribute. Equivalent to [`PUT /api/admin/catalog/attributes/{id}`](/api/rest-api/admin/catalog/attributes/attributes-update). ## Operation | Operation | Type | Purpose | |-----------|------|---------| | `updateAdminAttribute` | Mutation | Update an attribute | ## Input fields Same field set as the REST request body plus the resource IRI `id`. See the [REST page](/api/rest-api/admin/catalog/attributes/attributes-update) for the full list and immutable-field caveats. ## Notes - The mutation returns the IRI of the updated attribute. Follow up with the [`adminAttribute`](/api/graphql-api/admin/catalog/attributes/attributes-detail) query to load the refreshed detail. - `code` is immutable. Supplying a different code raises an `errors[]` entry with `Attribute code cannot be changed.` - Supplying `options` performs a full-set replacement (insert/update/delete) — same semantics as REST. --- # Categories URL: /api/graphql-api/admin/catalog/categories --- outline: false --- # Categories Categories are the storefront's browsing tree — the sections customers navigate and products are assigned to. The Categories menu lists them (flat or as a nested tree), shows a single category, and creates / edits / deletes / moves them. It mirrors the admin **Catalog → Categories** screen. ## Flat list vs. tree Two read shapes for the same data: - **List** (`adminCategories`) — a flat, cursor-paginated, filterable datagrid of categories. Each row carries the category's own fields plus its `parentId`. - **Tree** (`adminCategoryTrees`) — the full nested hierarchy, each node carrying its `children` recursively. Use this to render the category tree in one call. ## Hierarchy, status, display - **`parentId`** places a category under its parent. The root category (`id = 1`) and any channel's root category are structural and **cannot be deleted**. - **Moving** a category is just an update with a new `parentId` (and `position`) — there is no separate "move" operation. - **`status`** (1 enabled / 0 disabled) and **`position`** (sort order among siblings). - **`displayMode`** — what the category page shows: `products_and_description`, `products_only`, or `description_only`. ## Nested data is returned whole On the single-category query, `translations` and `filterableAttributeIds` are returned as **whole JSON** — query each as a bare field (`translations`, not `translations { … }`); the entire array comes back, and it resolves over GraphQL. The tree's `children` are likewise whole JSON nodes. The listing leaves `translations` / `filterableAttributeIds` out (detail-only). ## Operations in this menu | Action | Operation | |--------|-----------| | [List categories](/api/graphql-api/admin/catalog/categories/categories-listing) | `adminCategories` query | | [Category tree](/api/graphql-api/admin/catalog/categories/categories-tree) | `adminCategoryTrees` query | | [Category detail](/api/graphql-api/admin/catalog/categories/categories-detail) | `adminCategory(id:)` query | | [Create category](/api/graphql-api/admin/catalog/categories/categories-create) | `createAdminCategory` mutation | | [Update / move category](/api/graphql-api/admin/catalog/categories/categories-update) | `updateAdminCategory` mutation | | [Delete category](/api/graphql-api/admin/catalog/categories/categories-delete) | `deleteAdminCategory` mutation | | [Mass delete](/api/graphql-api/admin/catalog/categories/categories-mass-delete) | `createAdminCategoryMassDelete` mutation | | [Mass update status](/api/graphql-api/admin/catalog/categories/categories-mass-update-status) | `createAdminCategoryMassUpdateStatus` mutation | All Categories operations require an admin Bearer token — see [Authentication](/api/graphql-api/admin/authentication). Reads require `catalog.categories.view`; writes require the matching `catalog.categories.create` / `.edit` / `.delete` permission. --- # Category — Create URL: /api/graphql-api/admin/catalog/categories/categories-create --- outline: false examples: - id: admin-catalog-category-create title: Create Category description: Create a new category. File upload for logo/banner is NOT supported in v1. Mirrors POST /api/admin/catalog/categories. query: | mutation CreateCategory($input: createAdminCategoryInput!) { createAdminCategory(input: $input) { adminCategory { id _id } } } variables: | { "input": { "slug": "apparel", "name": "Apparel", "description": "Men's and women's apparel", "position": 1, "attributes": [11, 23], "parentId": 1, "displayMode": "products_and_description", "status": 1, "locale": "en" } } response: | { "data": { "createAdminCategory": { "adminCategory": { "id": "/api/admin/catalog/categories/7", "_id": 7 } } } } --- # Category — Create Creates a new category. Equivalent to [`POST /api/admin/catalog/categories`](/api/rest-api/admin/catalog/categories/categories-create). ## Operation | Operation | Type | Purpose | |-----------|------|---------| | `createAdminCategory` | Mutation | Create a new category | ## Input Same fields as the REST endpoint — `slug`, `name`, `description`, `position`, `attributes`, `parent_id`, `display_mode`, `status`, `locale`, and optional SEO fields. See the [REST page](/api/rest-api/admin/catalog/categories/categories-create) for the full field table and validation rules. ## Notes - **File upload not supported in v1** — `logo_path` / `banner_path` cannot be set via this mutation. - Follow up with the [`adminCategory`](/api/graphql-api/admin/catalog/categories/categories-detail) query to load the refreshed detail. --- # Category — Delete URL: /api/graphql-api/admin/catalog/categories/categories-delete --- outline: false examples: - id: admin-catalog-category-delete title: Delete Category description: Refused for the root category or any category referenced as a channel root. Mirrors DELETE /api/admin/catalog/categories/{id}. query: | mutation DeleteCategory($input: deleteAdminCategoryInput!) { deleteAdminCategory(input: $input) { adminCategory { id } } } variables: | { "input": { "id": "/api/admin/catalog/categories/7" } } response: | { "data": { "deleteAdminCategory": { "adminCategory": { "id": "/api/admin/catalog/categories/7" } } } } --- # Category — Delete Deletes a category. Equivalent to [`DELETE /api/admin/catalog/categories/{id}`](/api/rest-api/admin/catalog/categories/categories-delete). ::: tip Prerequisites The example uses an illustrative `id` value. Replace it with the id of a category that exists in your store — use the [`adminCategories`](./categories-listing.md) query to discover valid ids. ::: ## Operation | Operation | Type | Purpose | |-----------|------|---------| | `deleteAdminCategory` | Mutation | Delete a category | ## Errors | Condition | Message | |-----------|---------| | Root category (`id=1`) or a channel `root_category_id` | `Root and channel-root categories cannot be deleted.` | | Unknown id | `Category not found.` | For bulk deletion, use [`createAdminCategoryMassDelete`](/api/graphql-api/admin/catalog/categories/categories-mass-delete). --- # Catalog Category — Detail (GraphQL) URL: /api/graphql-api/admin/catalog/categories/categories-detail --- outline: false examples: - id: admin-catalog-category-detail title: Category Detail (with all translations) description: Fetch a single category by ID including the full translations array (all locales present in the DB) and the list of filterable attribute IDs. query: | query AdminCatalogCategory($id: ID!) { adminCategory(id: $id) { id _id name slug status position parentId displayMode logoUrl bannerUrl description locale createdAt updatedAt translations filterableAttributeIds } } variables: | { "id": "/api/admin/catalog/categories/7" } response: | { "data": { "adminCategory": { "id": "/api/admin/catalog/categories/7", "_id": 7, "name": "Apparel", "slug": "apparel", "status": 1, "position": 1, "parentId": 1, "displayMode": "products_and_description", "logoUrl": "https://example.com/storage/category/7/logo.webp", "bannerUrl": null, "description": "Men's and women's apparel", "locale": "en", "createdAt": "2026-01-12T08:15:00+00:00", "updatedAt": "2026-04-30T14:20:09+00:00", "translations": [ { "locale": "en", "name": "Apparel", "slug": "apparel", "description": "Men's and women's apparel", "metaTitle": null, "metaDescription": null, "metaKeywords": null }, { "locale": "fr", "name": "Vêtements", "slug": "vetements", "description": null, "metaTitle": null, "metaDescription": null, "metaKeywords": null } ], "filterableAttributeIds": [11, 23] } } } --- # Catalog Category — Detail (GraphQL) GraphQL item query that returns a single category by its IRI, including the **full translations array** (every locale present in the database) and the list of **filterable attribute IDs** configured for the category. This is the query to call when an admin needs complete metadata for a category — e.g. when pre-populating the edit form in Catalog → Categories. ## Operation | Operation | Type | |-----------|------| | `adminCategory` | Query (item) | ## Authentication Every request must include an admin Bearer token: ``` Authorization: Bearer ``` Obtain a token via the [`createAdminLogin`](/api/graphql-api/admin/authentication) mutation. ## Arguments | Argument | Type | Required | Description | |----------|------|----------|-------------| | `id` | `ID!` | Yes | API Platform IRI of the category (e.g. `"/api/admin/catalog/categories/7"`) | ::: tip Finding the IRI The IRI can be taken directly from `id` in any `adminCategories` or `adminCategoryTrees` edge node, or constructed as `/api/admin/catalog/categories/{numericId}`. ::: ## Node Fields | Field | Type | Description | |-------|------|-------------| | `id` | `ID` | API Platform IRI (e.g. `/api/admin/catalog/categories/7`) | | `_id` | `Int` | Raw category ID | | `name` | `String` | Category name in the current app locale | | `slug` | `String` | URL slug in the current app locale | | `status` | `Int` | `1` = enabled, `0` = disabled | | `position` | `Int` | Display order position | | `parentId` | `Int` | Parent category ID; `null` for root nodes | | `displayMode` | `String` | Category display mode (e.g. `products_and_description`) | | `logoUrl` | `String` | Storage URL for the category logo; `null` if not set | | `bannerUrl` | `String` | Storage URL for the category banner; `null` if not set | | `description` | `String` | Category description in the current app locale | | `locale` | `String` | App locale used for top-level scalar fields | | `createdAt` | `String` | ISO 8601 creation timestamp | | `updatedAt` | `String` | ISO 8601 last-update timestamp | | `translations` | scalar (JSON array) | All locale translations (see below) | | `filterableAttributeIds` | scalar (JSON array) | Integer IDs of filterable attributes configured for this category | ### `translations` item shape `translations` is returned as a **plain JSON array** (scalar in GraphQL). Each element corresponds to one locale row in `category_translations`: | Key | Type | Description | |-----|------|-------------| | `locale` | string | Locale code (e.g. `en`, `fr`) | | `name` | string\|null | Category name in this locale | | `slug` | string\|null | URL slug in this locale | | `description` | string\|null | Description in this locale | | `metaTitle` | string\|null | SEO meta title | | `metaDescription` | string\|null | SEO meta description | | `metaKeywords` | string\|null | SEO meta keywords | ## Example Query ```graphql query AdminCatalogCategory($id: ID!) { adminCategory(id: $id) { id _id name slug status position parentId displayMode logoUrl bannerUrl description locale createdAt updatedAt translations filterableAttributeIds } } ``` ```json { "id": "/api/admin/catalog/categories/7" } ``` ## Example Response ```json { "data": { "adminCategory": { "id": "/api/admin/catalog/categories/7", "_id": 7, "name": "Apparel", "slug": "apparel", "status": 1, "position": 1, "parentId": 1, "displayMode": "products_and_description", "logoUrl": "https://example.com/storage/category/7/logo.webp", "bannerUrl": null, "description": "Men's and women's apparel", "locale": "en", "createdAt": "2026-01-12T08:15:00+00:00", "updatedAt": "2026-04-30T14:20:09+00:00", "translations": [ { "locale": "en", "name": "Apparel", "slug": "apparel", "description": "Men's and women's apparel", "metaTitle": null, "metaDescription": null, "metaKeywords": null }, { "locale": "fr", "name": "Vêtements", "slug": "vetements", "description": null, "metaTitle": null, "metaDescription": null, "metaKeywords": null } ], "filterableAttributeIds": [11, 23] } } } ``` ## Errors | Scenario | GraphQL `errors[]` | HTTP Status | |----------|-------------------|-------------| | Unknown ID | `"Category not found"` in `errors[]`, `data.adminCategory: null` | `200` (GraphQL convention) | | Missing auth | `"Unauthenticated"` in `errors[]` | `200` | ## Notes - **`translations` is a plain JSON scalar**, not a typed GraphQL object list. You access it as a regular JSON array. This avoids API Platform serializing nested DTO objects as IRI strings instead of inline objects. - **`translations` contains every locale in the DB**, not just the current app locale. If the store has 3 locale rows (`en`, `fr`, `de`), all three are returned. Fields without content for a locale are `null`. - **`filterableAttributeIds`** is a plain JSON scalar array of integers. An empty array `[]` means no filterable attributes have been configured for this category. - **The `id` argument is the IRI**, not the numeric integer. Use the `_id` field from listing or tree queries and construct `"/api/admin/catalog/categories/{_id}"`, or pass the `id` field directly. - **`translations` and `filterableAttributeIds` are returned whole** — query each as a bare field (no sub-selection); the entire structure resolves over GraphQL. The REST detail endpoint returns the same data. --- # Catalog Categories — Datagrid Listing (GraphQL) URL: /api/graphql-api/admin/catalog/categories/categories-listing --- outline: false examples: - id: admin-catalog-categories-list title: List Catalog Categories (Datagrid) description: Cursor-paginated query returning the flat category list — the GraphQL equivalent of the admin Catalog → Categories datagrid. Supports filtering by name, status, position, parent_id, and locale. query: | query AdminCatalogCategories( $first: Int $after: String $name: String $status: Int $locale: String ) { adminCategories( first: $first after: $after name: $name status: $status locale: $locale ) { edges { cursor node { id _id name slug status position parentId displayMode locale logoUrl bannerUrl createdAt updatedAt } } pageInfo { hasNextPage hasPreviousPage endCursor startCursor } totalCount } } variables: | { "first": 10, "name": "Apparel", "status": 1, "locale": "en" } response: | { "data": { "adminCategories": { "edges": [ { "cursor": "MA==", "node": { "id": "/api/admin/catalog/categories/7", "_id": 7, "name": "Apparel", "slug": "apparel", "status": 1, "position": 1, "parentId": 1, "displayMode": "products_and_description", "locale": "en", "logoUrl": null, "bannerUrl": null, "createdAt": "2026-01-12T08:15:00+00:00", "updatedAt": "2026-04-30T14:20:09+00:00" } } ], "pageInfo": { "hasNextPage": false, "hasPreviousPage": false, "endCursor": "MA==", "startCursor": "MA==" }, "totalCount": 1 } } } --- # Catalog Categories — Datagrid Listing (GraphQL) Cursor-paginated GraphQL query that mirrors the Bagisto admin **Catalog → Categories** datagrid. Returns the flat category list with filtering, sorting, and cursor pagination. ::: tip How this menu works For the flat-list vs. tree shapes, hierarchy/move semantics, and display modes, see the [Categories overview](/api/graphql-api/admin/catalog/categories/). ::: ::: tip Distinct from the tree query `adminCategories` (this query) returns a **flat, cursor-paginated list** — ideal for datagrid/table views. `adminCategoryTrees` returns the **nested hierarchy** — ideal for tree-picker UIs. ::: ## Operation | Operation | Type | Pagination | |-----------|------|------------| | `adminCategories` | Query | Cursor (`first` / `after`) | ## Authentication Every request must include an admin Bearer token: ``` Authorization: Bearer ``` Obtain a token via the [`createAdminLogin`](/api/graphql-api/admin/authentication) mutation. ## Arguments | Argument | Type | Description | Example | |----------|------|-------------|---------| | `first` | `Int` | Number of items per page (default `10`, max `50`) | `10` | | `after` | `String` | Cursor from a previous `pageInfo.endCursor` for keyset pagination | `"MA=="` | | `category_id` | `String` | Filter by category ID — single integer or comma-separated list (e.g. `"12"` or `"12,18"`) | `"7"` | | `name` | `String` | Partial category name match (SQL `LIKE %value%`) | `"Apparel"` | | `position` | `Int` | Exact position filter | `1` | | `status` | `Int` | Filter by status: `0` = disabled, `1` = enabled | `1` | | `parent_id` | `Int` | Filter by parent category ID | `1` | | `locale` | `String` | Locale code for translation resolution (default: app locale) | `"en"` | | `sort` | `String` | Column to sort by (see Sorting section below) | `"id"` | | `order` | `String` | Sort direction: `"asc"` or `"desc"` (default `"desc"`) | `"desc"` | ::: tip extraArgs convention API Platform does not automatically expose filter arguments in the GraphQL schema for `QueryCollection` operations. This query uses `extraArgs` declared on the `QueryCollection` operation to surface all filter/sort arguments as first-class GraphQL arguments. Pass them directly alongside `first` and `after`. ::: ## Node Fields Each `edges[].node` object contains: | Field | Type | Description | |-------|------|-------------| | `id` | `ID` | API Platform IRI (e.g. `/api/admin/catalog/categories/7`) | | `_id` | `Int` | Raw category ID | | `name` | `String` | Category name resolved via `locale` | | `slug` | `String` | URL slug | | `status` | `Int` | `1` = enabled, `0` = disabled | | `position` | `Int` | Display order position | | `parentId` | `Int` | Parent category ID; `null` for root nodes | | `displayMode` | `String` | Category display mode (e.g. `products_and_description`) | | `locale` | `String` | Locale used for name/slug/description resolution | | `logoUrl` | `String` | Storage URL for the category logo; `null` if not set | | `bannerUrl` | `String` | Storage URL for the category banner; `null` if not set | | `createdAt` | `String` | ISO 8601 creation timestamp | | `updatedAt` | `String` | ISO 8601 last-update timestamp | ## Example Query ```graphql query AdminCatalogCategories( $first: Int $after: String $name: String $status: Int $locale: String ) { adminCategories( first: $first after: $after name: $name status: $status locale: $locale ) { edges { cursor node { id _id name slug status position parentId displayMode locale logoUrl bannerUrl createdAt updatedAt } } pageInfo { hasNextPage hasPreviousPage endCursor startCursor } totalCount } } ``` ```json { "first": 10, "name": "Apparel", "status": 1, "locale": "en" } ``` ## Example Response ```json { "data": { "adminCategories": { "edges": [ { "cursor": "MA==", "node": { "id": "/api/admin/catalog/categories/7", "_id": 7, "name": "Apparel", "slug": "apparel", "status": 1, "position": 1, "parentId": 1, "displayMode": "products_and_description", "locale": "en", "logoUrl": null, "bannerUrl": null, "createdAt": "2026-01-12T08:15:00+00:00", "updatedAt": "2026-04-30T14:20:09+00:00" } } ], "pageInfo": { "hasNextPage": false, "hasPreviousPage": false, "endCursor": "MA==", "startCursor": "MA==" }, "totalCount": 1 } } } ``` ## Sorting Pass `sort` with the column name and `order` for direction. The compound form `sort: "name-asc"` is also accepted (splits on `-`). **Sortable columns:** | `sort` value | Sorts by | |---|---| | `id` | Category ID (default) | | `name` | Category name | | `position` | Display order position | | `status` | Enabled/disabled status | ## Cursor Pagination - `first` controls the page size (default `10`, max `50`). - To fetch the next page, pass the `pageInfo.endCursor` value as `after` in the next request. - `totalCount` reflects the full count of matching categories across all pages. ## Notes - **`translations` and `filterableAttributeIds` are not available** in this query — they are only returned by the item query `adminCategory(id: ID!)`. Use the item query when you need per-locale metadata for a specific category. - **Same provider as the REST endpoint** — `AdminCategoryCollectionProvider` serves both transports with identical filter and sort semantics. - **No automatic status filter** — this query returns all statuses by default. Pass `status: 1` to restrict to enabled categories. - **`parent_id` filter** returns only direct children of the specified parent. For the full subtree use `adminCategoryTrees` with `rootId`. --- # Category — Mass Delete URL: /api/graphql-api/admin/catalog/categories/categories-mass-delete --- outline: false examples: - id: admin-catalog-category-mass-delete title: Mass Delete Categories description: Bulk-delete a batch of categories. Whole-batch validation — if any id is non-deletable (root, channel root), no row is touched. Mirrors POST /api/admin/catalog/categories/mass-delete. query: | mutation MassDeleteCategories($input: createAdminCategoryMassDeleteInput!) { createAdminCategoryMassDelete(input: $input) { adminCategoryMassDelete { id deleted message } } } variables: | { "input": { "indices": [12, 18] } } response: | { "data": { "createAdminCategoryMassDelete": { "adminCategoryMassDelete": { "id": "/api/admin/category_mass_deletes/1", "deleted": [12, 18], "message": "Categories deleted successfully." } } } } --- # Category — Mass Delete Bulk-deletes a batch of categories. Equivalent to [`POST /api/admin/catalog/categories/mass-delete`](/api/rest-api/admin/catalog/categories/categories-mass-delete). ## Operation | Operation | Type | Purpose | |-----------|------|---------| | `createAdminCategoryMassDelete` | Mutation | Delete multiple categories at once | ## Input | Field | Type | Notes | |-------|------|-------| | `indices` | `[Int!]!` | Category ids to delete | ## Notes - **All-or-nothing.** Any non-deletable id (root, channel root) rejects the entire batch with `errors[]` carrying `Root and channel-root categories cannot be deleted.` - Unknown ids are silently skipped — they do not appear in `deleted`. --- # Category — Mass Update Status URL: /api/graphql-api/admin/catalog/categories/categories-mass-update-status --- outline: false examples: - id: admin-catalog-category-mass-update-status title: Mass Update Category Status description: Bulk-flip status (enabled/disabled) on a batch of categories. Mirrors POST /api/admin/catalog/categories/mass-update-status. query: | mutation MassUpdateCategoryStatus($input: createAdminCategoryMassUpdateStatusInput!) { createAdminCategoryMassUpdateStatus(input: $input) { adminCategoryMassUpdateStatus { id updated message } } } variables: | { "input": { "indices": [12, 18], "value": 1 } } response: | { "data": { "createAdminCategoryMassUpdateStatus": { "adminCategoryMassUpdateStatus": { "id": "/api/admin/category_mass_update_statuses/1", "updated": [12, 18], "message": "Categories status updated successfully." } } } } --- # Category — Mass Update Status Bulk-flips the status of a batch of categories. Equivalent to [`POST /api/admin/catalog/categories/mass-update-status`](/api/rest-api/admin/catalog/categories/categories-mass-update-status). ## Operation | Operation | Type | Purpose | |-----------|------|---------| | `createAdminCategoryMassUpdateStatus` | Mutation | Enable or disable a batch of categories | ## Input | Field | Type | Notes | |-------|------|-------| | `indices` | `[Int!]!` | Category ids to update | | `value` | `Int!` | `0` to disable, `1` to enable | --- # Catalog Categories — Tree (Nested) (GraphQL) URL: /api/graphql-api/admin/catalog/categories/categories-tree --- outline: false examples: - id: admin-catalog-categories-tree title: Category Tree (Nested) description: Cursor-paginated GraphQL query that returns root category nodes, each carrying its full nested subtree under `children`. Supports locale, status, and rootId filters. query: | query AdminCatalogCategoryTrees( $first: Int $after: String $locale: String $status: Int $rootId: Int ) { adminCategoryTrees( first: $first after: $after locale: $locale status: $status rootId: $rootId ) { edges { cursor node { id _id name slug status position parentId displayMode children } } pageInfo { hasNextPage hasPreviousPage endCursor startCursor } totalCount } } variables: | { "first": 10, "locale": "en", "status": 1 } response: | { "data": { "adminCategoryTrees": { "edges": [ { "cursor": "MA==", "node": { "id": "/api/admin/admin_category_trees/1", "_id": 1, "name": "Root Category", "slug": "root", "status": 1, "position": 0, "parentId": null, "displayMode": null, "children": [ { "id": 2, "name": "Apparel", "slug": "apparel", "status": 1, "position": 1, "parentId": 1, "displayMode": null, "children": [] }, { "id": 5, "name": "Electronics", "slug": "electronics", "status": 1, "position": 2, "parentId": 1, "displayMode": null, "children": [] } ] } } ], "pageInfo": { "hasNextPage": false, "hasPreviousPage": false, "endCursor": "MA==", "startCursor": "MA==" }, "totalCount": 1 } } } --- # Catalog Categories — Tree (Nested) (GraphQL) Cursor-paginated GraphQL query that returns the full nested category hierarchy. Each edge node represents a root-level category and carries its complete subtree under `children`. Leaf nodes have `children: []`. ::: tip Distinct from the flat listing query `adminCategoryTrees` (this query) returns the **nested hierarchy** — ideal for tree-picker UIs and navigation menus. `adminCategories` returns a **flat, cursor-paginated list** — ideal for datagrid/table views with filtering and sorting. ::: ## Operation | Operation | Type | Pagination | |-----------|------|------------| | `adminCategoryTrees` | Query | Cursor (`first` / `after`) | ## Authentication Every request must include an admin Bearer token: ``` Authorization: Bearer ``` Obtain a token via the [`createAdminLogin`](/api/graphql-api/admin/authentication) mutation. ## Arguments | Argument | Type | Description | Example | |----------|------|-------------|---------| | `first` | `Int` | Number of root nodes per page (default `10`, max `50`) | `10` | | `after` | `String` | Cursor from a previous `pageInfo.endCursor` for keyset pagination | `"MA=="` | | `locale` | `String` | Locale code for name/slug resolution (default: app locale) | `"en"` | | `status` | `Int` | Filter by status: `0` = disabled, `1` = enabled. Ancestor nodes are preserved when they have qualifying descendants. | `1` | | `rootId` | `Int` | Limit the tree to descendants of this category ID (inclusive). Returns empty if the ID is unknown. | `1` | ::: tip extraArgs convention API Platform does not automatically expose filter arguments in the GraphQL schema for `QueryCollection` operations. This query uses `extraArgs` to surface `locale`, `status`, and `rootId` as first-class GraphQL arguments alongside `first` and `after`. ::: ## Node Fields Each `edges[].node` object represents a root-level category and contains: | Field | Type | Description | |-------|------|-------------| | `id` | `ID` | API Platform IRI (e.g. `/api/admin/admin_category_trees/1`) | | `_id` | `Int` | Raw category ID | | `name` | `String` | Category name resolved via `locale` | | `slug` | `String` | URL slug | | `status` | `Int` | `1` = enabled, `0` = disabled | | `position` | `Int` | Display order position | | `parentId` | `Int` | Parent category ID; `null` for root nodes | | `displayMode` | `String` | Category display mode | | `children` | scalar (JSON array) | Nested child nodes (recursive plain objects); `[]` for leaf nodes | ### `children` structure `children` is returned as a **plain JSON array** (not a GraphQL connection type). Each element in the array has the same scalar shape as the parent node: `id`, `name`, `slug`, `status`, `position`, `parentId`, `displayMode`, and `children`. This recursive structure continues down to leaf nodes which have `children: []`. ## Example Query ```graphql query AdminCatalogCategoryTrees( $first: Int $after: String $locale: String $status: Int $rootId: Int ) { adminCategoryTrees( first: $first after: $after locale: $locale status: $status rootId: $rootId ) { edges { cursor node { id _id name slug status position parentId displayMode children } } pageInfo { hasNextPage hasPreviousPage endCursor startCursor } totalCount } } ``` ```json { "first": 10, "locale": "en", "status": 1 } ``` ## Example Response ```json { "data": { "adminCategoryTrees": { "edges": [ { "cursor": "MA==", "node": { "id": "/api/admin/admin_category_trees/1", "_id": 1, "name": "Root Category", "slug": "root", "status": 1, "position": 0, "parentId": null, "displayMode": null, "children": [ { "id": 2, "name": "Apparel", "slug": "apparel", "status": 1, "position": 1, "parentId": 1, "displayMode": null, "children": [] }, { "id": 5, "name": "Electronics", "slug": "electronics", "status": 1, "position": 2, "parentId": 1, "displayMode": null, "children": [] } ] } } ], "pageInfo": { "hasNextPage": false, "hasPreviousPage": false, "endCursor": "MA==", "startCursor": "MA==" }, "totalCount": 1 } } } ``` ## Filtering by Root To get the subtree rooted at a specific category, pass `rootId`: ```json { "first": 10, "rootId": 2, "locale": "en" } ``` If the `rootId` does not exist in the database, `totalCount` will be `0` and `edges` will be empty. ## Notes - **`children` is a plain JSON array**, not a GraphQL connection. You traverse it as a standard JSON array without `edges`/`node` wrappers. This is intentional — nested DTOs would cause API Platform to serialize them as IRI strings instead of inline objects. - **`status` filtering preserves ancestor nodes.** When `status: 1` is applied, a disabled parent still appears if it has at least one enabled descendant, so the tree remains navigable. - **Cursor pagination operates on root nodes.** The `first`/`after` arguments page through the top-level nodes; each root node's full subtree is always returned in the same response. - **Same provider as the REST tree endpoint** — `AdminCategoryTreeProvider` serves both transports, so filter semantics are identical between `adminCategoryTrees` and `GET /api/admin/catalog/categories/tree`. - **`translations` and `filterableAttributeIds` are not included** in tree nodes. Use the item query `adminCategory(id: ID!)` when you need the full detail for a specific category. --- # Category — Update (and Move) URL: /api/graphql-api/admin/catalog/categories/categories-update --- outline: false examples: - id: admin-catalog-category-update title: Update (or Move) Category description: Update a category. Move-by-parent_id is handled here — there is NO separate move mutation. Translatable fields are nested under the locale key. Mirrors PUT /api/admin/catalog/categories/{id}. query: | mutation UpdateCategory($input: updateAdminCategoryInput!) { updateAdminCategory(input: $input) { adminCategory { id _id } } } variables: | { "input": { "id": "/api/admin/catalog/categories/7", "locale": "en", "position": 2, "attributes": [11, 23], "parentId": 1, "status": 1, "en": { "slug": "apparel", "name": "Apparel", "description": "Men's and women's apparel" } } } response: | { "data": { "updateAdminCategory": { "adminCategory": { "id": "/api/admin/catalog/categories/7", "_id": 7 } } } } --- # Category — Update (and Move) Updates an existing category. Equivalent to [`PUT /api/admin/catalog/categories/{id}`](/api/rest-api/admin/catalog/categories/categories-update). ::: warning No separate move mutation **Move semantics are part of `updateAdminCategory`.** Re-parenting and re-positioning are done by supplying `parent_id` and `position` on the ordinary update mutation. There is no `moveAdminCategory` — this mirrors the Bagisto admin panel, which has no separate move action either. ::: ## Operation | Operation | Type | Purpose | |-----------|------|---------| | `updateAdminCategory` | Mutation | Update an existing category (also handles moves) | ## Input Top-level fields: `id` (resource IRI), `locale`, `position`, `attributes`, `parent_id`, `status`. Translatable fields (`slug`, `name`, `description`, optional `meta_*`) are nested under a key matching the locale code (e.g. `"en"`). See the [REST page](/api/rest-api/admin/catalog/categories/categories-update) for the full schema. --- # Attribute Families URL: /api/graphql-api/admin/catalog/families --- outline: false --- # Attribute Families An attribute family is the **template of fields a product carries**. Every product belongs to exactly one family, and the family decides which attributes (and in what layout) appear on that product's edit form. The Attribute Families menu lists, creates, edits, and deletes them. It mirrors the admin **Catalog → Attribute Families** screen. ## How a family is structured A family is a set of **attribute groups**, and each group holds a list of **attributes**: - **Groups** (e.g. `General`, `Description`, `Meta Description`, `Price`, `Inventories`, `Images`) organise the edit form into sections. Each group has a `column` (1 or 2) and a `position` (order). - **Attributes** within a group are references to entries from the [Attributes](/api/graphql-api/admin/catalog/attributes/) menu, each with its own `position` and `isRequired` flag. ## Nested data is returned whole On the single-family query, `attributeGroups` (each group with its nested `attributes`) is returned as **whole JSON** — query it as a bare field (`attributeGroups`, not `attributeGroups { … }`); the entire structure comes back, and it resolves over GraphQL. The listing is slim (`id`, `code`, `name` only) — fetch the structure by id. ## Editing the structure Create and update accept the nested `attributeGroups` (with `customAttributes`). On update, groups are matched by id; a group keyed `group_*` is created, and an existing group id omitted from the payload is removed (and so are omitted attributes within a kept group). This mirrors the admin form's add/remove behaviour. ## Delete guards A family **cannot be deleted** if it is the last remaining family, or if any product is still assigned to it — both return an error. Reassign or remove those products first. ## Operations in this menu | Action | Operation | |--------|-----------| | [List families](/api/graphql-api/admin/catalog/families/families-listing) | `adminAttributeFamilies` query | | [Family detail](/api/graphql-api/admin/catalog/families/families-detail) | `adminAttributeFamily(id:)` query | | [Create family](/api/graphql-api/admin/catalog/families/families-create) | `createAdminAttributeFamily` mutation | | [Update family](/api/graphql-api/admin/catalog/families/families-update) | `updateAdminAttributeFamily` mutation | | [Delete family](/api/graphql-api/admin/catalog/families/families-delete) | `deleteAdminAttributeFamily` mutation | All Attribute Families operations require an admin Bearer token — see [Authentication](/api/graphql-api/admin/authentication). Reads require `catalog.families.view`; writes require the matching `catalog.families.create` / `.edit` / `.delete` permission. --- # Attribute Family — Create URL: /api/graphql-api/admin/catalog/families/families-create --- outline: false examples: - id: admin-catalog-family-create title: Create Attribute Family description: Create an attribute family with optional nested groups and per-group custom_attributes. Mirrors POST /api/admin/catalog/families. query: | mutation CreateFamily($input: createAdminAttributeFamilyInput!) { createAdminAttributeFamily(input: $input) { adminAttributeFamily { id _id } } } variables: | { "input": { "code": "electronics", "name": "Electronics", "attributeGroups": [ { "code": "general", "name": "General", "column": 1, "position": 1, "customAttributes": [ { "id": 1 }, { "id": 2 } ] } ] } } response: | { "data": { "createAdminAttributeFamily": { "adminAttributeFamily": { "id": "/api/admin/catalog/families/4", "_id": 4 } } } } --- # Attribute Family — Create Creates a new attribute family. Equivalent to [`POST /api/admin/catalog/families`](/api/rest-api/admin/catalog/families/families-create). ## Operation | Operation | Type | Purpose | |-----------|------|---------| | `createAdminAttributeFamily` | Mutation | Create a new attribute family | ## Input | Field | Notes | |-------|-------| | `code` | Snake_case identifier, unique. | | `name` | Display name. | | `attribute_groups` | Optional array of `{ code, name, column, position, custom_attributes }`. | See the [REST page](/api/rest-api/admin/catalog/families/families-create) for full field semantics. ## Notes - Follow up with the [`adminAttributeFamily`](/api/graphql-api/admin/catalog/families/families-detail) query to load the refreshed detail. - Same `code` validation rules as the REST endpoint. --- # Attribute Family — Delete URL: /api/graphql-api/admin/catalog/families/families-delete --- outline: false examples: - id: admin-catalog-family-delete title: Delete Attribute Family description: Refused if the family is the last one in the store or if any product is using it. Mirrors DELETE /api/admin/catalog/families/{id}. query: | mutation DeleteFamily($input: deleteAdminAttributeFamilyInput!) { deleteAdminAttributeFamily(input: $input) { adminAttributeFamily { id } } } variables: | { "input": { "id": "/api/admin/catalog/families/4" } } response: | { "data": { "deleteAdminAttributeFamily": { "adminAttributeFamily": { "id": "/api/admin/catalog/families/4" } } } } --- # Attribute Family — Delete Deletes an attribute family. Equivalent to [`DELETE /api/admin/catalog/families/{id}`](/api/rest-api/admin/catalog/families/families-delete). ::: tip Prerequisites The example uses an illustrative `id` value. Replace it with the id of a attribute family that exists in your store — use the [`adminAttributeFamilies`](./families-listing.md) query to discover valid ids. ::: ## Operation | Operation | Type | Purpose | |-----------|------|---------| | `deleteAdminAttributeFamily` | Mutation | Delete an attribute family | ## Errors | Condition | Message | |-----------|---------| | Family is the last one in the store | `At least one attribute family is required.` | | One or more products still use the family | `Cannot delete — attribute family is in use by N product(s).` | | Unknown id | `Attribute family not found.` | --- # Catalog Attribute Family — Detail (GraphQL) URL: /api/graphql-api/admin/catalog/families/families-detail --- outline: false examples: - id: admin-catalog-family-detail title: Attribute Family Detail (with attribute groups and attributes) description: Fetch a single attribute family by IRI including all attribute groups and — within each group — all associated attributes with pivot position. The attributeGroups field is returned as whole JSON — query it as a bare field; the entire structure resolves over GraphQL. query: | query AdminAttributeFamily($id: ID!) { adminAttributeFamily(id: $id) { id _id code name attributeGroups } } variables: | { "id": "/api/admin/catalog/families/1" } response: | { "data": { "adminAttributeFamily": { "id": "/api/admin/catalog/families/1", "_id": 1, "code": "default", "name": "Default", "attributeGroups": [ { "id": 1, "code": "general", "name": "General", "column": 1, "position": 1, "attributes": [ { "id": 1, "code": "sku", "type": "text", "isRequired": 1, "column": 1, "position": 1 }, { "id": 2, "code": "name", "type": "text", "isRequired": 1, "column": 1, "position": 2 } ] } ] } } } --- # Catalog Attribute Family — Detail (GraphQL) GraphQL item query that returns a single attribute family by its IRI, including all **attribute groups** and — within each group — all **attributes** associated via the `attribute_group_mappings` pivot (with their pivot `position` and `column`). This is the query to call when an admin needs the complete structure of an attribute family — e.g. when pre-populating the edit form in **Catalog → Attribute Families**. ## Operation | Operation | Type | |-----------|------| | `adminAttributeFamily` | Query (item) | ## Authentication Every request must include an admin Bearer token: ``` Authorization: Bearer ``` Obtain a token via the [`createAdminLogin`](/api/graphql-api/admin/authentication) mutation. ## Arguments | Argument | Type | Required | Description | |----------|------|----------|-------------| | `id` | `ID!` | Yes | API Platform IRI of the attribute family (e.g. `"/api/admin/catalog/families/1"`) | ::: tip Finding the IRI The IRI can be taken directly from `id` in any `adminAttributeFamilies` edge node, or constructed as `/api/admin/catalog/families/{numericId}`. Both forms are accepted by the resolver. ::: ## Fields | Field | Type | Description | |-------|------|-------------| | `id` | `ID` | API Platform IRI (e.g. `/api/admin/catalog/families/1`) | | `_id` | `Int` | Raw family ID | | `code` | `String` | Family code (e.g. `default`, `apparel`) | | `name` | `String` | Family display name (e.g. `Default`, `Apparel`) | | `attributeGroups` | scalar (JSON array\|null) | All attribute groups with nested attributes — see shape below | ### `attributeGroups` item shape `attributeGroups` is returned as a **plain JSON array** (scalar in GraphQL). Each element corresponds to one row in `attribute_groups`: | Key | Type | Description | |-----|------|-------------| | `id` | integer | Attribute group ID | | `code` | string | Group code (e.g. `general`, `price`) | | `name` | string | Group display name | | `column` | integer | Layout column position for the group | | `position` | integer | Display order position of the group within the family | | `attributes` | array | Attributes mapped to this group (see below) | ### `attributeGroups[].attributes[]` item shape Each element corresponds to one attribute mapped via `attribute_group_mappings`: | Key | Type | Description | |-----|------|-------------| | `id` | integer | Attribute ID | | `code` | string | Attribute code (e.g. `sku`, `name`, `color`) | | `type` | string | Attribute type (e.g. `text`, `select`, `boolean`) | | `isRequired` | integer | `1` = required on product forms, `0` = optional | | `column` | integer | Layout column position of this attribute within the group | | `position` | integer | Display order position of this attribute within the group | ::: warning attributeGroups is returned whole `attributeGroups` (each group with its nested `attributes`) is returned as **whole JSON** — query it as a bare field (`attributeGroups`, not `attributeGroups { … }`). The entire structure comes back, and it resolves over GraphQL. ::: ## Example Query ```graphql query AdminAttributeFamily($id: ID!) { adminAttributeFamily(id: $id) { id _id code name attributeGroups } } ``` ```json { "id": "/api/admin/catalog/families/1" } ``` ## Example Response ```json { "data": { "adminAttributeFamily": { "id": "/api/admin/catalog/families/1", "_id": 1, "code": "default", "name": "Default", "attributeGroups": [ { "id": 1, "code": "general", "name": "General", "column": 1, "position": 1, "attributes": [ { "id": 1, "code": "sku", "type": "text", "isRequired": 1, "column": 1, "position": 1 }, { "id": 2, "code": "name", "type": "text", "isRequired": 1, "column": 1, "position": 2 } ] } ] } } } ``` ## Errors | Scenario | GraphQL `errors[]` | HTTP Status | |----------|-------------------|-------------| | Unknown ID | `errors[]` populated or `data.adminAttributeFamily: null` | `200` (GraphQL convention) | | Missing auth | `"Unauthenticated"` in `errors[]` | `200` | ## Notes - **`attributeGroups` is a plain JSON scalar**, not a typed GraphQL object list. You access it as a regular JSON array in the response. This avoids API Platform serializing nested objects as IRI strings instead of inline objects — a known behavior when using nested DTO types in API Platform GraphQL. - **`attributeGroups` is returned whole** — query it as a bare field (not with a sub-selection); the entire structure resolves over GraphQL. The REST detail endpoint returns the same array. - **GraphQL and REST return identical data** for this query — both embed every group with its nested attributes including the `column` and `position` fields. - **The `id` argument is the IRI**, not the numeric integer. Construct it as `"/api/admin/catalog/families/{_id}"` using the `_id` field from a listing query, or pass the `id` field directly from a listing result. - **No timestamps.** The `attribute_families` table has `$timestamps = false`, so `createdAt` and `updatedAt` are not available on either transport. - **Attribute detail fields are slim.** Each attribute inside `attributeGroups[].attributes` carries only the fields needed for family-structure display: `id`, `code`, `type`, `isRequired`, `column`, `position`. For the full attribute payload (translations, options, validation), use `adminAttribute(id: ID!)` or `GET /api/admin/catalog/attributes/{id}`. --- # Catalog Attribute Families — Datagrid Listing (GraphQL) URL: /api/graphql-api/admin/catalog/families/families-listing --- outline: false examples: - id: admin-catalog-families-list title: List Attribute Families (Datagrid) description: Cursor-paginated query returning the flat attribute family list — the GraphQL equivalent of the admin Catalog → Attribute Families datagrid. Supports filtering by code and name. Returns only 3 fields per node (id, code, name) because the underlying table carries no timestamps. query: | query AdminAttributeFamilies( $first: Int $after: String $code: String $name: String $sort: String $order: String ) { adminAttributeFamilies( first: $first after: $after code: $code name: $name sort: $sort order: $order ) { edges { cursor node { id _id code name } } pageInfo { hasNextPage hasPreviousPage endCursor startCursor } totalCount } } variables: | { "first": 10, "sort": "id", "order": "desc" } response: | { "data": { "adminAttributeFamilies": { "edges": [ { "cursor": "MA==", "node": { "id": "/api/admin/catalog/families/3", "_id": 3, "code": "apparel", "name": "Apparel" } }, { "cursor": "MQ==", "node": { "id": "/api/admin/catalog/families/1", "_id": 1, "code": "default", "name": "Default" } } ], "pageInfo": { "hasNextPage": false, "hasPreviousPage": false, "endCursor": "MQ==", "startCursor": "MA==" }, "totalCount": 2 } } } --- # Catalog Attribute Families — Datagrid Listing (GraphQL) Cursor-paginated GraphQL query that mirrors the Bagisto admin **Catalog → Attribute Families** datagrid. Returns the flat family list with filtering, sorting, and cursor pagination. ::: tip How this menu works For how a family's attribute groups + attributes are structured and the delete guards, see the [Attribute Families overview](/api/graphql-api/admin/catalog/families/). ::: ## Operation | Operation | Type | Pagination | |-----------|------|------------| | `adminAttributeFamilies` | Query | Cursor (`first` / `after`) | ::: tip Operation name API Platform derives the GraphQL operation name from the resource `shortName`. `AdminAttributeFamily` has `shortName: 'AdminAttributeFamily'`, so the collection query is `adminAttributeFamilies` and the item query is `adminAttributeFamily(id: ID!)`. ::: ## Authentication Every request must include an admin Bearer token: ``` Authorization: Bearer ``` Obtain a token via the [`createAdminLogin`](/api/graphql-api/admin/authentication) mutation. ## Arguments | Argument | Type | Description | Example | |----------|------|-------------|---------| | `first` | `Int` | Number of items per page (default `10`, max `50`) | `10` | | `after` | `String` | Cursor from a previous `pageInfo.endCursor` for keyset pagination | `"MA=="` | | `id` | `String` | Filter by family ID — single integer or comma-separated list (e.g. `"1"` or `"1,2"`) | `"1"` | | `code` | `String` | Partial family code match (SQL `LIKE %value%`) | `"default"` | | `name` | `String` | Partial family name match (SQL `LIKE %value%`) | `"Apparel"` | | `sort` | `String` | Column to sort by (see Sorting section below) | `"id"` | | `order` | `String` | Sort direction: `"asc"` or `"desc"` (default `"desc"`) | `"desc"` | ::: tip extraArgs convention API Platform does not automatically expose filter arguments in the GraphQL schema for `QueryCollection` operations. This query uses `extraArgs` declared on the `QueryCollection` operation to surface all filter/sort arguments as first-class GraphQL arguments. Pass them directly alongside `first` and `after`. ::: ## Node Fields Each `edges[].node` object contains: | Field | Type | Description | |-------|------|-------------| | `id` | `ID` | API Platform IRI (e.g. `/api/admin/catalog/families/1`) | | `_id` | `Int` | Raw family ID | | `code` | `String` | Family code (e.g. `default`, `apparel`) | | `name` | `String` | Family display name (e.g. `Default`, `Apparel`) | ::: warning No timestamps or attribute groups in the listing The listing returns only 3 data fields. The `attribute_families` table has no timestamps (`$timestamps = false`), so `createdAt` / `updatedAt` do not exist. Attribute groups and nested attributes are only available via the item query `adminAttributeFamily(id: ID!)`. ::: ## Example Query ```graphql query AdminAttributeFamilies( $first: Int $after: String $code: String $name: String $sort: String $order: String ) { adminAttributeFamilies( first: $first after: $after code: $code name: $name sort: $sort order: $order ) { edges { cursor node { id _id code name } } pageInfo { hasNextPage hasPreviousPage endCursor startCursor } totalCount } } ``` ```json { "first": 10, "sort": "id", "order": "desc" } ``` ## Example Response ```json { "data": { "adminAttributeFamilies": { "edges": [ { "cursor": "MA==", "node": { "id": "/api/admin/catalog/families/3", "_id": 3, "code": "apparel", "name": "Apparel" } }, { "cursor": "MQ==", "node": { "id": "/api/admin/catalog/families/1", "_id": 1, "code": "default", "name": "Default" } } ], "pageInfo": { "hasNextPage": false, "hasPreviousPage": false, "endCursor": "MQ==", "startCursor": "MA==" }, "totalCount": 2 } } } ``` ## Sorting Pass `sort` with the column name and `order` for direction. **Sortable columns:** | `sort` value | Sorts by | |---|---| | `id` | Family ID (default) | | `code` | Family code | | `name` | Family display name | ## Cursor Pagination - `first` controls the page size (default `10`, max `50`). - To fetch the next page, pass the `pageInfo.endCursor` value as `after` in the next request. - `totalCount` reflects the full count of matching families across all pages. ## Notes - **Same provider as the REST endpoint** — `AdminAttributeFamilyCollectionProvider` serves both transports with identical filter and sort semantics. - **Only 3 node fields are available.** The listing is intentionally slim — `id`, `_id`, `code`, `name`. No timestamps exist on the table and attribute groups are not embedded in the listing. - **`extraArgs` declares custom arguments.** The filter/sort arguments are not part of the standard API Platform GraphQL schema for `QueryCollection` — they are explicitly declared via `extraArgs` on the operation. Pass them at the top level of the query, alongside `first` and `after`. - **`attributeGroups` is not a field in this query.** It is only populated by the item query `adminAttributeFamily(id: ID!)`. Requesting `attributeGroups` in the collection query will cause a GraphQL schema error. --- # Attribute Family — Update URL: /api/graphql-api/admin/catalog/families/families-update --- outline: false examples: - id: admin-catalog-family-update title: Update Attribute Family description: Update a family, optionally restructuring its attribute groups. Mirrors PUT /api/admin/catalog/families/{id}. query: | mutation UpdateFamily($input: updateAdminAttributeFamilyInput!) { updateAdminAttributeFamily(input: $input) { adminAttributeFamily { id _id } } } variables: | { "input": { "id": "/api/admin/catalog/families/4", "code": "electronics", "name": "Electronics (updated)", "attributeGroups": { "11": { "code": "general", "name": "General", "column": 1, "position": 1, "customAttributes": [ { "id": 1, "position": 1 } ] }, "group_new_1": { "code": "pricing", "name": "Pricing", "column": 2, "position": 2, "customAttributes": [ { "id": 11, "position": 1 } ] } } } } response: | { "data": { "updateAdminAttributeFamily": { "adminAttributeFamily": { "id": "/api/admin/catalog/families/4", "_id": 4 } } } } --- # Attribute Family — Update Updates an existing attribute family. Equivalent to [`PUT /api/admin/catalog/families/{id}`](/api/rest-api/admin/catalog/families/families-update). ## Operation | Operation | Type | Purpose | |-----------|------|---------| | `updateAdminAttributeFamily` | Mutation | Update an existing attribute family | ## `attribute_groups` semantics The `attribute_groups` field is an **object** keyed by numeric group ids (to update existing groups) or `group_*` placeholders (to create new groups). Existing ids that are omitted from the payload are deleted. Each value carries `code`, `name`, `column`, `position`, and `custom_attributes` as documented on the [REST page](/api/rest-api/admin/catalog/families/families-update). --- # Products URL: /api/graphql-api/admin/catalog/products --- outline: false examples: - id: admin-catalog-products-list title: List Products (datagrid) description: The full Catalog → Products datagrid. Cursor pagination via first / after. All scalar fields (including special-price) resolve over GraphQL; relation blocks are detail-only (query them on the single-product query). query: | query AdminCatalogProducts($first: Int, $after: String, $status: Int, $sort: String, $order: String) { adminCatalogProducts(first: $first, after: $after, status: $status, sort: $sort, order: $order) { edges { cursor node { id _id sku name type status price formattedPrice specialPrice formattedSpecialPrice specialPriceFrom specialPriceTo quantity baseImageUrl categoryName channel locale attributeFamilyId attributeFamilyName urlKey visibleIndividually featured new createdAt updatedAt } } pageInfo { hasNextPage endCursor } totalCount } } variables: | { "first": 1, "status": 1, "sort": "id", "order": "asc" } response: | { "data": { "adminCatalogProducts": { "edges": [ { "cursor": "MA==", "node": { "id": "/api/admin/catalog/products/1", "_id": 1, "sku": "COASTALBREEZEMENSHOODIE", "name": "Coastal Breeze Men's Blue Zipper Hoodie", "type": "simple", "status": 1, "price": "100.0000", "formattedPrice": "$100.00", "specialPrice": null, "formattedSpecialPrice": null, "specialPriceFrom": null, "specialPriceTo": null, "quantity": 10000, "baseImageUrl": "http://localhost:8000/storage/product/1/zKcWZTLDjcawJmaNg8g1cpARqwVONgEKEflabstT.webp", "categoryName": "Fashion", "channel": "default", "locale": "en", "attributeFamilyId": 1, "attributeFamilyName": "Default", "urlKey": "coastal-breeze-mens-blue-zipper-hoodie", "visibleIndividually": true, "featured": true, "new": true, "createdAt": "2024-04-16 17:32:38", "updatedAt": "2026-04-07 15:20:30" } } ], "pageInfo": { "hasNextPage": true, "endCursor": "MA==" }, "totalCount": 302 } } } - id: admin-catalog-products-list-filtered title: List Products (AND-combined filters) description: Filters are AND-combined — more arguments narrow the result. Here only active simple products of attribute family 1 within a price band are returned. GraphQL can also pull the relation blocks in the same single round trip on the detail query. query: | query AdminCatalogProducts($first: Int, $status: Int, $type: String, $attribute_family: Int, $price_from: Float, $price_to: Float, $sort: String, $order: String) { adminCatalogProducts(first: $first, status: $status, type: $type, attribute_family: $attribute_family, price_from: $price_from, price_to: $price_to, sort: $sort, order: $order) { edges { node { id _id sku name type status price formattedPrice specialPrice attributeFamilyName } } pageInfo { hasNextPage endCursor } totalCount } } variables: | { "first": 10, "status": 1, "type": "simple", "attribute_family": 1, "price_from": 50, "price_to": 200, "sort": "price", "order": "asc" } response: | { "data": { "adminCatalogProducts": { "edges": [ { "node": { "id": "/api/admin/catalog/products/1", "_id": 1, "sku": "COASTALBREEZEMENSHOODIE", "name": "Coastal Breeze Men's Blue Zipper Hoodie", "type": "simple", "status": 1, "price": "100.0000", "formattedPrice": "$100.00", "specialPrice": null, "attributeFamilyName": "Default" } } ], "pageInfo": { "hasNextPage": false, "endCursor": "MA==" }, "totalCount": 1 } } } --- # Products The Products menu is the catalog's product-management surface — list, search, create, edit, copy, and delete products, plus manage each product's images, per-source inventory, and customer-group prices. It mirrors the admin **Catalog → Products** screen. ## Product types Every product has a `type`, fixed at creation. There are seven: | Type | Notes | |------|-------| | `simple` | A standalone product with its own price and stock. | | `virtual` | Like simple but non-shippable (no weight/dimensions) — services, memberships. | | `downloadable` | Sells downloadable **links** (paid files) and **samples** (free previews); non-stockable. | | `grouped` | A storefront grouping of other simple products (**linked products**); has no own price. | | `bundle` | A configurable kit built from **bundle options**; its price is calculated from the chosen items. | | `configurable` | A parent with **variants** generated from variant-defining attributes (`super_attributes`, e.g. colour × size). Each variant is its own SKU with its own price/stock. | | `booking` | A bookable product (default / appointment / event / rental / table sub-types) with time slots; non-stockable. | **Composite types own no price or stock of their own.** For `configurable`, `bundle`, `grouped`, and `booking`, the price and inventory live on the children — the variants, bundle items, linked products, or slots. Their top-level `price` / `quantity` are derived or empty. ## Creating a product is two steps Creation is deliberately minimal: `createAdminCatalogProduct` creates the shell from just `sku` + `attributeFamilyId` + `type` (plus `superAttributes` for `configurable`). Everything else — name, description, price, images, categories, channels, inventory — is filled in afterwards via `updateAdminCatalogProduct`. This mirrors the admin's create-then-edit wizard. ## `status` vs `visibleIndividually` Two independent flags control storefront presence: - **`status`** — `1` enabled / `0` disabled. A disabled product is fully hidden from the storefront. - **`visibleIndividually`** — whether the product appears in category/search listings. Variant products and grouped-component products are usually set to `0` (reachable only through their parent), while still being `status = 1`. Both must effectively be on for a product to be browsable on its own. ## Nested data is returned whole On the single-product query, nested blocks (`translations`, `images`, `categories`, `inventories`, `customerGroupPrices`, and the type-specific blocks like `variants` / `bundleOptions` / `downloadableLinks`) are returned as **whole JSON** — query each as a bare field (`translations`, not `translations { … }`); the entire array comes back. They resolve over GraphQL on the detail query. ## Per-product sub-resources The product edit screen's tabs map to their own operations, each scoped to one product: - **Images** — upload (REST only — binary), reorder, and delete a product's images. - **Inventories** — read and bulk-update the per-inventory-source stock quantities. - **Customer-group prices** — tiered prices that apply to specific customer groups. These are not returned in full on the listing (they're detail-only); the single-product query embeds them inline. ## The product listing `adminCatalogProducts` is the datagrid query — cursor pagination via `first` / `after`. Every scalar field (including the special-price columns) **resolves over GraphQL**. ### Listing arguments (filters are AND-combined) All filter arguments are **combined with AND** — every one you add **narrows** the result set: | Arg | Type | Description | |-----|------|-------------| | `first` / `after` | cursor pagination | Page size (default `10`, max `50`) + cursor from a previous `pageInfo.endCursor`. | | `channel` | `String` | Channel code used to resolve per-channel values. | | `name` | `String` | Partial product-name match. | | `sku` | `String` | Partial SKU match. | | `attribute_family` | `Int` | Attribute-family ID. | | `price_from` / `price_to` | `Float` | Price band (inclusive). | | `product_id` | `String` | A single ID, or a comma-separated list (e.g. `"1,22,2705"`). | | `status` | `Int` | `0` (disabled) or `1` (active). | | `type` | `String` | One of the seven product types. | | `locale` | `String` | Locale code used to resolve translated values. | | `sort` | `String` | `product_id` (default), `sku`, `name`, `type`, `status`, `price`, `quantity`, `attribute_family`, `channel`. | | `order` | `String` | `asc` or `desc` (default `desc`). | ### Listing node fields Each `node` carries these scalar columns. **Heavy fields are `null` on the listing** (query them on the [detail query](/api/graphql-api/admin/catalog/products/products-detail)): | Field | Type | Notes | |-------|------|-------| | `id` | `ID` | IRI (`/api/admin/catalog/products/{id}`). | | `_id` | `Int` | Numeric product ID. | | `sku` | `String` | SKU. | | `name` | `String` | Resolved for the active locale/channel. `null` for draft products. | | `type` | `String` | Product type. | | `status` | `Int` | `1` active / `0` disabled. | | `price` / `formattedPrice` | `String` | Base price (composite types carry no own price → `null`). | | `specialPrice` / `formattedSpecialPrice` | `String` | Discounted price, when set. | | `specialPriceFrom` / `specialPriceTo` | `String` | Special-price window (`null` = always on / no end). | | `quantity` | `Int` | Total stock across inventory sources. | | `baseImageUrl` | `String` | Medium-cache base image URL. | | `imagesCount` | `Int` | Number of images. | | `categoryId` / `categoryName` | `Int` / `String` | Primary category. | | `channel` / `locale` | `String` | Resolved channel / locale. | | `attributeFamilyId` / `attributeFamilyName` | `Int` / `String` | Attribute family. | | `urlKey` | `String` | Storefront URL slug. | | `visibleIndividually` / `featured` / `new` | `Boolean` | Storefront flags. | | `shortDescription` / `description` / `metaTitle` / `metaDescription` / `metaKeywords` | `String` | Resolved content / SEO. | | `weight` | `Float` | Product weight. | | `createdAt` / `updatedAt` | `String` | Timestamps. | | `taxCategoryId` / `manageStock` / `inStock` | — | **Detail-only** — `null` on the listing. | | `translations`, `images`, `categories`, `inventories`, `customerGroupPrices`, `superAttributes`, `variants`, `bundleOptions`, `linkedProducts`, `downloadableLinks`, `downloadableSamples` | — | Relation blocks — **all `null` on the listing**; populated only on the [detail query](/api/graphql-api/admin/catalog/products/products-detail). | ## Actions | Action | What it does | |--------|--------------| | [Copy](/api/graphql-api/admin/catalog/products/copy) | Duplicates an existing product into a new **draft** product (a fresh SKU is generated). | | [Mass delete](/api/graphql-api/admin/catalog/products/mass-delete) | Deletes several products at once — `indices: [1, 22]`. Missing IDs are skipped. | | [Mass update status](/api/graphql-api/admin/catalog/products/mass-update-status) | Bulk enable/disable — `indices: [1, 22], value: 0` (`0` = disable, `1` = active). | | Export (CSV) | The datagrid "Export" button is **REST only** (binary file streams aren't expressible over GraphQL) — see [Export Products](/api/rest-api/admin/catalog/products/export). | ## Operations in this menu | Action | Operation | |--------|-----------| | [List products](/api/graphql-api/admin/catalog/products) | `adminCatalogProducts` query | | [Product detail](/api/graphql-api/admin/catalog/products/products-detail) | `adminCatalogProduct(id:)` query | | [Create product](/api/graphql-api/admin/catalog/products/create) | `createAdminCatalogProduct` mutation | | [Update product](/api/graphql-api/admin/catalog/products/update) | `updateAdminCatalogProduct` mutation | | [Delete product](/api/graphql-api/admin/catalog/products/delete) | `deleteAdminCatalogProduct` mutation | | [Copy product](/api/graphql-api/admin/catalog/products/copy) | `createAdminCatalogProductCopy` mutation | | [Mass delete](/api/graphql-api/admin/catalog/products/mass-delete) | `createAdminCatalogProductMassDelete` mutation | | [Mass update status](/api/graphql-api/admin/catalog/products/mass-update-status) | `createAdminCatalogProductMassUpdateStatus` mutation | | [Upload images](/api/graphql-api/admin/catalog/products/images-upload) | REST only (binary) | | [Reorder images](/api/graphql-api/admin/catalog/products/images-reorder) | `reorderAdminCatalogProductImage` mutation | | [Delete image](/api/graphql-api/admin/catalog/products/images-delete) | `deleteAdminCatalogProductImage` mutation | | [List inventories](/api/graphql-api/admin/catalog/products/inventories-list) | `adminCatalogProductInventories` query | | [Update inventories](/api/graphql-api/admin/catalog/products/inventories-update) | `updateAdminCatalogProductInventory` mutation | | [List customer-group prices](/api/graphql-api/admin/catalog/products/customer-group-prices-list) | `adminCatalogProductCustomerGroupPrices` query | | [Add customer-group price](/api/graphql-api/admin/catalog/products/customer-group-prices-create) | `createAdminCatalogProductCustomerGroupPrice` mutation | | [Update customer-group price](/api/graphql-api/admin/catalog/products/customer-group-prices-update) | `updateAdminCatalogProductCustomerGroupPrice` mutation | | [Delete customer-group price](/api/graphql-api/admin/catalog/products/customer-group-prices-delete) | `deleteAdminCatalogProductCustomerGroupPrice` mutation | The canonical product listing is [List products](/api/graphql-api/admin/catalog/products) (`adminCatalogProducts` query) above. There is also a separate slim [Add-Product Search](/api/graphql-api/admin/catalog/products/list) (`adminProducts` query) used only by the Create-Order "Add Product" modal — not the product listing. All Products operations require an admin Bearer token — see [Authentication](/api/graphql-api/admin/authentication). Reads require `catalog.products.view`; writes require the matching `catalog.products.create` / `.edit` / `.delete` permission. --- # List Products URL: /api/graphql-api/admin/catalog/products --- outline: false examples: - id: admin-catalog-products-list title: List Products (Datagrid) description: The canonical admin product listing over GraphQL — a cursor-paginated query returning the full catalog product row, mirroring the admin Catalog → Products datagrid. Supports filtering by type, status, SKU, name, price range, attribute family, channel, and locale. query: | query AdminCatalogProducts( $first: Int $after: String $sku: String $type: String $status: Int $priceFrom: Float $priceTo: Float ) { adminCatalogProducts( first: $first after: $after sku: $sku type: $type status: $status price_from: $priceFrom price_to: $priceTo ) { edges { cursor node { id _id sku name type status price formattedPrice specialPrice formattedSpecialPrice specialPriceFrom specialPriceTo quantity baseImageUrl imagesCount categoryId categoryName channel locale attributeFamilyId attributeFamilyName urlKey visibleIndividually shortDescription metaTitle metaDescription metaKeywords weight featured new createdAt updatedAt } } pageInfo { hasNextPage hasPreviousPage endCursor startCursor } totalCount } } variables: | { "first": 10, "sku": "SP-", "type": "simple", "status": 1 } response: | { "data": { "adminCatalogProducts": { "edges": [ { "cursor": "MA==", "node": { "id": "/api/admin/catalog/products/22", "_id": 22, "sku": "bagistoNGRY3424234KJCKJK", "name": "Acme Drawstring Bag", "type": "simple", "status": 1, "price": "3000.0000", "formattedPrice": "$3,000.00", "specialPrice": "2700.0000", "formattedSpecialPrice": "$2,700.00", "specialPriceFrom": null, "specialPriceTo": null, "quantity": 98, "baseImageUrl": "http://localhost:8000/storage/product/22/1qfyoglc5BP46kofrxYrkJ2MXRxu9lAVG3BDFlTZ.webp", "imagesCount": 1, "categoryId": null, "categoryName": null, "channel": "default", "locale": "en", "attributeFamilyId": 1, "attributeFamilyName": "Default", "urlKey": "acme-drawstring-bag", "visibleIndividually": true, "shortDescription": "Many desktop publishing packages and web page editors now use", "metaTitle": "", "metaDescription": "", "metaKeywords": "", "weight": 32, "featured": true, "new": true, "createdAt": "2024-04-19 11:56:43", "updatedAt": "2026-04-23 16:36:14" } } ], "pageInfo": { "hasNextPage": true, "hasPreviousPage": false, "endCursor": "MA==", "startCursor": "MA==" }, "totalCount": 265 } } } --- # List Products The **canonical admin product listing** over GraphQL — a cursor-paginated query that mirrors the Bagisto admin **Catalog → Products** datagrid. Returns the full product row with filtering, sorting, and cursor pagination. This is the query you want for product-management screens. ::: tip How this menu works For the product types, the two-step create flow, status vs. visibleIndividually, and the per-product sub-resources, see the [Products overview](/api/graphql-api/admin/catalog/products/). ::: ::: tip Not the Create-Order search `adminCatalogProducts` (this query) is the full product listing. A separate slim search — [`adminProducts`](/api/graphql-api/admin/catalog/products/list) — powers the admin Create-Order "Add Product" modal only. Use this query for the actual product listing. ::: ## Operation | Operation | Type | Pagination | |-----------|------|------------| | `adminCatalogProducts` | Query | Cursor (`first` / `after`) | ## Authentication Every request must include an admin Bearer token: ``` Authorization: Bearer ``` Obtain a token via the [`createAdminLogin`](/api/graphql-api/admin/authentication) mutation. ## Arguments | Argument | Type | Description | Example | |----------|------|-------------|---------| | `first` | `Int` | Number of items to return per page (default `10`, max `50`) | `10` | | `after` | `String` | Cursor from a previous `pageInfo.endCursor` for keyset pagination | `"MA=="` | | `product_id` | `String` | Filter by product ID — single integer or comma-separated list | `"142"` or `"1,2,3"` | | `sku` | `String` | Partial SKU match (SQL `LIKE %value%`) | `"SP-001"` | | `name` | `String` | Partial product name match (SQL `LIKE %value%`) | `"Classic Watch"` | | `type` | `String` | Filter by product type | `"simple"` | | `status` | `Int` | Filter by status: `0` = disabled, `1` = enabled | `1` | | `attribute_family` | `String` | Filter by attribute family ID | `"1"` | | `channel` | `String` | Channel code for value resolution (default: current channel) | `"default"` | | `locale` | `String` | Locale code for name/category resolution (default: app locale) | `"en"` | | `price_from` | `Float` | Minimum price filter (inclusive) | `10.0` | | `price_to` | `Float` | Maximum price filter (inclusive) | `500.0` | | `sort` | `String` | Column to sort by (see Sorting section below) | `"product_id"` | | `order` | `String` | Sort direction: `"asc"` or `"desc"` (default `"desc"`) | `"desc"` | ### Valid `type` values `"simple"`, `"configurable"`, `"bundle"`, `"grouped"`, `"downloadable"`, `"virtual"`, `"booking"` ## Node Fields Each `edges[].node` object contains the following fields: | Field | Type | Description | |-------|------|-------------| | `id` | `ID` | API Platform IRI (e.g. `/api/admin/catalog/products/142`) | | `_id` | `Int` | Raw product ID | | `sku` | `String` | Product SKU | | `name` | `String` | Product name (resolved via `locale` and `channel`) | | `type` | `String` | Product type | | `status` | `Int` | `1` = enabled, `0` = disabled | | `price` | `String` | Raw price as a decimal string (e.g. `"3000.0000"`) | | `formattedPrice` | `String` | Locale-formatted price (e.g. `"$3,000.00"`) | | `specialPrice` | `String` | Raw special (sale) price as a decimal string; `null` if none | | `formattedSpecialPrice` | `String` | Locale-formatted special price; `null` if none | | `specialPriceFrom` | `String` | Start of the special-price window; `null` unless a dated window is set | | `specialPriceTo` | `String` | End of the special-price window; `null` unless a dated window is set | | `quantity` | `Int` | Sum of inventory qty across all inventory sources | | `baseImageUrl` | `String` | Storage URL of the first product image; `null` if no images | | `imagesCount` | `Int` | Total number of images attached to the product | | `categoryId` | `Int` | ID of the first category this product belongs to; `null` if uncategorized | | `categoryName` | `String` | Translated name of that category; `null` if uncategorized | | `channel` | `String` | Channel code used for resolution | | `locale` | `String` | Locale code used for resolution | | `attributeFamilyId` | `Int` | Attribute family ID | | `attributeFamilyName` | `String` | Attribute family name | | `urlKey` | `String` | URL slug (e.g. `"acme-drawstring-bag"`) | | `visibleIndividually` | `Boolean` | Whether the product appears in category/search listings | | `shortDescription` | `String` | Short description | | `metaTitle` | `String` | SEO meta title (empty string when unset) | | `metaDescription` | `String` | SEO meta description (empty string when unset) | | `metaKeywords` | `String` | SEO meta keywords (empty string when unset) | | `weight` | `Float` | Product weight | | `featured` | `Boolean` | Whether the product is flagged as featured | | `new` | `Boolean` | Whether the product is flagged as "new" | | `createdAt` | `String` | Creation timestamp | | `updatedAt` | `String` | Last-update timestamp | Notes on the listing values: - `price`, `specialPrice` are **decimal strings**, not numbers. - `specialPriceFrom` / `specialPriceTo` are `null` unless a **dated** special-price window is configured. - `quantity` is the **summed** inventory across all sources. The following fields are **detail-only** and always come back `null` on the listing — fetch them from the single-product query `adminCatalogProduct(id:)`: - `taxCategoryId`, `manageStock`, `inStock` - the relation blocks: `translations`, `images`, `categories`, `inventories`, `customerGroupPrices`, `superAttributes`, `variants`, `bundleOptions`, `linkedProducts`, `downloadableLinks`, `downloadableSamples`, `videos`, `channels`, `relatedProducts`, `upSells`, `crossSells`. ## Example Query ```graphql query AdminCatalogProducts( $first: Int $after: String $sku: String $type: String $status: Int $priceFrom: Float $priceTo: Float ) { adminCatalogProducts( first: $first after: $after sku: $sku type: $type status: $status price_from: $priceFrom price_to: $priceTo ) { edges { cursor node { id _id sku name type status price formattedPrice specialPrice formattedSpecialPrice specialPriceFrom specialPriceTo quantity baseImageUrl imagesCount categoryId categoryName channel locale attributeFamilyId attributeFamilyName urlKey visibleIndividually shortDescription metaTitle metaDescription metaKeywords weight featured new createdAt updatedAt } } pageInfo { hasNextPage hasPreviousPage endCursor startCursor } totalCount } } ``` ```json { "first": 10, "sku": "SP-", "type": "simple", "status": 1 } ``` ## Example Response ```json { "data": { "adminCatalogProducts": { "edges": [ { "cursor": "MA==", "node": { "id": "/api/admin/catalog/products/22", "_id": 22, "sku": "bagistoNGRY3424234KJCKJK", "name": "Acme Drawstring Bag", "type": "simple", "status": 1, "price": "3000.0000", "formattedPrice": "$3,000.00", "specialPrice": "2700.0000", "formattedSpecialPrice": "$2,700.00", "specialPriceFrom": null, "specialPriceTo": null, "quantity": 98, "baseImageUrl": "http://localhost:8000/storage/product/22/1qfyoglc5BP46kofrxYrkJ2MXRxu9lAVG3BDFlTZ.webp", "imagesCount": 1, "categoryId": null, "categoryName": null, "channel": "default", "locale": "en", "attributeFamilyId": 1, "attributeFamilyName": "Default", "urlKey": "acme-drawstring-bag", "visibleIndividually": true, "shortDescription": "Many desktop publishing packages and web page editors now use", "metaTitle": "", "metaDescription": "", "metaKeywords": "", "weight": 32, "featured": true, "new": true, "createdAt": "2024-04-19 11:56:43", "updatedAt": "2026-04-23 16:36:14" } } ], "pageInfo": { "hasNextPage": true, "hasPreviousPage": false, "endCursor": "MA==", "startCursor": "MA==" }, "totalCount": 265 } } } ``` ## Sorting Pass `sort` with the column name and `order` for direction. A compound form `sort=name-asc` is also accepted (splits on `-` to extract column and direction). **Sortable columns:** | `sort` value | Sorts by | |---|---| | `product_id` | Product ID (default) | | `name` | Product name | | `sku` | SKU | | `price` | Price | | `quantity` | Inventory quantity (SUM across sources) | | `status` | Enabled/disabled status | | `type` | Product type | | `attribute_family` | Attribute family ID | | `channel` | Channel code | ## Cursor Pagination - `first` controls the page size (default `10`, max `50`). - To fetch the next page, pass the `pageInfo.endCursor` value as `after` in the next request. - `totalCount` reflects the full count of matching products across all pages. ## How It Differs from `adminProducts` | | `adminCatalogProducts` | `adminProducts` | |---|---|---| | Purpose | The canonical product listing (catalog management) | Slim Create-Order "Add Product" search | | Row shape | Full product row | Slim row (9 fields) | | Filters | Full set (price range, attribute family, status, type, etc.) | A handful (query text, sku, type, status, categoryId, channel) | | Default status filter | None — all statuses returned | None — all statuses returned | Use **this** query for the product listing; use [`adminProducts`](/api/graphql-api/admin/catalog/products/list) only for the Create-Order product search. ## Notes - **Elasticsearch is not yet supported.** Even when the Bagisto admin panel is configured to use Elasticsearch for catalog search, this query always uses the database path. - **No automatic status filter** — unlike `GET /api/shop/products` (the storefront product list), this query returns all statuses by default. Pass `status: 1` to restrict to enabled products. - **Multi-category products** — only the first associated category's `categoryId` and `categoryName` are included (matching the datagrid display). --- # Catalog Product — Copy URL: /api/graphql-api/admin/catalog/products/copy --- outline: false examples: - id: admin-catalog-product-copy title: Copy a Catalog Product description: Duplicates an existing product across all sub-resources. Refuses configurable variants. query: | mutation CopyProduct($input: createAdminCatalogProductCopyInput!) { createAdminCatalogProductCopy(input: $input) { adminCatalogProductCopy { id sourceId sku type name success message } } } variables: | { "input": { "sourceId": 12 } } response: | { "data": { "createAdminCatalogProductCopy": { "adminCatalogProductCopy": { "id": "/api/admin/catalog_product_copies/43", "sourceId": 12, "sku": "SKU-001-copy-1", "type": "simple", "name": "Test SKU-001 (Copy)", "success": true, "message": "Product copied successfully." } } } } --- # Catalog Product — Copy Equivalent to [`POST /api/admin/catalog/products/{sourceId}/copy`](/api/rest-api/admin/catalog/products/copy). ::: tip Prerequisites The example uses an illustrative `id` value. Replace it with the id of a product that exists in your store — use the [`adminCatalogProducts`](./list.md) query to discover valid ids. ::: ## Operation | Operation | Type | |-----------|------| | `createAdminCatalogProductCopy` | Mutation | ## Input | Field | Type | Required | Notes | |-------|------|----------|-------| | `sourceId` | `Int!` | yes | ID of the product to duplicate. | ## Notes - Refuses variants (parent_id != null) — surfaces in `errors[]`. - Fires `catalog.product.create.before` / `.after` on the copy. --- # Catalog Product — Create URL: /api/graphql-api/admin/catalog/products/create --- outline: false examples: - id: admin-catalog-product-create-simple title: Create — Simple description: Step-1 create for a simple product. Only sku, attributeFamilyId and type are submitted; everything else is added later via the Update mutation. query: | mutation CreateCatalogProduct($input: createAdminCatalogProductInput!) { createAdminCatalogProduct(input: $input) { adminCatalogProduct { id sku type } } } variables: | { "input": { "sku": "sp-001", "attributeFamilyId": 1, "type": "simple" } } response: | { "data": { "createAdminCatalogProduct": { "adminCatalogProduct": { "id": "/api/admin/catalog/products/43", "sku": "sp-001", "type": "simple" } } } } - id: admin-catalog-product-create-virtual title: Create — Virtual description: Step-1 create for a virtual (non-shippable) product. Same minimal input as a simple product. query: | mutation CreateCatalogProduct($input: createAdminCatalogProductInput!) { createAdminCatalogProduct(input: $input) { adminCatalogProduct { id sku type } } } variables: | { "input": { "sku": "vr-001", "attributeFamilyId": 1, "type": "virtual" } } response: | { "data": { "createAdminCatalogProduct": { "adminCatalogProduct": { "id": "/api/admin/catalog/products/44", "sku": "vr-001", "type": "virtual" } } } } - id: admin-catalog-product-create-downloadable title: Create — Downloadable description: Step-1 create for a downloadable product. The download links and samples are configured later via the Update mutation. query: | mutation CreateCatalogProduct($input: createAdminCatalogProductInput!) { createAdminCatalogProduct(input: $input) { adminCatalogProduct { id sku type } } } variables: | { "input": { "sku": "dl-001", "attributeFamilyId": 1, "type": "downloadable" } } response: | { "data": { "createAdminCatalogProduct": { "adminCatalogProduct": { "id": "/api/admin/catalog/products/45", "sku": "dl-001", "type": "downloadable" } } } } - id: admin-catalog-product-create-grouped title: Create — Grouped description: Step-1 create for a grouped product. The associated/linked products are added later via the Update mutation. query: | mutation CreateCatalogProduct($input: createAdminCatalogProductInput!) { createAdminCatalogProduct(input: $input) { adminCatalogProduct { id sku type } } } variables: | { "input": { "sku": "gr-001", "attributeFamilyId": 1, "type": "grouped" } } response: | { "data": { "createAdminCatalogProduct": { "adminCatalogProduct": { "id": "/api/admin/catalog/products/46", "sku": "gr-001", "type": "grouped" } } } } - id: admin-catalog-product-create-bundle title: Create — Bundle description: Step-1 create for a bundle product. The bundle option groups are configured later via the Update mutation. query: | mutation CreateCatalogProduct($input: createAdminCatalogProductInput!) { createAdminCatalogProduct(input: $input) { adminCatalogProduct { id sku type } } } variables: | { "input": { "sku": "bn-001", "attributeFamilyId": 1, "type": "bundle" } } response: | { "data": { "createAdminCatalogProduct": { "adminCatalogProduct": { "id": "/api/admin/catalog/products/47", "sku": "bn-001", "type": "bundle" } } } } - id: admin-catalog-product-create-configurable title: Create — Configurable description: Step-1 create for a configurable product. superAttributes is REQUIRED — a map of attribute code (or id) to a list of option ids. The store generates the cartesian product of variants from these options. query: | mutation CreateCatalogProduct($input: createAdminCatalogProductInput!) { createAdminCatalogProduct(input: $input) { adminCatalogProduct { id sku type } } } variables: | { "input": { "sku": "cf-001", "attributeFamilyId": 1, "type": "configurable", "superAttributes": { "color": [1, 2], "size": [6, 7] } } } response: | { "data": { "createAdminCatalogProduct": { "adminCatalogProduct": { "id": "/api/admin/catalog/products/48", "sku": "cf-001", "type": "configurable" } } } } - id: admin-catalog-product-create-booking title: Create — Booking description: Step-1 create for a booking product. The booking sub-type (default / appointment / event / rental / table) and its slots/tickets are configured later via the Update mutation. query: | mutation CreateCatalogProduct($input: createAdminCatalogProductInput!) { createAdminCatalogProduct(input: $input) { adminCatalogProduct { id sku type } } } variables: | { "input": { "sku": "bk-001", "attributeFamilyId": 1, "type": "booking" } } response: | { "data": { "createAdminCatalogProduct": { "adminCatalogProduct": { "id": "/api/admin/catalog/products/53", "sku": "bk-001", "type": "booking" } } } } --- # Catalog Product — Create Equivalent to [`POST /api/admin/catalog/products`](/api/rest-api/admin/catalog/products/create). Step-1 create — mirrors the Bagisto admin Create-Product wizard step 1. Only the bare-minimum fields are accepted at this step; everything else (name, description, price, variants, booking slots, etc.) is added through the [Update mutation](/api/graphql-api/admin/catalog/products/update). ## Operation | Operation | Type | |-----------|------| | `createAdminCatalogProduct` | Mutation | ## Input | Field | Type | Required | Notes | |-------|------|----------|-------| | `sku` | `String!` | yes | Must be unique. | | `attributeFamilyId` | `Int!` | yes | Existing family ID. | | `type` | `String!` | yes | One of `simple`, `virtual`, `downloadable`, `grouped`, `bundle`, `configurable`, `booking`. | | `superAttributes` | `Object` | conditional | **Required when `type=configurable`** — map of attribute code (or id) → non-empty list of option ids. e.g. `{ "color": [1, 2], "size": [6, 7] }`. | For every type except `configurable`, the input is just `sku` + `attributeFamilyId` + `type`. Configurable additionally requires `superAttributes`, from which the store generates the cartesian product of variants. ## Notes - Step-1 only: name, description, price, etc. are added via the [Update mutation](/api/graphql-api/admin/catalog/products/update). - Booking sub-type (`default` / `appointment` / `event` / `rental` / `table`) and its slots/tickets are set in step 2. - See [Update](/api/graphql-api/admin/catalog/products/update) for the per-type structure payloads (variants, bundle options, links, booking slots/tickets). --- # Product Customer-Group Prices — Create URL: /api/graphql-api/admin/catalog/products/customer-group-prices-create --- outline: false examples: - id: admin-catalog-product-cgp-create title: Add a Customer-Group Price description: "Adds a new tier-price row to a product. `customer_group_id: null` makes it apply to every group." query: | mutation AddCGP($input: createAdminCatalogProductCustomerGroupPriceInput!, $productId: Int!) { createAdminCatalogProductCustomerGroupPrice(input: $input, productId: $productId) { adminCatalogProductCustomerGroupPrice { id qty valueType value customerGroupId productId } } } variables: | { "productId": 1, "input": { "qty": 10, "valueType": "discount", "value": 15.0, "customerGroupId": 2 } } response: | { "data": { "createAdminCatalogProductCustomerGroupPrice": { "adminCatalogProductCustomerGroupPrice": { "id": "/api/admin/catalog_product_customer_group_prices/12", "qty": 10, "valueType": "discount", "value": 15.0, "customerGroupId": 2, "productId": 1 } } } } --- # Product Customer-Group Prices — Create Equivalent to [`POST /api/admin/catalog/products/{productId}/customer-group-prices`](/api/rest-api/admin/catalog/products/customer-group-prices-create). ## Operation | Operation | Type | |-----------|------| | `createAdminCatalogProductCustomerGroupPrice` | Mutation | ## Input | Field | Type | Required | Notes | |-------|------|----------|-------| | `productId` | `Int!` | yes | Extra arg — parent product. | | `qty` | `Int!` | yes | ≥ 1. | | `value_type` | `String!` | yes | `fixed` or `discount`. | | `value` | `Float!` | yes | | | `customer_group_id` | `Int` | no | `null` = applies to every group. | --- # Product Customer-Group Prices — Delete URL: /api/graphql-api/admin/catalog/products/customer-group-prices-delete --- outline: false examples: - id: admin-catalog-product-cgp-delete title: Delete a Customer-Group Price description: Deletes a single tier-price row. query: | mutation DeleteCGP($input: deleteAdminCatalogProductCustomerGroupPriceInput!) { deleteAdminCatalogProductCustomerGroupPrice(input: $input) { adminCatalogProductCustomerGroupPrice { id success message } } } variables: | { "input": { "id": "/api/admin/catalog/products/1/customer-group-prices/12" } } response: | { "data": { "deleteAdminCatalogProductCustomerGroupPrice": { "adminCatalogProductCustomerGroupPrice": { "id": "/api/admin/catalog_product_customer_group_prices/12" } } } } --- # Product Customer-Group Prices — Delete Equivalent to [`DELETE …/customer-group-prices/{id}`](/api/rest-api/admin/catalog/products/customer-group-prices-delete). ## Operation | Operation | Type | |-----------|------| | `deleteAdminCatalogProductCustomerGroupPrice` | Mutation | --- # Product Customer-Group Prices — List URL: /api/graphql-api/admin/catalog/products/customer-group-prices-list --- outline: false examples: - id: admin-catalog-product-cgp-list title: List Customer-Group (Tier) Prices description: Cursor connection of tier-price rows attached to a product. query: | query CGP($productId: Int!) { adminCatalogProductCustomerGroupPrices(productId: $productId) { id qty valueType value customerGroupId customerGroupName productId } } variables: | { "productId": 1 } response: | { "data": { "adminCatalogProductCustomerGroupPrices": [ { "id": 12, "qty": 5, "valueType": "discount", "value": 10, "customerGroupId": 2, "customerGroupName": "Wholesale", "productId": 1 } ] } } --- # Product Customer-Group Prices — List Equivalent to [`GET /api/admin/catalog/products/{productId}/customer-group-prices`](/api/rest-api/admin/catalog/products/customer-group-prices-list). ## Operation | Operation | Type | |-----------|------| | `adminCatalogProductCustomerGroupPrices(productId: Int!)` | Query | --- # Product Customer-Group Prices — Update URL: /api/graphql-api/admin/catalog/products/customer-group-prices-update --- outline: false examples: - id: admin-catalog-product-cgp-update title: Update a Customer-Group Price description: Partially updates a tier-price row. query: | mutation UpdateCGP($input: updateAdminCatalogProductCustomerGroupPriceInput!, $productId: Int!) { updateAdminCatalogProductCustomerGroupPrice(input: $input, productId: $productId) { adminCatalogProductCustomerGroupPrice { id qty valueType value customerGroupId } } } variables: | { "productId": 1, "input": { "id": "/api/admin/catalog_product_customer_group_prices/12", "qty": 5, "value": 17.5, "valueType": "fixed" } } response: | { "data": { "updateAdminCatalogProductCustomerGroupPrice": { "adminCatalogProductCustomerGroupPrice": { "id": "/api/admin/catalog_product_customer_group_prices/12", "qty": 5, "valueType": "fixed", "value": 17.5, "customerGroupId": null } } } } --- # Product Customer-Group Prices — Update Equivalent to [`PUT …/customer-group-prices/{id}`](/api/rest-api/admin/catalog/products/customer-group-prices-update). ## Operation | Operation | Type | |-----------|------| | `updateAdminCatalogProductCustomerGroupPrice` | Mutation | --- # Catalog Product — Delete URL: /api/graphql-api/admin/catalog/products/delete --- outline: false examples: - id: admin-catalog-product-delete title: Delete a Catalog Product description: GraphQL counterpart of DELETE /api/admin/catalog/products/{id}. For configurable products, variants cascade. query: | mutation DeleteCatalogProduct($input: deleteAdminCatalogProductInput!) { deleteAdminCatalogProduct(input: $input) { adminCatalogProduct { id } } } variables: | { "input": { "id": "/api/admin/catalog_products/42" } } response: | { "data": { "deleteAdminCatalogProduct": { "adminCatalogProduct": { "id": "/api/admin/catalog_products/42" } } } } --- # Catalog Product — Delete Equivalent to [`DELETE /api/admin/catalog/products/{id}`](/api/rest-api/admin/catalog/products/delete). ::: tip Prerequisites The example uses an illustrative `id` value. Replace it with the id of a product that exists in your store — use the [`adminCatalogProducts`](./list.md) query to discover valid ids. ::: ## Operation | Operation | Type | |-----------|------| | `deleteAdminCatalogProduct` | Mutation | ## Input | Field | Type | Required | Notes | |-------|------|----------|-------| | `id` | `ID!` | yes | Resource IRI of the product to delete. | ::: warning No "in-order" guard (parity with monolith) Bagisto admin does not refuse to delete products that appear in non-completed orders — neither does this mutation. ::: --- # Product Images — Delete URL: /api/graphql-api/admin/catalog/products/images-delete --- outline: false examples: - id: admin-catalog-product-image-delete title: Delete a Product Image description: Removes the DB row and the file on storage. query: | mutation DeleteImage($input: deleteAdminCatalogProductImageInput!) { deleteAdminCatalogProductImage(input: $input) { adminCatalogProductImage { id success message } } } variables: | { "input": { "id": "/api/admin/catalog/products/12/images/47" } } response: | { "data": { "deleteAdminCatalogProductImage": { "adminCatalogProductImage": { "id": "/api/admin/catalog_product_images/47", "success": true, "message": "Product image deleted successfully." } } } } --- # Product Images — Delete Equivalent to [`DELETE /api/admin/catalog/products/{productId}/images/{id}`](/api/rest-api/admin/catalog/products/images-delete). ## Operation | Operation | Type | |-----------|------| | `deleteAdminCatalogProductImage` | Mutation | ## Input | Field | Type | Required | Notes | |-------|------|----------|-------| | `productId` | `Int!` | yes | Parent product ID. | | `id` | `ID!` | yes | Image resource IRI. | --- # Product Images — Reorder URL: /api/graphql-api/admin/catalog/products/images-reorder --- outline: false examples: - id: admin-catalog-product-image-reorder title: Reorder Product Images description: Update positions of one or more existing images for a product. Each image ID must belong to the product. query: | mutation ReorderImages($input: reorderAdminCatalogProductImageInput!) { reorderAdminCatalogProductImage(input: $input) { adminCatalogProductImage { id success message } } } variables: | { "input": { "id": "/api/admin/catalog/products/12/images/reorder", "productId": 12, "order": [ {"id": 47, "position": 2}, {"id": 48, "position": 1} ] } } response: | { "data": { "reorderAdminCatalogProductImage": { "adminCatalogProductImage": { "id": "/api/admin/catalog_product_images/0", "success": true, "message": "Product images reordered successfully." } } } } --- # Product Images — Reorder Equivalent to [`PUT /api/admin/catalog/products/{productId}/images/reorder`](/api/rest-api/admin/catalog/products/images-reorder). ## Operation | Operation | Type | |-----------|------| | `reorderAdminCatalogProductImage` | Mutation | ## Input | Field | Type | Required | Notes | |-------|------|----------|-------| | `productId` | `Int!` | yes | Parent product ID. | | `order` | `[{ id: Int!, position: Int! }]!` | yes | Each `id` must belong to the product. | --- # Product Images — Upload URL: /api/graphql-api/admin/catalog/products/images-upload --- outline: false examples: - id: admin-catalog-product-image-upload title: Upload a Product Image (REST only) description: GraphQL upload is NOT supported — binary file parts are not transportable over JSON GraphQL. Use the REST endpoint. query: | # Image upload is REST-only — binary uploads are not transportable # over JSON GraphQL. Use the REST endpoint: # # POST /api/admin/catalog/products/{productId}/images # Content-Type: multipart/form-data # Body: image=; position= # # Example curl: # curl -X POST "https://your-domain.com/api/admin/catalog/products/12/images" \ # -H "Authorization: Bearer " \ # -F "image=@/path/to/photo.webp" variables: | {} response: | --- # Product Images — Upload ::: warning REST only — GraphQL upload not supported Binary uploads are not transportable over JSON GraphQL. The `createAdminCatalogProductImage` mutation exists as a placeholder only and will not accept a file. Use [`POST /api/admin/catalog/products/{productId}/images`](/api/rest-api/admin/catalog/products/images-upload) instead. ::: ## See also - [Reorder Images (GraphQL)](/api/graphql-api/admin/catalog/products/images-reorder) - [Delete Image (GraphQL)](/api/graphql-api/admin/catalog/products/images-delete) --- # Product Inventories — List URL: /api/graphql-api/admin/catalog/products/inventories-list --- outline: false examples: - id: admin-catalog-product-inventories-list title: List Per-Source Inventory Rows description: Cursor connection of per-source inventory rows for a product. query: | query Inventories($productId: Int!) { adminCatalogProductInventories(productId: $productId) { id sourceId sourceCode sourceName qty } } variables: | { "productId": 12 } response: | { "data": { "adminCatalogProductInventories": [ { "id": 14, "sourceId": 1, "sourceCode": "default", "sourceName": "Default", "qty": 25 } ] } } --- # Product Inventories — List Equivalent to [`GET /api/admin/catalog/products/{productId}/inventories`](/api/rest-api/admin/catalog/products/inventories-list). ## Operation | Operation | Type | |-----------|------| | `adminCatalogProductInventories(productId: Int!)` | Query (cursor) | ## Arguments | Arg | Type | Notes | |-----|------|-------| | `productId` | `Int!` | Required. | | `first`, `after` | cursor pagination | | --- # Product Inventories — Bulk Update URL: /api/graphql-api/admin/catalog/products/inventories-update --- outline: false examples: - id: admin-catalog-product-inventories-update title: Bulk-Update Per-Source Inventory description: Sources with qty=0 are kept but zeroed-out; sources NOT in the payload are left untouched. query: | mutation UpdateInventories($input: updateAdminCatalogProductInventoryInput!) { updateAdminCatalogProductInventory(input: $input) { adminCatalogProductInventory { id sourceId qty } } } variables: | { "input": { "id": "/api/admin/catalog/products/12/inventories", "productId": 12, "inventories": { "1": 25, "2": 0 } } } response: | { "data": { "updateAdminCatalogProductInventory": { "adminCatalogProductInventory": { "id": "/api/admin/catalog_product_inventories/14", "sourceId": 1, "qty": 25 } } } } --- # Product Inventories — Bulk Update Equivalent to [`PUT /api/admin/catalog/products/{productId}/inventories`](/api/rest-api/admin/catalog/products/inventories-update). ## Operation | Operation | Type | |-----------|------| | `updateAdminCatalogProductInventory` | Mutation | ## Input | Field | Type | Required | Notes | |-------|------|----------|-------| | `productId` | `Int!` | yes | Parent product. | | `inventories` | `Object!` | yes | Map of `inventory_source_id` → integer quantity (≥ 0). | --- # Add-Product Search (Create-Order) URL: /api/graphql-api/admin/catalog/products/list --- outline: false examples: - id: admin-products-list title: Add-Product Search (Create-Order) description: The slim product search behind the admin Create-Order "Add Product" modal. NOT the product listing — for the full product list with all columns and filters use the List Products datagrid. Returns ALL statuses by default (admin sees disabled / draft products too). Booking products ARE listed but blocked when added to an admin draft cart. query: | query AdminProducts($first: Int, $after: String, $type: String, $sku: String) { adminProducts(first: $first, after: $after, type: $type, sku: $sku) { edges { cursor node { id _id sku type name status price formattedPrice baseImageUrl isSaleable } } pageInfo { hasNextPage endCursor } totalCount } } variables: | { "first": 30, "type": "simple", "sku": "SP-001" } response: | { "data": { "adminProducts": { "edges": [ { "cursor": "MA==", "node": { "id": "/api/admin/admin_products/2512", "_id": 2512, "sku": "SP-001", "type": "simple", "name": "Arctic Cozy Knit Unisex Beanie", "status": 1, "price": 14, "formattedPrice": "$14.00", "baseImageUrl": "http://localhost:8000/cache/medium/product/2512/Muc0qeWks34MTZaxf38s6DBmfqMqrCxku81Uo8EB.webp", "isSaleable": true } } ], "pageInfo": { "hasNextPage": false, "hasPreviousPage": false, "endCursor": "MA==", "startCursor": "MA==" }, "totalCount": 1 } } } --- # Add-Product Search (Create-Order) The slim product **search** that powers the admin **Create Order** screen's "Add Product" modal — the GraphQL counterpart of `GET /api/admin/products`. Cursor pagination via `first` / `after`. ::: warning This is not the product listing For the full admin product listing — every column plus all the Channel / Name / SKU / Attribute Family / Price / ID / Status / Type filters — use [List Products](/api/graphql-api/admin/catalog/products) (the `adminCatalogProducts` query). This page documents only the Create-Order search tool. ::: ## Operation | Operation | Type | |-----------|------| | `adminProducts(first: Int, after: String, query: String, sku: String, type: String, status: Int, categoryId: Int, channel: String, locale: String)` | Query (cursor) | ## Arguments | Arg | Type | Description | |-----|------|-------------| | `first` | `Int` | Page size (default `30`, max `50`) | | `after` | `String` | Cursor from a previous `pageInfo.endCursor` | | `query` | `String` | Free-text — matches SKU OR product name (partial) | | `sku` | `String` | Exact SKU | | `type` | `String` | `simple`, `configurable`, `bundle`, `downloadable`, `grouped`, `virtual`, `booking` | | `status` | `Int` | `0` (disabled) or `1` (enabled) — omit to get both | | `categoryId` | `Int` | Filter by category ID | | `channel` | `String` | Channel code for value resolution | | `locale` | `String` | Locale code for value resolution | ## How it differs from the storefront `products` query - No automatic `status = 1` filter — admin sees all statuses. - Booking products are listed (but cannot be added to an admin draft cart — see the [Add Item to Cart](/api/graphql-api/admin/sales/carts/add-item) errors list). - Returns a slim row (9 fields) rather than the full storefront product schema. ## Booking Products Booking products are returned by this query for admin visibility. Attempting to add one to an admin draft cart via `addItemAdminCart` returns an error message "Booking products cannot be added to an admin draft order." — this matches the Bagisto monolith Create-Order UI which has no booking partial. --- # Catalog Products — Mass Delete URL: /api/graphql-api/admin/catalog/products/mass-delete --- outline: false examples: - id: admin-catalog-product-mass-delete title: Mass Delete Catalog Products description: GraphQL counterpart of POST /api/admin/catalog/products/mass-delete. Mirrors monolith best-effort semantics — non-existent IDs are silently skipped. query: | mutation MassDeleteProducts($input: createAdminCatalogProductMassDeleteInput!) { createAdminCatalogProductMassDelete(input: $input) { adminCatalogProductMassDelete { id deleted message } } } variables: | { "input": { "indices": [12, 18] } } response: | { "data": { "createAdminCatalogProductMassDelete": { "adminCatalogProductMassDelete": { "id": "/api/admin/catalog_product_mass_deletes/1", "deleted": [12, 18], "message": "Products deleted successfully." } } } } --- # Catalog Products — Mass Delete Equivalent to [`POST /api/admin/catalog/products/mass-delete`](/api/rest-api/admin/catalog/products/mass-delete). ## Operation | Operation | Type | |-----------|------| | `createAdminCatalogProductMassDelete` | Mutation | ## Input | Field | Type | Required | Notes | |-------|------|----------|-------| | `indices` | `[Int!]!` | yes | Non-empty array of product IDs. | ## Errors Surfaced in GraphQL `errors[]`. Underlying delete exceptions become errors that mirror the REST 500 path. --- # Catalog Products — Mass Update Status URL: /api/graphql-api/admin/catalog/products/mass-update-status --- outline: false examples: - id: admin-catalog-product-mass-update-status title: Mass Update Catalog Product Status description: GraphQL counterpart of POST /api/admin/catalog/products/mass-update-status. Fires catalog.product.update.{before,after} per ID. query: | mutation MassUpdateStatus($input: createAdminCatalogProductMassUpdateStatusInput!) { createAdminCatalogProductMassUpdateStatus(input: $input) { adminCatalogProductMassUpdateStatus { id updated message } } } variables: | { "input": { "indices": [12, 18], "value": 1 } } response: | { "data": { "createAdminCatalogProductMassUpdateStatus": { "adminCatalogProductMassUpdateStatus": { "id": "/api/admin/catalog_product_mass_update_statuses/1", "updated": [12, 18], "message": "Products status updated successfully." } } } } --- # Catalog Products — Mass Update Status Equivalent to [`POST /api/admin/catalog/products/mass-update-status`](/api/rest-api/admin/catalog/products/mass-update-status). ## Operation | Operation | Type | |-----------|------| | `createAdminCatalogProductMassUpdateStatus` | Mutation | ## Input | Field | Type | Required | Notes | |-------|------|----------|-------| | `indices` | `[Int!]!` | yes | Product IDs. | | `value` | `Int!` | yes | `0` (disabled) or `1` (enabled). | --- # Catalog Product — Detail (GraphQL) URL: /api/graphql-api/admin/catalog/products/products-detail --- outline: false examples: - id: admin-catalog-product-detail title: Catalog Product Detail — type-aware (GraphQL) description: Fetch a single catalog product by IRI. Type-specific blocks (superAttributes/variants for configurable, bundleOptions for bundle, linkedProducts for grouped, downloadableLinks/downloadableSamples for downloadable) are null on non-matching types. channels lists every channel with an assigned flag; attributes mirrors the admin edit-screen field set for the product's family (empty fields included). All nested arrays are returned whole — query each as a bare field. query: | query AdminCatalogProduct($id: ID!) { adminCatalogProduct(id: $id) { id _id sku name type status price formattedPrice quantity baseImageUrl imagesCount categoryId categoryName channel locale attributeFamilyId attributeFamilyName urlKey visibleIndividually shortDescription description metaTitle metaDescription metaKeywords weight taxCategoryId manageStock inStock featured new createdAt updatedAt translations images categories inventories customerGroupPrices superAttributes variants bundleOptions linkedProducts downloadableLinks downloadableSamples channels attributes } } variables: | { "id": "/api/admin/catalog/products/42" } response: | { "data": { "adminCatalogProduct": { "id": "/api/admin/catalog/products/42", "_id": 42, "sku": "SP-001", "name": "Classic Watch", "type": "simple", "status": 1, "price": "99.9900", "formattedPrice": "$99.99", "quantity": 42, "baseImageUrl": "http://localhost:8000/storage/product/42/image.webp", "imagesCount": 3, "categoryId": 5, "categoryName": "Accessories", "channel": "default", "locale": "en", "attributeFamilyId": 1, "attributeFamilyName": "Default", "urlKey": "classic-watch", "visibleIndividually": true, "shortDescription": "A premium timepiece.", "description": "Full HTML description.", "metaTitle": null, "metaDescription": null, "metaKeywords": null, "weight": 0.5, "taxCategoryId": null, "manageStock": true, "inStock": true, "featured": false, "new": true, "createdAt": "2026-01-12T08:15:00+00:00", "updatedAt": "2026-04-30T14:20:09+00:00", "translations": [ { "locale": "en", "name": "Classic Watch", "description": "Full HTML description.", "shortDescription": "A premium timepiece.", "urlKey": "classic-watch", "metaTitle": null, "metaDescription": null, "metaKeywords": null } ], "images": [ { "id": 1, "path": "product/42/img1.webp", "url": "http://localhost/storage/product/42/img1.webp", "sortOrder": 0 } ], "categories": [ { "id": 5, "name": "Accessories", "slug": "accessories" } ], "inventories": [ { "sourceId": 1, "sourceCode": "default", "qty": 42 } ], "customerGroupPrices": [], "superAttributes": null, "variants": null, "bundleOptions": null, "linkedProducts": null, "downloadableLinks": null, "downloadableSamples": null, "channels": [ { "id": 1, "code": "default", "name": "Default Channel", "assigned": true }, { "id": 2, "code": "mobile", "name": "Mobile Channel", "assigned": false } ], "attributes": [ { "id": 1, "code": "sku", "adminName": "SKU", "type": "text", "isRequired": true, "valuePerChannel": false, "valuePerLocale": false, "groupCode": "general", "groupName": "General", "value": "SP-001", "options": null }, { "id": 23, "code": "color", "adminName": "Color", "type": "select", "isRequired": false, "valuePerChannel": false, "valuePerLocale": false, "groupCode": "general", "groupName": "General", "value": null, "options": [ { "id": 1, "adminName": "Red", "swatchValue": "#ff0000", "sortOrder": 1 } ] }, { "id": 25, "code": "meta_title", "adminName": "Meta Title", "type": "textarea", "isRequired": false, "valuePerChannel": true, "valuePerLocale": true, "groupCode": "meta_description", "groupName": "Meta Description", "value": null, "options": null } ] } } } --- # Catalog Product — Detail (GraphQL) GraphQL item query that returns a single catalog product by its IRI, with all **detail-level fields populated**, including translations, images, categories, inventories, customer group prices, and type-specific blocks. ## Operation | Operation | Type | |-----------|------| | `adminCatalogProduct` | Query (item) | ## Authentication Every request must include an admin Bearer token: ``` Authorization: Bearer ``` Obtain a token via the [`createAdminLogin`](/api/graphql-api/admin/authentication) mutation. ## Arguments | Argument | Type | Required | Description | |----------|------|----------|-------------| | `id` | `ID!` | Yes | API Platform IRI of the product (e.g. `"/api/admin/catalog/products/42"`) | ::: tip Finding the IRI The IRI can be taken from the `id` field in any `adminCatalogProducts` edge node, or constructed as `/api/admin/catalog/products/{numericId}`. Both forms are accepted by the resolver. ::: ## Fields ### Core scalar fields | Field | Type | Description | |-------|------|-------------| | `id` | `ID` | API Platform IRI (e.g. `/api/admin/catalog/products/42`) | | `_id` | `Int` | Raw numeric product ID | | `sku` | `String` | Product SKU | | `name` | `String` | Localised product name | | `type` | `String` | Product type (`simple`, `configurable`, `bundle`, `grouped`, `downloadable`, `virtual`, `booking`) | | `status` | `Int` | `1` = enabled, `0` = disabled | | `price` | `String` | Raw decimal price string (e.g. `"99.9900"`) | | `formattedPrice` | `String` | Currency-formatted price (e.g. `"$99.99"`) | | `quantity` | `Int` | Total quantity across all inventory sources | | `baseImageUrl` | `String` | URL of the base/primary image | | `imagesCount` | `Int` | Total number of product images | | `categoryId` | `Int` | Primary category ID | | `categoryName` | `String` | Primary category display name | | `channel` | `String` | Channel code used for value resolution | | `locale` | `String` | Locale code used for value resolution | | `attributeFamilyId` | `Int` | Attribute family ID | | `attributeFamilyName` | `String` | Attribute family display name | | `urlKey` | `String` | URL slug (e.g. `classic-watch`) | | `visibleIndividually` | `Boolean` | Whether the product appears in storefront listings | | `shortDescription` | `String` | Short description (may contain HTML) | | `description` | `String` | Full description (may contain HTML) | | `metaTitle` | `String` | SEO meta title | | `metaDescription` | `String` | SEO meta description | | `metaKeywords` | `String` | SEO meta keywords | | `weight` | `Float` | Product weight | | `taxCategoryId` | `Int` | Tax category ID | | `manageStock` | `Boolean` | Whether inventory is managed | | `inStock` | `Boolean` | Whether the product is currently in stock | | `featured` | `Boolean` | Whether the product is featured | | `new` | `Boolean` | Whether the product is marked as new | | `createdAt` | `String` | ISO 8601 creation timestamp | | `updatedAt` | `String` | ISO 8601 last-updated timestamp | ### Array/scalar fields (plain JSON) | Field | Type | Description | |-------|------|-------------| | `translations` | scalar (JSON array\|null) | Per-locale translation rows — see shape below | | `images` | scalar (JSON array\|null) | Product image rows — see shape below | | `categories` | scalar (JSON array\|null) | Category references — see shape below | | `inventories` | scalar (JSON array\|null) | Per-source inventory rows — see shape below | | `customerGroupPrices` | scalar (JSON array\|null) | Customer-group price overrides (empty array when none) | | `channels` | scalar (JSON array) | Every channel, each flagged `assigned` for this product — see shape below | | `attributes` | scalar (JSON array) | The product's attribute-family field set (edit-screen parity) — see shape below | ### Type-specific blocks (null unless type matches) | Field | Present for type | Description | |-------|-----------------|-------------| | `superAttributes` | `configurable` | Configurable attributes and their options | | `variants` | `configurable` | Variant child products with attribute values | | `bundleOptions` | `bundle` | Bundle option groups with selectable products | | `linkedProducts` | `grouped` | Linked associated products | | `downloadableLinks` | `downloadable` | Download link rows | | `downloadableSamples` | `downloadable` | Sample download rows | ### `translations[]` element shape | Key | Type | Description | |-----|------|-------------| | `locale` | string | Locale code (e.g. `en`, `fr`) | | `name` | string\|null | Translated product name | | `description` | string\|null | Translated full description | | `shortDescription` | string\|null | Translated short description | | `urlKey` | string\|null | Translated URL slug | | `metaTitle` | string\|null | Translated SEO meta title | | `metaDescription` | string\|null | Translated SEO meta description | | `metaKeywords` | string\|null | Translated SEO meta keywords | ### `images[]` element shape | Key | Type | Description | |-----|------|-------------| | `id` | integer | Image ID | | `path` | string | Storage path relative to the disk root | | `url` | string | Full public URL | | `sortOrder` | integer | Display order position | ### `categories[]` element shape | Key | Type | Description | |-----|------|-------------| | `id` | integer | Category ID | | `name` | string | Category display name | | `slug` | string | Category URL slug | ### `inventories[]` element shape | Key | Type | Description | |-----|------|-------------| | `sourceId` | integer | Inventory source ID | | `sourceCode` | string | Inventory source code (e.g. `default`) | | `qty` | integer | Quantity at this source | ### `channels[]` element shape Every channel in the store, with `assigned` indicating whether this product belongs to it — mirrors the edit-screen Channels box (all options shown, the product's ticked). | Key | Type | Description | |-----|------|-------------| | `id` | integer | Channel ID | | `code` | string | Channel code | | `name` | string | Channel display name | | `assigned` | boolean | `true` if this product is assigned to the channel | ### `attributes[]` element shape The product's attribute-family field set — the same fields the admin edit screen renders (including family-specific ones like `color`, `size`, `brand`, `product_number`). Empty fields are present with `value: null`. | Key | Type | Description | |-----|------|-------------| | `id` | integer | Attribute ID | | `code` | string | Attribute code (e.g. `sku`, `color`, `meta_title`) | | `adminName` | string | Field label as shown in the admin | | `type` | string | Input type (`text`, `textarea`, `price`, `boolean`, `select`, `multiselect`, `checkbox`, `date`, `datetime`, `image`, `file`) | | `isRequired` | boolean | Whether the field is required | | `valuePerChannel` | boolean | Whether the value can differ per channel | | `valuePerLocale` | boolean | Whether the value can differ per locale | | `groupCode` | string | Code of the field group | | `groupName` | string | Display name of the field group | | `value` | mixed\|null | Resolved value for the requested channel/locale (`null` when unset); for `select` the chosen option ID, for `multiselect`/`checkbox` a comma-separated option-ID list | | `options` | array\|null | Selectable options (`id`, `adminName`, `swatchValue`, `sortOrder`) for `select`/`multiselect`/`checkbox`; `null` otherwise | ::: warning Nested data is returned whole `translations`, `images`, `categories`, `inventories`, `customerGroupPrices`, `channels`, `attributes`, and the type-specific blocks (`variants` / `bundleOptions` / `linkedProducts` / `downloadableLinks` / `downloadableSamples` / `superAttributes`) are returned as **whole JSON** — query each as a bare field (`attributes`, not `attributes { … }`). The entire array comes back, and it resolves over GraphQL on the detail query. ::: ## Example Query ```graphql query AdminCatalogProduct($id: ID!) { adminCatalogProduct(id: $id) { id _id sku name type status price formattedPrice quantity baseImageUrl imagesCount categoryId categoryName channel locale attributeFamilyId attributeFamilyName urlKey visibleIndividually shortDescription description metaTitle metaDescription metaKeywords weight taxCategoryId manageStock inStock featured new createdAt updatedAt translations images categories inventories customerGroupPrices superAttributes variants bundleOptions linkedProducts downloadableLinks downloadableSamples channels attributes } } ``` ```json { "id": "/api/admin/catalog/products/42" } ``` ## Example Response (simple product) ```json { "data": { "adminCatalogProduct": { "id": "/api/admin/catalog/products/42", "_id": 42, "sku": "SP-001", "name": "Classic Watch", "type": "simple", "status": 1, "price": "99.9900", "formattedPrice": "$99.99", "quantity": 42, "baseImageUrl": "http://localhost:8000/storage/product/42/image.webp", "imagesCount": 3, "categoryId": 5, "categoryName": "Accessories", "channel": "default", "locale": "en", "attributeFamilyId": 1, "attributeFamilyName": "Default", "urlKey": "classic-watch", "visibleIndividually": true, "shortDescription": "A premium timepiece.", "description": "Full HTML description.", "metaTitle": null, "metaDescription": null, "metaKeywords": null, "weight": 0.5, "taxCategoryId": null, "manageStock": true, "inStock": true, "featured": false, "new": true, "createdAt": "2026-01-12T08:15:00+00:00", "updatedAt": "2026-04-30T14:20:09+00:00", "translations": [ { "locale": "en", "name": "Classic Watch", "description": "Full HTML description.", "shortDescription": "A premium timepiece.", "urlKey": "classic-watch", "metaTitle": null, "metaDescription": null, "metaKeywords": null } ], "images": [ { "id": 1, "path": "product/42/img1.webp", "url": "http://localhost/storage/product/42/img1.webp", "sortOrder": 0 } ], "categories": [ { "id": 5, "name": "Accessories", "slug": "accessories" } ], "inventories": [ { "sourceId": 1, "sourceCode": "default", "qty": 42 } ], "customerGroupPrices": [], "superAttributes": null, "variants": null, "bundleOptions": null, "linkedProducts": null, "downloadableLinks": null, "downloadableSamples": null, "channels": [ { "id": 1, "code": "default", "name": "Default Channel", "assigned": true }, { "id": 2, "code": "mobile", "name": "Mobile Channel", "assigned": false } ], "attributes": [ { "id": 1, "code": "sku", "adminName": "SKU", "type": "text", "isRequired": true, "valuePerChannel": false, "valuePerLocale": false, "groupCode": "general", "groupName": "General", "value": "SP-001", "options": null }, { "id": 23, "code": "color", "adminName": "Color", "type": "select", "isRequired": false, "valuePerChannel": false, "valuePerLocale": false, "groupCode": "general", "groupName": "General", "value": null, "options": [ { "id": 1, "adminName": "Red", "swatchValue": "#ff0000", "sortOrder": 1 } ] }, { "id": 25, "code": "meta_title", "adminName": "Meta Title", "type": "textarea", "isRequired": false, "valuePerChannel": true, "valuePerLocale": true, "groupCode": "meta_description", "groupName": "Meta Description", "value": null, "options": null } ] } } } ``` ## Errors | Scenario | GraphQL `errors[]` | HTTP Status | |----------|-------------------|-------------| | Unknown ID | `errors[]` populated or `data.adminCatalogProduct: null` | `200` (GraphQL convention) | | Missing auth | `"Unauthenticated"` in `errors[]` | `200` | ## Notes - **Type-aware payload.** The six type-specific blocks are always requested in the selection set but are `null` for non-matching product types. Switch on the `type` field to know which block to read. - **`id` argument is the IRI, not the integer.** Construct it as `"/api/admin/catalog/products/{_id}"` using the `_id` field from a listing query, or pass the `id` field directly from a listing result. - **Same data as the REST detail endpoint.** This query and `GET /api/admin/catalog/products/{id}` return identical data; the REST response uses camelCase JSON, this query returns the same fields with the nested arrays as whole JSON values. - **Nested arrays are bare JSON, not sub-selections.** Query `attributes`, `channels`, `translations`, etc. as plain fields — they return the whole array. Plain scalar fields (`id`, `sku`, `name`, `type`, `price`, …) are always returned. - **Booking products are accessible.** Even though booking products cannot be added to an admin draft cart, their detail record is fully readable via this query. - **Route disambiguation.** The REST endpoint carries a `requirements: ['id' => '\d+']` constraint — the resolver uses the IRI path for lookup, so only numeric IDs (e.g. `/api/admin/catalog/products/42`) are accepted. --- # Catalog Product — Update URL: /api/graphql-api/admin/catalog/products/update --- outline: false examples: - id: admin-catalog-product-update-attributes title: Attributes (any type) description: Partial update — send only the fields you change. Structural fields (price, weight, status, categories, channels, ...) are named camelCase args on the input; any other attribute code (name, url_key, color, meta_title, ...) goes inside extras. Omitted fields keep their current value. query: | mutation UpdateCatalogProduct($input: updateAdminCatalogProductInput!) { updateAdminCatalogProduct(input: $input) { adminCatalogProduct { id sku type } } } variables: | { "input": { "id": "/api/admin/catalog/products/42", "price": "24.99", "weight": "0.3", "status": 1, "categories": [43, 44], "extras": { "name": "Arctic Beanie", "url_key": "arctic-beanie", "meta_title": "Arctic Beanie", "color": 1 } } } response: | { "data": { "updateAdminCatalogProduct": { "adminCatalogProduct": { "id": "/api/admin/catalog/products/42", "sku": "sp-001", "type": "simple" } } } } - id: admin-catalog-product-update-downloadable title: Downloadable — links & samples description: Replace the download links and samples. Send the full set under downloadableLinks / downloadableSamples — they replace the current structure. query: | mutation UpdateCatalogProduct($input: updateAdminCatalogProductInput!) { updateAdminCatalogProduct(input: $input) { adminCatalogProduct { id sku type } } } variables: | { "input": { "id": "/api/admin/catalog/products/45", "downloadableLinks": { "link_1": { "en": { "title": "Chapter 1 PDF" }, "price": "5.00", "downloads": "3", "sort_order": "1", "type": "url", "url": "https://example.com/ch1.pdf", "sample_type": "url", "sample_url": "https://example.com/sample.pdf" } }, "downloadableSamples": { "sample_1": { "title": "Preview", "sort_order": "1", "type": "url", "url": "https://example.com/preview.pdf" } } } } response: | { "data": { "updateAdminCatalogProduct": { "adminCatalogProduct": { "id": "/api/admin/catalog/products/45", "sku": "dl-001", "type": "downloadable" } } } } - id: admin-catalog-product-update-grouped title: Grouped — linked products description: Replace the associated products of a grouped product under links. Each link references an existing product id with a default quantity and sort order. query: | mutation UpdateCatalogProduct($input: updateAdminCatalogProductInput!) { updateAdminCatalogProduct(input: $input) { adminCatalogProduct { id sku type } } } variables: | { "input": { "id": "/api/admin/catalog/products/46", "links": { "link_1": { "associated_product_id": 142, "qty": "2", "sort_order": "1" }, "link_2": { "associated_product_id": 143, "qty": "1", "sort_order": "2" } } } } response: | { "data": { "updateAdminCatalogProduct": { "adminCatalogProduct": { "id": "/api/admin/catalog/products/46", "sku": "gr-001", "type": "grouped" } } } } - id: admin-catalog-product-update-bundle title: Bundle — options description: Replace the bundle option groups under bundleOptions. Each option has a label, a type (radio/checkbox/select/multiselect), and a set of selectable products with default flags. query: | mutation UpdateCatalogProduct($input: updateAdminCatalogProductInput!) { updateAdminCatalogProduct(input: $input) { adminCatalogProduct { id sku type } } } variables: | { "input": { "id": "/api/admin/catalog/products/47", "bundleOptions": { "option_1": { "en": { "label": "Choose your accessory" }, "type": "radio", "is_required": "1", "sort_order": "1", "products": { "p1": { "product_id": 142, "qty": "1", "is_default": "1", "sort_order": "1" }, "p2": { "product_id": 143, "qty": "1", "is_default": "0", "sort_order": "2" } } } } } } response: | { "data": { "updateAdminCatalogProduct": { "adminCatalogProduct": { "id": "/api/admin/catalog/products/47", "sku": "bn-001", "type": "bundle" } } } } - id: admin-catalog-product-update-configurable title: Configurable — variants description: Update per-variant fields under variants, keyed by the variant product id (from the create response or detail variants[].id). Replace-semantics — send every variant you want to keep. query: | mutation UpdateCatalogProduct($input: updateAdminCatalogProductInput!) { updateAdminCatalogProduct(input: $input) { adminCatalogProduct { id sku type } } } variables: | { "input": { "id": "/api/admin/catalog/products/48", "variants": { "2711": { "sku": "BEANIE-RED-S", "name": "Red / Small", "price": "29.99", "weight": "0.3", "status": "1" } } } } response: | { "data": { "updateAdminCatalogProduct": { "adminCatalogProduct": { "id": "/api/admin/catalog/products/48", "sku": "cf-001", "type": "configurable" } } } } - id: admin-catalog-product-update-booking-default title: Booking — default description: Configure a default booking product under booking — recurring weekly slots with a duration, break time and quantity. query: | mutation UpdateCatalogProduct($input: updateAdminCatalogProductInput!) { updateAdminCatalogProduct(input: $input) { adminCatalogProduct { id sku type } } } variables: | { "input": { "id": "/api/admin/catalog/products/53", "booking": { "type": "default", "qty": "1", "location": "Studio A", "available_every_week": "1", "booking_type": "many", "duration": "60", "break_time": "10", "slots": [ { "from": "09:00", "to": "17:00" } ] } } } response: | { "data": { "updateAdminCatalogProduct": { "adminCatalogProduct": { "id": "/api/admin/catalog/products/53", "sku": "bk-001", "type": "booking" } } } } - id: admin-catalog-product-update-booking-appointment title: Booking — appointment description: Configure an appointment booking product under booking — per-day slot windows with a fixed appointment duration. query: | mutation UpdateCatalogProduct($input: updateAdminCatalogProductInput!) { updateAdminCatalogProduct(input: $input) { adminCatalogProduct { id sku type } } } variables: | { "input": { "id": "/api/admin/catalog/products/53", "booking": { "type": "appointment", "qty": "1", "location": "Main Clinic", "available_every_week": "1", "duration": "30", "break_time": "10", "same_slot_all_days": "1", "slots": [ { "from": "09:00", "to": "12:00" }, { "from": "14:00", "to": "17:00" } ] } } } response: | { "data": { "updateAdminCatalogProduct": { "adminCatalogProduct": { "id": "/api/admin/catalog/products/53", "sku": "bk-001", "type": "booking" } } } } - id: admin-catalog-product-update-booking-event title: Booking — event description: Configure an event booking product under booking — a fixed date/time window with one or more named tickets. query: | mutation UpdateCatalogProduct($input: updateAdminCatalogProductInput!) { updateAdminCatalogProduct(input: $input) { adminCatalogProduct { id sku type } } } variables: | { "input": { "id": "/api/admin/catalog/products/53", "booking": { "type": "event", "location": "Grand Arena", "available_from": "2026-08-01 09:00:00", "available_to": "2026-08-01 22:00:00", "tickets": { "ticket_1": { "en": { "name": "VIP", "description": "Front row" }, "price": "120.00", "qty": "50", "special_price": "99.00" } } } } } response: | { "data": { "updateAdminCatalogProduct": { "adminCatalogProduct": { "id": "/api/admin/catalog/products/53", "sku": "bk-001", "type": "booking" } } } } - id: admin-catalog-product-update-booking-rental title: Booking — rental description: Configure a rental booking product under booking — daily and/or hourly pricing over recurring weekly slots. query: | mutation UpdateCatalogProduct($input: updateAdminCatalogProductInput!) { updateAdminCatalogProduct(input: $input) { adminCatalogProduct { id sku type } } } variables: | { "input": { "id": "/api/admin/catalog/products/53", "booking": { "type": "rental", "qty": "1", "location": "Bike Shop", "available_every_week": "1", "renting_type": "daily_hourly", "daily_price": "40.00", "hourly_price": "10.00", "same_slot_all_days": "1", "slots": [ { "from": "09:00", "to": "18:00" } ] } } } response: | { "data": { "updateAdminCatalogProduct": { "adminCatalogProduct": { "id": "/api/admin/catalog/products/53", "sku": "bk-001", "type": "booking" } } } } - id: admin-catalog-product-update-booking-table title: Booking — table description: Configure a table booking product under booking — per-guest pricing, guest limit and recurring weekly slots. query: | mutation UpdateCatalogProduct($input: updateAdminCatalogProductInput!) { updateAdminCatalogProduct(input: $input) { adminCatalogProduct { id sku type } } } variables: | { "input": { "id": "/api/admin/catalog/products/53", "booking": { "type": "table", "qty": "10", "location": "Downtown Bistro", "available_every_week": "1", "price_type": "guest", "guest_limit": "4", "duration": "60", "break_time": "15", "prevent_scheduling_before": "2", "same_slot_all_days": "1", "slots": [ { "from": "12:00", "to": "22:00" } ] } } } response: | { "data": { "updateAdminCatalogProduct": { "adminCatalogProduct": { "id": "/api/admin/catalog/products/53", "sku": "bk-001", "type": "booking" } } } } - id: admin-catalog-product-update-locale title: Change locale (extras) description: GraphQL has no query string, so the locale/channel default to the store default. Translatable fields (name, description, ...) go inside extras. To target a specific locale, use the REST endpoint's ?locale= query parameter. query: | mutation UpdateCatalogProduct($input: updateAdminCatalogProductInput!) { updateAdminCatalogProduct(input: $input) { adminCatalogProduct { id sku type } } } variables: | { "input": { "id": "/api/admin/catalog/products/42", "extras": { "name": "Bonnet Arctique", "description": "Texte complet." } } } response: | { "data": { "updateAdminCatalogProduct": { "adminCatalogProduct": { "id": "/api/admin/catalog/products/42", "sku": "sp-001", "type": "simple" } } } } --- # Catalog Product — Update Equivalent to [`PUT /api/admin/catalog/products/{id}`](/api/rest-api/admin/catalog/products/update). This is a **partial patch** — send only the fields you want to change inside `input`. Omitted fields keep their current value. Pass the product IRI as `id` (e.g. `"/api/admin/catalog/products/42"`). ::: tip Prerequisites The examples use illustrative IRIs. Replace them with the IRI of a product that exists in your store — use the [`adminCatalogProducts`](./list.md) query to discover valid ids. ::: ## Operation | Operation | Type | |-----------|------| | `updateAdminCatalogProduct` | Mutation | ## Input shape The input has **named camelCase args** for the common and structural fields, plus an `extras` object for everything else: | Arg | Notes | |-----|-------| | `id` | The product IRI. Required. | | `urlKey`, `status`, `price`, `weight` | Common scalar fields. | | `categories`, `channels` | `int[]` — replace the product's assignment when sent, preserved when omitted. | | `superAttributes`, `variants` | Configurable structure. | | `bundleOptions` | Bundle structure. | | `links` | Grouped structure. | | `downloadableLinks`, `downloadableSamples` | Downloadable structure. | | `booking` | Booking structure (`type` ∈ `default` / `appointment` / `event` / `rental` / `table`). | | `extras` | Any **other** attribute code — `name`, `color`, `meta_title`, `short_description`, `brand`, … — as a JSON object keyed by attribute code. | Family attribute fields (e.g. `name`, `color`, `meta_title`, `short_description`) that aren't one of the named args go inside `extras`. Structure args **replace** that structure when sent — send the full set. See the `examples:` dropdown for each type's verified payload. ## Locale & channel GraphQL has no query string, so translatable fields are written to the store's **default** locale and channel. To target a specific locale (e.g. write the French translation while leaving English untouched), use the REST endpoint with its `?locale=fr&channel=default` query parameter — see the [REST Update page](/api/rest-api/admin/catalog/products/update). ## Sub-resources are not updated here `images`, `videos`, `inventories`, and `customerGroupPrices` are **not** handled by this mutation — they have dedicated operations. If sent, they are ignored and noted in the response `_warnings` array: - Images → [reorder images](/api/graphql-api/admin/catalog/products/images-reorder) - Inventories → [update inventories](/api/graphql-api/admin/catalog/products/inventories-update) - Customer-group prices → [customer-group prices](/api/graphql-api/admin/catalog/products/customer-group-prices-create) ## Response Returns the updated product. Select `{ adminCatalogProduct { id sku type } }`, or query the full detail fields — same shape as the [detail query](/api/graphql-api/admin/catalog/products/products-detail). --- # Categories URL: /api/graphql-api/admin/categories --- outline: false examples: - id: get-all-admin-categories title: Get All Categories (Admin) description: Retrieve all categories from the admin panel. query: | # This page documents a legacy Shop GraphQL stub; the actual Admin API # query is `adminCategories`. See: ./catalog/categories/categories-listing.md variables: | {} response: | {} --- # Categories ## About The `categories` admin query retrieves the complete category structure for administrative management. Use this query to: - Display category management interfaces - Build category trees and hierarchies - Filter and search categories - Manage category metadata and SEO - Export category structure - Sync categories with external systems - Generate category reports This query provides full administrative details including parent relationships, product counts, and publishing status. ## Arguments | Argument | Type | Description | |----------|------|-------------| | `first` | `Int` | Categories per page (max: 100). Default: 20. | | `after` | `String` | Pagination cursor for forward pagination. | | `last` | `Int` | Categories for backward pagination (max: 100). | | `before` | `String` | Pagination cursor for backward pagination. | | `status` | `[CategoryStatus!]` | Filter by: `ACTIVE`, `INACTIVE`. | | `parent_id` | `ID` | Show only children of this parent category. | | `sortKey` | `CategorySortKeys` | Sort by: `ID`, `NAME`, `POSITION`. Default: `POSITION` | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `edges` | `[CategoryEdge!]!` | Category edges with pagination cursors. | | `edges.node` | `Category!` | Category object. | | `edges.node.id` | `ID!` | Category ID. | | `edges.node.name` | `String!` | Category name. | | `edges.node.slug` | `String!` | URL-friendly slug. | | `edges.node.description` | `String` | Category description. | | `edges.node.status` | `String!` | Status (ACTIVE, INACTIVE). | | `edges.node.parentId` | `ID` | Parent category ID. | | `edges.node.position` | `Int!` | Sort position. | | `edges.node.childCount` | `Int!` | Number of child categories. | | `edges.node.productCount` | `Int!` | Number of products. | | `edges.node.createdAt` | `DateTime!` | Creation timestamp. | | `nodes` | `[Category!]!` | Flattened category array. | | `pageInfo` | `PageInfo!` | Pagination metadata. | | `totalCount` | `Int!` | Total categories. | --- # CMS Pages URL: /api/graphql-api/admin/cms/pages --- outline: false --- # CMS Pages CMS Pages are **static storefront content pages** — About Us, Privacy Policy, custom landing pages, and the like. Each page is served on the storefront at its `urlKey` (e.g. `/page/about-us`). This menu mirrors the admin **CMS → Pages** screen: list, create, edit, delete, and view those pages. ## Multi-locale, multi-channel A page is content that spans languages and storefronts: - **Multi-locale** — a page holds **one content set per language** (its `translations`). Each locale has its own `pageTitle`, `htmlContent`, `urlKey`, and SEO fields. Over GraphQL, `translations` is a **field-selectable connection** — query it via `translations { edges { node { … } } }`, picking from `locale`, `pageTitle`, `urlKey`, `htmlContent`, `metaTitle`, `metaKeywords`, `metaDescription`. It returns one entry per authored locale on both the listing and the detail query. (Over REST it comes back as a plain JSON array.) - **Multi-channel** — a page is **assigned to one or more channels** (storefronts). Over GraphQL, `channels` is also a **field-selectable connection** — query it via `channels { edges { node { … } } }`, picking from `id`, `code`, `name`. (Over REST it comes back as a plain JSON array.) ## `previewUrl` — the "View" action Every node carries a `previewUrl` — the storefront URL where the page actually renders (built from its `urlKey`). This is the API equivalent of the admin **View** action: open it in a browser to preview the live page. ## All multi-word fields resolve over GraphQL `pageTitle`, `urlKey`, `metaTitle`, `metaKeywords`, `metaDescription`, `layout`, `previewUrl`, `htmlContent`, and the timestamps all **resolve over GraphQL** on both the listing and the detail query — none come back `null` for transport reasons. `translations` and `channels` are **field-selectable connections** (`translations { edges { node { … } } }` / `channels { edges { node { … } } }`) on both queries. The node `id` is the IRI `/api/admin/cms/pages/{id}`; `_id` is the numeric ID. The single-page query takes that IRI as its `id: ID!` argument. ## Create vs Update payload shapes The two write mutations take **different shapes** (this mirrors the admin form): - **Create** sends **top-level** fields (`urlKey`, `pageTitle`, `htmlContent`, `meta*`, `channels`). Those values are **broadcast to every configured locale** at creation. - **Update** sends a **locale-nested** body so you edit one locale at a time. ## GraphQL vs REST Both transports expose the same data and behaviour. The practical difference: GraphQL lets you fetch the page **plus** its `translations` and `channels` in a **single round trip** and pick exactly the fields you need, whereas REST returns the full fixed payload per endpoint. ## Operations in this menu | Action | Operation | |--------|-----------| | [List pages](/api/graphql-api/admin/cms/pages/queries/list) | `adminCmsPages` query (cursor) | | [Page detail](/api/graphql-api/admin/cms/pages/queries/detail) | `adminCmsPage(id:)` query | | [Create page](/api/graphql-api/admin/cms/pages/mutations/create) | `createAdminCmsPage` mutation | | [Update page](/api/graphql-api/admin/cms/pages/mutations/update) | `updateAdminCmsPage` mutation | | [Delete page](/api/graphql-api/admin/cms/pages/mutations/delete) | `deleteAdminCmsPage` mutation | | [Mass delete pages](/api/graphql-api/admin/cms/pages/mutations/mass-delete) | `createAdminCmsPageMassDelete` mutation | | Export pages (CSV) | **REST only** — see [Export CMS Pages](/api/rest-api/admin/cms/pages/export). | All CMS Pages operations require an admin Bearer token — see [Authentication](/api/graphql-api/admin/authentication). Writes require the matching `cms.create` / `cms.edit` / `cms.delete` permission. --- # CMS Page — Create URL: /api/graphql-api/admin/cms/pages/mutations/create --- outline: false examples: - id: admin-cms-pages-create title: Create a CMS Page description: Top-level translated fields are broadcast to every locale by the core PageRepository. query: | mutation CreateCmsPage($input: createAdminCmsPageInput!) { createAdminCmsPage(input: $input) { adminCmsPage { id _id urlKey pageTitle } } } variables: | { "input": { "urlKey": "about-us", "pageTitle": "About Us", "htmlContent": "

About Us

", "channels": [1] } } response: | { "data": { "createAdminCmsPage": { "adminCmsPage": { "id": "/api/admin/cms_pages/7", "_id": 7, "urlKey": "about-us", "pageTitle": "About Us" } } } } --- # CMS Page — Create Equivalent to [`POST /api/admin/cms/pages`](/api/rest-api/admin/cms/pages-create). ## Operation | Operation | Type | |-----------|------| | `createAdminCmsPage` | Mutation | ## Input | Field | Type | Required | Notes | |-------|------|----------|-------| | `url_key` | `String!` | yes | Unique slug. | | `page_title` | `String!` | yes | | | `html_content` | `String!` | yes | | | `channels` | `[Int!]!` | yes | | | `meta_title`, `meta_keywords`, `meta_description` | `String` | no | | ::: warning Create vs Update payload shape **Create** takes flat top-level fields (broadcast to all locales). **Update** requires a [locale-nested payload](/api/graphql-api/admin/cms/pages/mutations/update). ::: --- # CMS Page — Delete URL: /api/graphql-api/admin/cms/pages/mutations/delete --- outline: false examples: - id: admin-cms-pages-delete title: Delete a CMS Page description: Deletes a CMS page by IRI. query: | mutation DeleteCmsPage($input: deleteAdminCmsPageInput!) { deleteAdminCmsPage(input: $input) { adminCmsPage { id } } } variables: | { "input": { "id": "/api/admin/cms_pages/7" } } response: | { "data": { "deleteAdminCmsPage": { "adminCmsPage": { "id": "/api/admin/cms_pages/7" } } } } --- # CMS Page — Delete Equivalent to [`DELETE /api/admin/cms/pages/{id}`](/api/rest-api/admin/cms/pages-delete). ## Operation | Operation | Type | |-----------|------| | `deleteAdminCmsPage` | Mutation | --- # CMS Pages — Mass Delete URL: /api/graphql-api/admin/cms/pages/mutations/mass-delete --- outline: false examples: - id: admin-cms-pages-mass-delete title: Mass Delete CMS Pages description: Bulk-delete CMS pages. Non-existent IDs are silently skipped. query: | mutation MassDeleteCmsPages($input: createAdminCmsPageMassDeleteInput!) { createAdminCmsPageMassDelete(input: $input) { adminCmsPageMassDelete { id deleted message } } } variables: | { "input": { "indices": [12, 18] } } response: | { "data": { "createAdminCmsPageMassDelete": { "adminCmsPageMassDelete": { "id": "/api/admin/cms_page_mass_deletes/1", "deleted": [12, 18], "message": "CMS pages deleted successfully." } } } } --- # CMS Pages — Mass Delete Equivalent to [`POST /api/admin/cms/pages/mass-delete`](/api/rest-api/admin/cms/pages-mass-delete). ## Operation | Operation | Type | |-----------|------| | `createAdminCmsPageMassDelete` | Mutation | ## Input | Field | Type | Required | Notes | |-------|------|----------|-------| | `indices` | `[Int!]!` | yes | CMS page IDs. | --- # CMS Page — Update URL: /api/graphql-api/admin/cms/pages/mutations/update --- outline: false examples: - id: admin-cms-pages-update title: Update a CMS Page (locale-nested) description: Validation is LOCALE-NESTED. Top-level `locale` names which block is being updated. query: | mutation UpdateCmsPage($input: updateAdminCmsPageInput!) { updateAdminCmsPage(input: $input) { adminCmsPage { id _id urlKey pageTitle } } } variables: | { "input": { "id": "/api/admin/cms_pages/7", "locale": "en", "channels": [1], "en": { "url_key": "about-us", "page_title": "About Us (Updated)", "html_content": "

About Us

" } } } response: | { "data": { "updateAdminCmsPage": { "adminCmsPage": { "id": "/api/admin/cms_pages/7", "_id": 7, "urlKey": "about-us", "pageTitle": "About Us (Updated)" } } } } --- # CMS Page — Update Equivalent to [`PUT /api/admin/cms/pages/{id}`](/api/rest-api/admin/cms/pages-update). ::: tip Prerequisites The example uses an illustrative `id` value. Replace it with the id of a CMS page that exists in your store — use the [`adminCmsPages`](/api/graphql-api/admin/cms/pages/queries/list) query to discover valid ids. ::: ## Operation | Operation | Type | |-----------|------| | `updateAdminCmsPage` | Mutation | ## Input | Field | Type | Required | Notes | |-------|------|----------|-------| | `id` | `ID!` | yes | Resource IRI. | | `locale` | `String!` | yes | Which locale block is being updated. | | `channels` | `[Int!]!` | yes | Non-empty. | | `` | `Object` | yes | `{ url_key, page_title, html_content, meta_* }`. | --- # CMS Page — Detail URL: /api/graphql-api/admin/cms/pages/queries/detail --- outline: false examples: - id: admin-cms-pages-detail title: CMS Page Detail description: Returns a single CMS page with the full htmlContent body, all translations, and channels. translations and channels are field-selectable connections — query them via edges { node { … } }. The id argument is the IRI from the listing node. query: | query CmsPage($id: ID!) { adminCmsPage(id: $id) { id _id urlKey pageTitle htmlContent metaTitle metaKeywords metaDescription layout previewUrl locale channel channels { edges { node { id code name } } } translations { edges { node { locale pageTitle urlKey htmlContent metaTitle metaKeywords metaDescription } } } } } variables: | { "id": "/api/admin/cms/pages/1" } response: | { "data": { "adminCmsPage": { "id": "/api/admin/cms/pages/1", "_id": 1, "urlKey": "about-us", "pageTitle": "About Us", "htmlContent": "
\r\n
We are dedicated to providing high-quality products and services to our customers. Our team is passionate about innovation and customer satisfaction. We believe in transparency, integrity, and building long-term relationships with our users.
\r\n
", "metaTitle": "about us", "metaKeywords": "aboutus", "metaDescription": "", "layout": null, "previewUrl": "https://your-domain.com/page/about-us", "locale": "en", "channel": "default", "channels": { "edges": [ { "node": { "id": "/api/admin_cms_page_channels/1", "code": "default", "name": "Default" } } ] }, "translations": { "edges": [ { "node": { "locale": "ar", "pageTitle": "معلومات عنا", "urlKey": "about-us", "htmlContent": "
...
", "metaTitle": "معلومات عنا", "metaKeywords": "معلومات عنا", "metaDescription": "معلومات عنا" } }, { "node": { "locale": "en", "pageTitle": "About Us", "urlKey": "about-us", "htmlContent": "
...
", "metaTitle": "about us", "metaKeywords": "aboutus", "metaDescription": "" } } ] } } } } --- # CMS Page — Detail Equivalent to [`GET /api/admin/cms/pages/{id}`](/api/rest-api/admin/cms/pages-detail). ::: tip For what CMS Pages are and how multi-locale / multi-channel works, see the [CMS Pages overview](/api/graphql-api/admin/cms/pages/). ::: ## Operation | Operation | Type | |-----------|------| | `adminCmsPage(id: ID!)` | Query | The `id` argument is the **IRI** (`/api/admin/cms/pages/{id}`) — the same value returned as `node.id` on the [listing](/api/graphql-api/admin/cms/pages/queries/list). A bare numeric ID is not accepted. ## Fields | Field | Type | Notes | |-------|------|-------| | `id` | `ID` | IRI. | | `_id` | `Int` | Numeric page ID. | | `urlKey` | `String` | URL slug for the active locale. | | `pageTitle` | `String` | Title for the active locale. | | `htmlContent` | `String` | The full page HTML body for the active locale. | | `metaTitle` / `metaKeywords` / `metaDescription` | `String` | SEO fields. | | `layout` | `String` | Page layout identifier. | | `previewUrl` | `String` | Live storefront URL for the page (the "View" action). | | `locale` / `channel` | `String` | Resolved locale / channel. | | `channels` | Connection | Every assigned channel — query via `channels { edges { node { … } } }`. | | `translations` | Connection | Per-locale content — query via `translations { edges { node { … } } }`. | ### `channels` — connection A field-selectable Relay connection. Each `node` exposes: | Field | Type | Notes | |-------|------|-------| | `id` | `ID` | Channel IRI (e.g. `/api/admin_cms_page_channels/1`). | | `code` | `String` | Channel code. | | `name` | `String` | Channel display name. | ### `translations` — connection A field-selectable Relay connection — one entry per authored locale. Each `node` exposes: | Field | Type | Notes | |-------|------|-------| | `locale` | `String` | Locale code for this translation. | | `pageTitle` | `String` | Title in this locale. | | `urlKey` | `String` | URL slug in this locale. | | `htmlContent` | `String` | The full page body for this locale. | | `metaTitle` / `metaKeywords` / `metaDescription` | `String` | SEO fields in this locale. | `translations` returns one entry per authored locale; the `htmlContent` inside a translation is the full body for that locale. ::: tip Field-selectable connections `channels` and `translations` are **Relay connections**, not opaque JSON. Select them with the `edges { node { … } }` syntax and pick exactly the sub-fields you need. (Over REST these come back as plain JSON arrays.) ::: ## Errors | Code | Cause | |------|-------| | Not found | Page not found. | --- # CMS Pages — List URL: /api/graphql-api/admin/cms/pages/queries/list --- outline: false examples: - id: admin-cms-pages-list title: List CMS Pages description: Cursor-paginated CMS pages listing. translations and channels are field-selectable connections — query them via edges { node { … } }. query: | query CmsPages($first: Int, $after: String, $page_title: String, $sort: String, $order: String) { adminCmsPages(first: $first, after: $after, page_title: $page_title, sort: $sort, order: $order) { edges { cursor node { id _id urlKey pageTitle htmlContent metaTitle metaKeywords metaDescription layout previewUrl locale channel createdAt updatedAt channels { edges { node { id code name } } } translations { edges { node { locale pageTitle urlKey metaTitle metaKeywords metaDescription } } } } } pageInfo { hasNextPage endCursor } totalCount } } variables: | { "first": 1, "sort": "id", "order": "asc" } response: | { "data": { "adminCmsPages": { "edges": [ { "cursor": "MA==", "node": { "id": "/api/admin/cms/pages/1", "_id": 1, "urlKey": "about-us", "pageTitle": "About Us", "htmlContent": "
\r\n
We are dedicated to providing high-quality products and services to our customers...
\r\n
", "metaTitle": "about us", "metaKeywords": "aboutus", "metaDescription": "", "layout": null, "previewUrl": "https://your-domain.com/page/about-us", "locale": "en", "channel": "default", "createdAt": "2024-04-16T21:44:17+05:30", "updatedAt": "2024-04-16T21:44:17+05:30", "channels": { "edges": [ { "node": { "id": "/api/admin_cms_page_channels/1", "code": "default", "name": "Default" } } ] }, "translations": { "edges": [ { "node": { "locale": "ar", "pageTitle": "معلومات عنا", "urlKey": "about-us", "metaTitle": "معلومات عنا", "metaKeywords": "معلومات عنا", "metaDescription": "معلومات عنا" } }, { "node": { "locale": "en", "pageTitle": "About Us", "urlKey": "about-us", "metaTitle": "about us", "metaKeywords": "aboutus", "metaDescription": "" } } ] } } } ], "pageInfo": { "hasNextPage": true, "endCursor": "MA==" }, "totalCount": 13 } } } --- # CMS Pages — List Equivalent to [`GET /api/admin/cms/pages`](/api/rest-api/admin/cms/pages-list). Cursor pagination via `first` / `after`. ::: tip For what CMS Pages are, how multi-locale / multi-channel works, and the `previewUrl` / `htmlContent` semantics, see the [CMS Pages overview](/api/graphql-api/admin/cms/pages/). ::: ## Operation | Operation | Type | |-----------|------| | `adminCmsPages` | Query (cursor) | ## Arguments | Arg | Type | Notes | |-----|------|-------| | `first`, `after` | cursor pagination | Page size + cursor from a previous `pageInfo.endCursor`. | | `id` | `Int` | Filter by ID. | | `page_title` | `String` | Partial title match. | | `url_key` | `String` | Partial url_key match. | | `channel` | `Int` | Filter by channel ID. | | `locale` | `String` | Locale for translation resolution. | | `sort` | `String` | `id`, `page_title`, `url_key`, `created_at`. | | `order` | `String` | `asc` or `desc`. | ## Node fields | Field | Type | Notes | |-------|------|-------| | `id` | `ID` | IRI (`/api/admin/cms/pages/{id}`). | | `_id` | `Int` | Numeric page ID. | | `urlKey` | `String` | Storefront URL slug. | | `pageTitle` | `String` | Title for the active locale. | | `htmlContent` | `String` | The full page HTML body for the active locale. | | `metaTitle` / `metaKeywords` / `metaDescription` | `String` | SEO fields. | | `layout` | `String` | Page layout identifier. | | `previewUrl` | `String` | Live storefront URL for the page (the "View" action). | | `locale` | `String` | Resolved locale code. | | `channel` | `String` | Resolved channel code. | | `channels` | Connection | Every assigned channel — query via `channels { edges { node { … } } }` (node: `id`, `code`, `name`). | | `translations` | Connection | Per-locale content — query via `translations { edges { node { … } } }` (node: `locale`, `pageTitle`, `urlKey`, `htmlContent`, `metaTitle`, `metaKeywords`, `metaDescription`). | | `createdAt` / `updatedAt` | `String` | ISO 8601. | ::: tip Field-selectable connections on the listing `translations` and `channels` are **Relay connections** on the listing too — select them with `edges { node { … } }` and pick exactly the sub-fields you need. (Over REST they come back as plain JSON arrays.) Every scalar field above resolves over GraphQL — none come back `null` for transport reasons. ::: --- # Configuration (Admin) URL: /api/graphql-api/admin/configuration --- outline: false --- # Configuration (Admin) The admin **Configuration** screen edits Bagisto's store-wide settings — order settings, currencies, email, SEO, inventory, and everything every installed module adds. Internally these are one flat key/value store; the form layout you see in the admin (Section → Group → Field group → Field) is a **schema** that each module registers at runtime. Because that schema has hundreds of fields and grows with every plugin, the API does **not** expose one endpoint per screen. Three generic operations cover the entire Configuration area — current and future: | Operation | Type | Purpose | |-----------|------|---------| | [`menuAdminConfigurationMenu`](./menu) | Query | **Discover** the schema — which fields exist, their type, default, scoping, validation, and options | | [`valuesAdminConfigurationValues`](./values) | Query | **Read** the current effective values for a slug | | [`createAdminConfigurationUpdate`](./update) | Mutation | **Write** new values for a slug | ## How the three work together A client edits a section in four steps: 1. **Discover** — call the Menu query (scoped with `slug`) to learn the fields under a section: each field's dotted `code`, `type`, whether it is `channelBased` / `localeBased`, its `validation`, and any `options`. 2. **Read** — call the Values query for that slug to load the current values into your form. 3. **Write** — call the Update mutation with the changed `code → value` map. 4. **Refresh** — Update returns the re-resolved values, so you can update your form state without another read. Or skip steps 1–2 and call the Menu query with `include_values: true` to get the schema and the current values in a single round trip. ## Core concepts - **Slug** — a dotted path (`section.group`, e.g. `sales.order_settings`) that scopes a request to one subtree of the schema. Required by Values and Update; optional on Menu (omit it to get the whole tree). - **Code** — the fully-qualified field path (`sales.order_settings.reorder.admin`). This is the unit you read and write. - **Scoping** — `channelBased` / `localeBased` (reported by Menu) decide whether the `channel` / `locale` arguments matter for a field. A field with both `false` is global; passing `channel` / `locale` for it is harmless but ignored. - **Values are strings** — the store column is text, so booleans, numbers, and JSON all come back as strings (`"1"`, `"0"`, `"49.99"`). - **Defaults** — a field with no saved value falls back to the schema `default` reported by Menu. ## Rules to know - **Stay in scope** — every key you write must start with the request's `slug.`. You cannot accidentally overwrite a field in another section. - **Validation is server-side** — it is taken from each field's schema `validation` (discovered via Menu), never trusted from the client. - **File / image fields** are set via the REST multipart endpoint only — GraphQL has no binary transport. - **Custom fields** — fields reported as `type: "custom"` are blade-rendered in the admin and are **read-only** through the API. - **Password fields** are masked in the UI but stored as plaintext (a Bagisto core behaviour, not an API limitation). All Configuration operations require an admin Bearer token — see [Authentication](/api/graphql-api/admin/authentication). --- # Configuration Menu (GraphQL) URL: /api/graphql-api/admin/configuration/menu --- outline: false examples: - id: menu-schema title: Menu — schema only description: Discover the fields under a slug. include_values is false, so no values are embedded. query: | query AdminConfigurationMenu( $slug: String $include_values: Boolean $channel: String $locale: String ) { menuAdminConfigurationMenu( slug: $slug include_values: $include_values channel: $channel locale: $locale ) { slug tree } } variables: | { "slug": "sales.order_settings", "include_values": false, "channel": "default", "locale": "en" } response: | { "data": { "menuAdminConfigurationMenu": { "slug": "sales.order_settings", "tree": [ { "key": "sales.order_settings", "name": "Order Settings", "info": "Set order numbers, minimum orders and back orders.", "icon": "settings/order.svg", "sort": 4, "children": [ { "key": "sales.order_settings.reorder", "name": "Allow Reorder", "info": "Enable or disable the reordering feature for admin users.", "icon": null, "sort": 2, "fields": [ { "name": "admin", "code": "sales.order_settings.reorder.admin", "title": "Admin Reorder", "type": "boolean", "customView": null, "default": true, "channelBased": false, "localeBased": false, "validation": null, "options": null, "depends": null, "info": "Enable or disable the reordering feature for admin users." }, { "name": "shop", "code": "sales.order_settings.reorder.shop", "title": "Shop Reorder", "type": "boolean", "customView": null, "default": true, "channelBased": false, "localeBased": false, "validation": null, "options": null, "depends": null, "info": "Enable or disable the reordering feature for shop users." } ] } ] } ] } } } - id: menu-with-values title: Menu — schema + values description: Set include_values true to embed each field's current value (resolved for the given channel / locale). query: | query AdminConfigurationMenu( $slug: String $include_values: Boolean $channel: String $locale: String ) { menuAdminConfigurationMenu( slug: $slug include_values: $include_values channel: $channel locale: $locale ) { slug tree } } variables: | { "slug": "sales.order_settings.reorder", "include_values": true, "channel": "default", "locale": "en" } response: | { "data": { "menuAdminConfigurationMenu": { "slug": "sales.order_settings.reorder", "tree": [ { "key": "sales.order_settings.reorder", "name": "Allow Reorder", "info": "Enable or disable the reordering feature for admin users.", "icon": null, "sort": 2, "fields": [ { "name": "admin", "code": "sales.order_settings.reorder.admin", "title": "Admin Reorder", "type": "boolean", "customView": null, "default": true, "channelBased": false, "localeBased": false, "validation": null, "options": null, "depends": null, "info": "Enable or disable the reordering feature for admin users.", "value": "1" }, { "name": "shop", "code": "sales.order_settings.reorder.shop", "title": "Shop Reorder", "type": "boolean", "customView": null, "default": true, "channelBased": false, "localeBased": false, "validation": null, "options": null, "depends": null, "info": "Enable or disable the reordering feature for shop users.", "value": "0" } ] } ] } } } --- # Configuration Menu (GraphQL) Query field: **`menuAdminConfigurationMenu`**. This is the **discovery** operation — it returns the configuration schema tree (Section → Group → Field group → Field). Call it first to learn which fields a section has, each field's dotted `code` (the key you read and write), its `type`, `default`, scoping flags, `validation`, and `options`. See the [Configuration overview](./) for how Menu, Values, and Update fit together. ::: tip Selection set The query exposes two fields: `slug` (echoes the requested scope) and `tree` (the schema, a JSON array — select it bare; it is not a typed sub-object). ::: ## Arguments | Argument | Type | Notes | |----------|------|-------| | `slug` | `String` | Optional. Scopes the response to one node, e.g. `sales.order_settings`. Omit to return the whole tree. | | `include_values` | `Boolean` | When `true`, embeds each field's current `value` (resolved with `channel` / `locale`). | | `channel` | `String` | Channel code used when resolving values. Defaults to the default channel. | | `locale` | `String` | Locale code used when resolving values. Defaults to the app locale. | ::: warning Arguments are snake_case The flag is **`include_values`**, not `includeValues`. All admin GraphQL query arguments use snake_case in this API. ::: ## Field shape Each leaf field in `tree[*]…fields[]` carries: | Key | Meaning | |-----|---------| | `name` | Short field name within its group. | | `code` | Fully-qualified dotted path (e.g. `sales.order_settings.reorder.admin`). Use this to read / write. | | `title` | Human-readable label (already translated). | | `type` | `text`, `textarea`, `boolean`, `select`, `multiselect`, `password`, `image`, `file`, or `custom`. | | `default` | Default used when no value has been saved. | | `channelBased` / `localeBased` | Whether the field is scoped per channel / per locale. | | `validation` | Laravel validation string applied on Update (server-enforced). | | `options` | For `select` / `multiselect` — array of `{ title, value }`. | | `depends`, `info` | Optional UI hints. | | `customView` | Set for `type: "custom"` (blade-rendered) fields. These are read-only via the API. | | `value` | Only present when `include_values: true` — the field's current value (a string, or `null` if unset). | ## Errors | Cause | Response | |-------|----------| | Unknown `slug` | `errors[]` — slug not registered. | | Unauthenticated | `errors[]` — admin Bearer token required. | --- # Configuration Update (GraphQL) URL: /api/graphql-api/admin/configuration/update --- outline: false examples: - id: update title: Update Configuration description: Bulk-upsert a code → value map under a slug. Returns the re-resolved values. query: | mutation CreateAdminConfigurationUpdate($input: createAdminConfigurationUpdateInput!) { createAdminConfigurationUpdate(input: $input) { adminConfigurationUpdate { slug channel locale success message values } } } variables: | { "input": { "slug": "sales.order_settings", "channel": "default", "locale": "en", "values": { "sales.order_settings.reorder.admin": "1", "sales.order_settings.reorder.shop": "0" } } } response: | { "data": { "createAdminConfigurationUpdate": { "adminConfigurationUpdate": { "slug": "sales.order_settings", "channel": "default", "locale": "en", "success": true, "message": "Configuration updated successfully.", "values": { "sales.order_settings.reorder.admin": "1", "sales.order_settings.reorder.shop": "0" } } } } } --- # Configuration Update (GraphQL) Mutation field: **`createAdminConfigurationUpdate`**. Bulk-upserts every entry in `values` under the given `slug`. On success the mutation returns the freshly-resolved values for that slug (the same shape as the [Values](./values) query), so the client can refresh its form state without a follow-up read. See the [Configuration overview](./) for the full flow. ::: tip Selection set The payload wraps the result in `adminConfigurationUpdate` — select `slug`, `success`, `message`, `channel`, `locale`, and `values` inside it. ::: ## Input | Field | Type | Notes | |-------|------|-------| | `slug` | `String!` | The slug whose subtree is being written. | | `channel` | `String` | Channel code. Defaults to the default channel. | | `locale` | `String` | Locale code. Defaults to the app locale. | | `values` | `Iterable!` | Map of `code → value`. Every key MUST start with `slug.`. | ## Validation Each field's validation rules come from the schema (discovered via [Menu](./menu)), resolved and enforced on the server — they are never trusted from the client. Call Menu first to know which rules apply to a given code. ## Permission Requires the admin role to carry `configuration` (or the finer-grained `configuration.edit`), or `permission_type = "all"`. ::: warning Stay in scope Every key in `values` must start with the supplied `slug.` prefix. A request with `slug: "sales.order_settings"` cannot write `catalog.inventory.stock_threshold` even if the fully-qualified key is supplied — the server rejects it before any write. ::: ::: warning File / image fields are REST-only GraphQL has no binary transport, so `image` / `file` fields cannot be set here. Sending a non-string value for such a field is rejected; use the REST multipart endpoint instead. ::: ::: warning Custom fields are read-only Fields whose schema declares `type: "custom"` (blade-rendered in the admin) cannot be written via the API. ::: ::: warning Password fields are stored plaintext `type: "password"` fields are UI-masking only; the value is stored as plaintext. This is a Bagisto core behaviour, not an API limitation. ::: --- # Configuration Values (GraphQL) URL: /api/graphql-api/admin/configuration/values --- outline: false examples: - id: values-section title: Values — a whole section description: Effective values for every field under sales.order_settings. query: | query AdminConfigurationValues( $slug: String! $channel: String $locale: String ) { valuesAdminConfigurationValues( slug: $slug channel: $channel locale: $locale ) { slug channel locale values } } variables: | { "slug": "sales.order_settings", "channel": "default", "locale": "en" } response: | { "data": { "valuesAdminConfigurationValues": { "slug": "sales.order_settings", "channel": "default", "locale": "en", "values": { "sales.order_settings.order_number.order_number_prefix": null, "sales.order_settings.order_number.order_number_length": null, "sales.order_settings.order_number.order_number_suffix": null, "sales.order_settings.order_number.order_number_generator": null, "sales.order_settings.minimum_order.enable": null, "sales.order_settings.minimum_order.minimum_order_amount": null, "sales.order_settings.minimum_order.include_discount_amount": null, "sales.order_settings.minimum_order.include_tax_to_amount": null, "sales.order_settings.minimum_order.description": null, "sales.order_settings.reorder.admin": "1", "sales.order_settings.reorder.shop": "0" } } } } - id: values-group title: Values — a single group description: Narrow the slug to one group to read just those fields. query: | query AdminConfigurationValues( $slug: String! $channel: String $locale: String ) { valuesAdminConfigurationValues( slug: $slug channel: $channel locale: $locale ) { slug channel locale values } } variables: | { "slug": "sales.order_settings.reorder", "channel": "default", "locale": "en" } response: | { "data": { "valuesAdminConfigurationValues": { "slug": "sales.order_settings.reorder", "channel": "default", "locale": "en", "values": { "sales.order_settings.reorder.admin": "1", "sales.order_settings.reorder.shop": "0" } } } } --- # Configuration Values (GraphQL) Query field: **`valuesAdminConfigurationValues`**. Returns the flat `code → value` map of effective values for every field under the given `slug`. A field with no saved value falls back to the schema `default` reported by the [Menu](./menu) query. See the [Configuration overview](./) for the full read → write flow. ## Arguments | Argument | Type | Notes | |----------|------|-------| | `slug` | `String!` | **Required.** The `section.group` (or deeper) scope to read, e.g. `sales.order_settings`. | | `channel` | `String` | Channel code for resolution. Defaults to the requested channel. | | `locale` | `String` | Locale code for resolution. Defaults to the requested locale. | ## Response shape `values` is a string → string map — the underlying store column is text, so booleans, numbers, and JSON all come back as strings (`"1"`, `"0"`, `"49.99"`). `image` / `file` fields return the storage path written by Update. ::: warning slug is required Unlike Menu, `slug` is mandatory here — it prevents accidentally dumping the entire configuration store in one call. ::: ::: tip Scope is per-field Whether `channel` / `locale` change the result depends on each field's `channelBased` / `localeBased` flags (see [Menu](./menu)). A global field returns the same value regardless of the channel / locale you pass. ::: --- # Customers URL: /api/graphql-api/admin/customers --- outline: false examples: - id: get-all-customers title: Get All Customers description: Retrieve all customers from the admin panel. query: | # This page documents a legacy Shop GraphQL stub; the actual Admin API # query is `adminCustomers`. See: ./customers/main/list.md variables: | {} response: | {} --- # Customers ## About The `customers` admin query retrieves customer information for administrative purposes. Use this query to: - Display customer management dashboards - Build customer lists and search interfaces - Access customer contact and profile information - Analyze customer accounts and history - Filter customers by status or registration date - Export customer data - Sync customer information with external systems This query provides comprehensive customer data including contact info, status, and registration details for administrative management. ## Arguments | Argument | Type | Description | |----------|------|-------------| | `first` | `Int` | Customers per page (max: 100). Default: 20. | | `after` | `String` | Pagination cursor for forward pagination. | | `last` | `Int` | Customers for backward pagination (max: 100). | | `before` | `String` | Pagination cursor for backward pagination. | | `status` | `[CustomerStatus!]` | Filter: `ACTIVE`, `INACTIVE`, `SUSPENDED`. | | `sortKey` | `CustomerSortKeys` | Sort by: `ID`, `EMAIL`, `NAME`, `CREATED_AT`. | | `search` | `String` | Search by email, name, or phone number. | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `edges` | `[CustomerEdge!]!` | Customer edges with pagination. | | `edges.node` | `Customer!` | Customer object. | | `edges.node.id` | `ID!` | Customer ID. | | `edges.node.email` | `String!` | Email address. | | `edges.node.firstName` | `String!` | First name. | | `edges.node.lastName` | `String!` | Last name. | | `edges.node.status` | `String!` | Account status. | | `edges.node.phone` | `String` | Phone number. | | `edges.node.orderCount` | `Int!` | Total orders placed. | | `edges.node.totalSpent` | `Float!` | Total amount spent. | | `edges.node.createdAt` | `DateTime!` | Registration date. | | `edges.node.updatedAt` | `DateTime!` | Last update. | | `nodes` | `[Customer!]!` | Flattened customer array. | | `pageInfo` | `PageInfo!` | Pagination metadata. | | `totalCount` | `Int!` | Total customers. | --- # Customer Active Cart Items URL: /api/graphql-api/admin/customers/active-cart-items --- outline: false examples: - id: admin-customer-active-cart title: Get Customer's Active Cart Items description: Items in the customer's own active storefront cart (carts.is_active = 1). query: | query adminCustomerCartItems($customerId: Int!) { adminCustomerCartItems(customerId: $customerId) { totalCount edges { node { id productId sku type name quantity price formattedPrice total } } } } variables: | { "customerId": 19 } response: | { "data": { "adminCustomerCartItems": { "totalCount": 1, "edges": [ { "node": { "id": "/api/admin/.../cart-items/1701", "productId": 2358, "sku": "test65", "type": "simple", "name": "Classic Watch Hand", "quantity": 1, "price": 4000, "formattedPrice": "$4,000.00", "total": 4000 } } ] } } } --- # Customer Active Cart Items Items in the customer's **own** active storefront cart. Distinct from the admin draft cart. Returns top-level items only. Requires admin Bearer token. --- # Customer Addresses URL: /api/graphql-api/admin/customers/addresses --- outline: false examples: - id: admin-customer-addresses title: Get Customer Addresses description: All saved addresses for a customer — billing/shipping picker on the Create-Order screen. query: | query adminCustomerAddresses($customerId: Int!) { adminCustomerAddresses(customerId: $customerId) { totalCount edges { node { id firstName lastName companyName address city state country postcode email phone defaultAddress } } } } variables: | { "customerId": 122 } response: | { "data": { "adminCustomerAddresses": { "totalCount": 1, "edges": [ { "node": { "id": "/api/admin/customers/.../addresses/2638", "firstName": "John", "lastName": "Doe", "companyName": "Webkul Softwares", "address": "Grand Trunk road, Sector-62", "city": "Noida", "state": "UP", "country": "IN", "postcode": "201556", "email": "john@example.com", "phone": "78787887", "defaultAddress": false } } ] } } } --- # Customer Addresses All saved addresses for a customer. Returned as a GraphQL cursor connection; the REST counterpart returns plain arrays. Requires admin Bearer token. --- # Create Customer Address (GraphQL) URL: /api/graphql-api/admin/customers/addresses/create --- outline: false examples: - id: admin-customer-address-create-gql title: Create Customer Address query: | mutation CreateAddress($input: createAdminCustomerAddressInput!) { createAdminCustomerAddress(input: $input) { adminCustomerAddress { id _id customerId address city } } } variables: | { "input": { "customerId": 14, "firstName": "Jane", "lastName": "Doe", "address": "742 Evergreen Terrace", "city": "Springfield", "country": "US", "postcode": "62704", "phone": "+15551234567" } } response: | { "data": { "createAdminCustomerAddress": { "adminCustomerAddress": { "id": "/api/admin/customers/14/addresses/27", "_id": 27, "customerId": 14, "address": "742 Evergreen Terrace", "city": "Springfield" } } } } --- # Create Customer Address (GraphQL) Permission: `customers.addresses.create`. --- # Delete Customer Address (GraphQL) URL: /api/graphql-api/admin/customers/addresses/delete --- outline: false examples: - id: admin-customer-address-delete-gql title: Delete Customer Address query: | mutation DeleteAddress($input: deleteAdminCustomerAddressInput!) { deleteAdminCustomerAddress(input: $input) { adminCustomerAddress { id } } } variables: | { "input": { "id": "/api/admin/customers/14/addresses/27" } } response: | { "data": { "deleteAdminCustomerAddress": { "adminCustomerAddress": null } } } --- # Delete Customer Address (GraphQL) Same ownership guard. Permission: `customers.addresses.delete`. ::: tip Prerequisites The example uses an illustrative `id` value. Replace it with the id of a customer address that exists in your store — use the `adminCustomerAddresses` query to discover valid ids. ::: --- # Customer Address Detail (GraphQL) URL: /api/graphql-api/admin/customers/addresses/detail --- outline: false examples: - id: admin-customer-address-detail-gql title: Customer Address Detail query: | query AdminCustomerAddress($customerId: Int!, $id: ID!) { adminCustomerAddress(customerId: $customerId, id: $id) { id _id customerId firstName lastName address city state country postcode phone defaultAddress } } variables: | { "customerId": 14, "id": "/api/admin/customers/14/addresses/27" } response: | { "data": { "adminCustomerAddress": { "id": "/api/admin/customers/14/addresses/27", "_id": 27, "customerId": 14, "firstName": "Jane", "lastName": "Doe", "address": "742 Evergreen Terrace", "city": "Springfield", "state": "IL", "country": "US", "postcode": "62704", "phone": "+15551234567", "defaultAddress": true } } } --- # Customer Address Detail (GraphQL) --- # Update Customer Address (GraphQL) URL: /api/graphql-api/admin/customers/addresses/update --- outline: false examples: - id: admin-customer-address-update-gql title: Update Customer Address query: | mutation UpdateAddress($input: updateAdminCustomerAddressInput!) { updateAdminCustomerAddress(input: $input) { adminCustomerAddress { id _id city postcode } } } variables: | { "input": { "id": "/api/admin/customers/14/addresses/27", "customerId": 14, "city": "Chicago", "postcode": "60601" } } response: | { "data": { "updateAdminCustomerAddress": { "adminCustomerAddress": { "id": "/api/admin/customers/14/addresses/27", "_id": 27, "city": "Chicago", "postcode": "60601" } } } } --- # Update Customer Address (GraphQL) Cross-customer edits return errors[]. Permission: `customers.addresses.edit`. ::: tip Prerequisites The example uses an illustrative `id` value. Replace it with the id of a customer address that exists in your store — use the `adminCustomerAddresses` query to discover valid ids. ::: --- # Create Draft Cart URL: /api/graphql-api/admin/customers/create-draft-cart --- outline: false examples: - id: admin-create-draft-cart title: Create Draft Cart description: Bootstrap an empty admin draft cart for a customer (Create-Order entry). query: | mutation CreateAdminDraftCart($input: createAdminDraftCartInput!) { createAdminDraftCart(input: $input) { adminDraftCart { cartId customerId success message } } } variables: | { "input": { "customerId": 19 } } response: | { "data": { "createAdminDraftCart": { "adminDraftCart": { "cartId": 412, "customerId": 19, "success": true, "message": "Draft cart created." } } } } --- # Create Draft Cart Customer-nested Create-Order entry point. Creates an empty admin draft cart (`is_active = 0`) for the customer and returns its **`cartId`** — the integer handle you pass to every cart-keyed write mutation (`addItemAdminCart`, `saveAddressAdminCart`, `setShippingMethodAdminCart`, `setPaymentMethodAdminCart`, `createAdminPlaceOrder`) as their `cartId` input. ::: warning Select `cartId`, not `id` This is an action mutation — its result is not an addressable record, so the auto-generated `id` (IRI) field has no value and selecting it returns `Internal server error`. Select the result fields instead: **`cartId`**, `customerId`, `success`, `message`. ::: ## Operation | Operation | Type | Purpose | |-----------|------|---------| | `createAdminDraftCart(input: createAdminDraftCartInput!)` | Mutation | Bootstrap a fresh draft cart for a customer | ## Input | Field | Type | Notes | |-------|------|-------| | `customerId` | `Int!` | The customer the order is being created for. | Distinct from `createAdminReorder` (which seeds the cart from an existing order's items). ## Errors | GraphQL `errors[]` cause | Notes | |--------------------------|-------| | Customer not found | Returned for an unknown / `0` `customerId` | | Unauthenticated | Missing admin Bearer token | | Draft cart could not be created | Bubbles up the underlying error message | --- # Delete GDPR Request (GraphQL) URL: /api/graphql-api/admin/customers/gdpr/delete --- outline: false examples: - id: admin-customer-gdpr-delete-gql title: Delete GDPR Request query: | mutation Delete($input: deleteAdminCustomerGdprRequestInput!) { deleteAdminCustomerGdprRequest(input: $input) { adminCustomerGdprRequest { id } } } variables: | { "input": { "id": "/api/admin/customers/gdpr-requests/1" } } response: | { "data": { "deleteAdminCustomerGdprRequest": { "adminCustomerGdprRequest": null } } } --- # Delete GDPR Request (GraphQL) Permission: `customers.gdpr_requests.delete`. ::: tip Prerequisites The example uses an illustrative `id` value. Replace it with the id of a GDPR request that exists in your store — use the [`adminCustomerGdprRequests`](./list.md) query to discover valid ids. ::: --- # GDPR Request Detail (GraphQL) URL: /api/graphql-api/admin/customers/gdpr/detail --- outline: false examples: - id: admin-customer-gdpr-detail-gql title: GDPR Request Detail query: | query AdminGdprRequest($id: ID!) { adminCustomerGdprRequest(id: $id) { id _id customerId customerName email type status message revokedAt createdAt } } variables: | { "id": "/api/admin/customers/gdpr-requests/1" } response: | { "data": { "adminCustomerGdprRequest": { "id": "/api/admin/customers/gdpr-requests/1", "_id": 1, "customerId": 14, "customerName": "Jane Doe", "email": "jane@example.com", "type": "delete", "status": "pending", "message": "Please remove my account", "revokedAt": null, "createdAt": "2026-05-25 08:00:00" } } } --- # GDPR Request Detail (GraphQL) --- # Download GDPR Data Export (GraphQL) URL: /api/graphql-api/admin/customers/gdpr/download-data --- outline: false examples: - id: admin-customer-gdpr-download-data-gql title: Download GDPR Data Export query: | mutation DownloadGdpr($input: createAdminCustomerGdprDownloadDataInput!) { createAdminCustomerGdprDownloadData(input: $input) { adminCustomerGdprDownloadData { customerId customerEmail generatedAt data } } } variables: | { "input": { "customerId": 14 } } response: | { "data": { "createAdminCustomerGdprDownloadData": { "adminCustomerGdprDownloadData": { "customerId": 14, "customerEmail": "jane@example.com", "generatedAt": "2026-05-25 10:00:00", "data": { "customer": { "id": 14 }, "addresses": [], "orders": [], "reviews": [], "wishlist": [], "notes": [] } } } } } --- # Download GDPR Data Export (GraphQL) Ad-hoc dump (not bound to a request). `password` / `remember_token` stripped from `customer`. Permission: `customers.gdpr_requests.view`. --- # List GDPR Requests (GraphQL) URL: /api/graphql-api/admin/customers/gdpr/list --- outline: false examples: - id: admin-customer-gdpr-list-gql title: List GDPR Requests query: | query AdminGdpr($first: Int, $status: String) { adminCustomerGdprRequests(first: $first, status: $status) { edges { cursor node { id _id customerId customerName email type status createdAt } } pageInfo { hasNextPage endCursor } totalCount } } variables: | { "first": 10, "status": "pending" } response: | { "data": { "adminCustomerGdprRequests": { "edges": [{ "cursor": "MA==", "node": { "id": "/api/admin/customers/gdpr-requests/1", "_id": 1, "customerId": 14, "customerName": "Jane Doe", "email": "jane@example.com", "type": "delete", "status": "pending", "createdAt": "2026-05-25 08:00:00" } }], "pageInfo": { "hasNextPage": false, "endCursor": "MA==" }, "totalCount": 1 } } } --- # List GDPR Requests (GraphQL) --- # Process GDPR Request (GraphQL) URL: /api/graphql-api/admin/customers/gdpr/process --- outline: false examples: - id: admin-customer-gdpr-process-gql title: Process GDPR Request description: GraphQL input uses `requestId` because API Platform rewrites `id` to `_id` on mutation inputs. query: | mutation Process($input: createAdminCustomerGdprProcessInput!) { createAdminCustomerGdprProcess(input: $input) { adminCustomerGdprProcess { requestId customerId type status customerDeleted processedAt message } } } variables: | { "input": { "requestId": "1", "message": "Approved on customer request" } } response: | { "data": { "createAdminCustomerGdprProcess": { "adminCustomerGdprProcess": { "requestId": 1, "customerId": 14, "type": "delete", "status": "approved", "customerDeleted": true, "processedAt": "2026-05-25 10:30:00", "message": "GDPR request approved and processed." } } } } --- # Process GDPR Request (GraphQL) ::: warning Destructive For `type=delete`, cascades the customer deletion. Already-approved or revoked requests → errors[]. ::: ::: tip GraphQL input quirk The GraphQL input uses `requestId` (not `id`) — `id` is reserved as the resource IRI by API Platform. ::: Permission: `customers.gdpr_requests.edit`. --- # Update GDPR Request (GraphQL) URL: /api/graphql-api/admin/customers/gdpr/update --- outline: false examples: - id: admin-customer-gdpr-update-gql title: Update GDPR Request query: | mutation Update($input: updateAdminCustomerGdprRequestInput!) { updateAdminCustomerGdprRequest(input: $input) { adminCustomerGdprRequest { id _id status } } } variables: | { "input": { "id": "/api/admin/customers/gdpr-requests/1", "status": "processing" } } response: | { "data": { "updateAdminCustomerGdprRequest": { "adminCustomerGdprRequest": { "id": "/api/admin/customers/gdpr-requests/1", "_id": 1, "status": "processing" } } } } --- # Update GDPR Request (GraphQL) Status + message only. Use `createAdminCustomerGdprProcess` for the destructive cascade. Permission: `customers.gdpr_requests.edit`. ::: tip Prerequisites The example uses an illustrative `id` value. Replace it with the id of a GDPR request that exists in your store — use the [`adminCustomerGdprRequests`](./list.md) query to discover valid ids. ::: --- # Create Customer Group (GraphQL) URL: /api/graphql-api/admin/customers/groups/create --- outline: false examples: - id: admin-customer-group-create-gql title: Create Customer Group query: | mutation Create($input: createAdminCustomerGroupInput!) { createAdminCustomerGroup(input: $input) { adminCustomerGroup { id _id code name isUserDefined } } } variables: | { "input": { "code": "vip", "name": "VIP" } } response: | { "data": { "createAdminCustomerGroup": { "adminCustomerGroup": { "id": "/api/admin/customers/groups/5", "_id": 5, "code": "vip", "name": "VIP", "isUserDefined": 1 } } } } --- # Create Customer Group (GraphQL) Always `is_user_defined=1`. Permission: `customers.groups.create`. --- # Delete Customer Group (GraphQL) URL: /api/graphql-api/admin/customers/groups/delete --- outline: false examples: - id: admin-customer-group-delete-gql title: Delete Customer Group query: | mutation Delete($input: deleteAdminCustomerGroupInput!) { deleteAdminCustomerGroup(input: $input) { adminCustomerGroup { id } } } variables: | { "input": { "id": "/api/admin/customers/groups/5" } } response: | { "data": { "deleteAdminCustomerGroup": { "adminCustomerGroup": null } } } --- # Delete Customer Group (GraphQL) ::: warning Guards Refuses for system groups or groups with customers attached. Permission: `customers.groups.delete`. ::: ::: tip Prerequisites The example uses an illustrative `id` value. Replace it with the id of a customer group that exists in your store — use the [`adminCustomerGroups`](./list.md) query to discover valid ids. ::: --- # Customer Group Detail (GraphQL) URL: /api/graphql-api/admin/customers/groups/detail --- outline: false examples: - id: admin-customer-group-detail-gql title: Customer Group Detail query: | query AdminCustomerGroup($id: ID!) { adminCustomerGroup(id: $id) { id _id code name isUserDefined customersCount } } variables: | { "id": "/api/admin/customers/groups/4" } response: | { "data": { "adminCustomerGroup": { "id": "/api/admin/customers/groups/4", "_id": 4, "code": "wholesale", "name": "Wholesale", "isUserDefined": 1, "customersCount": 23 } } } --- # Customer Group Detail (GraphQL) --- # List Customer Groups (GraphQL) URL: /api/graphql-api/admin/customers/groups/list --- outline: false examples: - id: admin-customer-groups-list-gql title: List Customer Groups query: | query AdminCustomerGroups($first: Int) { adminCustomerGroups(first: $first) { edges { cursor node { id _id code name isUserDefined } } pageInfo { hasNextPage endCursor } totalCount } } variables: | { "first": 10 } response: | { "data": { "adminCustomerGroups": { "edges": [{ "cursor": "MA==", "node": { "id": "/api/admin/customers/groups/1", "_id": 1, "code": "general", "name": "General", "isUserDefined": 0 } }], "pageInfo": { "hasNextPage": false, "endCursor": "MA==" }, "totalCount": 1 } } } --- # List Customer Groups (GraphQL) --- # Mass Delete Customer Groups (GraphQL) URL: /api/graphql-api/admin/customers/groups/mass-delete --- outline: false examples: - id: admin-customer-group-mass-delete-gql title: Mass Delete Customer Groups query: | mutation MassDelete($input: createAdminCustomerGroupMassDeleteInput!) { createAdminCustomerGroupMassDelete(input: $input) { adminCustomerGroupMassDelete { deleted skipped message } } } variables: | { "input": { "indices": [4, 5, 1] } } response: | { "data": { "createAdminCustomerGroupMassDelete": { "adminCustomerGroupMassDelete": { "deleted": [5], "skipped": [{ "id": 1, "reason": "System group cannot be deleted" }, { "id": 4, "reason": "Group has customers attached" }], "message": "Customer groups processed." } } } } --- # Mass Delete Customer Groups (GraphQL) Permission: `customers.groups.delete`. --- # Update Customer Group (GraphQL) URL: /api/graphql-api/admin/customers/groups/update --- outline: false examples: - id: admin-customer-group-update-gql title: Update Customer Group query: | mutation Update($input: updateAdminCustomerGroupInput!) { updateAdminCustomerGroup(input: $input) { adminCustomerGroup { id _id code name } } } variables: | { "input": { "id": "/api/admin/customers/groups/4", "name": "Wholesale Tier A" } } response: | { "data": { "updateAdminCustomerGroup": { "adminCustomerGroup": { "id": "/api/admin/customers/groups/4", "_id": 4, "code": "wholesale", "name": "Wholesale Tier A" } } } } --- # Update Customer Group (GraphQL) ::: warning System groups System groups (`is_user_defined=0`) only allow `name` updates; other fields return errors[]. Permission: `customers.groups.edit`. ::: ::: tip Prerequisites The example uses an illustrative `id` value. Replace it with the id of a customer group that exists in your store — use the [`adminCustomerGroups`](./list.md) query to discover valid ids. ::: --- # Impersonate Customer (GraphQL) URL: /api/graphql-api/admin/customers/impersonate/create --- outline: false examples: - id: admin-customer-impersonate-gql title: Issue Impersonation Token query: | mutation Impersonate($input: createAdminCustomerImpersonateInput!) { createAdminCustomerImpersonate(input: $input) { adminCustomerImpersonate { token customerId customerEmail customerName impersonatedByAdminId expiresAt } } } variables: | { "input": { "customerId": 14 } } response: | { "data": { "createAdminCustomerImpersonate": { "adminCustomerImpersonate": { "token": "23|aLongRandomSanctumToken", "customerId": 14, "customerEmail": "jane@example.com", "customerName": "Jane Doe", "impersonatedByAdminId": 1, "expiresAt": "2026-05-25 11:00:00" } } } } --- # Impersonate Customer (GraphQL) ::: warning 1-hour expiry, one-time plaintext The plaintext token is shown once. Token name `admin-impersonate:{adminId}` for audit; abilities include `impersonated-by-admin:{adminId}`. Expires in 1 hour. ::: Permission: `customers.customers.edit`. --- # Create Customer (GraphQL) URL: /api/graphql-api/admin/customers/main/create --- outline: false examples: - id: admin-customer-create-gql title: Create Customer description: createAdminCustomer mutation. `send_password` defaults to true and triggers a credentials email. query: | mutation CreateAdminCustomer($input: createAdminCustomerInput!) { createAdminCustomer(input: $input) { adminCustomer { id _id email customerGroupId status } } } variables: | { "input": { "firstName": "Jane", "lastName": "Doe", "email": "jane@example.com", "customerGroupId": 2, "status": 1, "sendPassword": true } } response: | { "data": { "createAdminCustomer": { "adminCustomer": { "id": "/api/admin/customers/14", "_id": 14, "email": "jane@example.com", "customerGroupId": 2, "status": 1 } } } } --- # Create Customer (GraphQL) Permission: `customers.customers.create`. Fires `customer.registration.*` + `customer.create.*` events. --- # Delete Customer (GraphQL) URL: /api/graphql-api/admin/customers/main/delete --- outline: false examples: - id: admin-customer-delete-gql title: Delete Customer query: | mutation DeleteAdminCustomer($input: deleteAdminCustomerInput!) { deleteAdminCustomer(input: $input) { adminCustomer { id } } } variables: | { "input": { "id": "/api/admin/customers/14" } } response: | { "data": { "deleteAdminCustomer": { "adminCustomer": null } } } --- # Delete Customer (GraphQL) ::: warning Active orders guard Refuses (errors[]) if the customer has any pending/processing orders. ::: Permission: `customers.customers.delete`. --- # Customer Detail URL: /api/graphql-api/admin/customers/main/detail --- outline: false examples: - id: admin-customer-detail-gql title: Customer Detail description: Eager-loads group + counters. query: | query AdminCustomer($id: ID!) { adminCustomer(id: $id) { id _id firstName lastName email phone customerGroupId customerGroupName status totalAddresses totalOrders totalAmountSpent createdAt } } variables: | { "id": "/api/admin/customers/14" } response: | { "data": { "adminCustomer": { "id": "/api/admin/customers/14", "_id": 14, "firstName": "Jane", "lastName": "Doe", "email": "jane@example.com", "phone": "+15551234567", "customerGroupId": 2, "customerGroupName": "Wholesale", "status": 1, "totalAddresses": 2, "totalOrders": 5, "totalAmountSpent": 489.5, "createdAt": "2026-05-20 12:00:00" } } } --- # Customer Detail GraphQL counterpart of `GET /api/admin/customers/{id}`. --- # List Customers (Datagrid) URL: /api/graphql-api/admin/customers/main/list --- outline: false examples: - id: admin-customers-list-gql title: List Customers (Datagrid) description: Cursor pagination. Detail-only fields are null on listing. query: | query AdminCustomers($first: Int, $after: String, $customer_group_id: Int) { adminCustomers(first: $first, after: $after, customer_group_id: $customer_group_id) { edges { cursor node { id _id firstName lastName email phone customerGroupId customerGroupName status createdAt } } pageInfo { hasNextPage endCursor } totalCount } } variables: | { "first": 10, "customer_group_id": 2 } response: | { "data": { "adminCustomers": { "edges": [{ "cursor": "MA==", "node": { "id": "/api/admin/customers/14", "_id": 14, "firstName": "Jane", "lastName": "Doe", "email": "jane@example.com", "phone": "+15551234567", "customerGroupId": 2, "customerGroupName": "Wholesale", "status": 1, "createdAt": "2026-05-20 12:00:00" } }], "pageInfo": { "hasNextPage": false, "endCursor": "MA==" }, "totalCount": 1 } } } --- # List Customers (Datagrid) GraphQL counterpart of `GET /api/admin/customers`. Args: `name`, `email`, `phone`, `customer_group_id`, `status`, `channel_id`, `sort`, `order` + cursor `first`/`after`. --- # Mass Delete Customers (GraphQL) URL: /api/graphql-api/admin/customers/main/mass-delete --- outline: false examples: - id: admin-customer-mass-delete-gql title: Mass Delete Customers query: | mutation MassDelete($input: createAdminCustomerMassDeleteInput!) { createAdminCustomerMassDelete(input: $input) { adminCustomerMassDelete { deleted skipped message } } } variables: | { "input": { "indices": [12, 13, 14] } } response: | { "data": { "createAdminCustomerMassDelete": { "adminCustomerMassDelete": { "deleted": [12, 14], "skipped": [{ "id": 13, "reason": "Customer has active orders" }], "message": "Customers processed." } } } } --- # Mass Delete Customers (GraphQL) Per-id active-orders guard skips with a reason. Permission: `customers.customers.delete`. --- # Mass Update Customer Status (GraphQL) URL: /api/graphql-api/admin/customers/main/mass-update-status --- outline: false examples: - id: admin-customer-mass-update-status-gql title: Mass Update Customer Status query: | mutation MassUpdate($input: createAdminCustomerMassUpdateStatusInput!) { createAdminCustomerMassUpdateStatus(input: $input) { adminCustomerMassUpdateStatus { updated value message } } } variables: | { "input": { "indices": [12, 13], "value": 0 } } response: | { "data": { "createAdminCustomerMassUpdateStatus": { "adminCustomerMassUpdateStatus": { "updated": [12, 13], "value": 0, "message": "Status updated." } } } } --- # Mass Update Customer Status (GraphQL) Permission: `customers.customers.edit`. --- # Update Customer (GraphQL) URL: /api/graphql-api/admin/customers/main/update --- outline: false examples: - id: admin-customer-update-gql title: Update Customer query: | mutation UpdateAdminCustomer($input: updateAdminCustomerInput!) { updateAdminCustomer(input: $input) { adminCustomer { id _id firstName status } } } variables: | { "input": { "id": "/api/admin/customers/14", "firstName": "Janet", "status": 0 } } response: | { "data": { "updateAdminCustomer": { "adminCustomer": { "id": "/api/admin/customers/14", "_id": 14, "firstName": "Janet", "status": 0 } } } } --- # Update Customer (GraphQL) Partial update. Permission: `customers.customers.edit`. Fires `customer.update.*` events. --- # Add Customer Note (GraphQL) URL: /api/graphql-api/admin/customers/notes/create --- outline: false examples: - id: admin-customer-note-create-gql title: Add Note to Customer query: | mutation CreateNote($input: createAdminCustomerNoteInput!) { createAdminCustomerNote(input: $input) { adminCustomerNote { id _id customerId note customerNotified createdAt } } } variables: | { "input": { "customerId": 14, "note": "Followed up about return RMA-1023", "customerNotified": false } } response: | { "data": { "createAdminCustomerNote": { "adminCustomerNote": { "id": "/api/admin/customer_notes/5", "_id": 5, "customerId": 14, "note": "Followed up about return RMA-1023", "customerNotified": false, "createdAt": "2026-05-25 10:00:00" } } } } --- # Add Customer Note (GraphQL) Append-only. Permission: `customers.customers.edit`. --- # Customer Recent Order Items URL: /api/graphql-api/admin/customers/recent-order-items --- outline: false examples: - id: admin-customer-recent-items title: Get Customer's Recent Order Items description: Up to 5 most-recent distinct items the customer has ordered. query: | query adminCustomerRecentOrderItems($customerId: Int!) { adminCustomerRecentOrderItems(customerId: $customerId) { totalCount edges { node { id productId sku type name price formattedPrice productImage } } } } variables: | { "customerId": 19 } response: | { "data": { "adminCustomerRecentOrderItems": { "totalCount": 1, "edges": [ { "node": { "id": "/api/admin/.../recent-order-items/2694", "productId": 2358, "sku": "test65", "type": "simple", "name": "Classic Watch Hand", "price": 4000, "formattedPrice": "$4,000.00", "productImage": "http://localhost:8000/storage/product/2358/example.webp" } } ] } } } --- # Customer Recent Order Items Up to **5 most-recent distinct products** the customer has ordered. Each row carries `type` so the client can render type-specific UI. Requires admin Bearer token. --- # Delete Review (GraphQL) URL: /api/graphql-api/admin/customers/reviews/delete --- outline: false examples: - id: admin-customer-review-delete-gql title: Delete Review query: | mutation Delete($input: deleteAdminCustomerReviewInput!) { deleteAdminCustomerReview(input: $input) { adminCustomerReview { id } } } variables: | { "input": { "id": "/api/admin/customers/reviews/9" } } response: | { "data": { "deleteAdminCustomerReview": { "adminCustomerReview": null } } } --- # Delete Review (GraphQL) Permission: `customers.reviews.delete`. ::: tip Prerequisites The example uses an illustrative `id` value. Replace it with the id of a review that exists in your store — use the [`adminCustomerReviews`](./list.md) query to discover valid ids. ::: --- # Customer Review Detail (GraphQL) URL: /api/graphql-api/admin/customers/reviews/detail --- outline: false examples: - id: admin-customer-review-detail-gql title: Customer Review Detail query: | query AdminCustomerReview($id: ID!) { adminCustomerReview(id: $id) { id _id title comment rating status productSku customerName customerEmail images createdAt } } variables: | { "id": "/api/admin/customers/reviews/9" } response: | { "data": { "adminCustomerReview": { "id": "/api/admin/customers/reviews/9", "_id": 9, "title": "Great product!", "comment": "Loved it.", "rating": 5, "status": "pending", "productSku": "SP-001", "customerName": "Jane Doe", "customerEmail": "jane@example.com", "images": [{ "id": 3, "path": "reviews/9/photo.jpg", "url": "https://your-domain.com/storage/reviews/9/photo.jpg" }], "createdAt": "2026-05-25 09:00:00" } } } --- # Customer Review Detail (GraphQL) --- # List Customer Reviews (GraphQL) URL: /api/graphql-api/admin/customers/reviews/list --- outline: false examples: - id: admin-customer-reviews-list-gql title: List Customer Reviews query: | query AdminCustomerReviews($first: Int, $status: String) { adminCustomerReviews(first: $first, status: $status) { edges { cursor node { id _id title rating status productSku customerName createdAt } } pageInfo { hasNextPage endCursor } totalCount } } variables: | { "first": 10, "status": "pending" } response: | { "data": { "adminCustomerReviews": { "edges": [{ "cursor": "MA==", "node": { "id": "/api/admin/customers/reviews/9", "_id": 9, "title": "Great product!", "rating": 5, "status": "pending", "productSku": "SP-001", "customerName": "Jane Doe", "createdAt": "2026-05-25 09:00:00" } }], "pageInfo": { "hasNextPage": false, "endCursor": "MA==" }, "totalCount": 1 } } } --- # List Customer Reviews (GraphQL) Args: `status`, `rating`, `product_id`, `customer_id`, `created_at_from/to`, `sort`, `order` + cursor `first`/`after`. --- # Mass Delete Reviews (GraphQL) URL: /api/graphql-api/admin/customers/reviews/mass-delete --- outline: false examples: - id: admin-customer-review-mass-delete-gql title: Mass Delete Reviews query: | mutation MassDelete($input: createAdminCustomerReviewMassDeleteInput!) { createAdminCustomerReviewMassDelete(input: $input) { adminCustomerReviewMassDelete { deleted skipped message } } } variables: | { "input": { "indices": [9, 10, 11] } } response: | { "data": { "createAdminCustomerReviewMassDelete": { "adminCustomerReviewMassDelete": { "deleted": [9, 10, 11], "skipped": [], "message": "Reviews deleted." } } } } --- # Mass Delete Reviews (GraphQL) --- # Mass Update Review Status (GraphQL) URL: /api/graphql-api/admin/customers/reviews/mass-update-status --- outline: false examples: - id: admin-customer-review-mass-update-status-gql title: Mass Update Review Status query: | mutation MassUpdate($input: createAdminCustomerReviewMassUpdateStatusInput!) { createAdminCustomerReviewMassUpdateStatus(input: $input) { adminCustomerReviewMassUpdateStatus { updated value message } } } variables: | { "input": { "indices": [9, 10], "value": "approved" } } response: | { "data": { "createAdminCustomerReviewMassUpdateStatus": { "adminCustomerReviewMassUpdateStatus": { "updated": [9, 10], "value": "approved", "message": "Statuses updated." } } } } --- # Mass Update Review Status (GraphQL) `value` is a string (`pending|approved|disapproved`). Permission: `customers.reviews.edit`. --- # Update Review Status (GraphQL) URL: /api/graphql-api/admin/customers/reviews/update --- outline: false examples: - id: admin-customer-review-update-gql title: Update Review Status query: | mutation Update($input: updateAdminCustomerReviewInput!) { updateAdminCustomerReview(input: $input) { adminCustomerReview { id _id status } } } variables: | { "input": { "id": "/api/admin/customers/reviews/9", "status": "approved" } } response: | { "data": { "updateAdminCustomerReview": { "adminCustomerReview": { "id": "/api/admin/customers/reviews/9", "_id": 9, "status": "approved" } } } } --- # Update Review Status (GraphQL) Status-only. Permission: `customers.reviews.edit`. ::: tip Prerequisites The example uses an illustrative `id` value. Replace it with the id of a review that exists in your store — use the [`adminCustomerReviews`](./list.md) query to discover valid ids. ::: --- # Customer Wishlist Items URL: /api/graphql-api/admin/customers/wishlist-items --- outline: false examples: - id: admin-customer-wishlist title: Get Customer's Wishlist description: The customer's wishlist for the Create-Order sidebar. query: | query adminCustomerWishlistItems($customerId: Int!) { adminCustomerWishlistItems(customerId: $customerId) { totalCount edges { node { id productId sku name price formattedPrice productImage } } } } variables: | { "customerId": 19 } response: | { "data": { "adminCustomerWishlistItems": { "totalCount": 1, "edges": [ { "node": { "id": "/api/admin/.../wishlist-items/88", "productId": 2358, "sku": "test65", "name": "Classic Watch Hand", "price": 4000, "formattedPrice": "$4,000.00", "productImage": "http://localhost:8000/storage/product/2358/example.webp" } } ] } } } --- # Customer Wishlist Items The customer's full wishlist. Requires admin Bearer token. --- # Dashboard Statistics (GraphQL) URL: /api/graphql-api/admin/dashboard/stats --- outline: false examples: - id: gql-over-all title: Over-all description: Headline KPIs for the top "Overall Details" cards — customers, orders, sales, average order value and unpaid-invoice total, each compared against the previous period. query: | query AdminDashboard($type: String, $start: String, $end: String, $channel: String) { statsAdminDashboard(type: $type, start: $start, end: $end, channel: $channel) { type dateRange statistics } } variables: | { "type": "over-all", "start": "2026-05-03", "end": "2026-06-02", "channel": "default" } response: | { "data": { "statsAdminDashboard": { "type": "over-all", "dateRange": null, "statistics": { "total_customers": { "previous": 3, "current": 0, "progress": -100 }, "total_orders": { "previous": 3, "current": 22, "progress": 633.33 }, "total_sales": { "previous": 24247, "current": 90401, "formatted_total": "$90,401.00", "progress": 272.83 }, "avg_sales": { "previous": 8082.33, "current": 4109.14, "formatted_total": "$4,109.14", "progress": -49.16 }, "total_unpaid_invoices": { "total": 959719.38, "formatted_total": "$959,719.38" } } } } } - id: gql-today title: Today description: Today-only sales/orders/customers progress plus the list of orders placed today. Each order's `items` field is pre-rendered admin HTML, not structured data. query: | query AdminDashboard($type: String, $start: String, $end: String, $channel: String) { statsAdminDashboard(type: $type, start: $start, end: $end, channel: $channel) { type dateRange statistics } } variables: | { "type": "today", "start": "2026-05-03", "end": "2026-06-02" } response: | { "data": { "statsAdminDashboard": { "type": "today", "dateRange": null, "statistics": { "total_sales": { "previous": 0, "current": 1000, "formatted_total": "$1,000.00", "progress": 100 }, "total_orders": { "previous": 0, "current": 2, "progress": 100 }, "total_customers": { "previous": 0, "current": 0, "progress": 0 }, "orders": [ { "id": 638, "increment_id": 638, "status": "processing", "status_label": "Processing", "payment_method": "Money Transfer", "base_grand_total": "465.0000", "formatted_base_grand_total": "$465.00", "channel_name": "bagisto store", "customer_email": "demo@gmail.com", "customer_name": " ", "items": "
...pre-rendered admin HTML...
", "billing_address": "test, India", "created_at": "02 Jun 2026, 12:44:33" } ] } } } } - id: gql-stock-threshold-products title: Stock Threshold Products description: Up to 5 products at or under their inventory threshold. Here `statistics` is a flat array of product rows, not an object. query: | query AdminDashboard($type: String, $start: String, $end: String, $channel: String) { statsAdminDashboard(type: $type, start: $start, end: $end, channel: $channel) { type dateRange statistics } } variables: | { "type": "stock-threshold-products", "start": "2026-05-03", "end": "2026-06-02" } response: | { "data": { "statsAdminDashboard": { "type": "stock-threshold-products", "dateRange": null, "statistics": [ { "id": 95, "sku": "Puma-White-variant-2-6", "name": "Variant 2 6", "price": "0.0000", "formatted_price": "$0.00", "total_qty": "0", "image": null }, { "id": 97, "sku": "Puma-White-variant-4-6", "name": "Variant 4 6", "price": "0.0000", "formatted_price": "$0.00", "total_qty": "0", "image": null } ] } } } - id: gql-total-sales title: Total Sales (chart) description: Powers the "Store Stats" sales chart — order/sales progress plus an `over_time` series with one bucket per day across the range. query: | query AdminDashboard($type: String, $start: String, $end: String, $channel: String) { statsAdminDashboard(type: $type, start: $start, end: $end, channel: $channel) { type dateRange statistics } } variables: | { "type": "total-sales", "start": "2026-05-03", "end": "2026-06-02" } response: | { "data": { "statsAdminDashboard": { "type": "total-sales", "dateRange": null, "statistics": { "total_orders": { "previous": 3, "current": 22, "progress": 633.33 }, "total_sales": { "previous": 24247, "current": 90401, "formatted_total": "$90,401.00", "progress": 272.83 }, "over_time": [ { "label": "11 May", "total": 0, "count": 0 }, { "label": "12 May", "total": "909.0000", "count": 1 }, { "label": "25 May", "total": "18094.0000", "count": 3 } ] } } } } - id: gql-total-visitors title: Total Visitors (chart) description: Powers the "Visitors" chart — total vs. unique visitor progress plus an `over_time` series (one bucket per day). Requires visitor/analytics data to be populated. query: | query AdminDashboard($type: String, $start: String, $end: String, $channel: String) { statsAdminDashboard(type: $type, start: $start, end: $end, channel: $channel) { type dateRange statistics } } variables: | { "type": "total-visitors", "start": "2026-05-03", "end": "2026-06-02" } response: | { "data": { "statsAdminDashboard": { "type": "total-visitors", "dateRange": null, "statistics": { "total": { "previous": 0, "current": 0, "progress": 0 }, "unique": { "previous": 0, "current": 0, "progress": 0 }, "over_time": [ { "label": "31 May", "total": 0 }, { "label": "01 Jun", "total": 0 }, { "label": "02 Jun", "total": 0 } ] } } } } - id: gql-top-selling-products title: Top Selling Products description: Up to 5 best-selling products by revenue. `statistics` is a flat array; each row carries a nested `images` array. query: | query AdminDashboard($type: String, $start: String, $end: String, $channel: String) { statsAdminDashboard(type: $type, start: $start, end: $end, channel: $channel) { type dateRange statistics } } variables: | { "type": "top-selling-products", "start": "2026-05-03", "end": "2026-06-02" } response: | { "data": { "statsAdminDashboard": { "type": "top-selling-products", "dateRange": null, "statistics": [ { "id": 2359, "name": "Horizon Arc 49\" OLED Curved Gaming Monitor", "price": "4000.0000", "formatted_price": "$3,899.00", "revenue": "38990.0000", "formatted_revenue": "$38,990.00", "images": [ { "id": 786, "type": "images", "path": "product/2359/Whw0RJrR1dLPn5HHkyk7G7hiUpY6aH8BFYOE7rlc.webp", "product_id": 2359, "position": 1, "url": "https://your-domain.com/storage/product/2359/Whw0RJrR1dLPn5HHkyk7G7hiUpY6aH8BFYOE7rlc.webp" } ] } ] } } } - id: gql-top-customers title: Top Customers description: Up to 5 customers with the most sales in the range. `statistics` is a flat array. `id` may be null for guest checkouts. query: | query AdminDashboard($type: String, $start: String, $end: String, $channel: String) { statsAdminDashboard(type: $type, start: $start, end: $end, channel: $channel) { type dateRange statistics } } variables: | { "type": "top-customers", "start": "2026-05-03", "end": "2026-06-02" } response: | { "data": { "statsAdminDashboard": { "type": "top-customers", "dateRange": null, "statistics": [ { "id": 129, "email": "demo@gmail.com", "full_name": "webkul bagisto", "total": "35776.0000", "orders": 6, "formatted_total": "$35,776.00", "datetime": null }, { "id": null, "email": "kesh@king.com", "full_name": "Kesh King", "total": "26504.0000", "orders": 10, "formatted_total": "$26,504.00", "datetime": null } ] } } } --- # Dashboard Statistics (GraphQL) Returns the aggregate statistics that power the Bagisto admin **Dashboard** screen — sales, orders, customers, visitors, stock alerts, top products and top customers. | | | |---|---| | **Query** | `statsAdminDashboard` | | **Endpoint** | `POST /api/admin/graphql` | | **Returns** | A single object: `{ type, dateRange, statistics }` | All admin endpoints require an admin Bearer token — see [Authentication](/api/graphql-api/admin/authentication). ## Understanding `type` — the dashboard is **seven** separate calls This is the most important thing to understand about this API. The Bagisto admin Dashboard you see in the panel is **not one response**. The page is assembled from **seven independent queries**, one per section, and each is selected with the `type` argument. These seven groups are exactly the sections of the admin Dashboard screen — no more, no less. So a single call returns **one section** of the dashboard. To render the full screen, call `statsAdminDashboard` once per `type` (or only for the sections you need). The `statistics` payload **changes shape per `type`** — sometimes an object, sometimes a flat array — so always branch on `type` when consuming it. ### Which `type` maps to which part of the dashboard | Dashboard section (admin panel) | `type` | `statistics` is | |---|---|---| | **Overall Details** cards (Total Sales / Orders / Customers / Average Order Sale / Total Unpaid Invoices) | `over-all` | object | | **Today's Details** + today's order list | `today` | object | | **Stock Threshold** product list | `stock-threshold-products` | array | | **Store Stats** sales chart | `total-sales` | object (with `over_time` series) | | **Visitors** chart | `total-visitors` | object (with `over_time` series) | | **Top Selling Products** list | `top-selling-products` | array | | **Customer With Most Sales** list | `top-customers` | array | `over-all` is the default — if you omit `type`, you get the "Overall Details" cards. ## Arguments | Argument | Type | Required | Description | |---|---|---|---| | `type` | `String` | No | One of the seven values above. Defaults to `over-all`. An unknown value returns a GraphQL `errors[]` entry (`400`, `invalid-type`). | | `start` | `String` (YYYY-MM-DD) | No | Lower bound of the reporting window. Defaults to **30 days ago**. | | `end` | `String` (YYYY-MM-DD) | No | Upper bound. Defaults to **today**. | | `channel` | `String` | No | Channel **code** to scope the figures to a single channel. Defaults to all channels. | `start` / `end` drive both the figures and the `previous` baseline used for each `progress` percentage — the previous period is the same-length window immediately before `start`. ## Selection set ```graphql query AdminDashboard($type: String, $start: String, $end: String, $channel: String) { statsAdminDashboard(type: $type, start: $start, end: $end, channel: $channel) { type dateRange statistics } } ``` - `type` — echoes back the requested group. - `dateRange` — a human-readable label for the window (e.g. `"03 May - 02 Jun"`). - `statistics` — a JSON scalar whose shape depends on `type` (documented below). ::: warning `dateRange` is `null` over GraphQL Due to a known GraphQL serialization limitation in this build, the camelCase `dateRange` field returns **`null`** over GraphQL. `type` and `statistics` are fully populated. If you need the formatted date-range label, read it from the [REST endpoint](/api/rest-api/admin/dashboard/stats), which returns it correctly — or reconstruct it from the `start` / `end` you sent. ::: ## Response shapes by `type` Figures with a `previous` / `current` / `progress` shape are period comparisons: `current` is the chosen window, `previous` is the preceding window of equal length, and `progress` is the percentage change (can be negative). ### `over-all` | Key | Shape | Meaning | |---|---|---| | `total_customers` | `{ previous, current, progress }` | New customers registered. | | `total_orders` | `{ previous, current, progress }` | Orders placed. | | `total_sales` | `{ previous, current, formatted_total, progress }` | Gross sales; `formatted_total` is the current value in base currency. | | `avg_sales` | `{ previous, current, formatted_total, progress }` | Average order value. | | `total_unpaid_invoices` | `{ total, formatted_total }` | Outstanding invoice amount (no period comparison). | ### `today` | Key | Shape | Meaning | |---|---|---| | `total_sales` | `{ previous, current, formatted_total, progress }` | Today's sales vs. yesterday. | | `total_orders` | `{ previous, current, progress }` | Today's orders. | | `total_customers` | `{ previous, current, progress }` | Today's new customers. | | `orders` | `array` | Orders placed today. Each row: `id`, `increment_id`, `status`, `status_label`, `payment_method`, `base_grand_total`, `formatted_base_grand_total`, `channel_name`, `customer_email`, `customer_name`, `items`, `billing_address`, `created_at`. | ::: tip `orders[].items` is admin HTML The `items` field is a **pre-rendered admin-panel Blade snippet** (an HTML string of product thumbnails), carried over verbatim from core. It is not structured data — a headless client should ignore it and fetch line items from the Orders API (`/api/admin/orders/{id}`) when product detail is needed. ::: ### `stock-threshold-products` `statistics` is an **array** (up to 5 rows). Each row: `id`, `sku`, `name`, `price`, `formatted_price`, `total_qty`, `image` (base-image URL or `null`). ### `total-sales` | Key | Shape | Meaning | |---|---|---| | `total_orders` | `{ previous, current, progress }` | Orders in the window. | | `total_sales` | `{ previous, current, formatted_total, progress }` | Sales in the window. | | `over_time` | `array` of `{ label, total, count }` | One bucket **per day** across `start`→`end` for the chart line. `total` is sales, `count` is order count. | ### `total-visitors` | Key | Shape | Meaning | |---|---|---| | `total` | `{ previous, current, progress }` | All visits. | | `unique` | `{ previous, current, progress }` | Unique visitors. | | `over_time` | `array` of `{ label, total }` | One bucket per day for the chart. | Visitor figures depend on the Bagisto visitor/analytics tables being populated; on a fresh store they are `0`. ### `top-selling-products` `statistics` is an **array** (up to 5 rows). Each row: `id`, `name`, `price`, `formatted_price`, `revenue`, `formatted_revenue`, and `images` (array of `{ id, type, path, product_id, position, url }`). ### `top-customers` `statistics` is an **array** (up to 5 rows). Each row: `id` (may be `null` for guest checkouts), `email`, `full_name` (may be `null`), `total`, `orders`, `formatted_total`, `datetime`. ## Errors | Condition | Result | |---|---| | Missing / invalid Bearer token | `401 Unauthenticated` | | Unknown `type` value | GraphQL `errors[]` — "Invalid dashboard stat type." (`400`) | ## See also - [Dashboard Statistics (REST)](/api/rest-api/admin/dashboard/stats) — same data; also returns `dateRange`. - [Reporting](/api/graphql-api/admin/reporting/) — deeper, dedicated sales / customer / product report endpoints. --- # Inventory URL: /api/graphql-api/admin/inventory --- outline: false examples: - id: get-inventory-levels title: Get Inventory Levels description: Retrieve inventory information for products. query: | # This page documents a legacy Shop GraphQL stub; the actual Admin API # query is `adminCatalogProductInventories`. See: ./catalog/products/inventories-list.md variables: | {} response: | {} --- # Inventory ## About The `inventory` admin query retrieves detailed stock and inventory information for products. Use this query to: - Check real-time stock levels - Monitor low inventory alerts - Track inventory across multiple warehouses/locations - Build inventory dashboards and reports - Manage stock and restocking processes - Sync inventory with fulfillment systems - Generate stock-out and low-stock reports This query provides comprehensive inventory data including stock quantities, reorder levels, warehouse locations, and inventory status. ## Arguments | Argument | Type | Description | |----------|------|-------------| | `productId` | `ID!` | Product ID to retrieve inventory for. | | `warehouseId` | `ID` | Filter by warehouse/location (optional, shows all if omitted). | | `includeReservations` | `Boolean` | Include reserved quantities from pending orders. Default: `true` | | `includeHistory` | `Boolean` | Include inventory transaction history. Default: `false` | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `id` | `ID!` | Inventory record ID. | | `productId` | `ID!` | Associated product ID. | | `sku` | `String!` | Product SKU. | | `quantity` | `Int!` | Current available quantity (quantity - reserved). | | `totalQuantity` | `Int!` | Total quantity including reserved stock. | | `reserved` | `Int!` | Quantity reserved by pending orders. | | `reorderLevel` | `Int` | Threshold for reorder alerts. | | `status` | `String!` | Stock status: `in-stock`, `low-stock`, `out-of-stock`. | | `warehouse` | `Warehouse` | Warehouse/location information. | | `warehouse.id` | `ID!` | Warehouse ID. | | `warehouse.name` | `String!` | Warehouse name. | | `warehouse.code` | `String!` | Warehouse code. | | `lastRestockDate` | `DateTime` | Date of last inventory increase. | | `lastSaleDate` | `DateTime` | Date of most recent sale. | | `history` | `[InventoryTransaction!]` | Recent inventory changes (when includeHistory: true). | | `history.date` | `DateTime!` | Transaction date. | | `history.type` | `String!` | Type: `purchase`, `sale`, `return`, `adjustment`. | | `history.quantity` | `Int!` | Quantity changed. | | `updatedAt` | `DateTime!` | Last update timestamp. | --- # Create Marketing Campaign (GraphQL) URL: /api/graphql-api/admin/marketing/communications/campaigns-create --- outline: false examples: - id: gql title: Create Marketing Campaign query: | mutation Create($input: createAdminMarketingCampaignInput!) { createAdminMarketingCampaign(input: $input) { adminMarketingCampaign { id _id name subject status } } } variables: | { "input": { "name": "July Newsletter", "subject": "Big July deals inside!", "marketingTemplateId": 1, "marketingEventId": 1, "channelId": 1, "customerGroupId": 1, "status": 1 } } response: | { "data": { "createAdminMarketingCampaign": { "adminMarketingCampaign": { "id": "/api/admin/marketing/campaigns/1", "_id": 1, "name": "July Newsletter", "subject": "Big July deals inside!", "status": 1 } } } } --- # Create Marketing Campaign (GraphQL) Mutation: `createAdminMarketingCampaign`. --- # Delete Marketing Campaign (GraphQL) URL: /api/graphql-api/admin/marketing/communications/campaigns-delete --- outline: false examples: - id: gql title: Delete Marketing Campaign query: | mutation Delete($input: deleteAdminMarketingCampaignInput!) { deleteAdminMarketingCampaign(input: $input) { adminMarketingCampaign { id _id } } } variables: | { "input": { "id": "/api/admin/marketing/campaigns/1" } } response: | { "data": { "deleteAdminMarketingCampaign": { "adminMarketingCampaign": { "id": "/api/admin/marketing/campaigns/1", "_id": 1 } } } } --- # Delete Marketing Campaign (GraphQL) Mutation: `deleteAdminMarketingCampaign`. ::: tip Prerequisites The example uses an illustrative `id` value. Replace it with the id of a campaign that exists in your store — use the [`adminMarketingCampaigns`](./campaigns-list.md) query to discover valid ids. ::: --- # Marketing Campaign Detail (GraphQL) URL: /api/graphql-api/admin/marketing/communications/campaigns-detail --- outline: false examples: - id: gql title: Marketing Campaign Detail query: | query AdminCampaign($id: ID!) { adminMarketingCampaign(id: $id) { id _id name subject status marketingTemplateName marketingEventName channelName customerGroupCode } } variables: | { "id": "/api/admin/marketing/campaigns/1" } response: | { "data": { "adminMarketingCampaign": { "id": "/api/admin/marketing/campaigns/1", "_id": 1, "name": "July Newsletter", "subject": "Big July deals inside!", "status": 1, "marketingTemplateName": "Welcome Email", "marketingEventName": "Holiday Sale Kickoff", "channelName": "Default", "customerGroupCode": "general" } } } --- # Marketing Campaign Detail (GraphQL) Query: `adminMarketingCampaign(id:)`. --- # List Marketing Campaigns (GraphQL) URL: /api/graphql-api/admin/marketing/communications/campaigns-list --- outline: false examples: - id: gql title: List Marketing Campaigns query: | query AdminCampaigns($first: Int) { adminMarketingCampaigns(first: $first) { edges { node { id _id name subject status } } } } variables: | { "first": 10 } response: | { "data": { "adminMarketingCampaigns": { "edges": [{ "node": { "id": "/api/admin/marketing/campaigns/1", "_id": 1, "name": "July Newsletter", "subject": "Big July deals inside!", "status": 1 } }] } } } --- # List Marketing Campaigns (GraphQL) Query: `adminMarketingCampaigns`. Extra args: `name`, `status`, `marketing_template_id`, `marketing_event_id`, `channel_id`, `customer_group_id`, `sort`, `order`. --- # Send Marketing Campaign (GraphQL) URL: /api/graphql-api/admin/marketing/communications/campaigns-send --- outline: false examples: - id: gql title: Send Marketing Campaign query: | mutation Send($input: createAdminMarketingCampaignSendInput!) { createAdminMarketingCampaignSend(input: $input) { adminMarketingCampaignSend { campaignId queued message } } } variables: | { "input": { "campaignId": 12 } } response: | { "data": { "createAdminMarketingCampaignSend": { "adminMarketingCampaignSend": { "campaignId": 12, "queued": 5, "message": "Campaign queued for 5 recipient(s)." } } } } --- # Send Marketing Campaign (GraphQL) Mutation: `createAdminMarketingCampaignSend`. ::: warning Active campaigns only Inactive campaigns (`status = 0`) return an error. ::: ::: tip Manual triggers ignore date gate Bypasses the date-based event gate so admin can do test sends. ::: Permission: `marketing.communications.campaigns.edit`. ::: tip Prerequisites The example uses an illustrative `id` value. Replace it with the id of a campaign that exists in your store — use the [`adminMarketingCampaigns`](./campaigns-list.md) query to discover valid ids. ::: --- # Update Marketing Campaign (GraphQL) URL: /api/graphql-api/admin/marketing/communications/campaigns-update --- outline: false examples: - id: gql title: Update Marketing Campaign query: | mutation Update($input: updateAdminMarketingCampaignInput!) { updateAdminMarketingCampaign(input: $input) { adminMarketingCampaign { id _id subject status } } } variables: | { "input": { "id": "/api/admin/marketing/campaigns/1", "subject": "Updated subject", "status": 0 } } response: | { "data": { "updateAdminMarketingCampaign": { "adminMarketingCampaign": { "id": "/api/admin/marketing/campaigns/1", "_id": 1, "subject": "Updated subject", "status": 0 } } } } --- # Update Marketing Campaign (GraphQL) Mutation: `updateAdminMarketingCampaign`. ::: tip Prerequisites The example uses an illustrative `id` value. Replace it with the id of a campaign that exists in your store — use the [`adminMarketingCampaigns`](./campaigns-list.md) query to discover valid ids. ::: --- # Create Marketing Event (GraphQL) URL: /api/graphql-api/admin/marketing/communications/events-create --- outline: false examples: - id: gql title: Create Marketing Event query: | mutation Create($input: createAdminMarketingEventInput!) { createAdminMarketingEvent(input: $input) { adminMarketingEvent { id _id name date } } } variables: | { "input": { "name": "Holiday Sale Kickoff", "description": "Email blast to all subscribers.", "date": "2026-12-20" } } response: | { "data": { "createAdminMarketingEvent": { "adminMarketingEvent": { "id": "/api/admin/marketing/events/1", "_id": 1, "name": "Holiday Sale Kickoff", "date": "2026-12-20" } } } } --- # Create Marketing Event (GraphQL) Mutation: `createAdminMarketingEvent`. --- # Delete Marketing Event (GraphQL) URL: /api/graphql-api/admin/marketing/communications/events-delete --- outline: false examples: - id: gql title: Delete Marketing Event query: | mutation Delete($input: deleteAdminMarketingEventInput!) { deleteAdminMarketingEvent(input: $input) { adminMarketingEvent { id _id } } } variables: | { "input": { "id": "/api/admin/marketing/events/1" } } response: | { "data": { "deleteAdminMarketingEvent": { "adminMarketingEvent": { "id": "/api/admin/marketing/events/1", "_id": 1 } } } } --- # Delete Marketing Event (GraphQL) Mutation: `deleteAdminMarketingEvent`. --- # Marketing Event Detail (GraphQL) URL: /api/graphql-api/admin/marketing/communications/events-detail --- outline: false examples: - id: gql title: Marketing Event Detail query: | query AdminEvent($id: ID!) { adminMarketingEvent(id: $id) { id _id name description date } } variables: | { "id": "/api/admin/marketing/events/1" } response: | { "data": { "adminMarketingEvent": { "id": "/api/admin/marketing/events/1", "_id": 1, "name": "Holiday Sale Kickoff", "description": "Email blast to all subscribers.", "date": "2026-12-20" } } } --- # Marketing Event Detail (GraphQL) Query: `adminMarketingEvent(id:)`. --- # List Marketing Events (GraphQL) URL: /api/graphql-api/admin/marketing/communications/events-list --- outline: false examples: - id: gql title: List Marketing Events query: | query AdminEvents($first: Int) { adminMarketingEvents(first: $first) { edges { node { id _id name date } } } } variables: | { "first": 10 } response: | { "data": { "adminMarketingEvents": { "edges": [{ "node": { "id": "/api/admin/marketing/events/1", "_id": 1, "name": "Holiday Sale Kickoff", "date": "2026-12-20" } }] } } } --- # List Marketing Events (GraphQL) Query: `adminMarketingEvents`. Extra args: `name`, `date_from`, `date_to`, `sort`, `order`. --- # Update Marketing Event (GraphQL) URL: /api/graphql-api/admin/marketing/communications/events-update --- outline: false examples: - id: gql title: Update Marketing Event query: | mutation Update($input: updateAdminMarketingEventInput!) { updateAdminMarketingEvent(input: $input) { adminMarketingEvent { id _id date } } } variables: | { "input": { "id": "/api/admin/marketing/events/1", "date": "2026-12-22" } } response: | { "data": { "updateAdminMarketingEvent": { "adminMarketingEvent": { "id": "/api/admin/marketing/events/1", "_id": 1, "date": "2026-12-22" } } } } --- # Update Marketing Event (GraphQL) Mutation: `updateAdminMarketingEvent`. ::: tip Prerequisites The example uses an illustrative `id` value. Replace it with the id of a event that exists in your store — use the [`adminMarketingEvents`](./events-list.md) query to discover valid ids. ::: --- # Delete Subscription (GraphQL) URL: /api/graphql-api/admin/marketing/communications/subscribers-delete --- outline: false examples: - id: gql title: Delete Subscription query: | mutation Delete($input: deleteAdminMarketingSubscriberInput!) { deleteAdminMarketingSubscriber(input: $input) { adminMarketingSubscriber { id _id } } } variables: | { "input": { "id": "/api/admin/marketing/subscribers/1" } } response: | { "data": { "deleteAdminMarketingSubscriber": { "adminMarketingSubscriber": { "id": "/api/admin/marketing/subscribers/1", "_id": 1 } } } } --- # Delete Subscription (GraphQL) Mutation: `deleteAdminMarketingSubscriber`. ::: tip Prerequisites The example uses an illustrative `id` value. Replace it with the id of a subscriber that exists in your store — use the [`adminMarketingSubscribers`](./subscribers-list.md) query to discover valid ids. ::: --- # Newsletter Subscriber Detail (GraphQL) URL: /api/graphql-api/admin/marketing/communications/subscribers-detail --- outline: false examples: - id: gql title: Newsletter Subscriber Detail query: | query AdminSubscriber($id: ID!) { adminMarketingSubscriber(id: $id) { id _id email channelId channelName customerId customerName isSubscribed } } variables: | { "id": "/api/admin/marketing/subscribers/1" } response: | { "data": { "adminMarketingSubscriber": { "id": "/api/admin/marketing/subscribers/1", "_id": 1, "email": "subscriber@example.com", "channelId": 1, "channelName": "Default", "customerId": 12, "customerName": "Jane Doe", "isSubscribed": true } } } --- # Newsletter Subscriber Detail (GraphQL) Query: `adminMarketingSubscriber(id:)`. --- # List Newsletter Subscribers (GraphQL) URL: /api/graphql-api/admin/marketing/communications/subscribers-list --- outline: false examples: - id: gql title: List Newsletter Subscribers query: | query AdminSubscribers($first: Int) { adminMarketingSubscribers(first: $first) { edges { node { id _id email channelName customerName isSubscribed } } } } variables: | { "first": 10 } response: | { "data": { "adminMarketingSubscribers": { "edges": [{ "node": { "id": "/api/admin/marketing/subscribers/1", "_id": 1, "email": "subscriber@example.com", "channelName": "Default", "customerName": "Jane Doe", "isSubscribed": true } }] } } } --- # List Newsletter Subscribers (GraphQL) Query: `adminMarketingSubscribers`. Extra args: `email`, `channel_id`, `is_subscribed`, `sort`, `order`. ::: warning Storefront-originated Subscriptions are created via the storefront; admin only moderates. ::: --- # Toggle Subscription (GraphQL) URL: /api/graphql-api/admin/marketing/communications/subscribers-toggle --- outline: false examples: - id: gql title: Toggle Subscription query: | mutation Toggle($input: updateAdminMarketingSubscriberInput!) { updateAdminMarketingSubscriber(input: $input) { adminMarketingSubscriber { id _id isSubscribed } } } variables: | { "input": { "id": "/api/admin/marketing/subscribers/1", "isSubscribed": false } } response: | { "data": { "updateAdminMarketingSubscriber": { "adminMarketingSubscriber": { "id": "/api/admin/marketing/subscribers/1", "_id": 1, "isSubscribed": false } } } } --- # Toggle Subscription (GraphQL) Mutation: `updateAdminMarketingSubscriber`. Mirrors `is_subscribed` onto the linked customer (if any). --- # Create Email Template (GraphQL) URL: /api/graphql-api/admin/marketing/communications/templates-create --- outline: false examples: - id: gql title: Create Email Template query: | mutation Create($input: createAdminMarketingTemplateInput!) { createAdminMarketingTemplate(input: $input) { adminMarketingTemplate { id _id name status } } } variables: | { "input": { "name": "Welcome Email", "status": "active", "content": "

Welcome to our store!

" } } response: | { "data": { "createAdminMarketingTemplate": { "adminMarketingTemplate": { "id": "/api/admin/marketing/templates/1", "_id": 1, "name": "Welcome Email", "status": "active" } } } } --- # Create Email Template (GraphQL) Mutation: `createAdminMarketingTemplate`. --- # Delete Email Template (GraphQL) URL: /api/graphql-api/admin/marketing/communications/templates-delete --- outline: false examples: - id: gql title: Delete Email Template query: | mutation Delete($input: deleteAdminMarketingTemplateInput!) { deleteAdminMarketingTemplate(input: $input) { adminMarketingTemplate { id _id } } } variables: | { "input": { "id": "/api/admin/marketing/templates/1" } } response: | { "data": { "deleteAdminMarketingTemplate": { "adminMarketingTemplate": { "id": "/api/admin/marketing/templates/1", "_id": 1 } } } } --- # Delete Email Template (GraphQL) Mutation: `deleteAdminMarketingTemplate`. ::: tip Prerequisites The example uses an illustrative `id` value. Replace it with the id of a email template that exists in your store — use the [`adminMarketingTemplates`](./templates-list.md) query to discover valid ids. ::: --- # Email Template Detail (GraphQL) URL: /api/graphql-api/admin/marketing/communications/templates-detail --- outline: false examples: - id: gql title: Email Template Detail query: | query AdminTemplate($id: ID!) { adminMarketingTemplate(id: $id) { id _id name status content } } variables: | { "id": "/api/admin/marketing/templates/1" } response: | { "data": { "adminMarketingTemplate": { "id": "/api/admin/marketing/templates/1", "_id": 1, "name": "Welcome Email", "status": "active", "content": "

Welcome to our store!

" } } } --- # Email Template Detail (GraphQL) Query: `adminMarketingTemplate(id:)`. --- # List Email Templates (GraphQL) URL: /api/graphql-api/admin/marketing/communications/templates-list --- outline: false examples: - id: gql title: List Email Templates query: | query AdminTemplates($first: Int) { adminMarketingTemplates(first: $first) { edges { node { id _id name status } } } } variables: | { "first": 10 } response: | { "data": { "adminMarketingTemplates": { "edges": [{ "node": { "id": "/api/admin/marketing/templates/1", "_id": 1, "name": "Welcome Email", "status": "active" } }] } } } --- # List Email Templates (GraphQL) Query: `adminMarketingTemplates`. Extra args: `name`, `status`, `sort`, `order`. --- # Update Email Template (GraphQL) URL: /api/graphql-api/admin/marketing/communications/templates-update --- outline: false examples: - id: gql title: Update Email Template query: | mutation Update($input: updateAdminMarketingTemplateInput!) { updateAdminMarketingTemplate(input: $input) { adminMarketingTemplate { id _id name status } } } variables: | { "input": { "id": "/api/admin/marketing/templates/1", "name": "Welcome Email v2", "status": "active", "content": "

Welcome aboard!

" } } response: | { "data": { "updateAdminMarketingTemplate": { "adminMarketingTemplate": { "id": "/api/admin/marketing/templates/1", "_id": 1, "name": "Welcome Email v2", "status": "active" } } } } --- # Update Email Template (GraphQL) Mutation: `updateAdminMarketingTemplate`. ::: tip Prerequisites The example uses an illustrative `id` value. Replace it with the id of a email template that exists in your store — use the [`adminMarketingTemplates`](./templates-list.md) query to discover valid ids. ::: --- # Create Cart Rule Coupon (GraphQL) URL: /api/graphql-api/admin/marketing/promotions/cart-rule-coupons-create --- outline: false examples: - id: gql title: Create Cart Rule Coupon query: | mutation Create($input: createAdminMarketingCartRuleCouponInput!) { createAdminMarketingCartRuleCoupon(input: $input) { adminMarketingCartRuleCoupon { id _id cartRuleId code usageLimit } } } variables: | { "input": { "cartRuleId": 1, "code": "WELCOME10", "usageLimit": 100, "usagePerCustomer": 1, "expiredAt": "2027-12-31" } } response: | { "data": { "createAdminMarketingCartRuleCoupon": { "adminMarketingCartRuleCoupon": { "id": "/api/admin/marketing/cart-rule-coupons/12", "_id": 12, "cartRuleId": 1, "code": "WELCOME10", "usageLimit": 100 } } } } --- # Create Cart Rule Coupon (GraphQL) Mutation: `createAdminMarketingCartRuleCoupon`. --- # Delete Cart Rule Coupon (GraphQL) URL: /api/graphql-api/admin/marketing/promotions/cart-rule-coupons-delete --- outline: false examples: - id: gql title: Delete Cart Rule Coupon query: | mutation Delete($input: deleteAdminMarketingCartRuleCouponInput!) { deleteAdminMarketingCartRuleCoupon(input: $input) { adminMarketingCartRuleCoupon { id _id } } } variables: | { "input": { "id": "/api/admin/marketing/cart-rule-coupons/12" } } response: | { "data": { "deleteAdminMarketingCartRuleCoupon": { "adminMarketingCartRuleCoupon": { "id": "/api/admin/marketing/cart-rule-coupons/12", "_id": 12 } } } } --- # Delete Cart Rule Coupon (GraphQL) Mutation: `deleteAdminMarketingCartRuleCoupon`. ::: tip Prerequisites The example uses an illustrative `id` value. Replace it with the id of a cart rule coupon that exists in your store — use the [`adminMarketingCartRuleCoupons`](./cart-rule-coupons-list.md) query to discover valid ids. ::: --- # Bulk-Generate Cart Rule Coupons (GraphQL) URL: /api/graphql-api/admin/marketing/promotions/cart-rule-coupons-generate --- outline: false examples: - id: gql title: Bulk-Generate Coupons query: | mutation Generate($input: createAdminMarketingCartRuleCouponGenerateInput!) { createAdminMarketingCartRuleCouponGenerate(input: $input) { adminMarketingCartRuleCouponGenerate { cartRuleId generated success message } } } variables: | { "input": { "cartRuleId": 1, "length": 10, "format": "alphanumeric", "prefix": "SAVE-", "suffix": "-2026", "couponQty": 5 } } response: | { "data": { "createAdminMarketingCartRuleCouponGenerate": { "adminMarketingCartRuleCouponGenerate": { "cartRuleId": 1, "generated": 5, "success": true, "message": "Generated 5 coupons." } } } } --- # Bulk-Generate Cart Rule Coupons (GraphQL) Mutation: `createAdminMarketingCartRuleCouponGenerate`. Accepts both `length`/`format`/`prefix`/`suffix` and the core's `code_length`/`code_format`/`code_prefix`/`code_suffix` keys. --- # List Cart Rule Coupons (GraphQL) URL: /api/graphql-api/admin/marketing/promotions/cart-rule-coupons-list --- outline: false examples: - id: gql title: List Cart Rule Coupons query: | query AdminCoupons($cartRuleId: Int!) { adminMarketingCartRuleCoupons(cartRuleId: $cartRuleId) { id _id code usageLimit usagePerCustomer timesUsed } } variables: | { "cartRuleId": 1 } response: | { "data": { "adminMarketingCartRuleCoupons": [ { "id": "/api/admin/marketing/cart-rules/1/coupons/3", "_id": 3, "code": "WELCOME10", "usageLimit": 100, "usagePerCustomer": 1, "timesUsed": 0 } ] } } --- # List Cart Rule Coupons (GraphQL) Query: `adminMarketingCartRuleCoupons(cartRuleId:)`. Cursor pagination. --- # Mass Delete Cart Rule Coupons (GraphQL) URL: /api/graphql-api/admin/marketing/promotions/cart-rule-coupons-mass-delete --- outline: false examples: - id: gql title: Mass Delete Cart Rule Coupons query: | mutation MassDelete($input: createAdminMarketingCartRuleCouponMassDeleteInput!) { createAdminMarketingCartRuleCouponMassDelete(input: $input) { adminMarketingCartRuleCouponMassDelete { cartRuleId deleted skipped success message } } } variables: | { "input": { "cartRuleId": 1, "indices": [12, 13, 14] } } response: | { "data": { "createAdminMarketingCartRuleCouponMassDelete": { "adminMarketingCartRuleCouponMassDelete": { "cartRuleId": 1, "deleted": 3, "skipped": null, "success": true, "message": "Deleted 3 coupons." } } } } --- # Mass Delete Cart Rule Coupons (GraphQL) Mutation: `createAdminMarketingCartRuleCouponMassDelete`. IDs not belonging to `cartRuleId` are silently skipped. --- # Copy Cart Rule (GraphQL) URL: /api/graphql-api/admin/marketing/promotions/cart-rules-copy --- outline: false examples: - id: gql title: Copy Cart Rule query: | mutation Copy($input: copyAdminMarketingCartRuleInput!) { copyAdminMarketingCartRule(input: $input) { adminMarketingCartRule { id _id name status channels customerGroups } } } variables: | { "input": { "id": "/api/admin/marketing/cart-rules/17" } } response: | { "data": { "copyAdminMarketingCartRule": { "adminMarketingCartRule": { "id": "/api/admin/marketing/cart-rules/42", "_id": 42, "name": "Copy of 10% off summer", "status": 0, "channels": [1], "customerGroups": [1, 2, 3] } } } } --- # Copy Cart Rule (GraphQL) Mutation: `copyAdminMarketingCartRule`. Duplicates an existing cart rule into a brand-new rule (the API equivalent of the listing's **Copy** action). The `input.id` takes the **source** rule's IRI (e.g. `/api/admin/marketing/cart-rules/17`). The mutation returns the full detail of the newly created rule. ## What gets copied | Field | Behaviour | |-------|-----------| | `name` | Prefixed with `Copy of `. | | `status` | Forced to `0` (the copy starts inactive). | | `channels` | Copied from the source rule. | | `customerGroups` | Copied from the source rule. | | Coupons | **Not** copied — the new rule has no coupons. | An unknown source id returns an error in `errors[]`. Permission: `marketing.promotions.cart_rules.create`. --- # Create Cart Rule (GraphQL) URL: /api/graphql-api/admin/marketing/promotions/cart-rules-create --- outline: false examples: - id: gql title: Create Cart Rule query: | mutation Create($input: createAdminMarketingCartRuleInput!) { createAdminMarketingCartRule(input: $input) { adminMarketingCartRule { id _id name actionType discountAmount } } } variables: | { "input": { "name": "10% off summer", "channels": [1], "customerGroups": [1, 2, 3], "couponType": 1, "actionType": "by_percent", "discountAmount": 10, "status": 1 } } response: | { "data": { "createAdminMarketingCartRule": { "adminMarketingCartRule": { "id": "/api/admin/marketing/cart-rules/1", "_id": 1, "name": "10% off summer", "actionType": "by_percent", "discountAmount": 10 } } } } --- # Create Cart Rule (GraphQL) Mutation: `createAdminMarketingCartRule`. --- # Delete Cart Rule (GraphQL) URL: /api/graphql-api/admin/marketing/promotions/cart-rules-delete --- outline: false examples: - id: gql title: Delete Cart Rule query: | mutation Delete($input: deleteAdminMarketingCartRuleInput!) { deleteAdminMarketingCartRule(input: $input) { adminMarketingCartRule { id _id } } } variables: | { "input": { "id": "/api/admin/marketing/cart-rules/1" } } response: | { "data": { "deleteAdminMarketingCartRule": { "adminMarketingCartRule": { "id": "/api/admin/marketing/cart-rules/1", "_id": 1 } } } } --- # Delete Cart Rule (GraphQL) Mutation: `deleteAdminMarketingCartRule`. --- # Cart Rule Detail (GraphQL) URL: /api/graphql-api/admin/marketing/promotions/cart-rules-detail --- outline: false examples: - id: gql title: Cart Rule Detail query: | query AdminMarketingCartRule($id: ID!) { adminMarketingCartRule(id: $id) { id _id name status couponType actionType discountAmount channels customerGroups } } variables: | { "id": "/api/admin/marketing/cart-rules/1" } response: | { "data": { "adminMarketingCartRule": { "id": "/api/admin/marketing/cart-rules/1", "_id": 1, "name": "10% off summer", "status": 1, "couponType": 1, "actionType": "by_percent", "discountAmount": 10 } } } --- # Cart Rule Detail (GraphQL) Query: `adminMarketingCartRule(id:)`. --- # List Cart Rules (GraphQL) URL: /api/graphql-api/admin/marketing/promotions/cart-rules-list --- outline: false examples: - id: gql title: List Cart Rules query: | query AdminMarketingCartRules($first: Int) { adminMarketingCartRules(first: $first) { edges { cursor node { id _id name status couponType couponCode actionType discountAmount } } pageInfo { hasNextPage endCursor } totalCount } } variables: | { "first": 10 } response: | { "data": { "adminMarketingCartRules": { "edges": [{ "cursor": "MA==", "node": { "id": "/api/admin/marketing/cart-rules/1", "_id": 1, "name": "10% off summer", "status": 1, "couponType": 1, "couponCode": "SUMMER10", "actionType": "by_percent", "discountAmount": 10 } }], "pageInfo": { "hasNextPage": false, "endCursor": "MA==" }, "totalCount": 1 } } } --- # List Cart Rules (GraphQL) Query: `adminMarketingCartRules` (cursor pagination). Each row carries `couponCode` — the rule's primary coupon code (`null` when the rule has no coupon). Args (plus standard `first`, `after`): | Arg | Notes | |-----|-------| | `id` | Single id or comma-separated list. | | `name` | Partial match. | | `status` | `0`/`1`. | | `coupon_type` | `1`/`2`. | | `coupon_code` | Partial match on the primary coupon code. | | `sort_order` | Priority, exact match. | | `starts_from_from` / `starts_from_to` | Start-date range (ISO 8601). | | `ends_till_from` / `ends_till_to` | End-date range (ISO 8601). | | `sort`, `order` | `id`/`name`/`sort_order`, `asc`/`desc`. | --- # Mass Delete Cart Rules (GraphQL) URL: /api/graphql-api/admin/marketing/promotions/cart-rules-mass-delete --- outline: false examples: - id: gql title: Mass Delete Cart Rules query: | mutation MassDelete($input: createAdminMarketingCartRuleMassDeleteInput!) { createAdminMarketingCartRuleMassDelete(input: $input) { adminMarketingCartRuleMassDelete { deleted skipped message } } } variables: | { "input": { "indices": [3, 5] } } response: | { "data": { "createAdminMarketingCartRuleMassDelete": { "adminMarketingCartRuleMassDelete": { "deleted": [3, 5], "skipped": null, "message": "Cart rules deleted." } } } } --- # Mass Delete Cart Rules (GraphQL) Mutation: `createAdminMarketingCartRuleMassDelete`. --- # Update Cart Rule (GraphQL) URL: /api/graphql-api/admin/marketing/promotions/cart-rules-update --- outline: false examples: - id: gql title: Update Cart Rule query: | mutation Update($input: updateAdminMarketingCartRuleInput!) { updateAdminMarketingCartRule(input: $input) { adminMarketingCartRule { id _id name discountAmount } } } variables: | { "input": { "id": "/api/admin/marketing/cart-rules/1", "name": "15% off summer", "discountAmount": 15 } } response: | { "data": { "updateAdminMarketingCartRule": { "adminMarketingCartRule": { "id": "/api/admin/marketing/cart-rules/1", "_id": 1, "name": "15% off summer", "discountAmount": 15 } } } } --- # Update Cart Rule (GraphQL) Mutation: `updateAdminMarketingCartRule`. --- # Create Catalog Rule (GraphQL) URL: /api/graphql-api/admin/marketing/promotions/catalog-rules-create --- outline: false examples: - id: gql title: Create Catalog Rule query: | mutation Create($input: createAdminMarketingCatalogRuleInput!) { createAdminMarketingCatalogRule(input: $input) { adminMarketingCatalogRule { id _id name actionType discountAmount } } } variables: | { "input": { "name": "Summer 10% off", "channels": [1], "customerGroups": [1, 2], "actionType": "by_percent", "discountAmount": 10 } } response: | { "data": { "createAdminMarketingCatalogRule": { "adminMarketingCatalogRule": { "id": "/api/admin/marketing/catalog-rules/1", "_id": 1, "name": "Summer 10% off", "actionType": "by_percent", "discountAmount": 10 } } } } --- # Create Catalog Rule (GraphQL) Mutation: `createAdminMarketingCatalogRule`. Permission: `marketing.promotions.catalog_rules.create`. --- # Delete Catalog Rule (GraphQL) URL: /api/graphql-api/admin/marketing/promotions/catalog-rules-delete --- outline: false examples: - id: gql title: Delete Catalog Rule query: | mutation Delete($input: deleteAdminMarketingCatalogRuleInput!) { deleteAdminMarketingCatalogRule(input: $input) { adminMarketingCatalogRule { id _id } } } variables: | { "input": { "id": "/api/admin/marketing/catalog-rules/1" } } response: | { "data": { "deleteAdminMarketingCatalogRule": { "adminMarketingCatalogRule": { "id": "/api/admin/marketing/catalog-rules/1", "_id": 1 } } } } --- # Delete Catalog Rule (GraphQL) Mutation: `deleteAdminMarketingCatalogRule`. ::: tip Prerequisites The example uses an illustrative `id` value. Replace it with the id of a catalog rule that exists in your store — use the [`adminMarketingCatalogRules`](./catalog-rules-list.md) query to discover valid ids. ::: --- # Catalog Rule Detail (GraphQL) URL: /api/graphql-api/admin/marketing/promotions/catalog-rules-detail --- outline: false examples: - id: gql title: Catalog Rule Detail query: | query AdminMarketingCatalogRule($id: ID!) { adminMarketingCatalogRule(id: $id) { id _id name description status actionType discountAmount channels customerGroups conditions } } variables: | { "id": "/api/admin/marketing/catalog-rules/1" } response: | { "data": { "adminMarketingCatalogRule": { "id": "/api/admin/marketing/catalog-rules/1", "_id": 1, "name": "Summer 10% off", "actionType": "by_percent", "discountAmount": 10, "channels": [1], "customerGroups": [1, 2] } } } --- # Catalog Rule Detail (GraphQL) Query: `adminMarketingCatalogRule(id:)`. --- # List Catalog Rules (GraphQL) URL: /api/graphql-api/admin/marketing/promotions/catalog-rules-list --- outline: false examples: - id: gql title: List Catalog Rules query: | query AdminMarketingCatalogRules($first: Int) { adminMarketingCatalogRules(first: $first) { edges { cursor node { id _id name status actionType discountAmount } } pageInfo { hasNextPage endCursor } totalCount } } variables: | { "first": 10 } response: | { "data": { "adminMarketingCatalogRules": { "edges": [{ "cursor": "MA==", "node": { "id": "/api/admin/marketing/catalog-rules/1", "_id": 1, "name": "Summer 10% off", "status": 1, "actionType": "by_percent", "discountAmount": 10 } }], "pageInfo": { "hasNextPage": false, "endCursor": "MA==" }, "totalCount": 1 } } } --- # List Catalog Rules (GraphQL) Query: `adminMarketingCatalogRules` (cursor pagination). Args (plus standard `first`, `after`): | Arg | Notes | |-----|-------| | `id` | Single id or comma-separated list. | | `name` | Partial match. | | `status` | `0`/`1`. | | `sort_order` | Priority, exact match. | | `starts_from_from` / `starts_from_to` | Start-date range (ISO 8601). | | `ends_till_from` / `ends_till_to` | End-date range (ISO 8601). | | `sort`, `order` | `id`/`name`/`sort_order`, `asc`/`desc`. | --- # Mass Delete Catalog Rules (GraphQL) URL: /api/graphql-api/admin/marketing/promotions/catalog-rules-mass-delete --- outline: false examples: - id: gql title: Mass Delete Catalog Rules query: | mutation MassDelete($input: createAdminMarketingCatalogRuleMassDeleteInput!) { createAdminMarketingCatalogRuleMassDelete(input: $input) { adminMarketingCatalogRuleMassDelete { deleted message } } } variables: | { "input": { "indices": [12, 18] } } response: | { "data": { "createAdminMarketingCatalogRuleMassDelete": { "adminMarketingCatalogRuleMassDelete": { "deleted": [12, 18], "message": "Catalog rules deleted." } } } } --- # Mass Delete Catalog Rules (GraphQL) Mutation: `createAdminMarketingCatalogRuleMassDelete`. --- # Update Catalog Rule (GraphQL) URL: /api/graphql-api/admin/marketing/promotions/catalog-rules-update --- outline: false examples: - id: gql title: Update Catalog Rule query: | mutation Update($input: updateAdminMarketingCatalogRuleInput!) { updateAdminMarketingCatalogRule(input: $input) { adminMarketingCatalogRule { id _id name discountAmount } } } variables: | { "input": { "id": "/api/admin/marketing/catalog-rules/1", "name": "Summer 15% off", "discountAmount": 15 } } response: | { "data": { "updateAdminMarketingCatalogRule": { "adminMarketingCatalogRule": { "id": "/api/admin/marketing/catalog-rules/1", "_id": 1, "name": "Summer 15% off", "discountAmount": 15 } } } } --- # Update Catalog Rule (GraphQL) Mutation: `updateAdminMarketingCatalogRule`. --- # Create Search Synonym (GraphQL) URL: /api/graphql-api/admin/marketing/search-seo/search-synonyms-create --- outline: false examples: - id: gql title: Create Search Synonym query: | mutation Create($input: createAdminMarketingSearchSynonymInput!) { createAdminMarketingSearchSynonym(input: $input) { adminMarketingSearchSynonym { id _id name terms } } } variables: | { "input": { "name": "shirt-group", "terms": "shirt,tshirt,tee" } } response: | { "data": { "createAdminMarketingSearchSynonym": { "adminMarketingSearchSynonym": { "id": "/api/admin/marketing/search-synonyms/1", "_id": 1, "name": "shirt-group", "terms": "shirt,tshirt,tee" } } } } --- # Create Search Synonym (GraphQL) Mutation: `createAdminMarketingSearchSynonym`. --- # Delete Search Synonym (GraphQL) URL: /api/graphql-api/admin/marketing/search-seo/search-synonyms-delete --- outline: false examples: - id: gql title: Delete Search Synonym query: | mutation Delete($input: deleteAdminMarketingSearchSynonymInput!) { deleteAdminMarketingSearchSynonym(input: $input) { adminMarketingSearchSynonym { id _id } } } variables: | { "input": { "id": "/api/admin/marketing/search-synonyms/1" } } response: | { "data": { "deleteAdminMarketingSearchSynonym": { "adminMarketingSearchSynonym": { "id": "/api/admin/marketing/search-synonyms/1", "_id": 1 } } } } --- # Delete Search Synonym (GraphQL) Mutation: `deleteAdminMarketingSearchSynonym`. ::: tip Prerequisites The example uses an illustrative `id` value. Replace it with the id of a search synonym that exists in your store — use the [`adminMarketingSearchSynonyms`](./search-synonyms-list.md) query to discover valid ids. ::: --- # Search Synonym Detail (GraphQL) URL: /api/graphql-api/admin/marketing/search-seo/search-synonyms-detail --- outline: false examples: - id: gql title: Search Synonym Detail query: | query AdminSynonym($id: ID!) { adminMarketingSearchSynonym(id: $id) { id _id name terms } } variables: | { "id": "/api/admin/marketing/search-synonyms/1" } response: | { "data": { "adminMarketingSearchSynonym": { "id": "/api/admin/marketing/search-synonyms/1", "_id": 1, "name": "shirt-group", "terms": "shirt,tshirt,tee" } } } --- # Search Synonym Detail (GraphQL) Query: `adminMarketingSearchSynonym(id:)`. --- # List Search Synonyms (GraphQL) URL: /api/graphql-api/admin/marketing/search-seo/search-synonyms-list --- outline: false examples: - id: gql title: List Search Synonyms query: | query AdminSynonyms($first: Int) { adminMarketingSearchSynonyms(first: $first) { edges { node { id _id name terms } } } } variables: | { "first": 10 } response: | { "data": { "adminMarketingSearchSynonyms": { "edges": [{ "node": { "id": "/api/admin/marketing/search-synonyms/1", "_id": 1, "name": "shirt-group", "terms": "shirt,tshirt,tee" } }] } } } --- # List Search Synonyms (GraphQL) Query: `adminMarketingSearchSynonyms`. Extra args: `name`, `terms`, `sort`, `order`. --- # Mass Delete Search Synonyms (GraphQL) URL: /api/graphql-api/admin/marketing/search-seo/search-synonyms-mass-delete --- outline: false examples: - id: gql title: Mass Delete Search Synonyms query: | mutation MassDelete($input: createAdminMarketingSearchSynonymMassDeleteInput!) { createAdminMarketingSearchSynonymMassDelete(input: $input) { adminMarketingSearchSynonymMassDelete { deleted message } } } variables: | { "input": { "indices": [12, 18] } } response: | { "data": { "createAdminMarketingSearchSynonymMassDelete": { "adminMarketingSearchSynonymMassDelete": { "deleted": [12, 18], "message": "Search synonyms deleted." } } } } --- # Mass Delete Search Synonyms (GraphQL) Mutation: `createAdminMarketingSearchSynonymMassDelete`. --- # Update Search Synonym (GraphQL) URL: /api/graphql-api/admin/marketing/search-seo/search-synonyms-update --- outline: false examples: - id: gql title: Update Search Synonym query: | mutation Update($input: updateAdminMarketingSearchSynonymInput!) { updateAdminMarketingSearchSynonym(input: $input) { adminMarketingSearchSynonym { id _id terms } } } variables: | { "input": { "id": "/api/admin/marketing/search-synonyms/1", "terms": "shirt,tshirt,tee,polo" } } response: | { "data": { "updateAdminMarketingSearchSynonym": { "adminMarketingSearchSynonym": { "id": "/api/admin/marketing/search-synonyms/1", "_id": 1, "terms": "shirt,tshirt,tee,polo" } } } } --- # Update Search Synonym (GraphQL) Mutation: `updateAdminMarketingSearchSynonym`. ::: tip Prerequisites The example uses an illustrative `id` value. Replace it with the id of a search synonym that exists in your store — use the [`adminMarketingSearchSynonyms`](./search-synonyms-list.md) query to discover valid ids. ::: --- # Delete Search Term (GraphQL) URL: /api/graphql-api/admin/marketing/search-seo/search-terms-delete --- outline: false examples: - id: gql title: Delete Search Term query: | mutation Delete($input: deleteAdminMarketingSearchTermInput!) { deleteAdminMarketingSearchTerm(input: $input) { adminMarketingSearchTerm { id _id } } } variables: | { "input": { "id": "/api/admin/marketing/search-terms/1" } } response: | { "data": { "deleteAdminMarketingSearchTerm": { "adminMarketingSearchTerm": { "id": "/api/admin/marketing/search-terms/1", "_id": 1 } } } } --- # Delete Search Term (GraphQL) Mutation: `deleteAdminMarketingSearchTerm`. --- # Search Term Detail (GraphQL) URL: /api/graphql-api/admin/marketing/search-seo/search-terms-detail --- outline: false examples: - id: gql title: Search Term Detail query: | query AdminTerm($id: ID!) { adminMarketingSearchTerm(id: $id) { id _id term uses results redirectUrl channelName locale } } variables: | { "id": "/api/admin/marketing/search-terms/1" } response: | { "data": { "adminMarketingSearchTerm": { "id": "/api/admin/marketing/search-terms/1", "_id": 1, "term": "red shirt", "uses": 142, "results": 23, "redirectUrl": null, "channelName": "Default", "locale": "en" } } } --- # Search Term Detail (GraphQL) Query: `adminMarketingSearchTerm(id:)`. --- # List Search Terms (GraphQL) URL: /api/graphql-api/admin/marketing/search-seo/search-terms-list --- outline: false examples: - id: gql title: List Search Terms query: | query AdminTerms($first: Int) { adminMarketingSearchTerms(first: $first) { edges { node { id _id term uses results channelName locale } } } } variables: | { "first": 10 } response: | { "data": { "adminMarketingSearchTerms": { "edges": [{ "node": { "id": "/api/admin/marketing/search-terms/1", "_id": 1, "term": "red shirt", "uses": 142, "results": 23, "channelName": "Default", "locale": "en" } }] } } } --- # List Search Terms (GraphQL) Query: `adminMarketingSearchTerms`. Extra args: `term`, `channel_id`, `locale`, `sort` (`id`/`term`/`uses`/`results`), `order`. ::: warning Auto-recorded Search terms are recorded automatically by storefront searches; there is no create mutation. ::: --- # Mass Delete Search Terms (GraphQL) URL: /api/graphql-api/admin/marketing/search-seo/search-terms-mass-delete --- outline: false examples: - id: gql title: Mass Delete Search Terms query: | mutation MassDelete($input: createAdminMarketingSearchTermMassDeleteInput!) { createAdminMarketingSearchTermMassDelete(input: $input) { adminMarketingSearchTermMassDelete { deleted message } } } variables: | { "input": { "indices": [12, 18] } } response: | { "data": { "createAdminMarketingSearchTermMassDelete": { "adminMarketingSearchTermMassDelete": { "deleted": [12, 18], "message": "Search terms deleted." } } } } --- # Mass Delete Search Terms (GraphQL) Mutation: `createAdminMarketingSearchTermMassDelete`. --- # Update Search Term (GraphQL) URL: /api/graphql-api/admin/marketing/search-seo/search-terms-update --- outline: false examples: - id: gql title: Update Search Term query: | mutation Update($input: updateAdminMarketingSearchTermInput!) { updateAdminMarketingSearchTerm(input: $input) { adminMarketingSearchTerm { id _id term redirectUrl } } } variables: | { "input": { "id": "/api/admin/marketing/search-terms/1", "term": "red shirt", "redirectUrl": "https://example.com/shirts" } } response: | { "data": { "updateAdminMarketingSearchTerm": { "adminMarketingSearchTerm": { "id": "/api/admin/marketing/search-terms/1", "_id": 1, "term": "red shirt", "redirectUrl": "https://example.com/shirts" } } } } --- # Update Search Term (GraphQL) Mutation: `updateAdminMarketingSearchTerm`. Only `term` + `redirect_url` are editable. --- # Create Sitemap (GraphQL) URL: /api/graphql-api/admin/marketing/search-seo/sitemaps-create --- outline: false examples: - id: gql title: Create Sitemap query: | mutation Create($input: createAdminMarketingSitemapInput!) { createAdminMarketingSitemap(input: $input) { adminMarketingSitemap { id _id fileName path } } } variables: | { "input": { "fileName": "sitemap.xml", "path": "/" } } response: | { "data": { "createAdminMarketingSitemap": { "adminMarketingSitemap": { "id": "/api/admin/marketing/sitemaps/4", "_id": 4, "fileName": "sitemap.xml", "path": "/" } } } } --- # Create Sitemap (GraphQL) Mutation: `createAdminMarketingSitemap`. ::: warning Not auto-generated Creating the row does NOT build the XML files. Call `createAdminMarketingSitemapGenerate` afterwards. ::: --- # Delete Sitemap (GraphQL) URL: /api/graphql-api/admin/marketing/search-seo/sitemaps-delete --- outline: false examples: - id: gql title: Delete Sitemap query: | mutation Delete($input: deleteAdminMarketingSitemapInput!) { deleteAdminMarketingSitemap(input: $input) { adminMarketingSitemap { id _id } } } variables: | { "input": { "id": "/api/admin/marketing/sitemaps/4" } } response: | { "data": { "deleteAdminMarketingSitemap": { "adminMarketingSitemap": { "id": "/api/admin/marketing/sitemaps/4", "_id": 4 } } } } --- # Delete Sitemap (GraphQL) Mutation: `deleteAdminMarketingSitemap`. Removes the DB row and generated XML files. --- # Sitemap Detail (GraphQL) URL: /api/graphql-api/admin/marketing/search-seo/sitemaps-detail --- outline: false examples: - id: gql title: Sitemap Detail query: | query AdminSitemap($id: ID!) { adminMarketingSitemap(id: $id) { id _id fileName path generatedAt indexFile generatedSitemaps } } variables: | { "id": "/api/admin/marketing/sitemaps/4" } response: | { "data": { "adminMarketingSitemap": { "id": "/api/admin/marketing/sitemaps/4", "_id": 4, "fileName": "sitemap.xml", "path": "/", "generatedAt": "2026-05-23T11:02:55+00:00", "indexFile": "/sitemap.xml", "generatedSitemaps": ["/sitemap-4-1.xml"] } } } --- # Sitemap Detail (GraphQL) Query: `adminMarketingSitemap(id:)`. --- # Regenerate Sitemap (GraphQL) URL: /api/graphql-api/admin/marketing/search-seo/sitemaps-generate --- outline: false examples: - id: gql title: Regenerate Sitemap query: | mutation Generate($input: createAdminMarketingSitemapGenerateInput!) { createAdminMarketingSitemapGenerate(input: $input) { adminMarketingSitemapGenerate { sitemapId indexFile generatedSitemaps generatedAt message } } } variables: | { "input": { "sitemapId": 4 } } response: | { "data": { "createAdminMarketingSitemapGenerate": { "adminMarketingSitemapGenerate": { "sitemapId": 4, "indexFile": "/sitemap.xml", "generatedSitemaps": ["/sitemap-4-1.xml"], "generatedAt": "2026-05-23T11:02:55+00:00", "message": "Sitemap regenerated successfully." } } } } --- # Regenerate Sitemap (GraphQL) Mutation: `createAdminMarketingSitemapGenerate`. ::: tip Synchronous generation Runs `Webkul\Sitemap\Jobs\ProcessSitemap` via `dispatchSync` — the response carries the generated paths once finished (not queued). ::: Permission: `marketing.search_seo.sitemaps.edit`. ::: tip Prerequisites The example uses an illustrative `id` value. Replace it with the id of a sitemap that exists in your store — use the [`adminMarketingSitemaps`](./sitemaps-list.md) query to discover valid ids. ::: --- # List Sitemaps (GraphQL) URL: /api/graphql-api/admin/marketing/search-seo/sitemaps-list --- outline: false examples: - id: gql title: List Sitemaps query: | query AdminSitemaps($first: Int) { adminMarketingSitemaps(first: $first) { edges { node { id _id fileName path generatedAt } } } } variables: | { "first": 10 } response: | { "data": { "adminMarketingSitemaps": { "edges": [{ "node": { "id": "/api/admin/marketing/sitemaps/4", "_id": 4, "fileName": "sitemap.xml", "path": "/", "generatedAt": "2026-05-23T11:02:55+00:00" } }] } } } --- # List Sitemaps (GraphQL) Query: `adminMarketingSitemaps`. Extra args: `file_name`, `sort`, `order`. --- # Update Sitemap (GraphQL) URL: /api/graphql-api/admin/marketing/search-seo/sitemaps-update --- outline: false examples: - id: gql title: Update Sitemap query: | mutation Update($input: updateAdminMarketingSitemapInput!) { updateAdminMarketingSitemap(input: $input) { adminMarketingSitemap { id _id fileName path } } } variables: | { "input": { "id": "/api/admin/marketing/sitemaps/4", "fileName": "sitemap-v2.xml" } } response: | { "data": { "updateAdminMarketingSitemap": { "adminMarketingSitemap": { "id": "/api/admin/marketing/sitemaps/4", "_id": 4, "fileName": "sitemap-v2.xml", "path": "/" } } } } --- # Update Sitemap (GraphQL) Mutation: `updateAdminMarketingSitemap`. ::: warning No auto-regeneration Updating fields does NOT regenerate the XML; call `createAdminMarketingSitemapGenerate`. ::: --- # Create URL Rewrite (GraphQL) URL: /api/graphql-api/admin/marketing/search-seo/url-rewrites-create --- outline: false examples: - id: gql title: Create URL Rewrite query: | mutation Create($input: createAdminMarketingUrlRewriteInput!) { createAdminMarketingUrlRewrite(input: $input) { adminMarketingUrlRewrite { id _id entityType requestPath targetPath redirectType locale } } } variables: | { "input": { "entityType": "product", "requestPath": "old-path", "targetPath": "new-path", "redirectType": "301", "locale": "en" } } response: | { "data": { "createAdminMarketingUrlRewrite": { "adminMarketingUrlRewrite": { "id": "/api/admin/marketing/url-rewrites/1", "_id": 1, "entityType": "product", "requestPath": "old-path", "targetPath": "new-path", "redirectType": "301", "locale": "en" } } } } --- # Create URL Rewrite (GraphQL) Mutation: `createAdminMarketingUrlRewrite`. --- # Delete URL Rewrite (GraphQL) URL: /api/graphql-api/admin/marketing/search-seo/url-rewrites-delete --- outline: false examples: - id: gql title: Delete URL Rewrite query: | mutation Delete($input: deleteAdminMarketingUrlRewriteInput!) { deleteAdminMarketingUrlRewrite(input: $input) { adminMarketingUrlRewrite { id _id } } } variables: | { "input": { "id": "/api/admin/marketing/url-rewrites/1" } } response: | { "data": { "deleteAdminMarketingUrlRewrite": { "adminMarketingUrlRewrite": { "id": "/api/admin/marketing/url-rewrites/1", "_id": 1 } } } } --- # Delete URL Rewrite (GraphQL) Mutation: `deleteAdminMarketingUrlRewrite`. ::: tip Prerequisites The example uses an illustrative `id` value. Replace it with the id of a URL rewrite that exists in your store — use the [`adminMarketingUrlRewrites`](./url-rewrites-list.md) query to discover valid ids. ::: --- # URL Rewrite Detail (GraphQL) URL: /api/graphql-api/admin/marketing/search-seo/url-rewrites-detail --- outline: false examples: - id: gql title: URL Rewrite Detail query: | query AdminRewrite($id: ID!) { adminMarketingUrlRewrite(id: $id) { id _id entityType requestPath targetPath redirectType locale } } variables: | { "id": "/api/admin/marketing/url-rewrites/1" } response: | { "data": { "adminMarketingUrlRewrite": { "id": "/api/admin/marketing/url-rewrites/1", "_id": 1, "entityType": "product", "requestPath": "old-path", "targetPath": "new-path", "redirectType": "301", "locale": "en" } } } --- # URL Rewrite Detail (GraphQL) Query: `adminMarketingUrlRewrite(id:)`. --- # List URL Rewrites (GraphQL) URL: /api/graphql-api/admin/marketing/search-seo/url-rewrites-list --- outline: false examples: - id: gql title: List URL Rewrites query: | query AdminRewrites($first: Int) { adminMarketingUrlRewrites(first: $first) { edges { node { id _id entityType requestPath targetPath redirectType locale } } } } variables: | { "first": 10 } response: | { "data": { "adminMarketingUrlRewrites": { "edges": [{ "node": { "id": "/api/admin/marketing/url-rewrites/1", "_id": 1, "entityType": "product", "requestPath": "old-path", "targetPath": "new-path", "redirectType": "301", "locale": "en" } }] } } } --- # List URL Rewrites (GraphQL) Query: `adminMarketingUrlRewrites`. Extra args: `entity_type`, `request_path`, `redirect_type`, `locale`, `sort`, `order`. --- # Mass Delete URL Rewrites (GraphQL) URL: /api/graphql-api/admin/marketing/search-seo/url-rewrites-mass-delete --- outline: false examples: - id: gql title: Mass Delete URL Rewrites query: | mutation MassDelete($input: createAdminMarketingUrlRewriteMassDeleteInput!) { createAdminMarketingUrlRewriteMassDelete(input: $input) { adminMarketingUrlRewriteMassDelete { deleted message } } } variables: | { "input": { "indices": [12, 18] } } response: | { "data": { "createAdminMarketingUrlRewriteMassDelete": { "adminMarketingUrlRewriteMassDelete": { "deleted": [12, 18], "message": "URL rewrites deleted." } } } } --- # Mass Delete URL Rewrites (GraphQL) Mutation: `createAdminMarketingUrlRewriteMassDelete`. --- # Update URL Rewrite (GraphQL) URL: /api/graphql-api/admin/marketing/search-seo/url-rewrites-update --- outline: false examples: - id: gql title: Update URL Rewrite query: | mutation Update($input: updateAdminMarketingUrlRewriteInput!) { updateAdminMarketingUrlRewrite(input: $input) { adminMarketingUrlRewrite { id _id targetPath redirectType } } } variables: | { "input": { "id": "/api/admin/marketing/url-rewrites/1", "targetPath": "newer-path", "redirectType": "302" } } response: | { "data": { "updateAdminMarketingUrlRewrite": { "adminMarketingUrlRewrite": { "id": "/api/admin/marketing/url-rewrites/1", "_id": 1, "targetPath": "newer-path", "redirectType": "302" } } } } --- # Update URL Rewrite (GraphQL) Mutation: `updateAdminMarketingUrlRewrite`. --- # Create Product URL: /api/graphql-api/admin/mutations/create-product --- outline: false examples: - id: create-simple-product title: Create Simple Product description: Create a new simple product with basic information. query: | # This page documents a legacy Shop GraphQL stub; the actual Admin API # query is `createAdminCatalogProduct`. See: ./catalog/products/create.md variables: | {} response: | {} --- # Create Product ## About The `createProduct` mutation creates a new product in your store catalog. Use this mutation to: - Add new products programmatically - Build product creation workflows - Integrate external catalog feeds - Automate product onboarding - Create products from bulk imports - Add products via API integrations - Manage product data synchronization This mutation validates all product data, creates inventory records, and applies category/attribute assignments. ## Arguments | Argument | Type | Description | |----------|------|-------------| | `input` | `CreateProductInput!` | Product data object containing all product details. | | `input.name` | `String!` | Product name/title. | | `input.sku` | `String!` | Unique product SKU for inventory tracking. | | `input.type` | `String!` | Product type: `simple`, `configurable`, `grouped`, `bundle`. | | `input.price` | `Float!` | Base product price. | | `input.cost` | `Float` | Cost of goods for margin calculations. | | `input.status` | `String` | Initial status: `active`, `inactive`, `draft`. Default: `active` | | `input.description` | `String` | Full product description. | | `input.shortDescription` | `String` | Brief product summary. | | `input.weight` | `Float` | Product weight. | | `input.categories` | `[ID!]` | Category IDs to assign product to. | | `input.attributes` | `[AttributeInput!]` | Custom product attributes and values. | | `input.images` | `[ImageInput!]` | Product images. | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `product` | `Product!` | The newly created product object. | | `product.id` | `ID!` | New product ID. | | `product.name` | `String!` | Product name. | | `product.sku` | `String!` | Product SKU. | | `product.type` | `String!` | Product type. | | `product.price` | `Float!` | Product price. | | `product.status` | `String!` | Product status. | | `message` | `String!` | Success message. | | `success` | `Boolean!` | Indicates successful creation. | | `errors` | `[ErrorMessage!]` | Validation errors if creation failed. | --- # Delete Product URL: /api/graphql-api/admin/mutations/delete-product --- outline: false examples: - id: delete-product-simple title: Delete Product description: Delete a product from the system. query: | # This page documents a legacy Shop GraphQL stub; the actual Admin API # query is `deleteAdminCatalogProduct`. See: ./catalog/products/delete.md variables: | {} response: | {} --- # Delete Product ## About The `deleteProduct` mutation removes a product from your store catalog. Use this mutation to: - Remove discontinued or unwanted products - Clean up test/draft products - Manage product lifecycle - Delete products via API integrations - Bulk remove products from external feeds - Maintain accurate product catalogs This mutation performs validation checks (e.g., product has no active orders) before deletion to maintain data integrity. ## Arguments | Argument | Type | Description | |----------|------|-------------| | `id` | `ID!` | The product ID to delete. | | `force` | `Boolean` | Force deletion even if product has related data. Default: `false` | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `message` | `String!` | Success or error message. | | `success` | `Boolean!` | Indicates successful deletion. | | `deletedProductId` | `ID` | ID of deleted product. | | `errors` | `[ErrorMessage!]` | Validation errors if deletion failed. | | `warnings` | `[String!]` | Warnings about related data (e.g., orphaned reviews). | --- # Update Product URL: /api/graphql-api/admin/mutations/update-product --- outline: false examples: - id: update-product-price title: Update Product Price description: Update the price of an existing product. query: | # This page documents a legacy Shop GraphQL stub; the actual Admin API # query is `updateAdminCatalogProduct`. See: ./catalog/products/update.md variables: | {} response: | {} --- # Update Product ## About The `updateProduct` mutation modifies an existing product's information. Use this mutation to: - Update product prices and costs - Modify product descriptions and details - Change product status or visibility - Update inventory settings - Modify product attributes - Update category assignments - Sync product changes from external systems This mutation validates all product data and applies updates while maintaining inventory and category relationships. ## Arguments | Argument | Type | Description | |----------|------|-------------| | `id` | `ID!` | The product ID to update. | | `input` | `UpdateProductInput!` | Product data fields to update. | | `input.name` | `String` | Updated product name. | | `input.price` | `Float` | Updated base price. | | `input.cost` | `Float` | Updated cost of goods. | | `input.status` | `String` | New product status. | | `input.description` | `String` | Updated full description. | | `input.shortDescription` | `String` | Updated brief summary. | | `input.weight` | `Float` | Updated product weight. | | `input.categories` | `[ID!]` | Updated category assignments. | | `input.attributes` | `[AttributeInput!]` | Updated attribute values. | | `input.images` | `[ImageInput!]` | Updated product images. | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `product` | `Product!` | The updated product object. | | `product.id` | `ID!` | Product ID. | | `product.name` | `String!` | Product name. | | `product.sku` | `String!` | Product SKU (immutable). | | `product.price` | `Float!` | Updated price. | | `product.status` | `String!` | Current status. | | `product.updatedAt` | `DateTime!` | Update timestamp. | | `message` | `String!` | Success message. | | `success` | `Boolean!` | Indicates successful update. | | `errors` | `[ErrorMessage!]` | Validation errors if update failed. | --- # Orders URL: /api/graphql-api/admin/orders --- outline: false examples: - id: get-all-orders title: Get All Orders description: Retrieve all orders from the admin panel with pagination. query: | # This page documents a legacy Shop GraphQL stub; the actual Admin API # query is `adminOrders`. See: ./sales/orders/list.md variables: | {} response: | {} --- # Orders ## About The `orders` admin query retrieves comprehensive order information for administrative management and fulfillment. Use this query to: - Display order management dashboards - Build order lists with filtering and search - Retrieve order details and history - Track order status and fulfillment - Export orders for analysis and reporting - Sync orders with external fulfillment systems - Monitor revenue and sales metrics This query provides complete order metadata including customer info, items, pricing, payment status, and fulfillment details needed for order processing and management. ## Arguments | Argument | Type | Description | |----------|------|-------------| | `first` | `Int` | Orders per page (max: 100). Default: 20. | | `after` | `String` | Pagination cursor for forward pagination. | | `last` | `Int` | Orders for backward pagination (max: 100). | | `before` | `String` | Pagination cursor for backward pagination. | | `status` | `[OrderStatus!]` | Filter by: `PENDING`, `CONFIRMED`, `SHIPPED`, `DELIVERED`, `CANCELLED`. | | `sortKey` | `OrderSortKeys` | Sort by: `ID`, `ORDER_NUMBER`, `CREATED_AT`, `TOTAL`. Default: `CREATED_AT` | | `reverse` | `Boolean` | Reverse sort order. Default: `false` | | `dateFrom` | `DateTime` | Filter orders created after this date. | | `dateTo` | `DateTime` | Filter orders created before this date. | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `edges` | `[OrderEdge!]!` | Order edges with pagination cursors. | | `edges.node` | `Order!` | Order object. | | `edges.node.id` | `ID!` | Order ID. | | `edges.node.orderNumber` | `String!` | Order number/reference. | | `edges.node.status` | `String!` | Current order status. | | `edges.node.customerId` | `ID` | Customer ID. | | `edges.node.customerEmail` | `String!` | Customer email. | | `edges.node.items` | `[OrderItem!]!` | Line items in order. | | `edges.node.subTotal` | `Float!` | Subtotal before tax and shipping. | | `edges.node.taxAmount` | `Float!` | Tax amount. | | `edges.node.shippingAmount` | `Float!` | Shipping cost. | | `edges.node.discountAmount` | `Float` | Discount applied. | | `edges.node.total` | `Float!` | Order grand total. | | `edges.node.paymentStatus` | `String!` | Payment status. | | `edges.node.fulfillmentStatus` | `String!` | Fulfillment status. | | `edges.node.createdAt` | `DateTime!` | Order creation date. | | `edges.node.updatedAt` | `DateTime!` | Last update. | | `nodes` | `[Order!]!` | Flattened order array. | | `pageInfo` | `PageInfo!` | Pagination metadata. | | `totalCount` | `Int!` | Total orders matching filters. | --- # Products URL: /api/graphql-api/admin/products --- outline: false examples: - id: get-all-admin-products title: Get All Products (Admin) description: Retrieve all products from the admin panel with pagination. query: | query getProducts($first: Int, $after: String) { products(first: $first, after: $after) { edges { node { id name sku type status price } } pageInfo { hasNextPage endCursor } } } variables: | { "first": 10 } response: | { "data": { "products": { "edges": [ { "node": { "id": "1", "name": "Product Name", "sku": "PROD-001", "type": "simple", "status": "active", "price": 99.99 } } ], "pageInfo": { "hasNextPage": true, "endCursor": "YXJyYXljb25uZWN0aW9uOjEw" } } } } commonErrors: - error: UNAUTHORIZED cause: Admin authentication required solution: Provide valid admin credentials --- # Products ## About The `products` admin query retrieves a comprehensive list of all products in your store with full administrative details. Use this query to: - Display product management tables and listings - Build product inventory dashboards - Export product data for external systems - Filter and search products by various criteria - Manage product catalogs programmatically - Sync product data with inventory systems - Generate product reports This query returns all product metadata including pricing, inventory, status, and administrative flags needed for store management. ## Arguments | Argument | Type | Description | |----------|------|-------------| | `first` | `Int` | Number of products per page (max: 250). Default: 20. | | `after` | `String` | Cursor for forward pagination. | | `last` | `Int` | Number of products for backward pagination (max: 250). | | `before` | `String` | Cursor for backward pagination. | | `status` | `[ProductStatus!]` | Filter by status: `ACTIVE`, `INACTIVE`, `DRAFT`. | | `type` | `[ProductType!]` | Filter by product type: `simple`, `configurable`, `grouped`, `bundle`. | | `sortKey` | `ProductSortKeys` | Sort by: `ID`, `TITLE`, `CREATED_AT`, `UPDATED_AT`. Default: `ID` | | `reverse` | `Boolean` | Reverse sort order. Default: `false` | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `edges` | `[ProductEdge!]!` | Array of product edges with pagination cursors. | | `edges.node` | `Product!` | Complete product object. | | `edges.node.id` | `ID!` | Product ID. | | `edges.node.name` | `String!` | Product name. | | `edges.node.sku` | `String!` | Product SKU. | | `edges.node.type` | `String!` | Product type. | | `edges.node.status` | `String!` | Status (ACTIVE, INACTIVE, DRAFT). | | `edges.node.price` | `Float!` | Base product price. | | `edges.node.cost` | `Float` | Cost of goods (for margin calculations). | | `edges.node.inventory` | `InventoryInfo!` | Stock levels and status. | | `edges.node.createdAt` | `DateTime!` | Creation timestamp. | | `edges.node.updatedAt` | `DateTime!` | Last modification timestamp. | | `nodes` | `[Product!]!` | Flattened product array. | | `pageInfo` | `PageInfo!` | Pagination metadata. | | `totalCount` | `Int!` | Total products in store. | --- # Get Admin Profile URL: /api/graphql-api/admin/profile/get-profile --- outline: false examples: - id: admin-get-profile title: Get Admin Profile description: Read the authenticated admin's profile. Requires the Bearer token in the Authorization header. query: | query readAdminProfile { readAdminProfile { id name email image status roleId roleName success } } variables: | {} response: | { "data": { "readAdminProfile": { "id": "1", "name": "Example Admin", "email": "admin@example.com", "image": null, "status": "1", "roleId": 1, "roleName": "Administrator", "success": true } } } --- # Get Admin Profile Read the profile of the currently authenticated admin. ## Operation | Operation | Type | Purpose | |-----------|------|---------| | `readAdminProfile` | Query | Return the authenticated admin's profile | ## Details - Requires a valid admin Bearer token in the `Authorization` header. - Returns the admin's `id`, `name`, `email`, `image`, `status`, and role (`roleId` / `roleName`). - An unauthenticated request returns a GraphQL error and `null` data. ## Examples Use the interactive example on the right to run the query. --- # Promotions URL: /api/graphql-api/admin/promotions --- outline: false examples: - id: get-all-promotions title: Get All Promotions description: Retrieve all active promotions from the admin panel. query: | # This page documents a legacy Shop GraphQL stub; the actual Admin API # query is `adminMarketingCartRules`. See: ./marketing/promotions/cart-rules-list.md variables: | {} response: | {} --- # Promotions ## About The `promotions` admin query retrieves promotion and discount rule configurations. Use this query to: - Display promotion management interfaces - Build promotion listings and dashboards - Retrieve active and scheduled promotions - Analyze promotion performance and ROI - Export promotion rules for analysis - Manage promotional calendars - Monitor discount usage and impact This query provides complete promotion metadata including conditions, discount amounts, date ranges, and application rules for promotional management and analytics. ## Arguments | Argument | Type | Description | |----------|------|-------------| | `first` | `Int` | Promotions per page (max: 100). Default: 20. | | `after` | `String` | Pagination cursor for forward pagination. | | `last` | `Int` | Promotions for backward pagination (max: 100). | | `before` | `String` | Pagination cursor for backward pagination. | | `status` | `[PromotionStatus!]` | Filter by: `ACTIVE`, `INACTIVE`, `SCHEDULED`, `EXPIRED`. | | `sortKey` | `PromotionSortKeys` | Sort by: `ID`, `NAME`, `START_DATE`, `DISCOUNT`. Default: `START_DATE` | | `type` | `[PromotionType!]` | Filter by type: `CATALOG_RULE`, `CART_RULE`, `COUPON`. | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `edges` | `[PromotionEdge!]!` | Promotion edges with pagination. | | `edges.node` | `Promotion!` | Promotion object. | | `edges.node.id` | `ID!` | Promotion ID. | | `edges.node.name` | `String!` | Promotion name. | | `edges.node.description` | `String` | Promotion description. | | `edges.node.type` | `String!` | Type: `CATALOG_RULE`, `CART_RULE`, `COUPON`. | | `edges.node.status` | `String!` | Status: `ACTIVE`, `INACTIVE`, `SCHEDULED`, `EXPIRED`. | | `edges.node.startDate` | `DateTime!` | Promotion start date. | | `edges.node.endDate` | `DateTime` | Promotion end date. | | `edges.node.priority` | `Int!` | Execution priority (higher = executed first). | | `edges.node.discountType` | `String!` | Discount type: `FIXED`, `PERCENTAGE`, `BOGO`. | | `edges.node.discountAmount` | `Float!` | Discount value. | | `edges.node.discountPercentage` | `Float` | Percentage discount. | | `edges.node.conditions` | `[PromotionCondition!]!` | Promotion conditions and rules. | | `edges.node.appliedProductCount` | `Int!` | Number of products affected. | | `edges.node.totalRedemptions` | `Int!` | Total times promotion was used. | | `edges.node.averageDiscount` | `Float!` | Average discount value per transaction. | | `nodes` | `[Promotion!]!` | Flattened promotion array. | | `pageInfo` | `PageInfo!` | Pagination metadata. | | `totalCount` | `Int!` | Total promotions. | --- # Reporting — Customers (GraphQL) URL: /api/graphql-api/admin/reporting/customers --- outline: false examples: - id: gql title: Reporting — Customers query: | query AdminReportingCustomers($type: String, $start: String, $end: String, $channel: String) { statsAdminReportingCustomers(type: $type, start: $start, end: $end, channel: $channel) { entity type dateRange statistics } } variables: | { "type": "total-customers" } response: | { "data": { "statsAdminReportingCustomers": { "entity": "customers", "type": "total-customers", "dateRange": { "previous": "10 Apr 2026 - 10 May 2026", "current": "10 May 2026 - 09 Jun 2026" }, "statistics": { "customers": { "previous": 1, "current": 9, "progress": 800 }, "over_time": { "previous": [ { "label": "23 May", "total": 1 } ], "current": [ { "label": "26 May", "total": 9 } ] } } } } } - id: gql-filtered title: Reporting — Customers (Filtered by date + channel) query: | query AdminReportingCustomers($type: String, $start: String, $end: String, $channel: String) { statsAdminReportingCustomers(type: $type, start: $start, end: $end, channel: $channel) { entity type dateRange statistics } } variables: | { "type": "total-customers", "start": "2026-05-10", "end": "2026-06-09", "channel": "default" } response: | { "data": { "statsAdminReportingCustomers": { "entity": "customers", "type": "total-customers", "dateRange": { "previous": "10 Apr 2026 - 10 May 2026", "current": "10 May 2026 - 09 Jun 2026" }, "statistics": { "customers": { "previous": 1, "current": 9, "progress": 800 }, "over_time": { "previous": [ { "label": "23 May", "total": 1 } ], "current": [ { "label": "26 May", "total": 9 } ] } } } } } - id: gql-view title: Reporting — Customers (View Details) query: | query AdminReportingCustomersView($type: String, $start: String, $end: String, $channel: String) { viewStatsAdminReportingCustomers(type: $type, start: $start, end: $end, channel: $channel) { entity type dateRange statistics } } variables: | { "type": "customers-with-most-sales" } response: | { "data": { "viewStatsAdminReportingCustomers": { "entity": "customers", "type": "customers-with-most-sales", "dateRange": { "previous": "25 Mar - 24 Apr", "current": "25 Apr - 25 May" }, "statistics": { "columns": [{ "key": "name", "label": "Customer" }, { "key": "email", "label": "Email" }, { "key": "total", "label": "Total Sales" }], "records": [{ "name": "Jane Cooper", "email": "jane@example.com", "total": "$4,820.00" }, { "name": "Devon Lane", "email": "devon@example.com", "total": "$3,150.50" }] } } } } --- # Reporting — Customers (GraphQL) Query: `statsAdminReportingCustomers`. `type` values: `total-customers` (default), `customers-traffic`, `customers-with-most-sales`, `customers-with-most-orders`, `customers-with-most-reviews`, `top-customer-groups`. ## View Details `viewStatsAdminReportingCustomers` is the detailed table form of the matching `statsAdminReportingCustomers` query — its `statistics` carries `columns` (`{ key, label }`) and `records` (the row data behind a panel's **View Details** link), rather than the rolled-up headline figures. The CSV **Export** is REST only (a binary `text/csv` download); there is no GraphQL equivalent. --- # Reporting — Overview (GraphQL) URL: /api/graphql-api/admin/reporting/overview --- outline: false examples: - id: gql title: Reporting Overview query: | query AdminReportingOverview($type: String, $start: String, $end: String, $channel: String) { statsAdminReportingOverview(type: $type, start: $start, end: $end, channel: $channel) { entity type dateRange statistics } } variables: | { "type": "total-sales" } response: | { "data": { "statsAdminReportingOverview": { "entity": "overview", "type": "total-sales", "dateRange": { "previous": "10 Apr 2026 - 10 May 2026", "current": "10 May 2026 - 09 Jun 2026" }, "statistics": { "sales": { "previous": 27243.5, "current": 9697.53, "formatted_total": "$9,697.53", "progress": -64.32 }, "over_time": { "previous": [ { "label": "10 Apr", "total": 4200, "count": 6 } ], "current": [ { "label": "10 May", "total": 8500, "count": 12 } ] } } } } } - id: gql-filtered title: Reporting Overview (Filtered by date + channel) query: | query AdminReportingOverview($type: String, $start: String, $end: String, $channel: String) { statsAdminReportingOverview(type: $type, start: $start, end: $end, channel: $channel) { entity type dateRange statistics } } variables: | { "type": "total-sales", "start": "2026-05-10", "end": "2026-06-09", "channel": "default" } response: | { "data": { "statsAdminReportingOverview": { "entity": "overview", "type": "total-sales", "dateRange": { "previous": "10 Apr 2026 - 10 May 2026", "current": "10 May 2026 - 09 Jun 2026" }, "statistics": { "sales": { "previous": 27243.5, "current": 9697.53, "formatted_total": "$9,697.53", "progress": -64.32 }, "over_time": { "previous": [ { "label": "10 Apr", "total": 4200, "count": 6 } ], "current": [ { "label": "10 May", "total": 8500, "count": 12 } ] } } } } } --- # Reporting — Overview (GraphQL) Query: `statsAdminReportingOverview`. The Overview query returns a single headline figure across the whole store for the chosen `type`. It is an API convenience aggregation — there is **no** matching "Overview" screen in the admin panel (the admin Reporting menu goes straight to Sales / Customers / Products). Use it to fetch one top-line number without having to call the per-section queries. The `type` argument picks which headline you get back: - `total-sales` (default) — total revenue for the period. - `total-orders` — number of orders placed. - `total-customers` — number of new customers. - `top-selling-products-by-revenue` — the best-selling product by revenue. The `statistics` object carries a previous-vs-current comparison plus a `progress` percentage (how the current window compares to the previous one), and for time-based types an `over_time` series of per-day data points for charting. `dateRange` reports the two comparison windows — `current` is the window you asked for, `previous` is the equal-length window immediately before it. ## Arguments | Arg | Type | Notes | |-----|------|-------| | `type` | String | `total-sales` (default), `total-orders`, `total-customers`, `top-selling-products-by-revenue`. | | `start` | String | Start of the reporting window (`YYYY-MM-DD`). | | `end` | String | End of the reporting window (`YYYY-MM-DD`). | | `channel` | String | Channel code (e.g. `default`) — scopes the figures to one storefront channel. | Unlike the Sales / Customers / Products queries, the Overview query has **no View Details and no Export** — it is a top-line summary only. Reporting requires only authentication; there is no permission gate. --- # Reporting — Products (GraphQL) URL: /api/graphql-api/admin/reporting/products --- outline: false examples: - id: gql title: Reporting — Products query: | query AdminReportingProducts($type: String, $start: String, $end: String, $channel: String) { statsAdminReportingProducts(type: $type, start: $start, end: $end, channel: $channel) { entity type dateRange statistics } } variables: | { "type": "total-sold-quantities" } response: | { "data": { "statsAdminReportingProducts": { "entity": "products", "type": "total-sold-quantities", "dateRange": { "previous": "10 Apr 2026 - 10 May 2026", "current": "10 May 2026 - 09 Jun 2026" }, "statistics": { "quantities": { "previous": 82, "current": 13, "progress": -84.15 }, "over_time": { "previous": [ { "label": "10 Apr", "total": 21 } ], "current": [ { "label": "10 May", "total": 5 } ] } } } } } - id: gql-filtered title: Reporting — Products (Filtered by date + channel) query: | query AdminReportingProducts($type: String, $start: String, $end: String, $channel: String) { statsAdminReportingProducts(type: $type, start: $start, end: $end, channel: $channel) { entity type dateRange statistics } } variables: | { "type": "total-sold-quantities", "start": "2026-05-10", "end": "2026-06-09", "channel": "default" } response: | { "data": { "statsAdminReportingProducts": { "entity": "products", "type": "total-sold-quantities", "dateRange": { "previous": "10 Apr 2026 - 10 May 2026", "current": "10 May 2026 - 09 Jun 2026" }, "statistics": { "quantities": { "previous": 82, "current": 13, "progress": -84.15 }, "over_time": { "previous": [ { "label": "10 Apr", "total": 21 } ], "current": [ { "label": "10 May", "total": 5 } ] } } } } } - id: gql-view title: Reporting — Products (View Details) query: | query AdminReportingProductsView($type: String, $start: String, $end: String, $channel: String) { viewStatsAdminReportingProducts(type: $type, start: $start, end: $end, channel: $channel) { entity type dateRange statistics } } variables: | { "type": "top-selling-products-by-revenue" } response: | { "data": { "viewStatsAdminReportingProducts": { "entity": "products", "type": "top-selling-products-by-revenue", "dateRange": { "previous": "25 Mar - 24 Apr", "current": "25 Apr - 25 May" }, "statistics": { "columns": [{ "key": "name", "label": "Product" }, { "key": "sku", "label": "SKU" }, { "key": "revenue", "label": "Revenue" }], "records": [{ "name": "Wireless Headphones", "sku": "WH-100", "revenue": "$6,420.00" }, { "name": "Cotton T-Shirt", "sku": "CT-220", "revenue": "$3,980.50" }] } } } } --- # Reporting — Products (GraphQL) Query: `statsAdminReportingProducts`. `type` values: `total-sold-quantities` (default), `total-products-added-to-wishlist`, `top-selling-products-by-revenue`, `top-selling-products-by-quantity`, `products-with-most-reviews`, `products-with-most-visits`, `last-search-terms`, `top-search-terms`. ## View Details `viewStatsAdminReportingProducts` is the detailed table form of the matching `statsAdminReportingProducts` query — its `statistics` carries `columns` (`{ key, label }`) and `records` (the row data behind a panel's **View Details** link), rather than the rolled-up headline figures. The CSV **Export** is REST only (a binary `text/csv` download); there is no GraphQL equivalent. --- # Reporting — Sales (GraphQL) URL: /api/graphql-api/admin/reporting/sales --- outline: false examples: - id: gql title: Reporting — Sales query: | query AdminReportingSales($type: String, $start: String, $end: String, $channel: String) { statsAdminReportingSales(type: $type, start: $start, end: $end, channel: $channel) { entity type dateRange statistics } } variables: | { "type": "total-sales" } response: | { "data": { "statsAdminReportingSales": { "entity": "sales", "type": "total-sales", "dateRange": { "previous": "10 Apr 2026 - 10 May 2026", "current": "10 May 2026 - 09 Jun 2026" }, "statistics": { "sales": { "previous": 27243.5, "current": 9697.53, "formatted_total": "$9,697.53", "progress": -64.32 }, "over_time": { "previous": [ { "label": "10 Apr", "total": 4200, "count": 6 } ], "current": [ { "label": "10 May", "total": 8500, "count": 12 } ] } } } } } - id: gql-filtered title: Reporting — Sales (Filtered by date + channel) query: | query AdminReportingSales($type: String, $start: String, $end: String, $channel: String) { statsAdminReportingSales(type: $type, start: $start, end: $end, channel: $channel) { entity type dateRange statistics } } variables: | { "type": "total-sales", "start": "2026-05-10", "end": "2026-06-09", "channel": "default" } response: | { "data": { "statsAdminReportingSales": { "entity": "sales", "type": "total-sales", "dateRange": { "previous": "10 Apr 2026 - 10 May 2026", "current": "10 May 2026 - 09 Jun 2026" }, "statistics": { "sales": { "previous": 27243.5, "current": 9697.53, "formatted_total": "$9,697.53", "progress": -64.32 }, "over_time": { "previous": [ { "label": "10 Apr", "total": 4200, "count": 6 } ], "current": [ { "label": "10 May", "total": 8500, "count": 12 } ] } } } } } - id: gql-view title: Reporting — Sales (View Details) query: | query AdminReportingSalesView($type: String, $start: String, $end: String, $channel: String) { viewStatsAdminReportingSales(type: $type, start: $start, end: $end, channel: $channel) { entity type dateRange statistics } } variables: | { "type": "total-sales" } response: | { "data": { "viewStatsAdminReportingSales": { "entity": "sales", "type": "total-sales", "dateRange": { "previous": "25 Mar - 24 Apr", "current": "25 Apr - 25 May" }, "statistics": { "columns": [{ "key": "date", "label": "Date" }, { "key": "total", "label": "Total" }], "records": [{ "date": "2026-04-25", "total": "$1,240.00" }, { "date": "2026-04-26", "total": "$980.50" }] } } } } --- # Reporting — Sales (GraphQL) Query: `statsAdminReportingSales`. `type` values: `total-sales` (default), `average-sales`, `total-orders`, `purchase-funnel`, `abandoned-carts`, `refunds`, `tax-collected`, `shipping-collected`, `top-payment-methods`. ## View Details `viewStatsAdminReportingSales` is the detailed table form of the matching `statsAdminReportingSales` query — its `statistics` carries `columns` (`{ key, label }`) and `records` (the row data behind a panel's **View Details** link), rather than the rolled-up headline figures. The CSV **Export** is REST only (a binary `text/csv` download); there is no GraphQL equivalent. --- # Reports URL: /api/graphql-api/admin/reports --- outline: false examples: - id: get-sales-report title: Get Sales Report description: Retrieve sales statistics and report data. query: | # This page documents a legacy Shop GraphQL stub; the actual Admin API # query is `statsAdminReportingSales`. See: ./reporting/sales.md variables: | {} response: | {} --- # Reports ## About The `reports` admin query retrieves business analytics and reporting data. Use this query to: - Display sales dashboards and KPI metrics - Generate revenue and order reports - Analyze customer acquisition and behavior - Track product performance metrics - Monitor payment and shipping methods - Generate time-based sales trends - Export report data for analysis - Build custom analytics dashboards This query provides aggregated business metrics needed for financial reporting, performance analysis, and business intelligence. ## Arguments | Argument | Type | Description | |----------|------|-------------| | `startDate` | `DateTime` | Report start date (inclusive). Format: ISO 8601. | | `endDate` | `DateTime` | Report end date (inclusive). Format: ISO 8601. | | `granularity` | `String` | Report granularity: `DAILY`, `WEEKLY`, `MONTHLY`, `YEARLY`. Default: `DAILY` | | `reportType` | `[ReportType!]` | Types: `SALES`, `CUSTOMERS`, `PRODUCTS`, `PAYMENTS`, `SHIPPING`. | | `groupBy` | `String` | Group results by: `DAY`, `WEEK`, `MONTH`, `CATEGORY`, `PRODUCT`, `PAYMENT_METHOD`. | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `totalSales` | `Float!` | Total revenue for the period. | | `totalOrders` | `Int!` | Total orders placed. | | `totalCustomers` | `Int!` | Total unique customers. | | `averageOrderValue` | `Float!` | Average order value (total sales / total orders). | | `newCustomers` | `Int!` | New customers acquired in period. | | `repeatCustomers` | `Int!` | Returning customers. | | `repeatOrderRate` | `Float!` | Percentage of repeat orders. | | `topProducts` | `[ProductMetric!]!` | Best-selling products. | | `topProducts.productId` | `ID!` | Product ID. | | `topProducts.productName` | `String!` | Product name. | | `topProducts.unitsSold` | `Int!` | Quantity sold. | | `topProducts.revenue` | `Float!` | Revenue from product. | | `topCategories` | `[CategoryMetric!]!` | Revenue by category. | | `paymentMethods` | `[PaymentMetric!]!` | Revenue by payment method. | | `shippingMethods` | `[ShippingMetric!]!` | Shipping method usage. | | `trends` | `[TrendData!]!` | Time-series trend data. | | `trends.date` | `DateTime!` | Data point date. | | `trends.sales` | `Float!` | Sales for this period. | | `trends.orders` | `Int!` | Orders for this period. | | `trends.customers` | `Int!` | Customers for this period. | | `currencyCode` | `String!` | Currency of monetary amounts. | | `generatedAt` | `DateTime!` | Report generation timestamp. | --- # Sales URL: /api/graphql-api/admin/sales --- outline: false --- # Sales The Sales section covers everything about an order after (and during) checkout — browsing and managing orders, building an order from the admin side, and the post-order documents an order produces. ## Menus | Menu | What it's for | |------|----------------| | [Orders](/api/graphql-api/admin/sales/orders/) | Browse orders and run every per-order action — view, reorder, place, cancel, comment, and generate invoices / shipments / refunds. Admins can also **build an order for a customer** from here. | | [Invoices](/api/graphql-api/admin/sales/invoices/) | Store-wide list of generated invoices, plus send-duplicate and bulk status update. | | [Shipments](/api/graphql-api/admin/sales/shipments/) | Store-wide list of shipments created against orders. | | [Refunds](/api/graphql-api/admin/sales/refunds/) | Store-wide list of refunds issued against orders. | | [Transactions](/api/graphql-api/admin/sales/transactions/list) | Payment transactions recorded against orders and invoices. | | [Bookings](/api/graphql-api/admin/sales/bookings/) | Booking lines produced by orders that contain a booking product. | Invoices, shipments, refunds, transactions, and bookings are **documents an order produces** — a row appears in those menus only once the corresponding document exists for an order. All Sales operations require an admin Bearer token — see [Authentication](/api/graphql-api/admin/authentication). --- # Bookings URL: /api/graphql-api/admin/sales/bookings --- outline: false --- # Bookings The Bookings menu is a read-only list of booking lines produced by orders that contain a booking product. It mirrors the admin **Sales → Bookings** screen. This menu is **read-only** — list and detail only; booking rows are created automatically when an order is placed and cannot be created or edited through the API. ## When a row appears here A row appears whenever an order is placed that contains a booking product — each booked line shows here. A booking row carries the booked quantity, the from/to time window, and a summary of the order and order item it belongs to. ## Booking sub-types The `bookingType` field tells you what kind of booking the line is — it comes from the booked product's configuration: | `bookingType` | Meaning | |---------------|---------| | `default` | A simple bookable product with a fixed availability window. | | `appointment` | A time-slot appointment (e.g. a service booking). | | `event` | A ticketed event. The booking also carries a `bookingProductEventTicketId` identifying which ticket type was booked. | | `rental` | A rental booked for a date/time range (hourly or daily). | | `table` | A table reservation (e.g. restaurant), for a party at a time. | ## The booking window The booked time window is returned twice: `from` / `to` as raw **unix timestamps** (integers, for programmatic use) and `fromFormatted` / `toFormatted` as readable strings. Some sub-types are not strictly time-windowed, so all four can be `null` for those rows. ## What the booking detail embeds In the admin, viewing a booking jumps to the underlying order view. The booking detail mirrors that by embedding the order's **billing and shipping address**, its **payment & shipping info** (payment method/title, shipping method/title), and its **invoices, shipments and refunds** — matching what the admin Booking view shows when it opens the order. Addresses can be `null` when the order has none, and the invoices/shipments/refunds arrays are empty when there are none. These are returned as whole JSON objects/arrays — query them bare (no sub-field selection). ## Operations in this menu | Action | Operation | |--------|-----------| | [List bookings](/api/graphql-api/admin/sales/bookings/list) | `adminBookings` query | | [Get a single booking](/api/graphql-api/admin/sales/bookings/detail) | `adminBooking(id:)` query | All Bookings operations require the `sales.bookings.view` permission and an admin Bearer token — see [Authentication](/api/graphql-api/admin/authentication). --- # Booking Detail URL: /api/graphql-api/admin/sales/bookings/detail --- outline: false examples: - id: admin-booking-detail-gql title: Booking Detail description: Fetch a single booking with its sub-type, full booking window, and embedded order / order-item summaries. query: | query AdminBooking($id: ID!) { adminBooking(id: $id) { id _id orderId orderIncrementId orderItemId productId productSku productName bookingType qty from to fromFormatted toFormatted bookingProductEventTicketId createdAt order orderItem paymentMethod paymentTitle shippingMethod shippingTitle billingAddress shippingAddress invoices shipments refunds } } variables: | { "id": "/api/admin/bookings/1" } response: | { "data": { "adminBooking": { "id": "/api/admin/bookings/1", "_id": 1, "orderId": 8, "orderIncrementId": "2000000008", "orderItemId": 42, "productId": 99, "productSku": "BK-EVENT-01", "productName": "Concert Ticket", "bookingType": "event", "qty": 2, "from": 1716220800, "to": 1716224400, "fromFormatted": "20 May, 2026 12:00PM", "toFormatted": "20 May, 2026 13:00PM", "bookingProductEventTicketId": 5, "createdAt": "2026-05-20 10:00:00", "order": { "id": 8, "incrementId": "00000000008", "status": "processing", "customerName": "John Doe", "customerEmail": "john.doe@example.com", "grandTotal": 240, "orderCurrencyCode": "USD" }, "orderItem": { "id": 42, "sku": "BK-EVENT-01", "name": "Concert Ticket", "qtyOrdered": 2 }, "paymentMethod": "moneytransfer", "paymentTitle": "Money Transfer", "shippingMethod": null, "shippingTitle": null, "billingAddress": { "id": 4939, "addressType": "order_billing", "firstName": "John", "lastName": "Doe", "companyName": "Acme Inc.", "address": "123 Main St\nApt 4B", "city": "New York", "state": "NY", "country": "US", "postcode": "10001", "email": "john@example.com", "phone": "1234567890" }, "shippingAddress": null, "invoices": [ { "id": 559, "incrementId": "559", "state": "paid", "baseGrandTotal": 204, "formattedBaseGrandTotal": "$204.00", "createdAt": "2026-05-19 13:11:39" } ], "shipments": [], "refunds": [] } } } --- # Booking Detail GraphQL counterpart of `GET /api/admin/bookings/{id}`. Returns a single booking with everything the listing leaves out — the booking sub-type, the linked event-ticket id, embedded summaries of the parent order and order line item, and the underlying order's billing/shipping address, payment & shipping info, and its invoices / shipments / refunds — so you can render the booking view (and the order it opens) without any follow-up fetch. ## Operation | Operation | Type | |-----------|------| | `adminBooking(id: ID!)` | Query | Pass the booking IRI (`/api/admin/bookings/{id}`) as `id`. Permission: `sales.bookings.view`. ::: warning order, orderItem, billingAddress, shippingAddress, invoices, shipments and refunds are returned whole `order`, `orderItem`, `billingAddress`, `shippingAddress`, `invoices`, `shipments` and `refunds` are returned as whole JSON objects/arrays — **query them bare, without a sub-selection** (`billingAddress`, not `billingAddress { … }`; `invoices`, not `invoices { edges { node { … } } }`). The complete object/array comes back. The keys inside each are listed below for reference. ::: ## Fields The booking window is exposed two ways: `from` / `to` are raw **unix timestamps** (integers), and `fromFormatted` / `toFormatted` are the same instants pre-rendered as human-readable `d M, Y H:iA` strings (e.g. `"20 May, 2026 12:00PM"`). For booking sub-types with no time window all four can be `null`. | Field | Type | Description | |-------|------|-------------| | `id` | `ID` | Resource identifier (IRI form). | | `_id` | `Int` | Numeric booking id. | | `orderId` | `Int` | Id of the parent order. | | `orderIncrementId` | `String` | Human-facing number of the parent order. | | `orderItemId` | `Int` | Id of the order line item this booking was created from. | | `productId` | `Int` | Id of the booked product. | | `productSku` | `String` | SKU of the booked product. | | `productName` | `String` | Product name as ordered. | | `bookingType` | `String` | Booking sub-type — one of `default`, `appointment`, `event`, `rental`, `table`. | | `qty` | `Int` | Quantity booked on this line. | | `from` | `Int` | Start of the booking window as a **unix timestamp** (null for non-time-based bookings). | | `to` | `Int` | End of the booking window as a **unix timestamp** (null for non-time-based bookings). | | `fromFormatted` | `String` | `from` pre-rendered as a `d M, Y H:iA` string. | | `toFormatted` | `String` | `to` pre-rendered as a `d M, Y H:iA` string. | | `bookingProductEventTicketId` | `Int` | Linked event-ticket id (set only when `bookingType` is `event`). | | `createdAt` | `String` | When the parent order was created. | | `order` | `Object` | Summary of the parent order — see the table below. | | `orderItem` | `Object` | Summary of the parent order line item — see the table below. | | `paymentMethod` | `String` | The order's payment method code (e.g. `moneytransfer`). | | `paymentTitle` | `String` | The payment method's display title. | | `shippingMethod` | `String` | The order's shipping method code — `null` when the order had no shipping method. | | `shippingTitle` | `String` | The shipping method's display title — `null` when there was no shipping method. | | `billingAddress` | `JSON` | The order's billing address object, or `null` — see below. | | `shippingAddress` | `JSON` | The order's shipping address object, or `null` — see below. | | `invoices` | `JSON` | Array of the order's invoice summaries — empty when none. See below. | | `shipments` | `JSON` | Array of the order's shipment summaries — empty when none. See below. | | `refunds` | `JSON` | Array of the order's refund summaries — empty when none. See below. | ### Order fields (`order`) A slim summary of the order this booking belongs to, so you can render the booking view without a separate order fetch. `order` is returned as a whole JSON object — query it as a bare field (`order`), you cannot sub-select its keys in the query. The keys below are returned inside that object for reference. | Field | Type | Description | |-------|------|-------------| | `id` | `Int` | Order id. | | `incrementId` | `String` | Human-facing order number. | | `status` | `String` | Order status — e.g. `complete`, `processing`, `pending`. | | `customerName` | `String` | Name of the customer who placed the order. | | `customerEmail` | `String` | Email of the customer who placed the order. | | `grandTotal` | `Float` | Order grand total (order currency). | | `orderCurrencyCode` | `String` | Currency the order was placed in (e.g. `USD`). | ### Order item fields (`orderItem`) A slim summary of the order line item this booking was created from. `orderItem` is returned as a whole JSON object — query it as a bare field (`orderItem`), you cannot sub-select its keys in the query. The keys below are returned inside that object for reference. | Field | Type | Description | |-------|------|-------------| | `id` | `Int` | Order-item id. | | `sku` | `String` | SKU of the ordered item. | | `name` | `String` | Product name as ordered. | | `qtyOrdered` | `Float` | Quantity ordered for this line item. | ### Address objects (`billingAddress`, `shippingAddress`) The order's billing and shipping addresses, mirroring the admin order view's address panel. Each is returned as a whole JSON object (or `null` when the order has none) — query it as a bare field (`billingAddress`), you cannot sub-select its keys in the query. The keys below are returned inside each object. | Field | Type | Description | |-------|------|-------------| | `id` | `Int` | Address row id. | | `addressType` | `String` | `order_billing` or `order_shipping`. | | `firstName` / `lastName` | `String` | Recipient name. | | `companyName` | `String` | Company name — may be `null`. | | `address` | `String` | Street address (may span multiple lines). | | `city` | `String` | City. | | `state` | `String` | State / province. | | `country` | `String` | Country code (e.g. `US`). | | `postcode` | `String` | Postal code. | | `email` | `String` | Contact email. | | `phone` | `String` | Contact phone. | ### Invoices (`invoices`) Slim summaries of the order's invoices, returned as a whole JSON array (empty when none) — query it as a bare field (`invoices`), you cannot sub-select its keys in the query. Each entry has the keys below. | Field | Type | Description | |-------|------|-------------| | `id` | `Int` | Invoice id. | | `incrementId` | `String` | Human-facing invoice number. | | `state` | `String` | Invoice state — e.g. `paid`, `pending`. | | `baseGrandTotal` | `Float` | Invoice grand total in the store's base currency. | | `formattedBaseGrandTotal` | `String` | The same total pre-formatted for display. | | `createdAt` | `String` | When the invoice was created. | ### Shipments (`shipments`) Slim summaries of the order's shipments, returned as a whole JSON array (empty when none) — query it as a bare field (`shipments`), you cannot sub-select its keys in the query. Each entry has the keys below. | Field | Type | Description | |-------|------|-------------| | `id` | `Int` | Shipment id. | | `totalQty` | `Float` | Total quantity shipped. | | `carrierTitle` | `String` | Shipping carrier title — may be `null`. | | `trackNumber` | `String` | Carrier tracking number — may be `null`. | | `createdAt` | `String` | When the shipment was created. | ### Refunds (`refunds`) Slim summaries of the order's refunds, returned as a whole JSON array (empty when none) — query it as a bare field (`refunds`), you cannot sub-select its keys in the query. Each entry has the keys below. | Field | Type | Description | |-------|------|-------------| | `id` | `Int` | Refund id. | | `state` | `String` | Refund state. | | `baseGrandTotal` | `Float` | Refund grand total in the store's base currency. | | `formattedBaseGrandTotal` | `String` | The same total pre-formatted for display. | | `createdAt` | `String` | When the refund was created. | --- # List Bookings (Datagrid) URL: /api/graphql-api/admin/sales/bookings/list --- outline: false examples: - id: admin-bookings-list-gql title: List Bookings (Datagrid) description: Cursor-paginated bookings datagrid. Every booking column plus the linked order / order-item summary is populated on each row. query: | query AdminBookings($first: Int, $after: String) { adminBookings(first: $first, after: $after) { edges { cursor node { id _id orderId orderIncrementId orderItemId productId productSku productName bookingType qty from to fromFormatted toFormatted bookingProductEventTicketId order orderItem createdAt } } pageInfo { hasNextPage endCursor } totalCount } } variables: | { "first": 10 } response: | { "data": { "adminBookings": { "edges": [ { "cursor": "MA==", "node": { "id": "/api/admin/bookings/1", "_id": 1, "orderId": 8, "orderIncrementId": "00000000008", "orderItemId": 42, "productId": 99, "productSku": "BK-EVENT-01", "productName": "Concert Ticket", "bookingType": "event", "qty": 2, "from": 1716220800, "to": 1716224400, "fromFormatted": "20 May, 2026 12:00PM", "toFormatted": "20 May, 2026 13:00PM", "bookingProductEventTicketId": 5, "order": { "id": 8, "incrementId": "00000000008", "status": "processing", "customerName": "John Doe", "customerEmail": "john.doe@example.com", "grandTotal": 240, "orderCurrencyCode": "USD" }, "orderItem": { "id": 42, "sku": "BK-EVENT-01", "name": "Concert Ticket", "qtyOrdered": 2 }, "createdAt": "2026-05-20 10:00:00" } } ], "pageInfo": { "hasNextPage": false, "endCursor": "MA==" }, "totalCount": 1 } } } --- # List Bookings (Datagrid) GraphQL counterpart of `GET /api/admin/bookings`. Returns a cursor-paginated list of bookings — the same rows shown on the admin **Sales → Bookings** datagrid. Every booking **column** plus the linked `order` and `orderItem` summaries are populated on each row, so the field set is identical to [Booking Detail](/api/graphql-api/admin/sales/bookings/detail). ## Operation `adminBookings(first, after, id, order_id, qty, product_id, from_from, from_to, to_from, to_to, created_at_from, created_at_to, sort, order)` — a cursor `QueryCollection`. Every REST query parameter is also exposed as a GraphQL argument; see the [REST page](/api/rest-api/admin/sales/bookings/list) for the full argument table. ## Permission `sales.bookings.view` ::: warning Order objects are returned whole `order` and `orderItem` are returned as JSON — **query them bare, without a sub-selection** (`order`, not `order { … }`). The whole object comes back. See [Booking Detail](/api/graphql-api/admin/sales/bookings/detail) for the keys inside each. ::: ## Fields Every field is populated on each row (the booking columns, the booking sub-type, the two time-window representations, and the `order` / `orderItem` summary objects). The booking window is exposed two ways: `from` / `to` are raw **unix timestamps** (integers), and `fromFormatted` / `toFormatted` are the same instants pre-rendered as `d M, Y H:iA` strings (e.g. `"20 May, 2026 12:00PM"`). For non-time-based booking sub-types all four can be `null`. The full per-field reference is on the [Booking Detail](/api/graphql-api/admin/sales/bookings/detail) page. ## Listing vs. fetching one The listing already carries the full payload — fetching a single booking by id (`adminBooking(id:)`) is only needed when you already hold a booking id and want just that record. Typical flow: list with `adminBookings`, read `_id` from the row you want, then fetch the full record with `adminBooking(id:)`. --- # Add Item to Cart URL: /api/graphql-api/admin/sales/carts/add-item --- outline: false examples: - id: admin-cart-add-item title: Add Item (simple) description: Add a simple/virtual product. `id` is the resource IRI; `cartId` is forwarded as the raw integer for the processor. query: | mutation AddItem($input: addItemAdminCartInput!) { addItemAdminCart(input: $input) { adminCart { itemsCount grandTotal formattedGrandTotal success message } } } variables: | { "input": { "id": "/api/admin/carts/314", "cartId": "314", "productId": 142, "quantity": 1 } } response: | { "data": { "addItemAdminCart": { "adminCart": { "itemsCount": 1, "grandTotal": 100, "formattedGrandTotal": "$100.00", "success": true, "message": "Item added to cart." } } } } - id: admin-cart-add-configurable title: Add Item (configurable) description: Configurable products require the chosen variant's product id in selectedConfigurableOption. query: | mutation AddItem($input: addItemAdminCartInput!) { addItemAdminCart(input: $input) { adminCart { itemsCount grandTotal formattedGrandTotal success message } } } variables: | { "input": { "id": "/api/admin/carts/314", "cartId": "314", "productId": 123, "quantity": 1, "selectedConfigurableOption": 124 } } response: | { "data": { "addItemAdminCart": { "adminCart": { "itemsCount": 1, "grandTotal": 100, "formattedGrandTotal": "$100.00", "success": true, "message": "Item added to cart." } } } } - id: admin-cart-add-bundle title: Add Item (bundle) description: Bundle products require bundleOptions — a list of { optionId, productIds, quantity }. query: | mutation AddItem($input: addItemAdminCartInput!) { addItemAdminCart(input: $input) { adminCart { itemsCount grandTotal formattedGrandTotal success message } } } variables: | { "input": { "id": "/api/admin/carts/314", "cartId": "314", "productId": 2517, "quantity": 1, "bundleOptions": [ { "optionId": 5, "productIds": [10], "quantity": 1 }, { "optionId": 6, "productIds": [12], "quantity": 1 } ] } } response: | { "data": { "addItemAdminCart": { "adminCart": { "itemsCount": 1, "grandTotal": 100, "formattedGrandTotal": "$100.00", "success": true, "message": "Item added to cart." } } } } - id: admin-cart-add-grouped title: Add Item (grouped) description: Grouped products require groupedQuantities — a list of { productId, quantity }. query: | mutation AddItem($input: addItemAdminCartInput!) { addItemAdminCart(input: $input) { adminCart { itemsCount grandTotal formattedGrandTotal success message } } } variables: | { "input": { "id": "/api/admin/carts/314", "cartId": "314", "productId": 2516, "quantity": 1, "groupedQuantities": [ { "productId": 301, "quantity": 1 }, { "productId": 302, "quantity": 2 } ] } } response: | { "data": { "addItemAdminCart": { "adminCart": { "itemsCount": 1, "grandTotal": 100, "formattedGrandTotal": "$100.00", "success": true, "message": "Item added to cart." } } } } - id: admin-cart-add-downloadable title: Add Item (downloadable) description: Downloadable products require links — a list of downloadable-link ids. query: | mutation AddItem($input: addItemAdminCartInput!) { addItemAdminCart(input: $input) { adminCart { itemsCount grandTotal formattedGrandTotal success message } } } variables: | { "input": { "id": "/api/admin/carts/314", "cartId": "314", "productId": 2506, "quantity": 1, "links": [1, 2] } } response: | { "data": { "addItemAdminCart": { "adminCart": { "itemsCount": 1, "grandTotal": 100, "formattedGrandTotal": "$100.00", "success": true, "message": "Item added to cart." } } } } --- # Add Item to Cart GraphQL counterpart of `POST /api/admin/carts/{id}/items`. Mutation field is `addItemAdminCart`. Input type is `addItemAdminCartInput`. ::: warning Select cart fields, not `id` This mutation returns the updated cart — select `itemsCount`, `grandTotal`, `formattedGrandTotal`, `couponCode`, `success`, `message`, etc. Do **not** select `id` (or `_id`): this is an action result with no addressable record, so the auto-generated IRI field is `null` and selecting it errors out the whole payload. The same applies to every cart-write mutation. ::: ::: tip Prerequisites The example uses an illustrative cart id. Admin cart endpoints only operate on **draft carts** (`is_active=0`) — storefront carts are rejected by the admin cart guard. Create a draft cart first with the [`createAdminDraftCart`](../../customers/create-draft-cart.md) mutation and use the returned `cartId`. ::: ## Operation | Operation | Type | |-----------|------| | `addItemAdminCart(input: addItemAdminCartInput!)` | Mutation | ## Product type fields Each product type needs its own selection fields (besides `productId` and `quantity`). These are typed input fields, so they work over GraphQL exactly as they do over REST: | Product type | Field | |--------------|-------| | Simple / Virtual | — | | Configurable | `selectedConfigurableOption` — the chosen variant's product id | | Downloadable | `links` — list of downloadable-link ids | | Grouped | `groupedQuantities` — list of `{ productId, quantity }` | | Bundle | `bundleOptions` — list of `{ optionId, productIds, quantity }` | | Booking | not supported in admin Create-Order (returns an error) | See the examples dropdown for a per-type sample. ## Errors | Cause | Response | |-------|----------| | `productId` missing | `errors[]` — `productId is required.` | | Product not found | `errors[]` — `Product not found.` | | **Booking product** — admin draft orders do not support booking products | `errors[]` — `Booking products cannot be added to an admin draft order. Booking purchases must be made through the customer storefront.` Sample: `{"errors":[{"message":"Booking products cannot be added to an admin draft order. Booking purchases must be made through the customer storefront.","extensions":{"category":"user"}}],"data":{"addItemAdminCart":null}}` | | Cart is an active storefront cart | `errors[]` — `This cart cannot be modified through the admin API.` | --- # Apply a Coupon URL: /api/graphql-api/admin/sales/carts/apply-coupon --- outline: false examples: - id: admin-cart-apply-coupon title: Apply a Coupon description: Apply a cart-rule coupon code to the draft cart. Returns errors[] on unknown/inactive coupon (404 equivalent) or already-applied (422 equivalent). query: | mutation ApplyCoupon($input: applyCouponAdminCartInput!) { applyCouponAdminCart(input: $input) { adminCart { itemsCount grandTotal formattedGrandTotal success message } } } variables: | { "input": { "id": "/api/admin/carts/314", "cartId": "314", "code": "WELCOME10" } } response: | { "data": { "applyCouponAdminCart": { "adminCart": { "itemsCount": 1, "grandTotal": 100, "formattedGrandTotal": "$100.00", "success": true, "message": "Coupon applied." } } } } --- # Apply a Coupon GraphQL counterpart of `POST /api/admin/carts/{id}/coupon`. Mutation field is `applyCouponAdminCart`. ::: tip Prerequisites The example uses an illustrative cart id. Admin cart endpoints only operate on **draft carts** (`is_active=0`) — storefront carts are rejected by the admin cart guard. Create a draft cart first with the [`createAdminDraftCart`](../../customers/create-draft-cart.md) mutation and use the returned `cartId`. ::: ## Operation | Operation | Type | |-----------|------| | `applyCouponAdminCart(input: applyCouponAdminCartInput!)` | Mutation | --- # Get Cart URL: /api/graphql-api/admin/sales/carts/get-cart --- outline: false examples: - id: admin-cart-get title: Get Cart description: Read a draft cart by IRI. `id` is the resource IRI `/api/admin/carts/{cartId}`. query: | query AdminCart($id: ID!) { adminCart(id: $id) { id _id } } variables: | { "id": "/api/admin/carts/314" } response: | { "data": { "adminCart": { "id": "/api/admin/carts/314", "_id": 314 } } } --- # Get Cart Returns the admin draft cart. The REST endpoint (`GET /api/admin/carts/{id}`) returns the full payload — items, totals, addresses, payment method. Over GraphQL the resource exposes its `id` (IRI) and `_id` (integer); scalar camelCase fields (`customerId`, `isActive`, `grandTotal`, ...) currently serialise to `null` over GraphQL due to a project-wide API Platform quirk affecting all admin output resources. **For the populated payload, use the REST endpoint.** ## Operation | Operation | Type | Purpose | |-----------|------|---------| | `adminCart(id: ID!)` | Query | Read a draft cart | --- # List Payment Methods URL: /api/graphql-api/admin/sales/carts/list-payment-methods --- outline: false examples: - id: admin-cart-list-payment-methods title: List Payment Methods description: List payment methods supported for the draft cart. A shipping method must be selected first. query: | query AdminCartPaymentMethods($id: Int!) { adminCartPaymentMethods(cartId: $id) { edges { node { method methodTitle description sort } } } } variables: | { "id": 314 } response: | { "data": { "adminCartPaymentMethods": { "edges": [ { "node": { "method": "cashondelivery", "methodTitle": "Cash On Delivery", "description": "", "sort": 1 } }, { "node": { "method": "moneytransfer", "methodTitle": "Money Transfer", "description": "", "sort": 2 } } ] } } } --- # List Payment Methods ::: tip Prerequisites The example uses an illustrative cart id. Admin cart endpoints only operate on **draft carts** (`is_active=0`) — storefront carts are rejected by the admin cart guard. Create a draft cart first with the [`createAdminDraftCart`](../../customers/create-draft-cart.md) mutation and use the returned `cartId`. ::: ## Operation | Operation | Type | Purpose | |-----------|------|---------| | `adminCartPaymentMethods(cartId: Int!)` | QueryCollection | List supported payment methods | ## Sequence A shipping method must already be selected on the cart, otherwise the response carries an `errors[]` entry equivalent to HTTP 409 on REST. ## Note Only `cashondelivery` and `moneytransfer` succeed at `createAdminPlaceOrder` — matches the admin Create-Order screen. --- # List Shipping Methods URL: /api/graphql-api/admin/sales/carts/list-shipping-methods --- outline: false examples: - id: admin-cart-list-shipping-methods title: List Shipping Methods description: Returns available shipping rates for a draft cart. Both addresses must be saved first. query: | query AdminCartShippingRates($id: Int!) { adminCartShippingRates(cartId: $id) { edges { node { method carrierCode carrierTitle methodTitle price formattedPrice } } } } variables: | { "id": 314 } response: | { "data": { "adminCartShippingRates": { "edges": [ { "node": { "method": "flatrate_flatrate", "carrierCode": "flatrate", "carrierTitle": "Flat Rate", "methodTitle": "Fixed", "price": 10, "formattedPrice": "$10.00" } } ] } } } --- # List Shipping Methods ::: tip Prerequisites The example uses an illustrative cart id. Admin cart endpoints only operate on **draft carts** (`is_active=0`) — storefront carts are rejected by the admin cart guard. Create a draft cart first with the [`createAdminDraftCart`](../../customers/create-draft-cart.md) mutation and use the returned `cartId`. ::: ## Operation | Operation | Type | Purpose | |-----------|------|---------| | `adminCartShippingRates(cartId: Int!)` | QueryCollection | List available shipping rates | ## Sequence Both billing AND shipping addresses must be saved on the cart, otherwise the response carries an `errors[]` entry equivalent to HTTP 409 on REST (`Addresses must be saved before selecting a shipping method.`). ## Errors | Cause | Notes | |-------|-------| | Cart not found | Unknown id | | 403 — active storefront cart | Only draft carts (is_active = 0) are accessible | | 409 — addresses missing | Save addresses via `saveAddressAdminCart` first | | Unauthenticated | Missing admin Bearer token | --- # Remove Applied Coupon URL: /api/graphql-api/admin/sales/carts/remove-coupon --- outline: false examples: - id: admin-cart-remove-coupon title: Remove Applied Coupon description: Remove the currently-applied coupon from the draft cart. Idempotent. query: | mutation RemoveCoupon($input: removeCouponAdminCartInput!) { removeCouponAdminCart(input: $input) { adminCart { itemsCount grandTotal formattedGrandTotal success message } } } variables: | { "input": { "id": "/api/admin/carts/314", "cartId": "314" } } response: | { "data": { "removeCouponAdminCart": { "adminCart": { "itemsCount": 1, "grandTotal": 100, "formattedGrandTotal": "$100.00", "success": true, "message": "Coupon removed." } } } } --- # Remove Applied Coupon GraphQL counterpart of `DELETE /api/admin/carts/{id}/coupon`. Mutation field is `removeCouponAdminCart`. ::: tip Prerequisites The example uses an illustrative cart id. Admin cart endpoints only operate on **draft carts** (`is_active=0`) — storefront carts are rejected by the admin cart guard. Create a draft cart first with the [`createAdminDraftCart`](../../customers/create-draft-cart.md) mutation and use the returned `cartId`. ::: ## Operation | Operation | Type | |-----------|------| | `removeCouponAdminCart(input: removeCouponAdminCartInput!)` | Mutation | --- # Remove a Cart Item URL: /api/graphql-api/admin/sales/carts/remove-item --- outline: false examples: - id: admin-cart-remove-item title: Remove a Cart Item description: Remove a single line item from the draft cart. query: | mutation RemoveItem($input: removeItemAdminCartInput!) { removeItemAdminCart(input: $input) { adminCart { itemsCount grandTotal formattedGrandTotal success message } } } variables: | { "input": { "id": "/api/admin/carts/314", "cartId": "314", "cartItemId": 6711 } } response: | { "data": { "removeItemAdminCart": { "adminCart": { "itemsCount": 1, "grandTotal": 100, "formattedGrandTotal": "$100.00", "success": true, "message": "Item removed from cart." } } } } --- # Remove a Cart Item GraphQL counterpart of `DELETE /api/admin/carts/{id}/items`. Mutation field is `removeItemAdminCart`. ::: tip Prerequisites The example uses an illustrative cart id. Admin cart endpoints only operate on **draft carts** (`is_active=0`) — storefront carts are rejected by the admin cart guard. Create a draft cart first with the [`createAdminDraftCart`](../../customers/create-draft-cart.md) mutation and use the returned `cartId`. ::: ## Operation | Operation | Type | |-----------|------| | `removeItemAdminCart(input: removeItemAdminCartInput!)` | Mutation | --- # Save Cart Addresses URL: /api/graphql-api/admin/sales/carts/save-address --- outline: false examples: - id: admin-cart-save-address title: Save Cart Addresses description: Save billing (and optionally shipping) addresses on the draft cart. query: | mutation SaveAddress($input: saveAddressAdminCartInput!) { saveAddressAdminCart(input: $input) { adminCart { itemsCount grandTotal formattedGrandTotal success message } } } variables: | { "input": { "id": "/api/admin/carts/314", "cartId": "314", "billing": { "firstName": "Jane", "lastName": "Doe", "email": "jane@example.com", "address": ["12 Main St"], "city": "Berlin", "country": "DE", "state": "BE", "postcode": "10115", "phone": "+4930123456", "useForShipping": true } } } response: | { "data": { "saveAddressAdminCart": { "adminCart": { "itemsCount": 1, "grandTotal": 100, "formattedGrandTotal": "$100.00", "success": true, "message": "Address saved." } } } } --- # Save Cart Addresses GraphQL counterpart of `POST /api/admin/carts/{id}/addresses`. Mutation field is `saveAddressAdminCart`. ::: tip Prerequisites The example uses an illustrative cart id. Admin cart endpoints only operate on **draft carts** (`is_active=0`) — storefront carts are rejected by the admin cart guard. Create a draft cart first with the [`createAdminDraftCart`](../../customers/create-draft-cart.md) mutation and use the returned `cartId`. ::: ## Operation | Operation | Type | |-----------|------| | `saveAddressAdminCart(input: saveAddressAdminCartInput!)` | Mutation | ## Required fields Each address (billing always; shipping too when `useForShipping` is `false`) must include `firstName`, `lastName`, `email`, `address` (a non-empty array of street lines), `city`, `country`, `state`, `postcode` and `phone`, otherwise the mutation fails with a `422`-equivalent error. `companyName` is optional. --- # Set Payment Method URL: /api/graphql-api/admin/sales/carts/set-payment-method --- outline: false examples: - id: admin-cart-set-payment-method title: Set Payment Method description: Save the chosen payment method on the draft cart. query: | mutation SetPayment($input: setPaymentMethodAdminCartInput!) { setPaymentMethodAdminCart(input: $input) { adminCart { itemsCount grandTotal formattedGrandTotal success message } } } variables: | { "input": { "id": "/api/admin/carts/314", "cartId": 314, "method": "cashondelivery" } } response: | { "data": { "setPaymentMethodAdminCart": { "adminCart": { "itemsCount": 1, "grandTotal": 100, "formattedGrandTotal": "$100.00", "success": true, "message": "Payment method saved." } } } } --- # Set Payment Method ::: tip Prerequisites The example uses an illustrative cart id. Admin cart endpoints only operate on **draft carts** (`is_active=0`) — storefront carts are rejected by the admin cart guard. Create a draft cart first with the [`createAdminDraftCart`](../../customers/create-draft-cart.md) mutation and use the returned `cartId`. ::: ## Operation | Operation | Type | Purpose | |-----------|------|---------| | `setPaymentMethodAdminCart(input)` | Mutation | Save the chosen payment method on the draft cart | ## Errors | Cause | Notes | |-------|-------| | 409 — shipping missing | Select a shipping method first | | 400 — method missing | Input must include `method` | | 403 — active storefront cart | Only draft carts can be modified | | Unauthenticated | Missing admin Bearer token | --- # Set Shipping Method URL: /api/graphql-api/admin/sales/carts/set-shipping-method --- outline: false examples: - id: admin-cart-set-shipping-method title: Set Shipping Method description: Save the chosen shipping method on the draft cart. query: | mutation SetShipping($input: setShippingMethodAdminCartInput!) { setShippingMethodAdminCart(input: $input) { adminCart { itemsCount grandTotal formattedGrandTotal success message } } } variables: | { "input": { "id": "/api/admin/carts/314", "cartId": 314, "shippingMethod": "flatrate_flatrate" } } response: | { "data": { "setShippingMethodAdminCart": { "adminCart": { "itemsCount": 1, "grandTotal": 100, "formattedGrandTotal": "$100.00", "success": true, "message": "Shipping method saved." } } } } --- # Set Shipping Method ::: tip Prerequisites The example uses an illustrative cart id. Admin cart endpoints only operate on **draft carts** (`is_active=0`) — storefront carts are rejected by the admin cart guard. Create a draft cart first with the [`createAdminDraftCart`](../../customers/create-draft-cart.md) mutation and use the returned `cartId`. ::: ## Operation | Operation | Type | Purpose | |-----------|------|---------| | `setShippingMethodAdminCart(input)` | Mutation | Save the chosen shipping method on the draft cart | `input.id` is the resource IRI; `cartId` is the integer id (both are accepted because GraphQL inputs use `id` reserved as the resource IRI). ## Errors | Cause | Notes | |-------|-------| | 409 — addresses missing | Save addresses first via `saveAddressAdminCart` | | 400 — shippingMethod missing | Input must include `shippingMethod` | | 403 — active storefront cart | Only draft carts can be modified | | Unauthenticated | Missing admin Bearer token | --- # Update Cart Item Quantities URL: /api/graphql-api/admin/sales/carts/update-items --- outline: false examples: - id: admin-cart-update-items title: Update Cart Item Quantities description: Bulk-update line-item quantities. `qty` is a map of cartItemId → new quantity. query: | mutation UpdateItems($input: updateItemsAdminCartInput!) { updateItemsAdminCart(input: $input) { adminCart { itemsCount grandTotal formattedGrandTotal success message } } } variables: | { "input": { "id": "/api/admin/carts/314", "cartId": "314", "qty": { "6711": 3 } } } response: | { "data": { "updateItemsAdminCart": { "adminCart": { "itemsCount": 1, "grandTotal": 100, "formattedGrandTotal": "$100.00", "success": true, "message": "Cart items updated." } } } } --- # Update Cart Item Quantities GraphQL counterpart of `PUT /api/admin/carts/{id}/items`. Mutation field is `updateItemsAdminCart`. ::: tip Prerequisites The example uses an illustrative cart id. Admin cart endpoints only operate on **draft carts** (`is_active=0`) — storefront carts are rejected by the admin cart guard. Create a draft cart first with the [`createAdminDraftCart`](../../customers/create-draft-cart.md) mutation and use the returned `cartId`. ::: ## Operation | Operation | Type | |-----------|------| | `updateItemsAdminCart(input: updateItemsAdminCartInput!)` | Mutation | --- # Invoices URL: /api/graphql-api/admin/sales/invoices --- outline: false --- # Invoices The Invoices menu is the store-wide list of every invoice that has been generated across all orders, plus the actions you can take on a single invoice. It mirrors the admin **Sales → Invoices** screen. ## When a row appears here An invoice row exists only after an invoice has been **generated** for an order — created manually (the Create Invoice mutation) or auto-generated if your store enables auto-invoicing. Placing or paying for an order does not by itself create an invoice; until one is generated, the order has no row in this menu. ## `state` — what each value means In practice the grid shows just two badges — **Paid** and **Pending**. The "Overdue by N days" you usually see is a *derived countdown* shown under a **Pending** invoice (see [the countdown](#payment-due-date-the-red-countdown) below), **not** a stored state. | `state` | Badge | Meaning | |---------|-------|---------| | `paid` | green **Paid** | The invoiced amount has been **captured** — a payment transaction was recorded against it (cash-on-delivery, the "record transaction" option at create-invoice time, or a paid gateway). | | `pending` | yellow **Pending** | The invoice exists but the money has **not been captured yet**. This is the everyday non-paid state. | | `pending_payment` | yellow **Pending** | A programmatic variant of `pending` set by some payment flows; displayed identically. | | `overdue` | red **Overdue** | A status you can set **manually** via [Mass Update Status](/api/graphql-api/admin/sales/invoices/mass-update-status). Rarely used day-to-day — the "Overdue by N days" the grid normally shows is the derived countdown under a **Pending** invoice, whose stored `state` is still `pending`. | There is **no `refunded` invoice state** — refunds are tracked separately under [Refunds](/api/graphql-api/admin/sales/refunds/), not on an invoice's `state`. **Why an order can still read "pending" after you invoice it.** Generating an invoice records *what is owed*, not that it has been *paid*. The invoice (and the order's payment state) flips to `paid` only when a payment transaction is recorded against it. So an invoiced order whose payment hasn't been captured — e.g. an offline method, or an invoice generated without recording a transaction — stays `pending` until the money is taken. Expected behaviour, not a defect. ## Payment due date & the red countdown For a `pending` invoice the admin grid shows a small red line under the status: a countdown to the **payment due date**. That due date is the invoice's creation date plus your store's configured *payment term* — the "due duration" in days, set under **Sales → Invoice Settings → Payment Terms**. While the due date is in the future the line reads "N day(s) left"; once it passes it turns red and reads "**Overdue by N day(s)**". Either way the invoice's stored `state` is still `pending` — the countdown is a display only, never a separate state. The API returns the raw `state` and `createdAt`, so a client can reproduce the same line: `dueDate = createdAt + dueDuration(days)`, then compare to today. ## What each action does | Action | What it does | |--------|--------------| | **Print Invoice (PDF)** | Downloads a print-ready PDF of the invoice — a binary `application/pdf` attachment. This is **REST only**; binary downloads aren't expressible over GraphQL. | | **Send Duplicate Invoice** | Re-sends the invoice email to the customer. You can override the recipient with a custom email; otherwise it goes to the order's customer email. Re-delivers a copy without regenerating the invoice. | | **Mass Update Status** | Bulk-sets the status flag (`pending` / `paid` / `overdue`) on the selected invoices. It is a **manual flag only** — it does **not** capture or reverse a payment, and it does not create a transaction. Use it to correct or annotate state, not to take money. | | **Export (CSV)** | Downloads the invoices grid — with your current filters applied — as a CSV file. **REST only** (binary). | Invoice **creation** is not in this menu — it runs against an order; see [Create Invoice](/api/graphql-api/admin/sales/orders/create-invoice). ## Operations in this menu | Action | Operation | |--------|-----------| | [List invoices](/api/graphql-api/admin/sales/invoices/list) | `adminInvoices` query | | [Get a single invoice](/api/graphql-api/admin/sales/orders/get-invoice) | `adminInvoice(id:)` query | | [Print invoice (PDF)](/api/graphql-api/admin/sales/orders/print-invoice) | REST only (binary) | | [Send duplicate invoice email](/api/graphql-api/admin/sales/orders/send-duplicate-invoice) | `createAdminInvoiceSendDuplicate` mutation | | [Mass update status](/api/graphql-api/admin/sales/invoices/mass-update-status) | `createAdminInvoiceMassUpdateStatus` mutation | | [Export invoices (CSV)](/api/rest-api/admin/sales/invoices/export) | REST only — `GET /api/admin/invoices/export` | All Invoice operations require the `sales.invoices.view` permission and an admin Bearer token — see [Authentication](/api/graphql-api/admin/authentication). --- # List Invoices URL: /api/graphql-api/admin/sales/invoices/list --- outline: false examples: - id: admin-invoices-list-gql title: List Invoices (Datagrid) description: Cursor-paginated invoices listing. Every invoice column is populated on each row — query whichever ones you need. query: | query AdminInvoices($first: Int, $after: String) { adminInvoices(first: $first, after: $after) { edges { cursor node { id _id incrementId orderId orderIncrementId state emailSent totalQty orderCurrencyCode baseCurrencyCode channelCurrencyCode subTotal formattedSubTotal baseSubTotal formattedBaseSubTotal subTotalInclTax formattedSubTotalInclTax baseSubTotalInclTax formattedBaseSubTotalInclTax grandTotal formattedGrandTotal baseGrandTotal formattedBaseGrandTotal taxAmount formattedTaxAmount baseTaxAmount formattedBaseTaxAmount discountAmount formattedDiscountAmount baseDiscountAmount formattedBaseDiscountAmount shippingAmount formattedShippingAmount baseShippingAmount formattedBaseShippingAmount shippingAmountInclTax formattedShippingAmountInclTax baseShippingAmountInclTax formattedBaseShippingAmountInclTax shippingTaxAmount formattedShippingTaxAmount baseShippingTaxAmount formattedBaseShippingTaxAmount transactionId reminders nextReminderAt createdAt updatedAt orderStatus orderStatusLabel orderDate channelName customerName customerEmail billingAddress shippingAddress } } pageInfo { hasNextPage endCursor } totalCount } } variables: | { "first": 10 } response: | { "data": { "adminInvoices": { "edges": [ { "cursor": "MA==", "node": { "id": "/api/admin/invoices/560", "_id": 560, "incrementId": "560", "orderId": 2392, "orderIncrementId": "2392", "state": "paid", "emailSent": true, "totalQty": 1, "orderCurrencyCode": "USD", "baseCurrencyCode": "USD", "channelCurrencyCode": "USD", "subTotal": 4000, "formattedSubTotal": "$4,000.00", "baseSubTotal": 4000, "formattedBaseSubTotal": "$4,000.00", "subTotalInclTax": 4000, "formattedSubTotalInclTax": "$4,000.00", "baseSubTotalInclTax": 4000, "formattedBaseSubTotalInclTax": "$4,000.00", "grandTotal": 4000, "formattedGrandTotal": "$4,000.00", "baseGrandTotal": 4000, "formattedBaseGrandTotal": "$4,000.00", "taxAmount": 0, "formattedTaxAmount": "$0.00", "baseTaxAmount": 0, "formattedBaseTaxAmount": "$0.00", "discountAmount": 0, "formattedDiscountAmount": "$0.00", "baseDiscountAmount": 0, "formattedBaseDiscountAmount": "$0.00", "shippingAmount": 0, "formattedShippingAmount": "$0.00", "baseShippingAmount": 0, "formattedBaseShippingAmount": "$0.00", "shippingAmountInclTax": 0, "formattedShippingAmountInclTax": "$0.00", "baseShippingAmountInclTax": 0, "formattedBaseShippingAmountInclTax": "$0.00", "shippingTaxAmount": 0, "formattedShippingTaxAmount": "$0.00", "baseShippingTaxAmount": 0, "formattedBaseShippingTaxAmount": "$0.00", "transactionId": null, "reminders": 0, "nextReminderAt": null, "createdAt": "2026-05-19 13:13:30", "updatedAt": "2026-05-29 13:30:32", "orderStatus": "processing", "orderStatusLabel": "Processing", "orderDate": "2026-05-19 13:13:29", "channelName": "bagisto store", "customerName": "John Doe", "customerEmail": "john.doe@example.com", "billingAddress": { "id": 268, "addressType": "order_billing", "firstName": "John", "lastName": "Doe", "companyName": "Acme Trades", "address": "21 Market Street", "city": "Los Angeles", "state": "CA", "country": "US", "postcode": "90001", "email": "john.doe@example.com", "phone": "5551234567" }, "shippingAddress": { "id": 267, "addressType": "order_shipping", "firstName": "John", "lastName": "Doe", "companyName": "Acme Trades", "address": "21 Market Street", "city": "Los Angeles", "state": "CA", "country": "US", "postcode": "90001", "email": "john.doe@example.com", "phone": "5551234567" } } } ], "pageInfo": { "hasNextPage": true, "endCursor": "OQ==" }, "totalCount": 562 } } } --- # List Invoices GraphQL counterpart of `GET /api/admin/invoices`. Returns a cursor-paginated list of invoices — every invoice **column** is populated on each row, so you can query whichever fields you need without a follow-up call. Requires the `sales.invoices.view` permission. All admin endpoints require an admin Bearer token — see [Authentication](/api/graphql-api/admin/authentication). ::: tip How this menu works For the invoice `state` semantics, why a paid order can read "pending", the red payment-due countdown, and the print / send-duplicate / mass-status operations, see the [Invoices overview](/api/graphql-api/admin/sales/invoices/). ::: ## Operation `adminInvoices(first, after, id, order_id, state, base_grand_total_from, base_grand_total_to, created_at_from, created_at_to, date_range, sort, order)` — a cursor `QueryCollection`. Every REST query parameter is also exposed as a GraphQL argument; see the [REST page](/api/rest-api/admin/sales/invoices/list) for the full argument table. ::: tip What's on the listing Every **column** of the invoice (state, totals, currency codes, reminders, timestamps), the order/customer context (`customerName`, `customerEmail`, `orderStatus`, `orderStatusLabel`, `orderDate`, `channelName`), **and** the `billingAddress` / `shippingAddress` objects are returned on every listing row. Only `items` (the line items) is left empty (`[]`) on the listing — it's the one heavy per-row relation; fetch it with [Get Invoice](/api/graphql-api/admin/sales/orders/get-invoice). The field set is otherwise identical to the single-invoice query. ::: ## Fields Same field set as [Get Invoice](/api/graphql-api/admin/sales/orders/get-invoice) — see that page for the full per-field reference. Summary of what the **listing** populates: | Group | Fields | On listing | |-------|--------|:---------:| | Identity | `id`, `_id`, `incrementId`, `orderId`, `orderIncrementId`, `state`, `emailSent`, `totalQty` | ✓ | | Currency codes | `orderCurrencyCode`, `baseCurrencyCode`, `channelCurrencyCode` | ✓ | | Totals | `subTotal*`, `grandTotal*`, `taxAmount*`, `discountAmount*`, `shippingAmount*` (each in order + base currency, with `formatted*` and incl-tax variants) | ✓ | | Status & timestamps | `transactionId`, `reminders`, `nextReminderAt`, `createdAt`, `updatedAt` | ✓ | | Order & customer | `orderStatus`, `orderStatusLabel`, `orderDate`, `channelName`, `customerName`, `customerEmail` | ✓ | | Addresses | `billingAddress`, `shippingAddress` (JSON objects — query bare) | ✓ | | Line items | `items` | **detail only** (`[]` on listing) | ::: warning A `null` here means the DB is genuinely empty Listing rows return the actual stored value for every column. If a field comes back `null` (e.g. `baseCurrencyCode`, `transactionId`, `customerName`), that row has no value stored for it — it is **not** the listing withholding data. Only `items` is deliberately omitted on the listing. ::: **Amounts — which one to show.** Use `formattedGrandTotal` for a viewer working in the order's currency, and `baseGrandTotal` / `formattedBaseGrandTotal` for reporting in the store's base currency. For a single-currency store the two are identical. --- # Mass Update Invoice Status URL: /api/graphql-api/admin/sales/invoices/mass-update-status --- outline: false examples: - id: admin-invoices-mass-update-status-gql title: Mass Update Invoice Status description: Bulk-set the status of several invoices at once. A manual status override — it does not capture or reverse a payment. query: | mutation MassUpdateInvoiceStatus($input: createAdminInvoiceMassUpdateStatusInput!) { createAdminInvoiceMassUpdateStatus(input: $input) { adminInvoiceMassUpdateStatus { updated message } } } variables: | { "input": { "indices": [560, 561], "value": "paid" } } response: | { "data": { "createAdminInvoiceMassUpdateStatus": { "adminInvoiceMassUpdateStatus": { "updated": [560, 561], "message": "Invoice status updated successfully." } } } } --- # Mass Update Invoice Status GraphQL counterpart of `POST /api/admin/invoices/mass-update-status`. Bulk-sets the status of a batch of invoices — the operation behind the admin Invoices datagrid's "Update Status" bulk action. Requires the `sales.invoices.view` permission. All admin endpoints require an admin Bearer token — see [Authentication](/api/graphql-api/admin/authentication). ::: warning This is a manual status override Setting an invoice to `paid` here **does not capture a payment**, and setting it to `pending` / `overdue` does **not** reverse one. It only changes the stored status flag. ::: ## Operation | Operation | Type | |-----------|------| | `createAdminInvoiceMassUpdateStatus(input)` | Mutation | ## Input | Field | Type | Description | |-------|------|-------------| | `indices` | `[Int!]!` | Ids of the invoices to update. Must be non-empty. | | `value` | `String!` | New status — one of `pending`, `paid`, `overdue`. | ## Payload `message` is the field to read for confirmation. The `updated` ids are also returned; over GraphQL this list is wrapped in a paginated envelope, so the **REST** response is the canonical place to read the flat `updated` array. Empty `indices` or an out-of-range `value` returns an error in `errors[]`. --- # Orders URL: /api/graphql-api/admin/sales/orders --- outline: false --- # Orders The Orders menu is the heart of Sales: browse every order in the store and run every per-order action. From here you can view a single order in full, drive the admin **Create Order** flow (reorder an existing order, or place a prepared draft cart), run order lifecycle actions (cancel an order, add and list its comments), and generate the documents an order produces — invoices, shipments, and refunds. It mirrors the admin **Sales → Orders** screen. Invoices, shipments, and refunds are **generated from an order** here, but each also has its own store-wide menu listing every such document across all orders: see the [Invoices](/api/graphql-api/admin/sales/invoices/), [Shipments](/api/graphql-api/admin/sales/shipments/), and [Refunds](/api/graphql-api/admin/sales/refunds/) overviews. ## Creating an order for a customer (admin Create Order) An admin can place an order **on behalf of a customer** — the same "Create Order" flow as the admin panel. It works through a **draft cart**: create a draft cart for the customer, add products, save the billing/shipping addresses, choose a shipping method and a payment method, then place the order. The draft cart is an internal building block of order creation, not a separate menu. | Step | Operation | |------|-----------| | [Start a draft cart for a customer](/api/graphql-api/admin/customers/create-draft-cart) | `createAdminDraftCart` mutation | | [Get the draft cart](/api/graphql-api/admin/sales/carts/get-cart) | `adminCart(id:)` query | | [Add an item](/api/graphql-api/admin/sales/carts/add-item) · [update](/api/graphql-api/admin/sales/carts/update-items) · [remove](/api/graphql-api/admin/sales/carts/remove-item) | `addItemAdminCart` / `updateItemsAdminCart` / `removeItemAdminCart` mutations | | [Save addresses](/api/graphql-api/admin/sales/carts/save-address) | `saveAddressAdminCart` mutation | | [List](/api/graphql-api/admin/sales/carts/list-shipping-methods) / [set](/api/graphql-api/admin/sales/carts/set-shipping-method) shipping method | `adminCartShippingRates` query / `setShippingMethodAdminCart` mutation | | [List](/api/graphql-api/admin/sales/carts/list-payment-methods) / [set](/api/graphql-api/admin/sales/carts/set-payment-method) payment method | `adminCartPaymentMethods` query / `setPaymentMethodAdminCart` mutation | | [Place the order](/api/graphql-api/admin/sales/orders/place-order) | `createAdminPlaceOrder` mutation | (There's also [apply](/api/graphql-api/admin/sales/carts/apply-coupon) / [remove coupon](/api/graphql-api/admin/sales/carts/remove-coupon) on the draft cart.) [Reorder](/api/graphql-api/admin/sales/orders/reorder) is a shortcut that seeds a fresh draft cart from an existing order's items. ::: tip Only saleable products can be added [Add Item](/api/graphql-api/admin/sales/carts/add-item) accepts only products that are in stock and enabled. Adding an out-of-stock or disabled product returns a clear error and **leaves the draft cart intact** so you can add a different product — the cart is never lost. Booking products can't be added to an admin order (no admin Create-Order surface for them). ::: ## The order lifecycle — which action, in what order Once an order exists (placed through Create Order above, [Reorder](/api/graphql-api/admin/sales/orders/reorder), or the storefront), it moves through a lifecycle. Each action has prerequisites — this is the order they run in and what gates each one. **1. Invoice — record payment.** [Create Invoice](/api/graphql-api/admin/sales/orders/create-invoice) records that payment was collected for some or all of the order's items. An order generally can't be refunded until it has been invoiced (you refund money that was billed). You can invoice part of an order now and the rest later. Not available for orders paid via `paypal_standard` (those are captured by the gateway, not the admin). **2. Ship — fulfil.** [Create Shipment](/api/graphql-api/admin/sales/orders/create-shipment) marks items as dispatched and records the carrier and tracking number. It needs items still awaiting shipment and enough stock at the chosen inventory source. Partial shipments are allowed. **3. Refund — return money.** [Create Refund](/api/graphql-api/admin/sales/orders/create-refund) returns money for invoiced items and/or an arbitrary adjustment. Call [Refund Preview](/api/graphql-api/admin/sales/orders/refund-preview) first to see the computed totals without writing anything. Requires something left to refund (an un-refunded invoiced amount or a returnable quantity). **Cancel — abandon early.** [Cancel Order](/api/graphql-api/admin/sales/orders/cancel) is only possible while there is still something to cancel (nothing has been fully invoiced or shipped). A closed or fraud-flagged order can't be cancelled. **Comments — any time.** [Add Comment](/api/graphql-api/admin/sales/orders/add-comment) / [List Comments](/api/graphql-api/admin/sales/orders/list-comments) work at any stage; set `customerNotified` to email the customer the note. Every action refuses with a clear error when its prerequisite isn't met — nothing left to invoice/ship/refund, the order is already closed or flagged, insufficient stock, or a payment method that can't be invoiced. A typical fulfilled order runs **Create → Invoice → Ship** (then an optional **Refund**); an abandoned one runs **Create → Cancel**. ## Operations in this menu | Action | Operation | |--------|-----------| | [List Orders](/api/graphql-api/admin/sales/orders/list-orders) | `adminOrders` query | | [Order Detail](/api/graphql-api/admin/sales/orders/order-detail) | `adminOrderDetail` query | | [Reorder](/api/graphql-api/admin/sales/orders/reorder) | `createAdminReorder` mutation | | [Place Order](/api/graphql-api/admin/sales/orders/place-order) | `createAdminPlaceOrder` mutation | | [Cancel Order](/api/graphql-api/admin/sales/orders/cancel) | `createAdminCancelOrder` mutation | | [Add Comment](/api/graphql-api/admin/sales/orders/add-comment) | `createAdminOrderComment` mutation | | [List Comments](/api/graphql-api/admin/sales/orders/list-comments) | `adminOrderComments` query | | [Create Invoice](/api/graphql-api/admin/sales/orders/create-invoice) | `createAdminInvoice` mutation | | [Create Shipment](/api/graphql-api/admin/sales/orders/create-shipment) | `createAdminShipment` mutation | | [Create Refund](/api/graphql-api/admin/sales/orders/create-refund) | `createAdminRefund` mutation | | [Refund Preview](/api/graphql-api/admin/sales/orders/refund-preview) | `previewAdminRefund` mutation | All Orders endpoints require an admin Bearer token — see [Authentication](/api/graphql-api/admin/authentication). --- # Add Order Comment URL: /api/graphql-api/admin/sales/orders/add-comment --- outline: false examples: - id: admin-add-order-comment title: Add Order Comment description: Add a comment to an order. When `customerNotified` is true, the customer is sent a notification email with the comment. query: | mutation AddOrderComment($input: createAdminOrderCommentInput!) { createAdminOrderComment(input: $input) { adminOrderComment { id orderId comment customerNotified createdAt } } } variables: | { "input": { "orderId": 2392, "comment": "Customer called to confirm shipping address.", "customerNotified": true } } response: | { "data": { "createAdminOrderComment": { "adminOrderComment": { "id": "/api/admin/admin_order_comments/17", "orderId": 2392, "comment": "Customer called to confirm shipping address.", "customerNotified": true, "createdAt": "2026-05-21 10:14:31" } } } } --- # Add Order Comment Adds a comment to an order. When `customerNotified=true`, the customer is sent a notification email with the comment. **No permission gate** — any authenticated admin can add a comment. ## Operation | Operation | Type | |-----------|------| | `createAdminOrderComment` | Mutation | ## Errors | Condition | Lang key | Message | |-----------|----------|---------| | `comment` empty | `bagistoapi::app.admin.order.actions.comment.empty` | Comment is required. | --- # Cancel Order URL: /api/graphql-api/admin/sales/orders/cancel --- outline: false examples: - id: admin-cancel-order title: Cancel Order description: Cancel every cancellable item on an order. Returns the updated order summary. query: | mutation CancelOrder($input: createAdminCancelOrderInput!) { createAdminCancelOrder(input: $input) { adminCancelOrder { id orderId incrementId status statusLabel grandTotal success message } } } variables: | { "input": { "orderId": 2392 } } response: | { "data": { "createAdminCancelOrder": { "adminCancelOrder": { "id": "/api/admin/admin_cancel_orders/2392", "orderId": 2392, "incrementId": "2392", "status": "canceled", "statusLabel": "Canceled", "grandTotal": 219, "success": true, "message": "Order canceled successfully." } } } } --- # Cancel Order Cancels every cancellable item on an order. This is the same action as the **Cancel** button on the admin order-view screen, with the same eligibility gates as REST. ::: tip GraphQL returns a summary; REST returns the full detail The GraphQL mutation returns a slim order **summary** (`orderId`, `incrementId`, `status`, `statusLabel`, `grandTotal`, `success`, `message`). The REST endpoint (`POST /api/admin/orders/{id}/cancel`) returns the **full** updated order-detail payload. Use REST, or re-query `adminOrderDetail`, when you need the complete order after cancelling. ::: ::: tip Prerequisites The example targets an order with cancellable items. If your order has no items with `qty_to_cancel > 0` (already canceled / fully shipped / closed / fraud) the mutation returns an `errors[]` entry like *"There is nothing to cancel on this order."* — pick an order in `pending` or `processing` state. ::: ## Operation | Operation | Type | Purpose | |-----------|------|---------| | `createAdminCancelOrder` | Mutation | Cancel an order | ## Errors Each failure returns the `errors[]` array with one of these messages: | Condition | Lang key | Message | |-----------|----------|---------| | Order is `closed` | `bagistoapi::app.admin.order.actions.cancel.closed` | Closed orders cannot be canceled. | | Order is `fraud` | `bagistoapi::app.admin.order.actions.cancel.fraud` | Fraud orders cannot be canceled. | | No item has `qty_to_cancel > 0` | `bagistoapi::app.admin.order.actions.cancel.nothing-to-cancel` | There is nothing to cancel on this order. | | Admin role lacks `sales.orders.cancel` | `bagistoapi::app.admin.order.actions.cancel.no-permission` | You do not have permission to cancel orders. | --- # Create Invoice URL: /api/graphql-api/admin/sales/orders/create-invoice --- outline: false examples: - id: admin-create-invoice title: Create Invoice description: Create an invoice for one or more order items. query: | mutation CreateInvoice($input: createAdminInvoiceInput!) { createAdminInvoice(input: $input) { adminInvoice { id } } } variables: | { "input": { "orderId": 2392, "items": [ { "orderItemId": 42, "quantity": 3 }, { "orderItemId": 43, "quantity": 1 } ] } } response: | { "data": { "createAdminInvoice": { "adminInvoice": { "id": "/api/admin/invoices/88" } } } } --- # Create Invoice Creates an invoice for one or more order items. The same eligibility checks as the admin Invoice screen apply (the order must not be closed, marked fraud, or paid through PayPal Standard — those orders are hard-blocked from invoicing). Each item's requested quantity is validated against its still-invoiceable quantity, `qty_to_invoice`, before the invoice is created. After the mutation, fetch the full invoice via `adminInvoice(id:)` or the REST `GET /api/admin/invoices/{id}` endpoint to get the embedded items and totals. ::: tip Prerequisites The example targets an order with invoiceable items. If your order has no items with `qty_to_invoice > 0` (already fully invoiced / closed / fraud / paypal_standard payment method) the mutation returns *"There is nothing to invoice on this order."* — pick an order with outstanding qty to invoice. ::: ## Operation | Operation | Type | |-----------|------| | `createAdminInvoice` | Mutation | ## Errors | Condition | Lang key | Message | |-----------|----------|---------| | Order is `closed` | `bagistoapi::app.admin.order.actions.invoice.closed` | Closed orders cannot be invoiced. | | Order is `fraud` | `bagistoapi::app.admin.order.actions.invoice.fraud` | Fraud orders cannot be invoiced. | | Order paid with PayPal Standard | `bagistoapi::app.admin.order.actions.invoice.paypal-standard-blocked` | Invoices cannot be created for orders paid through PayPal Standard. | | Nothing to invoice | `bagistoapi::app.admin.order.actions.invoice.nothing-to-invoice` | There is nothing to invoice on this order. | | No permission | `bagistoapi::app.admin.order.actions.invoice.no-permission` | You do not have permission to create invoices. | | Items missing | `bagistoapi::app.admin.order.actions.invoice.items-required` | At least one item with a positive quantity is required. | | Qty exceeds available | `bagistoapi::app.admin.order.actions.invoice.qty-exceeds` | Requested quantity (`:requested`) exceeds available quantity (`:available`) for SKU `:sku`. | | Save failed | `bagistoapi::app.admin.order.actions.invoice.failed` | Could not create the invoice. | --- # Create Refund URL: /api/graphql-api/admin/sales/orders/create-refund --- outline: false examples: - id: admin-create-refund title: Create Refund description: Refund one or more order items, with optional shipping refund and adjustment fee/refund. query: | mutation CreateRefund($input: createAdminRefundInput!) { createAdminRefund(input: $input) { adminRefund { id } } } variables: | { "input": { "orderId": 2392, "items": [ { "orderItemId": 42, "quantity": 1 } ], "shipping": 0, "adjustmentRefund": 0, "adjustmentFee": 0 } } response: | { "data": { "createAdminRefund": { "adminRefund": { "id": "/api/admin/refunds/22" } } } } --- # Create Refund Refunds one or more order items, with an optional shipping refund and an adjustment fee/refund. The same eligibility checks as the admin Refund screen apply (the order must not be closed or marked fraud, and each item's requested quantity must be ≤ its still-refundable quantity, `qty_to_refund`). The refund totals are computed from the items + shipping + adjustments, and the total cannot exceed the order's remaining refundable balance. After the mutation, fetch the full refund via `adminRefund(id:)` or the REST `GET /api/admin/refunds/{id}` endpoint. ::: tip Prerequisites The example item quantities must be ≤ each item's `qty_to_refund`. Already-refunded quantities are rejected with *"We found an invalid quantity to refund items."* — fetch the order detail to see the current per-item refundable qty before submitting. ::: ## Operation | Operation | Type | |-----------|------| | `createAdminRefund` | Mutation | ## Errors | Condition | Lang key | Message | |-----------|----------|---------| | Order `closed` | `bagistoapi::app.admin.order.actions.refund.closed` | Closed orders cannot be refunded. | | Order `fraud` | `bagistoapi::app.admin.order.actions.refund.fraud` | Fraud orders cannot be refunded. | | Nothing to refund | `bagistoapi::app.admin.order.actions.refund.nothing-to-refund` | There is nothing left to refund on this order. | | No permission | `bagistoapi::app.admin.order.actions.refund.no-permission` | You do not have permission to create refunds. | | Qty exceeds available | `bagistoapi::app.admin.order.actions.refund.qty-exceeds` | Requested quantity (`:requested`) exceeds available quantity (`:available`) for SKU `:sku`. | | Amount zero | `bagistoapi::app.admin.order.actions.refund.amount-zero` | The computed refund amount is zero. Adjust quantity, shipping or adjustment values. | | Amount exceeds maximum | `bagistoapi::app.admin.order.actions.refund.amount-exceeds-max` | The refund amount (`:amount`) exceeds the maximum refundable amount (`:max`). | | Save failed | `bagistoapi::app.admin.order.actions.refund.failed` | Could not create the refund. | --- # Create Shipment URL: /api/graphql-api/admin/sales/orders/create-shipment --- outline: false examples: - id: admin-create-shipment title: Create Shipment description: Ship one or more order items from a chosen inventory source. query: | mutation CreateShipment($input: createAdminShipmentInput!) { createAdminShipment(input: $input) { adminShipment { id } } } variables: | { "input": { "orderId": 2392, "source": 1, "items": [ { "orderItemId": 42, "inventorySourceId": 1, "quantity": 3 } ], "carrierTitle": "UPS", "trackNumber": "1Z999AA1" } } response: | { "data": { "createAdminShipment": { "adminShipment": { "id": "/api/admin/shipments/55" } } } } --- # Create Shipment Ships one or more order items from a chosen inventory source. The same eligibility checks as the admin Shipment screen apply (the order must not be closed or marked fraud). Each item's requested quantity is validated against its still-shippable quantity, `qty_to_ship`, AND against the inventory available at the chosen source before the shipment is created. For **composite products** (bundle, configurable, grouped), the requested quantity is split across the line's component items, and each component's own shippable quantity and stock at the chosen source are validated — so a shipment can't be created for more component stock than is physically available. After the mutation, fetch the full shipment via `adminShipment(id:)` or the REST `GET /api/admin/shipments/{id}` endpoint. ::: tip Prerequisites The example targets an order with shippable items. If your order has no items with `qty_to_ship > 0` (already fully shipped / not yet invoiced) the mutation returns *"At least one item with a positive quantity is required."* — pick an order with outstanding qty to ship. ::: ## Operation | Operation | Type | |-----------|------| | `createAdminShipment` | Mutation | ## Errors | Condition | Lang key | Message | |-----------|----------|---------| | Order `closed` | `bagistoapi::app.admin.order.actions.shipment.closed` | Closed orders cannot be shipped. | | Order `fraud` | `bagistoapi::app.admin.order.actions.shipment.fraud` | Fraud orders cannot be shipped. | | Nothing to ship | `bagistoapi::app.admin.order.actions.shipment.nothing-to-ship` | There is nothing to ship on this order. | | No permission | `bagistoapi::app.admin.order.actions.shipment.no-permission` | You do not have permission to ship orders. | | Source missing | `bagistoapi::app.admin.order.actions.shipment.source-required` | Inventory source is required. | | Items missing | `bagistoapi::app.admin.order.actions.shipment.items-required` | At least one item with a positive quantity is required. | | Qty exceeds available | `bagistoapi::app.admin.order.actions.shipment.qty-exceeds` | Requested quantity (`:requested`) exceeds available quantity (`:available`) for SKU `:sku`. | | Inventory insufficient | `bagistoapi::app.admin.order.actions.shipment.inventory-insufficient` | Inventory at the selected source is insufficient for SKU `:sku`. | | Save failed | `bagistoapi::app.admin.order.actions.shipment.failed` | Could not create the shipment. | --- # Get Invoice URL: /api/graphql-api/admin/sales/orders/get-invoice --- outline: false examples: - id: admin-get-invoice title: Get Invoice description: Fetch a single invoice with the full totals breakdown, order/customer context, billing & shipping addresses, and embedded line items. query: | query GetInvoice($id: ID!) { adminInvoice(id: $id) { id _id incrementId orderId orderIncrementId state emailSent totalQty orderCurrencyCode baseCurrencyCode channelCurrencyCode subTotal formattedSubTotal baseSubTotal formattedBaseSubTotal subTotalInclTax formattedSubTotalInclTax baseSubTotalInclTax formattedBaseSubTotalInclTax grandTotal formattedGrandTotal baseGrandTotal formattedBaseGrandTotal taxAmount formattedTaxAmount baseTaxAmount formattedBaseTaxAmount discountAmount formattedDiscountAmount baseDiscountAmount formattedBaseDiscountAmount shippingAmount formattedShippingAmount baseShippingAmount formattedBaseShippingAmount shippingAmountInclTax formattedShippingAmountInclTax baseShippingAmountInclTax formattedBaseShippingAmountInclTax shippingTaxAmount formattedShippingTaxAmount baseShippingTaxAmount formattedBaseShippingTaxAmount transactionId reminders nextReminderAt createdAt updatedAt orderStatus orderStatusLabel orderDate channelName customerName customerEmail billingAddress shippingAddress items } } variables: | { "id": "/api/admin/invoices/1" } response: | { "data": { "adminInvoice": { "id": "/api/admin/invoices/1", "_id": 1, "incrementId": "1", "orderId": 58, "orderIncrementId": "58", "state": "paid", "emailSent": true, "totalQty": 2, "orderCurrencyCode": "USD", "baseCurrencyCode": "USD", "channelCurrencyCode": "USD", "subTotal": 8000, "formattedSubTotal": "$8,000.00", "baseSubTotal": 8000, "formattedBaseSubTotal": "$8,000.00", "subTotalInclTax": 8000, "formattedSubTotalInclTax": "$8,000.00", "baseSubTotalInclTax": 8000, "formattedBaseSubTotalInclTax": "$8,000.00", "grandTotal": 8000, "formattedGrandTotal": "$8,000.00", "baseGrandTotal": 8000, "formattedBaseGrandTotal": "$8,000.00", "taxAmount": 0, "formattedTaxAmount": "$0.00", "baseTaxAmount": 0, "formattedBaseTaxAmount": "$0.00", "discountAmount": 0, "formattedDiscountAmount": "$0.00", "baseDiscountAmount": 0, "formattedBaseDiscountAmount": "$0.00", "shippingAmount": 0, "formattedShippingAmount": "$0.00", "baseShippingAmount": 0, "formattedBaseShippingAmount": "$0.00", "shippingAmountInclTax": 0, "formattedShippingAmountInclTax": "$0.00", "baseShippingAmountInclTax": 0, "formattedBaseShippingAmountInclTax": "$0.00", "shippingTaxAmount": 0, "formattedShippingTaxAmount": "$0.00", "baseShippingTaxAmount": 0, "formattedBaseShippingTaxAmount": "$0.00", "transactionId": null, "reminders": 0, "nextReminderAt": null, "createdAt": "2024-07-01 06:41:14", "updatedAt": "2026-05-29 13:30:32", "orderStatus": "processing", "orderStatusLabel": "Processing", "orderDate": "2024-07-01 06:41:14", "channelName": "bagisto store", "customerName": "John Doe", "customerEmail": "john.doe@example.com", "billingAddress": { "id": 268, "addressType": "order_billing", "firstName": "John", "lastName": "Doe", "companyName": "Acme Trades", "address": "21 Market Street", "city": "Los Angeles", "state": "CA", "country": "US", "postcode": "90001", "email": "john.doe@example.com", "phone": "5551234567" }, "shippingAddress": { "id": 267, "addressType": "order_shipping", "firstName": "John", "lastName": "Doe", "companyName": "Acme Trades", "address": "21 Market Street", "city": "Los Angeles", "state": "CA", "country": "US", "postcode": "90001", "email": "john.doe@example.com", "phone": "5551234567" }, "items": [ { "id": 1, "orderItemId": 70, "sku": "Head13", "name": "Bagisto Cowboy Hat", "qty": 2, "price": 4000, "formattedPrice": "$4,000.00", "basePrice": 4000, "basePriceInclTax": 4000, "total": 8000, "formattedTotal": "$8,000.00", "baseTotal": 8000, "baseTotalInclTax": 8000, "taxAmount": 0, "formattedTaxAmount": "$0.00", "discountAmount": 0, "formattedDiscountAmount": "$0.00", "productId": 122, "productType": "simple", "baseImageUrl": "https://example.com/storage/product/122/cowboy-hat.webp", "additional": { "locale": "en", "quantity": 2, "product_id": "122" } } ] } } } --- # Get Invoice GraphQL counterpart of `GET /api/admin/invoices/{id}`. Returns a single invoice with the full totals breakdown, the order/customer context, the billing & shipping addresses, and the invoiced line items — everything the listing leaves out. ## Operation | Operation | Type | |-----------|------| | `adminInvoice(id: ID!)` | Query | Pass the invoice IRI (`/api/admin/invoices/{id}`) as `id`. Requires the `sales.invoices.view` permission. All admin endpoints require an admin Bearer token — see [Authentication](/api/graphql-api/admin/authentication). ::: tip Identifier vs IRI `id` is the resource IRI (`/api/admin/invoices/1`); `_id` is the plain numeric id (`1`). Use the IRI for the `id` argument. ::: ::: warning Objects & lists are returned whole `billingAddress`, `shippingAddress`, and `items` are returned as JSON — **query them bare, without a sub-selection** (`items`, not `items { … }`). The whole object/array comes back with all the keys documented below. (`items` is **not** a cursor connection — there is no `edges { node { … } }` wrapper.) ::: ## Invoice fields | Field | Type | Description | |-------|------|-------------| | `id` | `ID` | Resource identifier (IRI form). | | `_id` | `Int` | Numeric invoice id. | | `incrementId` | `String` | Human-facing invoice number. | | `orderId` | `Int` | Id of the parent order. | | `orderIncrementId` | `String` | Human-facing number of the parent order. | | `state` | `String` | Invoice state — `pending`, `pending_payment`, `paid`, `overdue`. | | `emailSent` | `Boolean` | Whether the invoice email was sent to the customer. | | `totalQty` | `Int` | Total quantity invoiced. | ### Currency codes | Field | Type | Description | |-------|------|-------------| | `orderCurrencyCode` | `String` | Currency the order/invoice was placed in (e.g. `USD`). | | `baseCurrencyCode` | `String` | The store's base currency. | | `channelCurrencyCode` | `String` | The sales channel's currency. | ### Totals Every money total is provided in the **order** currency and the store's **base** currency, each with a `formatted*` string carrying the currency symbol. Sub-total and shipping additionally expose an **incl-tax** variant. | Field | Type | Description | |-------|------|-------------| | `subTotal` / `formattedSubTotal` | `Float` / `String` | Line-items subtotal (order currency). | | `baseSubTotal` / `formattedBaseSubTotal` | `Float` / `String` | Subtotal (base currency). | | `subTotalInclTax` / `formattedSubTotalInclTax` | `Float` / `String` | Subtotal including tax (order currency). | | `baseSubTotalInclTax` / `formattedBaseSubTotalInclTax` | `Float` / `String` | Subtotal including tax (base currency). | | `grandTotal` / `formattedGrandTotal` | `Float` / `String` | Invoice total (order currency). | | `baseGrandTotal` / `formattedBaseGrandTotal` | `Float` / `String` | Invoice total (base currency). | | `taxAmount` / `formattedTaxAmount` | `Float` / `String` | Tax total (order currency). | | `baseTaxAmount` / `formattedBaseTaxAmount` | `Float` / `String` | Tax total (base currency). | | `discountAmount` / `formattedDiscountAmount` | `Float` / `String` | Discount total (order currency). | | `baseDiscountAmount` / `formattedBaseDiscountAmount` | `Float` / `String` | Discount total (base currency). | | `shippingAmount` / `formattedShippingAmount` | `Float` / `String` | Shipping total (order currency). | | `baseShippingAmount` / `formattedBaseShippingAmount` | `Float` / `String` | Shipping total (base currency). | | `shippingAmountInclTax` / `formattedShippingAmountInclTax` | `Float` / `String` | Shipping incl. tax (order currency). | | `baseShippingAmountInclTax` / `formattedBaseShippingAmountInclTax` | `Float` / `String` | Shipping incl. tax (base currency). | | `shippingTaxAmount` / `formattedShippingTaxAmount` | `Float` / `String` | Tax on shipping (order currency). | | `baseShippingTaxAmount` / `formattedBaseShippingTaxAmount` | `Float` / `String` | Tax on shipping (base currency). | ### Status & timestamps | Field | Type | Description | |-------|------|-------------| | `transactionId` | `String` | Payment transaction reference (null until captured). | | `reminders` | `Int` | Number of payment reminders sent (for pending invoices). | | `nextReminderAt` | `String` | When the next payment reminder is scheduled (null if none). | | `createdAt` | `String` | When the invoice was created. | | `updatedAt` | `String` | When the invoice was last updated. | ### Order & customer context Resolved from the parent order so the invoice can be rendered without a second call. | Field | Type | Description | |-------|------|-------------| | `orderStatus` | `String` | Parent order status code (e.g. `processing`). | | `orderStatusLabel` | `String` | Human-readable order status. | | `orderDate` | `String` | When the parent order was placed. | | `channelName` | `String` | Sales channel the order belongs to. | | `customerName` | `String` | Customer's full name. | | `customerEmail` | `String` | Customer's email. | ### Addresses (`billingAddress`, `shippingAddress`) JSON objects (query bare). Each contains: | Key | Type | Description | |-----|------|-------------| | `id` | `Int` | Address id. | | `addressType` | `String` | `order_billing` or `order_shipping`. | | `firstName` / `lastName` | `String` | Recipient name. | | `companyName` | `String` | Company (nullable). | | `address` | `String` | Street address. | | `city` / `state` / `country` / `postcode` | `String` | Location. | | `email` / `phone` | `String` | Contact details. | ### Line items (`items`) JSON array (query bare). Each element contains: | Key | Type | Description | |-----|------|-------------| | `id` | `Int` | Invoice-item id. | | `orderItemId` | `Int` | Id of the order item this line was invoiced from. | | `sku` | `String` | Product SKU. | | `name` | `String` | Product name as ordered. | | `qty` | `Int` | Quantity invoiced for this line. | | `price` / `formattedPrice` | `Float` / `String` | Unit price (order currency). | | `basePrice` | `Float` | Unit price (base currency). | | `basePriceInclTax` | `Float` | Unit price incl. tax (base currency). | | `total` / `formattedTotal` | `Float` / `String` | Line total (order currency). | | `baseTotal` | `Float` | Line total (base currency). | | `baseTotalInclTax` | `Float` | Line total incl. tax (base currency). | | `taxAmount` / `formattedTaxAmount` | `Float` / `String` | Tax for this line. | | `discountAmount` / `formattedDiscountAmount` | `Float` / `String` | Discount for this line. | | `productId` | `Int` | Id of the product. | | `productType` | `String` | Product type — `simple`, `configurable`, `bundle`, etc. | | `baseImageUrl` | `String` | URL of the product's base image (null if none). | | `additional` | `JSON` | Extra item data (selected options, configurable attributes, etc.). | --- # Get Refund URL: /api/graphql-api/admin/sales/orders/get-refund --- outline: false examples: - id: admin-get-refund title: Get Refund description: Fetch a single refund with the full totals/adjustment breakdown, order/customer context, payment info, billing & shipping addresses, and embedded line items. query: | query GetRefund($id: ID!) { adminRefund(id: $id) { id _id orderId orderIncrementId state emailSent totalQty orderCurrencyCode baseCurrencyCode channelCurrencyCode subTotal formattedSubTotal baseSubTotal formattedBaseSubTotal subTotalInclTax baseSubTotalInclTax grandTotal formattedGrandTotal baseGrandTotal formattedBaseGrandTotal taxAmount baseTaxAmount discountAmount baseDiscountAmount shippingAmount baseShippingAmount shippingAmountInclTax shippingTaxAmount adjustmentRefund formattedAdjustmentRefund baseAdjustmentRefund adjustmentFee formattedAdjustmentFee baseAdjustmentFee createdAt updatedAt billedTo orderStatus orderStatusLabel orderDate channelName customerName customerEmail paymentMethod paymentTitle shippingMethod shippingTitle billingAddress shippingAddress items } } variables: | { "id": "/api/admin/refunds/1" } response: | { "data": { "adminRefund": { "id": "/api/admin/refunds/1", "_id": 1, "orderId": 105, "orderIncrementId": "105", "state": "refunded", "emailSent": true, "totalQty": 3, "orderCurrencyCode": "USD", "baseCurrencyCode": "USD", "channelCurrencyCode": "USD", "subTotal": 4203, "formattedSubTotal": "$4,203.00", "baseSubTotal": 4203, "formattedBaseSubTotal": "$4,203.00", "subTotalInclTax": 4203, "baseSubTotalInclTax": 4203, "grandTotal": 4233, "formattedGrandTotal": "$4,233.00", "baseGrandTotal": 4233, "formattedBaseGrandTotal": "$4,233.00", "taxAmount": 0, "baseTaxAmount": 0, "discountAmount": 0, "baseDiscountAmount": 0, "shippingAmount": 30, "baseShippingAmount": 30, "shippingAmountInclTax": 30, "shippingTaxAmount": 0, "adjustmentRefund": 0, "formattedAdjustmentRefund": "$0.00", "baseAdjustmentRefund": 0, "adjustmentFee": 0, "formattedAdjustmentFee": "$0.00", "baseAdjustmentFee": 0, "createdAt": "2026-05-20 14:00:00", "updatedAt": "2026-05-20 14:00:02", "billedTo": "John Doe", "orderStatus": "closed", "orderStatusLabel": "Closed", "orderDate": "2026-05-19 16:47:17", "channelName": "bagisto store", "customerName": "John Doe", "customerEmail": "john.doe@example.com", "paymentMethod": "cashondelivery", "paymentTitle": "Cash On Delivery", "shippingMethod": "flatrate_flatrate", "shippingTitle": "Flat Rate - Flat Rate", "billingAddress": { "id": 493, "addressType": "order_billing", "firstName": "John", "lastName": "Doe", "companyName": "Acme Trades", "address": "21 Market Street", "city": "Los Angeles", "state": "CA", "country": "US", "postcode": "90001", "email": "john.doe@example.com", "phone": "5551234567" }, "shippingAddress": { "id": 492, "addressType": "order_shipping", "firstName": "John", "lastName": "Doe", "companyName": "Acme Trades", "address": "21 Market Street", "city": "Los Angeles", "state": "CA", "country": "US", "postcode": "90001", "email": "john.doe@example.com", "phone": "5551234567" }, "items": [ { "id": 1, "orderItemId": 119, "sku": "Nike-Shoes", "name": "Nike Shoes", "qty": 1, "price": 123, "formattedPrice": "$123.00", "basePrice": 123, "basePriceInclTax": 123, "total": 123, "formattedTotal": "$123.00", "baseTotal": 123, "baseTotalInclTax": 123, "taxAmount": 0, "discountAmount": 0, "productId": 114, "productType": "simple", "baseImageUrl": "https://example.com/storage/product/114/nike-shoes.webp", "additional": { "locale": "en", "quantity": 1, "product_id": "114" } } ] } } } --- # Get Refund GraphQL counterpart of `GET /api/admin/refunds/{id}`. Returns a single refund with the full totals/adjustment breakdown, order/customer context, payment info, billing & shipping addresses, and the refunded line items. Requires the `sales.refunds.view` permission. All admin endpoints require an admin Bearer token — see [Authentication](/api/graphql-api/admin/authentication). ## Operation | Operation | Type | |-----------|------| | `adminRefund(id: ID!)` | Query | Pass the refund IRI (`/api/admin/refunds/{id}`) as `id`. ::: tip Identifier vs IRI `id` is the resource IRI; `_id` is the plain numeric id. Use the IRI for the `id` argument. ::: ::: warning Objects & lists are returned whole `billingAddress`, `shippingAddress`, and `items` are returned as JSON — **query them bare, without a sub-selection** (`items`, not `items { … }`). The whole object/array comes back. `items` is **not** a cursor connection. ::: ## Fields Same field set as the REST [Get Refund](/api/rest-api/admin/sales/orders/get-refund) — every refund column (in order + base currency, with `formatted*` and incl-tax variants), the adjustment refund/fee, order/customer context, payment info, the two address objects, and the line `items`. --- # Get Shipment URL: /api/graphql-api/admin/sales/orders/get-shipment --- outline: false examples: - id: admin-shipment-detail-gql title: Get Shipment description: Fetch a single shipment by id, with the order/customer context, both addresses, and the shipped line items inlined. query: | query AdminShipment($id: ID!) { adminShipment(id: $id) { id _id orderId orderIncrementId shippedTo orderDate orderStatus orderStatusLabel channelName customerName customerEmail paymentMethod paymentTitle orderCurrencyCode shippingMethod shippingTitle baseShippingAmount formattedBaseShippingAmount status totalQty totalWeight carrierCode carrierTitle trackNumber emailSent inventorySourceId inventorySourceName billingAddress shippingAddress createdAt updatedAt items } } variables: | { "id": "/api/admin/shipments/7" } response: | { "data": { "adminShipment": { "id": "/api/admin/shipments/7", "_id": 7, "orderId": 8, "orderIncrementId": "00000000008", "shippedTo": "John Doe", "orderDate": "2026-05-20 10:00:00", "orderStatus": "processing", "orderStatusLabel": "Processing", "channelName": "Default", "customerName": "John Doe", "customerEmail": "john.doe@example.com", "paymentMethod": "cashondelivery", "paymentTitle": "Cash On Delivery", "orderCurrencyCode": "USD", "shippingMethod": "free_free", "shippingTitle": "Free Shipping - Free Shipping", "baseShippingAmount": 0, "formattedBaseShippingAmount": "$0.00", "status": null, "totalQty": 2, "totalWeight": null, "carrierCode": null, "carrierTitle": "UPS", "trackNumber": "1Z999AA1", "emailSent": false, "inventorySourceId": 1, "inventorySourceName": "Default", "billingAddress": { "id": 16, "addressType": "order_billing", "firstName": "John", "lastName": "Doe", "city": "Los Angeles", "country": "US", "postcode": "90001", "email": "john.doe@example.com", "phone": "5551234567" }, "shippingAddress": { "id": 15, "addressType": "order_shipping", "firstName": "John", "lastName": "Doe", "city": "Los Angeles", "country": "US", "postcode": "90001", "email": "john.doe@example.com", "phone": "5551234567" }, "createdAt": "2026-05-20 12:00:00", "updatedAt": "2026-05-20 12:00:00", "items": [ { "id": 11, "orderItemId": 42, "sku": "TSHIRT-RED-M", "name": "Red T-Shirt", "qty": 2 } ] } } } --- # Get Shipment GraphQL counterpart of `GET /api/admin/shipments/{id}`. Returns a single shipment with the order/customer context, both addresses, and the shipped line `items` inlined — everything the listing leaves out. ## Operation | Operation | Type | |-----------|------| | `adminShipment(id: ID!)` | Query | Pass the shipment IRI (`/api/admin/shipments/{id}`) as `id`. Permission: `sales.shipments.view`. ::: warning billingAddress, shippingAddress and items are returned whole `billingAddress`, `shippingAddress` and `items` are returned as JSON — **query them bare, without a sub-selection** (`shippingAddress`, not `shippingAddress { … }`). The whole object/array comes back. The keys inside each are listed below for reference. ::: ## Fields | Field | Type | Description | |-------|------|-------------| | `id` | `ID` | Resource identifier (IRI form). | | `_id` | `Int` | Numeric shipment id. | | `orderId` | `Int` | Id of the parent order. | | `orderIncrementId` | `String` | Human-facing number of the parent order. | | `shippedTo` | `String` | The name on the order's shipping address. | | `orderDate` | `String` | When the order was placed. | | `orderStatus` | `String` | Parent order status code. | | `orderStatusLabel` | `String` | Parent order status display label. | | `channelName` | `String` | Sales channel the order belongs to. | | `customerName` | `String` | Name of the customer who placed the order. | | `customerEmail` | `String` | Email of the customer who placed the order. | | `paymentMethod` | `String` | The order's payment method code (e.g. `cashondelivery`). | | `paymentTitle` | `String` | The payment method's display title. | | `orderCurrencyCode` | `String` | Currency the order was placed in (e.g. `USD`). | | `shippingMethod` | `String` | The order's shipping method code — may be `null`. | | `shippingTitle` | `String` | The shipping method's display title — may be `null`. | | `baseShippingAmount` | `Float` | Shipping price in the store's base currency — may be `null`. | | `formattedBaseShippingAmount` | `String` | The same shipping price pre-formatted for display — may be `null`. | | `status` | `String` | Shipment status — often `null`. | | `totalQty` | `Float` | Total quantity shipped across all line items. | | `totalWeight` | `Float` | Combined weight of the shipment — may be `null`. | | `carrierCode` | `String` | Shipping carrier code — may be `null`. | | `carrierTitle` | `String` | Shipping carrier display name — may be `null`. | | `trackNumber` | `String` | Carrier tracking number — may be `null`. | | `emailSent` | `Boolean` | Whether the shipment notification email was sent. | | `inventorySourceId` | `Int` | Id of the inventory source the items shipped from. | | `inventorySourceName` | `String` | Name of the inventory source (warehouse) the items shipped from. | | `billingAddress` | `JSON` | The order's billing address — see below. | | `shippingAddress` | `JSON` | The order's shipping address — see below. | | `createdAt` | `String` | When the shipment was created. | | `updatedAt` | `String` | When the shipment was last updated. | | `items` | `JSON` | The shipped line items — see below. | ### Payment and Shipping Mirrors the "Payment and Shipping" panel on the admin Shipment view — the order's payment and shipping details carried alongside the shipment. These are flat scalar fields and resolve normally (select them directly, no sub-selection). `shippingMethod`, `shippingTitle`, `baseShippingAmount` and `formattedBaseShippingAmount` are `null` when the order had no shipping method (e.g. virtual/free). | Field | Type | Description | |-------|------|-------------| | `paymentMethod` | `String` | The order's payment method code (e.g. `cashondelivery`). | | `paymentTitle` | `String` | The payment method's display title (e.g. `Cash On Delivery`). | | `orderCurrencyCode` | `String` | Currency the order was placed in (e.g. `USD`). | | `shippingMethod` | `String` | The order's shipping method code (e.g. `free_free`) — may be `null`. | | `shippingTitle` | `String` | The shipping method's display title — may be `null`. | | `baseShippingAmount` | `Float` | Shipping price in the store's base currency — may be `null`. | | `formattedBaseShippingAmount` | `String` | The same shipping price pre-formatted for display — may be `null`. | ### Address objects (`billingAddress`, `shippingAddress`) Each is returned as a whole JSON object — query it as a bare field (`shippingAddress`), you cannot sub-select its keys in the query. The keys below are returned inside each object. | Field | Type | Description | |-------|------|-------------| | `id` | `Int` | Address row id. | | `addressType` | `String` | `order_billing` or `order_shipping`. | | `firstName` / `lastName` | `String` | Recipient name. | | `city` | `String` | City. | | `country` | `String` | Country code (e.g. `US`). | | `postcode` | `String` | Postal code. | | `email` | `String` | Contact email. | | `phone` | `String` | Contact phone. | ### Shipped items (`items`) `items` is returned as a whole JSON array — query it as a bare field (`items`), you cannot sub-select its keys in the query. Each entry has the keys below. | Field | Type | Description | |-------|------|-------------| | `id` | `Int` | Shipment-item row id. | | `orderItemId` | `Int` | The order line this shipped item maps to. | | `sku` | `String` | Product SKU. | | `name` | `String` | Product name. | | `qty` | `Float` | Quantity shipped for this line. | --- # List Order Comments URL: /api/graphql-api/admin/sales/orders/list-comments --- outline: false examples: - id: admin-list-order-comments title: List Order Comments description: Cursor-paginated list of an order's comments, newest first. query: | query ListOrderComments($first: Int) { adminOrderComments(first: $first) { edges { node { id } } pageInfo { hasNextPage endCursor } totalCount } } variables: | { "first": 10 } response: | { "data": { "adminOrderComments": { "edges": [ { "node": { "id": "/api/admin/order-comments/17", "comment": "Customer called to confirm shipping address.", "customerNotified": true, "createdAt": "2026-05-21 10:14:31" }, "cursor": "MA==" } ], "pageInfo": { "hasNextPage": false, "endCursor": "MA==" } } } } --- # List Order Comments Cursor-paginated list of an order's comments, newest first. Uses API Platform's native cursor pagination — use `first` + `after` to page through the result. ## Operation | Operation | Type | |-----------|------| | `adminOrderComments` | QueryCollection (cursor) | --- # List Orders URL: /api/graphql-api/admin/sales/orders/list-orders --- outline: false examples: - id: admin-list-orders title: List Orders description: Paginated list of all orders across every customer (cursor pagination). query: | query adminOrders($first: Int) { adminOrders(first: $first) { totalCount pageInfo { hasNextPage endCursor } edges { node { id incrementId status statusLabel channelName customerEmail customerName grandTotal formattedGrandTotal totalQtyOrdered createdAt } } } } variables: | { "first": 10 } response: | { "data": { "adminOrders": { "totalCount": 616, "pageInfo": { "hasNextPage": true, "endCursor": "OQ==" }, "edges": [ { "node": { "id": 2392, "incrementId": "2392", "status": "processing", "statusLabel": "Processing", "channelName": "bagisto store", "customerEmail": "admin@example.com", "customerName": "Test User", "grandTotal": 4000, "formattedGrandTotal": "$4,000.00", "totalQtyOrdered": 1, "createdAt": "2026-05-19 13:13:29" } } ] } } } - id: admin-list-orders-filtered title: List Orders (filtered) description: Filter by status, grand-total range and a date preset. query: | query adminOrders( $first: Int $status: String $grand_total_from: Float $grand_total_to: Float $date_range: String ) { adminOrders( first: $first status: $status grand_total_from: $grand_total_from grand_total_to: $grand_total_to date_range: $date_range ) { totalCount edges { node { id incrementId status grandTotal createdAt } } } } variables: | { "first": 10, "status": "processing", "grand_total_from": 100, "grand_total_to": 5000, "date_range": "this_month" } response: | { "data": { "adminOrders": { "totalCount": 12, "edges": [ { "node": { "id": 2392, "incrementId": "2392", "status": "processing", "grandTotal": 4000, "createdAt": "2026-05-19 13:13:29" } } ] } } } --- # List Orders Lists every order across all customers — the data behind the admin **Sales → Orders** screen. ## Operation | Operation | Type | Purpose | |-----------|------|---------| | `adminOrders` | Query | Cursor-paginated list of all orders | ## Details - Requires an admin Bearer token in the `Authorization` header. - **Cursor pagination** — pass `first` for the page size and `after` (the `endCursor` from the previous page) to advance. `pageInfo.hasNextPage` tells you when to stop; `totalCount` is the grand total. - Each `node` is a **slim** order row — flat fields only. Heavy relations (full items, invoices, shipments) are not embedded here; fetch a single order's detail for those. - The REST equivalent (`GET /api/admin/orders`) uses offset pagination with a `{ data, meta }` envelope instead of cursors. ## Filtering Pass any of these arguments alongside `first` / `after` to narrow the list (they mirror the admin datagrid filters): | Argument | Description | |----------|-------------| | `order_id` | Order increment ID — partial match | | `status` | `pending`, `pending_payment`, `processing`, `completed`, `canceled`, `closed`, `fraud` | | `grand_total` | Exact grand total (matched against the base grand total) | | `grand_total_from`, `grand_total_to` | Grand total range (minimum / maximum) | | `channel` | Channel ID | | `customer` | Customer name — partial match | | `email` | Customer email — partial match | | `date_range` | Preset: `today`, `yesterday`, `this_week`, `this_month`, `last_month`, `last_three_months`, `last_six_months`, `this_year` | | `date_from`, `date_to` | Custom date range (`Y-m-d`) — overrides `date_range` | | `sort`, `order` | Sort field + `asc` / `desc` (default `created_at desc`) | --- # Order Detail URL: /api/graphql-api/admin/sales/orders/order-detail --- outline: false examples: - id: admin-order-detail title: Get Order Detail description: Full order-view payload for one order. Nested collections (items, invoices, shipments) are returned as plain JSON arrays (identical shape to REST). query: | query adminOrderDetail($id: ID!) { adminOrderDetail(id: $id) { id _id incrementId status statusLabel grandTotal formattedGrandTotal paymentTitle customer { id email name group { id code name } } billingAddress { city state country postcode } shippingAddress { city state country postcode } items { id sku type name qtyOrdered price formattedPrice } invoices { id state grandTotal } shipments { id status carrierTitle } refunds { id state grandTotal } comments { id comment customerNotified createdAt } } } variables: | { "id": "/api/admin/orders/2392" } response: | { "data": { "adminOrderDetail": { "id": "/api/admin/orders/2392", "_id": 2392, "incrementId": "2392", "status": "processing", "statusLabel": "Processing", "grandTotal": 4000, "formattedGrandTotal": "$4,000.00", "paymentTitle": "Money Transfer", "customer": { "id": "19", "email": "admin@example.com", "name": "Test User", "group": { "id": "2", "code": "general", "name": "General" } }, "billingAddress": { "city": "New York", "state": "NY", "country": "US", "postcode": "10001" }, "shippingAddress": { "city": "New York", "state": "NY", "country": "US", "postcode": "10001" }, "items": [ { "id": "/api/order_detail_items/2694", "sku": "test65", "type": "simple", "name": "Classic Watch Hand", "qtyOrdered": 1, "price": 4000, "formattedPrice": "$4,000.00" } ], "invoices": [], "shipments": [], "refunds": [], "comments": [ { "id": 11, "comment": "Customer called to confirm the shipping address.", "customerNotified": false, "createdAt": "2026-05-19 14:02:10" } ] } } } --- # Order Detail Returns the complete order-view payload for a single order — the data behind the admin **Sales → Orders → View** screen. ## Operation | Operation | Type | Purpose | |-----------|------|---------| | `adminOrderDetail` | Query | Full detail of one order by ID | ## Details - Requires an admin Bearer token in the `Authorization` header. - The `id` argument is the resource IRI — `"/api/admin/orders/{id}"`. - Everything the order-view screen needs is embedded inline: customer (with group), billing/shipping addresses, items, invoices, shipments, refunds, and the comment thread (`comments`, newest first). ## GraphQL shape notes - **Nested collections are plain JSON arrays.** `items`, `invoices`, `shipments`, `refunds`, and `comments` are returned as flat arrays (`items { id ... }`), identical to the REST endpoint. They are NOT GraphQL cursor connections — do **not** wrap them in `edges { node { ... } }`. (Prior to 2026-05-28 these were exposed as connections; the shape was unified with REST so both transports return the same payload.) - **`id` vs `_id`.** `id` is the resource IRI (`/api/admin/orders/2392`); `_id` is the raw integer. ## Product-type-aware items Each item carries a `type` (`simple`, `configurable`, `bundle`, `downloadable`, `grouped`, `virtual`). Select the type-specific fields you need and switch on `type` in the client: | `type` | Type-specific fields | |--------|----------------------| | `simple`, `virtual` | — | | `configurable` | `child { ... }`, `additional` | | `bundle`, `grouped` | `children { ... }`, `additional` | | `downloadable` | `downloadableLinks` | --- # Place Order URL: /api/graphql-api/admin/sales/orders/place-order --- outline: false examples: - id: admin-place-order title: Place Order description: Finalise a fully prepared draft cart into a real order. query: | mutation PlaceOrder($input: createAdminPlaceOrderInput!) { createAdminPlaceOrder(input: $input) { adminPlaceOrder { orderId incrementId customerId grandTotal success message } } } variables: | { "input": { "cartId": 314 } } response: | { "data": { "createAdminPlaceOrder": { "adminPlaceOrder": { "orderId": 1284, "incrementId": "1284", "customerId": 19, "grandTotal": 110, "success": true, "message": "Order placed successfully." } } } } --- # Place Order Finalises a fully prepared draft cart into a real order — the same flow as the admin Create-Order screen's place-order step. Items, addresses, shipping and payment must already be set on the draft cart. ::: tip Prerequisites The example uses an illustrative cart id. Admin cart endpoints only operate on **draft carts** (`is_active=0`) — storefront carts are rejected. Create a draft cart first with the [`createAdminDraftCart`](../../customers/create-draft-cart.md) mutation and use the returned `cartId`. ::: ## Operation | Operation | Type | Purpose | |-----------|------|---------| | `createAdminPlaceOrder(input)` | Mutation | Finalise a draft cart into an order | ## Sequence (errors[]) Each missing step surfaces as a distinct GraphQL error (REST counterpart in parentheses): - Cart empty (409) - Cart total below the store's minimum order amount (422) - Addresses missing (409) - Shipping method missing (409) - Payment method missing (409) - Payment method not in `[cashondelivery, moneytransfer]` (422) --- # Print Invoice (PDF) URL: /api/graphql-api/admin/sales/orders/print-invoice --- outline: false examples: - id: admin-print-invoice-gql title: Print Invoice (PDF) description: Downloading the invoice PDF is a binary stream, which GraphQL cannot return — use the REST endpoint shown here. The example is the equivalent curl. query: | # Not available over GraphQL — GraphQL cannot return a binary PDF stream. # Download the PDF with the REST endpoint instead: curl -X GET "https://your-domain.com/api/admin/invoices/585/print" \ -H "Authorization: Bearer " \ -H "Accept: application/pdf" \ --output invoice-585.pdf variables: | {} response: | # Binary response: an application/pdf attachment is written to invoice-585.pdf # (Content-Disposition: attachment; filename="invoice-585.pdf"). # There is no JSON body. --- # Print Invoice (PDF) ## Not exposed over GraphQL GraphQL transports cannot return a binary PDF stream, so this action is **REST only**. Use the REST endpoint to download the PDF: ``` GET /api/admin/invoices/{id}/print Authorization: Bearer ``` The response is an `application/pdf` binary attachment — the same PDF the admin panel produces. Requires the `sales.invoices.view` permission. See the [REST → Print Invoice](/api/rest-api/admin/sales/orders/print-invoice) page for the full details. --- # Refund Preview URL: /api/graphql-api/admin/sales/orders/refund-preview --- outline: false examples: - id: admin-refund-preview title: Refund Preview description: Compute refund totals (subtotal, discount, tax, shipping, grandTotal) without saving anything. query: | mutation RefundPreview($input: previewAdminRefundInput!) { previewAdminRefund(input: $input) { adminRefund { id } } } variables: | { "input": { "orderId": 2392, "items": [ { "orderItemId": 42, "quantity": 1 } ], "shipping": 0, "adjustmentRefund": 0, "adjustmentFee": 0 } } response: | { "data": { "previewAdminRefund": { "adminRefund": { "id": "/api/refund-totals-summaries/2392", "subtotal": 29.99, "formattedSubtotal": "$29.99", "tax": 3.0, "formattedTax": "$3.00", "shipping": 0.0, "formattedShipping": "$0.00", "grandTotal": 32.99, "formattedGrandTotal": "$32.99" } } } } --- # Refund Preview Computes refund totals (subtotal, discount, tax, shipping, grandTotal, plus pre-formatted variants) for a hypothetical refund body without saving anything. Same body shape as Create Refund — useful for live-updating the "Total Refund" widget in the admin refund form. ## Operation | Operation | Type | |-----------|------| | `previewAdminRefund` | Mutation | Eligibility gates fire here too — if the order cannot be refunded at all (closed / fraud / no permission / nothing-to-refund), preview rejects with the same error messages as Create Refund. --- # Reorder URL: /api/graphql-api/admin/sales/orders/reorder --- outline: false examples: - id: admin-reorder title: Reorder description: Build a fresh admin draft cart from a previous order's items. Returns the new cart ID. query: | mutation createAdminReorder($input: createAdminReorderInput!) { createAdminReorder(input: $input) { adminReorder { success message cartId } } } variables: | { "input": { "orderId": "/api/admin/orders/2392" } } response: | { "data": { "createAdminReorder": { "adminReorder": { "success": true, "message": "Reorder successful. A new draft cart has been created.", "cartId": 314 } } } } --- # Reorder Build a fresh admin draft cart from a previous order's items — the same flow as the **Reorder** button on the admin order-view screen. ## Operation | Operation | Type | Purpose | |-----------|------|---------| | `createAdminReorder` | Mutation | Create a draft cart from a past order | ## Input | Field | Type | Notes | |-------|------|-------| | `orderId` | `ID!` | The order's resource IRI — `"/api/admin/orders/{id}"`. | > **Why `orderId`, not `id`?** API Platform GraphQL reserves `id` as the > resource IRI, so a mutation input field named `id` collides. We use > `orderId` for the order reference. ## Behaviour This is the same action as the admin Reorder button: 1. A new draft admin cart is created for the order's customer (not the customer's own active cart). 2. Every item from the order is re-added to that draft cart. Per-item failures are swallowed. 3. Returns `success`, `message`, and the new `cartId`. ## Errors The mutation enforces the same 3-check guard as the admin panel. Each failure returns the `errors[]` array (the `data.createAdminReorder` payload is `null`) with a distinct message per failure mode: | Condition | `errors[0].message` | |-----------|---------------------| | Order was placed as guest (`is_guest = 1`) | `Reorder is not supported for guest orders.` | | At least one item's product is no longer purchasable | `One or more items in this order are no longer available for purchase.` | | Admin's role lacks `sales.orders.create` | `You do not have permission to create orders.` | | `sales.order_settings.reorder.admin` config is off | `Reorder by admin is disabled in store settings.` | ### Sample error response ```json { "errors": [ { "message": "Reorder is not supported for guest orders.", "extensions": { "category": "invalid_input" } } ], "data": { "createAdminReorder": null } } ``` --- # Send Duplicate Invoice URL: /api/graphql-api/admin/sales/orders/send-duplicate-invoice --- outline: false examples: - id: admin-send-duplicate-invoice-gql title: Send Duplicate Invoice description: Emails a copy of the invoice. The recipient is the email you pass, or the order's customer email when omitted. query: | mutation SendDuplicateInvoice($input: createAdminInvoiceSendDuplicateInput!) { createAdminInvoiceSendDuplicate(input: $input) { adminInvoiceSendDuplicate { id email success message } } } variables: | { "input": { "invoiceId": 585, "email": "customer@example.com" } } response: | { "data": { "createAdminInvoiceSendDuplicate": { "adminInvoiceSendDuplicate": { "id": "/api/admin/invoices/585", "email": "customer@example.com", "success": true, "message": "Invoice email sent to customer@example.com." } } } } --- # Send Duplicate Invoice GraphQL counterpart of `POST /api/admin/invoices/{id}/send-duplicate`. Emails a copy of the invoice. Requires the `sales.invoices.view` permission. All admin endpoints require an admin Bearer token — see [Authentication](/api/graphql-api/admin/authentication). ## Operation | Operation | Type | |-----------|------| | `createAdminInvoiceSendDuplicate` | Mutation | ## Input (`createAdminInvoiceSendDuplicateInput`) | Field | Type | Required | Description | |-------|------|----------|-------------| | `invoiceId` | `Int!` | Yes | Id of the invoice to send. | | `email` | `String` | No | Recipient address. Defaults to the order's customer email when omitted. Must be a valid email when provided. | ## Payload fields | Field | Type | Description | |-------|------|-------------| | `id` | `ID` | Invoice IRI. | | `email` | `String` | The address the invoice was sent to. | | `success` | `Boolean` | Whether the email was queued. | | `message` | `String` | Human-readable result message. | ::: tip Recipient Whatever address you pass in `email` is the actual recipient. Leave it out to send to the order's customer. ::: --- # Refunds URL: /api/graphql-api/admin/sales/refunds --- outline: false --- # Refunds The Refunds menu is the store-wide, **read-only** list of every refund that has been issued across all orders. It mirrors the admin **Sales → Refunds** screen — browse refunds and open one for detail. It does not create refunds (that happens against an order — see below). ## When a row appears here A refund row exists only after a refund has been **issued** for an order. A refund records money returned to the customer for some or all of the order's items, plus an optional adjustment-refund (an extra amount returned beyond the line items) and an optional adjustment-fee (an amount withheld from the refund). Placing, invoicing, or shipping an order does not by itself create a refund; until one is issued, the order has no row in this menu. Issuing a refund is an **order action**, not part of this menu — it runs against a specific order under [Orders → Create Refund](/api/graphql-api/admin/sales/orders/create-refund) (with a [Refund Preview](/api/graphql-api/admin/sales/orders/refund-preview) to compute totals first). ## Refunded Amount The list's **Refunded Amount** is the refund's grand total in the store's base currency (subtotal + tax + shipping + adjustment-refund − adjustment-fee) — not just the line-items subtotal. In the API payload this is `formattedBaseGrandTotal` (raw: `baseGrandTotal`). ## Operations in this menu | Action | Operation | |--------|-----------| | [List refunds](/api/graphql-api/admin/sales/refunds/list) | `adminRefunds` query | | [Get a single refund](/api/graphql-api/admin/sales/orders/get-refund) | `adminRefund(id:)` query | | [Export refunds (CSV)](/api/rest-api/admin/sales/refunds/export) | REST only (binary download) | All Refunds operations require the `sales.refunds.view` permission and an admin Bearer token — see [Authentication](/api/graphql-api/admin/authentication). --- # List Refunds (Datagrid) URL: /api/graphql-api/admin/sales/refunds/list --- outline: false examples: - id: admin-refunds-list-gql title: List Refunds (Datagrid) description: Cursor-paginated refunds datagrid. Every refund column plus the billing/shipping addresses are populated on each row — only line items and payment info are detail-only. query: | query AdminRefunds($first: Int, $after: String) { adminRefunds(first: $first, after: $after) { edges { cursor node { id _id orderId orderIncrementId state emailSent totalQty orderCurrencyCode baseCurrencyCode subTotal formattedSubTotal baseSubTotal grandTotal formattedGrandTotal baseGrandTotal formattedBaseGrandTotal taxAmount discountAmount shippingAmount adjustmentRefund adjustmentFee billedTo orderStatus orderStatusLabel channelName customerName customerEmail billingAddress shippingAddress createdAt updatedAt } } pageInfo { hasNextPage endCursor } totalCount } } variables: | { "first": 10 } response: | { "data": { "adminRefunds": { "edges": [ { "cursor": "MA==", "node": { "id": "/api/admin/refunds/1", "_id": 1, "orderId": 105, "orderIncrementId": "105", "state": "refunded", "emailSent": true, "totalQty": 3, "orderCurrencyCode": "USD", "baseCurrencyCode": "USD", "subTotal": 4203, "formattedSubTotal": "$4,203.00", "baseSubTotal": 4203, "grandTotal": 4233, "formattedGrandTotal": "$4,233.00", "baseGrandTotal": 4233, "formattedBaseGrandTotal": "$4,233.00", "taxAmount": 0, "discountAmount": 0, "shippingAmount": 30, "adjustmentRefund": 0, "adjustmentFee": 0, "billedTo": "John Doe", "orderStatus": "closed", "orderStatusLabel": "Closed", "channelName": "bagisto store", "customerName": "John Doe", "customerEmail": "john.doe@example.com", "billingAddress": { "id": 493, "addressType": "order_billing", "firstName": "John", "lastName": "Doe", "city": "Los Angeles", "country": "US", "postcode": "90001" }, "shippingAddress": { "id": 492, "addressType": "order_shipping", "firstName": "John", "lastName": "Doe", "city": "Los Angeles", "country": "US", "postcode": "90001" }, "createdAt": "2026-05-20 14:00:00", "updatedAt": "2026-05-20 14:00:02" } } ], "pageInfo": { "hasNextPage": false, "endCursor": "MA==" }, "totalCount": 1 } } } --- # List Refunds (Datagrid) GraphQL counterpart of `GET /api/admin/refunds`. Returns a cursor-paginated list of refunds — the same rows shown on the admin **Sales → Refunds** datagrid. Every refund **column** plus the billing/shipping addresses are populated on each row; only the line `items` and payment info are detail-only. ## Operation `adminRefunds(first, after, id, order_id, state, base_grand_total_from, base_grand_total_to, billed_to, created_at_from, created_at_to, sort, order)` — a cursor `QueryCollection`. Every REST query parameter is also exposed as a GraphQL argument; see the [REST page](/api/rest-api/admin/sales/refunds/list) for the full argument table. ## Permission `sales.refunds.view` ::: warning Address objects are returned whole `billingAddress` and `shippingAddress` are returned as JSON — **query them bare, without a sub-selection** (`billingAddress`, not `billingAddress { … }`). The whole object comes back. ::: ## Fields Every refund column is populated on each row (currency codes, all `base_*` / `formatted*` / incl-tax variants, the adjustment refund/fee, order & customer context, and both address objects). Only `items` (the refunded line items) and the payment info (`paymentMethod`, `paymentTitle`, `shippingMethod`, `shippingTitle`) are **detail-only** — they return `null` on the listing and are filled when you fetch the refund by id with `adminRefund(id:)`. The full field reference is on the [Get Refund](/api/graphql-api/admin/sales/orders/get-refund) page. **Amounts — which one to show.** Use `formattedGrandTotal` for a viewer working in the order's currency, and `baseGrandTotal` / `formattedBaseGrandTotal` for reporting in the store's base currency. For a single-currency store the two are identical. ## Listing vs. full record The listing already carries every column — the only reason to fetch a single refund is to read its line `items` and payment info, which are skipped on the listing because loading items for every row of a large list would be expensive. Typical flow: list with `adminRefunds`, read `_id` from the row you want, then fetch the full record with `adminRefund(id:)`. --- # Shipments URL: /api/graphql-api/admin/sales/shipments --- outline: false --- # Shipments The Shipments menu is the store-wide list of every shipment that has been created across all orders. It mirrors the admin **Sales → Shipments** screen. ## When a row appears here A shipment row exists only after a shipment has been **created** for an order (the Create Shipment mutation). Placing or invoicing an order does not by itself create a shipment; until one is created, the order has no row in this menu. ## What a shipment records A shipment records which items, in what quantity, were dispatched — and from which inventory source. It can optionally carry a shipping carrier (`carrierCode` / `carrierTitle`) and a tracking number (`trackNumber`). An order can be shipped in parts: each partial dispatch creates its own shipment, so a single order may have several shipment rows, each covering a subset of its items from a (possibly different) inventory source. ## Shipped-to address The `shippingAddress` carried on each shipment, and the `shippedTo` name, come from the **order's shipping address** — the destination the items were dispatched to. The order's billing address is also included as `billingAddress` for context. ## Payment and Shipping The shipment detail also surfaces the order's Payment and Shipping info — the payment method and its title, the order currency, and the shipping method, its title and price — matching the admin Shipment view's "Payment and Shipping" panel. The shipping fields can be `null` when the order had no shipping method (e.g. virtual/free). ## Operations in this menu | Action | Operation | |--------|-----------| | [List shipments](/api/graphql-api/admin/sales/shipments/list) | `adminShipments` query | | [Get a single shipment](/api/graphql-api/admin/sales/orders/get-shipment) | `adminShipment(id:)` query | | [Create shipment](/api/graphql-api/admin/sales/orders/create-shipment) | `createAdminShipment` mutation | Shipment **creation** runs against an order — see [Create Shipment](/api/graphql-api/admin/sales/orders/create-shipment). All Shipments operations require the `sales.shipments.view` permission and an admin Bearer token — see [Authentication](/api/graphql-api/admin/authentication). --- # List Shipments (Datagrid) URL: /api/graphql-api/admin/sales/shipments/list --- outline: false examples: - id: admin-shipments-list-gql title: List Shipments (Datagrid) description: Cursor-paginated shipments datagrid. Every shipment column plus the order/customer context and both addresses is populated on each row — only the shipped line items are detail-only. query: | query AdminShipments($first: Int, $after: String, $order_id: String) { adminShipments(first: $first, after: $after, order_id: $order_id) { edges { cursor node { id _id orderId orderIncrementId shippedTo orderDate orderStatus orderStatusLabel channelName customerName customerEmail status totalQty totalWeight carrierCode carrierTitle trackNumber emailSent inventorySourceId inventorySourceName billingAddress shippingAddress createdAt updatedAt items } } pageInfo { hasNextPage endCursor } totalCount } } variables: | { "first": 10, "order_id": "8" } response: | { "data": { "adminShipments": { "edges": [ { "cursor": "MA==", "node": { "id": "/api/admin/shipments/7", "_id": 7, "orderId": 8, "orderIncrementId": "8", "shippedTo": "John Doe", "orderDate": "2026-05-20 10:00:00", "orderStatus": "processing", "orderStatusLabel": "Processing", "channelName": "Default", "customerName": "John Doe", "customerEmail": "john.doe@example.com", "status": null, "totalQty": 2, "totalWeight": null, "carrierCode": null, "carrierTitle": "UPS", "trackNumber": "1Z999AA1", "emailSent": false, "inventorySourceId": 1, "inventorySourceName": "Default", "billingAddress": { "id": 16, "addressType": "order_billing", "firstName": "John", "lastName": "Doe", "city": "Los Angeles", "country": "US", "postcode": "90001", "email": "john.doe@example.com", "phone": "5551234567" }, "shippingAddress": { "id": 15, "addressType": "order_shipping", "firstName": "John", "lastName": "Doe", "city": "Los Angeles", "country": "US", "postcode": "90001", "email": "john.doe@example.com", "phone": "5551234567" }, "createdAt": "2026-05-20 12:00:00", "updatedAt": "2026-05-20 12:00:00", "items": [] } } ], "pageInfo": { "hasNextPage": false, "endCursor": "MA==" }, "totalCount": 1 } } } --- # List Shipments (Datagrid) GraphQL counterpart of `GET /api/admin/shipments`. Returns a cursor-paginated list of shipments — the same rows shown on the admin **Sales → Shipments** datagrid. Every shipment **column** plus the order/customer context and both the billing and shipping addresses are populated on each row, so the field set is identical to [Shipment Detail](/api/graphql-api/admin/sales/orders/get-shipment) except for the shipped line `items`, which are returned only by the detail query (`[]` on the listing). ## Operation `adminShipments(first, after, id, order_id, total_qty, inventory_source_name, shipped_to, order_date_from, order_date_to, created_at_from, created_at_to, sort, order)` — a cursor `QueryCollection`. Every REST query parameter is also exposed as a GraphQL argument; see the [REST page](/api/rest-api/admin/sales/shipments/list) for the full argument table. ## Permission `sales.shipments.view` ::: warning billingAddress, shippingAddress and items are returned whole `billingAddress`, `shippingAddress` and `items` are returned as JSON — **query them bare, without a sub-selection** (`shippingAddress`, not `shippingAddress { … }`). The whole object/array comes back. See [Shipment Detail](/api/graphql-api/admin/sales/orders/get-shipment) for the keys inside each. ::: ## Fields Every field is populated on each row — the shipment columns, the order/customer context, and both address objects. Only the shipped line `items` are left empty (`[]`) on the listing. The full per-field reference is on the [Shipment Detail](/api/graphql-api/admin/sales/orders/get-shipment) page. ## Listing vs. fetching one The listing already carries the full payload — fetching a single shipment by id (`adminShipment(id:)`) is only needed when you want the shipped line `items`, or when you already hold a shipment id and want just that record. Typical flow: list with `adminShipments`, read `_id` from the row you want, then fetch the full record. --- # List Transactions (Datagrid) URL: /api/graphql-api/admin/sales/transactions/list --- outline: false examples: - id: admin-transactions-list-gql title: List Transactions (Datagrid) description: Cursor-paginated transactions datagrid. Every transaction column plus the raw gateway data blob and the linked order summary is populated on each row. query: | query AdminTransactions($first: Int, $after: String, $status: String) { adminTransactions(first: $first, after: $after, status: $status) { edges { cursor node { id _id transactionId invoiceId orderId orderIncrementId amount formattedAmount status type paymentMethod paymentTitle data createdAt updatedAt order } } pageInfo { hasNextPage endCursor } totalCount } } variables: | { "first": 10, "status": "paid" } response: | { "data": { "adminTransactions": { "edges": [ { "cursor": "MA==", "node": { "id": "/api/admin/transactions/4", "_id": 4, "transactionId": "pi_3PqXyz9aBcD", "invoiceId": 12, "orderId": 8, "orderIncrementId": "00000000008", "amount": 99.99, "formattedAmount": "$99.99", "status": "paid", "type": "capture", "paymentMethod": "cashondelivery", "paymentTitle": "Cash On Delivery", "data": { "gateway": "offline", "captured": true }, "createdAt": "2026-05-20 12:35:00", "updatedAt": "2026-05-20 12:35:00", "order": { "id": 8, "incrementId": "00000000008", "status": "processing", "customerName": "John Doe", "customerEmail": "john.doe@example.com", "grandTotal": 99.99, "orderCurrencyCode": "USD" } } } ], "pageInfo": { "hasNextPage": false, "endCursor": "MA==" }, "totalCount": 1 } } } --- # List Transactions (Datagrid) GraphQL counterpart of `GET /api/admin/transactions`. Returns a cursor-paginated list of payment transactions — the same rows shown on the admin **Sales → Transactions** datagrid. Every transaction **column** plus the raw gateway `data` blob and the linked `order` summary are populated on each row. ## Operation `adminTransactions(first, after, id, transaction_id, invoice_id, order_id, status, created_at_from, created_at_to, sort, order)` — a cursor `QueryCollection`. Every REST query parameter is also exposed as a GraphQL argument; see the [REST page](/api/rest-api/admin/sales/transactions/list) for the full argument table. ## Permission `sales.transactions.view` ::: warning data and order are returned whole `data` (the gateway payload) and `order` (the order summary) are returned as JSON — **query them bare, without a sub-selection** (`data` / `order`, not `order { … }`). The whole object comes back. See the field reference below for the keys inside each. ::: ## Fields Every field is populated on each row — the transaction columns, the resolved `paymentTitle`, the raw gateway `data` object, and the `order` summary. | Field | Type | Description | |-------|------|-------------| | `id` | `ID` | Resource IRI of the transaction (`/api/admin/transactions/{id}`). | | `_id` | `Int` | Transaction row id. | | `transactionId` | `String` | Gateway transaction id. | | `invoiceId` | `Int` | The invoice this transaction paid (if any). | | `orderId` / `orderIncrementId` | `Int` / `String` | Parent order id and human-facing number. | | `amount` / `formattedAmount` | `Float` / `String` | Transaction amount, raw and formatted. | | `status` | `String` | Transaction status — e.g. `paid`, `pending`. | | `type` | `String` | Transaction type — e.g. `capture`. | | `paymentMethod` / `paymentTitle` | `String` | Payment method code and its human-readable title. | | `data` | JSON | The verbatim gateway response payload — query bare; shape varies by gateway; may be `null`. | | `createdAt` / `updatedAt` | `String` | Timestamps. | | `order` | JSON | Slim order summary (query bare) — `id`, `incrementId`, `status`, `customerName`, `customerEmail`, `grandTotal`, `orderCurrencyCode`. | --- # Create Channel (GraphQL) URL: /api/graphql-api/admin/settings/channels/create --- outline: false examples: - id: gql title: Create Channel query: | mutation Create($input: createAdminSettingsChannelInput!) { createAdminSettingsChannel(input: $input) { adminSettingsChannel { id _id code name hostname } } } variables: | { "input": { "code": "us", "name": "US Store", "hostname": "us.example.com", "locales": [1], "currencies": [1], "inventorySources": [1], "defaultLocaleId": 1, "baseCurrencyId": 1, "rootCategoryId": 1 } } response: | { "data": { "createAdminSettingsChannel": { "adminSettingsChannel": { "id": "/api/admin/settings/channels/2", "_id": 2, "code": "us", "name": "US Store", "hostname": "us.example.com" } } } } --- # Create Channel (GraphQL) ::: warning Logo / favicon upload deferred Multipart binary upload not yet supported via the API. ::: --- # Delete Channel (GraphQL) URL: /api/graphql-api/admin/settings/channels/delete --- outline: false examples: - id: gql title: Delete Channel query: | mutation Delete($input: deleteAdminSettingsChannelInput!) { deleteAdminSettingsChannel(input: $input) { adminSettingsChannel { id } } } variables: | { "input": { "id": "/api/admin/settings/channels/2" } } response: | { "data": { "deleteAdminSettingsChannel": { "adminSettingsChannel": null } } } --- # Delete Channel (GraphQL) ::: warning Guards Refuses if last channel or `app.channel` default. ::: ::: tip Prerequisites The example uses an illustrative `id` value. Replace it with the id of a channel that exists in your store — use the [`adminSettingsChannels`](./list.md) query to discover valid ids. ::: --- # Channel Detail (GraphQL) URL: /api/graphql-api/admin/settings/channels/detail --- outline: false examples: - id: gql title: Channel Detail query: | query A($id: ID!) { adminSettingsChannel(id: $id) { id _id code name hostname rootCategoryId defaultLocaleId baseCurrencyId } } variables: | { "id": "/api/admin/settings/channels/1" } response: | { "data": { "adminSettingsChannel": { "id": "/api/admin/settings/channels/1", "_id": 1, "code": "default", "name": "Default", "hostname": "store.example.com", "rootCategoryId": 1, "defaultLocaleId": 1, "baseCurrencyId": 1 } } } --- # Channel Detail (GraphQL) --- # List Channels (GraphQL) URL: /api/graphql-api/admin/settings/channels/list --- outline: false examples: - id: gql title: List Channels query: | query A($first: Int) { adminSettingsChannels(first: $first) { edges { cursor node { id _id code name hostname } } pageInfo { hasNextPage endCursor } totalCount } } variables: | { "first": 10 } response: | { "data": { "adminSettingsChannels": { "edges": [{ "cursor": "MA==", "node": { "id": "/api/admin/settings/channels/1", "_id": 1, "code": "default", "name": "Default", "hostname": "store.example.com" } }], "pageInfo": { "hasNextPage": false, "endCursor": "MA==" }, "totalCount": 1 } } } --- # List Channels (GraphQL) --- # Update Channel (GraphQL) URL: /api/graphql-api/admin/settings/channels/update --- outline: false examples: - id: gql title: Update Channel query: | mutation Update($input: updateAdminSettingsChannelInput!) { updateAdminSettingsChannel(input: $input) { adminSettingsChannel { id _id name } } } variables: | { "input": { "id": "/api/admin/settings/channels/2", "name": "United States Store" } } response: | { "data": { "updateAdminSettingsChannel": { "adminSettingsChannel": { "id": "/api/admin/settings/channels/2", "_id": 2, "name": "United States Store" } } } } --- # Update Channel (GraphQL) Use the `translations` map for locale-nested attributes. Top-level scalars broadcast to every locale. ::: tip Prerequisites The example uses an illustrative `id` value. Replace it with the id of a channel that exists in your store — use the [`adminSettingsChannels`](./list.md) query to discover valid ids. ::: --- # Create Currency (GraphQL) URL: /api/graphql-api/admin/settings/currencies/create --- outline: false examples: - id: gql title: Create Currency query: | mutation Create($input: createAdminSettingsCurrencyInput!) { createAdminSettingsCurrency(input: $input) { adminSettingsCurrency { id _id code name } } } variables: | { "input": { "code": "EUR", "name": "Euro", "symbol": "€" } } response: | { "data": { "createAdminSettingsCurrency": { "adminSettingsCurrency": { "id": "/api/admin/settings/currencies/2", "_id": 2, "code": "EUR", "name": "Euro" } } } } --- # Create Currency (GraphQL) Permission: `settings.currencies.create`. ::: tip Prerequisites The example uses currency code `USD` — if that currency already exists in your store, the mutation returns *"The code has already been taken."* Either delete the existing currency first or use a different 3-letter ISO code. ::: --- # Delete Currency (GraphQL) URL: /api/graphql-api/admin/settings/currencies/delete --- outline: false examples: - id: gql title: Delete Currency query: | mutation Delete($input: deleteAdminSettingsCurrencyInput!) { deleteAdminSettingsCurrency(input: $input) { adminSettingsCurrency { id } } } variables: | { "input": { "id": "/api/admin/settings/currencies/2" } } response: | { "data": { "deleteAdminSettingsCurrency": { "adminSettingsCurrency": null } } } --- # Delete Currency (GraphQL) ::: warning Guards Refuses if last currency or any channel uses it as base. ::: ::: tip Prerequisites The example uses an illustrative `id` value. Replace it with the id of a currency that exists in your store — use the [`adminSettingsCurrencies`](./list.md) query to discover valid ids. ::: --- # Currency Detail (GraphQL) URL: /api/graphql-api/admin/settings/currencies/detail --- outline: false examples: - id: gql title: Currency Detail query: | query AdminCurrency($id: ID!) { adminSettingsCurrency(id: $id) { id _id code name symbol } } variables: | { "id": "/api/admin/settings/currencies/1" } response: | { "data": { "adminSettingsCurrency": { "id": "/api/admin/settings/currencies/1", "_id": 1, "code": "USD", "name": "US Dollar", "symbol": "$" } } } --- # Currency Detail (GraphQL) --- # List Currencies (GraphQL) URL: /api/graphql-api/admin/settings/currencies/list --- outline: false examples: - id: gql title: List Currencies query: | query AdminCurrencies($first: Int) { adminSettingsCurrencies(first: $first) { edges { cursor node { id _id code name symbol } } pageInfo { hasNextPage endCursor } totalCount } } variables: | { "first": 10 } response: | { "data": { "adminSettingsCurrencies": { "edges": [{ "cursor": "MA==", "node": { "id": "/api/admin/settings/currencies/1", "_id": 1, "code": "USD", "name": "US Dollar", "symbol": "$" } }], "pageInfo": { "hasNextPage": false, "endCursor": "MA==" }, "totalCount": 1 } } } --- # List Currencies (GraphQL) --- # Mass Delete Currencies (GraphQL) URL: /api/graphql-api/admin/settings/currencies/mass-delete --- outline: false examples: - id: gql title: Mass Delete Currencies query: | mutation MassDelete($input: createAdminSettingsCurrencyMassDeleteInput!) { createAdminSettingsCurrencyMassDelete(input: $input) { adminSettingsCurrencyMassDelete { deleted message } } } variables: | { "input": { "indices": [3, 4] } } response: | { "data": { "createAdminSettingsCurrencyMassDelete": { "adminSettingsCurrencyMassDelete": { "deleted": [3, 4], "message": "Currencies deleted." } } } } --- # Mass Delete Currencies (GraphQL) --- # Update Currency (GraphQL) URL: /api/graphql-api/admin/settings/currencies/update --- outline: false examples: - id: gql title: Update Currency query: | mutation Update($input: updateAdminSettingsCurrencyInput!) { updateAdminSettingsCurrency(input: $input) { adminSettingsCurrency { id _id name } } } variables: | { "input": { "id": "/api/admin/settings/currencies/2", "name": "Euro (EU)" } } response: | { "data": { "updateAdminSettingsCurrency": { "adminSettingsCurrency": { "id": "/api/admin/settings/currencies/2", "_id": 2, "name": "Euro (EU)" } } } } --- # Update Currency (GraphQL) `code` is immutable. Permission: `settings.currencies.edit`. ::: tip Prerequisites The example uses an illustrative `id` value. Replace it with the id of a currency that exists in your store — use the [`adminSettingsCurrencies`](./list.md) query to discover valid ids. ::: --- # Index Import (GraphQL) URL: /api/graphql-api/admin/settings/data-transfer-imports --- outline: false examples: - id: gql title: Index Import description: API Platform GraphQL naming yields `indexAdminSettingsDataTransferImportIndex`. Clients typically alias the field. query: | mutation IndexImport($input: indexAdminSettingsDataTransferImportIndexInput!) { indexAdminSettingsDataTransferImportIndex(input: $input) { adminSettingsDataTransferImportIndex { id state processedRowsCount } } } variables: | { "input": { "importId": 12 } } response: | { "data": { "indexAdminSettingsDataTransferImportIndex": { "adminSettingsDataTransferImportIndex": { "id": 12, "state": "indexed", "processedRowsCount": 12 } } } } --- # Index Import (GraphQL) Runs the indexing stage of the import. This is the final stage, following the linking stage (see [Link](./link.md)), and makes the imported records searchable and visible on the storefront. Permission: `settings.data_transfer.imports.edit`. ::: tip Prerequisites The example uses an illustrative `importId`. Replace it with the id of an import in your store that has finished linking. ::: --- # Cancel Import (GraphQL) URL: /api/graphql-api/admin/settings/data-transfer-imports/cancel --- outline: false examples: - id: gql title: Cancel Import description: API Platform GraphQL naming yields `cancelAdminSettingsDataTransferImportCancel`. Clients typically alias the field. query: | mutation CancelImport($input: cancelAdminSettingsDataTransferImportCancelInput!) { cancelAdminSettingsDataTransferImportCancel(input: $input) { adminSettingsDataTransferImportCancel { id state message } } } variables: | { "input": { "id": "/api/admin/settings/data-transfer/imports/3" } } response: | { "data": { "cancelAdminSettingsDataTransferImportCancel": { "adminSettingsDataTransferImportCancel": { "id": 3, "state": "cancelled", "message": "Import cancelled successfully." } } } } --- # Cancel Import (GraphQL) ::: warning Terminal-state guard Refuses when the import is `completed`, `processed`, `failed` or already `cancelled` (errors[]). ::: Permission: `settings.data_transfer.imports.edit`. ::: tip Prerequisites The example uses an illustrative `id` value. Replace it with the id of a data transfer import that exists in your store — use the [`adminSettingsDataTransferImports`](./list.md) query to discover valid ids. ::: --- # Create Import (GraphQL) URL: /api/graphql-api/admin/settings/data-transfer-imports/create --- outline: false examples: - id: gql title: Create Import (rejected) description: Creating an import requires a file upload, which GraphQL cannot carry. The mutation returns an error directing you to the REST endpoint. query: | mutation CreateImport($input: createAdminSettingsDataTransferImportInput!) { createAdminSettingsDataTransferImport(input: $input) { adminSettingsDataTransferImport { id state } } } variables: | { "input": { "type": "products", "action": "append" } } response: | { "errors": [{ "message": "Importing files is only supported over the REST API. Use POST /api/admin/settings/data-transfer/imports." }] } --- # Create Import (GraphQL) ::: warning Use the REST endpoint Creating an import requires uploading a file, which cannot be done over GraphQL. The GraphQL `create` mutation returns an error pointing to the REST endpoint. Use the REST [Create Import](/api/rest-api/admin/settings/data-transfer-imports/create) endpoint instead. ::: Once an import exists, you can validate, start, link, index, and read its stats over GraphQL. Permission: `settings.data_transfer.imports.create`. --- # Delete Import (GraphQL) URL: /api/graphql-api/admin/settings/data-transfer-imports/delete --- outline: false examples: - id: gql title: Delete Import query: | mutation Delete($input: deleteAdminSettingsDataTransferImportInput!) { deleteAdminSettingsDataTransferImport(input: $input) { adminSettingsDataTransferImport { id } } } variables: | { "input": { "id": "/api/admin/settings/data-transfer/imports/3" } } response: | { "data": { "deleteAdminSettingsDataTransferImport": { "adminSettingsDataTransferImport": null } } } --- # Delete Import (GraphQL) ::: tip Prerequisites The example uses an illustrative `id` value. Replace it with the id of a data transfer import that exists in your store — use the [`adminSettingsDataTransferImports`](./list.md) query to discover valid ids. ::: --- # Import Detail (GraphQL) URL: /api/graphql-api/admin/settings/data-transfer-imports/detail --- outline: false examples: - id: gql title: Import Detail query: | query A($id: ID!) { adminSettingsDataTransferImport(id: $id) { id _id code action state processedRowsCount summary createdAt } } variables: | { "id": "/api/admin/settings/data-transfer/imports/3" } response: | { "data": { "adminSettingsDataTransferImport": { "id": "/api/admin/settings/data-transfer/imports/3", "_id": 3, "code": "products", "action": "append", "state": "completed", "processedRowsCount": 150, "summary": { "created": 100, "updated": 50, "deleted": 0 }, "createdAt": "2026-05-25 09:00:00" } } } --- # Import Detail (GraphQL) --- # Link Import (GraphQL) URL: /api/graphql-api/admin/settings/data-transfer-imports/link --- outline: false examples: - id: gql title: Link Import description: API Platform GraphQL naming yields `linkAdminSettingsDataTransferImportLink`. Clients typically alias the field. query: | mutation LinkImport($input: linkAdminSettingsDataTransferImportLinkInput!) { linkAdminSettingsDataTransferImportLink(input: $input) { adminSettingsDataTransferImportLink { id state processedRowsCount } } } variables: | { "input": { "importId": 12 } } response: | { "data": { "linkAdminSettingsDataTransferImportLink": { "adminSettingsDataTransferImportLink": { "id": 12, "state": "linked", "processedRowsCount": 12 } } } } --- # Link Import (GraphQL) Runs the post-process linking stage of the import. This follows the processing stage (see [Start](./start.md)) and resolves relationships between the imported records. After linking, run the [Index](./index.md) stage to finish the import. Permission: `settings.data_transfer.imports.edit`. ::: tip Prerequisites The example uses an illustrative `importId`. Replace it with the id of an import in your store that has finished processing. ::: --- # List Imports (GraphQL) URL: /api/graphql-api/admin/settings/data-transfer-imports/list --- outline: false examples: - id: gql title: List Imports query: | query A($first: Int) { adminSettingsDataTransferImports(first: $first) { edges { cursor node { id _id code action state processedRowsCount createdAt } } pageInfo { hasNextPage endCursor } totalCount } } variables: | { "first": 10 } response: | { "data": { "adminSettingsDataTransferImports": { "edges": [{ "cursor": "MA==", "node": { "id": "/api/admin/settings/data-transfer/imports/3", "_id": 3, "code": "products", "action": "append", "state": "completed", "processedRowsCount": 150, "createdAt": "2026-05-25 09:00:00" } }], "pageInfo": { "hasNextPage": false, "endCursor": "MA==" }, "totalCount": 1 } } } --- # List Imports (GraphQL) --- # Start Import (GraphQL) URL: /api/graphql-api/admin/settings/data-transfer-imports/start --- outline: false examples: - id: gql title: Start Import description: API Platform GraphQL naming yields `startAdminSettingsDataTransferImportStart`. Clients typically alias the field. query: | mutation StartImport($input: startAdminSettingsDataTransferImportStartInput!) { startAdminSettingsDataTransferImportStart(input: $input) { adminSettingsDataTransferImportStart { id state processedRowsCount } } } variables: | { "input": { "importId": 12 } } response: | { "data": { "startAdminSettingsDataTransferImportStart": { "adminSettingsDataTransferImportStart": { "id": 12, "state": "processing", "processedRowsCount": 10 } } } } --- # Start Import (GraphQL) Processes the next pending batch of rows. Call this repeatedly until there are no pending batches left — each call advances the import. After the rows are processed, run the [Link](./link.md) stage, then the [Index](./index.md) stage to finish the import. ## Errors The mutation reports an error when there is nothing left to import, when the import has not been validated, or when asynchronous processing is requested but the queue is not configured. Permission: `settings.data_transfer.imports.edit`. ::: tip Prerequisites The example uses an illustrative `importId`. Replace it with the id of a validated import in your store. ::: --- # Import Stats (GraphQL) URL: /api/graphql-api/admin/settings/data-transfer-imports/stats --- outline: false examples: - id: gql title: Import Stats query: | query ImportStats($id: ID!) { adminSettingsDataTransferImportStats(id: $id) { _id type action state processedRowsCount invalidRowsCount errorsCount } } variables: | { "id": "/api/admin/settings/data-transfer/imports/12/stats" } response: | { "data": { "adminSettingsDataTransferImportStats": { "_id": 12, "type": "products", "action": "append", "state": "processed", "processedRowsCount": 12, "invalidRowsCount": 0, "errorsCount": 0 } } } --- # Import Stats (GraphQL) Returns the current progress of an import without advancing it. The counts (`processedRowsCount`, `invalidRowsCount`, `errorsCount`) reflect how far the import has run. Permission: `settings.data_transfer.imports.view`. ::: tip Prerequisites The example uses an illustrative `id`. Replace it with the id of a data transfer import that exists in your store — use the [`adminSettingsDataTransferImports`](./list.md) query to discover valid ids. ::: --- # Validate Import (GraphQL) URL: /api/graphql-api/admin/settings/data-transfer-imports/validate --- outline: false examples: - id: gql title: Validate Import description: API Platform GraphQL naming yields `validateAdminSettingsDataTransferImportValidate`. Clients typically alias the field. query: | mutation ValidateImport($input: validateAdminSettingsDataTransferImportValidateInput!) { validateAdminSettingsDataTransferImportValidate(input: $input) { adminSettingsDataTransferImportValidate { id isValid } } } variables: | { "input": { "importId": 12 } } response: | { "data": { "validateAdminSettingsDataTransferImportValidate": { "adminSettingsDataTransferImportValidate": { "id": 12, "isValid": true } } } } --- # Validate Import (GraphQL) Runs validation over the uploaded file without importing any data. This is the second step of the import lifecycle (after the import is created). The response carries an `isValid` flag. When `isValid` is `false`, inspect the import's error counts and download the error report over REST to see which rows failed. Permission: `settings.data_transfer.imports.edit`. ::: tip Prerequisites The example uses an illustrative `importId`. Replace it with the id of a data transfer import that exists in your store — use the [`adminSettingsDataTransferImports`](./list.md) query to discover valid ids. ::: --- # Create Exchange Rate (GraphQL) URL: /api/graphql-api/admin/settings/exchange-rates/create --- outline: false examples: - id: gql title: Create Exchange Rate query: | mutation Create($input: createAdminSettingsExchangeRateInput!) { createAdminSettingsExchangeRate(input: $input) { adminSettingsExchangeRate { id _id targetCurrency rate } } } variables: | { "input": { "targetCurrency": 2, "rate": 0.92 } } response: | { "data": { "createAdminSettingsExchangeRate": { "adminSettingsExchangeRate": { "id": "/api/admin/settings/exchange-rates/1", "_id": 1, "targetCurrency": 2, "rate": 0.92 } } } } --- # Create Exchange Rate (GraphQL) Permission: `settings.exchange_rates.create`. --- # Delete Exchange Rate (GraphQL) URL: /api/graphql-api/admin/settings/exchange-rates/delete --- outline: false examples: - id: gql title: Delete Exchange Rate query: | mutation Delete($input: deleteAdminSettingsExchangeRateInput!) { deleteAdminSettingsExchangeRate(input: $input) { adminSettingsExchangeRate { id } } } variables: | { "input": { "id": "/api/admin/settings/exchange-rates/1" } } response: | { "data": { "deleteAdminSettingsExchangeRate": { "adminSettingsExchangeRate": null } } } --- # Delete Exchange Rate (GraphQL) ::: tip Prerequisites The example uses an illustrative `id` value. Replace it with the id of a exchange rate that exists in your store — use the [`adminSettingsExchangeRates`](./list.md) query to discover valid ids. ::: --- # Exchange Rate Detail (GraphQL) URL: /api/graphql-api/admin/settings/exchange-rates/detail --- outline: false examples: - id: gql title: Exchange Rate Detail query: | query A($id: ID!) { adminSettingsExchangeRate(id: $id) { id _id targetCurrency targetCurrencyCode targetCurrencyName rate } } variables: | { "id": "/api/admin/settings/exchange-rates/1" } response: | { "data": { "adminSettingsExchangeRate": { "id": "/api/admin/settings/exchange-rates/1", "_id": 1, "targetCurrency": 2, "targetCurrencyCode": "EUR", "targetCurrencyName": "Euro", "rate": 0.92 } } } --- # Exchange Rate Detail (GraphQL) --- # List Exchange Rates (GraphQL) URL: /api/graphql-api/admin/settings/exchange-rates/list --- outline: false examples: - id: gql title: List Exchange Rates query: | query A($first: Int) { adminSettingsExchangeRates(first: $first) { edges { cursor node { id _id targetCurrency targetCurrencyCode targetCurrencyName rate } } pageInfo { hasNextPage endCursor } totalCount } } variables: | { "first": 10 } response: | { "data": { "adminSettingsExchangeRates": { "edges": [{ "cursor": "MA==", "node": { "id": "/api/admin/settings/exchange-rates/1", "_id": 1, "targetCurrency": 2, "targetCurrencyCode": "EUR", "targetCurrencyName": "Euro", "rate": 0.92 } }], "pageInfo": { "hasNextPage": false, "endCursor": "MA==" }, "totalCount": 1 } } } --- # List Exchange Rates (GraphQL) --- # Mass Delete Exchange Rates (GraphQL) URL: /api/graphql-api/admin/settings/exchange-rates/mass-delete --- outline: false examples: - id: gql title: Mass Delete Exchange Rates query: | mutation MassDelete($input: createAdminSettingsExchangeRateMassDeleteInput!) { createAdminSettingsExchangeRateMassDelete(input: $input) { adminSettingsExchangeRateMassDelete { deleted message } } } variables: | { "input": { "indices": [1, 2] } } response: | { "data": { "createAdminSettingsExchangeRateMassDelete": { "adminSettingsExchangeRateMassDelete": { "deleted": [1, 2], "message": "Exchange rates deleted." } } } } --- # Mass Delete Exchange Rates (GraphQL) --- # Update Exchange Rate (GraphQL) URL: /api/graphql-api/admin/settings/exchange-rates/update --- outline: false examples: - id: gql title: Update Exchange Rate query: | mutation Update($input: updateAdminSettingsExchangeRateInput!) { updateAdminSettingsExchangeRate(input: $input) { adminSettingsExchangeRate { id _id rate } } } variables: | { "input": { "id": "/api/admin/settings/exchange-rates/1", "rate": 0.94 } } response: | { "data": { "updateAdminSettingsExchangeRate": { "adminSettingsExchangeRate": { "id": "/api/admin/settings/exchange-rates/1", "_id": 1, "rate": 0.94 } } } } --- # Update Exchange Rate (GraphQL) ::: tip Prerequisites The example uses an illustrative `id` value. Replace it with the id of a exchange rate that exists in your store — use the [`adminSettingsExchangeRates`](./list.md) query to discover valid ids. ::: --- # Update Rates (auto-sync) — GraphQL URL: /api/graphql-api/admin/settings/exchange-rates/update-rates --- outline: false examples: - id: gql title: Update Rates (auto-sync) query: | mutation UpdateRates($input: createAdminSettingsExchangeRateUpdateRatesInput!) { createAdminSettingsExchangeRateUpdateRates(input: $input) { adminSettingsExchangeRateUpdateRates { success updated message } } } variables: | { "input": {} } response: | { "data": { "createAdminSettingsExchangeRateUpdateRates": { "adminSettingsExchangeRateUpdateRates": { "success": true, "updated": 3, "message": "Exchange rates updated successfully." } } } } --- # Update Rates (auto-sync) — GraphQL Refreshes every non-base currency's exchange rate from the store's configured external rate provider — the **Update Rates** action on the Exchange Rates screen. - The mutation takes an empty input. - `updated` is the number of exchange-rate rows present after the sync. - If the external provider fails (missing/invalid API key, network error), the response carries an `errors` entry with the provider's message. Permission: `settings.exchange_rates.edit`. --- # Create Inventory Source (GraphQL) URL: /api/graphql-api/admin/settings/inventory-sources/create --- outline: false examples: - id: gql title: Create Inventory Source query: | mutation Create($input: createAdminSettingsInventorySourceInput!) { createAdminSettingsInventorySource(input: $input) { adminSettingsInventorySource { id _id code name } } } variables: | { "input": { "code": "warehouse-east", "name": "East Coast Warehouse", "contactName": "Ops", "contactEmail": "ops@example.com", "contactNumber": "+15551112222", "country": "US", "state": "NY", "city": "Brooklyn", "street": "123 Front St", "postcode": "11201", "status": 1 } } response: | { "data": { "createAdminSettingsInventorySource": { "adminSettingsInventorySource": { "id": "/api/admin/settings/inventory-sources/2", "_id": 2, "code": "warehouse-east", "name": "East Coast Warehouse" } } } } --- # Create Inventory Source (GraphQL) Permission: `settings.inventory_sources.create`. --- # Delete Inventory Source (GraphQL) URL: /api/graphql-api/admin/settings/inventory-sources/delete --- outline: false examples: - id: gql title: Delete Inventory Source query: | mutation Delete($input: deleteAdminSettingsInventorySourceInput!) { deleteAdminSettingsInventorySource(input: $input) { adminSettingsInventorySource { id } } } variables: | { "input": { "id": "/api/admin/settings/inventory-sources/2" } } response: | { "data": { "deleteAdminSettingsInventorySource": { "adminSettingsInventorySource": null } } } --- # Delete Inventory Source (GraphQL) ::: warning Guards Refuses if last source or referenced by `product_inventories`. ::: ::: tip Prerequisites The example uses an illustrative `id` value. Replace it with the id of a inventory source that exists in your store — use the [`adminSettingsInventorySources`](./list.md) query to discover valid ids. ::: --- # Inventory Source Detail (GraphQL) URL: /api/graphql-api/admin/settings/inventory-sources/detail --- outline: false examples: - id: gql title: Inventory Source Detail query: | query A($id: ID!) { adminSettingsInventorySource(id: $id) { id _id code name country state city postcode priority status } } variables: | { "id": "/api/admin/settings/inventory-sources/1" } response: | { "data": { "adminSettingsInventorySource": { "id": "/api/admin/settings/inventory-sources/1", "_id": 1, "code": "default", "name": "Default Warehouse", "country": "US", "state": "IL", "city": "Springfield", "postcode": "62704", "priority": 1, "status": 1 } } } --- # Inventory Source Detail (GraphQL) --- # List Inventory Sources (GraphQL) URL: /api/graphql-api/admin/settings/inventory-sources/list --- outline: false examples: - id: gql title: List Inventory Sources query: | query A($first: Int) { adminSettingsInventorySources(first: $first) { edges { cursor node { id _id code name priority status } } pageInfo { hasNextPage endCursor } totalCount } } variables: | { "first": 10 } response: | { "data": { "adminSettingsInventorySources": { "edges": [{ "cursor": "MA==", "node": { "id": "/api/admin/settings/inventory-sources/1", "_id": 1, "code": "default", "name": "Default Warehouse", "priority": 1, "status": 1 } }], "pageInfo": { "hasNextPage": false, "endCursor": "MA==" }, "totalCount": 1 } } } --- # List Inventory Sources (GraphQL) --- # Mass Delete Inventory Sources (GraphQL) URL: /api/graphql-api/admin/settings/inventory-sources/mass-delete --- outline: false examples: - id: gql title: Mass Delete Inventory Sources query: | mutation MassDelete($input: createAdminSettingsInventorySourceMassDeleteInput!) { } variables: | { "input": { "indices": [3, 4] } } response: | { "data": { "createAdminSettingsInventorySourceMassDelete": { "adminSettingsInventorySourceMassDelete": { "deleted": [4], "skipped": [{ "id": 3, "reason": "In use" }], "message": "Inventory sources processed." } } } } --- # Mass Delete Inventory Sources (GraphQL) --- # Update Inventory Source (GraphQL) URL: /api/graphql-api/admin/settings/inventory-sources/update --- outline: false examples: - id: gql title: Update Inventory Source query: | mutation Update($input: updateAdminSettingsInventorySourceInput!) { updateAdminSettingsInventorySource(input: $input) { adminSettingsInventorySource { id _id name } } } variables: | { "input": { "id": "/api/admin/settings/inventory-sources/2", "name": "East Coast (NY)" } } response: | { "data": { "updateAdminSettingsInventorySource": { "adminSettingsInventorySource": { "id": "/api/admin/settings/inventory-sources/2", "_id": 2, "name": "East Coast (NY)" } } } } --- # Update Inventory Source (GraphQL) ::: tip Prerequisites The example uses an illustrative `id` value. Replace it with the id of a inventory source that exists in your store — use the [`adminSettingsInventorySources`](./list.md) query to discover valid ids. ::: --- # Create Locale (GraphQL) URL: /api/graphql-api/admin/settings/locales/create --- outline: false examples: - id: gql title: Create Locale query: | mutation Create($input: createAdminSettingsLocaleInput!) { createAdminSettingsLocale(input: $input) { adminSettingsLocale { id _id code name } } } variables: | { "input": { "code": "fr", "name": "French", "direction": "ltr" } } response: | { "data": { "createAdminSettingsLocale": { "adminSettingsLocale": { "id": "/api/admin/settings/locales/2", "_id": 2, "code": "fr", "name": "French" } } } } --- # Create Locale (GraphQL) Image upload deferred. Permission: `settings.locales.create`. --- # Delete Locale (GraphQL) URL: /api/graphql-api/admin/settings/locales/delete --- outline: false examples: - id: gql title: Delete Locale query: | mutation Delete($input: deleteAdminSettingsLocaleInput!) { deleteAdminSettingsLocale(input: $input) { adminSettingsLocale { id } } } variables: | { "input": { "id": "/api/admin/settings/locales/2" } } response: | { "data": { "deleteAdminSettingsLocale": { "adminSettingsLocale": null } } } --- # Delete Locale (GraphQL) ::: warning Guards Refuses if last locale or used as a channel default. ::: ::: tip Prerequisites The example uses an illustrative `id` value. Replace it with the id of a locale that exists in your store — use the [`adminSettingsLocales`](./list.md) query to discover valid ids. ::: --- # Locale Detail (GraphQL) URL: /api/graphql-api/admin/settings/locales/detail --- outline: false examples: - id: gql title: Locale Detail query: | query AdminLocale($id: ID!) { adminSettingsLocale(id: $id) { id _id code name direction logoUrl } } variables: | { "id": "/api/admin/settings/locales/1" } response: | { "data": { "adminSettingsLocale": { "id": "/api/admin/settings/locales/1", "_id": 1, "code": "en", "name": "English", "direction": "ltr", "logoUrl": null } } } --- # Locale Detail (GraphQL) --- # List Locales (GraphQL) URL: /api/graphql-api/admin/settings/locales/list --- outline: false examples: - id: gql title: List Locales query: | query AdminLocales($first: Int) { adminSettingsLocales(first: $first) { edges { cursor node { id _id code name direction logoUrl } } pageInfo { hasNextPage endCursor } totalCount } } variables: | { "first": 10 } response: | { "data": { "adminSettingsLocales": { "edges": [{ "cursor": "MA==", "node": { "id": "/api/admin/settings/locales/1", "_id": 1, "code": "en", "name": "English", "direction": "ltr", "logoUrl": null } }], "pageInfo": { "hasNextPage": false, "endCursor": "MA==" }, "totalCount": 1 } } } --- # List Locales (GraphQL) --- # Mass Delete Locales (GraphQL) URL: /api/graphql-api/admin/settings/locales/mass-delete --- outline: false examples: - id: gql title: Mass Delete Locales query: | mutation MassDelete($input: createAdminSettingsLocaleMassDeleteInput!) { createAdminSettingsLocaleMassDelete(input: $input) { adminSettingsLocaleMassDelete { deleted skipped message } } } variables: | { "input": { "indices": [3, 4] } } response: | { "data": { "createAdminSettingsLocaleMassDelete": { "adminSettingsLocaleMassDelete": { "deleted": [4], "skipped": [{ "id": 3, "reason": "Channel default" }], "message": "Locales processed." } } } } --- # Mass Delete Locales (GraphQL) --- # Update Locale (GraphQL) URL: /api/graphql-api/admin/settings/locales/update --- outline: false examples: - id: gql title: Update Locale query: | mutation Update($input: updateAdminSettingsLocaleInput!) { updateAdminSettingsLocale(input: $input) { adminSettingsLocale { id _id name } } } variables: | { "input": { "id": "/api/admin/settings/locales/2", "name": "Français" } } response: | { "data": { "updateAdminSettingsLocale": { "adminSettingsLocale": { "id": "/api/admin/settings/locales/2", "_id": 2, "name": "Français" } } } } --- # Update Locale (GraphQL) ::: tip Prerequisites The example uses an illustrative `id` value. Replace it with the id of a locale that exists in your store — use the [`adminSettingsLocales`](./list.md) query to discover valid ids. ::: --- # Create Role (GraphQL) URL: /api/graphql-api/admin/settings/roles/create --- outline: false examples: - id: gql title: Create Role query: | mutation Create($input: createAdminSettingsRoleInput!) { createAdminSettingsRole(input: $input) { adminSettingsRole { id _id name permissionType } } } variables: | { "input": { "name": "Catalog Manager", "description": "Catalog only", "permissionType": "custom", "permissions": ["catalog.products.view", "catalog.products.edit"] } } response: | { "data": { "createAdminSettingsRole": { "adminSettingsRole": { "id": "/api/admin/settings/roles/3", "_id": 3, "name": "Catalog Manager", "permissionType": "custom" } } } } --- # Create Role (GraphQL) --- # Delete Role (GraphQL) URL: /api/graphql-api/admin/settings/roles/delete --- outline: false examples: - id: gql title: Delete Role query: | mutation Delete($input: deleteAdminSettingsRoleInput!) { deleteAdminSettingsRole(input: $input) { adminSettingsRole { id } } } variables: | { "input": { "id": "/api/admin/settings/roles/3" } } response: | { "data": { "deleteAdminSettingsRole": { "adminSettingsRole": null } } } --- # Delete Role (GraphQL) ::: warning Guards Refuses if in use by admins or last role. ::: ::: tip Prerequisites The example role id must not be assigned to any admin. If admins are using it, deletion is refused with *"This role is assigned to one or more admins and cannot be deleted."* — reassign those admins to a different role first, or pick an unused role id. ::: --- # Role Detail (GraphQL) URL: /api/graphql-api/admin/settings/roles/detail --- outline: false examples: - id: gql title: Role Detail query: | query A($id: ID!) { adminSettingsRole(id: $id) { id _id name description permissionType permissions } } variables: | { "id": "/api/admin/settings/roles/1" } response: | { "data": { "adminSettingsRole": { "id": "/api/admin/settings/roles/1", "_id": 1, "name": "Administrator", "description": "Full access", "permissionType": "all", "permissions": null } } } --- # Role Detail (GraphQL) --- # List Roles (GraphQL) URL: /api/graphql-api/admin/settings/roles/list --- outline: false examples: - id: gql title: List Roles query: | query A($first: Int) { adminSettingsRoles(first: $first) { edges { cursor node { id _id name description permissionType } } pageInfo { hasNextPage endCursor } totalCount } } variables: | { "first": 10 } response: | { "data": { "adminSettingsRoles": { "edges": [{ "cursor": "MA==", "node": { "id": "/api/admin/settings/roles/1", "_id": 1, "name": "Administrator", "description": "Full access", "permissionType": "all" } }], "pageInfo": { "hasNextPage": false, "endCursor": "MA==" }, "totalCount": 1 } } } --- # List Roles (GraphQL) --- # Update Role (GraphQL) URL: /api/graphql-api/admin/settings/roles/update --- outline: false examples: - id: gql title: Update Role query: | mutation Update($input: updateAdminSettingsRoleInput!) { updateAdminSettingsRole(input: $input) { adminSettingsRole { id _id name } } } variables: | { "input": { "id": "/api/admin/settings/roles/3", "name": "Catalog Manager+", "description": "Updated", "permissionType": "custom", "permissions": ["catalog.products.view", "catalog.products.edit"] } } response: | { "data": { "updateAdminSettingsRole": { "adminSettingsRole": { "id": "/api/admin/settings/roles/3", "_id": 3, "name": "Catalog Manager+" } } } } --- # Update Role (GraphQL) --- # Create Tax Category (GraphQL) URL: /api/graphql-api/admin/settings/tax-categories/create --- outline: false examples: - id: gql title: Create Tax Category query: | mutation Create($input: createAdminSettingsTaxCategoryInput!) { createAdminSettingsTaxCategory(input: $input) { adminSettingsTaxCategory { id _id code name } } } variables: | { "input": { "code": "us-tax", "name": "US Tax", "description": "Standard", "taxrates": [1, 2] } } response: | { "data": { "createAdminSettingsTaxCategory": { "adminSettingsTaxCategory": { "id": "/api/admin/settings/tax-categories/1", "_id": 1, "code": "us-tax", "name": "US Tax" } } } } --- # Create Tax Category (GraphQL) --- # Delete Tax Category (GraphQL) URL: /api/graphql-api/admin/settings/tax-categories/delete --- outline: false examples: - id: gql title: Delete Tax Category query: | mutation Delete($input: deleteAdminSettingsTaxCategoryInput!) { deleteAdminSettingsTaxCategory(input: $input) { adminSettingsTaxCategory { id } } } variables: | { "input": { "id": "/api/admin/settings/tax-categories/1" } } response: | { "data": { "deleteAdminSettingsTaxCategory": { "adminSettingsTaxCategory": null } } } --- # Delete Tax Category (GraphQL) ::: warning Guard Refuses if tax rates still attached. ::: --- # Tax Category Detail (GraphQL) URL: /api/graphql-api/admin/settings/tax-categories/detail --- outline: false examples: - id: gql title: Tax Category Detail query: | query A($id: ID!) { adminSettingsTaxCategory(id: $id) { id _id code name description taxRates } } variables: | { "id": "/api/admin/settings/tax-categories/1" } response: | { "data": { "adminSettingsTaxCategory": { "id": "/api/admin/settings/tax-categories/1", "_id": 1, "code": "us-tax", "name": "US Tax", "description": "Standard", "taxRates": [1, 2] } } } --- # Tax Category Detail (GraphQL) --- # List Tax Categories (GraphQL) URL: /api/graphql-api/admin/settings/tax-categories/list --- outline: false examples: - id: gql title: List Tax Categories query: | query A($first: Int) { adminSettingsTaxCategories(first: $first) { edges { cursor node { id _id code name description } } pageInfo { hasNextPage endCursor } totalCount } } variables: | { "first": 10 } response: | { "data": { "adminSettingsTaxCategories": { "edges": [{ "cursor": "MA==", "node": { "id": "/api/admin/settings/tax-categories/1", "_id": 1, "code": "us-tax", "name": "US Tax", "description": "Standard" } }], "pageInfo": { "hasNextPage": false, "endCursor": "MA==" }, "totalCount": 1 } } } --- # List Tax Categories (GraphQL) --- # Update Tax Category (GraphQL) URL: /api/graphql-api/admin/settings/tax-categories/update --- outline: false examples: - id: gql title: Update Tax Category query: | mutation Update($input: updateAdminSettingsTaxCategoryInput!) { updateAdminSettingsTaxCategory(input: $input) { adminSettingsTaxCategory { id _id name } } } variables: | { "input": { "id": "/api/admin/settings/tax-categories/1", "code": "us-tax", "name": "US Sales Tax", "description": "Updated", "taxrates": [1, 2, 3] } } response: | { "data": { "updateAdminSettingsTaxCategory": { "adminSettingsTaxCategory": { "id": "/api/admin/settings/tax-categories/1", "_id": 1, "name": "US Sales Tax" } } } } --- # Update Tax Category (GraphQL) ::: tip Prerequisites The example uses an illustrative `id` value. Replace it with the id of a tax category that exists in your store — use the [`adminSettingsTaxCategories`](./list.md) query to discover valid ids. ::: --- # Create Tax Rate (GraphQL) URL: /api/graphql-api/admin/settings/tax-rates/create --- outline: false examples: - id: gql title: Create Tax Rate query: | mutation Create($input: createAdminSettingsTaxRateInput!) { createAdminSettingsTaxRate(input: $input) { adminSettingsTaxRate { id _id identifier taxRate } } } variables: | { "input": { "identifier": "us-il-7", "taxRate": 7.25, "country": "US", "state": "IL", "isZip": false, "zipCode": "62704" } } response: | { "data": { "createAdminSettingsTaxRate": { "adminSettingsTaxRate": { "id": "/api/admin/settings/tax-rates/1", "_id": 1, "identifier": "us-il-7", "taxRate": 7.25 } } } } --- # Create Tax Rate (GraphQL) Pass `zip_code` when `is_zip=false`; pass `zip_from` + `zip_to` when `is_zip=true`. --- # Delete Tax Rate (GraphQL) URL: /api/graphql-api/admin/settings/tax-rates/delete --- outline: false examples: - id: gql title: Delete Tax Rate query: | mutation Delete($input: deleteAdminSettingsTaxRateInput!) { deleteAdminSettingsTaxRate(input: $input) { adminSettingsTaxRate { id } } } variables: | { "input": { "id": "/api/admin/settings/tax-rates/1" } } response: | { "data": { "deleteAdminSettingsTaxRate": { "adminSettingsTaxRate": null } } } --- # Delete Tax Rate (GraphQL) ::: tip Prerequisites The example uses an illustrative `id` value. Replace it with the id of a tax rate that exists in your store — use the [`adminSettingsTaxRates`](./list.md) query to discover valid ids. ::: --- # Tax Rate Detail (GraphQL) URL: /api/graphql-api/admin/settings/tax-rates/detail --- outline: false examples: - id: gql title: Tax Rate Detail query: | query A($id: ID!) { adminSettingsTaxRate(id: $id) { id _id identifier taxRate country state isZip zipCode zipFrom zipTo } } variables: | { "id": "/api/admin/settings/tax-rates/1" } response: | { "data": { "adminSettingsTaxRate": { "id": "/api/admin/settings/tax-rates/1", "_id": 1, "identifier": "us-il-7", "taxRate": 7.25, "country": "US", "state": "IL", "isZip": false, "zipCode": "62704", "zipFrom": null, "zipTo": null } } } --- # Tax Rate Detail (GraphQL) --- # List Tax Rates (GraphQL) URL: /api/graphql-api/admin/settings/tax-rates/list --- outline: false examples: - id: gql title: List Tax Rates query: | query A($first: Int) { adminSettingsTaxRates(first: $first) { edges { cursor node { id _id identifier taxRate country state isZip zipCode } } pageInfo { hasNextPage endCursor } totalCount } } variables: | { "first": 10 } response: | { "data": { "adminSettingsTaxRates": { "edges": [{ "cursor": "MA==", "node": { "id": "/api/admin/settings/tax-rates/1", "_id": 1, "identifier": "us-il-7", "taxRate": 7.25, "country": "US", "state": "IL", "isZip": false, "zipCode": "62704" } }], "pageInfo": { "hasNextPage": false, "endCursor": "MA==" }, "totalCount": 1 } } } --- # List Tax Rates (GraphQL) --- # Update Tax Rate (GraphQL) URL: /api/graphql-api/admin/settings/tax-rates/update --- outline: false examples: - id: gql title: Update Tax Rate query: | mutation Update($input: updateAdminSettingsTaxRateInput!) { updateAdminSettingsTaxRate(input: $input) { adminSettingsTaxRate { id _id taxRate } } } variables: | { "input": { "id": "/api/admin/settings/tax-rates/1", "taxRate": 7.5 } } response: | { "data": { "updateAdminSettingsTaxRate": { "adminSettingsTaxRate": { "id": "/api/admin/settings/tax-rates/1", "_id": 1, "taxRate": 7.5 } } } } --- # Update Tax Rate (GraphQL) --- # Create Theme Customization (GraphQL) URL: /api/graphql-api/admin/settings/themes/create --- outline: false examples: - id: gql title: Create Theme Customization query: | mutation Create($input: createAdminSettingsThemeInput!) { createAdminSettingsTheme(input: $input) { adminSettingsTheme { id _id name } } } variables: | { "input": { "name": "Homepage Banner", "type": "image_carousel", "sortOrder": 1, "channelId": 1, "themeCode": "default" } } response: | { "data": { "createAdminSettingsTheme": { "adminSettingsTheme": { "id": "/api/admin/settings/themes/1", "_id": 1, "name": "Homepage Banner" } } } } --- # Create Theme Customization (GraphQL) ::: warning Image upload deferred Multipart binary upload not yet supported. ::: --- # Delete Theme Customization (GraphQL) URL: /api/graphql-api/admin/settings/themes/delete --- outline: false examples: - id: gql title: Delete Theme Customization query: | mutation Delete($input: deleteAdminSettingsThemeInput!) { deleteAdminSettingsTheme(input: $input) { adminSettingsTheme { id } } } variables: | { "input": { "id": "/api/admin/settings/themes/1" } } response: | { "data": { "deleteAdminSettingsTheme": { "adminSettingsTheme": null } } } --- # Delete Theme Customization (GraphQL) --- # Theme Customization Detail (GraphQL) URL: /api/graphql-api/admin/settings/themes/detail --- outline: false examples: - id: gql title: Theme Customization Detail query: | query A($id: ID!) { adminSettingsTheme(id: $id) { id _id name type sortOrder channelId themeCode status translations } } variables: | { "id": "/api/admin/settings/themes/1" } response: | { "data": { "adminSettingsTheme": { "id": "/api/admin/settings/themes/1", "_id": 1, "name": "Homepage Banner", "type": "image_carousel", "sortOrder": 1, "channelId": 1, "themeCode": "default", "status": 1, "translations": [{ "locale": "en", "options": {} }] } } } --- # Theme Customization Detail (GraphQL) --- # List Theme Customizations (GraphQL) URL: /api/graphql-api/admin/settings/themes/list --- outline: false examples: - id: gql title: List Theme Customizations query: | query A($first: Int) { adminSettingsThemes(first: $first) { edges { cursor node { id _id name type sortOrder channelId themeCode status } } pageInfo { hasNextPage endCursor } totalCount } } variables: | { "first": 10 } response: | { "data": { "adminSettingsThemes": { "edges": [{ "cursor": "MA==", "node": { "id": "/api/admin/settings/themes/1", "_id": 1, "name": "Homepage Banner", "type": "image_carousel", "sortOrder": 1, "channelId": 1, "themeCode": "default", "status": 1 } }], "pageInfo": { "hasNextPage": false, "endCursor": "MA==" }, "totalCount": 1 } } } --- # List Theme Customizations (GraphQL) --- # Mass Delete Theme Customizations (GraphQL) URL: /api/graphql-api/admin/settings/themes/mass-delete --- outline: false examples: - id: gql title: Mass Delete Theme Customizations query: | mutation MassDelete($input: createAdminSettingsThemeMassDeleteInput!) { createAdminSettingsThemeMassDelete(input: $input) { adminSettingsThemeMassDelete { deleted message } } } variables: | { "input": { "indices": [1, 2] } } response: | { "data": { "createAdminSettingsThemeMassDelete": { "adminSettingsThemeMassDelete": { "deleted": [1, 2], "message": "Themes deleted." } } } } --- # Mass Delete Theme Customizations (GraphQL) --- # Mass Update Theme Status (GraphQL) URL: /api/graphql-api/admin/settings/themes/mass-update-status --- outline: false examples: - id: gql title: Mass Update Theme Status query: | mutation MassUpdate($input: createAdminSettingsThemeMassUpdateStatusInput!) { } variables: | response: | { "data": { "createAdminSettingsThemeMassUpdateStatus": { "adminSettingsThemeMassUpdateStatus": { "updated": [1, 2], "value": 0, "message": "Statuses updated." } } } } --- # Mass Update Theme Status (GraphQL) --- # Update Theme Customization (GraphQL) URL: /api/graphql-api/admin/settings/themes/update --- outline: false examples: - id: gql title: Update Theme Customization query: | mutation Update($input: updateAdminSettingsThemeInput!) { updateAdminSettingsTheme(input: $input) { adminSettingsTheme { id _id name } } } variables: | { "input": { "id": "/api/admin/settings/themes/1", "locale": "en", "options": { "title": "Welcome" } } } response: | { "data": { "updateAdminSettingsTheme": { "adminSettingsTheme": { "id": "/api/admin/settings/themes/1", "_id": 1, "name": "Homepage Banner" } } } } --- # Update Theme Customization (GraphQL) ::: tip Prerequisites The example uses an illustrative `id` value. Replace it with the id of a theme that exists in your store — use the [`adminSettingsThemes`](./list.md) query to discover valid ids. ::: --- # Create Admin User (GraphQL) URL: /api/graphql-api/admin/settings/users/create --- outline: false examples: - id: gql title: Create Admin User query: | mutation Create($input: createAdminSettingsUserInput!) { createAdminSettingsUser(input: $input) { adminSettingsUser { id _id name email roleId } } } variables: | { "input": { "name": "Ops User", "email": "ops@example.com", "password": "secret123", "roleId": 2, "status": 1 } } response: | { "data": { "createAdminSettingsUser": { "adminSettingsUser": { "id": "/api/admin/settings/users/4", "_id": 4, "name": "Ops User", "email": "ops@example.com", "roleId": 2 } } } } --- # Create Admin User (GraphQL) --- # Delete Admin User (GraphQL) URL: /api/graphql-api/admin/settings/users/delete --- outline: false examples: - id: gql title: Delete Admin User query: | mutation Delete($input: deleteAdminSettingsUserInput!) { deleteAdminSettingsUser(input: $input) { adminSettingsUser { id } } } variables: | { "input": { "id": "/api/admin/settings/users/4" } } response: | { "data": { "deleteAdminSettingsUser": { "adminSettingsUser": null } } } --- # Delete Admin User (GraphQL) ::: warning Guards Refuses self-delete or last-admin. ::: --- # Delete My Account — GraphQL URL: /api/graphql-api/admin/settings/users/delete-self --- outline: false examples: - id: gql title: Delete My Account query: | mutation DeleteSelf($input: createAdminSettingsUserDeleteSelfInput!) { createAdminSettingsUserDeleteSelf(input: $input) { adminSettingsUserDeleteSelf { success message } } } variables: | { "input": { "password": "current-password" } } response: | { "data": { "createAdminSettingsUserDeleteSelf": { "adminSettingsUserDeleteSelf": { "success": true, "message": "Your admin account has been deleted." } } } } --- # Delete My Account — GraphQL Deletes the **authenticated** admin's own account after re-confirming their password. - Requires the caller's current `password`. A missing or incorrect password returns an `errors` entry; the account is left intact. - Refuses to delete the **last remaining** admin. - Distinct from [Delete User](./delete), which deletes another admin and always refuses self-deletion. - Deleting the account also invalidates the token that owns it. No additional permission is required beyond authentication — the password confirmation is the gate. --- # Admin User Detail (GraphQL) URL: /api/graphql-api/admin/settings/users/detail --- outline: false examples: - id: gql title: Admin User Detail query: | query A($id: ID!) { adminSettingsUser(id: $id) { id _id name email roleId status } } variables: | { "id": "/api/admin/settings/users/1" } response: | { "data": { "adminSettingsUser": { "id": "/api/admin/settings/users/1", "_id": 1, "name": "Super Admin", "email": "admin@example.com", "roleId": 1, "status": 1 } } } --- # Admin User Detail (GraphQL) --- # List Admin Users (GraphQL) URL: /api/graphql-api/admin/settings/users/list --- outline: false examples: - id: gql title: List Admin Users query: | query A($first: Int) { adminSettingsUsers(first: $first) { edges { cursor node { id _id name email roleId status } } pageInfo { hasNextPage endCursor } totalCount } } variables: | { "first": 10 } response: | { "data": { "adminSettingsUsers": { "edges": [{ "cursor": "MA==", "node": { "id": "/api/admin/settings/users/1", "_id": 1, "name": "Super Admin", "email": "admin@example.com", "roleId": 1, "status": 1 } }], "pageInfo": { "hasNextPage": false, "endCursor": "MA==" }, "totalCount": 1 } } } --- # List Admin Users (GraphQL) --- # Update Admin User (GraphQL) URL: /api/graphql-api/admin/settings/users/update --- outline: false examples: - id: gql title: Update Admin User query: | mutation Update($input: updateAdminSettingsUserInput!) { updateAdminSettingsUser(input: $input) { adminSettingsUser { id _id name } } } variables: | { "input": { "id": "/api/admin/settings/users/4", "name": "Ops User (Updated)" } } response: | { "data": { "updateAdminSettingsUser": { "adminSettingsUser": { "id": "/api/admin/settings/users/4", "_id": 4, "name": "Ops User (Updated)" } } } } --- # Update Admin User (GraphQL) ::: tip Prerequisites The example uses an illustrative `id` value. Replace it with the id of a user that exists in your store — use the [`adminSettingsUsers`](./list.md) query to discover valid ids. ::: --- # AttributeOption GraphQL API Guide URL: /api/graphql-api/attribute-option # AttributeOption GraphQL API Guide ## Overview The AttributeOption GraphQL API provides two primary ways to query attribute options: 1. **Nested Query** - Access options as a subresource of an Attribute 2. **Direct Query** - Query options directly with a required attribute ID parameter Both methods support **cursor-based pagination** for efficient data retrieval. --- ## Table of Contents - [Authentication](#authentication) - [Nested Query (via Attribute)](#nested-query-via-attribute) - [Direct Query (attributeOptions)](#direct-query-attributeoptions) - [Fields & Responses](#fields--responses) - [Pagination](#pagination) - [Error Handling](#error-handling) - [Complete Examples](#complete-examples) - [Best Practices](#best-practices) --- ## Authentication All AttributeOption GraphQL queries require **authentication**. ### Bearer Token Include your authentication token in the `Authorization` header: ```bash curl -X POST "http://your-api.com/api/graphql" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: application/json" \ -d '{"query":"..."}' ``` ### GraphQL Query ```graphql query { attribute(id: "/api/admin/attributes/23") { id code } } ``` --- ## Nested Query (via Attribute) Access attribute options as a subresource of the Attribute resource. ### Query Structure ```graphql query { attribute(id: "ATTRIBUTE_ID") { id code options(first: LIMIT, after: "CURSOR", last: LIMIT, before: "CURSOR") { edges { node { # fields } cursor } pageInfo { hasNextPage hasPreviousPage startCursor endCursor } } } } ``` ### Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `id` | String | ✅ Yes | Attribute ID in format `/api/admin/attributes/{id}` | | `first` | Int | ❌ No | Number of items to retrieve (forward pagination) | | `last` | Int | ❌ No | Number of items to retrieve (backward pagination) | | `after` | String | ❌ No | Cursor for forward pagination | | `before` | String | ❌ No | Cursor for backward pagination | ### Basic Example ```graphql query { attribute(id: "/api/admin/attributes/23") { id code options(first: 5) { edges { node { id adminName sortOrder swatchValue } } pageInfo { hasNextPage endCursor } } } } ``` **Response:** ```json { "data": { "attribute": { "id": "/api/admin/attributes/23", "code": "color", "options": { "edges": [ { "node": { "id": "/api/admin/attribute-options/1", "adminName": "Red", "sortOrder": 0, "swatchValue": "#e10e0e" }, "cursor": "MA==" }, { "node": { "id": "/api/admin/attribute-options/2", "adminName": "Green", "sortOrder": 1, "swatchValue": "#155616" }, "cursor": "MQ==" } ], "pageInfo": { "hasNextPage": true, "endCursor": "MQ==" } } } } } ``` ### With Translations Query translations for each option: ```graphql query { attribute(id: "/api/admin/attributes/23") { code options(first: 3) { edges { node { id adminName translation { id locale label } translations(first: 3) { edges { node { id locale label } } pageInfo { hasNextPage } } } } } } } ``` **Response:** ```json { "data": { "attribute": { "code": "color", "options": { "edges": [ { "node": { "id": "/api/admin/attribute-options/1", "adminName": "Red", "translation": { "id": "/api/attribute_option_translations/1", "locale": "en", "label": "Red" }, "translations": { "edges": [ { "node": { "id": "/api/attribute_option_translations/1", "locale": "en", "label": "Red" } }, { "node": { "id": "/api/attribute_option_translations/84", "locale": "ar", "label": "أحمر" } } ], "pageInfo": { "hasNextPage": false } } } } ] } } } } ``` --- ## Direct Query (attributeOptions) Query attribute options directly using the `attributeId` parameter. ### Query Structure ```graphql query { attributeOptions( attributeId: ATTRIBUTE_ID first: LIMIT after: "CURSOR" last: LIMIT before: "CURSOR" ) { edges { node { # fields } cursor } pageInfo { hasNextPage hasPreviousPage startCursor endCursor } } } ``` ### Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `attributeId` | Int | ✅ **Yes** | Numeric attribute ID (e.g., `23`) | | `first` | Int | ❌ No | Number of items to retrieve (forward pagination) | | `last` | Int | ❌ No | Number of items to retrieve (backward pagination) | | `after` | String | ❌ No | Cursor for forward pagination | | `before` | String | ❌ No | Cursor for backward pagination | ### Basic Example ```graphql query { attributeOptions(attributeId: 23, first: 10) { edges { node { id _id adminName sortOrder swatchValue swatchValueUrl } cursor } pageInfo { hasNextPage endCursor } } } ``` **Response:** ```json { "data": { "attributeOptions": { "edges": [ { "node": { "id": "/api/admin/attribute-options/1", "_id": 1, "adminName": "Red", "sortOrder": 0, "swatchValue": "#e10e0e", "swatchValueUrl": null }, "cursor": "MA==" }, { "node": { "id": "/api/admin/attribute-options/2", "_id": 2, "adminName": "Green", "sortOrder": 1, "swatchValue": "#155616", "swatchValueUrl": null }, "cursor": "MQ==" } ], "pageInfo": { "hasNextPage": false, "endCursor": "MQ==" } } } } ``` ### With All Fields ```graphql query { attributeOptions(attributeId: 23, first: 5) { edges { node { id _id adminName sortOrder swatchValue swatchValueUrl translation { id locale label } translations(first: 2) { edges { node { id locale label } } pageInfo { hasNextPage } } } } pageInfo { hasNextPage endCursor } } } ``` --- ## Fields & Responses ### AttributeOption Fields | Field | Type | Description | |-------|------|-------------| | `id` | String | Unique identifier in format `/api/admin/attribute-options/{id}` | | `_id` | Int | Numeric ID (alias for `id`) | | `adminName` | String | Admin-facing name (e.g., "Red", "Small") | | `sortOrder` | Int | Display sort order (0, 1, 2, ...) | | `swatchValue` | String | Swatch color value (hex code) or text representation | | `swatchValueUrl` | String | URL to swatch image file (nullable) | | `translation` | Object | Single translation for default/current locale | | `translations` | Connection | Collection of all translations with pagination | ### Translation Fields | Field | Type | Description | |-------|------|-------------| | `id` | String | Translation ID in format `/api/attribute_option_translations/{id}` | | `_id` | Int | Numeric translation ID | | `locale` | String | Language locale code (e.g., "en", "ar", "fr") | | `label` | String | Translated label for the option | ### PageInfo Fields | Field | Type | Description | |-------|------|-------------| | `hasNextPage` | Boolean | Whether more items exist forward | | `hasPreviousPage` | Boolean | Whether more items exist backward | | `startCursor` | String | Cursor for the first item | | `endCursor` | String | Cursor for the last item | --- ## Pagination ### Forward Pagination (Limit & Offset) Use `first` to limit results and `after` to continue from a cursor: ```graphql query { attributeOptions(attributeId: 23, first: 2) { edges { node { id adminName } cursor } pageInfo { hasNextPage endCursor } } } ``` To get the next page, use the `endCursor` as the `after` parameter: ```graphql query { attributeOptions(attributeId: 23, first: 2, after: "MQ==") { edges { node { id adminName } } pageInfo { hasNextPage endCursor } } } ``` ### Backward Pagination Use `last` to retrieve the last N items and `before` to go backward: ```graphql query { attributeOptions(attributeId: 23, last: 3) { edges { node { id adminName } cursor } pageInfo { hasPreviousPage startCursor } } } ``` --- ## Error Handling ### Missing Required attributeId **Query:** ```graphql query { attributeOptions(first: 10) { edges { node { id } } } } ``` **Error Response:** ```json { "errors": [ { "message": "Field \"attributeOptions\" argument \"attributeId\" of type \"Int!\" is required but not provided.", "locations": [ { "line": 1, "column": 9 } ] } ] } ``` ### Invalid Attribute ID Type **Query:** ```graphql query { attributeOptions(attributeId: "not-a-number", first: 10) { edges { node { id } } } } ``` **Error Response:** ```json { "errors": [ { "message": "Variable \"$attributeId\" got invalid value \"not-a-number\"; Int cannot represent non-integer value: \"not-a-number\"" } ] } ``` ### Attribute Not Found If the attribute ID doesn't exist, an empty result is returned: ```json { "data": { "attributeOptions": { "edges": [], "pageInfo": { "hasNextPage": false, "hasPreviousPage": false, "startCursor": null, "endCursor": null } } } } ``` --- ## Complete Examples ### Example 1: Fetch Color Attribute with All Options ```graphql query GetColorAttribute { attribute(id: "/api/admin/attributes/23") { id code name options(first: 20) { edges { node { id adminName sortOrder swatchValue } } pageInfo { hasNextPage } } } } ``` ### Example 2: Paginate Through All Options ```graphql query GetOptionsPage($attributeId: Int!, $cursor: String) { attributeOptions(attributeId: $attributeId, first: 10, after: $cursor) { edges { node { id adminName sortOrder } cursor } pageInfo { hasNextPage endCursor } } } ``` **Variables:** ```json { "attributeId": 23, "cursor": null } ``` ### Example 3: Fetch With Translated Labels ```graphql query GetTranslatedOptions { attributeOptions(attributeId: 23, first: 5) { edges { node { id adminName translations(first: 5) { edges { node { locale label } } } } } } } ``` ### Example 4: Multi-language Support ```graphql query GetOptionsInMultipleLanguages { attribute(id: "/api/admin/attributes/23") { code options(first: 10) { edges { node { adminName translation { locale label } translations(first: 10) { edges { node { locale label } } } } } } } } ``` ### Example 5: Using Variables in Queries ```graphql query GetAttributeOptions($attrId: Int!, $limit: Int!) { attributeOptions(attributeId: $attrId, first: $limit) { edges { node { id _id adminName sortOrder swatchValue translation { locale label } } cursor } pageInfo { hasNextPage endCursor hasPreviousPage startCursor } } } ``` **Variables:** ```json { "attrId": 23, "limit": 5 } ``` --- ## Best Practices ### 1. Use Specific Fields Only request fields you need: ```graphql # ✅ Good - Only fetch needed fields query { attributeOptions(attributeId: 23, first: 10) { edges { node { id adminName } } } } # ❌ Avoid - Fetching everything query { attributeOptions(attributeId: 23, first: 10) { edges { node { id adminName sortOrder swatchValue swatchValueUrl translation { id locale label } translations { edges { node { id locale label } } } } } } } ``` ### 2. Set Reasonable Pagination Limits ```graphql # ✅ Good - Reasonable limit query { attributeOptions(attributeId: 23, first: 20) { edges { node { id adminName } } pageInfo { hasNextPage endCursor } } } # ❌ Avoid - Too large limit query { attributeOptions(attributeId: 23, first: 10000) { ... } } ``` ### 3. Use Query Variables ```graphql # ✅ Good - Using variables query GetOptions($id: Int!) { attributeOptions(attributeId: $id, first: 10) { ... } } # ❌ Avoid - Hardcoding values query { attributeOptions(attributeId: 23, first: 10) { ... } } ``` ### 4. Handle Pagination Properly ```graphql # ✅ Good - Always check hasNextPage query { attributeOptions(attributeId: 23, first: 10) { edges { node { id adminName } } pageInfo { hasNextPage # Use this to determine if more pages exist endCursor # Use this for the next request } } } ``` ### 5. Cache Cursors Store cursors from responses for pagination rather than computing them: ```javascript // ✅ Good const response = await fetch('/api/graphql', { method: 'POST', body: JSON.stringify({ query: `query { attributeOptions(attributeId: 23, first: 10) { ... } }` }) }); const nextCursor = response.data.attributeOptions.pageInfo.endCursor; // Use nextCursor in next request const nextPage = await fetch('/api/graphql', { method: 'POST', body: JSON.stringify({ query: `query { attributeOptions(attributeId: 23, first: 10, after: "${nextCursor}") { ... } }` }) }); ``` --- ## FAQ **Q: What's the difference between nested and direct queries?** A: - **Nested Query**: Get options as part of an attribute. Useful when you already have the attribute ID. - **Direct Query**: Get options directly with just the attribute ID. More efficient when you only need options. **Q: Can I use both `first` and `last` together?** A: No. Use either `first` (forward pagination) or `last` (backward pagination), not both. **Q: How do I get all options for an attribute?** A: Use pagination with `first` and the `hasNextPage` field: ```graphql query { attributeOptions(attributeId: 23, first: 100) { edges { node { id adminName } } pageInfo { hasNextPage endCursor } } } ``` Then if `hasNextPage` is true, fetch the next page using `after: "endCursor"`. **Q: Why does `totalCount` show 0?** A: This is a known limitation with API Platform's relationship pagination. Use `hasNextPage` instead to determine if more results exist. --- ## Related Topics - [Pagination Guide](/api/graphql-api/pagination) - Comprehensive cursor pagination documentation - [Authentication](/api/graphql-api/authentication) - Secure your API requests - [Best Practices](/api/graphql-api/best-practices) - API performance and security guidelines - [GraphQL Overview](/api/graphql) - GraphQL API introduction --- **Next Steps:** - 📚 [Shop API Reference](/api/graphql-api/shop-api) - Customer-facing attribute operations - 🔑 [Admin API Reference](/api/graphql-api/admin-api) - Administrative attribute management --- # Authentication Guide URL: /api/graphql-api/authentication # Authentication Guide Bagisto GraphQL API supports multiple authentication methods to handle different use cases - from guest checkouts to admin operations. ## Authentication Methods Overview | Method | Use Case | Token Type | Expiration | |--------|----------|-----------|-----------| | Guest Cart | Unauthenticated users | UUID Token | Session-based | | Customer Token | Registered customers | JWT/Bearer Token | Configurable | | Admin Token | Administrator operations | JWT/Bearer Token | Configurable | ## 1. Guest Checkout Authentication Perfect for unauthenticated users who want to browse and checkout without creating an account. ### Create Guest Cart Token ```graphql mutation { createCartToken(input: {}) { cartToken } } ``` **Response:** ```json { "data": { "createCartToken": { "cartToken": "550e8400-e29b-41d4-a716-446655440000" } } } ``` ### Using Guest Token Pass the token as a Bearer token in the `Authorization` header for all subsequent requests: ```bash curl -X POST https://your-domain.com/api/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer 550e8400-e29b-41d4-a716-446655440000" \ -d '{ "query": "query { cart { items { edges { node { id } } } } }" }' ``` ### Guest Cart Workflow ```graphql # 1. Create cart token mutation { createCartToken(input: {}) { cartToken { id cartToken } } } # 2. Add product to guest cart mutation { addProductsToCart(input: { productId: 50 quantity: 1 }) { addProductInCart { id items { edges { node { id name sku } } } } } } # 3. Proceed to checkout mutation { mutation createCheckoutOrder { createCheckoutOrder(input:{}) { checkoutOrder { id orderId } } } } ``` ## 2. Customer Authentication For registered users, provide a secure login/logout flow. ### Customer Registration ```graphql mutation { createCustomer(input: { firstName: "John", lastName: "Doe", gender: "Male", dateOfBirth: "01/15/1990", phone: "555-0123", status: "1", isVerified: "1", isSuspended: "0", email: "john.doe@example.com", password: "SecurePass@123", confirmPassword: "SecurePass@123", subscribedToNewsLetter: true }) { customer { id _id apiToken channelId customerGroupId dateOfBirth email gender isSuspended isVerified name firstName lastName rememberToken subscribedToNewsLetter status token phone } } } ``` **Response:** ```json { "data": { "createCustomer": { "customer": { "id": "1", "firstName": "John", "email": "john@example.com", "createdAt": "2024-12-19T10:30:00Z" } } } } ``` ### Customer Login ```graphql mutation { createCustomerLogin( input: { email: "john@example.com" password: "SecurePassword123!" } ) { customerLogin { id _id token success message } } } ``` **Response:** ```json { "data": { "createCustomerLogin": { "customerLogin": { "id": "1", "_id": 1, "apiToken": "OOxDk2s06JCndg5FHb8WbfF6ZR8jGq231...", "token": "1|xy56RHXcttcDnimTHVEGIeyxzMKAWX6MyICCsZTA7dc...", "success": true, "message": "You have logged in successfully" } } } } ``` ### Using Customer Token Include the access token in the `Authorization` header: ```bash curl -X POST https://your-domain.com/api/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \ -d '{ "query": "query { customer { id firstName email } }" }' ``` ### Verify Token Check if a token is still valid: ```graphql mutation { createVerifyToken(input: { token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." }) { verifyToken { isValid message } } } ``` **Response:** ```json { "data": { "createVerifyToken": { "verifyToken": { "isValid": true, "message": "Token is valid" } } } } ``` ### Customer Logout ```graphql mutation { createLogout(input: {}) { status message } } ``` **Response:** ```json { "data": { "createLogout": { "status": "success", "message": "Logout successful" } } } ``` ### Authenticated Customer Workflow ```graphql # 1. Register/Login mutation { createLogin(input: { email: "john@example.com" password: "SecurePassword123!" }) { accessToken } } # 2. Get customer profile (requires token) query { customer { id firstName email addresses { edges { node { id firstName address city } } } } } # 3. Create authenticated cart mutation { createCart(input: {}) { cart { id } } } # 4. Manage cart and checkout with customer info mutation { createOrder(input: { cartId: "your-cart-id" billingAddressId: "1" shippingAddressId: "1" shippingMethod: "flatrate_flatrate" paymentMethod: "paypal" }) { order { id incrementId status } } } # 5. Logout mutation { createLogout(input: {}) { status } } ``` ## 3. Admin Authentication For administrative operations and management tasks. Admin requests authenticate with a pre-issued **Integration token** — there is no login call. The whole flow runs through the **Integration** plugin in the admin panel: 1. In the admin panel, open the **Integration** menu (`Admin → Integration`). 2. Generate an Integration token. A store owner generates tokens here and shares them with the sub-admins who need API access — each token is tied to a specific admin user and inherits that admin's permissions. 3. Copy the token the moment it is shown — it is displayed **once**. 4. Send it on every admin request as a Bearer token: ``` POST /api/admin/graphql Authorization: Bearer | Content-Type: application/json ``` Admin GraphQL has its own endpoint (`/api/admin/graphql`), separate from the shop endpoint (`/api/graphql`). It takes the `Authorization` header **only** — the storefront key is not used here. See [Admin Authentication](/api/graphql-api/admin/authentication) for token lifecycle, IP allowlists, and rate limits. ### Endpoint | URL | Headers | |-----|---------| | `POST /api/admin/graphql` | `Authorization: Bearer ` | | `GET /api/admin/graphiql` | Browser-only playground UI | ### Using the Admin Token ```bash curl -X POST https://your-domain.com/api/admin/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer 5|1dYWpciAn2Ro8dfsabA89ohhduVWWXqicyPyQeIH" \ -d '{ "query": "query { adminProducts(first: 10) { edges { node { id sku name } } } }" }' ``` ### Revoking a Token Open the **Integration** menu, find the token row, and click **Revoke** (or use the signed one-click link in the lifecycle email). A revoked token stops working immediately. ## Authentication Best Practices ### 🔒 Security Tips 1. **HTTPS Only**: Always use HTTPS in production ``` ✓ https://your-domain.com/api/graphql ✗ http://your-domain.com/api/graphql ``` 2. **Secure Token Storage** - **Frontend**: Use httpOnly cookies (JavaScript cannot access) - **Mobile**: Use secure device storage - **Backend**: Store in secure session or encrypted database 3. **Token Expiration** - Set reasonable expiration times (e.g., 24 hours) - Implement refresh token mechanism - Validate token expiration on each request 4. **Never Log Tokens** ```javascript // ✗ Bad console.log(accessToken); localStorage.setItem('token', accessToken); // ✓ Good // Store in httpOnly cookie (server-side) // Use secure storage on mobile ``` 5. **Validate on Server** ```graphql # Always verify token server-side mutation { createVerifyToken(input: { token: userToken }) { verifyToken { isValid } } } ``` ### Handling Expired Tokens ```javascript async function makeGraphQLRequest(query, token) { let response = await fetch('/api/graphql', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ query }) }); if (response.status === 401) { // Token expired, refresh or re-authenticate token = await refreshToken(); return makeGraphQLRequest(query, token); } return response.json(); } ``` ### Environment Variables Store sensitive information securely: ```bash # .env GRAPHQL_API_URL=https://your-domain.com/api/graphql CUSTOMER_TOKEN=your_token_here ADMIN_TOKEN=admin_token_here ``` ```javascript const API_URL = process.env.REACT_APP_API_URL; const token = process.env.REACT_APP_CUSTOMER_TOKEN; ``` ## Token Refresh Strategy Implement a refresh token mechanism for long-lived sessions: ```graphql # Initial login mutation { createLogin(input: { email: "user@example.com" password: "password" }) { accessToken refreshToken } } # When access token expires, use refresh token mutation { refreshToken(input: { refreshToken: "refresh_token_here" }) { accessToken } } ``` ## Multi-Channel Authentication Bagisto supports multiple channels. Include channel information: ```graphql query { channel { id code name rootCategoryId } } ``` Use channel-specific queries: ```graphql query { products(channel: "default", first: 10) { edges { node { id name prices { channel price } } } } } ``` ## Error Handling ### Common Authentication Errors ```json { "errors": [ { "message": "Unauthenticated", "extensions": { "guard": "api" } } ] } ``` ```json { "errors": [ { "message": "Invalid credentials", "extensions": { "category": "authentication" } } ] } ``` ### Error Response Handling ```javascript async function login(email, password) { const response = await fetch('/api/graphql', { method: 'POST', body: JSON.stringify({ query: `mutation { createLogin(input: {email: "${email}", password: "${password}"}) { accessToken } }` }) }); const result = await response.json(); if (result.errors) { result.errors.forEach(error => { console.error('Auth Error:', error.message); }); return null; } return result.data.createLogin.accessToken; } ``` ## Logout and Cleanup Always implement proper logout: ```javascript async function logout() { // Clear GraphQL session await fetch('/api/graphql', { method: 'POST', headers: { 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ query: 'mutation { createLogout(input: {}) { status } }' }) }); // Clear local storage localStorage.removeItem('token'); // Redirect to login window.location.href = '/login'; } ``` ## Multi-Tenant Authentication For multi-tenant setups: ```graphql query { channel(code: "channel-code") { id code locales currencies } } ``` --- **Next Steps:** - 📚 [Shop API Reference](/api/graphql-api/shop-api) - Use authentication in shop operations - 🔑 [Admin API Reference](/api/graphql-api/admin-api) - Admin authentication flows - 💡 [Best Practices](/api/graphql-api/best-practices) - Advanced security topics --- # Best Practices & Testing Guide URL: /api/graphql-api/best-practices # Best Practices & Testing Guide Comprehensive guide for optimizing your Bagisto GraphQL API integration with best practices, performance tips, and testing strategies. ## Performance Optimization ### 1. Query Optimization #### Only Request Needed Fields **❌ Bad - Requesting too much data:** ```graphql query { products(first: 100) { edges { node { id name price description shortDescription weight sku status createdAt updatedAt images { edges { node { id type path url alt } } } attributes { edges { node { id code label value } } } } } } } ``` **✅ Good - Request only what you need:** ```graphql query { products(first: 100) { edges { node { id name price } } } } ``` #### Use Pagination ```graphql query { products(first: 20, after: "cursor-from-previous-query") { pageInfo { hasNextPage endCursor } edges { node { id name price } } } } ``` **Why:** - Reduces payload size - Improves response time - Decreases memory usage - Better for mobile devices #### Use Aliases for Multiple Queries ```graphql query { newProducts: products( channel: "default" first: 10 sort: "newest" ) { edges { node { id name price } } } saleProducts: products( channel: "default" first: 10 filters: { onSale: true } ) { edges { node { id name price } } } } ``` ### 2. Caching Strategies #### Client-Side Caching with Apollo ```javascript import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client'; const cache = new InMemoryCache({ typePolicies: { Query: { fields: { products: { merge(existing, incoming) { return incoming; } } } } } }); const client = new ApolloClient({ link: new HttpLink({ uri: 'https://your-domain.com/api/graphql' }), cache, defaultOptions: { watchQuery: { fetchPolicy: 'cache-and-network', errorPolicy: 'all', }, query: { fetchPolicy: 'network-only', errorPolicy: 'all', }, } }); ``` #### Cache Key Configuration ```javascript const cache = new InMemoryCache({ typePolicies: { Product: { keyFields: ['id', 'channel'] // Cache per channel }, Customer: { keyFields: ['id'] } } }); ``` #### Redis Caching in Backend ```php // Laravel example Cache::remember('products:channel:default', 3600, function () { return BagitoService::getProducts('default'); }); ``` ### 3. Connection Pooling **Node.js/JavaScript:** ```javascript const http = require('http'); const https = require('https'); const httpAgent = new http.Agent({ keepAlive: true, maxSockets: 50 }); const httpsAgent = new https.Agent({ keepAlive: true, maxSockets: 50 }); const client = new ApolloClient({ link: new HttpLink({ uri: 'https://your-domain.com/api/graphql', fetchOptions: { agent: httpsAgent } }) }); ``` ### 4. Batch Requests Instead of multiple individual requests: ```graphql # ❌ Bad - Multiple requests query { product(id: "1") { id name } } query { product(id: "2") { id name } } # ✅ Good - Single batch query query { product1: product(id: "1") { id name } product2: product(id: "2") { id name } product3: product(id: "3") { id name } } ``` ## Security Best Practices ### 1. Authentication & Authorization #### Always Validate Tokens Server-Side ```graphql mutation { createVerifyToken(input: { token: userToken }) { verifyToken { isValid message } } } ``` #### Use HTTPS Only ```javascript // ✓ Correct const API_URL = 'https://your-domain.com/api/graphql'; // ✗ Never use HTTP const API_URL = 'http://your-domain.com/api/graphql'; ``` #### Implement Rate Limiting ```javascript class RateLimiter { constructor(maxRequests = 100, windowMs = 60000) { this.maxRequests = maxRequests; this.windowMs = windowMs; this.requests = []; } allow() { const now = Date.now(); this.requests = this.requests.filter( time => now - time < this.windowMs ); if (this.requests.length >= this.maxRequests) { return false; } this.requests.push(now); return true; } } const limiter = new RateLimiter(100, 60000); async function makeRequest(query) { if (!limiter.allow()) { throw new Error('Rate limit exceeded'); } return fetch('/api/graphql', { body: JSON.stringify({ query }) }); } ``` ### 2. Input Validation Always validate input before sending: ```javascript function validateEmail(email) { const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; return regex.test(email); } function validatePassword(password) { return password.length >= 8 && /[A-Z]/.test(password); } async function register(email, password) { if (!validateEmail(email)) { throw new Error('Invalid email format'); } if (!validatePassword(password)) { throw new Error('Password must be at least 8 chars with uppercase'); } // Proceed with mutation } ``` ### 3. Secure Token Storage **❌ Never:** ```javascript // ✗ Don't store in localStorage (XSS vulnerable) localStorage.setItem('token', token); // ✗ Don't log tokens console.log(token); // ✗ Don't expose in URLs window.location = `https://example.com?token=${token}`; ``` **✅ Do:** ```javascript // ✓ Use httpOnly cookies (set by server) // Server-side: res.cookie('token', token, { httpOnly: true, secure: true, // HTTPS only sameSite: 'Strict', maxAge: 3600000 }); // Client-side cookie will be sent automatically fetch('/api/graphql', { credentials: 'include' // Send cookies }); ``` ### 4. Error Handling (Don't Expose Sensitive Data) **❌ Bad:** ```javascript catch (error) { console.error('Database error:', error); // Could expose DB details return { error: error.message }; // Expose internals to client } ``` **✅ Good:** ```javascript catch (error) { console.error('Error:', error); // Log internally return { error: 'An error occurred' }; // Generic message to client } ``` ## Error Handling ### Common GraphQL Errors ```javascript async function handleGraphQLResponse(response) { const result = await response.json(); // Check for HTTP errors if (!response.ok) { throw new Error(`HTTP Error: ${response.status}`); } // Check for GraphQL errors if (result.errors) { result.errors.forEach(error => { switch (error.extensions?.category) { case 'authentication': // Handle auth errors - refresh token refreshToken(); break; case 'validation': // Handle validation errors displayValidationErrors(error.extensions.validation); break; case 'rate_limit': // Handle rate limiting console.error('Rate limited'); break; default: console.error(error.message); } }); throw new Error(result.errors[0].message); } return result.data; } ``` ### Retry Logic with Exponential Backoff ```javascript async function fetchWithRetry(query, maxRetries = 3) { for (let i = 0; i < maxRetries; i++) { try { const response = await fetch('/api/graphql', { method: 'POST', body: JSON.stringify({ query }) }); if (response.status === 429) { // Rate limited - retry with backoff const delay = Math.pow(2, i) * 1000; await new Promise(resolve => setTimeout(resolve, delay)); continue; } if (!response.ok) { throw new Error(`HTTP ${response.status}`); } return response.json(); } catch (error) { if (i === maxRetries - 1) throw error; const delay = Math.pow(2, i) * 1000; await new Promise(resolve => setTimeout(resolve, delay)); } } } ``` ## Testing ### Unit Tests with Jest ```javascript // products.test.js import { ApolloClient, InMemoryCache, gql } from '@apollo/client'; import { SchemaLink } from '@apollo/client/link/schema'; describe('Product Queries', () => { let client; beforeEach(() => { client = new ApolloClient({ link: new SchemaLink({ schema }), cache: new InMemoryCache() }); }); test('should fetch products', async () => { const query = gql` query { products(channel: "default", first: 10) { edges { node { id name price } } } } `; const result = await client.query({ query }); expect(result.data).toBeDefined(); expect(result.data.products.edges).toHaveLength(10); expect(result.data.products.edges[0].node).toHaveProperty('id'); expect(result.data.products.edges[0].node).toHaveProperty('name'); expect(result.data.products.edges[0].node).toHaveProperty('price'); }); test('should handle authentication errors', async () => { const mutation = gql` mutation { createLogin(input: { email: "invalid@example.com" password: "wrongpassword" }) { accessToken } } `; expect(async () => { await client.mutate({ mutation }); }).rejects.toThrow(); }); }); ``` ### Integration Testing with Postman Create a Postman collection with tests: ```javascript // Test script pm.test('Response status is 200', function () { pm.response.to.have.status(200); }); pm.test('Response has no errors', function () { var jsonData = pm.response.json(); pm.expect(jsonData.errors).to.be.undefined; }); pm.test('Products are returned', function () { var jsonData = pm.response.json(); pm.expect(jsonData.data.products.edges.length).to.be.above(0); }); ``` ### E2E Testing with Cypress ```javascript // cypress/integration/checkout.spec.js describe('Checkout Flow', () => { it('should complete checkout', () => { cy.visit('/shop'); cy.contains('Add to Cart').first().click(); cy.get('[data-cy=cart-count]').should('contain', '1'); cy.visit('/cart'); cy.get('[data-cy=checkout-btn]').click(); cy.get('[data-cy=email-input]').type('test@example.com'); cy.get('[data-cy=address-input]').type('123 Main St'); cy.get('[data-cy=submit-btn]').click(); cy.url().should('include', '/order-confirmation'); }); }); ``` ### Load Testing with Artillery ```yaml # load-test.yml config: target: 'https://your-domain.com/api/graphql' phases: - duration: 60 arrivalRate: 10 name: 'Warm up' - duration: 120 arrivalRate: 50 name: 'Ramp up load' - duration: 60 arrivalRate: 100 name: 'Spike' scenarios: - name: 'Get Products' flow: - post: url: '/' json: query: 'query { products(first: 20) { edges { node { id name } } } }' capture: json: '$.data.products.edges[0].node.id' as: 'productId' - name: 'Add to Cart' flow: - post: url: '/' json: query: 'mutation { addProductsToCart(input: { cartId: "123" items: [{ productId: "{{ productId }}", quantity: 1 }] }) { cart { id } } }' ``` ## Debugging ### Enable Query Debugging ```javascript const client = new ApolloClient({ link: ApolloLink.from([ new ApolloLink((operation, forward) => { console.log('GraphQL Query:', operation.operationName); console.log('Variables:', operation.variables); return forward(operation).map(response => { console.log('Response:', response); return response; }); }), new HttpLink({ uri: 'https://your-domain.com/api/graphql' }) ]), cache: new InMemoryCache() }); ``` ### Monitor Network Traffic ```bash # Using Charles Proxy # Or use browser DevTools Network tab # GraphQL requests will show full query/mutation in request body ``` ### Logging Middleware ```php // Laravel middleware class LogGraphQLRequests { public function handle($request, Closure $next) { if ($request->path() === 'api/graphql') { Log::info('GraphQL Query', [ 'body' => $request->getContent(), 'user' => auth()->id() ]); } return $next($request); } } ``` ## Performance Monitoring ### Monitor Query Performance ```javascript async function measureQuery(query) { const startTime = performance.now(); const response = await fetch('/api/graphql', { method: 'POST', body: JSON.stringify({ query }) }); const endTime = performance.now(); const duration = endTime - startTime; console.log(`Query completed in ${duration}ms`); return response.json(); } ``` ### Use APM Tools - **New Relic** - Application Performance Monitoring - **Datadog** - Infrastructure monitoring - **Sentry** - Error tracking - **GraphQL Apollo Studio** - GraphQL-specific monitoring ## Documentation ### Maintain API Documentation 1. **Use GraphQL Directives** ```graphql directive @auth(requires: String!) on FIELD_DEFINITION type Query { customer: Customer @auth(requires: "CUSTOMER") } ``` 2. **Document Fields** ```graphql type Product { """ Unique product identifier """ id: ID! """ Product display name (localized) """ name: String! """ Product price in store currency """ price: Float! } ``` 3. **Keep Schema Updated** - Version your API - Document breaking changes - Provide migration guides --- **💡 Key Takeaways:** - Optimize queries for performance - Implement proper security measures - Handle errors gracefully - Test thoroughly before production - Monitor performance continuously - Keep documentation up-to-date **📚 Related Documentation:** - 🔐 [Authentication](/api/graphql-api/authentication) - 🛍️ [Shop API](/api/graphql-api/shop-api) - 👨‍💼 [Admin API](/api/graphql-api/admin-api) - 💻 [Integration Guides](/api/graphql-api/integrations) --- # GraphQL API URL: /api/graphql-api/graphql-api # GraphQL API Bagisto's GraphQL API delivers a modern, flexible approach to e-commerce data access. Built on Laravel Lighthouse, it provides efficient querying capabilities perfect for headless commerce, mobile apps, and modern frontend frameworks. ## 🚀 Quick Navigation Choose your next step: | Documentation | Description | |---|---| | 📖 [Introduction](/api/graphql-api/introduction) | Get started with GraphQL basics and API overview | | 🔐 [Authentication](/api/graphql-api/authentication) | Learn all authentication methods and token management | | 🛍️ [Shop API](/api/graphql-api/shop-api) | Customer-facing e-commerce operations reference | | 👨‍💼 [Admin API](/api/graphql-api/admin-api) | Administrative operations and management reference | | 🎮 [Playground Guide](/api/graphql-api/playground) | Interactive testing with sample queries | | 💻 [Integration Guides](/api/graphql-api/integrations) | Code examples for multiple programming languages | | 💡 [Best Practices](/api/graphql-api/best-practices) | Performance, security, and testing best practices | ## 🌐 Live Playground Test queries instantly without any setup: 🎮 **[GraphQL Playground](https://api-demo.bagisto.com/api/graphiql)** - Interactive query builder with schema explorer ## Key Features ✨ **Developer Friendly** - Interactive GraphiQL playground - Auto-complete and schema documentation - Copy as cURL functionality - Real-time error reporting 🚀 **High Performance** - Request only the data you need - Cursor-based pagination - Query optimization tools - Caching support 🔒 **Secure** - Multiple authentication methods - Guest checkout support - Token-based security - Rate limiting 📱 **Mobile Ready** - Optimized for low bandwidth - Small payload sizes - Perfect for native apps ## What Can You Build? - 🛒 Headless storefronts and e-commerce sites - 📱 Mobile apps (iOS & Android) - 🔄 Third-party integrations and marketplaces - 📊 Analytics dashboards - ⚡ Progressive Web Apps (PWA) - 🤖 AI-powered shopping assistants ## Quick Start ### 1. Choose Your Path **For Building Customer-Facing Apps:** - Start with [Shop API Reference](/api/graphql-api/shop-api) - Learn [Authentication Methods](/api/graphql-api/authentication) **For Admin Dashboards:** - Start with [Admin API Reference](/api/graphql-api/admin-api) - Review [Permission Requirements](/api/graphql-api/admin-api#permission--role-management) **For Your Programming Language:** - Find your language in [Integration Guides](/api/graphql-api/integrations) - Copy-paste code examples and adapt ### 2. Test in Playground - Visit [GraphQL Playground](https://api-demo.bagisto.com/api/graphiql) - Try [Sample Queries](/api/graphql-api/playground#quick-start-queries) - Explore the [Schema](/api/graphql-api/playground#schema-explorer) ### 3. Implement in Your App - Follow the [Authentication Guide](/api/graphql-api/authentication) - Use examples from [Integration Guides](/api/graphql-api/integrations) - Apply [Best Practices](/api/graphql-api/best-practices) ## Documentation Structure ### Core Documentation 1. **[Introduction](/api/graphql-api/introduction)** - GraphQL fundamentals, setup, and endpoints 2. **[Authentication](/api/graphql-api/authentication)** - All auth methods (guest, customer, admin) 3. **[Shop API](/api/graphql-api/shop-api)** - Complete Shop API with all queries and mutations ### Advanced Documentation 4. **[Admin API](/api/graphql-api/admin-api)** - Admin operations for management tasks 5. **[Playground Guide](/api/graphql-api/playground)** - Interactive testing with sample queries 6. **[Integration Guides](/api/graphql-api/integrations)** - Code examples for: - JavaScript / Node.js / React / Next.js - Python / Django - PHP / Laravel - Ruby / Rails - Go - Java ### Best Practices 7. **[Best Practices](/api/graphql-api/best-practices)** - Performance optimization, security, testing, debugging ## Common Use Cases ### Building a Headless Storefront ``` 1. Get Products → [Shop API - Products](/api/graphql-api/shop-api#products) 2. Manage Cart → [Shop API - Shopping Cart](/api/graphql-api/shop-api#shopping-cart) 3. Checkout → [Shop API - Checkout](/api/graphql-api/shop-api#checkout) 4. Learn Auth → [Authentication Guide](/api/graphql-api/authentication) ``` ### Building a Mobile App ``` 1. Learn Guest Auth → [Authentication - Guest](/api/graphql-api/authentication#1-guest-checkout-authentication) 2. Browse Products → [Shop API - Products](/api/graphql-api/shop-api#products) 3. Integrate Language → [Integration Guides](/api/graphql-api/integrations) 4. Apply Best Practices → [Best Practices](/api/graphql-api/best-practices) ``` ### Building an Admin Dashboard ``` 1. Admin Login → [Authentication - Admin](/api/graphql-api/authentication#3-admin-authentication) 2. Manage Data → [Admin API Reference](/api/graphql-api/admin-api) 3. Optimize Performance → [Best Practices - Performance](/api/graphql-api/best-practices#performance-optimization) 4. Implement Testing → [Best Practices - Testing](/api/graphql-api/best-practices#testing) ``` ### Building a Third-Party Integration ``` 1. Choose Auth Method → [Authentication Guide](/api/graphql-api/authentication) 2. Decide Shop or Admin → [Shop API](/api/graphql-api/shop-api) or [Admin API](/api/graphql-api/admin-api) 3. Select Language → [Integration Guides](/api/graphql-api/integrations) 4. Handle Errors → [Best Practices - Error Handling](/api/graphql-api/best-practices#error-handling) ``` ## API Endpoints | Endpoint | Purpose | Authentication | |----------|---------|-----------------| | `/api/graphql` | Main GraphQL API | Optional (Shop) / Required (Admin) | | `/api/graphiql` | GraphiQL Playground | None | | `/api/sandbox` | Apollo Sandbox UI | None | ## Popular Queries ### Get Products ```graphql query { products(channel: "default", first: 10) { edges { node { id name price } } } } ``` [See more Shop API queries →](/api/graphql-api/shop-api#products) ### Customer Login ```graphql mutation { createLogin(input: { email: "user@example.com" password: "password" }) { accessToken } } ``` [See more Auth examples →](/api/graphql-api/authentication#2-customer-authentication) ### Create Order ```graphql mutation { createOrder(input: { cartId: "CART_ID" billingAddressId: "ADDRESS_ID" shippingMethod: "flatrate_flatrate" paymentMethod: "paypal" }) { order { id incrementId } } } ``` [See complete checkout flow →](/api/graphql-api/shop-api#checkout) ## Getting Help | Resource | Purpose | |----------|---------| | 🎮 [Live Playground](https://api-demo.bagisto.com/api/graphiql) | Test queries instantly | | 📚 [Documentation](/api/graphql-api/introduction) | Comprehensive guides | | 💬 [Community Forum](https://forums.bagisto.com) | Ask questions | | 🐛 [Issue Tracker](https://github.com/bagisto/bagisto/issues) | Report bugs | | 📧 [Contact Support](https://bagisto.com/en/contacts/) | Enterprise support | --- **Start Building Today!** 👉 **New to GraphQL?** Start with [Introduction](/api/graphql-api/introduction) 👉 **Ready to code?** Choose your language in [Integration Guides](/api/graphql-api/integrations) 👉 **Want to test?** Visit [GraphQL Playground](https://api-demo.bagisto.com/api/graphiql) --- # Integration Guides URL: /api/graphql-api/integrations # Integration Guides Complete integration examples for popular programming languages and frameworks to get started with Bagisto GraphQL API. ## JavaScript / Node.js ### Using Fetch API ```javascript // Simple GraphQL query async function getProducts() { const query = ` query { products(channel: "default", first: 10) { edges { node { id name price description } } } } `; const response = await fetch('https://your-domain.com/api/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ query }) }); const data = await response.json(); return data.data.products; } // With authentication async function getCustomerProfile(token) { const query = ` query { customer { id firstName lastName email addresses { edges { node { id address city } } } } } `; const response = await fetch('https://your-domain.com/api/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ query }) }); const data = await response.json(); return data.data.customer; } ``` ### Using Apollo Client (React) ```bash npm install @apollo/client graphql ``` ```javascript import { ApolloClient, InMemoryCache, HttpLink, gql } from '@apollo/client'; // Initialize Apollo Client const client = new ApolloClient({ link: new HttpLink({ uri: 'https://your-domain.com/api/graphql', credentials: 'include', // Include cookies for session headers: { Authorization: `Bearer ${localStorage.getItem('token')}` } }), cache: new InMemoryCache() }); // Query products const PRODUCTS_QUERY = gql` query GetProducts($first: Int!, $channel: String!) { products(channel: $channel, first: $first) { edges { node { id name price images { edges { node { url } } } } } } } `; const { data, loading, error } = await client.query({ query: PRODUCTS_QUERY, variables: { first: 20, channel: 'default' } }); ``` ### React Component Example ```javascript import { useQuery, useMutation, gql } from '@apollo/client'; const PRODUCTS_QUERY = gql` query { products(channel: "default", first: 20) { edges { node { id name price productFlat { url } } } } } `; const ADD_TO_CART_MUTATION = gql` mutation AddToCart($cartId: String!, $productId: String!, $quantity: Int!) { addProductsToCart(input: { cartId: $cartId items: [{ productId: $productId, quantity: $quantity }] }) { cart { id itemsCount } } } `; export function ProductList() { const { data, loading, error } = useQuery(PRODUCTS_QUERY); const [addToCart] = useMutation(ADD_TO_CART_MUTATION); if (loading) return

Loading...

; if (error) return

Error: {error.message}

; return (
{data.products.edges.map(({ node: product }) => (

{product.name}

${product.price}

))}
); } ``` ### Using GraphQL Request ```bash npm install graphql-request ``` ```javascript import { GraphQLClient, gql } from 'graphql-request'; const client = new GraphQLClient('https://your-domain.com/api/graphql', { headers: { Authorization: `Bearer ${token}` } }); const query = gql` query GetProductById($id: String!) { product(id: $id) { id name price description images { edges { node { url } } } } } `; const variables = { id: '1' }; const data = await client.request(query, variables); ``` ### Next.js Integration ```typescript // lib/apolloClient.ts import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client'; const client = new ApolloClient({ ssrMode: typeof window === 'undefined', link: new HttpLink({ uri: 'https://your-domain.com/api/graphql', credentials: 'include' }), cache: new InMemoryCache() }); export default client; ``` ```typescript // pages/products.tsx import { useQuery, gql } from '@apollo/client'; const PRODUCTS_QUERY = gql`...`; export default function ProductsPage() { const { data, loading } = useQuery(PRODUCTS_QUERY); // Render component } ``` ## Python ### Using Requests + JSON ```python import requests import json API_URL = "https://your-domain.com/api/graphql" def get_products(): query = """ query { products(channel: "default", first: 10) { edges { node { id name price description } } } } """ response = requests.post( API_URL, json={'query': query} ) data = response.json() return data['data']['products'] # With authentication def get_customer_profile(token): query = """ query { customer { id firstName lastName email } } """ headers = { 'Authorization': f'Bearer {token}' } response = requests.post( API_URL, json={'query': query}, headers=headers ) data = response.json() return data['data']['customer'] # Usage if __name__ == '__main__': products = get_products() print(json.dumps(products, indent=2)) ``` ### Using Gql Library ```bash pip install gql ``` ```python from gql import Client, gql from gql.transport.requests import RequestsHTTPTransport # Create transport transport = RequestsHTTPTransport( url="https://your-domain.com/api/graphql", verify=True, retries=3, ) # Create client client = Client(transport=transport, fetch_schema_from_transport=True) # Execute query query = gql(""" query { products(channel: "default", first: 10) { edges { node { id name price } } } } """) result = client.execute(query) print(result) ``` ### Using Strawberry / GraphQL-Core ```bash pip install strawberry-graphql httpx ``` ```python import httpx from typing import Optional class BagitoGraphQLClient: def __init__(self, url: str, token: Optional[str] = None): self.url = url self.token = token async def execute(self, query: str, variables: dict = None): headers = { 'Content-Type': 'application/json', } if self.token: headers['Authorization'] = f'Bearer {self.token}' payload = { 'query': query, 'variables': variables or {} } async with httpx.AsyncClient() as client: response = await client.post( self.url, json=payload, headers=headers ) return response.json() # Usage async def main(): client = BagitoGraphQLClient('https://your-domain.com/api/graphql') query = """ query { products(channel: "default", first: 10) { edges { node { id name price } } } } """ result = await client.execute(query) print(result) ``` ### Django Integration ```python # settings.py GRAPHENE = { 'SCHEMA': 'myapp.schema.schema', 'MIDDLEWARE': [ 'graphene_django.debug.DjangoDebugMiddleware', ] } # views.py from django.http import JsonResponse import requests def sync_bagisto_products(request): query = """ query { products(channel: "default", first: 100) { edges { node { id name sku price description } } } } """ response = requests.post( 'https://your-domain.com/api/graphql', json={'query': query} ) products = response.json()['data']['products'] # Sync to Django database return JsonResponse({'status': 'synced', 'count': len(products['edges'])}) ``` ## PHP ### Using cURL ```php url = $url; $this->token = $token; } public function query($query, $variables = []) { $payload = [ 'query' => $query, 'variables' => $variables ]; $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $this->url); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload)); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $headers = ['Content-Type: application/json']; if ($this->token) { $headers[] = 'Authorization: Bearer ' . $this->token; } curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); $result = curl_exec($ch); curl_close($ch); return json_decode($result, true); } public function getProducts() { $query = ' query { products(channel: "default", first: 10) { edges { node { id name price description } } } } '; return $this->query($query); } public function getCustomerProfile() { $query = ' query { customer { id firstName lastName email } } '; return $this->query($query); } } // Usage $client = new BagitoGraphQL('https://your-domain.com/api/graphql'); $products = $client->getProducts(); print_r($products); ?> ``` ### Using Laravel HTTP Client ```php apiUrl, [ 'query' => $query ]); return $response->json()['data']['products']; } public function login($email, $password) { $mutation = " mutation { createLogin(input: { email: \"$email\" password: \"$password\" }) { accessToken customer { id email } } } "; $response = Http::post($this->apiUrl, [ 'query' => $mutation ]); return $response->json()['data']['createLogin']; } public function getCustomerOrders($token) { $query = ' query { customerOrders(first: 20) { edges { node { id incrementId status grandTotal } } } } '; $response = Http::withToken($token) ->post($this->apiUrl, ['query' => $query]); return $response->json()['data']['customerOrders']; } } // Usage in controller Route::get('/products', function () { $bagitoService = new BagitoService(); $products = $bagitoService->getProducts(); return response()->json($products); }); ?> ``` ### Using Guzzle ```php client = new Client([ 'base_uri' => $url, 'timeout' => 30.0, ]); $this->token = $token; } public function query($query, $variables = []) { $options = [ 'json' => [ 'query' => $query, 'variables' => $variables ] ]; if ($this->token) { $options['headers'] = [ 'Authorization' => 'Bearer ' . $this->token ]; } try { $response = $this->client->post('', $options); return json_decode($response->getBody(), true); } catch (Exception $e) { return ['error' => $e->getMessage()]; } } } // Usage $client = new BagitoGraphQLClient('https://your-domain.com/api/graphql'); $result = $client->query('query { products(channel: "default", first: 10) { ... } }'); ?> ``` ## Ruby ### Using Net::HTTP ```ruby require 'net/http' require 'json' require 'uri' class BagitoClient def initialize(url, token = nil) @url = url @token = token @uri = URI(@url) end def query(query_string, variables = {}) http = Net::HTTP.new(@uri.host, @uri.port) http.use_ssl = true if @uri.scheme == 'https' request = Net::HTTP::Post.new(@uri.path) request['Content-Type'] = 'application/json' request['Authorization'] = "Bearer #{@token}" if @token payload = { query: query_string, variables: variables } request.body = payload.to_json response = http.request(request) JSON.parse(response.body) end def get_products query(' query { products(channel: "default", first: 10) { edges { node { id name price description } } } } ') end def login(email, password) query(" mutation { createLogin(input: { email: \"#{email}\" password: \"#{password}\" }) { accessToken customer { id email } } } ") end end # Usage client = BagitoClient.new('https://your-domain.com/api/graphql') products = client.get_products puts products ``` ### Using GraphQL-Client Gem ```bash gem 'graphql-client' ``` ```ruby require 'graphql/client' require 'graphql/client/http' # Create HTTP adapter HTTP = GraphQL::Client::HTTP.new('https://your-domain.com/api/graphql') # Create client Schema = GraphQL::Client.load_schema(HTTP) Client = GraphQL::Client.new(schema: Schema, execute_document_directly: true) # Define query ProductsQuery = Client.parse <<-'GRAPHQL' query { products(channel: "default", first: 10) { edges { node { id name price } } } } GRAPHQL # Execute query result = Client.query(ProductsQuery) puts result.data.products ``` ### Rails Integration ```ruby # app/services/bagisto_service.rb class BagitoService API_URL = 'https://your-domain.com/api/graphql' def self.get_products(channel = 'default') query = %{ query { products(channel: "#{channel}", first: 20) { edges { node { id name sku price description } } } } } response = execute_query(query) response['data']['products'] end def self.create_order(customer_token, cart_id, address_id) mutation = %{ mutation { createOrder(input: { cartId: "#{cart_id}" billingAddressId: "#{address_id}" shippingAddressId: "#{address_id}" shippingMethod: "flatrate_flatrate" paymentMethod: "paypal" }) { order { id incrementId status grandTotal } } } } response = execute_query(mutation, customer_token) response['data']['createOrder'] end private def self.execute_query(query, token = nil) headers = { 'Content-Type' => 'application/json' } headers['Authorization'] = "Bearer #{token}" if token response = HTTParty.post(API_URL, body: { query: query }.to_json, headers: headers) response.parsed_response end end # Usage in controller class ProductsController < ApplicationController def index @products = BagitoService.get_products end end ``` ## Go ### Using GraphQL-Go ```go package main import ( "bytes" "encoding/json" "fmt" "io" "net/http" ) type GraphQLRequest struct { Query string `json:"query"` Variables map[string]interface{} `json:"variables"` } type Product struct { ID string `json:"id"` Name string `json:"name"` Price string `json:"price"` Description string `json:"description"` } type ProductsResponse struct { Data struct { Products struct { Edges []struct { Node Product `json:"node"` } `json:"edges"` } `json:"products"` } `json:"data"` } func GetProducts() ([]Product, error) { query := ` query { products(channel: "default", first: 10) { edges { node { id name price description } } } } ` req := GraphQLRequest{ Query: query, } jsonBody, _ := json.Marshal(req) body := bytes.NewReader(jsonBody) resp, err := http.Post( "https://your-domain.com/api/graphql", "application/json", body, ) if err != nil { return nil, err } defer resp.Body.Close() respBody, _ := io.ReadAll(resp.Body) var result ProductsResponse json.Unmarshal(respBody, &result) var products []Product for _, edge := range result.Data.Products.Edges { products = append(products, edge.Node) } return products, nil } func main() { products, err := GetProducts() if err != nil { fmt.Println("Error:", err) return } for _, product := range products { fmt.Printf("Product: %s - $%s\n", product.Name, product.Price) } } ``` ## Java ### Using OkHttp ```java import okhttp3.*; import com.google.gson.Gson; import com.google.gson.JsonObject; public class BagitoGraphQLClient { private final String apiUrl; private final String token; private final OkHttpClient client; public BagitoGraphQLClient(String apiUrl, String token) { this.apiUrl = apiUrl; this.token = token; this.client = new OkHttpClient(); } public String query(String graphqlQuery) throws Exception { JsonObject json = new JsonObject(); json.addProperty("query", graphqlQuery); RequestBody body = RequestBody.create( json.toString(), MediaType.parse("application/json") ); Request.Builder requestBuilder = new Request.Builder() .url(apiUrl) .post(body) .addHeader("Content-Type", "application/json"); if (token != null && !token.isEmpty()) { requestBuilder.addHeader("Authorization", "Bearer " + token); } Request request = requestBuilder.build(); Response response = client.newCall(request).execute(); return response.body().string(); } public static void main(String[] args) throws Exception { String query = "query { products(channel: \"default\", first: 10) { edges { node { id name price } } } }"; BagitoGraphQLClient client = new BagitoGraphQLClient( "https://your-domain.com/api/graphql", null ); String result = client.query(query); System.out.println(result); } } ``` ## cURL Simple command-line testing: ```bash # Get products curl -X POST https://your-domain.com/api/graphql \ -H "Content-Type: application/json" \ -d '{ "query": "query { products(channel: \"default\", first: 10) { edges { node { id name price } } } }" }' # With authentication curl -X POST https://your-domain.com/api/graphql \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_TOKEN" \ -d '{ "query": "query { customer { id firstName email } }" }' # Customer login curl -X POST https://your-domain.com/api/graphql \ -H "Content-Type: application/json" \ -d '{ "query": "mutation { createLogin(input: { email: \"user@example.com\", password: \"password\" }) { accessToken } }" }' ``` ## Best Practices Across All Languages 1. **Error Handling** ```javascript // Always check for errors in response if (response.errors) { response.errors.forEach(error => { console.error('Error:', error.message); }); } ``` 2. **Token Management** - Store tokens securely - Implement token refresh logic - Handle token expiration gracefully 3. **Caching** - Cache frequently accessed data - Implement cache invalidation strategies 4. **Rate Limiting** - Implement backoff and retry logic - Monitor rate limit headers 5. **Testing** - Use the GraphiQL playground for testing - Write unit tests for API calls --- **Ready to integrate?** Choose your language above and start building! 📚 **Related Documentation:** - 🔐 [Authentication Guide](/api/graphql-api/authentication) - 🛍️ [Shop API Reference](/api/graphql-api/shop-api) - 👨‍💼 [Admin API Reference](/api/graphql-api/admin-api) --- # GraphQL API - Introduction URL: /api/graphql-api/introduction # GraphQL API - Introduction Welcome to the Bagisto GraphQL API documentation! This guide will help you build modern, efficient e-commerce applications using our comprehensive GraphQL platform. ## What is GraphQL? GraphQL is a query language and runtime that allows clients to request exactly the data they need—nothing more, nothing less. It provides a strongly typed schema and enables developers to build flexible, efficient APIs. **Key Benefits for Bagisto:** - 🎯 **Precise Data Fetching** - Request only the fields you need - ⚡ **Reduced Bandwidth** - Smaller payloads improve performance - 📱 **Mobile Optimized** - Perfect for bandwidth-constrained environments - 🔄 **Single Endpoint** - All operations through one `/api/graphql` endpoint - 📚 **Self-Documenting** - Schema includes inline documentation - 🛠️ **Developer Friendly** - Interactive playground included ## Architecture Overview Bagisto's GraphQL API is built on **API Platform for Laravel**, a powerful framework that provides robust GraphQL support out of the box. This architecture enables a modern, type-safe API layer with minimal configuration. Bagisto's GraphQL API is built using the **Platforma API Laravel plugin** with **Bagisto's BagistoApi plugin**, providing two distinct API layers: ### 🛍️ Shop API (Frontend) The public-facing API for customer-facing operations: - Product browsing and search - Customer authentication and profile management - Shopping cart management - Checkout and order placement - Reviews and ratings - Wishlist management ### 👨‍💼 Admin API (Backend) The administrative API for management operations: - Product and category management - Customer administration - Order management and fulfillment - System configuration - Reports and analytics ## Quick Start ### Access the Playground Two ways to explore the API: **Interactive GraphQL Playground:** ``` https://your-domain.com/api/graphiql ``` ## API Endpoints | Endpoint | Purpose | Authentication | |----------|---------|-----------------| | `/api/graphql` | Main GraphQL endpoint | Optional (Shop APIs) / Required (Admin APIs) | ## Authentication Methods ### Guest Checkout Perfect for unauthenticated users: ```graphql mutation { createCartToken(input: {}) { cartToken { id cartToken } } } ``` Use the `cartToken` in the `Authorization: Bearer TOKEN` header along with the `X-STOREFRONT-KEY` . ### Customer Authentication ```graphql mutation { createCustomerLogin( input: { email: "customer@example.com" password: "password123" } ) { customerLogin { id _id apiToken token success message } } } ``` Use the `accessToken` in the `Authorization: Bearer TOKEN` header along with the `X-STOREFRONT-KEY` . ### Token Verification ```graphql mutation { createVerifyToken(input: { token: "your-token-here" }) { verifyToken { isValid message } } } ``` ## Making Your First Request ### Using cURL ```bash curl -X POST https://your-domain.com/api/graphql \ -H "Content-Type: application/json" \ -d '{ "query": "query { products(first: 10) { edges { node { id name } } } }" }' ``` ## Response Format All GraphQL responses follow a consistent format: ```json { "data": { "products": { "edges": [ { "node": { "id": "1", "name": "Product Name", "price": "99.99" } } ] } } } ``` ### Error Responses ```json { "errors": [ { "message": "Validation failed", "extensions": { "validation": { "email": ["The email field is invalid"] } } } ], "data": null } ``` ## Common Headers | Header | Required | Purpose | Example | |--------|----------|---------|---------| | `Content-Type` | Yes | Request format | `application/json` | | `X-STOREFRONT-KEY` | Yes | Storefront API key for public data access | `pk_storefront_WaZh0x0FlbKF1suY...` | | `Authorization` | Conditional | Authentication token (required for customer/admin APIs) | `Bearer 867\|DlQxl04kMnUj...` | | `X-LOCALE` | No | Locale code for localized content | `fr` | | `X-CURRENCY` | No | Currency code for pricing | `EUR` | | `X-CHANNEL` | No | Channel code for multi-channel stores | `default` | ### Context Headers (X-LOCALE, X-CURRENCY, X-CHANNEL) These optional headers let you control which locale, currency, and channel context the API uses when returning data. This is useful for building multi-language, multi-currency, or multi-channel storefronts. ```bash curl -X POST https://your-domain.com/api/graphql \ -H "Content-Type: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_WaZh0x0FlbKF1suYmDD37YTfkRKm6BJ1" \ -H "Authorization: Bearer 867|DlQxl04kMnUjSpduZpd2gaVWX8oi3vvGY3RZn4pE03404429" \ -H "X-LOCALE: fr" \ -H "X-CURRENCY: EUR" \ -H "X-CHANNEL: default" \ -d '{ "query": "query { products(first: 10) { edges { node { id name price } } } }" }' ``` > **Pre-requisite: Channel Configuration** > Before the `X-LOCALE` and `X-CURRENCY` headers will have any effect, the desired locale and currency must first be **added to the channel** in the Bagisto admin panel. Navigate to **Settings → Channels → [Your Channel]** and add the locales and currencies you want to support. Only locales and currencies that are assigned to the channel will be recognized by the API — passing a locale or currency that is not configured on the channel will cause the system to fall back to the channel's default values. **Fallback behavior:** - If a header is **not present**, the system uses the **default value** configured in your Bagisto instance (e.g., the default locale, base currency, or default channel). - If the value passed in a header **does not exist** in the system (e.g., `X-LOCALE: xx` where `xx` is not a configured locale), the system falls back to the **default value** instead of throwing an error. | Header | Fallback When Missing or Invalid | |--------|----------------------------------| | `X-LOCALE` | Uses the channel's default locale | | `X-CURRENCY` | Uses the channel's base currency | | `X-CHANNEL` | Uses the default channel | ## Pagination Bagisto uses cursor-based pagination for efficient data retrieval: ```graphql query { products(first: 20, after: "cursor-value") { pageInfo { hasNextPage hasPreviousPage startCursor endCursor } edges { cursor node { id name } } } } ``` Parameters: - `first`: Number of items to return (max: 100) - `after`: Cursor to start from - `last`: Number of items from end - `before`: Cursor to end at ## What's Next? - 📚 [Shop API Reference](/api/graphql-api/shop-api) - Complete shop operations guide - 🔑 [Admin API Reference](/api/graphql-api/admin-api) - Admin operations guide - 🔐 [Authentication Guide](/api/graphql-api/authentication) - Detailed auth methods - 🧪 [Integration Guides](/api/graphql-api/integrations) - Code examples for your stack - 💡 [Best Practices](/api/graphql-api/best-practices) - Performance and security tips ## Support & Resources - 🌐 [Interactive Playground](https://api-demo.bagisto.com/api/graphiql) - 💬 [Community Forum](https://forums.bagisto.com) - 🐛 [Issue Tracker](https://github.com/bagisto/bagisto/issues) - 📧 [Contact Support](https://bagisto.com/en/contacts/) --- --- # Cursor Pagination Guide URL: /api/graphql-api/pagination # Cursor Pagination Guide Bagisto GraphQL API uses cursor-based pagination for efficiently handling large datasets. This guide explains how to implement pagination in your applications. ## Why Cursor Pagination? Cursor-based pagination offers several advantages over offset-based pagination: - **Consistency** - Eliminates issues with data changing between requests - **Performance** - Efficient for large datasets - **Infinite Scrolling** - Perfect for mobile apps and dynamic feeds - **Predictability** - Cursors remain valid even if data is added/removed ## Pagination Parameters ### `first` Retrieves the first N items from the collection. ```graphql query { products(first: 10) { edges { node { id name } } } } ``` ### `after` Returns items after a specific cursor. Used for forward pagination (next page). ```graphql query { products(first: 10, after: "cursor_value") { edges { node { id name } } } } ``` ### `last` Retrieves the last N items from the collection. ```graphql query { products(last: 10) { edges { node { id name } } } } ``` ### `before` Returns items before a specific cursor. Used for backward pagination (previous page). ```graphql query { products(last: 10, before: "cursor_value") { edges { node { id name } } } } ``` ### `totalItems` Returns the total count of items in the collection. ```graphql query { products(first: 10) { totalItems edges { node { id name } } } } ``` ## Response Structure Paginated responses have a standard structure: ```json { "data": { "products": { "edges": [ { "node": { "id": "1", "name": "Product Name" }, "cursor": "Y3Vyc29yOjE=" } ], "pageInfo": { "hasNextPage": true, "hasPreviousPage": false, "startCursor": "Y3Vyc29yOjE=", "endCursor": "Y3Vyc29yOjEw" }, "totalItems": 150 } } } ``` ### Response Fields | Field | Type | Description | |-------|------|-------------| | `edges` | Array | Collection of items with their cursors | | `node` | Object | The actual item data | | `cursor` | String | Encoded cursor for this item | | `pageInfo` | Object | Pagination metadata | | `hasNextPage` | Boolean | Whether more items exist after current page | | `hasPreviousPage` | Boolean | Whether items exist before current page | | `startCursor` | String | Cursor of first item in current page | | `endCursor` | String | Cursor of last item in current page | | `totalItems` | Integer | Total number of items in collection | ## Common Pagination Patterns ### Forward Pagination (Next Page) Load initial page: ```graphql query { products(first: 10) { edges { node { id name } cursor } pageInfo { hasNextPage endCursor } } } ``` Load next page using the `endCursor`: ```graphql query { products(first: 10, after: "Y3Vyc29yOjEw") { edges { node { id name } cursor } pageInfo { hasNextPage endCursor } } } ``` ### Backward Pagination (Previous Page) Load previous page using `before` and `last`: ```graphql query { products(last: 10, before: "Y3Vyc29yOjEw") { edges { node { id name } cursor } pageInfo { hasPreviousPage startCursor } } } ``` ### Infinite Scrolling Implement infinite scrolling for dynamic feeds: ```javascript let hasMore = true; let endCursor = null; async function loadMore() { const query = ` query { products(first: 20, ${endCursor ? `after: "${endCursor}"` : ''}) { edges { node { id name price } cursor } pageInfo { hasNextPage endCursor } } } `; const response = await fetch('/api/graphql', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query }) }); const { data } = await response.json(); const { edges, pageInfo } = data.products; // Add items to DOM edges.forEach(({ node }) => { appendProductToUI(node); }); // Update pagination state hasMore = pageInfo.hasNextPage; endCursor = pageInfo.endCursor; } // Call loadMore() when user scrolls to bottom window.addEventListener('scroll', () => { if (window.innerHeight + window.scrollY >= document.body.offsetHeight && hasMore) { loadMore(); } }); ``` ### Search with Pagination Combine search filters with pagination: ```graphql query { products( first: 20 filter: { search: "laptop" minPrice: 500 maxPrice: 2000 } ) { edges { node { id name price } } pageInfo { hasNextPage endCursor } totalItems } } ``` ### Get Total Count Get pagination metadata including total items: ```graphql query { products(first: 1) { totalItems pageInfo { hasNextPage } } } ``` ## Pagination with Multiple Collections When working with nested collections, apply pagination at each level: ```graphql query { products(first: 10) { edges { node { id name reviews(first: 5) { edges { node { id title rating } } pageInfo { hasNextPage } } } } pageInfo { hasNextPage } } } ``` ## Best Practices ### 1. **Choose Appropriate Page Size** ```graphql # Good - reasonable page size query { products(first: 20) { ... } } # Avoid - too small, excessive requests query { products(first: 1) { ... } } # Avoid - too large, performance issues query { products(first: 1000) { ... } } ``` ### 2. **Always Check hasNextPage** ```javascript if (pageInfo.hasNextPage) { // Load more button or trigger } ``` ### 3. **Store Cursors for Navigation** ```javascript // Store cursor for next/previous navigation const nextCursor = pageInfo.endCursor; const prevCursor = pageInfo.startCursor; ``` ### 4. **Handle Empty Results** ```javascript if (edges.length === 0) { displayEmptyState(); } else { renderItems(edges); } ``` ### 5. **Implement Loading States** ```javascript let isLoading = false; async function loadMore() { if (isLoading) return; isLoading = true; try { // Fetch data } finally { isLoading = false; } } ``` ## Performance Considerations ### Selective Field Queries Request only needed fields: ```graphql # Good - specific fields query { products(first: 20) { edges { node { id name price } } } } # Avoid - unnecessary fields query { products(first: 20) { edges { node { __typename id name price description longDescription images { ... } relatedProducts { ... } variants { ... } } } } } ``` ### Caching with Pagination Implement client-side caching: ```javascript const cache = new Map(); async function getProducts(first, after) { const key = `products:${first}:${after || 'start'}`; if (cache.has(key)) { return cache.get(key); } const data = await fetchProducts(first, after); cache.set(key, data); return data; } ``` ## Error Handling Handle pagination errors gracefully: ```javascript async function getPagedProducts(cursor) { try { const response = await fetch('/api/graphql', { method: 'POST', body: JSON.stringify({ query: paginationQuery(cursor) }) }); const { data, errors } = await response.json(); if (errors) { console.error('GraphQL Error:', errors); // Handle error - reset pagination or show message return null; } return data.products; } catch (error) { console.error('Network Error:', error); // Handle network error return null; } } ``` ## Collection-Specific Pagination Different collections support pagination. Here are common examples: ### Products ```graphql query { products(first: 20) { ... } } ``` ### Categories ```graphql query { categories(first: 20) { ... } } ``` ### Orders ```graphql query { orders(first: 20) { ... } } ``` ### Reviews ```graphql query { productReviews(first: 20) { ... } } ``` ### Customers (Admin API) ```graphql query { customers(first: 20) { ... } } ``` ## Migration from Offset Pagination If migrating from offset-based pagination: ### Before (Offset) ```graphql query { products(limit: 20, offset: 0) { items { ... } total } } ``` ### After (Cursor) ```graphql query { products(first: 20) { edges { node { ... } cursor } totalItems pageInfo { hasNextPage endCursor } } } ``` ## Troubleshooting ### Cursor Expired **Issue:** `"Invalid cursor"` error **Solution:** Re-fetch from the beginning with fresh cursors ```javascript // Reset to first page endCursor = null; await loadMore(); ``` ### Inconsistent Results **Issue:** Items missing or duplicated between pages **Solution:** Use consistent sorting with pagination: ```graphql query { products(first: 20, sort: { field: "createdAt", direction: "DESC" }) { ... } } ``` ### Performance Issues **Issue:** Pagination queries are slow **Solution:** - Reduce page size - Use specific field queries - Add proper indexes on sorted fields - Implement caching --- **Related Topics:** - [Best Practices](/api/graphql-api/best-practices) - General API best practices - [Performance](/getting-started/performance) - Optimize API performance - [Authentication](/api/graphql-api/authentication) - Secure your requests --- # Interactive Playground Guide URL: /api/graphql-api/playground # Interactive Playground Guide Test and explore the Bagisto GraphQL API using our interactive playgrounds. No authentication required to explore the schema! ## GraphiQL Playground The primary interactive query editor for testing GraphQL queries and mutations. ### Access the Playground Visit one of these URLs: 🌐 **Live Demo:** ``` https://api-demo.bagisto.com/api/graphiql ``` **Local Development:** ``` https://your-local-domain.com/api/graphiql ``` ### Features ✅ **Auto-complete** - Full schema introspection with code completion ✅ **Documentation** - Built-in schema documentation explorer ✅ **Query History** - Access your previous queries ✅ **Variables** - Test with different variable values ✅ **Query Prettifier** - Auto-format your queries ✅ **Error Highlighting** - Real-time validation ### Interface Overview ``` ┌─────────────────────────────────────────────────────────────────┐ │ GraphiQL Playground │ ├──────────────────┬──────────────────────┬──────────────────────┤ │ │ │ │ │ QUERY EDITOR │ VARIABLES PANEL │ RESULT / DOCS │ │ │ │ │ │ • Write queries │ • Set variables │ • View responses │ │ • Auto-complete │ • Format JSON │ • See errors │ │ • Shortcuts │ • Validation │ • Browse schema │ │ │ │ │ └──────────────────┴──────────────────────┴──────────────────────┘ ``` ## Quick Start Queries Copy-paste these queries directly into GraphiQL: ### 1. Get Products ```graphql query GetProducts { products(channel: "default", first: 10) { pageInfo { hasNextPage endCursor } edges { node { id name sku price description } } } } ``` **Test it:** 1. Open https://api-demo.bagisto.com/api/graphiql 2. Paste the query above 3. Click **Play** button (▶) 4. See results on the right ### 2. Search Products ```graphql query SearchProducts($search: String!) { products(channel: "default", search: $search, first: 20) { edges { node { id name price productFlat { url } } } } } ``` **Variables:** ```json { "search": "laptop" } ``` ### 3. Get Product Details ```graphql query GetProduct($id: String!) { product(id: $id) { id name sku type description price weight status images { edges { node { id url alt } } } attributes { edges { node { code label value } } } reviews(first: 5) { edges { node { id rating title comment customerName } } } } } ``` **Variables:** ```json { "id": "1" } ``` ### 4. Get Categories ```graphql query GetCategories { treeCategories(parentId: 1) { id translation { name slug } children { edges { node { id translation { name slug } children { edges { node { id translation { name slug } } } } } } } } } ``` ### 5. Create Guest Cart ```graphql mutation CreateGuestCart { createCartToken(input: {}) { cartToken } } ``` **After running:** 1. Copy the `cartToken` value 2. Store for later use in cart operations ### 6. Add to Cart (Guest) ```graphql mutation AddToCart($cartId: String!, $productId: String!) { addProductsToCart(input: { cartId: $cartId items: [ { productId: $productId, quantity: 1 } ] }) { cart { id itemsCount grandTotal items { edges { node { id product { name price } quantity } } } } } } ``` **Variables:** ```json { "cartId": "replace-with-your-cart-id", "productId": "1" } ``` ### 7. Customer Login ```graphql mutation LoginCustomer($email: String!, $password: String!) { createLogin(input: { email: $email password: $password }) { accessToken customer { id firstName lastName email } } } ``` **Variables:** ```json { "email": "customer@example.com", "password": "password123" } ``` **After running:** 1. Copy the `accessToken` value 2. Add to request headers (see below) ### 8. Get Customer Profile (Requires Auth) ```graphql query GetProfile { customer { id firstName lastName email createdAt addresses { edges { node { id firstName lastName address city state country zipCode defaultBilling defaultShipping } } } } } ``` **Headers:** ```json { "Authorization": "Bearer YOUR_ACCESS_TOKEN" } ``` ### 9. Get Customer Orders ```graphql query GetOrders { customerOrders(first: 20) { edges { node { id incrementId status grandTotal createdAt items { edges { node { productName quantity price } } } } } } } ``` ### 10. Create Product Review ```graphql mutation CreateReview($productId: String!, $title: String!, $rating: Int!, $comment: String!, $name: String!, $email: String!) { createReview(input: { productId: $productId title: $title rating: $rating comment: $comment name: $name email: $email }) { review { id title rating comment status createdAt } } } ``` **Variables:** ```json { "productId": "1", "title": "Excellent Product", "rating": 5, "comment": "Great quality and fast shipping!", "name": "John Doe", "email": "john@example.com" } ``` ## Using Variables in Queries Variables make queries reusable and flexible. ### Example: Filter Products with Variables **Query:** ```graphql query GetFilteredProducts( $channel: String! $first: Int! $search: String $category: Int ) { products( channel: $channel first: $first search: $search categoryId: $category ) { edges { node { id name price } } } } ``` **Variables:** ```json { "channel": "default", "first": 20, "search": "laptop", "category": 2 } ``` ## Headers for Authentication ### Setting Headers in GraphiQL 1. Click **HTTP Headers** at the bottom 2. Add headers as JSON: ```json { "Authorization": "Bearer YOUR_ACCESS_TOKEN", "Content-Type": "application/json", "Accept-Language": "en_US" } ``` ### Common Headers | Header | Purpose | Example | |--------|---------|---------| | `Authorization` | Customer/Admin token | `Bearer eyJhbGc...` | | `X-Cart-Token` | Guest cart token | `550e8400-e29b...` | | `Content-Type` | Request format | `application/json` | | `Accept-Language` | Locale | `en_US` or `fr_FR` | ## Schema Explorer GraphiQL includes a powerful schema browser on the right sidebar. ### How to Use 1. Click **Docs** tab (top right) 2. Search for a type, query, or mutation 3. Click to view details 4. See field documentation and arguments ### Common Query Types to Explore - **Query** - Read operations - `products` - Get products - `customer` - Get customer profile - `cart` - Get cart details - **Mutation** - Write operations - `createLogin` - Customer login - `addProductsToCart` - Add to cart - `createOrder` - Create order ## Troubleshooting Common Issues ### Query Returns `null` **Problem:** ```graphql query { product(id: "999") { name } } # Returns: { "data": { "product": null } } ``` **Solution:** - Check the ID exists - Verify correct ID format - Check status/channel filters ### Authentication Error **Problem:** ```json { "errors": [ { "message": "Unauthenticated" } ] } ``` **Solution:** 1. Get valid token via `createLogin` mutation 2. Add token to headers 3. Verify token hasn't expired ### Validation Error **Problem:** ```json { "errors": [ { "extensions": { "validation": { "email": ["The email field is invalid"] } } } ] } ``` **Solution:** - Check input format - Verify all required fields provided - See error details in extensions ### Rate Limit Error **Problem:** ```json { "errors": [ { "message": "Too many requests" } ] } ``` **Solution:** - Wait before making new requests - Reduce request frequency - Implement exponential backoff ## Exporting Queries ### Copy cURL Command 1. Right-click query result 2. Select "Copy as cURL" 3. Use in terminal: ```bash curl 'https://your-domain.com/api/graphql' \ -H 'content-type: application/json' \ --data-raw '{"query":"query { ... }"}' ``` ### Export to Postman 1. Get query text 2. Create Postman request with: - **Method:** POST - **URL:** `https://your-domain.com/api/graphql` - **Body:** GraphQL query ## Performance Tips ### Write Efficient Queries **❌ Slow - Too many fields:** ```graphql query { products(first: 1000) { edges { node { id name description attributes { edges { node { id code value } } } # ... 20+ more fields } } } } ``` **✅ Fast - Only needed fields:** ```graphql query { products(first: 20) { edges { node { id name price } } } } ``` ### Use Pagination Always use pagination with `first` or `last`: ```graphql query { products(first: 20, after: "cursor") { pageInfo { hasNextPage endCursor } edges { node { id name } } } } ``` ## Keyboard Shortcuts | Shortcut | Action | |----------|--------| | `Ctrl+Enter` / `Cmd+Enter` | Execute query | | `Ctrl+Shift+P` | Prettify query | | `Ctrl+Space` | Auto-complete | | `Ctrl+F` | Find in query | ## External Resources ### Postman Collection Download the full Postman collection with pre-built requests: 📥 [Download Bagisto GraphQL Postman Collection](../../Bagisto%20Graphql%20API%20Platform.postman_collection.json) **Steps:** 1. Download collection file 2. Import into Postman 3. Set environment variables 4. Execute pre-built requests ## Real-World Examples ### Complete Checkout Flow 1. **Create Cart** ```graphql mutation { createCartToken(input: {}) { cartToken } } ``` 2. **Add Product** ```graphql mutation { addProductsToCart(input: { cartId: "CART_ID_FROM_STEP_1" items: [{ productId: "1", quantity: 2 }] }) { cart { id } } } ``` 3. **Estimate Shipping** ```graphql mutation { estimateShipping(input: { cartId: "CART_ID" country: "US" state: "CA" zipCode: "90210" }) { shippingMethods { edges { node { code title price } } } } } ``` 4. **Create Order** ```graphql mutation { createGuestOrder(input: { cartId: "CART_ID" billingAddress: { firstName: "John" lastName: "Doe" email: "john@example.com" address: "123 Main St" city: "New York" country: "US" state: "NY" zipCode: "10001" } shippingMethod: "flatrate_flatrate" paymentMethod: "paypal" }) { order { id incrementId } } } ``` --- **Get Started:** 1. 🌐 Open [GraphiQL Playground](https://api-demo.bagisto.com/api/graphiql) 2. 📋 Try one of the queries above 3. 💡 Explore the schema using Docs 4. 🚀 Build your application! **Need Help?** - 📚 [Authentication Guide](/api/graphql-api/authentication) - 🛍️ [Shop API Reference](/api/graphql-api/shop-api) - 💻 [Integration Guides](/api/graphql-api/integrations) - 💬 [Community Forum](https://forums.bagisto.com) --- # GraphQL API Quick Reference URL: /api/graphql-api/quick-reference # GraphQL API Quick Reference ## Shop API Quick Links ### Products - **List Products** - `GET /graphql` with `query GetProducts` - **Search Products** - Filter with search parameter - **Get Product Details** - By ID, SKU, or URL - [Full Products Documentation](/api/graphql-api/shop/products) ### Cart Operations - **Create Cart** - Guest or authenticated - **Add to Cart** - With variant selection - **Update Quantity** - Modify cart items - **Apply Coupon** - Discount codes - [Full Cart Documentation](/api/graphql-api/shop/cart) ### Customers - **Register** - New customer account - **Login** - Authenticate with email/password - **Manage Profile** - Update customer info - **Manage Addresses** - Add/update shipping addresses - [Full Customer Documentation](/api/graphql-api/shop/customers) ### Checkout Flow 1. Add items to cart 2. Save billing address 3. Estimate shipping 4. Select shipping method 5. Choose payment method 6. Create order - [Full Checkout Documentation](/api/graphql-api/shop/checkout) ### Orders & Tracking - **Get My Orders** - List customer orders - **Order Details** - Complete order info - **Track Shipment** - Real-time tracking - [Full Orders Documentation](/api/graphql-api/shop/orders) ### Reviews - **Get Reviews** - List product reviews - **Submit Review** - Create review with rating - [Full Reviews Documentation](/api/graphql-api/shop/reviews) --- ## Admin API Quick Links ### Product Management - **Create Product** - New simple or configurable products - **Update Product** - Modify product details - **Delete Product** - Remove products - **Bulk Operations** - Update multiple products - **Manage Images** - Add/remove product images - [Full Products Documentation](/api/graphql-api/admin/products) ### Order Management - **List Orders** - With filters and pagination - **Order Details** - Full order information - **Create Shipment** - Ship order items - **Create Invoice** - Generate invoices - **Create Refund** - Process refunds - **Add Comments** - Track order notes - [Full Orders Documentation](/api/graphql-api/admin/orders) ### Inventory Management - **Update Stock** - Warehouse inventory levels - **Adjust Inventory** - Add/subtract stock - **Transfer Stock** - Move between warehouses - **Warehouse Management** - Create/update warehouses - **Inventory History** - Track changes - [Full Inventory Documentation](/api/graphql-api/admin/inventory) ### Promotions & Discounts - **Create Coupons** - Discount codes - **Cart Rules** - Condition-based discounts - **Assign Products** - Coupon product scope - **Track Usage** - Coupon analytics - [Full Promotions Documentation](/api/graphql-api/admin/promotions) ### Attributes & Categories - **Create Attributes** - Product attributes with options - **Manage Attributes** - Update and delete - **Attribute Sets** - Group related attributes - **Category Management** - Create/update categories - **Assign Products** - Link to categories - [Full Attributes Documentation](/api/graphql-api/admin/attributes) - [Full Categories Documentation](/api/graphql-api/admin/categories) ### Analytics & Reports - **Sales Reports** - Revenue, orders, refunds - **Product Reports** - Units sold, revenue - **Customer Reports** - Customer metrics - **Inventory Reports** - Stock analysis - **Export Data** - CSV/JSON exports - **Scheduled Reports** - Automated delivery - [Full Reports Documentation](/api/graphql-api/admin/reports) --- ## Authentication Methods ### Guest Token (Shop API) ```graphql mutation { generateGuestToken { token } } ``` ### Customer Login (Shop API) ```graphql mutation { customerLogin(email: "user@example.com", password: "password") { accessToken customer { id email } } } ``` ### Admin Authentication (Admin API) Admin requests use a pre-issued **Integration token** — there is no login mutation. Generate it from the **Integration** menu, then send `Authorization: Bearer |` on every request to `POST /api/admin/graphql`. See [Authentication](/api/graphql-api/admin/authentication). --- ## Common Query Patterns ### Paginated Results ```graphql query { products(first: 20, after: "cursor") { pageInfo { hasNextPage endCursor } edges { node { id name } } } } ``` ### Filtered Results ```graphql query { products( filters: { status: "active" priceFrom: 10 priceTo: 100 } ) { edges { node { id } } } } ``` ### Nested Relations ```graphql query { product(id: "1") { id name images { edges { node { url } } } attributes { edges { node { code value } } } } } ``` --- ## Response Status Codes | Code | Meaning | |------|---------| | 200 | Successful query/mutation | | 400 | Bad request - invalid syntax | | 401 | Unauthorized - invalid/missing token | | 403 | Forbidden - insufficient permissions | | 404 | Not found - resource doesn't exist | | 429 | Rate limited - too many requests | | 500 | Server error | --- ## Error Handling ### Sample Error Response ```json { "errors": [ { "message": "Product not found", "extensions": { "code": "PRODUCT_NOT_FOUND", "category": "NOT_FOUND" } } ] } ``` ### Common Error Codes - `PRODUCT_NOT_FOUND` - Product doesn't exist - `INVALID_INPUT` - Invalid query parameters - `AUTHENTICATION_ERROR` - Invalid credentials - `AUTHORIZATION_ERROR` - Insufficient permissions - `RATE_LIMIT_EXCEEDED` - Too many requests --- ## Useful Resources - [GraphQL Playground Guide](/api/graphql-api/playground) - Interactive testing - [Integration Guides](/api/graphql-api/integrations) - Language examples - [Best Practices](/api/graphql-api/best-practices) - Production tips - [Full API Overview](/api/graphql-api/index) - All resources --- **Need Help?** - Check [Best Practices](/api/graphql-api/best-practices) for common issues - Review [Integration Guides](/api/graphql-api/integrations) for code examples - Visit [Bagisto Forums](https://forums.bagisto.com/) for community support --- # Bagisto GraphQL API Documentation URL: /api/graphql-api/README # Bagisto GraphQL API Documentation Complete, developer-friendly GraphQL API documentation for Bagisto e-commerce platform with Shopify-style organization and interactive examples. ## 📚 Documentation Overview This comprehensive documentation covers all Bagisto GraphQL API endpoints for both customer-facing (Shop) and administrative (Admin) operations. ### 🌟 Key Highlights - **95+ GraphQL Operations** - Complete coverage of Shop and Admin APIs - **Shopify-Style Organization** - Intuitive dropdown menus by resource type - **7+ Language Integration Guides** - JavaScript, Python, PHP, Ruby, Go, Java examples - **Interactive Playground** - Test queries and mutations in real-time - **Production-Ready** - Best practices, security, and performance guidance - **150+ KB of Content** - 25+ comprehensive documentation pages ## � Documentation Structure ### Core Guides - **[index.md](/api/graphql-api/)** - Complete API overview and resource index - **[introduction.md](/api/graphql-api/introduction)** - GraphQL fundamentals and setup - **[authentication.md](/api/graphql-api/authentication)** - All authentication methods - **[playground.md](/api/graphql-api/playground)** - Interactive testing guide - **[integrations.md](/api/graphql-api/integrations)** - 40+ code examples (7 languages) - **[best-practices.md](/api/graphql-api/best-practices)** - Production deployment guide - **[quick-reference.md](/api/graphql-api/quick-reference)** - Fast lookup guide ### Shop API Resources (7 Resource Pages) | Resource | File | Endpoints | Description | |----------|------|-----------|-------------| | **Products** | `/shop/products.md` | 5+ | Browse, search, filter, sort products | | **Categories** | `/shop/categories.md` | 2+ | Category navigation and product lists | | **Cart** | `/shop/cart.md` | 8+ | Shopping cart management | | **Customers** | `/shop/customers.md` | 10+ | Authentication and profile management | | **Orders** | `/shop/orders.md` | 3+ | Order retrieval and tracking | | **Checkout** | `/shop/checkout.md` | 7+ | Complete checkout workflow | | **Reviews** | `/shop/reviews.md` | 3+ | Product reviews and ratings | ### Admin API Resources (8 Resource Pages) | Resource | File | Endpoints | Description | |----------|------|-----------|-------------| | **Products** | `/admin/products.md` | 8+ | Complete product management with bulk operations | | **Categories** | `/admin/categories.md` | 7+ | Category hierarchy and product assignment | | **Orders** | `/admin/orders.md` | 8+ | Order fulfillment, shipments, invoices, refunds | | **Customers** | `/admin/customers.md` | 10+ | Customer administration and management | | **Inventory** | `/admin/inventory.md` | 9+ | Stock management across warehouses | | **Promotions** | `/admin/promotions.md` | 9+ | Coupons, discounts, and cart rules | | **Attributes** | `/admin/attributes.md` | 10+ | Product attributes and attribute sets | | **Reports** | `/admin/reports.md` | 11+ | Sales, product, customer, and inventory analytics | ### Supporting Files - **[shop-api.md](/api/graphql-api/shop-api)** - Shop API overview (legacy link) - **[admin-api.md](/api/graphql-api/admin-api)** - Admin API overview (legacy link) ## 🚀 Quick Start ### 1. **Start Here** - [API Overview](/api/graphql-api/) - Complete resource index - [Introduction](/api/graphql-api/introduction) - GraphQL basics ### 2. **Choose Your Path** **Building a Customer App/Storefront?** - Start: [Shop Products](/api/graphql-api/shop/products) - Then: [Shop Cart](/api/graphql-api/shop/cart) - Then: [Shop Checkout](/api/graphql-api/shop/checkout) **Building Admin Tools?** - Start: [Admin Products](/api/graphql-api/admin/products) - Then: [Admin Orders](/api/graphql-api/admin/orders) - Then: [Admin Reports](/api/graphql-api/admin/reports) **Integrating with External System?** - Setup: [Authentication](/api/graphql-api/authentication) - Code: [Integration Guides](/api/graphql-api/integrations) - Optimize: [Best Practices](/api/graphql-api/best-practices) ### 3. **Test Your First Query** - Open [Interactive Playground](/api/graphql-api/playground) - Copy a sample query from any resource page - Execute and see results instantly ## 📊 API Coverage ### Shop API - 43 Total Operations - **Products** (5 operations) - Browse, search, filter, sort products - **Categories** (2 operations) - Navigate category hierarchies - **Cart** (8 operations) - Shopping cart and coupon management - **Customers** (10 operations) - Authentication and profiles - **Orders** (3 operations) - Order retrieval and tracking - **Checkout** (7 operations) - Billing, shipping, payment, order creation - **Reviews** (3 operations) - Product reviews and ratings ### Admin API - 52 Total Operations - **Products** (8 operations) - Complete product management - **Categories** (7 operations) - Category management - **Orders** (8 operations) - Fulfillment, shipments, invoices, refunds - **Customers** (10 operations) - Customer administration - **Inventory** (9 operations) - Stock management, warehouses - **Promotions** (9 operations) - Coupons, discounts, cart rules - **Attributes** (10 operations) - Product attributes and sets - **Reports** (11 operations) - Analytics and data export **Total: 95+ GraphQL Operations Documented** ## 🎯 Features at a Glance ✅ **95+ Documented Operations** - Complete Shop and Admin API coverage ✅ **Interactive Playground** - Test queries live ✅ **7 Language Integrations** - JavaScript, Python, PHP, Ruby, Go, Java ✅ **40+ Code Examples** - Copy-paste ready code ✅ **Best Practices Guide** - Production deployment tips ✅ **Shopify-Style Navigation** - Intuitive dropdown menus ✅ **Authentication Guide** - All auth methods explained ✅ **Error Handling** - Common errors and solutions ✅ **Performance Tips** - Optimization strategies ✅ **Security Guide** - Production security best practices ## � Documentation Files at a Glance | File | Purpose | Coverage | |------|---------|----------| | `index.md` | API Overview & Resource Index | All resources with descriptions | | `introduction.md` | GraphQL Fundamentals | Setup, concepts, basics | | `authentication.md` | Auth Methods | Guest, Customer, Admin auth | | `playground.md` | Interactive Testing | GraphQL Playground guide | | `integrations.md` | Code Examples | 7 languages, 40+ examples | | `best-practices.md` | Production Guide | Performance, security, testing | | `quick-reference.md` | Quick Lookup | Common patterns, error codes | | `shop/*.md` | Shop API Resources | 7 resource pages | | `admin/*.md` | Admin API Resources | 8 resource pages | ## 🔗 Direct Links to All Resources ### Shop API - [Products](/api/graphql-api/shop/products) - Browse and search - [Categories](/api/graphql-api/shop/categories) - Navigation - [Cart](/api/graphql-api/shop/cart) - Shopping management - [Customers](/api/graphql-api/shop/customers) - Accounts - [Orders](/api/graphql-api/shop/orders) - Tracking - [Checkout](/api/graphql-api/shop/checkout) - Purchase flow - [Reviews](/api/graphql-api/shop/reviews) - Feedback ### Admin API - [Products](/api/graphql-api/admin/products) - Management - [Categories](/api/graphql-api/admin/categories) - Organization - [Orders](/api/graphql-api/admin/orders) - Fulfillment - [Customers](/api/graphql-api/admin/customers) - Administration - [Inventory](/api/graphql-api/admin/inventory) - Stock - [Promotions](/api/graphql-api/admin/promotions) - Discounts - [Attributes](/api/graphql-api/admin/attributes) - Configuration - [Reports](/api/graphql-api/admin/reports) - Analytics ### Guides - [Authentication](/api/graphql-api/authentication) - All auth methods - [Playground](/api/graphql-api/playground) - Interactive testing - [Integrations](/api/graphql-api/integrations) - Language examples - [Best Practices](/api/graphql-api/best-practices) - Production guide - [Quick Reference](/api/graphql-api/quick-reference) - Quick lookup ## 🎓 How to Use This Documentation ### For Beginners 1. Read [Introduction](/api/graphql-api/introduction) - Understand GraphQL basics 2. Setup [Authentication](/api/graphql-api/authentication) - Learn auth methods 3. Try [Playground](/api/graphql-api/playground) - Test queries interactively 4. Check [Quick Reference](/api/graphql-api/quick-reference) - Common patterns ### For Developers Building Apps 1. Choose your [Language](/api/graphql-api/integrations) - Find code examples 2. Review [Shop API](/api/graphql-api/shop/products) - Browse resources 3. Test in [Playground](/api/graphql-api/playground) - Validate queries 4. Implement & [Optimize](/api/graphql-api/best-practices) - Production readiness ### For Admin/Management Systems 1. Review [Authentication](/api/graphql-api/authentication) - Admin auth 2. Check [Admin API](/api/graphql-api/admin/products) - Management tools 3. Study [Reports](/api/graphql-api/admin/reports) - Analytics 4. Apply [Best Practices](/api/graphql-api/best-practices) - Optimization ### For Integrations 1. Setup [Authentication](/api/graphql-api/authentication) - Credentials 2. Find [Integrations](/api/graphql-api/integrations) - Your language 3. Choose [Resources](/api/graphql-api/) - What you need 4. Implement & [Test](/api/graphql-api/best-practices#testing-strategies) - QA ## � Use Cases & Resources ### E-Commerce Storefront **Need:** Product catalog, shopping cart, checkout, customer accounts - [Shop Products](/api/graphql-api/shop/products) - [Shop Cart](/api/graphql-api/shop/cart) - [Shop Checkout](/api/graphql-api/shop/checkout) - [Shop Customers](/api/graphql-api/shop/customers) ### Mobile App **Need:** Product browsing, categories, cart, checkout, orders - [Shop Products](/api/graphql-api/shop/products) - [Shop Categories](/api/graphql-api/shop/categories) - [Shop Cart](/api/graphql-api/shop/cart) - [Shop Orders](/api/graphql-api/shop/orders) ### Admin Dashboard **Need:** Product management, order fulfillment, customer admin, analytics - [Admin Products](/api/graphql-api/admin/products) - [Admin Orders](/api/graphql-api/admin/orders) - [Admin Customers](/api/graphql-api/admin/customers) - [Admin Reports](/api/graphql-api/admin/reports) ### Inventory System **Need:** Stock management, warehouse operations, tracking - [Admin Inventory](/api/graphql-api/admin/inventory) - [Admin Products](/api/graphql-api/admin/products) - [Admin Reports](/api/graphql-api/admin/reports) ### Promotions Engine **Need:** Coupons, discounts, cart rules, promotion management - [Admin Promotions](/api/graphql-api/admin/promotions) - [Shop Cart](/api/graphql-api/shop/cart) - [Admin Reports](/api/graphql-api/admin/reports) - Community forum for discussions - Issue tracker for bug reports --- **Total Documentation Pages:** 7 **Total Code Examples:** 40+ **Supported Languages:** 7 **API Endpoints Documented:** 90+ **Best Practices:** 25+ --- # Shop API Reference URL: /api/graphql-api/shop-api # Shop API Reference The Bagisto Shop API provides all necessary endpoints for customer-facing e-commerce operations including product browsing, shopping cart management, and checkout functionality. ## Overview The Shop API is designed for: - **Headless Commerce**: Frontend frameworks (React, Vue, Angular, Next.js) - **Mobile Applications**: iOS and Android apps - **Third-party Integrations**: External marketplace apps - **Progressive Web Apps (PWA)**: Mobile-web hybrids ## Categories Organize and browse products by category. ### Get Category Tree Retrieve the hierarchical category structure. ```graphql query { treeCategories(parentId: 1) { id position status displayMode logoUrl bannerUrl translation { name slug urlPath metaTitle metaDescription metaKeywords } children { edges { node { id position status translation { name slug } children { edges { node { id translation { name slug } } } } } } } } } ``` **Parameters:** - `parentId`: Parent category ID (default: 1 for root) **Response Example:** ```json { "data": { "treeCategories": { "id": "1", "position": 0, "status": true, "translation": { "name": "Root", "slug": "root", "urlPath": "" }, "children": { "edges": [ { "node": { "id": "2", "position": 1, "translation": { "name": "Electronics", "slug": "electronics" }, "children": { "edges": [ { "node": { "id": "3", "translation": { "name": "Smartphones", "slug": "smartphones" } } } ] } } } ] } } } } ``` ### Get Products by Category ```graphql query { products( channel: "default" categoryId: 2 first: 20 filters: { status: [1] } ) { pageInfo { hasNextPage hasPreviousPage startCursor endCursor } edges { cursor node { id name sku description shortDescription price productFlat { id url new featured status } } } } } ``` ## Products Browse, search, and filter products with advanced capabilities. ### Get All Products ```graphql query { products( channel: "default" first: 50 after: "cursor" ) { pageInfo { hasNextPage endCursor } edges { node { id name sku type shortDescription description price cost status weight images { edges { node { id type path url } } } videos { edges { node { id type path url } } } attributes { edges { node { id code label value } } } productFlat { id url metaTitle metaDescription metaKeywords new featured status } } } } } ``` ### Get Product by ID ```graphql query { product(id: "1") { id name sku type description shortDescription price cost weight status created_at updated_at images { edges { node { id url alt type } } } attributes { edges { node { id code label value } } } reviews { pageInfo { hasNextPage } edges { node { id title rating comment customerName createdAt } } } } } ``` ### Get Product by SKU ```graphql query { productBySkU(sku: "PROD-001") { id name sku description price availability status } } ``` ### Get Product by URL Key ```graphql query { productByUrl(url: "awesome-product-name") { id name sku description price productFlat { url metaTitle metaDescription } } } ``` ### Search Products ```graphql query { products( channel: "default" search: "laptop" first: 20 ) { edges { node { id name sku price description } } } } ``` ### Filter & Sort Products ```graphql query { products( channel: "default" first: 20 sort: "name" order: "DESC" filters: { price: { from: 100, to: 500 } status: [1] brand: ["Apple", "Samsung"] rating: { from: 4 } } ) { edges { node { id name price rating { rate count } } } } } ``` **Available Sorting Options:** - `name`: Product name A-Z - `price`: Price ascending - `rating`: Customer ratings - `newest`: Recently added - `popularity`: Most viewed **Available Filters:** - `search`: Text search - `price`: Price range - `status`: Product status - `brand`: Brand filter - `rating`: Star rating ### Get Configurable Products For products with multiple variants (e.g., size, color). ```graphql query { product(id: "1") { id name type variants { edges { node { id sku price weight images { edges { node { url } } } attributes { edges { node { code value } } } } } } superAttributes { edges { node { id code label options { edges { node { id label value } } } } } } } } ``` ### Get Product Attributes ```graphql query { attribute(code: "color") { id code label type options { edges { node { id label value swatchValue } } } } } ``` ## Shopping Cart Manage shopping cart operations for both guest and authenticated customers. ### Create Cart **For Guest Users:** ```graphql mutation { createCartToken(input: {}) { cartToken } } ``` **For Authenticated Customers:** ```graphql mutation { createCart(input: {}) { cart { id itemsCount itemsQty grandTotal } } } ``` ### Add Product to Cart ```graphql mutation { addProductsToCart(input: { cartId: "your-cart-id" items: [ { productId: "1" quantity: 2 superAttributeSelection: { color: "red" size: "L" } } ] }) { cart { id itemsCount items { edges { node { id productId quantity product { id name price } } } } } } } ``` ### Update Cart Item ```graphql mutation { updateCart(input: { cartId: "your-cart-id" items: [ { cartItemId: "1" quantity: 5 } ] }) { cart { id itemsCount items { edges { node { id quantity } } } } } } ``` ### Remove Item from Cart ```graphql mutation { removeCartItem(input: { cartId: "your-cart-id" cartItemId: "1" }) { cart { id itemsCount items { edges { node { id } } } } } } ``` ### Get Cart ```graphql query { cart(id: "your-cart-id") { id itemsCount itemsQty grandTotal subtotal taxTotal shippingTotal discountAmount couponCode items { edges { node { id productId quantity basePrice totalPrice product { id name sku images { edges { node { url } } } } } } } address { firstName lastName address city state country zipCode } } } ``` ### Apply Coupon ```graphql mutation { applyCoupon(input: { cartId: "your-cart-id" couponCode: "SUMMER2024" }) { cart { id couponCode discountAmount grandTotal } } } ``` ### Remove Coupon ```graphql mutation { removeCoupon(input: { cartId: "your-cart-id" }) { cart { id couponCode discountAmount grandTotal } } } ``` ### Estimate Shipping ```graphql mutation { estimateShipping(input: { cartId: "your-cart-id" country: "US" state: "CA" zipCode: "90210" }) { shippingMethods { edges { node { id code title description price basePrice } } } } } ``` ### Move to Wishlist ```graphql mutation { moveToWishlist(input: { cartId: "your-cart-id" cartItemId: "1" }) { status message } } ``` ## Checkout Complete the checkout process and create orders. ### Save Address ```graphql mutation { saveCustomerAddress(input: { firstName: "John" lastName: "Doe" email: "john@example.com" address: "123 Main Street" city: "New York" country: "US" state: "NY" zipCode: "10001" phoneNumber: "2125551234" defaultAddress: true }) { address { id firstName lastName address city state country zipCode } } } ``` ### Set Shipping Method ```graphql mutation { saveShippingMethod(input: { cartId: "your-cart-id" shippingMethodCode: "flatrate_flatrate" }) { cart { id shippingMethodCode shippingTotal grandTotal } } } ``` ### Get Payment Methods ```graphql query { paymentMethods(cartId: "your-cart-id") { edges { node { code title description image sortOrder } } } } ``` ### Save Payment Method ```graphql mutation { savePaymentMethod(input: { cartId: "your-cart-id" method: "paypal" }) { status message } } ``` ### Create Order **For Authenticated Customers:** ```graphql mutation { createOrder(input: { cartId: "your-cart-id" billingAddressId: "1" shippingAddressId: "1" shippingMethod: "flatrate_flatrate" paymentMethod: "paypal" }) { order { id incrementId status grandTotal shippingTotal taxTotal discountAmount createdAt items { edges { node { id productId quantity price } } } } } } ``` **For Guest Users:** ```graphql mutation { createGuestOrder(input: { cartId: "your-cart-id" billingAddress: { firstName: "John" lastName: "Doe" email: "john@example.com" address: "123 Main Street" city: "New York" country: "US" state: "NY" zipCode: "10001" phoneNumber: "2125551234" } shippingMethod: "flatrate_flatrate" paymentMethod: "paypal" }) { order { id incrementId status grandTotal createdAt } } } ``` ## Customers Manage customer profiles and accounts. ### Customer Registration ```graphql mutation { createCustomer(input: { firstName: "John" lastName: "Doe" email: "john@example.com" password: "SecurePassword123!" passwordConfirmation: "SecurePassword123!" }) { customer { id firstName lastName email createdAt } } } ``` ### Customer Login ```graphql mutation { createLogin(input: { email: "john@example.com" password: "SecurePassword123!" }) { accessToken customer { id firstName lastName email } } } ``` ### Get Customer Profile ```graphql query { customer { id firstName lastName email gender dateOfBirth createdAt addresses { edges { node { id firstName lastName address city state country zipCode defaultBilling defaultShipping } } } } } ``` ### Update Customer Profile ```graphql mutation { updateCustomer(input: { firstName: "Jane" lastName: "Smith" email: "jane@example.com" gender: "Female" dateOfBirth: "1990-05-15" }) { customer { id firstName lastName email } } } ``` ### Change Password ```graphql mutation { changeCustomerPassword(input: { oldPassword: "OldPassword123!" newPassword: "NewPassword456!" passwordConfirmation: "NewPassword456!" }) { status message } } ``` ### Add Customer Address ```graphql mutation { addCustomerAddress(input: { firstName: "John" lastName: "Doe" address: "456 Oak Avenue" city: "Los Angeles" state: "CA" country: "US" zipCode: "90001" phoneNumber: "2105551234" defaultBilling: false defaultShipping: true }) { address { id firstName address city defaultShipping } } } ``` ### Update Customer Address ```graphql mutation { updateCustomerAddress(input: { addressId: "1" firstName: "Jane" lastName: "Smith" address: "789 Pine Road" city: "San Francisco" state: "CA" country: "US" zipCode: "94102" }) { address { id firstName address city } } } ``` ### Delete Customer Address ```graphql mutation { deleteCustomerAddress(input: { addressId: "1" }) { status message } } ``` ### Forgot Password ```graphql mutation { forgotPassword(input: { email: "john@example.com" }) { status message } } ``` ### Reset Password ```graphql mutation { resetPassword(input: { token: "reset-token-from-email" email: "john@example.com" password: "NewPassword123!" passwordConfirmation: "NewPassword123!" }) { status message } } ``` ### Customer Logout ```graphql mutation { createLogout(input: {}) { status message } } ``` ## Orders Retrieve order information and history. ### Get Customer Orders ```graphql query { customerOrders(first: 20) { pageInfo { hasNextPage endCursor } edges { node { id incrementId status grandTotal itemsCount createdAt shippingMethod paymentMethod items { edges { node { id productId quantity price productName } } } } } } } ``` ### Get Order Details ```graphql query { order(id: "1") { id incrementId status grandTotal subtotal taxTotal shippingTotal discountAmount couponCode createdAt billingAddress { firstName lastName address city state country zipCode } shippingAddress { firstName lastName address city state country zipCode } items { edges { node { id productId productName sku quantity price totalPrice } } } shipments { edges { node { id status carrierTitle trackNumber createdAt items { edges { node { id quantity } } } } } } } } ``` ## Reviews Browse and manage product reviews. ### Get Product Reviews ```graphql query { reviews(productId: "1", first: 20) { pageInfo { hasNextPage endCursor } edges { node { id title rating comment customerName email status createdAt productId } } } } ``` ### Create Product Review ```graphql mutation { createReview(input: { productId: "1" title: "Great Product!" rating: 5 comment: "Excellent quality and fast shipping." name: "John Doe" email: "john@example.com" }) { review { id title rating comment status createdAt } } } ``` ## Wishlist Manage customer wishlists. ### Add to Wishlist ```graphql mutation { addToWishlist(input: { productId: "1" }) { status message wishlist { id items { edges { node { id productId product { id name price } } } } } } } ``` ### Get Wishlist ```graphql query { wishlist { id items { edges { node { id productId addedAt product { id name sku price images { edges { node { url } } } } } } } } } ``` ### Remove from Wishlist ```graphql mutation { removeFromWishlist(input: { wishlistItemId: "1" }) { status message } } ``` ## Utilities ### Get Countries and States ```graphql query { countries { edges { node { id code name states { edges { node { id code defaultName } } } } } } } ``` ### Get Default Channel ```graphql query { channel(code: "default") { id code name currencyCode localeCode rootCategoryId } } ``` ### Get Available Locales ```graphql query { locales { edges { node { id code name } } } } ``` --- **💡 Pro Tips:** - Use pagination for large result sets - Cache category trees for faster navigation - Implement optimistic UI updates for cart changes - Load high-resolution product images only when needed **📚 Related Documentation:** - 🔐 [Authentication](/api/graphql-api/authentication) - 💻 [Integration Guides](/api/graphql-api/integrations) - 🎯 [Best Practices](/api/graphql-api/best-practices) --- # Shop API - Attribute Options URL: /api/graphql-api/shop/attribute-options # Shop API - Attribute Options Retrieve attribute options for products in your Bagisto store, including color swatches, size options, and other product variants. ## Overview Attribute options represent the values available for product attributes (e.g., "Red", "Large", "Cotton"). The Attribute Options API allows you to fetch these values with support for multi-language translations and swatch information. ## Base Resource Object ```json { "id": "/api/shop/attribute-options/1", "_id": 1, "adminName": "Red", "sortOrder": 0, "swatchValue": "#e10e0e", "swatchValueUrl": null, "translation": { "id": "/api/attribute_option_translations/1", "locale": "en", "label": "Red" } } ``` ## Get Attribute Options Retrieve all options for a specific attribute with pagination support. ### Query ```graphql query GetAttributeOptions( $attributeId: Int! $first: Int $after: String ) { attributeOptions( attributeId: $attributeId first: $first after: $after ) { edges { node { id _id adminName sortOrder swatchValue swatchValueUrl translation { locale label } } cursor } pageInfo { hasNextPage endCursor hasPreviousPage startCursor } } } ``` ### Variables ```json { "attributeId": 23, "first": 10 } ``` ### Response ```json { "data": { "attributeOptions": { "edges": [ { "node": { "id": "/api/shop/attribute-options/1", "_id": 1, "adminName": "Red", "sortOrder": 0, "swatchValue": "#e10e0e", "swatchValueUrl": null, "translation": { "locale": "en", "label": "Red" } }, "cursor": "MA==" }, { "node": { "id": "/api/shop/attribute-options/2", "_id": 2, "adminName": "Green", "sortOrder": 1, "swatchValue": "#155616", "swatchValueUrl": null, "translation": { "locale": "en", "label": "Green" } }, "cursor": "MQ==" } ], "pageInfo": { "hasNextPage": false, "endCursor": "MQ==", "hasPreviousPage": false, "startCursor": "MA==" } } } } ``` ## Get Attribute Options with Translations Retrieve attribute options with all available translations. ### Query ```graphql query GetAttributeOptionsWithTranslations( $attributeId: Int! $first: Int ) { attributeOptions( attributeId: $attributeId first: $first ) { edges { node { id adminName sortOrder swatchValue translations(first: 10) { edges { node { id locale label } } pageInfo { hasNextPage } } } } } } ``` ### Variables ```json { "attributeId": 23, "first": 5 } ``` ### Response ```json { "data": { "attributeOptions": { "edges": [ { "node": { "id": "/api/shop/attribute-options/1", "adminName": "Red", "sortOrder": 0, "swatchValue": "#e10e0e", "translations": { "edges": [ { "node": { "id": "/api/attribute_option_translations/1", "locale": "en", "label": "Red" } }, { "node": { "id": "/api/attribute_option_translations/84", "locale": "ar", "label": "أحمر" } }, { "node": { "id": "/api/attribute_option_translations/167", "locale": "fr", "label": "Rouge" } } ], "pageInfo": { "hasNextPage": false } } } } ] } } } ``` ## Get Attribute Options with Swatch Images Retrieve color or image swatches for attribute options. ### Query ```graphql query GetSwatchOptions( $attributeId: Int! $first: Int ) { attributeOptions( attributeId: $attributeId first: $first ) { edges { node { id adminName swatchValue swatchValueUrl translation { locale label } } } } } ``` ### Variables ```json { "attributeId": 24, "first": 20 } ``` ### Response ```json { "data": { "attributeOptions": { "edges": [ { "node": { "id": "/api/shop/attribute-options/10", "adminName": "Pattern1", "swatchValue": null, "swatchValueUrl": "https://api-demo.bagisto.com/storage/swatches/pattern1.png", "translation": { "locale": "en", "label": "Pattern 1" } } }, { "node": { "id": "/api/shop/attribute-options/11", "adminName": "Pattern2", "swatchValue": null, "swatchValueUrl": "https://api-demo.bagisto.com/storage/swatches/pattern2.png", "translation": { "locale": "en", "label": "Pattern 2" } } } ] } } } ``` ## Get Attribute Options via Attribute Retrieve options as a nested resource within an attribute query. ### Query ```graphql query GetAttributeWithOptions($attributeId: String!) { attribute(id: $attributeId) { id code name options(first: 20) { edges { node { id adminName sortOrder swatchValue translation { locale label } } cursor } pageInfo { hasNextPage endCursor } } } } ``` ### Variables ```json { "attributeId": "/api/shop/attributes/23" } ``` ### Response ```json { "data": { "attribute": { "id": "/api/shop/attributes/23", "code": "color", "name": "Color", "options": { "edges": [ { "node": { "id": "/api/shop/attribute-options/1", "adminName": "Red", "sortOrder": 0, "swatchValue": "#e10e0e", "translation": { "locale": "en", "label": "Red" } }, "cursor": "MA==" }, { "node": { "id": "/api/shop/attribute-options/2", "adminName": "Green", "sortOrder": 1, "swatchValue": "#155616", "translation": { "locale": "en", "label": "Green" } }, "cursor": "MQ==" } ], "pageInfo": { "hasNextPage": false, "endCursor": "MQ==" } } } } } ``` ## Paginate Through Attribute Options Use cursor-based pagination to navigate through large sets of options. ### First Page ```graphql query GetFirstPage($attributeId: Int!) { attributeOptions(attributeId: $attributeId, first: 10) { edges { node { id adminName } cursor } pageInfo { hasNextPage endCursor } } } ``` ### Next Page ```graphql query GetNextPage( $attributeId: Int! $cursor: String! ) { attributeOptions( attributeId: $attributeId first: 10 after: $cursor ) { edges { node { id adminName } cursor } pageInfo { hasNextPage endCursor } } } ``` ### Variables ```json { "attributeId": 23, "cursor": "MQ==" } ``` ## Get All Options for a Product's Attributes Retrieve all attribute options for a specific product. ### Query ```graphql query GetProductAttributeOptions($productId: String!) { product(id: $productId) { id name attributes { edges { node { id code label attributeId } } } } } ``` ### Variables ```json { "productId": "1" } ``` ### Response ```json { "data": { "product": { "id": "1", "name": "Awesome Shirt", "attributes": { "edges": [ { "node": { "id": "/api/shop/attributes/1", "code": "color", "label": "Color", "attributeId": 1 } }, { "node": { "id": "/api/shop/attributes/2", "code": "size", "label": "Size", "attributeId": 2 } } ] } } } } ``` ## Filter Products by Attribute Option Search for products that have a specific attribute option. ### Query ```graphql query GetProductsByAttributeOption( $channel: String! $filters: ProductFilterInput $first: Int ) { products( channel: $channel filter: $filters first: $first ) { edges { node { id name sku price attributes { edges { node { code label value } } } } } } } ``` ### Variables ```json { "channel": "default", "filters": { "color": "1" }, "first": 20 } ``` ### Response ```json { "data": { "products": { "edges": [ { "node": { "id": "1", "name": "Red Shirt", "sku": "SHIRT-RED-01", "price": "29.99", "attributes": { "edges": [ { "node": { "code": "color", "label": "Color", "value": "Red" } } ] } } } ] } } } ``` ## Field Reference ### AttributeOption Fields | Field | Type | Description | |-------|------|-------------| | `id` | String | Unique identifier in format `/api/shop/attribute-options/{id}` | | `_id` | Int | Numeric ID | | `adminName` | String | Admin-facing option name (e.g., "Red", "Large") | | `sortOrder` | Int | Display order of the option | | `swatchValue` | String | Hex color code (for color attributes) or text value | | `swatchValueUrl` | String | URL to swatch image file | | `translation` | Object | Single translation for current locale | | `translations` | Connection | Collection of all translations with pagination | ### Translation Fields | Field | Type | Description | |-------|------|-------------| | `id` | String | Translation ID | | `locale` | String | Language locale code (e.g., "en", "ar") | | `label` | String | Translated label for the option | ## Common Use Cases ### Display Color Swatches in Product Listing ```graphql query GetColorOptions($attributeId: Int!) { attributeOptions(attributeId: $attributeId, first: 50) { edges { node { adminName swatchValue swatchValueUrl translation { label } } } } } ``` ### Build Size Guide with Options ```graphql query GetSizeOptions($attributeId: Int!) { attributeOptions(attributeId: $attributeId, first: 100) { edges { node { adminName sortOrder translation { label } } } } } ``` ### Multi-language Product Configuration ```graphql query GetMultiLanguageOptions($attributeId: Int!) { attributeOptions(attributeId: $attributeId, first: 20) { edges { node { id adminName translations(first: 10) { edges { node { locale label } } } } } } } ``` ## Error Handling ### Missing Attribute ID ```json { "errors": [ { "message": "Field \"attributeOptions\" argument \"attributeId\" of type \"Int!\" is required but not provided." } ] } ``` ### Invalid Pagination Cursor ```json { "errors": [ { "message": "Invalid cursor provided" } ] } ``` ### Non-existent Attribute ```json { "data": { "attributeOptions": { "edges": [], "pageInfo": { "hasNextPage": false, "endCursor": null } } } } ``` ## Best Practices 1. **Use Specific Fields** - Only request the fields you need 2. **Implement Pagination** - Always use pagination for large option sets 3. **Cache Results** - Cache attribute options as they change infrequently 4. **Handle Translations** - Always request translations for multi-language support 5. **Optimize Images** - Pre-load swatch images for better UX ## Related Resources - [Products API](/api/graphql-api/shop/products) - Product information and management - [Categories API](/api/graphql-api/shop/categories) - Product categories - [Pagination Guide](/api/graphql-api/pagination) - Cursor pagination documentation - [Shop API Overview](/api/graphql-api/shop-api) - Overview of Shop API --- # Shop API - Cart URL: /api/graphql-api/shop/cart # Shop API - Cart Manage shopping cart operations for both guest and authenticated customers. ## Create Guest Cart Create a new cart for guest users. ```graphql mutation CreateGuestCart { createCartToken(input: {}) { cartToken } } ``` ## Create Authenticated Cart Create a cart for logged-in customers. ```graphql mutation CreateAuthCart { createCart(input: {}) { cart { id itemsCount } } } ``` ## Add Product to Cart Add one or more products to a cart. ```graphql mutation AddToCart($cartId: String!, $items: [CartItemInput!]!) { addProductsToCart(input: { cartId: $cartId items: $items }) { cart { id itemsCount items { edges { node { id productId quantity product { name price } } } } } } } ``` **Variables:** ```json { "cartId": "your-cart-id", "items": [ { "productId": "1", "quantity": 2 } ] } ``` ## Update Cart Item Modify the quantity of a cart item. ```graphql mutation UpdateCartItem($cartId: String!, $cartItemId: String!, $quantity: Int!) { updateCart(input: { cartId: $cartId items: [ { cartItemId: $cartItemId quantity: $quantity } ] }) { cart { id itemsCount } } } ``` ## Remove Cart Item Remove an item from the cart. ```graphql mutation RemoveCartItem($cartId: String!, $cartItemId: String!) { removeCartItem(input: { cartId: $cartId cartItemId: $cartItemId }) { cart { id itemsCount } } } ``` ## Get Cart Retrieve full cart details. ```graphql query GetCart($cartId: String!) { cart(id: $cartId) { id itemsCount grandTotal subtotal taxTotal shippingTotal discountAmount items { edges { node { id quantity price product { name } } } } } } ``` ## Apply Coupon Apply a coupon code to the cart. ```graphql mutation ApplyCoupon($cartId: String!, $code: String!) { applyCoupon(input: { cartId: $cartId couponCode: $code }) { cart { couponCode discountAmount grandTotal } } } ``` ## Remove Coupon Remove an applied coupon. ```graphql mutation RemoveCoupon($cartId: String!) { removeCoupon(input: { cartId: $cartId }) { cart { grandTotal } } } ``` ## Related Resources - [Products](/api/graphql-api/shop/products) - [Checkout](/api/graphql-api/shop/checkout) - [Orders](/api/graphql-api/shop/orders) --- # Shop API - Categories URL: /api/graphql-api/shop/categories # Shop API - Categories Manage and retrieve product categories. ## Get Category Tree Retrieve the complete hierarchical category structure. ```graphql query GetCategoryTree($parentId: Int!) { treeCategories(parentId: $parentId) { id position status translation { name slug urlPath } children { edges { node { id position translation { name slug } children { edges { node { id translation { name slug } } } } } } } } } ``` **Variables:** ```json { "parentId": 1 } ``` ## Get Products by Category Retrieve products from a specific category. ```graphql query GetCategoryProducts($categoryId: Int!, $first: Int!) { products(categoryId: $categoryId, first: $first) { edges { node { id name sku price } } } } ``` ## Related Resources - [Products](/api/graphql-api/shop/products) - [Cart](/api/graphql-api/shop/cart) --- # Shop API - Checkout URL: /api/graphql-api/shop/checkout # Shop API - Checkout Complete the checkout process and create orders. ## Save Billing Address Save billing address for checkout. ```graphql mutation SaveBillingAddress($input: SaveAddressInput!) { saveCustomerAddress(input: $input) { address { id firstName lastName address city state country zipCode } } } ``` **Variables:** ```json { "input": { "firstName": "John", "lastName": "Doe", "address": "123 Main St", "city": "New York", "state": "NY", "country": "US", "zipCode": "10001" } } ``` ## Estimate Shipping Get available shipping methods and costs. ```graphql mutation EstimateShipping($input: EstimateShippingInput!) { estimateShipping(input: $input) { shippingMethods { edges { node { code title description price } } } } } ``` **Variables:** ```json { "input": { "cartId": "cart-id", "country": "US", "state": "CA", "zipCode": "90210" } } ``` ## Set Shipping Method Select a shipping method for the order. ```graphql mutation SetShipping($input: ShippingMethodInput!) { saveShippingMethod(input: $input) { cart { id shippingTotal grandTotal } } } ``` **Variables:** ```json { "input": { "cartId": "cart-id", "shippingMethodCode": "flatrate_flatrate" } } ``` ## Get Payment Methods Retrieve available payment methods for the store. ```graphql query GetPaymentMethods($cartId: String!) { paymentMethods(cartId: $cartId) { edges { node { code title description image } } } } ``` ## Set Payment Method Select a payment method for the order. ```graphql mutation SetPayment($input: PaymentMethodInput!) { savePaymentMethod(input: $input) { status message } } ``` **Variables:** ```json { "input": { "cartId": "cart-id", "method": "paypal" } } ``` ## Create Order (Authenticated) Place an order as an authenticated customer. ```graphql mutation CreateOrder($input: CreateOrderInput!) { createOrder(input: $input) { order { id incrementId status grandTotal createdAt } } } ``` **Variables:** ```json { "input": { "cartId": "cart-id", "billingAddressId": "1", "shippingAddressId": "1", "shippingMethod": "flatrate_flatrate", "paymentMethod": "paypal" } } ``` ## Create Order (Guest) Place an order as a guest user. ```graphql mutation CreateGuestOrder($input: CreateGuestOrderInput!) { createGuestOrder(input: $input) { order { id incrementId status grandTotal } } } ``` **Variables:** ```json { "input": { "cartId": "cart-id", "billingAddress": { "firstName": "John", "lastName": "Doe", "email": "john@example.com", "address": "123 Main St", "city": "New York", "country": "US", "state": "NY", "zipCode": "10001" }, "shippingMethod": "flatrate_flatrate", "paymentMethod": "paypal" } } ``` ## Related Resources - [Cart](/api/graphql-api/shop/cart) - [Orders](/api/graphql-api/shop/orders) - [Customers](/api/graphql-api/shop/customers) --- # Shop API - Customers URL: /api/graphql-api/shop/customers # Shop API - Customers Manage customer accounts, authentication, and profiles. ## Customer Registration Create a new customer account. ```graphql mutation Register($input: CreateCustomerInput!) { createCustomer(input: $input) { customer { id firstName lastName email createdAt } } } ``` **Variables:** ```json { "input": { "firstName": "John", "lastName": "Doe", "email": "john@example.com", "password": "SecurePassword123!", "passwordConfirmation": "SecurePassword123!" } } ``` ## Customer Login Authenticate a customer and receive an access token. ```graphql mutation Login($email: String!, $password: String!) { createLogin(input: { email: $email password: $password }) { accessToken customer { id firstName lastName email } } } ``` **Variables:** ```json { "email": "john@example.com", "password": "SecurePassword123!" } ``` ## Get Customer Profile Retrieve the current customer's profile information. ```graphql query GetProfile { customer { id firstName lastName email gender dateOfBirth addresses { edges { node { id firstName lastName address city state country } } } } } ``` ## Update Customer Profile Update customer profile information. ```graphql mutation UpdateProfile($input: UpdateCustomerInput!) { updateCustomer(input: $input) { customer { id firstName lastName email } } } ``` ## Change Password Update customer password. ```graphql mutation ChangePassword($input: ChangePasswordInput!) { changeCustomerPassword(input: $input) { status message } } ``` ## Add Address Add a new address to customer account. ```graphql mutation AddAddress($input: AddAddressInput!) { addCustomerAddress(input: $input) { address { id firstName lastName address city state country zipCode } } } ``` ## Update Address Update an existing customer address. ```graphql mutation UpdateAddress($input: UpdateAddressInput!) { updateCustomerAddress(input: $input) { address { id firstName address city } } } ``` ## Delete Address Remove an address from customer account. ```graphql mutation DeleteAddress($addressId: String!) { deleteCustomerAddress(input: { addressId: $addressId }) { status } } ``` ## Forgot Password Request password reset email. ```graphql mutation ForgotPassword($email: String!) { forgotPassword(input: { email: $email }) { status message } } ``` ## Reset Password Reset password using token from email. ```graphql mutation ResetPassword($input: ResetPasswordInput!) { resetPassword(input: $input) { status message } } ``` ## Customer Logout Logout the current customer. ```graphql mutation Logout { createLogout(input: {}) { status } } ``` ## Related Resources - [Orders](/api/graphql-api/shop/orders) - [Cart](/api/graphql-api/shop/cart) - [Addresses](/api/graphql-api/shop/customers#add-address) --- # Get Locales URL: /api/graphql-api/shop/locales/queries/locales --- outline: false examples: - id: get-locales-basic title: Get Locales - Basic description: Retrieve all store locales with basic information. query: | query getLocales { locales { edges { node { id _id code name direction } } pageInfo { hasNextPage endCursor } } } variables: | {} response: | { "data": { "locales": { "edges": [ { "node": { "id": "/api/shop/locales/1", "_id": 1, "code": "en", "name": "English", "direction": "ltr" } }, { "node": { "id": "/api/shop/locales/10", "_id": 10, "code": "AR", "name": "Arabic", "direction": "rtl" } } ], "pageInfo": { "hasNextPage": false, "endCursor": "MQ==" } } } } commonErrors: - error: UNAUTHORIZED cause: Invalid or missing authentication token solution: Provide valid authentication credentials - error: NO_LOCALES cause: No locales configured in the system solution: Create locales in the admin panel - id: get-locales-complete title: Get Locales - Complete Details description: Retrieve all locales with complete information including logo paths. query: | query getLocales { locales { edges { cursor node { id _id code name direction logoPath logoUrl } } pageInfo { endCursor startCursor hasNextPage hasPreviousPage } totalCount } } variables: | {} response: | { "data": { "locales": { "edges": [ { "cursor": "MA==", "node": { "id": "/api/shop/locales/1", "_id": 1, "code": "en", "name": "English", "direction": "ltr", "logoPath": "locales/en.png", "logoUrl": "https://api-demo.bagisto.com/storage/locales/en.png" } }, { "cursor": "MQ==", "node": { "id": "/api/shop/locales/10", "_id": 10, "code": "AR", "name": "Arabic", "direction": "rtl", "logoPath": "locales/AR.png", "logoUrl": "https://api-demo.bagisto.com/storage/locales/AR.png" } } ], "pageInfo": { "endCursor": "MQ==", "startCursor": "MA==", "hasNextPage": false, "hasPreviousPage": false }, "totalCount": 2 } } } commonErrors: - error: INVALID_PAGINATION cause: Invalid pagination parameters provided solution: Ensure first/last are positive integers and cursors are valid - error: INVALID_CURSOR cause: Pagination cursor is invalid or expired solution: Use cursor values from the previous response - id: get-locales-with-pagination title: Get Locales with Pagination description: Retrieve locales with cursor-based pagination for handling large datasets. query: | query getLocalesWithPagination($first: Int, $after: String) { locales(first: $first, after: $after) { edges { cursor node { id _id code name direction logoUrl logoPath } } pageInfo { endCursor startCursor hasNextPage hasPreviousPage } totalCount } } variables: | { "first": 10, "after": null } response: | { "data": { "locales": { "edges": [ { "cursor": "MA==", "node": { "id": "/api/shop/locales/1", "_id": 1, "code": "en", "name": "English", "direction": "ltr", "logoUrl": "https://api-demo.bagisto.com/storage/locales/en.png", "logoPath": "locales/en.png" } }, { "cursor": "MQ==", "node": { "id": "/api/shop/locales/10", "_id": 10, "code": "AR", "name": "Arabic", "direction": "rtl", "logoUrl": "https://api-demo.bagisto.com/storage/locales/AR.png", "logoPath": "locales/AR.png" } } ], "pageInfo": { "endCursor": "MQ==", "startCursor": "MA==", "hasNextPage": false, "hasPreviousPage": false }, "totalCount": 2 } } } commonErrors: - error: INVALID_FIRST_VALUE cause: The first argument exceeds maximum allowed value solution: Use first value between 1 and 100 - error: INVALID_CURSOR cause: The provided cursor is invalid solution: Use a valid cursor from a previous response --- # Get Locales ## About The `getLocales` query retrieves locale information from your store with support for pagination and detailed field access. This query is essential for: - Displaying available language and locale options - Building multi-language selector interfaces - Determining text direction (LTR/RTL) for UI layout - Retrieving locale-specific logos and branding - Managing store language configurations - Building locale management interfaces The query supports cursor-based pagination and allows you to fetch all locales with full relationship access. ## Arguments | Argument | Type | Required | Description | |----------|------|----------|-------------| | `first` | `Int` | No | Number of locales to retrieve from the start (forward pagination). Max: 100. | | `after` | `String` | No | Cursor to start after for forward pagination. | | `last` | `Int` | No | Number of locales to retrieve from the end (backward pagination). Max: 100. | | `before` | `String` | No | Cursor to start before for backward pagination. | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `edges` | `[LocaleEdge!]!` | Array of locale edges containing locales and cursors. | | `edges.node` | `Locale!` | The actual locale object with id, code, name, direction, and other fields. | | `edges.cursor` | `String!` | Pagination cursor for this locale. Use with `after` or `before` arguments. | | `pageInfo` | `PageInfo!` | Pagination metadata object. | | `pageInfo.hasNextPage` | `Boolean!` | Whether more locales exist after the current page. | | `pageInfo.hasPreviousPage` | `Boolean!` | Whether locales exist before the current page. | | `pageInfo.startCursor` | `String` | Cursor of the first locale on the current page. | | `pageInfo.endCursor` | `String` | Cursor of the last locale on the current page. | | `totalCount` | `Int!` | Total number of locales available. | ## Locale Fields | Field | Type | Description | |-------|------|-------------| | `id` | `String!` | Unique identifier in format `/api/shop/locales/{id}` | | `_id` | `Int!` | Numeric identifier for the locale | | `code` | `String!` | Unique locale code (e.g., `"en"`, `"AR"`) | | `name` | `String!` | Display name of the locale (e.g., `"English"`, `"Arabic"`) | | `direction` | `String!` | Text direction: `"ltr"` (left-to-right) or `"rtl"` (right-to-left) | | `logoPath` | `String` | File path to the locale logo (e.g., `"locales/en.png"`) | | `logoUrl` | `String` | Full URL to the locale logo image | --- # Get Single Locale URL: /api/graphql-api/shop/locales/queries/single-locale --- outline: false examples: - id: get-locale-basic title: Get Single Locale - Basic description: Retrieve a single locale by ID with basic information. query: | query getSingleLocale($id: ID!) { locale(id: $id) { id _id code name direction } } variables: | { "id": "/api/shop/locales/10" } response: | { "data": { "locale": { "id": "/api/shop/locales/10", "_id": 10, "code": "AR", "name": "Arabic", "direction": "rtl" } } } commonErrors: - error: Variable "$id" of required type "ID!" was not provided. cause: Locale ID parameter is required solution: Provide a valid locale ID in format /api/shop/locales/{id} or numeric ID - error: Locale not found cause: Locale ID does not exist in the system solution: Verify the locale ID is correct and exists - id: get-locale-complete title: Get Single Locale - Complete Details description: Retrieve a single locale with all fields including logo path and URL. query: | query getSingleLocale($id: ID!) { locale(id: $id) { id _id code name direction logoPath logoUrl } } variables: | { "id": "/api/shop/locales/10" } response: | { "data": { "locale": { "id": "/api/shop/locales/10", "_id": 10, "code": "AR", "name": "Arabic", "direction": "rtl", "logoPath": "locales/AR.png", "logoUrl": "https://api-demo.bagisto.com/storage/locales/AR.png" } } } commonErrors: - error: Locale not found cause: The provided locale ID does not exist solution: Use a valid locale ID from the get-locales query - id: get-locale-by-code title: Get Single Locale - Using Numeric ID description: Retrieve a single locale by its numeric ID instead of IRI format. query: | query getSingleLocale($id: ID!) { locale(id: $id) { id _id code name direction logoPath logoUrl } } variables: | { "id": "10" } response: | { "data": { "locale": { "id": "/api/shop/locales/10", "_id": 10, "code": "AR", "name": "Arabic", "direction": "rtl", "logoPath": "locales/AR.png", "logoUrl": "https://api-demo.bagisto.com/storage/locales/AR.png" } } } commonErrors: - error: Locale not found cause: The provided locale ID does not exist solution: Use a valid locale ID from the get-locales query --- # Get Single Locale ## About The `locale` query retrieves a single locale by ID with support for detailed field access. This query is essential for: - Fetching specific locale details for UI configuration - Checking text direction (LTR/RTL) for layout adjustments - Retrieving locale-specific branding and logos - Validating locale existence before operations - Building locale detail pages - Configuring locale-specific settings The query allows you to fetch a specific locale with all its properties and relationships. ## Arguments | Argument | Type | Required | Description | |----------|------|----------|-------------| | `id` | `ID!` | Yes | The unique identifier of the locale. Can be either numeric ID or IRI format (`/api/shop/locales/{id}`). | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `locale` | `Locale` | The requested locale object, or null if not found. | ## Locale Fields | Field | Type | Description | |-------|------|-------------| | `id` | `String!` | Unique identifier in format `/api/shop/locales/{id}` | | `_id` | `Int!` | Numeric identifier for the locale | | `code` | `String!` | Unique locale code (e.g., `"en"`, `"AR"`) | | `name` | `String!` | Display name of the locale (e.g., `"English"`, `"Arabic"`) | | `direction` | `String!` | Text direction: `"ltr"` (left-to-right) or `"rtl"` (right-to-left) | | `logoPath` | `String` | File path to the locale logo (e.g., `"locales/en.png"`) | | `logoUrl` | `String` | Full URL to the locale logo image | ## Error Handling ### Locale Not Found ```json { "data": { "locale": null } } ``` ### Missing Required ID Parameter ```json { "errors": [ { "message": "Field \"locale\" argument \"id\" of type \"ID!\" is required but not provided." } ] } ``` ## Related Resources - [Get Locales](/api/graphql-api/shop/locales/queries/locales) - Retrieve all locales with pagination --- # Add to Cart URL: /api/graphql-api/shop/mutations/add-to-cart --- outline: false examples: - id: add-simple-product-to-cart title: Add Simple Product to Cart description: Add a simple product to the cart. Simple products only require `productId` and `quantity` — no additional options are needed. query: | mutation createAddProductInCart( $productId: Int! $quantity: Int! ) { createAddProductInCart( input: { productId: $productId quantity: $quantity } ) { addProductInCart { id _id cartToken customerId channelId subtotal baseSubtotal discountAmount baseDiscountAmount taxAmount baseTaxAmount shippingAmount baseShippingAmount grandTotal baseGrandTotal formattedSubtotal formattedDiscountAmount formattedTaxAmount formattedShippingAmount formattedGrandTotal couponCode items { totalCount pageInfo { startCursor endCursor hasNextPage hasPreviousPage } edges { cursor node { id cartId productId name sku quantity price basePrice total baseTotal discountAmount baseDiscountAmount taxAmount baseTaxAmount type formattedPrice formattedTotal priceInclTax basePriceInclTax formattedPriceInclTax totalInclTax baseTotalInclTax formattedTotalInclTax productUrlKey canChangeQty } } } success message sessionToken isGuest itemsQty itemsCount haveStockableItems paymentMethod paymentMethodTitle subTotalInclTax baseSubTotalInclTax formattedSubTotalInclTax taxTotal formattedTaxTotal shippingAmountInclTax baseShippingAmountInclTax formattedShippingAmountInclTax } } } variables: | { "productId": 2359, "quantity": 1 } response: | { "data": { "createAddProductInCart": { "addProductInCart": { "id": "4635", "_id": 4635, "cartToken": "4635", "customerId": 19, "channelId": 1, "subtotal": 4000, "baseSubtotal": 4000, "discountAmount": 0, "baseDiscountAmount": 0, "taxAmount": 0, "baseTaxAmount": 0, "shippingAmount": 0, "baseShippingAmount": 0, "grandTotal": 4000, "baseGrandTotal": 4000, "formattedSubtotal": "$4,000.00", "formattedDiscountAmount": "$0.00", "formattedTaxAmount": "$0.00", "formattedShippingAmount": "$0.00", "formattedGrandTotal": "$4,000.00", "couponCode": null, "items": { "totalCount": 1, "pageInfo": { "startCursor": "MA==", "endCursor": "MA==", "hasNextPage": false, "hasPreviousPage": false }, "edges": [ { "cursor": "MA==", "node": { "id": "5809", "cartId": 4635, "productId": 2359, "name": "Horizon Arc 49\" OLED Curved Gaming Monitor", "sku": "HORIZON-MONITOR-49", "quantity": 1, "price": 4000, "basePrice": 4000, "total": 4000, "baseTotal": 4000, "discountAmount": 0, "baseDiscountAmount": 0, "taxAmount": 0, "baseTaxAmount": 0, "type": "simple", "formattedPrice": "$4,000.00", "formattedTotal": "$4,000.00", "priceInclTax": 4000, "basePriceInclTax": 4000, "formattedPriceInclTax": "$4,000.00", "totalInclTax": 4000, "baseTotalInclTax": 4000, "formattedTotalInclTax": "$4,000.00", "productUrlKey": "horizon-arc-49-oled-curved-gaming-monitor", "canChangeQty": true } } ] }, "success": true, "message": "Product added to cart successfully", "sessionToken": null, "isGuest": false, "itemsQty": 1, "itemsCount": 1, "haveStockableItems": true, "paymentMethod": null, "paymentMethodTitle": null, "subTotalInclTax": 4000, "baseSubTotalInclTax": 4000, "formattedSubTotalInclTax": "$4,000.00", "taxTotal": 0, "formattedTaxTotal": "$0.00", "shippingAmountInclTax": 0, "baseShippingAmountInclTax": 0, "formattedShippingAmountInclTax": "$0.00" } } } } commonErrors: - error: PRODUCT_NOT_FOUND cause: Product ID does not exist or product is inactive solution: Verify the product ID exists and the product status is active - error: OUT_OF_STOCK cause: Requested quantity exceeds available stock solution: Reduce quantity or choose a different product - id: add-virtual-product-to-cart title: Add Virtual Product to Cart description: | Add a virtual product (e.g., a subscription, service, or digital access) to the cart. Virtual products are added the same way as simple products — only `productId` and `quantity` are required. Virtual products have no physical shipment, so `haveStockableItems` will be `false` and no shipping step is needed during checkout. query: | mutation createAddProductInCart( $productId: Int! $quantity: Int! ) { createAddProductInCart( input: { productId: $productId quantity: $quantity } ) { addProductInCart { id _id cartToken customerId channelId subtotal baseSubtotal discountAmount baseDiscountAmount taxAmount baseTaxAmount shippingAmount baseShippingAmount grandTotal baseGrandTotal formattedSubtotal formattedDiscountAmount formattedTaxAmount formattedShippingAmount formattedGrandTotal couponCode items { totalCount pageInfo { startCursor endCursor hasNextPage hasPreviousPage } edges { cursor node { id cartId productId name sku quantity price basePrice total baseTotal discountAmount baseDiscountAmount taxAmount baseTaxAmount type formattedPrice formattedTotal priceInclTax basePriceInclTax formattedPriceInclTax totalInclTax baseTotalInclTax formattedTotalInclTax productUrlKey canChangeQty } } } success message sessionToken isGuest itemsQty itemsCount haveStockableItems paymentMethod paymentMethodTitle subTotalInclTax baseSubTotalInclTax formattedSubTotalInclTax taxTotal formattedTaxTotal shippingAmountInclTax baseShippingAmountInclTax formattedShippingAmountInclTax } } } variables: | { "productId": 2505, "quantity": 1 } response: | { "data": { "createAddProductInCart": { "addProductInCart": { "id": "4645", "_id": 4645, "cartToken": "4645", "customerId": 19, "channelId": 1, "subtotal": 59, "baseSubtotal": 59, "discountAmount": 0, "baseDiscountAmount": 0, "taxAmount": 0, "baseTaxAmount": 0, "shippingAmount": 0, "baseShippingAmount": 0, "grandTotal": 59, "baseGrandTotal": 59, "formattedSubtotal": "$59.00", "formattedDiscountAmount": "$0.00", "formattedTaxAmount": "$0.00", "formattedShippingAmount": "$0.00", "formattedGrandTotal": "$59.00", "couponCode": null, "items": { "totalCount": 1, "pageInfo": { "startCursor": "MA==", "endCursor": "MA==", "hasNextPage": false, "hasPreviousPage": false }, "edges": [ { "cursor": "MA==", "node": { "id": "5842", "cartId": 4645, "productId": 2505, "name": "HD Streaming Subscription - 1 Month Access", "sku": "HD-STREAMING-SUB-1M", "quantity": 1, "price": 59, "basePrice": 59, "total": 59, "baseTotal": 59, "discountAmount": 0, "baseDiscountAmount": 0, "taxAmount": 0, "baseTaxAmount": 0, "type": "virtual", "formattedPrice": "$59.00", "formattedTotal": "$59.00", "priceInclTax": 59, "basePriceInclTax": 59, "formattedPriceInclTax": "$59.00", "totalInclTax": 59, "baseTotalInclTax": 59, "formattedTotalInclTax": "$59.00", "productUrlKey": "hd-streaming-subscription-1-month-access", "canChangeQty": true } } ] }, "success": true, "message": "Product added to cart successfully", "sessionToken": null, "isGuest": false, "itemsQty": 1, "itemsCount": 1, "haveStockableItems": false, "paymentMethod": null, "paymentMethodTitle": null, "subTotalInclTax": 59, "baseSubTotalInclTax": 59, "formattedSubTotalInclTax": "$59.00", "taxTotal": 0, "formattedTaxTotal": "$0.00", "shippingAmountInclTax": 0, "baseShippingAmountInclTax": 0, "formattedShippingAmountInclTax": "$0.00" } } } } commonErrors: - error: PRODUCT_NOT_FOUND cause: Product ID does not exist or product is inactive solution: Verify the product ID exists and the product status is active - error: OUT_OF_STOCK cause: Requested quantity exceeds available stock solution: Reduce quantity or choose a different product - id: add-configurable-product-to-cart title: Add Configurable Product to Cart description: | Add a configurable product (e.g., a product with color/size options) to the cart. Requires `selectedConfigurableOption` (the child variant product ID) and `superAttribute` (an array mapping attribute IDs to their selected option IDs). query: | mutation createAddProductInCart( $productId: Int! $quantity: Int! $selectedConfigurableOption: Int! $superAttribute: Iterable ) { createAddProductInCart( input: { productId: $productId quantity: $quantity selectedConfigurableOption: $selectedConfigurableOption superAttribute: $superAttribute } ) { addProductInCart { id _id cartToken customerId channelId subtotal baseSubtotal grandTotal baseGrandTotal formattedSubtotal formattedGrandTotal items { totalCount edges { cursor node { id cartId productId name sku quantity price basePrice total baseImage baseTotal type formattedPrice formattedTotal productUrlKey canChangeQty options } } } success message sessionToken isGuest itemsQty itemsCount } } } variables: | { "productId": 123, "quantity": 1, "selectedConfigurableOption": 124, "superAttribute": [ { "23": 1 }, { "24": 6 } ] } response: | { "data": { "createAddProductInCart": { "addProductInCart": { "id": "4634", "_id": 4634, "cartToken": "4634", "customerId": 19, "channelId": 1, "subtotal": 4080, "baseSubtotal": 4080, "grandTotal": 4080, "baseGrandTotal": 4080, "formattedSubtotal": "$4,080.00", "formattedGrandTotal": "$4,080.00", "items": { "totalCount": 1, "edges": [ { "cursor": "MA==", "node": { "id": "5805", "cartId": 4634, "productId": 123, "name": "Zoe Tank", "sku": "SDASDAS123123", "quantity": 1, "price": 2040, "basePrice": 2040, "total": 4080, "baseImage": "{\"small_image_url\":\"https:\\/\\/api-demo.bagisto.com\\/cache\\/small\\/product\\/124\\/Ba8yoli6aFgjpiFLUcfEpuHaGzbaRCYu7wEvKR2d.webp\",\"medium_image_url\":\"https:\\/\\/api-demo.bagisto.com\\/cache\\/medium\\/product\\/124\\/Ba8yoli6aFgjpiFLUcfEpuHaGzbaRCYu7wEvKR2d.webp\",\"large_image_url\":\"https:\\/\\/api-demo.bagisto.com\\/cache\\/large\\/product\\/124\\/Ba8yoli6aFgjpiFLUcfEpuHaGzbaRCYu7wEvKR2d.webp\",\"original_image_url\":\"https:\\/\\/api-demo.bagisto.com\\/cache\\/original\\/product\\/124\\/Ba8yoli6aFgjpiFLUcfEpuHaGzbaRCYu7wEvKR2d.webp\"}", "baseTotal": 4080, "type": "configurable", "formattedPrice": "$2,040.00", "formattedTotal": "$4,080.00", "productUrlKey": "zoe-tank", "canChangeQty": true, "options": { "edges": [ { "node": { "option_id": 6, "option_label": "S", "attribute_name": "Size" } }, { "node": { "option_id": 1, "option_label": "Red", "attribute_name": "Color" } } ] } } } ] }, "success": true, "message": "Product added to cart successfully", "sessionToken": null, "isGuest": false, "itemsQty": 2, "itemsCount": 1 } } } } commonErrors: - error: PRODUCT_NOT_FOUND cause: Product ID or selected configurable option does not exist solution: Verify both the parent product ID and the child variant ID are correct - error: INVALID_CONFIGURATION cause: The superAttribute values don't match any valid variant solution: Ensure superAttribute maps the correct attribute IDs to valid option IDs for the selected variant - error: OUT_OF_STOCK cause: The selected variant is out of stock solution: Choose a different variant or reduce quantity - id: add-downloadable-product-to-cart title: Add Downloadable Product to Cart description: | Add a downloadable product to the cart. Requires `links` — an array of downloadable link IDs that the customer has selected to purchase. Link IDs can be retrieved from the product query. query: | mutation createAddProductInCart( $productId: Int! $quantity: Int! $links: Iterable ) { createAddProductInCart( input: { productId: $productId quantity: $quantity links: $links } ) { addProductInCart { id _id cartToken subtotal grandTotal formattedSubtotal formattedGrandTotal items { totalCount edges { cursor node { id cartId productId name sku quantity price basePrice total baseTotal type formattedPrice formattedTotal productUrlKey canChangeQty options } } } success message sessionToken isGuest itemsQty itemsCount haveStockableItems } } } variables: | { "productId": 2506, "quantity": 1, "links": [2] } response: | { "data": { "createAddProductInCart": { "addProductInCart": { "id": "4638", "_id": 4638, "cartToken": "4638", "subtotal": 138, "grandTotal": 138, "formattedSubtotal": "$138.00", "formattedGrandTotal": "$138.00", "items": { "totalCount": 1, "edges": [ { "cursor": "MA==", "node": { "id": "5813", "cartId": 4638, "productId": 2506, "name": "Complete Personal Finance Guide (eBook PDF)", "sku": "COMPLETE-PERSONAL-FINANCE-GUIDE-EBOOK", "quantity": 1, "price": 138, "basePrice": 138, "total": 138, "baseTotal": 138, "type": "downloadable", "formattedPrice": "$138.00", "formattedTotal": "$138.00", "productUrlKey": "complete-personal-finance-guide-ebook-pdf", "canChangeQty": false, "options": { "edges": [ { "node": { "option_id": 0, "option_label": "Full eBook PDF", "attribute_name": "Downloads" } } ] } } } ] }, "success": true, "message": "Product added to cart successfully", "sessionToken": null, "isGuest": false, "itemsQty": 1, "itemsCount": 1, "haveStockableItems": false } } } } commonErrors: - error: PRODUCT_NOT_FOUND cause: Product ID does not exist solution: Verify the product ID - error: INVALID_LINKS cause: One or more link IDs are invalid or do not belong to this product solution: Use valid link IDs from the product query response - id: add-grouped-product-to-cart title: Add Grouped Product to Cart description: | Add a grouped product to the cart. Requires `groupedQty` — a JSON string mapping each associated product ID to its desired quantity. All associated products in the group must be included in the map. query: | mutation createAddProductInCart( $productId: Int! $quantity: Int! $groupedQty: String ) { createAddProductInCart( input: { productId: $productId quantity: $quantity groupedQty: $groupedQty } ) { addProductInCart { id _id cartToken subtotal grandTotal formattedSubtotal formattedGrandTotal items { totalCount edges { cursor node { id cartId productId name sku quantity price basePrice total baseTotal type formattedPrice formattedTotal productUrlKey canChangeQty options } } } success message sessionToken isGuest itemsQty itemsCount } } } variables: | { "productId": 2516, "quantity": 1, "groupedQty": "{\"2512\":1,\"2514\":1,\"2515\":1}" } response: | { "data": { "createAddProductInCart": { "addProductInCart": { "id": "4641", "_id": 4641, "cartToken": "4641", "subtotal": 52, "grandTotal": 52, "formattedSubtotal": "$52.00", "formattedGrandTotal": "$52.00", "items": { "totalCount": 3, "edges": [ { "cursor": "MA==", "node": { "id": "5819", "cartId": 4641, "productId": 2512, "name": "Arctic Cozy Knit Unisex Beanie", "sku": "SP-001", "quantity": 1, "price": 14, "basePrice": 14, "total": 14, "baseTotal": 14, "type": "simple", "formattedPrice": "$14.00", "formattedTotal": "$14.00", "productUrlKey": "arctic-cozy-knit-unisex-beanie", "canChangeQty": true, "options": null } }, { "cursor": "MQ==", "node": { "id": "5820", "cartId": 4641, "productId": 2514, "name": "Arctic Touchscreen Winter Gloves", "sku": "SP-003", "quantity": 1, "price": 17, "basePrice": 17, "total": 17, "baseTotal": 17, "type": "simple", "formattedPrice": "$17.00", "formattedTotal": "$17.00", "productUrlKey": "arctic-touchscreen-winter-gloves", "canChangeQty": true, "options": null } }, { "cursor": "Mg==", "node": { "id": "5821", "cartId": 4641, "productId": 2515, "name": "Arctic Warmth Wool Blend Socks", "sku": "SP-004", "quantity": 1, "price": 21, "basePrice": 21, "total": 21, "baseTotal": 21, "type": "simple", "formattedPrice": "$21.00", "formattedTotal": "$21.00", "productUrlKey": "arctic-warmth-wool-blend-socks", "canChangeQty": true, "options": null } } ] }, "success": true, "message": "Product added to cart successfully", "sessionToken": null, "isGuest": false, "itemsQty": 3, "itemsCount": 3 } } } } commonErrors: - error: PRODUCT_NOT_FOUND cause: Product ID does not exist solution: Verify the product ID - error: MISSING_GROUPED_QUANTITIES cause: Not all associated product IDs are included in groupedQty solution: Include all associated product IDs in the groupedQty JSON with their quantities - id: add-bundle-product-to-cart title: Add Bundle Product to Cart description: | Add a bundle product to the cart. Requires `bundleOptions` (a JSON string mapping bundle option IDs to arrays of selected product IDs) and `bundleOptionQty` (a JSON string mapping bundle option IDs to their quantities). For checkbox/multiselect options, quantities are fixed by admin; for radio/select options, customers can specify quantities. query: | mutation createAddProductInCart( $productId: Int! $quantity: Int! $bundleOptions: String $bundleOptionQty: String ) { createAddProductInCart( input: { productId: $productId quantity: $quantity bundleOptions: $bundleOptions bundleOptionQty: $bundleOptionQty } ) { addProductInCart { id _id cartToken subtotal grandTotal formattedSubtotal formattedGrandTotal items { totalCount edges { cursor node { id cartId productId name sku quantity price basePrice total baseImage baseTotal type formattedPrice formattedTotal productUrlKey canChangeQty options } } } success message sessionToken isGuest itemsQty itemsCount } } } variables: | { "productId": 2517, "quantity": 1, "bundleOptions": "{\"1\":[1],\"2\":[2],\"3\":[3],\"4\":[4]}", "bundleOptionQty": "{\"1\":2,\"2\":3}" } response: | { "data": { "createAddProductInCart": { "addProductInCart": { "id": "4643", "_id": 4643, "cartToken": "4643", "subtotal": 117, "grandTotal": 117, "formattedSubtotal": "$117.00", "formattedGrandTotal": "$117.00", "items": { "totalCount": 1, "edges": [ { "cursor": "MA==", "node": { "id": "5837", "cartId": 4643, "productId": 2517, "name": "Arctic Frost Winter Accessories Bundle", "sku": "BP-001", "quantity": 1, "price": 117, "basePrice": 117, "total": 117, "baseImage": "{\"small_image_url\":\"https:\\/\\/api-demo.bagisto.com\\/cache\\/small\\/product\\/2517\\/lW2A3FH3oKBJnnukyUyKUArdrr8dwTJxxDKthSgq.webp\",\"medium_image_url\":\"https:\\/\\/api-demo.bagisto.com\\/cache\\/medium\\/product\\/2517\\/lW2A3FH3oKBJnnukyUyKUArdrr8dwTJxxDKthSgq.webp\",\"large_image_url\":\"https:\\/\\/api-demo.bagisto.com\\/cache\\/large\\/product\\/2517\\/lW2A3FH3oKBJnnukyUyKUArdrr8dwTJxxDKthSgq.webp\",\"original_image_url\":\"https:\\/\\/api-demo.bagisto.com\\/cache\\/original\\/product\\/2517\\/lW2A3FH3oKBJnnukyUyKUArdrr8dwTJxxDKthSgq.webp\"}", "baseTotal": 117, "type": "bundle", "formattedPrice": "$117.00", "formattedTotal": "$117.00", "productUrlKey": "arctic-frost-winter-accessories-bundle", "canChangeQty": true, "options": { "edges": [ { "node": { "option_id": 1, "option_label": "2 x Arctic Cozy Knit Unisex Beanie $14.00", "attribute_name": "Bundle Option 1", "is_required": true, "can_change_qty": true } }, { "node": { "option_id": 2, "option_label": "3 x Arctic Bliss Stylish Winter Scarf $17.00", "attribute_name": "Bundle Option 1", "is_required": true, "can_change_qty": true } }, { "node": { "option_id": 3, "option_label": "1 x Arctic Touchscreen Winter Gloves $17.00", "attribute_name": "Bundle Option 2", "is_required": true, "can_change_qty": false } }, { "node": { "option_id": 4, "option_label": "1 x Arctic Warmth Wool Blend Socks $21.00", "attribute_name": "Bundle Option 2", "is_required": true, "can_change_qty": false } } ] } } } ] }, "success": true, "message": "Product added to cart successfully", "sessionToken": null, "isGuest": false, "itemsQty": 1, "itemsCount": 1 } } } } commonErrors: - error: PRODUCT_NOT_FOUND cause: Product ID does not exist solution: Verify the product ID - error: INVALID_BUNDLE_OPTIONS cause: Required bundle options are missing or invalid product IDs provided solution: Include all required bundle option IDs with valid product selections - error: INVALID_BUNDLE_QTY cause: Trying to change quantity for a checkbox/multiselect bundle option (admin-fixed qty) solution: Only specify quantities for radio/select bundle options; checkbox/multiselect quantities are fixed by admin - id: add-default-booking-product-to-cart title: Add Default Booking Product to Cart description: | Add a default booking product to the cart. The `booking` input is a JSON string containing `type`, `date` (YYYY-MM-DD), and `slot` (a time range string like "12:00 PM - 08:00 PM"). The API automatically converts the time range to Unix timestamps. query: | mutation createAddProductInCart( $productId: Int! $booking: String! $quantity: Int ) { createAddProductInCart( input: { productId: $productId quantity: $quantity booking: $booking } ) { addProductInCart { id _id cartToken subtotal grandTotal formattedSubtotal formattedGrandTotal items { totalCount edges { cursor node { id cartId productId name sku quantity price basePrice total baseTotal type formattedPrice formattedTotal productUrlKey canChangeQty options } } } success message sessionToken isGuest itemsQty itemsCount } } } variables: | { "productId": 2507, "quantity": 1, "booking": "{\"type\":\"default\",\"date\":\"2026-04-15\",\"slot\":\"12:00 PM - 06:00 PM\"}" } response: | { "data": { "createAddProductInCart": { "addProductInCart": { "id": "4663", "_id": 4663, "cartToken": "4663", "subtotal": 99, "grandTotal": 99, "formattedSubtotal": "$99.00", "formattedGrandTotal": "$99.00", "items": { "totalCount": 1, "edges": [ { "cursor": "MA==", "node": { "id": "5861", "cartId": 4663, "productId": 2507, "name": "Professional Photography Session", "sku": "PROFESSIONAL-PHOTOGRAPHY-SESSION", "quantity": 1, "price": 99, "basePrice": 99, "total": 99, "baseTotal": 99, "type": "booking", "formattedPrice": "$99.00", "formattedTotal": "$99.00", "productUrlKey": "professional-photography-session", "canChangeQty": true, "options": { "edges": [ { "node": { "option_id": 0, "option_label": "15 April, 2026 12:00 PM", "attribute_name": "Booking From" } }, { "node": { "option_id": 0, "option_label": "15 April, 2026 06:00 PM", "attribute_name": "Booking Till" } } ] } } } ] }, "success": true, "message": "Product added to cart successfully", "sessionToken": null, "isGuest": false, "itemsQty": 1, "itemsCount": 1 } } } } commonErrors: - error: PRODUCT_NOT_FOUND cause: Product ID does not exist solution: Verify the product ID - error: INVALID_BOOKING cause: Booking JSON is malformed or missing required fields solution: Ensure booking JSON contains type, date, and slot fields - error: SLOT_NOT_AVAILABLE cause: The selected time slot is already booked or not available on the selected date solution: Choose a different date or time slot - id: add-appointment-booking-product-to-cart title: Add Appointment Booking Product to Cart description: | Add an appointment booking product to the cart. The `booking` input is a JSON string with `type` set to "appointment", a `date` (YYYY-MM-DD), and a `slot` (time range string). Appointment slots are generated based on the product's configured duration and break time. query: | mutation createAddProductInCart( $productId: Int! $booking: String! ) { createAddProductInCart( input: { productId: $productId booking: $booking } ) { addProductInCart { id _id cartToken subtotal grandTotal formattedSubtotal formattedGrandTotal items { totalCount edges { cursor node { id cartId productId name sku quantity price basePrice total baseTotal type formattedPrice formattedTotal productUrlKey canChangeQty options } } } success message sessionToken isGuest itemsQty itemsCount } } } variables: | { "productId": 2509, "booking": "{\"type\":\"appointment\",\"date\":\"2026-04-08\",\"slot\":\"10:00 AM - 10:45 AM\"}" } response: | { "data": { "createAddProductInCart": { "addProductInCart": { "id": "4666", "_id": 4666, "cartToken": "4666", "subtotal": 55, "grandTotal": 55, "formattedSubtotal": "$55.00", "formattedGrandTotal": "$55.00", "items": { "totalCount": 1, "edges": [ { "cursor": "MA==", "node": { "id": "5863", "cartId": 4666, "productId": 2509, "name": "Men's Haircut Appointment", "sku": "SALON-HAIRCUT-APPOINTMENT", "quantity": 1, "price": 55, "basePrice": 55, "total": 55, "baseTotal": 55, "type": "booking", "formattedPrice": "$55.00", "formattedTotal": "$55.00", "productUrlKey": "mens-haircut-appointment", "canChangeQty": false, "options": { "edges": [ { "node": { "option_id": 0, "option_label": "08 April, 2026 10:00 AM", "attribute_name": "Booking From" } }, { "node": { "option_id": 0, "option_label": "08 April, 2026 10:45 AM", "attribute_name": "Booking Till" } } ] } } } ] }, "success": true, "message": "Product added to cart successfully", "sessionToken": null, "isGuest": false, "itemsQty": 1, "itemsCount": 1 } } } } commonErrors: - error: PRODUCT_NOT_FOUND cause: Product ID does not exist solution: Verify the product ID - error: SLOT_NOT_AVAILABLE cause: The selected appointment slot is already booked solution: Choose a different time slot or date - id: add-rental-booking-product-daily-to-cart title: Add Rental Booking Product to Cart (Daily) description: | Add a rental booking product with daily renting to the cart. For daily rentals, the `booking` JSON requires `renting_type` set to "daily", `date_from` (start date), and `date_to` (end date). The price is calculated based on the number of rental days. query: | mutation createAddProductInCart( $productId: Int! $booking: String! $quantity: Int ) { createAddProductInCart( input: { productId: $productId quantity: $quantity booking: $booking } ) { addProductInCart { id _id cartToken subtotal grandTotal formattedSubtotal formattedGrandTotal items { totalCount edges { cursor node { id cartId productId name sku quantity price total type formattedPrice formattedTotal productUrlKey canChangeQty options } } } success message sessionToken isGuest itemsQty itemsCount } } } variables: | { "productId": 2510, "quantity": 1, "booking": "{\"type\":\"rental\",\"renting_type\":\"daily\",\"date_from\":\"2026-04-08\", \"date_to\":\"2026-04-09\"}" } response: | { "data": { "createAddProductInCart": { "addProductInCart": { "id": "4677", "_id": 4677, "cartToken": "4677", "subtotal": 594, "grandTotal": 594, "formattedSubtotal": "$594.00", "formattedGrandTotal": "$594.00", "items": { "totalCount": 2, "edges": [ { "cursor": "MA==", "node": { "id": "5872", "cartId": 4677, "productId": 2510, "name": "Wooden Folding Chair Rental", "sku": "WOODEN-FOLDING-CHAIR-RENTAL", "quantity": 1, "price": 297, "total": 297, "type": "booking", "formattedPrice": "$297.00", "formattedTotal": "$297.00", "productUrlKey": "wooden-folding-chair-rental", "canChangeQty": true, "options": { "edges": [ { "node": { "option_id": 0, "option_label": "Daily Basis", "attribute_name": "Rent Type" } }, { "node": { "option_id": 0, "option_label": "08 April, 2026", "attribute_name": "Rent From" } }, { "node": { "option_id": 0, "option_label": "09 April, 2026", "attribute_name": "Rent Till" } } ] } } }, { "cursor": "MQ==", "node": { "id": "5873", "cartId": 4677, "productId": 2510, "name": "Wooden Folding Chair Rental", "sku": "WOODEN-FOLDING-CHAIR-RENTAL", "quantity": 1, "price": 297, "total": 297, "type": "booking", "formattedPrice": "$297.00", "formattedTotal": "$297.00", "productUrlKey": "wooden-folding-chair-rental", "canChangeQty": true, "options": { "edges": [ { "node": { "option_id": 0, "option_label": "Daily Basis", "attribute_name": "Rent Type" } }, { "node": { "option_id": 0, "option_label": "08 April, 2026", "attribute_name": "Rent From" } }, { "node": { "option_id": 0, "option_label": "09 April, 2026", "attribute_name": "Rent Till" } } ] } } } ] }, "success": true, "message": "Product added to cart successfully", "sessionToken": null, "isGuest": false, "itemsQty": 2, "itemsCount": 2 } } } } commonErrors: - error: INVALID_DATE_RANGE cause: date_from is after date_to or dates are in the past solution: Ensure date_from is before date_to and both are future dates - id: add-rental-booking-product-hourly-to-cart title: Add Rental Booking Product to Cart (Hourly) description: | Add a rental booking product with hourly renting to the cart. For hourly rentals, the `booking` JSON requires `renting_type` set to "hourly", a `date`, and a `slot` (time range string). The API converts the time range to timestamps automatically. query: | mutation createAddProductInCart( $productId: Int! $booking: String! $quantity: Int ) { createAddProductInCart( input: { productId: $productId quantity: $quantity booking: $booking } ) { addProductInCart { id _id cartToken subtotal grandTotal formattedSubtotal formattedGrandTotal items { totalCount edges { cursor node { id cartId productId name sku quantity price total type formattedPrice formattedTotal productUrlKey canChangeQty options } } } success message sessionToken isGuest itemsQty itemsCount } } } variables: | { "productId": 2510, "quantity": 1, "booking": "{\"type\":\"rental\",\"renting_type\":\"hourly\",\"date\":\"2026-04-22\",\"slot\":\"12:00 PM - 01:00 PM\"}" } response: | { "data": { "createAddProductInCart": { "addProductInCart": { "id": "4676", "_id": 4676, "cartToken": "4676", "subtotal": 204, "grandTotal": 204, "formattedSubtotal": "$204.00", "formattedGrandTotal": "$204.00", "items": { "totalCount": 1, "edges": [ { "cursor": "MA==", "node": { "id": "5871", "cartId": 4676, "productId": 2510, "name": "Wooden Folding Chair Rental", "sku": "WOODEN-FOLDING-CHAIR-RENTAL", "quantity": 1, "price": 204, "total": 204, "type": "booking", "formattedPrice": "$204.00", "formattedTotal": "$204.00", "productUrlKey": "wooden-folding-chair-rental", "canChangeQty": true, "options": { "edges": [ { "node": { "option_id": 0, "option_label": "Hourly Basis", "attribute_name": "Rent Type" } }, { "node": { "option_id": 0, "option_label": "22 April, 2026 12:00 PM", "attribute_name": "Rent From" } }, { "node": { "option_id": 0, "option_label": "22 April, 2026 01:00 PM", "attribute_name": "Rent Till" } } ] } } } ] }, "success": true, "message": "Product added to cart successfully", "sessionToken": null, "isGuest": false, "itemsQty": 1, "itemsCount": 1 } } } } commonErrors: - error: SLOT_NOT_AVAILABLE cause: The selected hourly slot is not available on the chosen date solution: Choose a different time slot or date - id: add-event-booking-product-to-cart title: Add Event Booking Product to Cart description: | Add an event booking product to the cart. The `booking` JSON requires `type` set to "event" and `qty` — an object mapping ticket type IDs to their quantities. At least one ticket must have a quantity greater than 0. query: | mutation createAddProductInCart( $productId: Int! $booking: String! $quantity: Int ) { createAddProductInCart( input: { productId: $productId quantity: $quantity booking: $booking } ) { addProductInCart { id _id cartToken subtotal grandTotal formattedSubtotal formattedGrandTotal items { totalCount edges { cursor node { id cartId productId name sku quantity price total type formattedPrice formattedTotal productUrlKey canChangeQty options } } } success message sessionToken isGuest itemsQty itemsCount } } } variables: | { "productId": 2508, "quantity": 1, "booking": "{\"type\":\"event\",\"qty\":{\"7\":2,\"8\":1}}" } response: | { "data": { "createAddProductInCart": { "addProductInCart": { "id": "4680", "_id": 4680, "cartToken": "4680", "subtotal": 710, "grandTotal": 710, "formattedSubtotal": "$710.00", "formattedGrandTotal": "$710.00", "items": { "totalCount": 2, "edges": [ { "cursor": "MA==", "node": { "id": "5879", "cartId": 4680, "productId": 2508, "name": "Live Music Concert Ticket", "sku": "LIVE-MUSIC-CONCERT-TICKET", "quantity": 2, "price": 235, "total": 470, "type": "booking", "formattedPrice": "$235.00", "formattedTotal": "$470.00", "productUrlKey": "live-music-concert-ticket", "canChangeQty": false, "options": { "edges": [ { "node": { "option_id": 0, "option_label": "Standard Entry Ticket", "attribute_name": "Event Ticket" } }, { "node": { "option_id": 0, "option_label": "06 April, 2026", "attribute_name": "Event From" } }, { "node": { "option_id": 0, "option_label": "30 April, 2026", "attribute_name": "Event Till" } } ] } } }, { "cursor": "MQ==", "node": { "id": "5880", "cartId": 4680, "productId": 2508, "name": "Live Music Concert Ticket", "sku": "LIVE-MUSIC-CONCERT-TICKET", "quantity": 1, "price": 240, "total": 240, "type": "booking", "formattedPrice": "$240.00", "formattedTotal": "$240.00", "productUrlKey": "live-music-concert-ticket", "canChangeQty": false, "options": { "edges": [ { "node": { "option_id": 0, "option_label": "VIP Access Ticket", "attribute_name": "Event Ticket" } }, { "node": { "option_id": 0, "option_label": "06 April, 2026", "attribute_name": "Event From" } }, { "node": { "option_id": 0, "option_label": "30 April, 2026", "attribute_name": "Event Till" } } ] } } } ] }, "success": true, "message": "Product added to cart successfully", "sessionToken": null, "isGuest": false, "itemsQty": 3, "itemsCount": 2 } } } } commonErrors: - error: INVALID_TICKET_QTY cause: All ticket quantities are 0 or no ticket quantities provided solution: At least one ticket type must have a quantity greater than 0 - error: TICKETS_SOLD_OUT cause: Requested quantity exceeds available tickets solution: Reduce ticket quantity or choose a different ticket type - id: add-table-booking-product-to-cart title: Add Table Booking Product to Cart description: | Add a table booking product to the cart. The `booking` JSON requires `type` set to "table", a `date`, and a `slot`. Table bookings also require a `bookingNote` (or `specialNote`) for special requests — this is mandatory for table reservations. query: | mutation createAddProductInCart( $productId: Int! $booking: String! $quantity: Int $specialNote: String ) { createAddProductInCart( input: { productId: $productId quantity: $quantity booking: $booking bookingNote: $specialNote } ) { addProductInCart { id _id cartToken subtotal grandTotal formattedSubtotal formattedGrandTotal items { totalCount edges { cursor node { id cartId productId name sku quantity price total type formattedPrice formattedTotal productUrlKey canChangeQty options } } } success message sessionToken isGuest itemsQty itemsCount } } } variables: | { "productId": 2511, "quantity": 2, "booking": "{\"type\":\"table\",\"date\":\"2026-04-15\",\"slot\":\"12:00 PM - 12:45 PM\"}", "specialNote": "Table near a window is much appreciated" } response: | { "data": { "createAddProductInCart": { "addProductInCart": { "id": "4682", "_id": 4682, "cartToken": "4682", "subtotal": 390, "grandTotal": 390, "formattedSubtotal": "$390.00", "formattedGrandTotal": "$390.00", "items": { "totalCount": 1, "edges": [ { "cursor": "MA==", "node": { "id": "5883", "cartId": 4682, "productId": 2511, "name": "Fine Dining Table Reservation", "sku": "FINE-DINING-TABLE-RESERVATION", "quantity": 2, "price": 195, "total": 390, "type": "booking", "formattedPrice": "$195.00", "formattedTotal": "$390.00", "productUrlKey": "fine-dining-table-reservation", "canChangeQty": true, "options": { "edges": [ { "node": { "option_id": 0, "option_label": "15th Apr, 2026 12:00 PM", "attribute_name": "Booking From" } }, { "node": { "option_id": 0, "option_label": "15th Apr, 2026 12:45 PM", "attribute_name": "Booking Till" } }, { "node": { "option_id": 0, "option_label": "Table near a window is much appreciated", "attribute_name": "Special Request/Notes" } } ] } } } ] }, "success": true, "message": "Product added to cart successfully", "sessionToken": null, "isGuest": false, "itemsQty": 2, "itemsCount": 1 } } } } commonErrors: - error: MISSING_BOOKING_NOTE cause: Table bookings require a note/special request solution: Provide a bookingNote or specialNote parameter - error: SLOT_NOT_AVAILABLE cause: The selected table slot is already reserved solution: Choose a different time slot or date --- # Add to Cart ## About The `createAddProductInCart` mutation adds a product to a customer's shopping cart. Use this mutation to: - Add products from product detail pages to cart - Implement "Add to Cart" buttons and flows - Add products programmatically from search results - Handle quantity selection and product-specific options - Support all product types: simple, virtual, configurable, downloadable, grouped, bundle, and booking This mutation validates product availability, applies applicable pricing rules, and updates the cart totals. It returns the complete updated cart state including all items, pricing, and tax information. ## Authentication This mutation supports both authenticated customers and guest users: - **Authenticated customers**: Provide a valid customer authentication token in the `Authorization` header. Obtain this token via the [Customer Login API](/api/graphql-api/shop/mutations/customer-login). - **Guest users**: Provide the Guest Cart Token `cartToken` obtained from the [Create Cart mutation](/api/graphql-api/shop/mutations/create-cart). ``` Authorization: Bearer ``` ## Input Arguments | Argument | Type | Required | Description | |----------|------|----------|-------------| | `productId` | `Int!` | Yes | The product ID to add to the cart. | | `quantity` | `Int` | No | Number of units to add. Defaults to `1` if omitted. | | `selectedConfigurableOption` | `Int` | Configurable only | The child variant product ID for configurable products. | | `superAttribute` | `Iterable` | Configurable only | Array of attribute-option mappings, e.g., `[{"23": 2}, {"24": 8}]` where keys are attribute IDs and values are option IDs. | | `links` | `Iterable` | Downloadable only | Array of downloadable link IDs the customer wants to purchase, e.g., `[1, 2]`. | | `groupedQty` | `String` | Grouped only | JSON string mapping each associated product ID to its quantity, e.g., `"{\"1\":1,\"3\":2}"`. All associated products must be included. | | `bundleOptions` | `String` | Bundle only | JSON string mapping bundle option IDs to arrays of selected product IDs, e.g., `"{\"1\":[1],\"2\":[2]}"`. | | `bundleOptionQty` | `String` | Bundle only | JSON string mapping bundle option IDs to their quantities, e.g., `"{\"2\":3,\"3\":4}"`. Only applies to radio/select options; checkbox/multiselect quantities are fixed by admin. | | `booking` | `String` | Booking only | JSON string containing booking details. Structure varies by booking type (see [Booking Input Reference](#booking-input-reference) below). | | `bookingNote` | `String` | Table booking only | Special note or request for table bookings. Required for table booking type. | | `isBuyNow` | `Int` | No | Set to `1` for buy-now flow (creates a new cart for immediate checkout). Default: `0`. | ## Input Requirements by Product Type | Product Type | Required Fields | Optional Fields | |---|---|---| | **Simple** | `productId` | `quantity` | | **Virtual** | `productId` | `quantity` | | **Configurable** | `productId`, `selectedConfigurableOption`, `superAttribute` | `quantity` | | **Downloadable** | `productId`, `links` | `quantity` | | **Grouped** | `productId`, `groupedQty` | `quantity` | | **Bundle** | `productId`, `bundleOptions` | `quantity`, `bundleOptionQty` | | **Booking** | `productId`, `booking` | `quantity`, `bookingNote` | ## Booking Input Reference The `booking` field accepts a JSON string whose structure varies by booking type. The API automatically converts human-readable time range strings (e.g., `"12:00 PM - 08:00 PM"`) to Unix timestamps internally. | Booking Type | JSON Structure | Example | |---|---|---| | **Default** | `{"type":"default","date":"YYYY-MM-DD","slot":"HH:MM AM - HH:MM PM"}` | `{"type":"default","date":"2026-03-25","slot":"12:00 PM - 08:00 PM"}` | | **Appointment** | `{"type":"appointment","date":"YYYY-MM-DD","slot":"HH:MM AM - HH:MM PM"}` | `{"type":"appointment","date":"2026-03-28","slot":"12:00 PM - 12:45 PM"}` | | **Rental (Daily)** | `{"type":"rental","renting_type":"daily","date_from":"YYYY-MM-DD","date_to":"YYYY-MM-DD"}` | `{"type":"rental","renting_type":"daily","date_from":"2026-03-24","date_to":"2026-03-26"}` | | **Rental (Hourly)** | `{"type":"rental","renting_type":"hourly","date":"YYYY-MM-DD","slot":"HH:MM AM - HH:MM PM"}` | `{"type":"rental","renting_type":"hourly","date":"2026-03-19","slot":"11:00 AM - 12:00 PM"}` | | **Event** | `{"type":"event","qty":{"ticketId":qty,...}}` | `{"type":"event","qty":{"1":1,"2":1}}` | | **Table** | `{"type":"table","date":"YYYY-MM-DD","slot":"HH:MM AM - HH:MM PM"}` + `bookingNote` param | `{"type":"table","date":"2026-03-25","slot":"12:00 PM - 12:45 PM"}` | ::: info Booking Type Notes - **Event bookings**: At least one ticket type must have a quantity greater than 0. - **Table bookings**: The `bookingNote` input parameter is required. It is passed separately from the `booking` JSON. - **Rental bookings**: Daily rentals use `date_from`/`date_to`; hourly rentals use `date`/`slot`. - **Slot format**: Time ranges like `"12:00 PM - 08:00 PM"` are automatically parsed and converted to Unix timestamps by the API. ::: ## Cart Item `options` Field The `options` field on each cart item contains product-specific selection details. This data is essential for displaying what the customer selected on the cart page. The structure varies by product type: | Product Type | Options Content | Example | |---|---|---| | **Simple** | Not present — no additional options needed | — | | **Virtual** | Not present — no additional options needed. `haveStockableItems` is `false` as virtual products have no physical shipment. | — | | **Configurable** | Selected attribute values (color, size, etc.) | `[{"id":"23","label":"Color","value":"Blue","option_id":"2"}]` | | **Downloadable** | Selected downloadable links | `[{"link_id":"1","title":"PDF Version","price":15}]` | | **Grouped** | Individual product quantities | `[{"product_id":"101","name":"Product A","qty":2}]` | | **Bundle** | Selected bundle options and quantities | `[{"option_id":"1","label":"Mouse","can_change_qty":true,"value":[...]}]` | | **Booking** | Booking date, slot, and note details | `[{"label":"Date","value":"2026-03-25"},{"label":"Slot","value":"12:00 PM - 12:45 PM"}]` | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `id` | `ID!` | Cart identifier in IRI format. | | `_id` | `Int!` | Numeric cart ID. | | `cartToken` | `String!` | Cart session token for guest users. | | `customerId` | `Int` | Customer ID (null for guest users). | | `channelId` | `Int!` | Channel the cart belongs to. | | `subtotal` | `String!` | Cart subtotal before tax and shipping. | | `grandTotal` | `String!` | Final cart total including tax and discounts. | | `discountAmount` | `String` | Total discount applied. | | `taxAmount` | `String` | Total tax amount. | | `shippingAmount` | `String` | Shipping cost. | | `couponCode` | `String` | Applied coupon code, if any. | | `items` | `CartItemConnection!` | Paginated collection of cart items. | | `items.totalCount` | `Int!` | Total number of items in cart. | | `items.edges.node` | `CartItem!` | Individual cart item with product details, pricing, and options. | | `success` | `Boolean!` | Whether the product was added successfully. | | `message` | `String!` | Success or error message. | | `sessionToken` | `String` | Session token for cart persistence. | | `isGuest` | `Boolean!` | Whether the cart belongs to a guest user. | | `itemsQty` | `Int!` | Total quantity of all items in cart. | | `itemsCount` | `Int!` | Number of distinct items in cart. | | `haveStockableItems` | `Boolean!` | Whether cart contains physical/shippable items. | ## Best Practices 1. **Use Product-Specific Fields** — Only pass the input fields relevant to the product type being added (e.g., don't pass `booking` for simple products) 2. **Handle Options in Cart Display** — Use the `options` field on cart items to display what the customer selected (variant attributes, bundle choices, booking dates, etc.) 3. **Check `success` Field** — Always check the `success` boolean and `message` in the response to handle errors gracefully 4. **Persist Session Token** — Store the `sessionToken`/`cartToken` returned for guest users to maintain cart state across requests 5. **Validate Booking Slots** — For booking products, retrieve available slots from the product query before adding to cart 6. **Bundle Qty Restrictions** — For bundle products, note that checkbox/multiselect option quantities are admin-controlled and cannot be changed by customers ## Related Resources - [Create Cart](/api/graphql-api/shop/mutations/create-cart) - Create a new cart (required for guest users) - [Update Cart Item](/api/graphql-api/shop/mutations/update-cart-item) - Update quantity of items in cart - [Remove Cart Item](/api/graphql-api/shop/mutations/remove-cart-item) - Remove items from cart - [Get Cart](/api/graphql-api/shop/queries/get-cart) - Retrieve current cart state - [Single Product](/api/graphql-api/shop/queries/get-product) - Get product details and available options --- # Apply Coupon URL: /api/graphql-api/shop/mutations/apply-coupon --- outline: false examples: - id: apply-coupon-to-cart title: Apply Coupon Code to Cart description: Apply a valid coupon code to reduce cart total. query: | mutation createApplyCoupon ( $couponCode: String! ) { createApplyCoupon(input: { couponCode: $couponCode }) { applyCoupon { id discountAmount grandTotal } } } variables: | { "couponCode": "BIRTHDAY20" } response: | { "data": { "createApplyCoupon": { "applyCoupon": { "id": "5163", "discountAmount": 200, "grandTotal": 4800 } } } } commonErrors: - error: INVALID_COUPON cause: Coupon code does not exist solution: Verify coupon code spelling - error: COUPON_EXPIRED cause: Coupon validity period has ended solution: Use an active coupon - error: MINIMUM_ORDER_NOT_MET cause: Cart total doesn't meet minimum requirement solution: Add more items to cart --- # Apply Coupon ## About The `applyCoupon` mutation applies a promotional coupon code to a shopping cart. Use this mutation to: - Apply discount codes to reduce cart total - Enable promo/coupon code functionality - Implement "Apply Coupon" features on cart page - Validate coupon eligibility - Display discount calculations - Support promotional campaigns This mutation validates coupon code, checks eligibility conditions, and recalculates cart totals with applied discount. ## Authentication This mutation supports both authenticated customers and guest users: - **Authenticated customers**: Provide a valid customer authentication token in the `Authorization` header. Obtain this token via the [Customer Login API](/api/graphql-api/shop/mutations/customer-login). - **Guest users**: Provide the Guest Cart Token `cartToken` obtained from the [Create Cart mutation](/api/graphql-api/shop/mutations/create-cart). ``` Authorization: Bearer ``` ## Arguments | Argument | Type | Description | |----------|------|-------------| | `couponCode` | `String!` | Promotional coupon code to apply. | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `applyCoupon` | `Coupon!` | Return coupon and cart related values. | | `applyCoupon.id` | `String` | id of the cart. | | `applyCoupon.discountAmount` | `Float` | Total discount from all applied coupons. | | `applyCoupon.grandTotal` | `Float!` | Recalculated cart total with discount. | | `errors` | `[ErrorMessage!]` | Validation errors if application failed. | --- # Cancel Customer Order URL: /api/graphql-api/shop/mutations/cancel-customer-order --- outline: false examples: - id: cancel-customer-order-success title: Cancel a Pending Customer Order description: Cancel a customer order that is in pending status, restoring inventory to stock. query: | mutation { createCancelOrder(input: { orderId: 2 }) { cancelOrder { success message orderId status } } } variables: | {} response: | { "data": { "createCancelOrder": { "cancelOrder": { "success": true, "message": "Order has been canceled successfully", "orderId": 2, "status": "canceled" } } } } commonErrors: - error: UNAUTHENTICATED cause: Missing or invalid Bearer token solution: Login and provide a valid customer authentication token - error: NOT_FOUND cause: Order with specified ID does not exist or does not belong to the customer solution: Verify the order ID and ensure it belongs to the authenticated customer - error: CANNOT_CANCEL cause: Order cannot be canceled (already completed, shipped, or canceled) solution: Check order status - only pending orders can be canceled - id: cancel-customer-order-blocked title: Cancel Order - Already Canceled (Error) description: Attempt to cancel an order that is already canceled (should fail). query: | mutation { createCancelOrder(input: { orderId: 2 }) { cancelOrder { success message orderId status } } } variables: | {} response: | { "data": { "createCancelOrder": { "cancelOrder": { "success": false, "message": "Order cannot be canceled. It may have already been processed, shipped, or canceled", "orderId": 2, "status": "canceled" } } } } commonErrors: - error: CANCEL_NOT_ALLOWED cause: Order status does not allow cancellation solution: Only orders with pending status can be canceled --- # Cancel Customer Order ## Overview The Cancel Order API allows authenticated customers to cancel their pending orders. This mutation validates order ownership, checks cancellation eligibility, and reverses inventory allocation when successful. ## Endpoint **GraphQL Mutation URL**: `POST /api/graphql` ## Authentication **Required**: Bearer Token (Sanctum) **Scope**: Customer only (order must belong to authenticated customer) ## Request ### GraphQL Mutation Schema ```graphql mutation CreateCancelOrder($input: CancelOrderInput!) { createCancelOrder(input: $input) { cancelOrder { success # Boolean: true if cancel succeeded message # String: response message orderId # Int: the order ID status # String: final order status } } } ``` ### Input Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `orderId` | Integer | Yes | The numeric ID of the order to cancel | ## Response ### Success Response (200 OK) ```json { "data": { "createCancelOrder": { "cancelOrder": { "success": true, "message": "Order has been canceled successfully", "orderId": 2, "status": "canceled" } } } } ``` ### Failure Response - Cannot Cancel When order cannot be canceled (already canceled, shipped, or completed): ```json { "data": { "createCancelOrder": { "cancelOrder": { "success": false, "message": "Order cannot be canceled. It may have already been processed, shipped, or canceled", "orderId": 2, "status": "canceled" } } } } ``` ### Error Response - Unauthorized (401) ```json { "errors": [ { "message": "Unauthenticated", "extensions": { "code": "UNAUTHENTICATED" } } ] } ``` ### Error Response - Order Not Found (404) When order ID doesn't exist or belongs to a different customer: ```json { "data": { "createCancelOrder": null } } ``` ## Business Logic ### Cancellation Eligibility An order can be canceled if: - Order status is `pending` (not shipped, completed, or already canceled) - Order belongs to the authenticated customer - Order is not a composite/grouped order (or all child items are cancelable) ### When Cancellation Succeeds 1. **Order Status**: Changed from `pending` → `canceled` 2. **Inventory**: All ordered items are returned to stock 3. **Qty Canceled**: Updated to match ordered quantity for all items 4. **Events Fired**: - `order.cancel.before` — before cancel operations - `order.cancel.after` — after successful cancellation 5. **Database**: Order and item records updated atomically ### When Cancellation Fails - Order status remains unchanged - No inventory is returned - Appropriate error message returned ## State Transitions ``` pending → canceled (valid) shipped → (cannot cancel) completed → (cannot cancel) canceled → (cannot re-cancel) ``` ## Use Cases - Cancel unwanted orders before processing - Allow customers to self-service cancel pending orders - Automatically restore inventory when customers cancel - Reduce support requests by enabling customer-driven cancellation - Workflow: Customer views order → clicks cancel → inventory restored ## Testing Examples ### Test Case 1: Cancel Pending Order (Success) **Precondition**: Order #2 with status `pending` exists for customer #2 **Query**: ```graphql mutation { createCancelOrder(input: { orderId: 2 }) { cancelOrder { success message orderId status } } } ``` **Expected Result**: ```json { "success": true, "message": "Order has been canceled successfully", "orderId": 2, "status": "canceled" } ``` **Database Verification**: ```sql SELECT id, status FROM orders WHERE id = 2; -- Result: id=2, status='canceled' SELECT order_id, qty_ordered, qty_canceled FROM order_items WHERE order_id = 2; -- Result: qty_canceled should equal qty_ordered for all items ``` ### Test Case 2: Re-Cancel Already Canceled Order (Failure) **Precondition**: Order #2 with status `canceled` (from Test Case 1) **Query**: ```graphql mutation { createCancelOrder(input: { orderId: 2 }) { cancelOrder { success message orderId status } } } ``` **Expected Result**: ```json { "success": false, "message": "Order cannot be canceled. It may have already been processed, shipped, or canceled", "orderId": 2, "status": "canceled" } ``` **No Database Changes**: Order remains in `canceled` status ### Test Case 3: Cancel Without Authentication (Failure) **Query** (without Authorization header): ```bash curl -X POST https://api-demo.bagisto.com/api/graphql \ -H "Content-Type: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_qrr4vsdbs6xNpL7DN0GHUcB0XnhjnjIS" \ -d '{"query":"mutation { createCancelOrder(input: { orderId: 2 }) { cancelOrder { success message } } }"}' ``` **Expected Result**: ``` HTTP 401 Unauthorized { "errors": [ { "message": "Unauthenticated", "extensions": {"code": "UNAUTHENTICATED"} } ] } ``` ## Implementation Details ### Files Involved | File | Purpose | |------|---------| | `CancelOrderInput.php` | Input DTO with orderId parameter | | `CancelOrderProcessor.php` | Handles mutation business logic | | `CancelOrder.php` | Response model with success/message/orderId/status | ### Key Components **CancelOrderProcessor**: - Authenticates request via `Auth::guard('sanctum')` - Scopes orders to authenticated customer only - Delegates to `OrderRepository::cancel()` for business logic - Returns `CancelOrder` response model **OrderRepository::cancel()**: - Validates `$order->canCancel()` method - Fires `order.cancel.before` event - Iterates order items and returns inventory - Updates `qty_canceled` for each item - Calls `updateOrderStatus('canceled')` - Fires `order.cancel.after` event ## HTTP Status Codes | Code | Scenario | |------|----------| | 200 | Mutation executed (check `success` field for actual result) | | 400 | Invalid request (missing storefront key, malformed query) | | 401 | Unauthorized (missing or invalid Bearer token) | | 422 | Validation error (invalid orderId type) | ## Rate Limiting No specific rate limiting for cancel mutations. General API rate limits apply per storefront key. ## Notes - **Customer isolation**: Orders are scoped to the authenticated customer. A customer cannot cancel another customer's order. - **Inventory restoration**: When an order is canceled, all items are returned to inventory automatically. - **Status tracking**: The operation is governed by Bagisto's order cancellation rules and configuration. - **Event hooks**: Extensions can listen to `order.cancel.before` and `order.cancel.after` events. - **Localization**: All messages are localized to the customer's language. ## Related Resources - [Get Customer Orders](/api/graphql-api/shop/queries/get-customer-orders) — Query all customer orders - [Get Customer Order](/api/graphql-api/shop/queries/get-customer-order) — Query a specific order before canceling - [Reorder Customer Order](/api/graphql-api/shop/mutations/reorder-customer-order) — Re-add items from a canceled order - [Customer Login](/api/graphql-api/shop/mutations/customer-login) — Obtain authentication token - [Place Order](/api/graphql-api/shop/mutations/place-order) — Create new orders --- # Create Cart URL: /api/graphql-api/shop/mutations/create-cart --- outline: false examples: - id: create-cart-simple title: Create Simple Cart description: Create a new shopping cart session. query: | mutation createCart { createCartToken(input: {}) { cartToken { id _id cartToken customerId channelId itemsCount subtotal baseSubtotal discountAmount baseDiscountAmount taxAmount baseTaxAmount shippingAmount baseShippingAmount grandTotal baseGrandTotal formattedSubtotal formattedDiscountAmount formattedTaxAmount formattedShippingAmount formattedGrandTotal couponCode success message sessionToken isGuest } } } variables: | {} response: | { "data": { "createCartToken": { "cartToken": { "id": "4484", "_id": 4484, "cartToken": "4484", "customerId": 122, "channelId": 1, "itemsCount": 1, "subtotal": 4500, "baseSubtotal": 4500, "discountAmount": 0, "baseDiscountAmount": 0, "taxAmount": 0, "baseTaxAmount": 0, "shippingAmount": 0, "baseShippingAmount": 0, "grandTotal": 4500, "baseGrandTotal": 4500, "formattedSubtotal": "$4,500.00", "formattedDiscountAmount": "$0.00", "formattedTaxAmount": "$0.00", "formattedShippingAmount": "$0.00", "formattedGrandTotal": "$4,500.00", "couponCode": null, "success": true, "message": "Using authenticated customer cart", "sessionToken": null, "isGuest": false } } } } commonErrors: - error: CART_CREATION_FAILED cause: Unable to create cart session solution: Try again or check server logs --- # Create Cart ## About The `createCart` mutation creates a new shopping cart session. Use this mutation to: - Initialize a new shopping cart for checkout flows - Generate a unique cart token for session tracking - Start the shopping and checkout process - Create guest carts without customer authentication - Reset or recover abandoned carts - Manage multiple concurrent cart sessions This mutation returns a unique cart token that identifies the cart session. This token must be used in subsequent cart operations (add items, update, checkout). > **Note:** This mutation is primarily intended for **guest (non-logged-in) users**. It generates a `sessionToken` that must be passed as the `Authorization` header in all subsequent cart operations (e.g. add to cart, get cart). Authenticated customers already have a cart session tied to their account, so they do not need to call this mutation. ## Authentication This query requires a valid customer authentication token in the `Authorization` header. Use the [Customer Login API](/api/graphql-api/shop/mutations/customer-login) to retrieve the token. For guest users, no authentication is required. The mutation returns a unique cart token that must be used as the `Authorization` header in subsequent cart operations. ``` Authorization: Bearer ``` ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `cartToken` | `String!` | Unique cart session token. Use this in all subsequent cart operations. | | `id` | `String!` | Unique cart identifier. | | `message` | `String!` | Success or error message. | | `success` | `Boolean!` | Indicates if cart creation was successful. | | `isGuest` | `Boolean!` | Indicates whether the cart is for a guest user (`true`) or an authenticated customer (`false`). | | `formattedGrandTotal` | `String` | Current cart grand total with currency symbol. | | `errors` | `[ErrorMessage!]` | Array of validation or processing errors if applicable. | --- # Create Compare Item URL: /api/graphql-api/shop/mutations/create-compare-item --- outline: false examples: - id: create-compare-item-basic title: Add Product to Compare List description: Add a product to the authenticated customer's comparison list. The customer is auto-detected from the Bearer token. query: | mutation CreateCompareItem($input: createCompareItemInput!) { createCompareItem(input: $input) { compareItem { id _id createdAt updatedAt product { id _id sku type name description shortDescription price formattedPrice minimumPrice formattedMinimumPrice maximumPrice formattedMaximumPrice guestCheckout locale channel } customer { id firstName lastName gender dateOfBirth } } } } variables: | { "input": { "productId": 2514 } } response: | { "data": { "createCompareItem": { "compareItem": { "id": "/api/shop/compare_items/38", "_id": 38, "createdAt": "2026-04-06T18:50:10+05:30", "updatedAt": "2026-04-06T18:50:10+05:30", "product": { "id": "/api/shop/compare_items/38", "_id": 2514, "sku": "SP-003", "type": "simple", "name": "Arctic Touchscreen Winter Gloves", "description": "Introducing the Arctic Touchscreen Winter Gloves – where warmth, style, and connectivity meet to enhance your winter experience. Crafted from high-quality acrylic, these gloves are designed to provide exceptional warmth and durability.", "shortDescription": "Stay connected and warm with our Arctic Touchscreen Winter Gloves. These gloves are crafted from high-quality acrylic for warmth and durability with a touchscreen-compatible design.", "price": "21", "formattedPrice": "$21.00", "minimumPrice": "17", "formattedMinimumPrice": "$17.00", "maximumPrice": "17", "formattedMaximumPrice": "$17.00", "guestCheckout": "1", "locale": null, "channel": null }, "customer": { "id": "/api/shop/customers/122", "firstName": "John", "lastName": "Doe", "gender": "Male", "dateOfBirth": "1990-01-15" } } } } } commonErrors: - error: UNAUTHENTICATED cause: Missing or invalid Bearer token solution: Login and provide a valid customer authentication token - error: PRODUCT_NOT_FOUND cause: The product ID does not exist solution: Use a valid product ID that exists in the catalog - error: DUPLICATE_ITEM cause: This product is already in the comparison list solution: Check existing compare items before adding --- # Create Compare Item ## About The `createCompareItem` mutation adds a product to the authenticated customer's comparison list. Use this mutation to: - Add products to the compare list from product pages - Implement "Add to Compare" buttons - Build product comparison flows programmatically - Allow customers to compare product features side by side The customer is automatically detected from the Bearer token — no customer ID is needed in the input. ## Authentication This mutation requires customer authentication: - **Authenticated customers**: Provide a valid customer authentication token in the `Authorization` header. Obtain this token via the [Customer Login API](/api/graphql-api/shop/mutations/customer-login). ``` Authorization: Bearer ``` ## Arguments | Argument | Type | Description | |----------|------|-------------| | `productId` | `Int` | The ID of the product to add to the comparison list. | | `clientMutationId` | `String` | Optional client-side mutation identifier for tracking. | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `compareItem` | `CompareItem!` | The newly created compare item. | | `compareItem.id` | `ID!` | IRI identifier (e.g. `/api/shop/compare-items/607`). | | `compareItem._id` | `Int!` | Numeric identifier. | | `compareItem.product` | `Product!` | The associated product. | | `compareItem.customer` | `Customer!` | The authenticated customer. | | `compareItem.createdAt` | `String` | Timestamp when the item was added. | | `compareItem.updatedAt` | `String` | Timestamp when the item was last updated. | --- # Create Contact Us URL: /api/graphql-api/shop/mutations/create-contact-us --- outline: false examples: - id: create-contact-us-basic title: Create Contact Us - Basic description: Submit a contact us form with name, email, contact number, and message. query: | mutation createContactUs($input: createContactUsInput!) { createContactUs(input: $input) { contactUs { success message } } } variables: | { "input": { "name": "John Doe", "email": "john@example.com", "contact": "+1234567890", "message": "I have a question about your products" } } response: | { "data": { "createContactUs": { "contactUs": { "success": true, "message": "Your message has been submitted successfully. We will get back to you shortly." } } } } commonErrors: - error: input-required cause: Input parameter is missing or incomplete solution: Provide all required input fields (name, email, contact, message) - error: invalid-email cause: The email address provided is not valid solution: Use a properly formatted email address (e.g. john@example.com) - error: validation-error cause: One or more fields fail server-side validation solution: Ensure name and message are non-empty strings, and email is valid - id: create-contact-us-with-mutation-id title: Create Contact Us - With Client Mutation ID description: Submit a contact us form and track the mutation with a client-side identifier. query: | mutation createContactUs($input: createContactUsInput!) { createContactUs(input: $input) { contactUs { success message } clientMutationId } } variables: | { "input": { "name": "Jane Smith", "email": "jane.smith@example.com", "contact": "+0987654321", "message": "I would like to inquire about bulk order discounts for your clothing range.", "clientMutationId": "contact-form-001" } } response: | { "data": { "createContactUs": { "contactUs": { "success": true, "message": "Your message has been submitted successfully. We will get back to you shortly." }, "clientMutationId": "contact-form-001" } } } commonErrors: - error: input-required cause: Input parameter is missing or incomplete solution: Provide all required input fields (name, email, contact, message) - error: invalid-email cause: The email address provided is not valid solution: Use a properly formatted email address - error: validation-error cause: One or more input fields fail validation solution: Ensure all fields meet the required format and length constraints --- # Create Contact Us ## About The `createContactUs` mutation allows customers to submit a contact form enquiry to the store. Use this mutation to: - Submit customer enquiries from the storefront contact page - Send support requests to the store team - Allow guests (unauthenticated users) to reach out without an account - Collect customer contact details alongside their message - Track form submissions using `clientMutationId` - Integrate contact forms in custom storefronts or mobile apps This mutation does not require authentication — it is available to all visitors. ## Arguments | Argument | Type | Required | Description | |----------|------|----------|-------------| | `input` | `createContactUsInput!` | ✅ Yes | Input object containing all contact form fields. | ### Input Fields (`createContactUsInput`) | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | `String!` | ✅ Yes | Full name of the person submitting the enquiry. | | `email` | `String!` | ✅ Yes | Email address to reply to. Must be a valid email format. | | `contact` | `String!` | ✅ Yes | Contact phone number (e.g. `+1234567890`). | | `message` | `String!` | ✅ Yes | The enquiry or message body. | | `clientMutationId` | `String` | ❌ No | Optional client-side identifier for tracking the mutation request. | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `contactUs.success` | `Boolean!` | `true` if the contact form was submitted successfully. | | `contactUs.message` | `String!` | Human-readable confirmation or error message returned by the server. | | `clientMutationId` | `String` | Echoed back from the input if provided; useful for client-side request tracking. | ## Use Cases ### 1. Storefront Contact Page Trigger this mutation when a customer submits the contact form on `/contact-us`, then display the response `message` as a success or error notification. ### 2. Guest Enquiries Since no authentication is required, use this mutation for anonymous visitors who want to reach out without creating an account. ### 3. Mobile App Contact Form Integrate into a React Native or Flutter app to allow users to send support messages directly from within the app. ### 4. Track Submissions Client-Side Pass a unique `clientMutationId` (e.g. a UUID) to correlate the GraphQL response with the originating form submission in your frontend state. ### 5. Headless Storefronts Use in any headless commerce setup to power the contact form without relying on Bagisto's default frontend templates. ## Best Practices 1. **Validate on the client first** — Check email format and non-empty fields before calling the mutation to reduce unnecessary API calls 2. **Always check `success`** — Do not rely solely on the absence of errors; always read `contactUs.success` to confirm submission 3. **Display `message` to the user** — Show the server's response `message` as feedback so users know whether their enquiry was received 4. **Prevent duplicate submissions** — Disable the submit button after a successful response to avoid repeated submissions 5. **Rate limit on the server** — Ensure your Bagisto instance has rate limiting configured to protect this endpoint from abuse 6. **Use `clientMutationId` for tracking** — Pass a unique ID per submission if you need to correlate requests in analytics or error monitoring ## Related Resources - [Customer Registration](/api/graphql-api/shop/mutations/customer-registration) - Register a new customer account - [Customer Login](/api/graphql-api/shop/mutations/customer-login) - Authenticate a customer - [Shop API Overview](/api/graphql-api/shop-api) - Overview of all Shop API resources - [Best Practices](/api/graphql-api/best-practices) - GraphQL API best practices --- # Create Customer Address URL: /api/graphql-api/shop/mutations/create-customer-address --- outline: false examples: - id: create-customer-address title: Create Customer Address description: Create a new address for the authenticated customer. query: | mutation createCustomerAddress($input: createAddUpdateCustomerAddressInput!) { createAddUpdateCustomerAddress(input: $input) { addUpdateCustomerAddress{ id firstName lastName companyName vatId city state country phone addressId email address1 address2 postcode defaultAddress } } } variables: | { "input": { "firstName": "John", "lastName": "Doe", "companyName": "ANC Corporation", "vatId": "GB123456789", "email": "hello@example.com", "phone": "+918888888888", "address1": "123 Main Street", "address2": "NY", "postcode": "10001", "city": "New York", "state": "NY", "country": "US", "defaultAddress": false } } response: | { "data": { "createAddUpdateCustomerAddress": { "addUpdateCustomerAddress": { "id": "2851", "firstName": "John", "lastName": "Doe", "companyName": "ANC Corporation", "vatId": "GB123456789", "city": "New York", "state": "NY", "country": "US", "phone": "+918888888888", "addressId": 2851, "email": "hello@example.com", "address1": "123 Main Street", "address2": "NY", "postcode": "10001", "defaultAddress": false } } } } --- # Create Customer Address Create a new address for the authenticated customer. ## Authentication This query requires a valid customer authentication token in the `Authorization` header. Use the [Customer Login API](/api/graphql-api/shop/mutations/customer-login) to retrieve the token. ``` Authorization: Bearer ``` ## Arguments | Name | Type | Required | Description | |------|------|----------|-------------| | `firstName` | String | ✅ Yes | First name | | `lastName` | String | ✅ Yes | Last name | | `companyName` | String | ❌ No | Company name | | `vatId` | String | ❌ No | VAT identification number | | `email` | String | ✅ Yes | Email address | | `phone` | String | ✅ Yes | Phone number | | `address1` | String | ✅ Yes | Street address line 1 | | `address2` | String | ❌ No | Street address line 2 | | `city` | String | ✅ Yes | City | | `state` | String | ✅ Yes | State/Province | | `country` | String | ✅ Yes | Country code (ISO 3166-1 alpha-2) | | `postcode` | String | ✅ Yes | Postal/Zip code | | `defaultAddress` | Boolean | ❌ No | Set as default address | ## Response | Field | Type | Description | |-------|------|-------------| | `addUpdateCustomerAddress` | Address | Created address object | ## Validation Rules - First name and last name required - Complete address required - Valid country code must be provided - Postal code format depends on country - Phone number should be in valid format ## Related Documentation - [Get Customer Addresses](/api/graphql-api/shop/queries/get-customer-addresses) - [Update Customer Address](/api/graphql-api/shop/mutations/update-customer-address) - [Delete Customer Address](/api/graphql-api/shop/mutations/delete-customer-address) --- # Create Order URL: /api/graphql-api/shop/mutations/create-order --- outline: false examples: - id: create-order-basic title: Create Order from Cart description: Convert cart to order with billing and shipping information. query: | mutation createOrder($input: createOrderInput!) { createOrder(input: $input) { order { id orderNumber status total items { id productName quantity price } } message success } } variables: | { "input": { "cartId": "eyJpdiI6IjhWM...", "billingAddress": { "firstName": "John", "lastName": "Doe", "address": "123 Main St", "city": "New York", "state": "NY", "postalCode": "10001", "country": "US", "email": "john@example.com", "phone": "1234567890" }, "shippingAddress": { "firstName": "John", "lastName": "Doe", "address": "123 Main St", "city": "New York", "state": "NY", "postalCode": "10001", "country": "US" }, "shippingMethod": "flat_rate", "paymentMethod": "cashondelivery" } } response: | { "data": { "createOrder": { "order": { "id": "1", "orderNumber": "100001", "status": "pending", "total": 99.99, "items": [ { "id": "1", "productName": "Product Name", "quantity": 1, "price": 99.99 } ] }, "message": "Order created successfully", "success": true } } } commonErrors: - error: CART_EMPTY cause: Cart has no items solution: Add products to cart before checkout - error: INVALID_ADDRESS cause: Billing or shipping address invalid solution: Verify address information - error: PAYMENT_FAILED cause: Payment processing failed solution: Verify payment method details --- # Create Order ## About The `createOrder` mutation converts a cart into a finalized order. Use this mutation to: - Complete checkout process - Create orders from cart items - Collect billing and shipping information - Process payments - Apply shipping methods - Finalize customer orders - Handle order confirmation This mutation validates cart contents, addresses, payment method, and creates order with appropriate status. ## Arguments | Argument | Type | Description | |----------|------|-------------| | `input` | `createOrderInput!` | Order creation data. | | `input.cartId` | `String!` | Cart token to convert to order. | | `input.billingAddress` | `AddressInput!` | Billing address details. | | `input.shippingAddress` | `AddressInput!` | Shipping address details. | | `input.shippingMethod` | `String!` | Selected shipping method ID. | | `input.paymentMethod` | `String!` | Selected payment method. | | `input.customerNote` | `String` | Optional customer note/message. | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `order` | `Order!` | Created order object. | | `order.id` | `ID!` | Order ID. | | `order.orderNumber` | `String!` | Order reference number. | | `order.status` | `String!` | Initial order status. | | `order.total` | `Float!` | Order grand total. | | `order.items` | `[OrderItem!]!` | Order line items. | | `order.createdAt` | `DateTime!` | Order creation timestamp. | | `message` | `String!` | Success message. | | `success` | `Boolean!` | Indicates successful order creation. | | `errors` | `[ErrorMessage!]` | Validation errors if order failed. | --- # Create Product Review URL: /api/graphql-api/shop/mutations/create-product-review --- outline: false examples: - id: create-product-review-basic title: Create Product Review - Basic description: Create a basic product review with title, comment, and rating. query: | mutation createProductReview($input: createProductReviewInput!) { createProductReview(input: $input) { productReview { id _id name title rating comment status createdAt updatedAt } } } variables: | { "input": { "productId": 2511, "title": "Excellent quality and very stylish", "comment": "Very impressed with the EleganceKnits cardigan sweatercoat. The fabric feels premium and soft, the fitting is perfect, and the collar design adds a classy look. Suitable for office wear as well as casual outings. Lightweight yet warm. Highly recommended.", "rating": 5, "name": "John Doe", "email": "john.doe@example.com", "status": 0 } } response: | { "data": { "createProductReview": { "productReview": { "id": "/api/shop/reviews/92", "_id": 92, "name": "John Doe", "title": "Excellent quality and very stylish", "rating": 5, "comment": "Very impressed with the EleganceKnits cardigan sweatercoat. The fabric feels premium and soft, the fitting is perfect, and the collar design adds a classy look. Suitable for office wear as well as casual outings. Lightweight yet warm. Highly recommended.", "status": 0, "createdAt": "2024-12-26T10:30:45+05:30", "updatedAt": "2024-12-26T10:30:45+05:30" } } } } commonErrors: - error: input-required cause: Input parameter is missing solution: Provide all required input fields (productId, title, comment, rating, name) - error: invalid-product-id cause: Product ID is invalid or product does not exist solution: Use a valid product ID that exists in the system - error: invalid-rating cause: Rating value is out of valid range solution: Use rating between 1 and 5 - id: create-product-review-with-attachments title: Create Product Review - With Image Attachments description: Create a product review with Base64-encoded image attachments. query: | mutation createProductReview($input: createProductReviewInput!) { createProductReview(input: $input) { productReview { id _id name title rating comment status attachments createdAt updatedAt } } } variables: | { "input": { "productId": 2511, "title": "Great Product with Photos", "comment": "Here's the product with photos attached. The quality is excellent as you can see from the images.", "rating": 5, "name": "Jane Smith", "email": "jane.smith@example.com", "status": 0, "attachments": "[\"data:image/webp;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==\", \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==\"]" } } response: | { "data": { "createProductReview": { "productReview": { "id": "/api/shop/reviews/93", "_id": 93, "name": "Jane Smith", "title": "Great Product with Photos", "rating": 5, "comment": "Here's the product with photos attached. The quality is excellent as you can see from the images.", "status": 0, "attachments": "[{\"type\":\"image\",\"url\":\"https://api-demo.bagisto.com/storage/review/93/image1.webp\"},{\"type\":\"image\",\"url\":\"https://api-demo.bagisto.com/storage/review/93/image2.png\"}]", "createdAt": "2024-12-26T11:15:30+05:30", "updatedAt": "2024-12-26T11:15:30+05:30" } } } } commonErrors: - error: input-required cause: Input parameter is missing solution: Provide all required input fields - error: invalid-attachment-format cause: Attachment format is not valid Base64 encoded data solution: Provide attachments as valid Base64-encoded image or video data - error: invalid-product-id cause: Product ID is invalid or product does not exist solution: Use a valid product ID that exists in the system - error: attachment-size-exceeded cause: Attachment file size exceeds maximum allowed solution: Use smaller image or video files - id: create-product-review-complete title: Create Product Review - Complete with Metadata description: Create a product review with all fields including client mutation ID for tracking. query: | mutation createProductReview($input: createProductReviewInput!) { createProductReview(input: $input) { productReview { id _id name title rating comment status attachments createdAt updatedAt } clientMutationId } } variables: | { "input": { "productId": 2511, "title": "Professional Review with Attachments", "comment": "This is a detailed product review with multiple attachments including product photos and a video demonstration. The product quality exceeded my expectations.", "rating": 5, "name": "Tom Wilson", "email": "tom.wilson@example.com", "status": 1, "clientMutationId": "review-mutation-001", "attachments": "[\"data:image/webp;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==\", \"data:video/mp4;base64,AAAAIGZ0eXBpc29tAAACAGlzb21pc28yYXZjMW1wNDEAAAAIZnJlZQAACKw=\"]" } } response: | { "data": { "createProductReview": { "productReview": { "id": "/api/shop/reviews/94", "_id": 94, "name": "Tom Wilson", "title": "Professional Review with Attachments", "rating": 5, "comment": "This is a detailed product review with multiple attachments including product photos and a video demonstration. The product quality exceeded my expectations.", "status": 1, "attachments": "[{\"type\":\"image\",\"url\":\"https://api-demo.bagisto.com/storage/review/94/photo1.webp\"},{\"type\":\"video\",\"url\":\"https://api-demo.bagisto.com/storage/review/94/demo.mp4\"}]", "createdAt": "2024-12-26T12:45:20+05:30", "updatedAt": "2024-12-26T12:45:20+05:30" }, "clientMutationId": "review-mutation-001" } } } commonErrors: - error: input-required cause: Input parameter is missing solution: Provide the input object with all required fields - error: invalid-attachment-format cause: Attachment is not properly Base64 encoded solution: Ensure attachments are valid Base64-encoded data with proper MIME type prefix - error: invalid-product-id cause: Product ID is invalid or exceeds 32-bit integer range solution: Use a valid numeric product ID - error: invalid-rating cause: Rating is outside valid range solution: Use rating value between 1 and 5 - error: missing-required-field cause: Required field is missing (title, comment, rating, name) solution: Provide all required input fields --- # Create Product Review ## About The `createProductReview` mutation allows customers to submit product reviews with ratings, comments, and media attachments. Use this mutation to: - Submit product reviews from customers - Add images and videos as review attachments - Set review status (pending, approved, rejected) - Track review submissions with client mutation ID - Enable customer feedback on products - Build user-generated content on storefront - Collect product ratings and reviews This mutation supports Base64-encoded image and video attachments for rich media reviews. ## Arguments | Argument | Type | Required | Description | |----------|------|----------|-------------| | `title` | `String!` | ✅ Yes | Review title/headline. | | `comment` | `String!` | ✅ Yes | Review comment/text. | | `rating` | `Int!` | ✅ Yes | Star rating (1-5). | | `name` | `String!` | ✅ Yes | Reviewer's name. | | `email` | `String` | ❌ No | Reviewer's email address. | | `status` | `Int` | ❌ No | Review status (0 = pending, 1 = approved, 2 = rejected). Default: 0 (pending). | | `attachments` | `String` | ❌ No | JSON string array of Base64-encoded images/videos. | | `clientMutationId` | `String` | ❌ No | Optional client mutation tracking ID. | ## Input Fields Details ### productId - **Type**: Integer (32-bit signed) - **Required**: Yes - **Description**: The product ID being reviewed. Must exist in the system. - **Example**: `2511` ### title - **Type**: String - **Required**: Yes - **Description**: Review headline (typically 5-100 characters). - **Example**: `"Excellent quality and very stylish"` ### comment - **Type**: String - **Required**: Yes - **Description**: Full review text with detailed feedback. - **Example**: `"Very impressed with the product. Fabric feels premium..."` ### rating - **Type**: Integer (1-5) - **Required**: Yes - **Description**: Star rating (1 = lowest, 5 = highest). - **Valid Values**: 1, 2, 3, 4, 5 ### name - **Type**: String - **Required**: Yes - **Description**: Reviewer's name (how it appears on review). - **Example**: `"John Doe"` ### email - **Type**: String - **Required**: No - **Description**: Reviewer's email address. - **Example**: `"john.doe@example.com"` ### status - **Type**: Integer - **Required**: No - **Default**: 0 (Pending) - **Valid Values**: - `0` - Pending approval - `1` - Approved and visible - `2` - Rejected/hidden - **Description**: Initial review status. ### attachments - **Type**: JSON String Array - **Required**: No - **Format**: Array of Base64-encoded data URIs - **Supported MIME Types**: - Images: `image/webp`, `image/png`, `image/jpeg`, `image/jpg`, `image/gif` - Videos: `video/mp4`, `video/webm`, `video/quicktime` - **Description**: Media files attached to review (maximum 10 MB per file recommended) - **Example**: ```json [ "data:image/webp;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB...", "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAB...", "data:video/mp4;base64,AAAAIGZ0eXBpc29tAAACAG..." ] ``` ### clientMutationId - **Type**: String - **Required**: No - **Description**: Optional tracking ID for this mutation request. - **Example**: `"review-mutation-001"` ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `productReview` | `ProductReview!` | The created product review object. | | `productReview.id` | `ID!` | Unique review API identifier. | | `productReview._id` | `Int!` | Numeric review ID. | | `productReview.name` | `String!` | Reviewer's name. | | `productReview.title` | `String!` | Review title. | | `productReview.rating` | `Int!` | Star rating (1-5). | | `productReview.comment` | `String!` | Review text. | | `productReview.status` | `Int!` | Review status. | | `productReview.attachments` | `String` | JSON string of attachment objects with type and URL. | | `productReview.createdAt` | `DateTime` | Review creation timestamp. | | `productReview.updatedAt` | `DateTime` | Last update timestamp. | | `clientMutationId` | `String` | Echoed client mutation ID for tracking. | ## Attachments Format ### Input Format (Creating Review) - Must be a **JSON string** containing an array - Each item is a **Base64-encoded data URI** - Format: `"data:{MIME_TYPE};base64,{BASE64_DATA}"` **Example Input:** ```json "[\"data:image/webp;base64,iVBORw0KG...\", \"data:image/png;base64,iVBORw0KG...\"]" ``` ### Response Format (Retrieved Review) - Returned as a **JSON string** containing an array of objects - Each object has `type` (image/video) and `url` (file URL) **Example Response:** ```json "[{\"type\":\"image\",\"url\":\"https://api-demo.bagisto.com/storage/review/94/photo1.webp\"},{\"type\":\"video\",\"url\":\"https://api-demo.bagisto.com/storage/review/94/demo.mp4\"}]" ``` ## Review Status | Status | Description | Usage | |--------|-------------|-------| | `0` | Pending | Awaiting admin approval before display | | `1` | Approved | Published on product page | | `2` | Rejected | Hidden from public view | ## Use Cases ### 1. Customer Review Submission Use the "Basic" example for customers to submit reviews without attachments. ### 2. Review with Product Photos Use the "With Image Attachments" example for customers to upload product photos. ### 3. Detailed Review with Multiple Media Use the "Complete" example for detailed reviews with both images and videos. ### 4. Admin Review Creation Create reviews with `status: 1` for immediate publication. ## Best Practices 1. **Validate Input** - Ensure all required fields are provided before submission 2. **Optimize Attachments** - Use compressed images (WebP format recommended) 3. **File Size Limits** - Keep individual files under 10 MB 4. **Rating Validation** - Verify rating is between 1 and 5 5. **Content Moderation** - Set status to 0 (pending) by default for customer reviews 6. **Handle Errors** - Provide clear error messages for failed submissions 7. **Duplicate Prevention** - Implement client-side deduplication before submission 8. **Attachment Limits** - Limit number of attachments per review (typically 5-10 maximum) ## Error Scenarios ### Missing Input When `input` parameter is not provided, GraphQL returns validation error. ### Invalid Product ID When product ID doesn't exist or is in invalid format. ### Invalid Attachment Format When Base64 encoding is malformed or MIME type is unsupported. ### Missing Required Fields When any required field (title, comment, rating, name) is missing. ## Related Resources - [Get Product Reviews](/api/graphql-api/shop/queries/get-product-reviews) - Query product reviews - [Get Product Review](/api/graphql-api/shop/queries/get-product-review) - Query single review details - [Get Product](/api/graphql-api/shop/queries/get-product) - Query product details - [Mutations Guide](/api/graphql-api/shop/mutations) - Overview of shop mutations - [Shop API Overview](/api/graphql-api/shop-api) - Overview of Shop API resources --- # Create Review URL: /api/graphql-api/shop/mutations/create-review --- outline: false examples: - id: create-product-review title: Create Product Review description: Submit a review and rating for a purchased product. query: | mutation createReview($input: createProductReviewInput!) { createProductReview(input: $input) { productReview { id title comment rating name status createdAt } message success } } variables: | { "input": { "productId": "1", "title": "Great product!", "comment": "This product exceeded my expectations. Highly recommended!", "rating": 5, "name": "John Doe", "email": "john@example.com" } } response: | { "data": { "createProductReview": { "productReview": { "id": "1", "title": "Great product!", "comment": "This product exceeded my expectations. Highly recommended!", "rating": 5, "name": "John Doe", "status": "pending", "createdAt": "2025-12-19T10:30:00Z" }, "message": "Review submitted successfully", "success": true } } } commonErrors: - error: PRODUCT_NOT_FOUND cause: Product ID does not exist solution: Verify product ID - error: INVALID_RATING cause: Rating not between 1-5 solution: Use rating from 1 to 5 - error: DUPLICATE_REVIEW cause: Customer already reviewed this product solution: Update existing review instead --- # Create Review ## About The `createReview` mutation submits a product review with rating and feedback. Use this mutation to: - Enable customer product reviews - Collect customer feedback and ratings - Build social proof through reviews - Display customer testimonials - Improve products based on feedback - Increase engagement and trust This mutation validates input, checks for duplicate reviews, and creates review record (possibly pending moderation). ## Arguments | Argument | Type | Description | |----------|------|-------------| | `input` | `createProductReviewInput!` | Review submission data. | | `input.productId` | `ID!` | Product being reviewed. | | `input.title` | `String!` | Review title/summary. | | `input.comment` | `String!` | Detailed review comment. | | `input.rating` | `Int!` | Star rating from 1 to 5. | | `input.name` | `String!` | Reviewer name. | | `input.email` | `String!` | Reviewer email. | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `productReview` | `ProductReview!` | Created review object. | | `productReview.id` | `ID!` | Review ID. | | `productReview.title` | `String!` | Review title. | | `productReview.comment` | `String!` | Review comment. | | `productReview.rating` | `Int!` | Star rating (1-5). | | `productReview.name` | `String!` | Reviewer name. | | `productReview.status` | `String!` | Review status (pending, approved, rejected). | | `productReview.createdAt` | `DateTime!` | Review submission date. | | `message` | `String!` | Success message. | | `success` | `Boolean!` | Indicates successful submission. | | `errors` | `[ErrorMessage!]` | Validation errors if submission failed. | --- # Create Wishlist Item URL: /api/graphql-api/shop/mutations/create-wishlist --- outline: false examples: - id: create-wishlist-basic title: Add Product to Wishlist description: Add a product to the authenticated customer's wishlist. query: | mutation CreateWishlist($input: createWishlistInput!) { createWishlist(input: $input) { wishlist { id _id product { _id id name price } createdAt } } } variables: | { "input": { "productId": 2499 } } response: | { "data": { "createWishlist": { "wishlist": { "id": "/api/shop/wishlists/89", "_id": 89, "product": { "_id": 2499, "id": "/api/shop/wishlists/89", "name": "Ivory Frost Classic Overcoat XL", "price": "500" }, "createdAt": "2026-04-07T13:55:19+05:30" } } } } commonErrors: - error: UNAUTHENTICATED cause: Missing or invalid Bearer token solution: Login and provide a valid customer authentication token - error: PRODUCT_NOT_FOUND cause: The product ID does not exist solution: Use a valid product ID that exists in the catalog - error: DUPLICATE_ITEM cause: This product is already in the wishlist solution: Use the toggle mutation or check existing wishlist items --- # Create Wishlist Item ## About The `createWishlist` mutation adds a product to the authenticated customer's wishlist. Use this mutation to: - Add products to the wishlist from product pages - Implement "Add to Wishlist" buttons and heart icons - Save products for later purchase - Build wishlist flows programmatically The customer is automatically detected from the Bearer token — no customer ID is needed in the input. ## Authentication This mutation requires customer authentication: - **Authenticated customers**: Provide a valid customer authentication token in the `Authorization` header. Obtain this token via the [Customer Login API](/api/graphql-api/shop/mutations/customer-login). ``` Authorization: Bearer ``` ## Arguments | Argument | Type | Description | |----------|------|-------------| | `productId` | `Int` | The ID of the product to add to the wishlist. | | `clientMutationId` | `String` | Optional client-side mutation identifier for tracking. | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `wishlist` | `Wishlist!` | The newly created wishlist item. | | `wishlist.id` | `ID!` | IRI identifier (e.g. `/api/shop/wishlists/70`). | | `wishlist._id` | `Int!` | Numeric identifier. | | `wishlist.product` | `Product!` | The associated product with id, name, price. | | `wishlist.createdAt` | `String` | Timestamp when the item was added. | --- # Customer Login URL: /api/graphql-api/shop/mutations/customer-login --- outline: false examples: - id: customer-login title: Customer Login description: Authenticate a customer with email and password. query: | mutation createCustomerLogin( $email: String! $password: String! ) { createCustomerLogin( input: { email: $email password: $password } ) { customerLogin { id _id apiToken token success message } } } variables: | { "email": "john.doe@example.com", "password": "SecurePass@123" } response: | { "data": { "createCustomerLogin": { "customerLogin": { "id": "1", "_id": 1, "apiToken": "OOxDk2s06JCndg5FHb8WbfF6ZR8jGq23168m9gm37J9Cmz4xah8B8AFK0Cp95x...", "token": "1|xy56RHXcttcDnimTHVEGIeyxzMKAWX6MyICCsZTA7dc...", "success": true, "message": "You have logged in successfully" } } } } - id: customer-login-with-device-token title: Customer Login with Device Token description: Authenticate a customer with email, password and an FCM device token. Only applicable if the Bagisto Push Notification package is installed. query: | mutation createCustomerLogin( $email: String! $password: String! $deviceToken: String! ) { createCustomerLogin( input: { email: $email password: $password deviceToken: $deviceToken } ) { customerLogin { id _id apiToken token success message } } } variables: | { "email": "john.doe@example.com", "password": "SecurePass@123", "deviceToken": "your_fcm_device_token" } response: | { "data": { "createCustomerLogin": { "customerLogin": { "id": "1", "_id": 1, "apiToken": "OOxDk2s06JCndg5FHb8WbfF6ZR8jGq23168m9gm37J9Cmz4xah8B8AFK0Cp95x...", "token": "1|xy56RHXcttcDnimTHVEGIeyxzMKAWX6MyICCsZTA7dc...", "success": true, "message": "You have logged in successfully" } } } } --- # Customer Login Authenticate a customer account with email and password. > **Push Notifications:** The `deviceToken` field is only applicable if the [Bagisto Push Notification](https://bagisto.com/en/extensions/push-notifications-for-bagisto/) package is installed. Pass the FCM device token here to associate the device with the customer session for push notification delivery. If the package is not installed, this field can be omitted. ## Arguments | Name | Type | Required | Description | |------|------|----------|-------------| | `email` | String | ✅ Yes | Customer's email address | | `password` | String | ✅ Yes | Customer's password | | `deviceToken` | String | ❌ No | FCM device token for push notifications. Only required if the [Bagisto Push Notification](https://bagisto.com/en/extensions/push-notifications-for-bagisto/) package is installed. | ## Response | Field | Type | Description | |-------|------|-------------| | `createCustomerLogin` | Customer | The authenticated customer login object | | `token` | String | JWT token for API authentication | | `message` | String | Success or error message | | `success` | Boolean | Login success status | ## Token Usage Once logged in, use the `token` in the `Authorization` header for authenticated requests: ``` Authorization: Bearer ``` e.g. ``` "Authorization": "Bearer 220|KZhQuUwUzb6Z7j....." ``` ## Error Responses ```json { "errors": { "email": ["Invalid email or password."] } } ``` ## Related Documentation - [Customer Registration](/api/graphql-api/shop/mutations/customer-registration) - [Customer Logout](/api/graphql-api/shop/mutations/customer-logout) - [Authentication Guide](/api/graphql-api/authentication) --- # Customer Logout URL: /api/graphql-api/shop/mutations/customer-logout --- outline: false examples: - id: customer-logout title: Customer Logout description: Logout a customer and invalidate their authentication tokens. query: | mutation createLogout { createLogout(input: {}) { logout { success message } } } variables: | {} response: | { "data": { "createLogout": { "logout": { "success": true, "message": "Logged out successfully" } } } } - id: customer-logout-with-device-token title: Customer Logout with Device Token description: Logout a customer and deregister the FCM device token to stop push notifications. Only applicable if the Bagisto Push Notification package is installed. query: | mutation createLogout($deviceToken: String) { createLogout(input: { deviceToken: $deviceToken }) { logout { success message } } } variables: | { "deviceToken": "your_fcm_device_token" } response: | { "data": { "createLogout": { "logout": { "success": true, "message": "Logged out successfully" } } } } --- # Customer Logout Logout a customer and invalidate their authentication tokens. > **Push Notifications:** The `deviceToken` field is only applicable if the [Bagisto Push Notification](https://bagisto.com/en/extensions/push-notifications-for-bagisto/) package is installed. If the customer logged in with a `deviceToken`, the same token must be passed here on logout to properly deregister the device and stop push notifications for that session. If the package is not installed, this field can be omitted. ## Authentication This query requires a valid customer authentication token in the `Authorization` header. Use the [Customer Login API](/api/graphql-api/shop/mutations/customer-login) to retrieve the token. ``` Authorization: Bearer ``` ## Arguments | Argument | Type | Required | Description | |----------|------|----------|-------------| | `deviceToken` | `String` | ❌ No | FCM device token. Required only if the customer logged in with a `deviceToken` and the Push Notification package is installed. | ## Response | Field | Type | Description | |-------|------|-------------| | `message` | String | Success or error message | | `success` | Boolean | Logout success status | ## Behavior - Invalidates the current access token - Invalidates the refresh token - Clears any session-related data - Customer will need to login again for future requests - If logged in with a `deviceToken`, passing the same token on logout deregisters the device from push notifications ## Error Responses ```json { "errors": { "authentication": ["Unauthorized: Invalid or expired token"] } } ``` ## Related Documentation - [Customer Login](/api/graphql-api/shop/mutations/customer-login) - [Authentication Guide](/api/graphql-api/authentication) --- # Customer Registration URL: /api/graphql-api/shop/mutations/customer-registration --- outline: false examples: - id: customer-registration title: Customer Registration description: Register a new customer account with email and password. query: | mutation registerCustomer($input: createCustomerInput!) { createCustomer(input: $input) { customer { id _id token channelId customerGroupId dateOfBirth email gender isSuspended isVerified name firstName lastName rememberToken subscribedToNewsLetter status phone } } } variables: | { "input": { "firstName": "John", "lastName": "Doe", "gender": "Male", "dateOfBirth": "01/15/1990", "phone": "5550123", "status": "1", "isVerified": "1", "isSuspended": "0", "email": "john.doe@example.com", "password": "SecurePass@123", "confirmPassword": "SecurePass@123", "subscribedToNewsLetter": true } } response: | { "data": { "createCustomer": { "customer": { "id": "/api/shop/customers/1", "_id": 1, "channelId": null, "customerGroupId": null, "dateOfBirth": "1990-01-15", "email": "john.doe@example.com", "gender": "Male", "isSuspended": "0", "isVerified": "1", "name": "John Doe", "firstName": "John", "lastName": "Doe", "rememberToken": null, "subscribedToNewsLetter": true, "status": "1", "token": "7b65c50e0c15cdc684d36e5819eb7c19", "phone": "5550123" } } } } - id: customer-registration-with-device-token title: Customer Registration with Device Token description: Register a new customer account and associate an FCM device token for push notifications. Only applicable if the Bagisto Push Notification package is installed. query: | mutation registerCustomer($input: createCustomerInput!) { createCustomer(input: $input) { customer { id _id token channelId customerGroupId dateOfBirth email gender isSuspended isVerified name firstName lastName rememberToken subscribedToNewsLetter status phone deviceToken } } } variables: | { "input": { "firstName": "John", "lastName": "Doe", "gender": "Male", "dateOfBirth": "01/15/1990", "phone": "5550123", "status": "1", "isVerified": "1", "isSuspended": "0", "email": "john.doe@example.com", "password": "SecurePass@123", "confirmPassword": "SecurePass@123", "subscribedToNewsLetter": true, "deviceToken": "your_fcm_device_token" } } response: | { "data": { "createCustomer": { "customer": { "id": "/api/shop/customers/1", "_id": 1, "channelId": null, "customerGroupId": null, "dateOfBirth": "1990-01-15", "email": "john.doe@example.com", "gender": "Male", "isSuspended": "0", "isVerified": "1", "name": "John Doe", "firstName": "John", "lastName": "Doe", "rememberToken": null, "subscribedToNewsLetter": true, "status": "1", "token": "7b65c50e0c15cdc684d36e5819eb7c19", "phone": "5550123", "deviceToken": "your_fcm_device_token" } } } } --- # Customer Registration Register a new customer account with Bagisto. > **Push Notifications:** The `deviceToken` field is only applicable if the [Bagisto Push Notification](https://bagisto.com/en/extensions/push-notifications-for-bagisto/) package is installed. Pass the FCM device token here during registration to immediately associate the device with the new customer account for push notification delivery. If the package is not installed, this field can be omitted. ## Arguments | Name | Type | Required | Description | |------|------|----------|-------------| | `firstName` | String | ✅ Yes | Customer's first name | | `lastName` | String | ✅ Yes | Customer's last name | | `email` | String | ✅ Yes | Customer's email address (must be unique) | | `password` | String | ✅ Yes | Password for the account (min. 8 characters) | | `confirmPassword` | String | ✅ Yes | Must match `password` | | `phone` | String | ❌ No | Customer's phone number | | `gender` | String | ❌ No | Customer's gender (e.g. `Male`, `Female`) | | `dateOfBirth` | String | ❌ No | Date of birth in `MM/DD/YYYY` format | | `subscribedToNewsLetter` | Boolean | ❌ No | Opt-in to marketing emails. Default: `false` | | `deviceToken` | String | ❌ No | FCM device token for push notifications. Only required if the [Bagisto Push Notification](https://bagisto.com/en/extensions/push-notifications-for-bagisto/) package is installed. | ## Response | Field | Type | Description | |-------|------|-------------| | `id` | ID | IRI-style unique identifier (e.g. `/api/shop/customers/1`) | | `_id` | Int | Numeric database ID | | `email` | String | Registered email address | | `firstName` | String | Customer's first name | | `lastName` | String | Customer's last name | | `name` | String | Full name (first + last) | | `token` | String | Authentication token for use in subsequent requests | | `status` | String | Account status (`1` = active) | | `isVerified` | String | Whether the account is verified | | `isSuspended` | String | Whether the account is suspended | | `subscribedToNewsLetter` | Boolean | Newsletter subscription status | | `gender` | String | Customer's gender | | `dateOfBirth` | String | Customer's date of birth | | `phone` | String | Customer's phone number | | `deviceToken` | String | Associated FCM device token (if Push Notification package is installed) | ## Token Usage After registration, use the `token` in the `Authorization` header for authenticated requests: ``` Authorization: Bearer ``` e.g. ``` "Authorization": "Bearer 7b65c50e0c15cdc684d36e5819eb7c19" ``` ## Validation Rules - Email must be in valid format and unique across all customers - Password must be at least 8 characters - `confirmPassword` must match `password` - First name and last name are required ## Error Responses ```json { "errors": { "email": ["The email has already been taken."], "password": ["The password must be at least 8 characters."] } } ``` ## Related Documentation - [Customer Login](/api/graphql-api/shop/mutations/customer-login) - [Customer Logout](/api/graphql-api/shop/mutations/customer-logout) - [Authentication Guide](/api/graphql-api/authentication) --- # Verify Customer Token URL: /api/graphql-api/shop/mutations/customer-verify-token --- outline: false examples: - id: customer-verify-token title: Verify Email Token description: Verify a customer's email using a verification token. query: | mutation createVerifyToken { createVerifyToken(input: {}) { verifyToken { isValid message } } } response: | { "data": { "createVerifyToken": { "verifyToken": { "isValid": true, "message": "Token is valid" } } } } --- # Verify Customer Token Verify a customer's token is valid or not. ## Request Headers | Name | Type | Required | Description | |------|------|----------|-------------| | `Authorization` | String | ✅ Yes | Customer login token use as Bearer Token | | `X-STOREFRONT-KEY` | String | ✅ Yes | Storefront API key for authentication | ## Response | Field | Type | Description | |-------|------|-------------| | `message` | String | Success or error message | | `isValid` | Boolean | Verification success status | ## Use Cases - Check user login or not - Customer token is valid or not - Account activation ## Error Responses ```json { "errors": { "token": ["Unauthenticated. Please login to perform this action"] } } ``` ## Related Documentation - [Customer Registration](/api/graphql-api/shop/mutations/customer-registration) - [Authentication Guide](/api/graphql-api/authentication) --- # Delete All Compare Items URL: /api/graphql-api/shop/mutations/delete-all-compare-items --- outline: false examples: - id: delete-all-compare-items title: Delete All Compare Items description: Remove all compare items for the authenticated customer. Returns the count of deleted items. query: | mutation DeleteAllCompareItems { createDeleteAllCompareItems(input: {}) { deleteAllCompareItems { message deletedCount } } } variables: | {} response: | { "data": { "createDeleteAllCompareItems": { "deleteAllCompareItems": { "message": "All compare items have been removed successfully", "deletedCount": 3 } } } } commonErrors: - error: UNAUTHENTICATED cause: Missing or invalid Bearer token solution: Login and provide a valid customer authentication token --- # Delete All Compare Items ## About The `createDeleteAllCompareItems` mutation removes all products from the authenticated customer's comparison list at once. Use this mutation to: - Clear the entire comparison list - Implement a "Clear All" button for the compare feature - Reset the customer's comparison state > **Note:** This is an authenticated-only operation. The customer is auto-detected from the Bearer token. ## Authentication This mutation requires customer authentication: - **Authenticated customers**: Provide a valid customer authentication token in the `Authorization` header. Obtain this token via the [Customer Login API](/api/graphql-api/shop/mutations/customer-login). ``` Authorization: Bearer ``` ## Arguments | Argument | Type | Description | |----------|------|-------------| | `clientMutationId` | `String` | Optional client-side mutation identifier for tracking. | > **Note:** No additional input is required. Pass an empty input `{}`. ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `deleteAllCompareItems.message` | `String!` | Success message confirming deletion. | | `deleteAllCompareItems.deletedCount` | `Int!` | Number of compare items that were removed. | > **Note:** If the customer has no compare items, the mutation still succeeds and returns `deletedCount: 0` with the same success message. No error is thrown for an already-empty list. --- # Delete All Wishlist Items URL: /api/graphql-api/shop/mutations/delete-all-wishlists --- outline: false examples: - id: delete-all-wishlists title: Delete All Wishlist Items description: Remove all wishlist items for the authenticated customer. Returns the count of deleted items. query: | mutation DeleteAllWishlists { createDeleteAllWishlists(input: {}) { deleteAllWishlists { message deletedCount } } } variables: | {} response: | { "data": { "createDeleteAllWishlists": { "deleteAllWishlists": { "message": "All wishlist items have been removed successfully", "deletedCount": 3 } } } } commonErrors: - error: UNAUTHENTICATED cause: Missing or invalid Bearer token solution: Login and provide a valid customer authentication token --- # Delete All Wishlist Items ## About The `createDeleteAllWishlists` mutation removes all items from the authenticated customer's wishlist at once. Use this mutation to: - Clear the entire wishlist - Implement a "Clear All" button on the wishlist page - Reset the customer's wishlist state > **Note:** This is an authenticated-only operation. The customer is auto-detected from the Bearer token. ## Authentication This mutation requires customer authentication: - **Authenticated customers**: Provide a valid customer authentication token in the `Authorization` header. Obtain this token via the [Customer Login API](/api/graphql-api/shop/mutations/customer-login). ``` Authorization: Bearer ``` ## Arguments | Argument | Type | Description | |----------|------|-------------| | `clientMutationId` | `String` | Optional client-side mutation identifier for tracking. | > **Note:** No additional input is required. Pass an empty input `{}`. ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `deleteAllWishlists.message` | `String!` | Success message confirming deletion. | | `deleteAllWishlists.deletedCount` | `Int!` | Number of wishlist items that were removed. | > **Note:** If the customer has no wishlist items, the mutation still succeeds and returns `deletedCount: 0` with the same success message. No error is thrown for an already-empty wishlist. --- # Delete Compare Item URL: /api/graphql-api/shop/mutations/delete-compare-item --- outline: false examples: - id: delete-compare-item-basic title: Remove Product from Compare List description: Remove a product from the customer's comparison list by its IRI. query: | mutation DeleteCompareItem($input: deleteCompareItemInput!) { deleteCompareItem(input: $input) { compareItem { id _id product { id _id sku type name price formattedPrice minimumPrice formattedMinimumPrice } } } } variables: | { "input": { "id": "/api/shop/compare-items/38" } } response: | { "data": { "deleteCompareItem": { "compareItem": { "id": "/api/shop/compare_items/38", "_id": 38, "product": { "id": "/api/shop/compare_items/38", "_id": 2514, "sku": "SP-003", "type": "simple", "name": "Arctic Touchscreen Winter Gloves", "price": "21", "formattedPrice": "$21.00", "minimumPrice": "17", "formattedMinimumPrice": "$17.00" } } } } } commonErrors: - error: UNAUTHENTICATED cause: Missing or invalid Bearer token solution: Login and provide a valid customer authentication token - error: ITEM_NOT_FOUND cause: Compare item does not exist or belongs to another customer solution: Use a valid compare item IRI from the compareItems collection query --- # Delete Compare Item ## About The `deleteCompareItem` mutation removes a product from the authenticated customer's comparison list. Use this mutation to: - Remove individual products from the compare list - Implement "Remove from Compare" buttons - Clean up comparison lists programmatically ## Authentication This mutation requires customer authentication: - **Authenticated customers**: Provide a valid customer authentication token in the `Authorization` header. Obtain this token via the [Customer Login API](/api/graphql-api/shop/mutations/customer-login). ``` Authorization: Bearer ``` ## Arguments | Argument | Type | Description | |----------|------|-------------| | `id` | `ID!` | The IRI of the compare item to delete (e.g. `/api/shop/compare-items/606`). | | `clientMutationId` | `String` | Optional client-side mutation identifier for tracking. | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `compareItem` | `CompareItem!` | The deleted compare item. | | `compareItem.id` | `ID!` | IRI identifier of the removed item. | | `compareItem.product` | `Product!` | Product details of the removed item. | --- # Delete Customer Address URL: /api/graphql-api/shop/mutations/delete-customer-address --- outline: false examples: - id: delete-customer-address title: Delete Customer Address description: Delete a customer address. query: | mutation deleteCustomerAddress($input: createDeleteCustomerAddressInput!) { createDeleteCustomerAddress(input: $input) { deleteCustomerAddress { id } } } variables: | { "input": { "addressId": 2858 } } response: | { "data": { "createDeleteCustomerAddress": { "deleteCustomerAddress": { "id": "2858" } } } } --- # Delete Customer Address Delete a customer address. ## Authentication This query requires a valid customer authentication token in the `Authorization` header. Use the [Customer Login API](/api/graphql-api/shop/mutations/customer-login) to retrieve the token. ``` Authorization: Bearer ``` ## Arguments | Name | Type | Required | Description | |------|------|----------|-------------| | `addressId` | String | ✅ Yes | Address ID to delete | ## Response | Field | Type | Description | |-------|------|-------------| | `createDeleteCustomerAddress` | String | Return deleted customer address object | ## Important Notes - Address must belong to the authenticated customer - Default address cannot be deleted without setting a new default - Addresses used in incomplete orders may be archived instead of deleted ## Error Responses ```json { "errors": { "message": "Address not found or does not belong to this customer", } } ``` ## Related Documentation - [Create Customer Address](/api/graphql-api/shop/mutations/create-customer-address) - [Update Customer Address](/api/graphql-api/shop/mutations/update-customer-address) - [Get Customer Addresses](/api/graphql-api/shop/queries/get-customer-addresses) --- # Delete Customer Account URL: /api/graphql-api/shop/mutations/delete-customer-profile --- outline: false examples: - id: delete-customer-profile title: Delete Customer Account description: Permanently delete the authenticated customer's account. query: | mutation deleteCustomerProfile { createCustomerProfileDelete(input: {}) { customerProfileDelete { id } } } response: | { "data": { "createCustomerProfileDelete": { "customerProfileDelete": null } } } --- # Delete Customer Account Permanently delete the authenticated customer's account and all associated data. ## Authentication This query requires a valid customer authentication token in the `Authorization` header. Use the [Customer Login API](/api/graphql-api/shop/mutations/customer-login) to retrieve the token. ``` Authorization: Bearer ``` ## Important Notes ⚠️ **This action is permanent and cannot be undone** When a customer account is deleted: - All personal information is removed - Order history is archived (for legal compliance) - Wishlist items are removed - Addresses are deleted - Authentication tokens are invalidated ## Error Responses ```json { "errors": { "message": "Invalid or expired token", } } ``` ## Related Documentation - [Update Customer Profile](/api/graphql-api/shop/mutations/update-customer-profile) - [Customer Logout](/api/graphql-api/shop/mutations/customer-logout) --- # Delete Product Review URL: /api/graphql-api/shop/mutations/delete-product-review --- outline: false examples: - id: delete-product-review-basic title: Delete Product Review - Basic description: Delete a product review by providing its ID in IRI format. query: | mutation deleteProductReview($input: deleteProductReviewInput!) { deleteProductReview(input: $input) { productReview { id } } } variables: | { "input": { "id": "/api/shop/reviews/93" } } response: | { "data": { "deleteProductReview": { "productReview": { "id": "/api/shop/reviews/93" } } } } commonErrors: - error: id-required cause: Review ID parameter is missing solution: Provide the review ID in IRI format (e.g., "/api/shop/reviews/93") - error: invalid-id-format cause: Invalid ID format. Expected IRI format like "/api/shop/reviews/93" solution: Use IRI format ID (/api/shop/reviews/{id}) for review deletion - error: not-found cause: Review with given ID does not exist solution: Verify the review ID is correct and the review exists - id: delete-product-review-with-tracking title: Delete Product Review - With Tracking description: Delete a product review and track the deletion with client mutation ID. query: | mutation deleteProductReview($input: deleteProductReviewInput!) { deleteProductReview(input: $input) { productReview { id } clientMutationId } } variables: | { "input": { "id": "/api/shop/reviews/60", "clientMutationId": "delete-review-mutation-001" } } response: | { "data": { "deleteProductReview": { "productReview": { "id": "/api/shop/reviews/60" }, "clientMutationId": "delete-review-mutation-001" } } } commonErrors: - error: id-required cause: Review ID parameter is missing solution: Provide the review ID in IRI format (e.g., "/api/shop/reviews/93") - error: invalid-id-format cause: Invalid ID format. Expected IRI format like "/api/shop/reviews/93" solution: Use IRI format ID (/api/shop/reviews/{id}) for review deletion - error: not-found cause: Review with given ID does not exist solution: Verify the review ID is correct and the review exists - error: unauthorized cause: User does not have permission to delete this review solution: Ensure the user has admin privileges or is the review owner --- # Delete Product Review ## About The `deleteProductReview` mutation allows deleting product reviews. Use this mutation to: - Remove inappropriate or spam reviews - Delete duplicate reviews - Remove reviews at customer request - Manage review inventory - Clean up test/demo reviews - Enforce moderation policies - Track deletion operations with audit trail This mutation requires the review ID in IRI format and is a permanent operation that cannot be undone. ## Arguments | Argument | Type | Required | Description | |----------|------|----------|-------------| | `id` | `ID!` | ✅ Yes | Review ID in IRI format (e.g., `/api/shop/reviews/93`). Required for identifying which review to delete. | | `clientMutationId` | `String` | ❌ No | Optional client mutation tracking ID for audit trail. | ## Input Fields Details ### id - **Type**: ID (IRI Format) - **Required**: Yes - **Format**: `/api/shop/reviews/{id}` or `/api/shop/reviews/{id}` - **Description**: Unique identifier for the review being deleted. - **Example**: `/api/shop/reviews/93` - **Note**: Only IRI format is supported; numeric IDs are not accepted. - **Important**: This operation is permanent and cannot be reversed. ### clientMutationId - **Type**: String - **Required**: No - **Description**: Optional tracking ID for this deletion request. Useful for audit trails and request tracking. - **Example**: `"delete-review-mutation-001"` - **Usage**: Echoed back in response for request verification and logging. ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `productReview` | `ProductReview!` | The deleted product review object. | | `productReview.id` | `ID!` | ID of the deleted review (returned as confirmation). | | `clientMutationId` | `String` | Echoed client mutation ID for tracking and audit purposes. | ## ID Format Requirements ### Valid ID Format (IRI) ``` /api/shop/reviews/93 /api/shop/reviews/60 /api/shop/reviews/100 ``` ### Invalid Formats (Not Supported) ``` 93 ❌ Numeric ID only reviews/93 ❌ Partial path /reviews/93 ❌ Incorrect path ``` ## Important Notes ### Permanent Operation - Deletion is **permanent and irreversible** - No data recovery possible after deletion - Consider archiving instead of deleting for sensitive data - Always confirm before deletion in UI ### Before Deletion - Verify the correct review ID - Consider user notification requirements - Check for related data dependencies - Log deletion reason for audit trail ### After Deletion - Review cannot be queried by ID - Related product review counts update - Customer notification (if applicable) - Audit log should record deletion ## Use Cases ### 1. Remove Spam Review Delete reviews that are spam or off-topic. ### 2. Delete Duplicate Reviews Remove duplicate reviews from same user. ### 3. User Requested Deletion Delete reviews at customer's explicit request. ### 4. Violation of Policies Remove reviews that violate community guidelines. ### 5. Test/Demo Data Cleanup Delete temporary reviews used for testing. ### 6. Data Correction Remove incorrectly published reviews. ## Best Practices 1. **Confirm Before Delete** - Always confirm deletion in UI before submitting 2. **Log Deletions** - Use clientMutationId to track deletion operations 3. **Archive First** - Consider archiving sensitive reviews instead of deleting 4. **Audit Trail** - Maintain records of who deleted what and when 5. **User Notification** - Notify customers if their review is deleted 6. **Batch Operations** - For multiple deletions, execute sequentially with tracking 7. **Verify ID** - Double-check IRI format ID before submission 8. **Access Control** - Restrict deletion to authorized users only ## Deletion Workflow ``` 1. Fetch review details (verify correct review) 2. Show confirmation dialog to user 3. If confirmed: a. Submit deleteProductReview mutation b. Track with clientMutationId c. Handle success response d. Update UI (remove from list) e. Log deletion in audit trail 4. If cancelled: a. Keep review in system b. No action needed ``` ## Error Scenarios ### Missing ID When `id` is not provided, mutation fails with validation error. ### Invalid ID Format When ID is provided in numeric format instead of IRI format. ### Review Not Found When provided ID doesn't correspond to existing review (already deleted or invalid). ### Unauthorized Access When user lacks permissions to delete the review. ### Database Constraint When review deletion fails due to database constraints or triggers. ## Related Operations **Before Deleting:** - [Get Product Review](/api/graphql-api/shop/queries/get-product-review) - Fetch review details - [Get Product Reviews](/api/graphql-api/shop/queries/get-product-reviews) - View all reviews **Review Management:** - [Create Product Review](/api/graphql-api/shop/mutations/create-product-review) - Create new reviews - [Update Product Review](/api/graphql-api/shop/mutations/update-product-review) - Modify existing reviews **Related Resources:** - [Get Product](/api/graphql-api/shop/queries/get-product) - Query product details - [Mutations Guide](/api/graphql-api/shop/mutations) - Overview of shop mutations - [Shop API Overview](/api/graphql-api/shop-api) - Overview of Shop API resources ## Audit Trail Example ``` Event: Product Review Deleted Review ID: /api/shop/reviews/93 Mutation ID: delete-review-mutation-001 User: admin@example.com Timestamp: 2025-12-26T20:15:30+05:30 Reason: Spam content Status: Success ``` ## Recovery Options Since deletion is permanent: - **Database Backup**: Restore from database backup (if available) - **Archive Strategy**: Use status=2 (rejected) instead of deletion for soft-delete - **Soft Delete**: Flag review as deleted without removing data - **Audit Log**: Maintain detailed deletion logs for compliance ## Safety Checklist Before executing delete mutation: - ✅ Verified correct review ID in IRI format - ✅ Confirmed review content requires deletion - ✅ User authorization verified - ✅ Reason for deletion documented - ✅ Audit trail prepared - ✅ User notification plan confirmed - ✅ Backup verified (if needed) - ✅ No critical data dependencies --- **⚠️ Warning**: This operation is irreversible. Always verify the review ID and ensure proper authorization before executing deletion. --- # Delete Wishlist Item URL: /api/graphql-api/shop/mutations/delete-wishlist --- outline: false examples: - id: delete-wishlist-basic title: Delete Wishlist Item description: Delete a specific wishlist item by its IRI. query: | mutation DeleteWishlist($input: deleteWishlistInput!) { deleteWishlist(input: $input) { wishlist { id _id } } } variables: | { "input": { "id": "/api/shop/wishlists/76" } } response: | { "data": { "deleteWishlist": { "wishlist": { "id": "/api/shop/wishlists/76", "_id": 76 } } } } commonErrors: - error: UNAUTHENTICATED cause: Missing or invalid Bearer token solution: Login and provide a valid customer authentication token - error: ITEM_NOT_FOUND cause: Wishlist item does not exist or belongs to another customer solution: Use a valid wishlist item IRI from the wishlists collection query --- # Delete Wishlist Item ## About The `deleteWishlist` mutation removes a specific item from the authenticated customer's wishlist. Use this mutation to: - Remove individual products from the wishlist - Implement "Remove from Wishlist" buttons - Clean up wishlist items programmatically ## Authentication This mutation requires customer authentication: - **Authenticated customers**: Provide a valid customer authentication token in the `Authorization` header. Obtain this token via the [Customer Login API](/api/graphql-api/shop/mutations/customer-login). ``` Authorization: Bearer ``` ## Arguments | Argument | Type | Description | |----------|------|-------------| | `id` | `ID!` | The IRI of the wishlist item to delete (e.g. `/api/shop/wishlists/76`). | | `clientMutationId` | `String` | Optional client-side mutation identifier for tracking. | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `wishlist` | `Wishlist!` | The deleted wishlist item. | | `wishlist.id` | `ID!` | IRI identifier of the removed item. | | `wishlist._id` | `Int!` | Numeric identifier of the removed item. | --- # Forgot Password URL: /api/graphql-api/shop/mutations/forgot-password --- outline: false examples: - id: forgot-password title: Forgot Password description: Request a password reset email for an account. query: | mutation createForgotPassword($email: String!) { createForgotPassword(input: { email: $email }) { forgotPassword { success message } } } variables: | { "email": "john.doe@example.com" } response: | { "data": { "forgotPassword": { "message": "Password reset link sent to your email", "success": true } } } --- # Forgot Password Request a password reset email for an account. ## Arguments | Name | Type | Required | Description | |------|------|----------|-------------| | `email` | String | ✅ Yes | Customer's registered email address | ## Response | Field | Type | Description | |-------|------|-------------| | `message` | String | Success or error message | | `success` | Boolean | Request success status | ## Behavior - Sends a password reset link to the customer's email - The reset link is valid for a configurable period (typically 24 hours) - Customer uses the link to set a new password - Old tokens are invalidated when password is reset ## Error Responses ```json { "errors": { "email": ["No account found with this email address."] } } ``` ## Email Content The reset email typically contains: - A unique password reset link/token - Expiration time for the token - Instructions to reset the password - Security information ## Next Steps After requesting a password reset: 1. The customer receives an email with a reset link. 2. The customer clicks the link and sets a new password **on the web page** the link opens. There is no reset-password API operation — the reset is completed through the emailed web link. A logged-in customer who knows their current password can change it directly via the profile-update mutation instead. ## Related Documentation - [Update Customer Profile](/api/graphql-api/shop/mutations/update-customer-profile) — change the password while logged in (current + new) - [Customer Login](/api/graphql-api/shop/mutations/customer-login) --- # Login Customer URL: /api/graphql-api/shop/mutations/login-customer --- outline: false examples: - id: customer-login-basic title: Customer Login description: Authenticate customer with email and password. query: | mutation createLogin($email: String!, $password: String!) { createLogin(input: {email: $email, password: $password}) { login { id email firstName lastName token apiToken success message } } } variables: | { "email": "customer@example.com", "password": "SecurePassword123!" } response: | { "data": { "createLogin": { "login": { "id": "1", "email": "customer@example.com", "firstName": "John", "lastName": "Doe", "token": "eyJpdiI6IjhWM...", "apiToken": "abc123xyz789", "success": true, "message": "Login successful" } } } } commonErrors: - error: INVALID_CREDENTIALS cause: Email or password is incorrect solution: Verify email and password - error: ACCOUNT_INACTIVE cause: Customer account is suspended solution: Contact support to reactivate - error: ACCOUNT_NOT_FOUND cause: Email is not registered solution: Register new account or verify email --- # Login Customer ## About The `loginCustomer` mutation authenticates a customer and generates authentication tokens. Use this mutation to: - Implement customer login/signin flows - Generate authentication credentials - Validate customer credentials - Enable authenticated API access - Support session management - Integrate with external auth systems - Handle customer access control This mutation validates credentials, verifies account status, and generates tokens for authenticated requests. ## Arguments | Argument | Type | Description | |----------|------|-------------| | `email` | `String!` | Customer email address registered in system. | | `password` | `String!` | Customer account password. | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `login` | `LoginResponse!` | Login response containing customer data and tokens. | | `login.id` | `ID!` | Customer ID. | | `login.email` | `String!` | Customer email. | | `login.firstName` | `String!` | First name. | | `login.lastName` | `String!` | Last name. | | `login.token` | `String!` | Authentication token for API requests. | | `login.apiToken` | `String!` | API token for programmatic access. | | `login.success` | `Boolean!` | Login success indicator. | | `login.message` | `String!` | Success or error message. | | `errors` | `[ErrorMessage!]` | Authentication errors if login failed. | --- # Merge Cart URL: /api/graphql-api/shop/mutations/merge-cart --- outline: false examples: - id: merge-guest-cart title: Merge Guest Cart with Customer Cart description: Merge a guest cart into the authenticated customer's cart. Pass the guest cart's _id as cartId and the customer's Bearer token in the Authorization header. Returns the full merged cart with all items. query: | mutation createMergeCart { createMergeCart(input: { cartId: 4929 }) { mergeCart { grandTotal discountAmount cartToken customerId channelId subtotal baseSubtotal discountAmount baseDiscountAmount taxAmount baseTaxAmount shippingAmount baseShippingAmount grandTotal baseGrandTotal formattedSubtotal formattedDiscountAmount formattedTaxAmount formattedShippingAmount formattedGrandTotal couponCode success message sessionToken isGuest itemsQty itemsCount haveStockableItems paymentMethod paymentMethodTitle subTotalInclTax baseSubTotalInclTax formattedSubTotalInclTax taxTotal formattedTaxTotal shippingAmountInclTax baseShippingAmountInclTax formattedShippingAmountInclTax items { totalCount pageInfo { startCursor endCursor hasNextPage hasPreviousPage } edges { cursor node { id cartId productId name sku quantity price basePrice total baseImage baseTotal discountAmount baseDiscountAmount taxAmount baseTaxAmount type formattedPrice formattedTotal priceInclTax basePriceInclTax formattedPriceInclTax totalInclTax baseTotalInclTax formattedTotalInclTax productUrlKey canChangeQty options } } } } } } variables: | {} response: | { "data": { "createMergeCart": { "mergeCart": { "grandTotal": 2584, "discountAmount": 0, "cartToken": "4928", "customerId": 19, "channelId": 1, "subtotal": 2584, "baseSubtotal": 2584, "baseDiscountAmount": 0, "taxAmount": 0, "baseTaxAmount": 0, "shippingAmount": 0, "baseShippingAmount": 0, "baseGrandTotal": 2584, "formattedSubtotal": "$2,584.00", "formattedDiscountAmount": "$0.00", "formattedTaxAmount": "$0.00", "formattedShippingAmount": "$0.00", "formattedGrandTotal": "$2,584.00", "couponCode": null, "success": true, "message": "Guest cart merged successfully", "sessionToken": null, "isGuest": false, "itemsQty": 2, "itemsCount": 2, "haveStockableItems": true, "paymentMethod": null, "paymentMethodTitle": null, "subTotalInclTax": 2584, "baseSubTotalInclTax": 2584, "formattedSubTotalInclTax": "$2,584.00", "taxTotal": 0, "formattedTaxTotal": "$0.00", "shippingAmountInclTax": 0, "baseShippingAmountInclTax": 0, "formattedShippingAmountInclTax": "$0.00", "items": { "totalCount": 2, "pageInfo": { "startCursor": "MA==", "endCursor": "MQ==", "hasNextPage": false, "hasPreviousPage": false }, "edges": [ { "cursor": "MA==", "node": { "id": "6258", "cartId": 4928, "productId": 2500, "name": "Mint Axis Unisex Tailored Blazer", "sku": "MINT-BLAZER-001", "quantity": 1, "price": 544, "basePrice": 544, "total": 544, "baseImage": "{\"small_image_url\":\"https://api-demo.bagisto.com/cache/small/product/2503/XcPrmG5JXJNJ6WdOmGOYkY1kMyVvWc4DnIUaUiJG.webp\",\"original_image_url\":\"https://api-demo.bagisto.com/cache/original/product/2503/XcPrmG5JXJNJ6WdOmGOYkY1kMyVvWc4DnIUaUiJG.webp\"}", "baseTotal": 544, "discountAmount": 0, "baseDiscountAmount": 0, "taxAmount": 0, "baseTaxAmount": 0, "type": "configurable", "formattedPrice": "$544.00", "formattedTotal": "$544.00", "priceInclTax": 544, "basePriceInclTax": 544, "formattedPriceInclTax": "$544.00", "totalInclTax": 544, "baseTotalInclTax": 544, "formattedTotalInclTax": "$544.00", "productUrlKey": "mint-axis-unisex-tailored-blazer", "canChangeQty": true, "options": { "edges": [ { "node": { "option_id": 8, "option_label": "L", "attribute_name": "Size" } } ] } } }, { "cursor": "MQ==", "node": { "id": "6262", "cartId": 4928, "productId": 123, "name": "Zoe Tank", "sku": "ZOE-TANK-001", "quantity": 1, "price": 2040, "basePrice": 2040, "total": 2040, "baseImage": "{\"small_image_url\":\"https://api-demo.bagisto.com/cache/small/product/124/Ba8yoli6aFgjpiFLUcfEpuHaGzbaRCYu7wEvKR2d.webp\",\"original_image_url\":\"https://api-demo.bagisto.com/cache/original/product/124/Ba8yoli6aFgjpiFLUcfEpuHaGzbaRCYu7wEvKR2d.webp\"}", "baseTotal": 2040, "discountAmount": 0, "baseDiscountAmount": 0, "taxAmount": 0, "baseTaxAmount": 0, "type": "configurable", "formattedPrice": "$2,040.00", "formattedTotal": "$2,040.00", "priceInclTax": 2040, "basePriceInclTax": 2040, "formattedPriceInclTax": "$2,040.00", "totalInclTax": 2040, "baseTotalInclTax": 2040, "formattedTotalInclTax": "$2,040.00", "productUrlKey": "zoe-tank", "canChangeQty": true, "options": { "edges": [ { "node": { "option_id": 6, "option_label": "S", "attribute_name": "Size" } }, { "node": { "option_id": 1, "option_label": "Red", "attribute_name": "Color" } } ] } } } ] } } } } } commonErrors: - error: UNAUTHENTICATED cause: Missing or invalid customer Bearer token solution: Login and provide a valid customer authentication token in the Authorization header - error: CART_NOT_FOUND cause: The guest cart ID does not exist or has already been merged solution: Verify the cartId is a valid guest cart _id from the Create Cart mutation - error: EMPTY_CART cause: The guest cart has no items to merge solution: Ensure the guest cart contains at least one item before merging --- # Merge Cart ## About The `createMergeCart` mutation merges a guest (unauthenticated) cart into the authenticated customer's cart. This is essential for preserving the shopping experience when a guest user logs in after adding items to their cart. Use this mutation to: - Transfer guest cart items to the customer's cart after login - Preserve products added during anonymous browsing - Combine guest and existing customer cart items seamlessly ## How It Works 1. A guest user browses the store and adds items to a cart using the [Create Cart](/api/graphql-api/shop/mutations/create-cart) mutation, which returns a `cartToken` and cart `_id` 2. The guest decides to log in via the [Customer Login](/api/graphql-api/shop/mutations/customer-login) mutation and receives a Bearer token 3. The `createMergeCart` mutation is called with the guest cart's `_id` as `cartId` and the customer's Bearer token in the `Authorization` header 4. The system merges all guest cart items into the customer's cart and returns the updated cart state ## Authentication This mutation requires **customer authentication**. The Bearer token identifies which customer's cart the guest cart should be merged into. ``` Authorization: Bearer X-STOREFRONT-KEY: ``` ## Arguments | Argument | Type | Required | Description | |----------|------|----------|-------------| | `cartId` | `Int!` | Yes | The `_id` of the guest cart to merge. This is the numeric ID returned by the [Create Cart](/api/graphql-api/shop/mutations/create-cart) mutation during the guest session. | ## Response | Field | Type | Description | |-------|------|-------------| | `id` | `String` | The merged cart ID (customer's cart) | | `itemsCount` | `Int` | Total number of distinct items in the merged cart | | `grandTotal` | `Float` | Updated grand total of the merged cart | | `success` | `Boolean` | Whether the merge was successful | | `message` | `String` | Success or error message | ## Typical Flow ``` Guest Session: 1. createCartToken → get cartToken + cart _id (e.g. 364) 2. addProductInCart → add items using guest cart token Login: 3. createCustomerLogin → get customer Bearer token Merge: 4. createMergeCart(cartId: 364) → with customer Bearer token → Guest cart items are now in the customer's cart ``` > **Note:** After a successful merge, the guest cart is invalidated. The `cartToken` from the guest session can no longer be used. All subsequent cart operations should use the customer's Bearer token. ## Related Documentation - [Create Cart](/api/graphql-api/shop/mutations/create-cart) — Create a guest cart and get the cart `_id` - [Add to Cart](/api/graphql-api/shop/mutations/add-to-cart) — Add products to cart - [Get Cart](/api/graphql-api/shop/queries/get-cart) — View the merged cart contents - [Customer Login](/api/graphql-api/shop/mutations/customer-login) — Authenticate and get Bearer token --- # Move Wishlist Item to Cart URL: /api/graphql-api/shop/mutations/move-wishlist-to-cart --- outline: false examples: - id: move-wishlist-to-cart-basic title: Move Wishlist Item to Cart description: Move a wishlist item to the shopping cart and remove it from the wishlist. query: | mutation MoveWishlistToCart($input: moveWishlistToCartInput!) { moveWishlistToCart(input: $input) { wishlistToCart { message } } } variables: | { "input": { "wishlistItemId": 77, "quantity": 2 } } response: | { "data": { "moveWishlistToCart": { "wishlistToCart": { "message": "Item moved to cart successfully" } } } } commonErrors: - error: UNAUTHENTICATED cause: Missing or invalid Bearer token solution: Login and provide a valid customer authentication token - error: ITEM_NOT_FOUND cause: Wishlist item does not exist or belongs to another customer solution: Use a valid wishlist item numeric _id - error: INVALID_QUANTITY cause: Quantity is less than 1 or invalid solution: Provide a positive integer quantity - error: OPTIONS_REQUIRED cause: Configurable product requires options to be selected solution: Provide the required product options when moving configurable products - id: move-wishlist-to-cart-default-qty title: Move Wishlist Item to Cart - Default Quantity description: Move a wishlist item to the cart with the default quantity of 1 by omitting the quantity field. query: | mutation MoveWishlistToCart($input: moveWishlistToCartInput!) { moveWishlistToCart(input: $input) { wishlistToCart { message } } } variables: | { "input": { "wishlistItemId": 68 } } response: | { "data": { "moveWishlistToCart": { "wishlistToCart": { "message": "Item moved to cart successfully" } } } } commonErrors: - error: ITEM_NOT_FOUND cause: Wishlist item does not exist solution: Use a valid wishlist item numeric _id --- # Move Wishlist Item to Cart ## About The `moveWishlistToCart` mutation moves a product from the customer's wishlist directly into their shopping cart. Use this mutation to: - Implement "Move to Cart" buttons on the wishlist page - Convert saved-for-later items into active cart items - Streamline the purchase flow from wishlist to checkout The item is removed from the wishlist after it is successfully added to the cart. > **Note:** `wishlistItemId` is the numeric `_id` of the wishlist item (not the IRI). The `quantity` field defaults to `1` if omitted. ## Authentication This mutation requires customer authentication: - **Authenticated customers**: Provide a valid customer authentication token in the `Authorization` header. Obtain this token via the [Customer Login API](/api/graphql-api/shop/mutations/customer-login). ``` Authorization: Bearer ``` ## Arguments | Argument | Type | Description | |----------|------|-------------| | `wishlistItemId` | `Int!` | The numeric `_id` of the wishlist item to move (not the IRI). | | `quantity` | `Int` | Number of units to add to cart. Defaults to `1` if omitted. | | `clientMutationId` | `String` | Optional client-side mutation identifier for tracking. | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `wishlistToCart.message` | `String!` | Success message confirming the item was moved. | --- # Place Order URL: /api/graphql-api/shop/mutations/place-order --- outline: false examples: - id: place-order title: Place Order description: Create an order from a cart. query: | mutation createCheckoutOrder { createCheckoutOrder(input:{}) { checkoutOrder { id orderId } } } response: | { "data": { "createCheckoutOrder": { "checkoutOrder": { "id": "4814", "orderId": "554", } } } } --- # Place Order Create an order from a cart and complete the checkout process. ## Authentication This query supports both authenticated customers and guest users: - **Authenticated customers**: Provide a valid customer authentication token in the `Authorization` header. Obtain this token via the [Customer Login API](/api/graphql-api/shop/mutations/customer-login). - **Guest users**: Provide the Guest Cart Token `cartToken` obtained from the [Create Cart mutation](/api/graphql-api/shop/mutations/create-cart). ``` Authorization: Bearer ``` ## Response | Field | Type | Description | |-------|------|-------------| | `id` | String | Checkout order ID | | `orderId` | String | Created order ID | ## Prerequisites All of these must be completed before placing an order: 1. ✅ Cart must contain items 2. ✅ Shipping address must be set 3. ✅ Billing address must be set 4. ✅ Shipping method must be selected 5. ✅ Payment method must be selected 6. ✅ Valid coupon (if applicable) ## Validation Rules - Cart must have at least one item - All checkout steps must be completed - Billing and shipping addresses are required - Shipping and payment methods must be selected - Stock must be available for all items - Inventory must not be exceeded ## Error Responses ```json { "errors": { "checkout": ["Unable to complete checkout. Please verify all required fields."], "inventory": ["Insufficient stock for one or more items."], "payment": ["Payment processing failed."] } } ``` ## Order Status Values | Status | Description | |--------|-------------| | `pending` | Order created, awaiting payment | | `processing` | Payment confirmed, preparing shipment | | `shipped` | Order has been shipped | | `delivered` | Order delivered | | `cancelled` | Order cancelled | | `refunded` | Order refunded | ## After Order Placement 1. Cart is cleared 2. Order confirmation email is sent 3. Inventory is updated 4. Customer can track order using order ID ## Related Documentation - [Create Cart](/api/graphql-api/shop/mutations/create-cart) - [Set Checkout Address](/api/graphql-api/shop/mutations/set-billing-address) - [Set Payment Method](/api/graphql-api/shop/mutations/set-payment-method) - [Get Customer Orders](/api/graphql-api/shop/queries/get-customer-orders) --- # Register Customer URL: /api/graphql-api/shop/mutations/register-customer --- outline: false examples: - id: register-customer-basic title: Register New Customer description: Create a new customer account with email and password. query: | mutation registerCustomer($input: createCustomerInput!) { createCustomer(input: $input) { customer { id _id token channelId customerGroupId dateOfBirth email gender isSuspended isVerified name firstName lastName rememberToken subscribedToNewsLetter status phone } } } variables: | { "input": { "firstName": "John", "lastName": "Doe", "email": "john@example.com", "password": "Password123!", "confirmPassword": "Password123!", "phone": "1234567890", "gender": "Male", "subscribedToNewsLetter": true } } response: | { "data": { "createCustomer": { "customer": { "id": "1", "email": "john@example.com", "firstName": "John", "lastName": "Doe", "status": "active", "token": "eyJpdiI6IjhWM..." } } } } commonErrors: - error: EMAIL_ALREADY_EXISTS cause: Email address already registered solution: Use different email or login instead - error: INVALID_PASSWORD cause: Password doesn't meet requirements solution: Use stronger password (8+ chars, mix of types) - error: INVALID_EMAIL cause: Email format is invalid solution: Provide valid email address - id: register-customer-with-device-token title: Register New Customer with Device Token description: Create a new customer account and associate an FCM device token for push notifications. Only applicable if the Bagisto Push Notification package is installed. query: | mutation registerCustomer($input: createCustomerInput!) { createCustomer(input: $input) { customer { id _id token channelId customerGroupId dateOfBirth email gender isSuspended isVerified name firstName lastName rememberToken subscribedToNewsLetter status phone deviceToken } } } variables: | { "input": { "firstName": "John", "lastName": "Doe", "email": "john@example.com", "password": "Password123!", "confirmPassword": "Password123!", "phone": "1234567890", "gender": "Male", "subscribedToNewsLetter": true, "deviceToken": "your_fcm_device_token" } } response: | { "data": { "createCustomer": { "customer": { "id": "1", "email": "john@example.com", "firstName": "John", "lastName": "Doe", "status": "active", "token": "eyJpdiI6IjhWM...", "deviceToken": "your_fcm_device_token" } } } } commonErrors: - error: EMAIL_ALREADY_EXISTS cause: Email address already registered solution: Use different email or login instead - error: INVALID_PASSWORD cause: Password doesn't meet requirements solution: Use stronger password (8+ chars, mix of types) - error: INVALID_EMAIL cause: Email format is invalid solution: Provide valid email address --- # Register Customer ## About The `registerCustomer` mutation creates a new customer account. Use this mutation to: - Implement customer registration/signup flows - Create new user accounts programmatically - Enable self-service customer registration - Integrate with external registration systems - Collect customer information during signup - Generate authentication tokens - Set up customer profiles This mutation validates input, checks for duplicate emails, and generates authentication credentials for immediate use. > **Push Notifications:** The `deviceToken` field is only applicable if the [Bagisto Push Notification](https://bagisto.com/en/extensions/push-notifications-for-bagisto/) package is installed. It accepts an FCM (Firebase Cloud Messaging) device token to enable push notifications for the registered customer. If the package is not installed, this field can be omitted. ## Arguments | Argument | Type | Description | |----------|------|-------------| | `input` | `createCustomerInput!` | Customer registration data. | | `input.email` | `String!` | Unique customer email address. | | `input.password` | `String!` | Account password (minimum 8 characters recommended). | | `input.firstName` | `String!` | Customer first name. | | `input.lastName` | `String!` | Customer last name. | | `input.phone` | `String` | Customer phone number. | | `input.gender` | `String` | Gender: `male`, `female`, `other`. | | `input.dateOfBirth` | `Date` | Customer birth date (YYYY-MM-DD). | | `input.subscribeToNewsletter` | `Boolean` | Opt-in to marketing emails. Default: `false` | | `input.deviceToken` | `String` | FCM device token for push notifications. Only required if the [Bagisto Push Notification](https://bagisto.com/en/extensions/push-notifications-for-bagisto/) package is installed. | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `customer` | `Customer!` | Newly created customer object. | | `customer.id` | `ID!` | Customer ID. | | `customer.email` | `String!` | Customer email. | | `customer.firstName` | `String!` | First name. | | `customer.lastName` | `String!` | Last name. | | `customer.token` | `String!` | Authentication token for use in requests. | | `customer.apiToken` | `String!` | API token for programmatic access. | | `customer.status` | `String!` | Account status (active, inactive). | | `message` | `String!` | Success message. | | `success` | `Boolean!` | Indicates successful registration. | | `errors` | `[ErrorMessage!]` | Validation errors if registration failed. | --- # Remove Cart Item URL: /api/graphql-api/shop/mutations/remove-cart-item --- outline: false examples: - id: remove-cart-item-simple title: Remove Item from Cart description: Remove a specific item from the shopping cart. query: | mutation removeItem( $cartItemId: Int! ) { createRemoveCartItem( input: { cartItemId: $cartItemId} ) { removeCartItem { id _id cartToken items { totalCount edges { node { id cartId productId name sku quantity price basePrice total baseTotal productUrlKey canChangeQty } } } } } } variables: | { "cartItemId": 54 } response: | { "data": { "removeCartItem": { "cart": { "id": "1", "items": [] }, "message": "Item removed from cart successfully" } } } commonErrors: - error: ITEM_NOT_FOUND cause: Cart item ID does not exist solution: Verify item ID in cart - error: CART_NOT_FOUND cause: Cart session is invalid solution: Create a new cart and try again --- # Remove Cart Item ## About The `removeCartItem` mutation deletes a product from a customer's shopping cart. Use this mutation to: - Remove items from the cart page UI - Delete products from mini cart displays - Clear unwanted items from ongoing checkout - Manage cart contents programmatically - Restore inventory reservations - Update cart totals and pricing This mutation removes the line item completely, updates inventory, and recalculates cart totals, discounts, and taxes. ## Authentication This mutation supports both authenticated customers and guest users: - **Authenticated customers**: Provide a valid customer authentication token in the `Authorization` header. Obtain this token via the [Customer Login API](/api/graphql-api/shop/mutations/customer-login). - **Guest users**: Provide the Guest Cart Token `cartToken` obtained from the [Create Cart mutation](/api/graphql-api/shop/mutations/create-cart). ``` Authorization: Bearer ``` ## Arguments | Argument | Type | Description | |----------|------|-------------| | `cartItemId` | `String!` | Cart Item id. | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `cart` | `Cart!` | Updated cart after item removal. | | `message` | `String!` | Success or error message. | | `success` | `Boolean!` | Indicates successful removal. | | `removedItem` | `CartItem!` | The item that was removed. | | `cart.items` | `[CartItem!]!` | Remaining cart items. | | `cart.itemsCount` | `Int!` | Updated item count. | | `cart.subTotal` | `Float!` | Recalculated subtotal. | | `cart.total` | `Float!` | Recalculated grand total. | | `errors` | `[ErrorMessage!]` | Errors if removal failed. | --- # Remove Coupon URL: /api/graphql-api/shop/mutations/remove-coupon --- outline: false examples: - id: remove-coupon title: Remove Coupon description: Remove an applied coupon code from the cart. query: | mutation createRemoveCoupon { createRemoveCoupon(input: {}) { removeCoupon { id discountAmount grandTotal } } } response: | { "data": { "createRemoveCoupon": { "removeCoupon": { "id": "5163", "discountAmount": 0, "grandTotal": 5000 } } } } --- # Remove Coupon Remove an applied coupon code from the cart. ## Authentication This mutation supports both authenticated customers and guest users: - **Authenticated customers**: Provide a valid customer authentication token in the `Authorization` header. Obtain this token via the [Customer Login API](/api/graphql-api/shop/mutations/customer-login). - **Guest users**: Provide the Guest Cart Token `cartToken` obtained from the [Create Cart mutation](/api/graphql-api/shop/mutations/create-cart). ``` Authorization: Bearer ``` ## Response | Field | Type | Description | |-------|------|-------------| | `createRemoveCoupon` | Cart | Updated cart without coupon | | `message` | String | Success or error message | | `success` | Boolean | Removal success status | ## Cart Response Fields | Field | Type | Description | |-------|------|-------------| | `id` | String | Cart ID | | `couponCode` | String | Coupon code (null if removed) | | `discountAmount` | Float | Discount amount (0 if removed) | | `grandTotal` | Float | Cart total without discount | | `discountAmount` | Float | Discounted amount | ## Behavior - Removes the applied coupon from the cart - Recalculates cart totals - Discount is no longer applied - Cart becomes valid again without coupon requirement ## Error Responses ```json { "errors": { "cartId": ["Cart not found."], "coupon": ["No coupon is currently applied to this cart."] } } ``` ## Related Documentation - [Apply Coupon](/api/graphql-api/shop/mutations/apply-coupon) - [Get Cart](/api/graphql-api/shop/queries/get-cart) - [Create Cart](/api/graphql-api/shop/mutations/create-cart) --- # Reorder Customer Order URL: /api/graphql-api/shop/mutations/reorder-customer-order --- outline: false examples: - id: reorder-customer-order-basic title: Reorder Items from a Previous Order description: Add all items from a previous order to the customer's cart. query: | mutation { createReorderOrder(input: { orderId: 5 }) { reorderOrder { success message orderId itemsAddedCount } } } variables: | {} response: | { "data": { "createReorderOrder": { "reorderOrder": { "success": true, "message": "Items have been added to your cart successfully (3 items)", "orderId": 5, "itemsAddedCount": 3 } } } } commonErrors: - error: UNAUTHENTICATED cause: Missing or invalid Bearer token solution: Login and provide a valid customer authentication token - error: NOT_FOUND cause: Order with specified ID does not exist or does not belong to the customer solution: Verify the order ID and ensure it belongs to the authenticated customer - error: NO_ITEMS_ADDED cause: No items could be added to cart (all out of stock or unavailable) solution: Check product availability - some items may be discontinued or out of stock - id: reorder-customer-order-with-cart title: Reorder and Fetch Updated Cart description: Reorder items and retrieve the updated cart with all items and totals. query: | mutation { createReorderOrder(input: { orderId: 3 }) { reorderOrder { success message orderId itemsAddedCount } } } query { cart { id itemsCount items { id productId name quantity price } totals { subtotal tax shipping grandTotal } } } variables: | {} response: | { "data": { "createReorderOrder": { "reorderOrder": { "success": true, "message": "Items have been added to your cart successfully (2 items)", "orderId": 3, "itemsAddedCount": 2 } }, "cart": { "id": "1", "itemsCount": 2, "items": [ { "id": "101", "productId": "1", "name": "Blue T-Shirt", "quantity": 1, "price": "29.99" }, { "id": "102", "productId": "2", "name": "Black Jeans", "quantity": 1, "price": "79.99" } ], "totals": { "subtotal": "109.98", "tax": "9.10", "shipping": "10.00", "grandTotal": "129.08" } } } } commonErrors: - error: UNAUTHENTICATED cause: Missing or invalid Bearer token solution: Login and provide a valid customer authentication token - error: NOT_FOUND cause: Order does not exist or belongs to another customer solution: Verify the order ID and ensure it belongs to the authenticated customer --- # Reorder Customer Order ## About The `reorderOrder` mutation allows authenticated customers to quickly re-add items from a previous order to their cart. This enables: - One-click reordering of frequently purchased items - Convenient replenishment of consumables or recurring purchases - Enhanced customer experience by reducing re-selection friction - Support for reordering from any order status (pending, completed, shipped, canceled, etc.) When items are reordered: - Items are added to the customer's current/active cart - Out-of-stock or unavailable items are silently skipped - The mutation succeeds if at least one item is added - Item quantities, prices, and options are preserved from the original order - Inventory is decremented as normal for new cart items Use this feature to: - Enable "Quick Reorder" buttons in order history pages - Support one-click replenishment ordering - Improve customer retention through convenient purchasing - Reduce cart abandonment for repeat purchases - Implement "Frequently Reordered" sections ## Authentication This mutation requires customer authentication: - **Authenticated customers**: Provide a valid customer authentication token in the `Authorization` header. Obtain this token via the [Customer Login API](/api/graphql-api/shop/mutations/customer-login). ``` Authorization: Bearer X-STOREFRONT-KEY: ``` ## Arguments | Argument | Type | Required | Description | |----------|------|----------|-------------| | `input.orderId` | `Int!` | ✅ Yes | The numeric ID of the order to reorder items from. | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `success` | `Boolean!` | `true` if at least one item was added to cart, `false` if no items could be added. | | `message` | `String!` | Human-readable message with outcome (number of items added, reasons for failures, etc.). Localized. | | `orderId` | `Int!` | The numeric ID of the source order. | | `itemsAddedCount` | `Int!` | Number of items successfully added to the cart. | ## Business Logic 1. **Authentication Check**: Verifies customer is logged in 2. **Ownership Validation**: Ensures order belongs to the authenticated customer 3. **Item Iteration**: Loops through each item in the order 4. **Availability Check**: For each item: - Verifies product still exists - Checks if item is purchasable - Validates stock availability 5. **Add to Cart**: Calls cart add-to-cart logic for available items 6. **Silent Failure**: Items that can't be added are skipped without error 7. **Success Response**: Returns count of successfully added items 8. **Inventory Update**: Stock is decremented for each item added ## Success Scenarios | Scenario | Success? | itemsAddedCount | Notes | |----------|----------|-----------------|-------| | All 5 items available | ✅ Yes | 5 | Complete reorder | | 3 of 5 items available | ✅ Yes | 3 | Partial reorder (2 out of stock) | | Only 1 item available | ✅ Yes | 1 | Single item reorder | | No items available | ❌ No | 0 | All items discontinued/out of stock | ## Error Handling ### Order Not Found ```json { "errors": [ { "message": "Order not found (ID: 99999)", "extensions": { "category": "user_error" } } ] } ``` **Cause**: Order ID doesn't exist or belongs to another customer **Solution**: Verify the order ID and ensure it belongs to the authenticated customer ### No Items Could Be Added ```json { "data": { "reorderOrder": { "success": false, "message": "No items were added to cart. All items are out of stock or unavailable.", "orderId": 5, "itemsAddedCount": 0 } } } ``` **Cause**: All items in the order are out of stock, discontinued, or unavailable **Solution**: Check product availability. Some items may have been discontinued or removed from the catalog. ### Unauthenticated Request ```json { "errors": [ { "message": "Unauthenticated", "extensions": { "category": "authentication" } } ] } ``` **Cause**: Missing or invalid Bearer token **Solution**: Login and provide a valid customer authentication token ## Use Cases ### Quick Replenishment Customer frequently reorders the same items: ```graphql mutation { createReorderOrder(input: { orderId: 5 }) { reorderOrder { success message itemsAddedCount } } } ``` Result: 3 items (coffee, tea, sugar) added to cart in one click ### Check Availability Before Purchase ```graphql query { order(id: 42) { id items { productId name quantity } } } mutation { createReorderOrder(input: { orderId: 42 }) { reorderOrder { itemsAddedCount } } } ``` Then proceed to checkout if `itemsAddedCount > 0` ### Reorder with Cart Updates Customer adds new items, then reorders previous favorites: ```graphql mutation { addToCart(input: { productId: 10, quantity: 2 }) { success } createReorderOrder(input: { orderId: 15 }) { reorderOrder { success itemsAddedCount } } } ``` Result: New items + previously ordered items in single cart ## Notes - **Order Status Agnostic**: Items can be reordered from any order status (pending, completed, shipped, canceled, etc.) - **Partial Reorder**: If some items are unavailable, operation succeeds with those available - **Pricing Update**: Current prices from the catalog are used (not original order prices) - **Inventory Decremented**: Reordered items follow normal inventory management rules - **No Special Pricing**: Promotions/discounts from original order are not automatically applied - **Customer Isolation**: Customers cannot reorder other customers' orders - **Localization**: All messages are localized to the customer's language - **Bundled Products**: Bundles and configurable products are handled gracefully ## Implementation Details ### Reorder Behavior | Item Type | Behavior | |-----------|----------| | Simple product | Added with same quantity | | Configurable product | Added with same options/attributes | | Bundle product | Added with same bundle configuration | | Grouped product | Added with same selections | | Out of stock | Silently skipped, not added | | Discontinued | Silently skipped, not added | | Not purchasable | Silently skipped, not added | ### Inventory Management - Reordered items consume inventory normally - Stock is reserved for the active cart - No special reservation or hold period - Stock can run out between reorder and checkout ## Related Resources - [Get Customer Orders](/api/graphql-api/shop/queries/get-customer-orders) — Query orders to enable reorder buttons - [Get Customer Order](/api/graphql-api/shop/queries/get-customer-order) — Query a specific order details - [Get Cart](/api/graphql-api/shop/queries/get-cart) — Check cart after reorder - [Add to Cart](/api/graphql-api/shop/mutations/add-to-cart) — Manual item addition - [Cancel Customer Order](/api/graphql-api/shop/mutations/cancel-customer-order) — Cancel orders - [Customer Login](/api/graphql-api/shop/mutations/customer-login) — Obtain authentication token - [Place Order](/api/graphql-api/shop/mutations/place-order) — Checkout after reorder --- # Set Checkout Address URL: /api/graphql-api/shop/mutations/set-billing-address --- outline: false examples: - id: set-checkout-address-same title: Set Checkout Address — Same for Billing & Shipping description: Set the billing address and use it as the shipping address too by setting useForShipping to true. query: | mutation createCheckoutAddress( $billingFirstName: String! $billingLastName: String! $billingEmail: String! $billingAddress: String! $billingCity: String! $billingCountry: String! $billingState: String! $billingPostcode: String! $billingPhoneNumber: String! $useForShipping: Boolean ) { createCheckoutAddress( input: { billingFirstName: $billingFirstName billingLastName: $billingLastName billingEmail: $billingEmail billingAddress: $billingAddress billingCity: $billingCity billingCountry: $billingCountry billingState: $billingState billingPostcode: $billingPostcode billingPhoneNumber: $billingPhoneNumber useForShipping: $useForShipping } ) { checkoutAddress { _id success message id cartToken billingFirstName billingLastName billingAddress billingCity billingState billingPostcode billingPhoneNumber shippingFirstName shippingLastName shippingAddress shippingCity shippingState shippingPostcode shippingPhoneNumber } } } variables: | { "billingFirstName": "John", "billingLastName": "Doe", "billingEmail": "john@example.com", "billingAddress": "123 Main St", "billingCity": "Los Angeles", "billingCountry": "US", "billingState": "CA", "billingPostcode": "90001", "billingPhoneNumber": "2125551234", "useForShipping": true } response: | { "data": { "createCheckoutAddress": { "checkoutAddress": { "_id": 1239, "success": true, "message": "Address saved successfully", "id": "1239", "cartToken": "255", "billingFirstName": "John", "billingLastName": "Doe", "billingAddress": "123 Main St", "billingCity": "Los Angeles", "billingState": "CA", "billingPostcode": "90001", "billingPhoneNumber": "2125551234", "shippingFirstName": "John", "shippingLastName": "Doe", "shippingAddress": "123 Main St", "shippingCity": "Los Angeles", "shippingState": "CA", "shippingPostcode": "90001", "shippingPhoneNumber": "2125551234" } } } } commonErrors: - error: VALIDATION_ERROR cause: Required billing fields are missing solution: Provide all required billing address fields - error: INVALID_EMAIL cause: Billing email format is invalid solution: Provide a valid email address - error: INVALID_COUNTRY cause: Country code is not valid solution: Use a valid ISO 3166-1 alpha-2 country code - id: set-checkout-address-different title: Set Checkout Address — Different Shipping Address description: Set separate billing and shipping addresses by setting useForShipping to false and providing shipping address fields. query: | mutation createCheckoutAddress( $billingFirstName: String! $billingLastName: String! $billingEmail: String! $billingAddress: String! $billingCity: String! $billingCountry: String! $billingState: String! $billingPostcode: String! $billingPhoneNumber: String! $shippingFirstName: String! $shippingLastName: String! $shippingEmail: String! $shippingAddress: String! $shippingCity: String! $shippingCountry: String! $shippingState: String! $shippingPostcode: String! $shippingPhoneNumber: String! $useForShipping: Boolean ) { createCheckoutAddress( input: { billingFirstName: $billingFirstName billingLastName: $billingLastName billingEmail: $billingEmail billingAddress: $billingAddress billingCity: $billingCity billingCountry: $billingCountry billingState: $billingState billingPostcode: $billingPostcode billingPhoneNumber: $billingPhoneNumber shippingFirstName: $shippingFirstName shippingLastName: $shippingLastName shippingEmail: $shippingEmail shippingAddress: $shippingAddress shippingCity: $shippingCity shippingCountry: $shippingCountry shippingState: $shippingState shippingPostcode: $shippingPostcode shippingPhoneNumber: $shippingPhoneNumber useForShipping: $useForShipping } ) { checkoutAddress { _id success message id cartToken billingFirstName billingLastName billingAddress billingCity billingState billingPostcode billingPhoneNumber shippingFirstName shippingLastName shippingAddress shippingCity shippingState shippingPostcode shippingPhoneNumber } } } variables: | { "billingFirstName": "John", "billingLastName": "Doe", "billingEmail": "john@example.com", "billingAddress": "123 Main St", "billingCity": "Los Angeles", "billingCountry": "US", "billingState": "CA", "billingPostcode": "90001", "billingPhoneNumber": "2125551234", "shippingFirstName": "Jane", "shippingLastName": "Doe", "shippingEmail": "jane@example.com", "shippingAddress": "456 Oak Ave", "shippingCity": "San Francisco", "shippingCountry": "US", "shippingState": "CA", "shippingPostcode": "94102", "shippingPhoneNumber": "4155559876", "useForShipping": false } response: | { "data": { "createCheckoutAddress": { "checkoutAddress": { "_id": 1240, "success": true, "message": "Address saved successfully", "id": "1240", "cartToken": "255", "billingFirstName": "John", "billingLastName": "Doe", "billingAddress": "123 Main St", "billingCity": "Los Angeles", "billingState": "CA", "billingPostcode": "90001", "billingPhoneNumber": "2125551234", "shippingFirstName": "Jane", "shippingLastName": "Doe", "shippingAddress": "456 Oak Ave", "shippingCity": "San Francisco", "shippingState": "CA", "shippingPostcode": "94102", "shippingPhoneNumber": "4155559876" } } } } commonErrors: - error: VALIDATION_ERROR cause: Required shipping fields are missing when useForShipping is false solution: Provide all required shipping address fields - error: INVALID_EMAIL cause: Email format is invalid solution: Provide a valid email address - error: INVALID_COUNTRY cause: Country code is not valid solution: Use a valid ISO 3166-1 alpha-2 country code --- # Set Checkout Address ## About The `createCheckoutAddress` mutation sets the billing and shipping address for the current checkout session. This is a single mutation that handles both addresses based on the `useForShipping` flag: - **`useForShipping: true`** — Only billing address fields are required. The billing address is automatically copied to the shipping address. - **`useForShipping: false`** — Both billing and shipping address fields must be provided separately. This allows the customer to ship to a different address than their billing address. ## Authentication This mutation supports both authenticated customers and guest users: - **Authenticated customers**: Provide a valid customer authentication token in the `Authorization` header. Obtain this token via the [Customer Login API](/api/graphql-api/shop/mutations/customer-login). - **Guest users**: Provide the Guest Cart Token `cartToken` obtained from the [Create Cart mutation](/api/graphql-api/shop/mutations/create-cart). ``` Authorization: Bearer ``` ## Arguments ### Billing Address (always required) | Name | Type | Required | Description | |------|------|----------|-------------| | `billingFirstName` | String | Yes | Billing first name | | `billingLastName` | String | Yes | Billing last name | | `billingEmail` | String | Yes | Billing email address | | `billingAddress` | String | Yes | Billing street address | | `billingCity` | String | Yes | Billing city | | `billingCountry` | String | Yes | Billing country code (ISO 3166-1 alpha-2) | | `billingState` | String | Yes | Billing state/province code | | `billingPostcode` | String | Yes | Billing postal/zip code | | `billingPhoneNumber` | String | Yes | Billing phone number | | `useForShipping` | Boolean | No | If `true`, billing address is used as shipping address. If `false`, separate shipping fields are required. Default: `false` | ### Shipping Address (required when `useForShipping` is `false`) | Name | Type | Required | Description | |------|------|----------|-------------| | `shippingFirstName` | String | Conditional | Shipping first name | | `shippingLastName` | String | Conditional | Shipping last name | | `shippingEmail` | String | Conditional | Shipping email address | | `shippingAddress` | String | Conditional | Shipping street address | | `shippingCity` | String | Conditional | Shipping city | | `shippingCountry` | String | Conditional | Shipping country code (ISO 3166-1 alpha-2) | | `shippingState` | String | Conditional | Shipping state/province code | | `shippingPostcode` | String | Conditional | Shipping postal/zip code | | `shippingPhoneNumber` | String | Conditional | Shipping phone number | ## Response | Field | Type | Description | |-------|------|-------------| | `_id` | Integer | Internal address identifier | | `id` | String | Address ID | | `success` | Boolean | Success status | | `message` | String | Success or error message | | `cartToken` | String | Cart token for the checkout session | | `billingFirstName` | String | Billing first name | | `billingLastName` | String | Billing last name | | `billingAddress` | String | Billing street address | | `billingCity` | String | Billing city | | `billingState` | String | Billing state/province | | `billingPostcode` | String | Billing postal/zip code | | `billingPhoneNumber` | String | Billing phone number | | `shippingFirstName` | String | Shipping first name | | `shippingLastName` | String | Shipping last name | | `shippingAddress` | String | Shipping street address | | `shippingCity` | String | Shipping city | | `shippingState` | String | Shipping state/province | | `shippingPostcode` | String | Shipping postal/zip code | | `shippingPhoneNumber` | String | Shipping phone number | ## Validation Rules - All required billing address fields must be provided - `billingEmail` must be a valid email address - Country codes must be valid ISO 3166-1 alpha-2 codes - Phone numbers should be in valid format - When `useForShipping` is `false`, all shipping address fields become required ## Error Responses ```json { "errors": { "billingEmail": ["The billing email must be a valid email address."], "billingCountry": ["Invalid country code."], "shippingFirstName": ["The shipping first name field is required."] } } ``` ## Related Documentation - [Get Checkout Addresses](/api/graphql-api/shop/queries/get-addresses) - [Set Shipping Method](/api/graphql-api/shop/mutations/set-shipping-method) - [Checkout Flow](/api/graphql-api/shop/checkout) --- # Set Payment Method URL: /api/graphql-api/shop/mutations/set-payment-method --- outline: false examples: - id: set-payment-method title: Set Payment Method description: Set the payment method for a cart. query: | mutation createCheckoutPaymentMethod( $paymentMethod: String!, $successUrl: String, $failureUrl: String, $cancelUrl: String ) { createCheckoutPaymentMethod( input: { paymentMethod: $paymentMethod, paymentSuccessUrl: $successUrl, paymentFailureUrl: $failureUrl, paymentCancelUrl: $cancelUrl } ) { checkoutPaymentMethod { success message paymentGatewayUrl paymentData } } } variables: | { "paymentMethod": "moneytransfer", "successUrl": "https://myapp.com/payment/success", "failureUrl": "https://myapp.com/payment/failure", "cancelUrl": "https://myapp.com/payment/cancel" } response: | { "data": { "createCheckoutPaymentMethod": { "checkoutPaymentMethod": { "success": true, "message": "Payment method saved successfully", "paymentGatewayUrl": null, "paymentData": null } } } } --- # Set Payment Method Set the payment method for a cart. ## Authentication This query supports both authenticated customers and guest users: - **Authenticated customers**: Provide a valid customer authentication token in the `Authorization` header. Obtain this token via the [Customer Login API](/api/graphql-api/shop/mutations/customer-login). - **Guest users**: Provide the Guest Cart Token `cartToken` obtained from the [Create Cart mutation](/api/graphql-api/shop/mutations/create-cart). ``` Authorization: Bearer ``` ## Arguments | Name | Type | Required | Description | |------|------|----------|-------------| | `paymentMethod` | String | ✅ Yes | Payment method code (e.g., moneytransfer, paypal) | | `successUrl` | String | ❌ No | URL to redirect on successful payment | | `failureUrl` | String | ❌ No | URL to redirect on failed payment | | `cancelUrl` | String | ❌ No | URL to redirect on cancelled payment | ## Response | Field | Type | Description | |-------|------|-------------| | `success` | Boolean | Success status | | `message` | String | Success or error message | | `paymentGatewayUrl` | String \| null | Payment gateway URL for external payment processing | | `paymentData` | String \| null | Additional payment data if required | ## Common Payment Method Codes | Code | Description | |------|-------------| | `moneytransfer` | Money Transfer | | `paypal` | PayPal | | `stripe` | Stripe | | `bank_transfer` | Bank Transfer | | `cash_on_delivery` | Cash on Delivery (COD) | ## Prerequisites - Billing address must be set - Shipping method must be selected - Payment method must be enabled in store ## Validation Rules - Payment method code must be valid and enabled - Payment method must be available for the customer's country - Redirect URLs must be valid if provided - Cart must have items - All previous checkout steps must be completed ## Error Responses ```json { "errors": { "paymentMethod": ["Invalid or unavailable payment method."], "successUrl": ["The success URL must be a valid URL."], "billingAddress": ["Billing address must be set first."], "shippingMethod": ["Shipping method must be set first."] } } ``` ## Related Documentation - [Get Payment Methods](/api/graphql-api/shop/queries/get-payment-methods) - [Set Billing Address](/api/graphql-api/shop/mutations/set-billing-address) - [Place Order](/api/graphql-api/shop/mutations/place-order) --- # Set Shipping Address URL: /api/graphql-api/shop/mutations/set-shipping-address --- redirect: /api/graphql-api/shop/mutations/set-billing-address --- This page has been merged into [Set Checkout Address](/api/graphql-api/shop/mutations/set-billing-address). --- # Set Shipping Method URL: /api/graphql-api/shop/mutations/set-shipping-method --- outline: false examples: - id: set-shipping-method title: Set Shipping Method description: Set the shipping method for a checkout. query: | mutation createCheckoutShippingMethod( $shippingMethod: String! ) { createCheckoutShippingMethod( input: { shippingMethod: $shippingMethod } ) { checkoutShippingMethod { success id message } } } variables: | { "shippingMethod": "flatrate_flatrate" } response: | { "data": { "createCheckoutShippingMethod": { "checkoutShippingMethod": { "success": true, "id": "4813", "message": "Shipping method saved successfully" } } } } --- # Set Shipping Method Set the shipping method for a cart. ## Authentication This query supports both authenticated customers and guest users: - **Authenticated customers**: Provide a valid customer authentication token in the `Authorization` header. Obtain this token via the [Customer Login API](/api/graphql-api/shop/mutations/customer-login). - **Guest users**: Provide the Guest Cart Token `cartToken` obtained from the [Create Cart mutation](/api/graphql-api/shop/mutations/create-cart). ``` Authorization: Bearer ``` ## Arguments | Name | Type | Required | Description | |------|------|----------|-------------| | `shippingMethod` | String | ✅ Yes | Shipping method code (e.g., flatrate_flatrate) | ## Response | Field | Type | Description | |-------|------|-------------| | `success` | Boolean | Success status | | `id` | String | Shipping method ID | | `message` | String | Success or error message | ## Common Shipping Method Codes | Code | Description | |------|-------------| | `flatrate_flatrate` | Flat Rate - Fixed shipping cost | | `free_free` | Free Shipping - No shipping charge | ## Prerequisites - Shipping address must be set before this mutation - Available shipping methods depend on: - Cart items - Shipping address country/region - Store configuration ## Validation Rules - Shipping method code must be valid and available - Shipping address must be set - Cart must have items ## Error Responses ```json { "errors": { "shippingMethod": ["Invalid or unavailable shipping method."], "shippingAddress": ["Shipping address must be set first."] } } ``` ## Related Documentation - [Get Shipping Methods](/api/graphql-api/shop/queries/get-shipping-methods) - [Set Checkout Address](/api/graphql-api/shop/mutations/set-billing-address) - [Set Payment Method](/api/graphql-api/shop/mutations/set-payment-method) --- # Toggle Wishlist Item URL: /api/graphql-api/shop/mutations/toggle-wishlist --- outline: false examples: - id: toggle-wishlist-add title: Toggle Wishlist - Add Item description: Toggle a product in the wishlist. If the product is not in the wishlist, it gets added. query: | mutation ToggleWishlist($input: toggleWishlistInput!) { toggleWishlist(input: $input) { wishlist { id _id product { _id id name price } createdAt } } } variables: | { "input": { "productId": 2499 } } response: | { "data": { "toggleWishlist": { "wishlist": { "id": "/api/shop/wishlists/89", "_id": 89, "product": { "_id": 2499, "id": "/api/shop/wishlists/89", "name": "Ivory Frost Classic Overcoat XL", "price": "500" }, "createdAt": "2026-04-07T13:55:19+05:30" } } } } commonErrors: - error: UNAUTHENTICATED cause: Missing or invalid Bearer token solution: Login and provide a valid customer authentication token - error: PRODUCT_NOT_FOUND cause: The product ID does not exist solution: Use a valid product ID that exists in the catalog - id: toggle-wishlist-remove title: Toggle Wishlist - Remove Item description: Toggle a product that already exists in the wishlist. The item is removed and an error-style message is returned with toggleWishlist set to null. query: | mutation ToggleWishlist($input: toggleWishlistInput!) { toggleWishlist(input: $input) { wishlist { id _id product { _id id name price } createdAt } } } variables: | { "input": { "productId": 2499 } } response: | { "errors": [ { "message": "Item Successfully Removed From Wishlist", "locations": [ { "line": 2, "column": 3 } ], "path": [ "toggleWishlist" ] } ], "data": { "toggleWishlist": null } } commonErrors: - error: UNAUTHENTICATED cause: Missing or invalid Bearer token solution: Login and provide a valid customer authentication token --- # Toggle Wishlist Item ## About The `toggleWishlist` mutation adds or removes a product from the authenticated customer's wishlist based on its current state. Use this mutation to: - Implement toggle-style wishlist buttons (heart icons) - Add a product if it's not in the wishlist - Remove a product if it's already in the wishlist - Simplify wishlist UI logic with a single mutation > **Note:** When a product is **removed** from the wishlist, the API returns an error-style response with the message `"Item Successfully Removed From Wishlist"`. When a product is **added**, a standard success response with the wishlist object is returned. ## Authentication This mutation requires customer authentication: - **Authenticated customers**: Provide a valid customer authentication token in the `Authorization` header. Obtain this token via the [Customer Login API](/api/graphql-api/shop/mutations/customer-login). ``` Authorization: Bearer ``` ## Arguments | Argument | Type | Description | |----------|------|-------------| | `productId` | `Int` | The ID of the product to toggle in the wishlist. | | `clientMutationId` | `String` | Optional client-side mutation identifier for tracking. | ## Possible Returns **When item is added:** | Field | Type | Description | |-------|------|-------------| | `wishlist` | `Wishlist!` | The newly created wishlist item. | | `wishlist.id` | `ID!` | IRI identifier (e.g. `/api/shop/wishlists/71`). | | `wishlist._id` | `Int!` | Numeric identifier. | | `wishlist.product` | `Product!` | The associated product. | | `wishlist.createdAt` | `String` | Timestamp when the item was added. | **When item is removed:** | Field | Type | Description | |-------|------|-------------| | `errors[].message` | `String!` | `"Item Successfully Removed From Wishlist"` | --- # Update Cart Item URL: /api/graphql-api/shop/mutations/update-cart-item --- outline: false examples: - id: update-cart-item-quantity title: Update Cart Item Quantity description: Modify the quantity of an item in the shopping cart. query: | mutation createUpdateCartItem( $cartItemId: Int! $quantity: Int! ) { createUpdateCartItem( input: { cartItemId: $cartItemId quantity: $quantity } ) { updateCartItem { id _id cartToken customerId channelId subtotal baseSubtotal discountAmount baseDiscountAmount taxAmount baseTaxAmount shippingAmount baseShippingAmount grandTotal baseGrandTotal formattedSubtotal formattedDiscountAmount formattedTaxAmount formattedShippingAmount formattedGrandTotal couponCode items { totalCount pageInfo { startCursor endCursor hasNextPage hasPreviousPage } edges { cursor node { id cartId productId name sku quantity price basePrice total baseTotal discountAmount baseDiscountAmount taxAmount baseTaxAmount type formattedPrice formattedTotal priceInclTax basePriceInclTax formattedPriceInclTax totalInclTax baseTotalInclTax formattedTotalInclTax productUrlKey canChangeQty } } } success message sessionToken isGuest itemsQty itemsCount haveStockableItems paymentMethod paymentMethodTitle subTotalInclTax baseSubTotalInclTax formattedSubTotalInclTax taxTotal formattedTaxTotal shippingAmountInclTax baseShippingAmountInclTax formattedShippingAmountInclTax } } } variables: | { "cartItemId": 5883, "quantity": 4 } response: | { "data": { "createUpdateCartItem": { "updateCartItem": { "id": "4682", "_id": 4682, "cartToken": "4682", "customerId": 19, "channelId": 1, "subtotal": 780, "baseSubtotal": 780, "discountAmount": 0, "baseDiscountAmount": 0, "taxAmount": 0, "baseTaxAmount": 0, "shippingAmount": 0, "baseShippingAmount": 0, "grandTotal": 780, "baseGrandTotal": 780, "formattedSubtotal": "$780.00", "formattedDiscountAmount": "$0.00", "formattedTaxAmount": "$0.00", "formattedShippingAmount": "$0.00", "formattedGrandTotal": "$780.00", "couponCode": null, "items": { "totalCount": 1, "pageInfo": { "startCursor": "MA==", "endCursor": "MA==", "hasNextPage": false, "hasPreviousPage": false }, "edges": [ { "cursor": "MA==", "node": { "id": "5883", "cartId": 4682, "productId": 2511, "name": "Fine Dining Table Reservation", "sku": "FINE-DINING-TABLE-RESERVATION", "quantity": 4, "price": 195, "basePrice": 195, "total": 780, "baseTotal": 780, "discountAmount": 0, "baseDiscountAmount": 0, "taxAmount": 0, "baseTaxAmount": 0, "type": "booking", "formattedPrice": "$195.00", "formattedTotal": "$780.00", "priceInclTax": 195, "basePriceInclTax": 195, "formattedPriceInclTax": "$195.00", "totalInclTax": 780, "baseTotalInclTax": 780, "formattedTotalInclTax": "$780.00", "productUrlKey": "fine-dining-table-reservation", "canChangeQty": true } } ] }, "success": true, "message": "Cart item updated successfully", "sessionToken": null, "isGuest": false, "itemsQty": 4, "itemsCount": 1, "haveStockableItems": false, "paymentMethod": null, "paymentMethodTitle": null, "subTotalInclTax": 780, "baseSubTotalInclTax": 780, "formattedSubTotalInclTax": "$780.00", "taxTotal": 0, "formattedTaxTotal": "$0.00", "shippingAmountInclTax": 0, "baseShippingAmountInclTax": 0, "formattedShippingAmountInclTax": "$0.00" } } } } commonErrors: - error: ITEM_NOT_FOUND cause: Cart item ID does not exist solution: Verify item ID - error: INVALID_QUANTITY cause: Quantity is invalid or exceeds stock solution: Use valid quantity --- # Update Cart Item ## About The `updateCartItem` mutation modifies the quantity or options of an existing cart item. Use this mutation to: - Update product quantities in the shopping cart - Adjust cart items from the cart page - Modify product options or variants after adding - Handle quantity increase/decrease operations - Validate inventory availability for new quantities - Recalculate cart totals and discounts This mutation validates the new quantity against available inventory and updates cart totals including any applicable discounts and taxes. ::: warning Quantity Update Not Supported for All Product Types **Event booking** and **appointment booking** products do not support quantity updates after being added to the cart. These products have fixed quantities tied to their booking configuration — event bookings have ticket quantities set during add-to-cart, and appointment bookings are always for a single slot. You can identify these items by checking the `canChangeQty` field on the cart item, which will be `false` for these product types. To change the quantity, the customer must remove the item and re-add it with the desired configuration. ::: ## Arguments | Argument | Type | Description | |----------|------|-------------| | `cartItemId` | `Int!` | The numeric ID of the cart item to update. | | `quantity` | `Int!` | The new quantity to set for the cart item. | > **How to get `cartItemId`:** This is the `id` field returned on each cart item from the [Add to Cart](/api/graphql-api/shop/mutations/add-to-cart) or [Get Cart](/api/graphql-api/shop/queries/get-cart) response, available under `items.edges[].node.id`. ## Authentication This mutation supports both authenticated customers and guest users: - **Authenticated customers**: Provide a valid customer authentication token in the `Authorization` header. Obtain this token via the [Customer Login API](/api/graphql-api/shop/mutations/customer-login). - **Guest users**: Provide the Guest Cart Token `cartToken` obtained from the [Create Cart mutation](/api/graphql-api/shop/mutations/create-cart). ``` Authorization: Bearer ``` ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `createUpdateCartItem` | `Cart!` | Updated cart with modified item. | | `updateCartItem` | `CartItem!` | The updated cart. | | `message` | `String!` | Success or error message. | | `success` | `Boolean!` | Indicates successful update. | | `errors` | `[ErrorMessage!]` | Validation errors if quantity unavailable. | --- # Update Customer Address URL: /api/graphql-api/shop/mutations/update-customer-address --- outline: false examples: - id: update-customer-address title: Update Customer Address description: Update an existing customer address. query: | mutation updateCustomerAddress($input: createAddUpdateCustomerAddressInput!) { createAddUpdateCustomerAddress(input: $input) { addUpdateCustomerAddress{ id firstName lastName companyName vatId city state country phone addressId email address1 address2 postcode defaultAddress } } } variables: | { "input": { "addressId": 2851, "firstName": "John", "lastName": "Doe", "companyName": "ANC Corporation", "vatId": "GB123456789", "email": "hello@example.com", "phone": "+918888888888", "address1": "123 Main Street", "address2": "NY", "postcode": "10001", "city": "New York", "state": "NY", "country": "US" } } response: | { "data": { "createAddUpdateCustomerAddress": { "addUpdateCustomerAddress": { "id": "2851", "firstName": "John", "lastName": "Doe", "companyName": "ANC Corporation", "vatId": "GB123456789", "city": "New York", "state": "NY", "country": "US", "phone": "+918888888888", "addressId": 2851, "email": "hello@example.com", "address1": "123 Main Street", "address2": "NY", "postcode": "10001", "defaultAddress": false } } } } --- # Update Customer Address Update an existing customer address. ## Authentication This query requires a valid customer authentication token in the `Authorization` header. Use the [Customer Login API](/api/graphql-api/shop/mutations/customer-login) to retrieve the token. ``` Authorization: Bearer ``` ## Arguments | Name | Type | Required | Description | |------|------|----------|-------------| | `addressId` | Int | ✅ Yes | Address ID to update | | `firstName` | String | ❌ No | First name | | `lastName` | String | ❌ No | Last name | | `companyName` | String | ❌ No | Company name | | `vatId` | String | ❌ No | VAT identification number | | `email` | String | ❌ No | Email address | | `phone` | String | ❌ No | Phone number | | `address1` | String | ❌ No | Street address line 1 | | `address2` | String | ❌ No | Street address line 2 | | `city` | String | ❌ No | City | | `state` | String | ❌ No | State/Province | | `country` | String | ❌ No | Country code | | `postcode` | String | ❌ No | Postal/Zip code | | `defaultAddress` | Boolean | ❌ No | Set as default address | ## Response | Field | Type | Description | |-------|------|-------------| | `addUpdateCustomerAddress` | Address | Updated address object | ## Validation Rules - Address ID must be valid and belong to the customer - All required fields must be provided if being updated - Valid country code must be provided if country is being changed - Phone number should be in valid format if provided ## Error Responses ```json { "errors": { "message": ["Address not found or does not belong to this customer."], } } ``` ## Related Documentation - [Create Customer Address](/api/graphql-api/shop/mutations/create-customer-address) - [Delete Customer Address](/api/graphql-api/shop/mutations/delete-customer-address) - [Get Customer Addresses](/api/graphql-api/shop/queries/get-customer-addresses) --- # Update Customer Profile URL: /api/graphql-api/shop/mutations/update-customer-profile --- outline: false examples: - id: update-customer-profile title: Update Customer Profile description: Update the authenticated customer's profile information. query: | mutation updateCustomerProfile($input: createCustomerProfileUpdateInput!) { createCustomerProfileUpdate(input: $input) { customerProfileUpdate { id firstName lastName email phone gender dateOfBirth status subscribedToNewsLetter isVerified isSuspended image success message } } } variables: | { "input": { "firstName": "Jane", "lastName": "Doe", "email": "jane.doe@example.com", "phone": "+15551234567", "dateOfBirth": "1990-01-15", "gender": "female", "subscribedToNewsLetter": true, "currentPassword": "OldPassword123!", "password": "NewPassword456!", "confirmPassword": "NewPassword456!" } } response: | { "data": { "createCustomerProfileUpdate": { "customerProfileUpdate": { "id": "1", "firstName": "Jane", "lastName": "Doe", "email": "jane.doe@example.com", "phone": "+15551234567", "gender": "female", "dateOfBirth": "1990-01-15", "status": "1", "subscribedToNewsLetter": true, "isVerified": "1", "isSuspended": "0", "image": null, "success": true, "message": "Profile updated successfully" } } } } - id: update-customer-profile-password title: Update Customer Password description: Change the authenticated customer's password by providing the current password and a new password. query: | mutation updateCustomerProfile($input: createCustomerProfileUpdateInput!) { createCustomerProfileUpdate(input: $input) { customerProfileUpdate { id success message } } } variables: | { "input": { "currentPassword": "OldPassword123!", "password": "NewPassword456!", "confirmPassword": "NewPassword456!" } } response: | { "data": { "createCustomerProfileUpdate": { "customerProfileUpdate": { "id": "1", "success": true, "message": "Password updated successfully" } } } } - id: update-customer-profile-image title: Update Profile Image description: Upload a new profile image (base64 encoded) or remove the existing one. query: | mutation updateCustomerProfile($input: createCustomerProfileUpdateInput!) { createCustomerProfileUpdate(input: $input) { customerProfileUpdate { id image success message } } } variables: | { "input": { "image": "data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEA..." } } response: | { "data": { "createCustomerProfileUpdate": { "customerProfileUpdate": { "id": "1", "image": "/storage/customer/1/profile.jpg", "success": true, "message": "Profile image updated successfully" } } } } --- # Update Customer Profile Update the authenticated customer's profile information. All input fields are optional — send only the fields you want to change. > **Note on password fields:** `currentPassword`, `password`, and `confirmPassword` are **only required when the customer wants to change their password**. For a regular profile update (name, email, phone, date of birth, etc.) you can omit all three. If you do change the password, you must send all three together — `currentPassword` to verify the existing password, plus `password` and `confirmPassword` for the new password (which must match). The same mutation also handles profile image updates via the `image` (base64 upload) or `deleteImage` (remove existing) fields. ## Authentication This query requires a valid customer authentication token in the `Authorization` header. Use the [Customer Login API](/api/graphql-api/shop/mutations/customer-login) to retrieve the token. ``` Authorization: Bearer ``` ## Mutation ```graphql mutation updateCustomerProfile($input: createCustomerProfileUpdateInput!) { createCustomerProfileUpdate(input: $input) { customerProfileUpdate { id firstName lastName email phone gender dateOfBirth status subscribedToNewsLetter isVerified isSuspended image success message } } } ``` ## Input Fields | Name | Type | Required | Description | |------|------|----------|-------------| | `firstName` | String | ❌ No | Customer's first name | | `lastName` | String | ❌ No | Customer's last name | | `email` | String | ❌ No | Customer's email address (must be unique) | | `phone` | String | ❌ No | Phone number | | `gender` | String | ❌ No | Gender — one of `male`, `female`, `other` | | `dateOfBirth` | String | ❌ No | Date of birth in `YYYY-MM-DD` format | | `currentPassword` | String | ⚠️ Conditional | Required when changing password | | `password` | String | ⚠️ Conditional | New password | | `confirmPassword` | String | ⚠️ Conditional | New password confirmation (must match `password`) | | `subscribedToNewsLetter` | Boolean | ❌ No | Newsletter subscription flag | | `status` | String | ❌ No | Customer status (admin-controlled fields) | | `isVerified` | String | ❌ No | Verification status | | `isSuspended` | String | ❌ No | Suspension status | | `image` | String | ❌ No | Profile image as a base64 data URI (e.g. `data:image/jpeg;base64,...`) | | `deleteImage` | Boolean | ❌ No | Set to `true` to remove the existing profile image | ## Response Fields The mutation returns the updated profile under `createCustomerProfileUpdate.customerProfileUpdate`. | Field | Type | Description | |-------|------|-------------| | `id` | ID | Customer identifier | | `firstName` | String | First name | | `lastName` | String | Last name | | `email` | String | Email address | | `phone` | String | Phone number | | `gender` | String | Gender | | `dateOfBirth` | String | Date of birth (`YYYY-MM-DD`) | | `status` | String | Customer status | | `subscribedToNewsLetter` | Boolean | Newsletter subscription flag | | `isVerified` | String | Verification flag | | `isSuspended` | String | Suspension flag | | `image` | String | URL/path to stored profile image | | `success` | Boolean | Operation success flag | | `message` | String | Success or error message | ## Validation Rules - First name and last name can contain letters and spaces. - Email must be a valid format and unique across all customers. - Date of birth must be in `YYYY-MM-DD` format. - Gender must be one of: `male`, `female`, `other`. - Password change requires **all three** of `currentPassword`, `password`, and `confirmPassword`. `password` must match `confirmPassword`, and `currentPassword` must match the existing password on file. - `image` must be a valid base64 data URI (`data:image/;base64,...`). - Sending `deleteImage: true` removes any existing stored image — pair it with no `image` field to clear the profile picture. ## Error Responses ```json { "errors": [ { "message": "The email has already been taken.", "extensions": { "category": "validation" } } ] } ``` Other common error cases: - `Current password is incorrect.` — `currentPassword` does not match. - `The password confirmation does not match.` — `password` ≠ `confirmPassword`. - `Invalid image format.` — `image` is not a recognized base64 data URI. - `Unauthenticated.` — missing or invalid Bearer token. ## Related Documentation - [Get Customer Profile](/api/graphql-api/shop/queries/get-customer-profile) - [Delete Customer Profile](/api/graphql-api/shop/mutations/delete-customer-profile) --- # Update Product Review URL: /api/graphql-api/shop/mutations/update-product-review --- outline: false examples: - id: update-product-review-basic title: Update Product Review - Basic description: Update basic product review information like title and comment. query: | mutation updateProductReview($input: updateProductReviewInput!) { updateProductReview(input: $input) { productReview { id _id name title rating comment status } } } variables: | { "input": { "id": "/api/shop/reviews/1", "title": "Updated: Excellent quality and very stylish", "comment": "After using this for a few weeks, I can confirm it's one of the best purchases. Very durable and comfortable." } } response: | { "data": { "updateProductReview": { "productReview": { "id": "/api/shop/reviews/1", "_id": 93, "name": "John Doe", "title": "Updated: Excellent quality and very stylish", "rating": 5, "comment": "After using this for a few weeks, I can confirm it's one of the best purchases. Very durable and comfortable.", "status": 0, } } } } commonErrors: - error: id-required cause: Review ID parameter is missing solution: Provide the review ID in IRI format (e.g., "/api/shop/reviews/1") - error: invalid-id-format cause: Invalid ID format. Expected IRI format like "/api/shop/reviews/1" solution: Use IRI format ID (/api/shop/reviews/{id}) for review updates - error: not-found cause: Review with given ID does not exist solution: Verify the review ID is correct and the review exists - id: update-product-review-status title: Update Product Review - Change Status description: Update product review status (pending, approved, or rejected). query: | mutation updateProductReview($input: updateProductReviewInput!) { updateProductReview(input: $input) { productReview { id _id name title rating comment status } } } variables: | { "input": { "id": "/api/shop/reviews/92", "status": 1 } } response: | { "data": { "updateProductReview": { "productReview": { "id": "/api/shop/reviews/92", "_id": 92, "name": "Jane Smith", "title": "Great Product with Excellent Service", "rating": 5, "comment": "Received the product on time. Packaging was excellent. Product quality is top-notch. Highly satisfied!", "status": 1, } } } } commonErrors: - error: id-required cause: Review ID parameter is missing solution: Provide the review ID in IRI format - error: invalid-id-format cause: Invalid ID format solution: Use IRI format ID (/api/shop/reviews/{id}) - error: not-found cause: Review with given ID does not exist solution: Verify the review ID is correct - error: invalid-status cause: Status value is not valid solution: Use status 0 (pending), 1 (approved), or 2 (rejected) - id: update-product-review-complete title: Update Product Review - Complete Details description: Update all product review fields including rating, comment, and status with tracking. query: | mutation updateProductReview($input: updateProductReviewInput!) { updateProductReview(input: $input) { productReview { id _id name title rating comment status } clientMutationId } } variables: | { "input": { "id": "/api/shop/reviews/1", "productId": 357, "title": "Excellent quality and very stylish", "comment": "Very impressed with the EleganceKnits cardigan sweatercoat. The fabric feels premium and soft, the fitting is perfect, and the collar design adds a classy look. Suitable for office wear as well as casual outings. Lightweight yet warm. Highly recommended.", "rating": 5, "name": "John Doe", "status": 1, "clientMutationId": "demo-review-update-001" } } response: | { "data": { "updateProductReview": { "productReview": { "id": "/api/shop/reviews/1", "_id": 93, "name": "John Doe", "title": "Excellent quality and very stylish", "rating": 5, "comment": "Very impressed with the EleganceKnits cardigan sweatercoat. The fabric feels premium and soft, the fitting is perfect, and the collar design adds a classy look. Suitable for office wear as well as casual outings. Lightweight yet warm. Highly recommended.", "status": 1 }, "clientMutationId": "demo-review-update-001" } } } commonErrors: - error: id-required cause: Review ID parameter is missing solution: Provide the review ID in IRI format (e.g., "/api/shop/reviews/1") - error: invalid-id-format cause: Invalid ID format. Expected IRI format like "/api/shop/reviews/1" solution: Use IRI format ID (/api/shop/reviews/{id}) for review updates - error: not-found cause: Review with given ID does not exist solution: Verify the review ID is correct and the review exists - error: invalid-product-id cause: Product ID is invalid or product does not exist solution: Use a valid product ID that exists in the system - error: invalid-rating cause: Rating value is out of valid range solution: Use rating between 1 and 5 - error: invalid-status cause: Status value is not valid solution: Use status 0 (pending), 1 (approved), or 2 (rejected) --- # Update Product Review ## About The `updateProductReview` mutation allows updating existing product reviews. Use this mutation to: - Update review title and comment - Change review rating - Modify reviewer information - Update review status (pending, approved, rejected) - Correct review mistakes - Approve or reject pending reviews - Track review updates with client mutation ID This mutation requires the review ID in IRI format and returns the updated review with current timestamps. ## Arguments | Argument | Type | Required | Description | |----------|------|----------|-------------| | `id` | `ID!` | ✅ Yes | Review ID in IRI format (e.g., `/api/shop/reviews/1`). Required for identifying which review to update. | | `productId` | `Int` | ❌ No | The ID of the product being reviewed. | | `title` | `String` | ❌ No | Review title/headline. | | `comment` | `String` | ❌ No | Review comment/text. | | `rating` | `Int` | ❌ No | Star rating (1-5). | | `name` | `String` | ❌ No | Reviewer's name. | | `status` | `Int` | ❌ No | Review status (0 = pending, 1 = approved, 2 = rejected). | | `clientMutationId` | `String` | ❌ No | Optional client mutation tracking ID. | ## Input Fields Details ### id - **Type**: ID (IRI Format) - **Required**: Yes - **Format**: `/api/shop/reviews/{id}` or `/api/shop/reviews/{id}` - **Description**: Unique identifier for the review being updated. - **Example**: `/api/shop/reviews/1` - **Note**: Only IRI format is supported for review updates; numeric IDs are not accepted. ### productId - **Type**: Integer - **Required**: No - **Description**: The product ID associated with this review. - **Example**: `357` - **Note**: Typically not changed during review update. ### title - **Type**: String - **Required**: No - **Description**: Review headline. Leave empty to keep current value. - **Example**: `"Excellent quality and very stylish"` ### comment - **Type**: String - **Required**: No - **Description**: Full review text with detailed feedback. Leave empty to keep current value. - **Example**: `"Very impressed with the product..."` ### rating - **Type**: Integer (1-5) - **Required**: No - **Description**: Star rating. Leave empty to keep current value. - **Valid Values**: 1, 2, 3, 4, 5 ### name - **Type**: String - **Required**: No - **Description**: Reviewer's name as displayed on review. - **Example**: `"John Doe"` ### status - **Type**: Integer - **Required**: No - **Valid Values**: - `0` - Pending approval - `1` - Approved and visible - `2` - Rejected/hidden - **Description**: Current review status. - **Example**: `1` ### clientMutationId - **Type**: String - **Required**: No - **Description**: Optional tracking ID for this mutation request. - **Example**: `"demo-review-update-001"` ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `productReview` | `ProductReview!` | The updated product review object. | | `productReview.id` | `ID!` | Unique review API identifier. | | `productReview._id` | `Int!` | Numeric review ID. | | `productReview.name` | `String!` | Reviewer's name. | | `productReview.title` | `String!` | Review title. | | `productReview.rating` | `Int!` | Star rating (1-5). | | `productReview.comment` | `String!` | Review text. | | `productReview.status` | `Int!` | Review status. | | `clientMutationId` | `String` | Echoed client mutation ID for tracking. | ## Review Status | Status | Description | Usage | |--------|-------------|-------| | `0` | Pending | Awaiting admin approval before display | | `1` | Approved | Published on product page | | `2` | Rejected | Hidden from public view | ## ID Format Requirements ### Valid ID Format (IRI) ``` /api/shop/reviews/1 /api/shop/reviews/92 ``` ### Invalid Formats (Not Supported) ``` 93 ❌ Numeric ID only reviews/93 ❌ Partial path /reviews/93 ❌ Incorrect path ``` ## Update Behavior - **Partial Updates**: You can update only specific fields; omitted fields keep their current values - **Required ID**: The review ID must always be provided in IRI format - **Status Changes**: Can change review status from any state to any other state ## Use Cases ### 1. Approve Pending Review Use the "Change Status" example to approve a pending review for display on product page. ### 2. Correct Review Mistake Use the "Basic" example to fix typos or clarifications in review text. ### 3. Update Review Rating Update the rating if customer changed their assessment after further use. ### 4. Reject Inappropriate Review Change status to 2 (rejected) to hide inappropriate content. ### 5. Complete Admin Review Update Use the "Complete" example for comprehensive review updates by admin staff. ## Best Practices 1. **Always Use IRI Format** - Always provide review ID in IRI format (`/api/shop/reviews/{id}`) 2. **Validate Before Update** - Fetch current review data before making changes 3. **Track Changes** - Use clientMutationId for audit trail and tracking 4. **Partial Updates** - Update only necessary fields to preserve existing data 5. **Status Management** - Only approve genuine, quality reviews 6. **Audit Trail** - Log who made changes and when using timestamps 7. **Moderation** - Review text for appropriate content before approval 8. **Notify Users** - Consider notifying customers when review status changes ## Common Update Scenarios ### Approve Review from Pending Set `status: 1` to make pending review visible to customers. ### Reject Inappropriate Review Set `status: 2` to hide review with offensive content. ### Update Customer Feedback Modify `comment` field if customer requests clarification or updates. ### Correct Reviewer Name Update `name` field if incorrect information was initially submitted. ### Change Rating Modify `rating` if customer reassesses product after extended use. ## Error Scenarios ### Missing ID When `id` is not provided, mutation fails with validation error. ### Invalid ID Format When ID is provided in numeric format instead of IRI format. ### Review Not Found When provided ID doesn't correspond to existing review. ### Invalid Status Value When status is outside the valid range (0, 1, 2). ### Invalid Rating Value When rating is outside the valid range (1-5). ## Related Resources - [Create Product Review](/api/graphql-api/shop/mutations/create-product-review) - Create new product reviews - [Get Product Reviews](/api/graphql-api/shop/queries/get-product-reviews) - Query product reviews - [Get Product Review](/api/graphql-api/shop/queries/get-product-review) - Query single review details - [Get Product](/api/graphql-api/shop/queries/get-product) - Query product details - [Mutations Guide](/api/graphql-api/shop/mutations) - Overview of shop mutations - [Shop API Overview](/api/graphql-api/shop-api) - Overview of Shop API resources --- # Shop API - Orders URL: /api/graphql-api/shop/orders # Shop API - Orders Retrieve and manage customer orders. ## Get Customer Orders Retrieve a list of orders for the authenticated customer. ```graphql query GetCustomerOrders($first: Int!) { customerOrders(first: $first) { pageInfo { hasNextPage endCursor } edges { node { id incrementId status grandTotal itemsCount createdAt } } } } ``` **Variables:** ```json { "first": 20 } ``` ## Get Order Details Retrieve complete details for a specific order. ```graphql query GetOrder($id: String!) { order(id: $id) { id incrementId status grandTotal subtotal taxTotal shippingTotal discountAmount createdAt billingAddress { firstName lastName address city state country } shippingAddress { firstName lastName address city state country } items { edges { node { id productId productName quantity price } } } shipments { edges { node { id status trackNumber } } } } } ``` **Variables:** ```json { "id": "1" } ``` ## Track Order Get shipping and tracking information. ```graphql query TrackOrder($incrementId: String!) { order(incrementId: $incrementId) { incrementId status shipments { edges { node { id status carrierTitle trackNumber createdAt items { edges { node { quantity } } } } } } } } ``` ## Related Resources - [Customers](/api/graphql-api/shop/customers) - [Cart](/api/graphql-api/shop/cart) - [Checkout](/api/graphql-api/shop/checkout) --- # Shop API - CMS Pages URL: /api/graphql-api/shop/pages # Shop API - CMS Pages Retrieve CMS (Content Management System) pages from your Bagisto store, including their HTML content, SEO metadata, and locale-specific translations. ## Overview CMS Pages allow store administrators to create and manage static content pages such as About Us, Privacy Policy, Terms & Conditions, and more. This API exposes two queries: | Query | Description | |-------|-------------| | `pages` | Retrieve all CMS pages (paginated) | | `page(id:)` | Retrieve a single CMS page by IRI ID | --- ## Get All CMS Pages Retrieve all CMS pages with their translation details. ```graphql query getCmsPagesDetails { pages { edges { node { id _id layout createdAt updatedAt translation { id _id pageTitle urlKey htmlContent metaTitle metaDescription metaKeywords locale } } } } } ``` --- ## Get Single CMS Page Retrieve a single CMS page by its IRI-style ID. ```graphql query getCmsPageDetail { page(id: "/api/shop/pages/1") { id _id layout createdAt updatedAt translation { id _id pageTitle urlKey htmlContent metaTitle metaDescription metaKeywords locale } } } ``` --- ## Fields Reference ### Page Fields | Field | Type | Description | |-------|------|-------------| | `id` | String | IRI-style unique identifier (e.g. `/api/shop/pages/1`) | | `_id` | Int | Numeric database ID | | `layout` | String | Page layout template (e.g. `default`) | | `createdAt` | DateTime | Timestamp when the page was created | | `updatedAt` | DateTime | Timestamp when the page was last updated | | `translation` | PageTranslation | Active locale translation for this page | ### PageTranslation Fields | Field | Type | Description | |-------|------|-------------| | `id` | String | IRI-style ID of the translation record | | `_id` | Int | Numeric translation record ID | | `pageTitle` | String | Display title of the CMS page | | `urlKey` | String | URL slug (e.g. `about-us`) | | `htmlContent` | String | Full HTML body content of the page | | `metaTitle` | String | SEO meta title | | `metaDescription` | String | SEO meta description | | `metaKeywords` | String | SEO meta keywords | | `locale` | String | Locale code (e.g. `en`, `fr`) | --- ## Related Resources - [Categories](/api/graphql-api/shop/categories) - [Products](/api/graphql-api/shop/products) - [Theme Customisations](/api/graphql-api/shop/queries/theme-customisations) --- # Categories URL: /api/graphql-api/shop/queries/categories --- outline: false examples: - id: get-categories-basic title: Get Categories - Basic description: Retrieve categories with pagination using first and after arguments. query: | query getCategories($first: Int, $after: String) { categories(first: $first, after: $after) { edges { node { id _id position status translation { name slug urlPath } } } pageInfo { hasNextPage endCursor } } } variables: | { "first": 10, "after": null } response: | { "data": { "categories": { "edges": [ { "node": { "id": "/api/shop/categories/1", "_id": 1, "position": 1, "status": "0", "translation": { "name": "Root", "slug": "root", "urlPath": "" } } }, { "node": { "id": "/api/shop/categories/8", "_id": 8, "position": 2, "status": "1", "translation": { "name": "Electronics", "slug": "electronics", "urlPath": "electronics" } } }, { "node": { "id": "/api/shop/categories/19", "_id": 19, "position": 7, "status": "1", "translation": { "name": "Plastic Sofa", "slug": "plastic-sofa", "urlPath": "" } } }, { "node": { "id": "/api/shop/categories/20", "_id": 20, "position": 6, "status": "1", "translation": { "name": "Wooden Sofa", "slug": "wooden-sofa", "urlPath": "" } } }, { "node": { "id": "/api/shop/categories/21", "_id": 21, "position": 5, "status": "1", "translation": { "name": "Leather Sofa", "slug": "leather-sofa", "urlPath": "" } } }, { "node": { "id": "/api/shop/categories/22", "_id": 22, "position": 4, "status": "1", "translation": { "name": "Fashion", "slug": "fashion", "urlPath": "" } } }, { "node": { "id": "/api/shop/categories/23", "_id": 23, "position": 3, "status": "1", "translation": { "name": "Furniture", "slug": "furniture", "urlPath": "" } } } ], "pageInfo": { "hasNextPage": false, "endCursor": "Ng==" } } } } commonErrors: - error: INVALID_FIRST_VALUE cause: First argument exceeds maximum allowed value or is negative solution: Use first value between 1 and 100 - error: INVALID_CURSOR cause: Pagination cursor is invalid solution: Use cursor values from previous response - id: get-categories-complete title: Get Categories - Complete Details description: Retrieve categories with all fields including logos, banners, translations, and children. query: | query getCategories($first: Int, $after: String) { categories(first: $first, after: $after) { edges { node { id _id position status logoPath displayMode _lft _rgt additional bannerPath createdAt updatedAt url logoUrl bannerUrl filterableAttributes { edges { node { id _id code adminName type } } } translation { name slug urlPath } children(first: 100) { edges { node { id _id position status translation { name slug urlPath } } } } translations(first: 1) { edges { node { id _id categoryId name slug urlPath description metaTitle metaDescription metaKeywords localeId locale } cursor } pageInfo { endCursor startCursor hasNextPage hasPreviousPage } totalCount } } } pageInfo { endCursor startCursor hasNextPage hasPreviousPage } totalCount } } variables: | { "first": 10, "after": null } response: | { "data": { "categories": { "edges": [ { "node": { "id": "/api/shop/categories/1", "_id": 1, "position": 1, "status": "0", "logoPath": null, "displayMode": "products_and_description", "_lft": "1", "_rgt": "26", "additional": null, "bannerPath": null, "createdAt": "2024-04-16T16:14:16+05:30", "updatedAt": "2025-08-28T19:13:57+05:30", "url": "https://api-demo.bagisto.com/root", "logoUrl": null, "bannerUrl": null, "filterableAttributes": { "edges": [] }, "translation": { "name": "Root", "slug": "root", "urlPath": "" }, "children": { "edges": [ { "node": { "id": "/api/shop/categories/8", "_id": 8, "position": 2, "status": "1", "translation": { "name": "Electronics", "slug": "electronics", "urlPath": "electronics" } } }, { "node": { "id": "/api/shop/categories/22", "_id": 22, "position": 4, "status": "1", "translation": { "name": "Fashion", "slug": "fashion", "urlPath": "" } } }, { "node": { "id": "/api/shop/categories/23", "_id": 23, "position": 3, "status": "1", "translation": { "name": "Furniture", "slug": "furniture", "urlPath": "" } } } ] }, "translations": { "edges": [ { "node": { "id": "/api/shop/category_translations/1", "_id": 1, "categoryId": "1", "name": "Root", "slug": "root", "urlPath": "", "description": "Root", "metaTitle": "", "metaDescription": "", "metaKeywords": "", "localeId": null, "locale": "en" }, "cursor": "MA==" } ], "pageInfo": { "endCursor": "MA==", "startCursor": "MA==", "hasNextPage": true, "hasPreviousPage": false }, "totalCount": 5 } } }, { "node": { "id": "/api/shop/categories/8", "_id": 8, "position": 2, "status": "1", "logoPath": "category/8/Vk59z6w128ExCrY3lwlSYWhVrYenucFhTuick0VD.webp", "displayMode": "products_and_description", "_lft": "14", "_rgt": "15", "additional": null, "bannerPath": null, "createdAt": "2024-04-19T13:36:12+05:30", "updatedAt": "2026-01-02T19:23:45+05:30", "url": "https://api-demo.bagisto.com/electronics", "logoUrl": "https://api-demo.bagisto.com/storage/category/8/Vk59z6w128ExCrY3lwlSYWhVrYenucFhTuick0VD.webp", "bannerUrl": null, "filterableAttributes": { "edges": [ { "node": { "id": "/api/shop/attributes/1", "_id": 1, "code": "color", "adminName": "Color", "type": "select" } }, { "node": { "id": "/api/shop/attributes/2", "_id": 2, "code": "size", "adminName": "Size", "type": "select" } } ] }, "translation": { "name": "Electronics", "slug": "electronics", "urlPath": "electronics" }, "children": { "edges": [] }, "translations": { "edges": [ { "node": { "id": "/api/shop/category_translations/60", "_id": 60, "categoryId": "8", "name": "Electronics", "slug": "electronics", "urlPath": "electronics", "description": "

Discover a wide range of cutting-edge electronics, from smartphones and laptops to home appliances and gadgets.

", "metaTitle": "Electronics", "metaDescription": "", "metaKeywords": "electronics, electronics-keyboard", "localeId": "1", "locale": "en" }, "cursor": "MA==" } ], "pageInfo": { "endCursor": "MA==", "startCursor": "MA==", "hasNextPage": true, "hasPreviousPage": false }, "totalCount": 2 } } }, { "node": { "id": "/api/shop/categories/22", "_id": 22, "position": 4, "status": "1", "logoPath": "category/22/MDbnYET88gzG1ipz3ClxiKSO2wOybEzESa0o0jHc.webp", "displayMode": "products_and_description", "_lft": "16", "_rgt": "17", "additional": null, "bannerPath": null, "createdAt": "2025-08-28T18:52:22+05:30", "updatedAt": "2026-01-02T19:24:08+05:30", "url": "https://api-demo.bagisto.com/fashion", "logoUrl": "https://api-demo.bagisto.com/storage/category/22/MDbnYET88gzG1ipz3ClxiKSO2wOybEzESa0o0jHc.webp", "bannerUrl": null, "filterableAttributes": { "edges": [ { "node": { "id": "/api/shop/attributes/1", "_id": 1, "code": "color", "adminName": "Color", "type": "select" } } ] }, "translation": { "name": "Fashion", "slug": "fashion", "urlPath": "" }, "children": { "edges": [] }, "translations": { "edges": [ { "node": { "id": "/api/shop/category_translations/186", "_id": 186, "categoryId": "22", "name": "Fashion", "slug": "fashion", "urlPath": "", "description": "

Explore the latest trends in fashion with our curated collection of clothing, accessories, and footwear.

", "metaTitle": "", "metaDescription": "", "metaKeywords": "", "localeId": "1", "locale": "en" }, "cursor": "MA==" } ], "pageInfo": { "endCursor": "MA==", "startCursor": "MA==", "hasNextPage": true, "hasPreviousPage": false }, "totalCount": 2 } } }, { "node": { "id": "/api/shop/categories/23", "_id": 23, "position": 3, "status": "1", "logoPath": "category/23/GuIZOJY3oW09ku4zqxIfKvtXho9gOnq4eCl0HmOW.webp", "displayMode": "products_and_description", "_lft": "18", "_rgt": "25", "additional": null, "bannerPath": null, "createdAt": "2025-09-03T12:43:50+05:30", "updatedAt": "2025-09-03T18:26:45+05:30", "url": "https://api-demo.bagisto.com/furniture", "logoUrl": "https://api-demo.bagisto.com/storage/category/23/GuIZOJY3oW09ku4zqxIfKvtXho9gOnq4eCl0HmOW.webp", "bannerUrl": null, "filterableAttributes": { "edges": [ { "node": { "id": "/api/shop/attributes/3", "_id": 3, "code": "material", "adminName": "Material", "type": "select" } } ] }, "translation": { "name": "Furniture", "slug": "furniture", "urlPath": "" }, "children": { "edges": [ { "node": { "id": "/api/shop/categories/19", "_id": 19, "position": 7, "status": "1", "translation": { "name": "Plastic Sofa", "slug": "plastic-sofa", "urlPath": "" } } }, { "node": { "id": "/api/shop/categories/20", "_id": 20, "position": 6, "status": "1", "translation": { "name": "Wooden Sofa", "slug": "wooden-sofa", "urlPath": "" } } }, { "node": { "id": "/api/shop/categories/21", "_id": 21, "position": 5, "status": "1", "translation": { "name": "Leather Sofa", "slug": "leather-sofa", "urlPath": "" } } } ] }, "translations": { "edges": [ { "node": { "id": "/api/shop/category_translations/195", "_id": 195, "categoryId": "23", "name": "Furniture", "slug": "furniture", "urlPath": "", "description": "

Discover our wide range of furniture designed to bring comfort, style, and functionality to every corner of your home.

", "metaTitle": "", "metaDescription": "", "metaKeywords": "", "localeId": "1", "locale": "en" }, "cursor": "MA==" } ], "pageInfo": { "endCursor": "MA==", "startCursor": "MA==", "hasNextPage": true, "hasPreviousPage": false }, "totalCount": 2 } } } ], "pageInfo": { "endCursor": "Ng==", "startCursor": "MA==", "hasNextPage": false, "hasPreviousPage": false }, "totalCount": 7 } } } commonErrors: - error: INVALID_FIRST_VALUE cause: First argument value is out of bounds solution: Use first value between 1 and 100 - error: INVALID_CURSOR cause: Cursor format is invalid or expired solution: Use cursor values from the previous response - id: get-categories-with-pagination title: Get Categories with Cursor Pagination description: Paginate through categories using cursor-based pagination for optimal performance. query: | query getCategories($first: Int, $after: String, $last: Int, $before: String) { categories(first: $first, after: $after, last: $last, before: $before) { edges { node { id _id position translation { name slug } status children { totalCount } } cursor } pageInfo { endCursor startCursor hasNextPage hasPreviousPage } totalCount } } variables: | { "first": 5, "after": null } response: | { "data": { "categories": { "edges": [ { "node": { "id": "/api/shop/categories/1", "_id": 1, "position": 1, "translation": { "name": "Root", "slug": "root" }, "status": "0", "children": { "totalCount": 3 } }, "cursor": "MA==" }, { "node": { "id": "/api/shop/categories/8", "_id": 8, "position": 2, "translation": { "name": "Electronics", "slug": "electronics" }, "status": "1", "children": { "totalCount": 0 } }, "cursor": "MQ==" }, { "node": { "id": "/api/shop/categories/19", "_id": 19, "position": 7, "translation": { "name": "Plastic Sofa", "slug": "plastic-sofa" }, "status": "1", "children": { "totalCount": 0 } }, "cursor": "Mg==" }, { "node": { "id": "/api/shop/categories/20", "_id": 20, "position": 6, "translation": { "name": "Wooden Sofa", "slug": "wooden-sofa" }, "status": "1", "children": { "totalCount": 0 } }, "cursor": "Mw==" }, { "node": { "id": "/api/shop/categories/21", "_id": 21, "position": 5, "translation": { "name": "Leather Sofa", "slug": "leather-sofa" }, "status": "1", "children": { "totalCount": 0 } }, "cursor": "NA==" } ], "pageInfo": { "endCursor": "NA==", "startCursor": "MA==", "hasNextPage": true, "hasPreviousPage": false }, "totalCount": 7 } } } commonErrors: - error: INVALID_CURSOR cause: Pagination cursor is invalid solution: Use cursor from previous response pageInfo - error: INVALID_PAGINATION_PARAMS cause: Using both forward and backward pagination solution: Use either (first, after) or (last, before), not both - id: get-categories-with-children title: Get Categories with Child Categories description: Retrieve categories along with their child categories for hierarchical display. query: | query getCategories($first: Int, $after: String) { categories(first: $first, after: $after) { edges { node { id _id position translation { name slug } children(first: 50) { edges { node { id _id position translation { name slug } } } totalCount } } cursor } pageInfo { hasNextPage endCursor } totalCount } } variables: | { "first": 5, "after": null } response: | { "data": { "categories": { "edges": [ { "node": { "id": "/api/shop/categories/1", "_id": 1, "position": 1, "translation": { "name": "Root", "slug": "root" }, "children": { "edges": [ { "node": { "id": "/api/shop/categories/8", "_id": 8, "position": 2, "translation": { "name": "Electronics", "slug": "electronics" } } }, { "node": { "id": "/api/shop/categories/22", "_id": 22, "position": 4, "translation": { "name": "Fashion", "slug": "fashion" } } }, { "node": { "id": "/api/shop/categories/23", "_id": 23, "position": 3, "translation": { "name": "Furniture", "slug": "furniture" } } } ], "totalCount": 3 } }, "cursor": "MA==" }, { "node": { "id": "/api/shop/categories/8", "_id": 8, "position": 2, "translation": { "name": "Electronics", "slug": "electronics" }, "children": { "edges": [], "totalCount": 0 } }, "cursor": "MQ==" }, { "node": { "id": "/api/shop/categories/19", "_id": 19, "position": 7, "translation": { "name": "Plastic Sofa", "slug": "plastic-sofa" }, "children": { "edges": [], "totalCount": 0 } }, "cursor": "Mg==" }, { "node": { "id": "/api/shop/categories/20", "_id": 20, "position": 6, "translation": { "name": "Wooden Sofa", "slug": "wooden-sofa" }, "children": { "edges": [], "totalCount": 0 } }, "cursor": "Mw==" }, { "node": { "id": "/api/shop/categories/21", "_id": 21, "position": 5, "translation": { "name": "Leather Sofa", "slug": "leather-sofa" }, "children": { "edges": [], "totalCount": 0 } }, "cursor": "NA==" } ], "pageInfo": { "hasNextPage": true, "endCursor": "NA==" }, "totalCount": 7 } } } commonErrors: - error: INVALID_FIRST_VALUE cause: First value exceeds maximum solution: Use value between 1 and 100 --- # Categories ## About The `categories` query retrieves the complete list of product categories with full details including translations, media assets, and hierarchy information. Use this query to: - Build category navigation menus and sidebars - Display breadcrumb paths for product browsing - Implement category-based product filtering - Create category landing pages and collections - Sync category structure with external systems - Display category metadata (images, descriptions, logos, banners) - Support multi-language category content - Show category hierarchy with children counts This query supports full pagination with cursor-based navigation and includes complete SEO metadata and display settings for each category. ## Arguments | Argument | Type | Required | Description | |----------|------|----------|-------------| | `first` | `Int` | ❌ No | Number of categories to return (forward pagination). Max: 100. | | `after` | `String` | ❌ No | Pagination cursor for forward navigation. Use with `first`. | | `last` | `Int` | ❌ No | Number of categories for backward pagination. Max: 100. | | `before` | `String` | ❌ No | Pagination cursor for backward navigation. Use with `last`. | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `id` | `ID!` | Unique category API identifier. | | `_id` | `Int!` | Numeric category ID. | | `position` | `Int` | Display position among siblings. | | `logoPath` | `String` | File path to category logo. | | `logoUrl` | `String` | Full URL to category logo image. | | `bannerPath` | `String` | File path to category banner. | | `bannerUrl` | `String` | Full URL to category banner image. | | `status` | `Int` | Category status (0 = inactive, 1 = active). | | `displayMode` | `String` | Display mode: `products_only`, `category_and_products`, `products_and_description`. | | `_lft` | `Int` | Left value for nested set tree structure. | | `_rgt` | `Int` | Right value for nested set tree structure. | | `additional` | `String` | Additional metadata (JSON format). | | `translation` | `CategoryTranslation!` | Default locale translation. | | `translation.id` | `ID!` | Translation identifier. | | `translation._id` | `Int!` | Numeric translation ID. | | `translation.categoryId` | `Int!` | Associated category ID. | | `translation.name` | `String!` | Category name in current language. | | `translation.slug` | `String!` | URL slug for the category. | | `translation.urlPath` | `String!` | Full URL path including hierarchy. | | `translation.description` | `String` | Category description text. | | `translation.metaTitle` | `String` | SEO meta title tag. | | `translation.metaDescription` | `String` | SEO meta description. | | `translation.metaKeywords` | `String` | SEO keywords. | | `translation.localeId` | `Int` | Locale identifier. | | `translation.locale` | `String!` | Language locale code (e.g., 'en', 'ar', 'fr'). | | `translations` | `CategoryTranslationCollection!` | All available translations. | | `translations.edges` | `[Edge!]!` | Translation edges with cursors. | | `translations.pageInfo` | `PageInfo!` | Pagination info for translations. | | `translations.totalCount` | `Int!` | Total translations for this category. | | `createdAt` | `DateTime!` | Category creation timestamp. | | `updatedAt` | `DateTime!` | Last update timestamp. | | `url` | `String` | Full category URL. | | `filterableAttributes` | `AttributeCollection!` | Attributes configured for filtering in this category. | | `filterableAttributes.edges` | `[Edge!]!` | Attribute edges. | | `filterableAttributes.edges.node.id` | `ID!` | Attribute API identifier. | | `filterableAttributes.edges.node._id` | `Int!` | Numeric attribute ID. | | `filterableAttributes.edges.node.code` | `String!` | Attribute code (e.g., `color`, `size`). | | `filterableAttributes.edges.node.adminName` | `String!` | Attribute label shown in admin panel. | | `filterableAttributes.edges.node.type` | `String!` | Attribute input type (e.g., `select`, `multiselect`, `boolean`). | | `children` | `CategoryCollection!` | Child categories. | | `children.edges` | `[Edge!]!` | Child category edges. | | `children.totalCount` | `Int!` | Total child categories. | | `pageInfo` | `PageInfo!` | Pagination information. | | `pageInfo.hasNextPage` | `Boolean!` | Whether more pages exist forward. | | `pageInfo.hasPreviousPage` | `Boolean!` | Whether more pages exist backward. | | `pageInfo.startCursor` | `String` | Cursor for first item in page. | | `pageInfo.endCursor` | `String` | Cursor for last item in page. | | `totalCount` | `Int!` | Total categories matching filters. | ## Use Cases ### 1. Main Navigation Menu Use the "Optimized for Navigation" example for rendering dropdown category menus. ### 2. Multi-Language Support Use the "With All Translations" example to display categories in multiple languages. ### 3. Category Listing Page Use the "Complete Details" example for full category information with images. ### 4. Pagination Use the "With Cursor Pagination" example for handling large category lists. ## Best Practices 1. **Request Only Needed Fields** - Minimize data transfer by selecting only required fields 2. **Use Pagination** - Always use pagination for better performance with many categories 3. **Cache Results** - Categories change infrequently, cache the full list 4. **Filter by Status** - Only fetch active categories by default 5. **Include SEO Data** - Always fetch meta tags for search engine optimization 6. **Use Translations** - Fetch translations for multi-language support ## Related Resources - [Tree Categories](/api/graphql-api/shop/queries/tree-categories) - Hierarchical category tree for navigation - [Get Products](/api/graphql-api/shop/queries/get-products) - Query products within a category - [Pagination Guide](/api/graphql-api/pagination) - Cursor pagination documentation - [Shop API Overview](/api/graphql-api/shop-api) - Overview of Shop API resources --- # Download Downloadable Product URL: /api/graphql-api/shop/queries/download-downloadable-product --- outline: false apiType: rest examples: - id: download-purchased-product title: Download Purchased Product description: Download the actual purchased downloadable product file. Requires customer authentication. Each download decrements the remaining download count. query: | curl -X GET "https://api-demo.bagisto.com/api/shop/customer-downloadable-products/4/download" \ -H "X-STOREFRONT-KEY: " \ -H "Authorization: Bearer " \ -o purchased-product.png variables: | {} response: | HTTP/1.1 200 OK Content-Type: image/png (or application/pdf, application/zip, etc.) Content-Disposition: attachment; filename="downloaded-file.png" [Binary file content — the actual purchased file] The response is the raw file content, not JSON. In Postman, the file opens directly in the browser or downloads automatically. In cURL, use the -o flag to save it to a file. On authentication failure (HTTP 401): { "message": "Unauthorized: Customer authentication required.", "error": "unauthenticated" } commonErrors: - error: Unauthenticated cause: Missing or invalid Bearer token solution: Login and provide a valid customer authentication token - error: Not Found cause: The purchase ID does not exist or belongs to another customer solution: Verify the purchase ID from the Get Downloadable Products query - error: Download Limit Reached cause: All allowed downloads have been used solution: Check remainingDownloads via the Get Downloadable Products query --- # Download Downloadable Product ## About After a customer purchases a downloadable product and the order is completed, the purchased files can be downloaded using the `downloadUrl` field. This URL is available from both the [Get Downloadable Products](/api/graphql-api/shop/queries/get-customer-downloadable-products) query (list of all purchases) and the [Get Downloadable Product](/api/graphql-api/shop/queries/get-customer-downloadable-product) query (single purchase). Use the `downloadUrl` value directly in a cURL request to download the file. This download is handled via a **REST API endpoint** (not a GraphQL query), as the response is binary file content rather than JSON. ## Purchased Product Downloads After a customer purchases a downloadable product and the order is completed, they can download the actual product files. This requires customer authentication. Use the `downloadUrl` from the GraphQL query response as the URL in your cURL request. **Endpoint:** ``` GET /api/shop/customer-downloadable-products/{purchase_id}/download ``` The `{purchase_id}` is the `_id` field from the [Get Downloadable Products](/api/graphql-api/shop/queries/get-customer-downloadable-products) query. ### Authentication This endpoint requires a valid customer authentication token: ``` Authorization: Bearer X-STOREFRONT-KEY: pk_storefront_your_key_here ``` ### Example ```bash curl -X GET "https://your-domain.com/api/shop/customer-downloadable-products/403/download" \ -H "X-STOREFRONT-KEY: pk_storefront_your_key_here" \ -H "Authorization: Bearer 2879|mZB2airbqULCamPo5LmADjzYawSRYUM8lyvWIM3e" \ -o purchased-product.png ``` ## Implementation Guide > **Important:** When implementing the purchased product download in your application, do **not** directly save the response to a file (i.e. do not use `-o` on the first request). Instead, first make the request **without** `-o` and check the response. If authentication fails, the API returns a JSON error response instead of the file content. If you blindly save with `-o`, you may end up saving the error JSON as your "downloaded file." **Recommended two-step approach:** **Step 1 — Verify the request:** ```bash curl -X GET "https://your-domain.com/api/shop/customer-downloadable-products/403/download" \ -H "X-STOREFRONT-KEY: pk_storefront_your_key_here" \ -H "Authorization: Bearer " ``` If the token is invalid or missing, you will see: ```json { "message": "Unauthorized: Customer authentication required.", "error": "unauthenticated" } ``` **Step 2 — Download the file (only after confirming success):** ```bash curl -X GET "https://your-domain.com/api/shop/customer-downloadable-products/403/download" \ -H "X-STOREFRONT-KEY: pk_storefront_your_key_here" \ -H "Authorization: Bearer " \ -o purchased-product.png ``` Alternatively, in your application code, check the response `Content-Type` header — a successful download returns a file content type (e.g. `image/png`, `application/pdf`), while an error returns `application/json`. ## Headers | Header | Required | Description | |--------|----------|-------------| | `X-STOREFRONT-KEY` | Yes | Storefront API key | | `Authorization` | Only for purchased downloads | Customer authentication token (`Bearer `) | ## Related Documentation - [Get Downloadable Products](/api/graphql-api/shop/queries/get-customer-downloadable-products) — List all purchased downloadable products - [Get Downloadable Product](/api/graphql-api/shop/queries/get-customer-downloadable-product) — Get details of a single purchased downloadable product --- # Download Invoice URL: /api/graphql-api/shop/queries/download-invoice --- outline: false apiType: rest examples: - id: download-invoice-with-bearer-token title: Download Invoice PDF with Bearer Token description: Download a customer invoice as PDF using the downloadUrl with Bearer token authentication. query: | curl -X GET "https://api-demo.bagisto.com/api/shop/customer-invoices/1/pdf" \ -H "X-STOREFRONT-KEY: " \ -H "Authorization: Bearer " \ -o invoice-1.pdf variables: | {} response: | HTTP/1.1 200 OK Content-Type: application/pdf Content-Disposition: attachment; filename="invoice-1-02-19-2026.pdf" Content-Length: 4352 [PDF Binary Content] commonErrors: - error: UNAUTHENTICATED cause: Missing or invalid Bearer token solution: Login and provide a valid customer authentication token - error: NOT_FOUND cause: Invoice with specified ID does not exist or does not belong to the customer solution: Verify the invoice ID and ensure it belongs to the authenticated customer - error: FORBIDDEN cause: Customer is trying to download another customer's invoice solution: Verify the invoice ID belongs to the authenticated customer - id: download-invoice-via-graphql-url title: Get Invoice Download URL via GraphQL description: Retrieve the downloadUrl for a customer invoice via GraphQL, then use it to download the PDF. query: | query GetInvoiceDownloadUrl { customerInvoice(id: "/api/shop/customer-invoices/1") { _id incrementId downloadUrl } } variables: | {} response: | { "data": { "customerInvoice": { "_id": 1, "incrementId": "INV-001", "downloadUrl": "https://api-demo.bagisto.com/api/shop/customer-invoices/1/pdf" } } } commonErrors: - error: UNAUTHENTICATED cause: Missing or invalid Bearer token solution: Login and provide a valid customer authentication token - error: NOT_FOUND cause: Invoice with specified ID does not exist solution: Verify the invoice ID and ensure it belongs to the authenticated customer --- # Download Invoice ## About The `downloadUrl` field from invoice queries provides a direct link to download customer invoices as PDF files. This REST endpoint allows customers to: - Download invoices as PDF files - Use the `downloadUrl` directly from GraphQL invoice queries - Implement invoice download functionality in customer portals - Generate invoice archives - Email invoices to customers The download endpoint is secured with Bearer token authentication and enforces customer isolation — customers can only download their own invoices. ## Authentication The download endpoint requires customer authentication: - **Method**: Bearer Token in Authorization header - **Header**: `Authorization: Bearer ` - **Storefront Key**: `X-STOREFRONT-KEY: ` Obtain the access token via the [Customer Login API](/api/graphql-api/shop/mutations/customer-login). ## Getting the Download URL ### Via GraphQL Retrieve the `downloadUrl` from any invoice query: ```graphql query GetInvoiceDownloadUrl { customerInvoice(id: "/api/shop/customer-invoices/1") { downloadUrl } } ``` Or retrieve multiple download URLs: ```graphql query GetInvoiceList { customerInvoices(first: 10) { edges { node { _id incrementId downloadUrl } } } } ``` ## Download Endpoint ### URL Format ``` GET /api/shop/customer-invoices/{invoiceId}/pdf ``` ### Parameters | Parameter | Type | Location | Required | Description | |-----------|------|----------|----------|-------------| | `invoiceId` | Integer | URL Path | ✅ Yes | The numeric invoice ID to download. | ### Headers | Header | Value | Required | Description | |--------|-------|----------|-------------| | `Authorization` | `Bearer ` | ✅ Yes | Valid customer authentication token. | | `X-STOREFRONT-KEY` | `` | ✅ Yes | Storefront API key. | ## Download Examples ### Using cURL with Bearer Token ```bash curl -X GET "https://api-demo.bagisto.com/api/shop/customer-invoices/1/pdf" \ -H "X-STOREFRONT-KEY: pk_storefront_qrr4vsdbs6xNpL7DN0GHUcB0XnhjnjIS" \ -H "Authorization: Bearer 4|RZI3ySNlzbcz5osLbnfuAcTgy2eqRN5i987eUsMS22e18f1c" \ -o invoice-1.pdf ``` ### Using JavaScript/Fetch ```javascript const invoiceId = 1; const downloadUrl = `https://api-demo.bagisto.com/api/shop/customer-invoices/${invoiceId}/pdf`; const accessToken = ''; const storefrontKey = ''; fetch(downloadUrl, { method: 'GET', headers: { 'Authorization': `Bearer ${accessToken}`, 'X-STOREFRONT-KEY': storefrontKey } }) .then(response => response.blob()) .then(blob => { const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'invoice.pdf'; a.click(); window.URL.revokeObjectURL(url); }); ``` ### Using Python Requests ```python import requests invoice_id = 1 download_url = f"https://api-demo.bagisto.com/api/shop/customer-invoices/{invoice_id}/pdf" access_token = '' storefront_key = '' headers = { 'Authorization': f'Bearer {access_token}', 'X-STOREFRONT-KEY': storefront_key } response = requests.get(download_url, headers=headers) if response.status_code == 200: with open(f'invoice-{invoice_id}.pdf', 'wb') as f: f.write(response.content) print("Invoice downloaded successfully!") else: print(f"Error: {response.status_code}") ``` ## Response ### Success Response ``` HTTP/1.1 200 OK Content-Type: application/pdf Content-Disposition: attachment; filename="invoice-1-02-19-2026.pdf" Content-Length: 4352 [PDF Binary Content] ``` ### Response Headers | Header | Value | Description | |--------|-------|-------------| | `Content-Type` | `application/pdf` | MIME type indicating PDF file. | | `Content-Disposition` | `attachment; filename="..."` | Instructions to download as file attachment. | | `Content-Length` | `{size}` | Size of the PDF file in bytes. | ## Error Responses ### 401 Unauthorized - Missing Token ``` HTTP/1.1 401 Unauthorized Content-Type: text/html Redirects to login page ``` **Cause**: Missing `Authorization` header or invalid token **Solution**: Provide a valid customer access token via `Authorization: Bearer ` header ### 403 Forbidden - Invalid Token ```json { "message": "Unauthenticated. Please login to perform this action", "exception": "AuthorizationException" } ``` **Cause**: Bearer token is invalid or expired **Solution**: Login again to obtain a fresh access token ### 404 Not Found ```json { "errors": [ { "message": "Invoice with ID \"999\" not found", "path": ["customerInvoice"] } ] } ``` **Cause**: Invoice does not exist or belongs to another customer **Solution**: Verify the invoice ID and ensure it belongs to the authenticated customer ## Use Cases - **Customer Dashboard**: Direct download button for each invoice - **Invoice Archive**: Batch download multiple invoices - **Email Integration**: Attach invoices to customer emails - **Document Management**: Store invoices locally for recordkeeping - **Accounting Software**: Import invoices into accounting systems - **Receipt Generation**: Generate customer receipts with invoice details ## Notes - **Authentication Required**: All downloads require a valid Bearer token - **Customer Isolation**: Customers can only download their own invoices - **PDF Format**: Invoices are generated as standard PDF documents (version 1.7) - **File Size**: PDF size varies based on invoice complexity (typically 3-10KB) - **Caching**: Download URLs are unique per invoice and do not expire - **Rate Limiting**: Subject to API rate limiting Same as other authenticated endpoints ## Implementation Workflow 1. **Get Invoice List**: Query `customerInvoices` to retrieve invoice IDs and `downloadUrl` 2. **Extract Download URL**: Use the `downloadUrl` from the GraphQL response 3. **Add Download Link**: Create a download button/link using the URL 4. **User Clicks Download**: Browser requests the PDF with authentication headers 5. **Receive PDF**: Browser downloads the invoice PDF file ## Related Resources - [Get Customer Invoice](/api/graphql-api/shop/queries/get-customer-invoice) — Query a single invoice with downloadUrl - [Get All Customer Invoices](/api/graphql-api/shop/queries/get-customer-invoices) — Query all invoices with downloadUrl - [Customer Login](/api/graphql-api/shop/mutations/customer-login) — Obtain authentication token - [REST API Authentication](/api/rest-api/authentication) — Learn more about API authentication --- # Get Checkout Addresses URL: /api/graphql-api/shop/queries/get-addresses --- outline: false examples: - id: get-addresses title: Get Checkout Addresses description: Retrieve the address applied to the current checkout session — either the guest-entered address or the authenticated customer's selected checkout address. query: | query collectionGetCheckoutAddresses { collectionGetCheckoutAddresses { edges { node { id _id name addressType parentAddressId firstName lastName companyName address city state country postcode email phone vatId defaultAddress useForShipping additional createdAt updatedAt } } } } variables: | {} response: | { "data": { "collectionGetCheckoutAddresses": { "edges": [ { "node": { "id": "/api/shop/get_checkout_addresses/3025", "_id": 3025, "name": "John Doe", "addressType": "cart_billing", "parentAddressId": null, "firstName": "John", "lastName": "Doe", "companyName": null, "address": "123 Main St", "city": "Los Angeles", "state": "CA", "country": "US", "postcode": "90001", "email": "john@example.com", "phone": "2125551234", "vatId": null, "defaultAddress": false, "useForShipping": false, "additional": null, "createdAt": "2026-04-02T13:44:34+05:30", "updatedAt": "2026-04-02T13:44:34+05:30" } }, { "node": { "id": "/api/shop/get_checkout_addresses/3026", "_id": 3026, "name": "John Doe", "addressType": "cart_shipping", "parentAddressId": null, "firstName": "John", "lastName": "Doe", "companyName": null, "address": "123 Main St", "city": "Los Angeles", "state": "CA", "country": "US", "postcode": "90001", "email": "john@example.com", "phone": "2125551234", "vatId": null, "defaultAddress": false, "useForShipping": false, "additional": null, "createdAt": "2026-04-02T13:44:34+05:30", "updatedAt": "2026-04-02T13:44:34+05:30" } } ] } } } --- # Get Checkout Addresses Retrieve the address associated with the current checkout session. > **Note:** This query does **not** return all saved addresses for a customer. It returns only the address applied to the active checkout — either the address entered during guest checkout or the address the authenticated customer has selected for the current order. To fetch all saved customer addresses, use the [Get Customer Addresses](/api/graphql-api/shop/queries/get-customer-addresses) query instead. ## Authentication This query supports both authenticated customers and guest users: - **Authenticated customers**: Provide a valid customer authentication token in the `Authorization` header. Obtain this token via the [Customer Login API](/api/graphql-api/shop/mutations/customer-login). - **Guest users**: Provide the Guest Cart Token `cartToken` obtained from the [Create Cart mutation](/api/graphql-api/shop/mutations/create-cart). ``` Authorization: Bearer ``` ## Response | Field | Type | Description | |-------|------|-------------| | `id` | String | Address ID | | `addressType` | String | Type of address (billing or shipping) | | `firstName` | String | First name | | `lastName` | String | Last name | | `address` | String | Street address | | `city` | String | City | | `state` | String | State/Province | | `country` | String | Country code | | `postcode` | String | Postal/Zip code | | `email` | String | Email address | | `phone` | String | Phone number | | `useForShipping` | Boolean | Whether this address is also used for shipping | | `defaultAddress` | Boolean | Whether this is the customer's default address | | `createdAt` | DateTime | When address was created | ## Use Cases - Confirm the address applied to the current checkout session - Display the selected address on the order review/summary page - Verify guest checkout address before placing the order ## Related Documentation - [Get Customer Addresses](/api/graphql-api/shop/queries/get-customer-addresses) - [Set Checkout Address](/api/graphql-api/shop/mutations/set-billing-address) - [Set Billing Address](/api/graphql-api/shop/mutations/set-billing-address) --- # Get Attribute URL: /api/graphql-api/shop/queries/get-attribute --- outline: false examples: - id: get-attribute-basic title: Get Attribute - Basic description: Retrieve basic attribute information by ID. query: | query getAttributeByID($id: ID!){ attribute(id: $id) { id _id code adminName type swatchType validation regex position isRequired isUnique isFilterable isComparable isConfigurable isUserDefined isVisibleOnFront valuePerLocale valuePerChannel defaultValue enableWysiwyg createdAt updatedAt columnName validations } } variables: | { "id": "/api/shop/attributes/23" } response: | { "data": { "attribute": { "id": "/api/shop/attributes/23", "_id": 23, "code": "color", "adminName": "Color", "type": "select", "swatchType": "color", "validation": null, "regex": null, "position": 26, "isRequired": "0", "isUnique": "0", "isFilterable": "1", "isComparable": "0", "isConfigurable": "1", "isUserDefined": "1", "isVisibleOnFront": "0", "valuePerLocale": "0", "valuePerChannel": "0", "defaultValue": null, "enableWysiwyg": "0", "createdAt": "2023-11-02T16:40:10+05:30", "updatedAt": "2023-12-06T12:52:51+05:30", "columnName": "integer_value", "validations": "{ }" } } } commonErrors: - error: Variable \"$id\" of required type \"ID!\" was not provided. cause: Attribute ID parameter is required solution: Provide a valid attribute ID in format /api/shop/attributes/{id} - error: Invalid ID format. Expected IRI format like \"/api/shop/attributes/1\" or numeric ID cause: Attribute ID is not valid solution: Verify the attribute ID is correct format - error: Attribute not found cause: Attribute ID does not exist solution: Verify the attribute ID is correct - id: get-attribute-with-details title: Get Attribute with Full Details description: Retrieve attribute with all configuration flags and metadata. query: | query getAttributeByID($id: ID!){ attribute(id: $id) { id _id code adminName type swatchType validation regex position isRequired isUnique isFilterable isComparable isConfigurable isUserDefined isVisibleOnFront valuePerLocale valuePerChannel defaultValue enableWysiwyg createdAt updatedAt columnName validations options { edges { node { id _id adminName sortOrder swatchValue swatchValueUrl translation { id _id attributeOptionId locale label } translations { edges { node { id _id attributeOptionId locale label } } pageInfo { endCursor startCursor hasNextPage hasPreviousPage } totalCount } } cursor } pageInfo { endCursor startCursor hasNextPage hasPreviousPage } totalCount } translations { edges { node { id _id attributeId locale name } cursor } pageInfo { endCursor startCursor hasNextPage hasPreviousPage } totalCount } } } variables: | { "id": "/api/shop/attributes/23" } response: | { "data": { "attribute": { "id": "/api/shop/attributes/23", "_id": 23, "code": "color", "adminName": "Color", "type": "select", "swatchType": "color", "validation": null, "regex": null, "position": 26, "isRequired": "0", "isUnique": "0", "isFilterable": "1", "isComparable": "0", "isConfigurable": "1", "isUserDefined": "1", "isVisibleOnFront": "0", "valuePerLocale": "0", "valuePerChannel": "0", "defaultValue": null, "enableWysiwyg": "0", "createdAt": "2023-11-02T16:40:10+05:30", "updatedAt": "2023-12-06T12:52:51+05:30", "columnName": "integer_value", "validations": "{ }", "options": { "edges": [ { "node": { "id": "/api/shop/attribute-options/1", "_id": 1, "adminName": "Red", "sortOrder": 0, "swatchValue": "#e10e0e", "swatchValueUrl": null, "translation": { "id": "/api/attribute_option_translations/1", "_id": 1, "attributeOptionId": "1", "locale": "en", "label": "Red" }, "translations": { "edges": [ { "node": { "id": "/api/attribute_option_translations/84", "_id": 84, "attributeOptionId": "1", "locale": "ar", "label": "" } }, { "node": { "id": "/api/attribute_option_translations/1", "_id": 1, "attributeOptionId": "1", "locale": "en", "label": "Red" } } ], "pageInfo": { "endCursor": "MQ==", "startCursor": "MA==", "hasNextPage": false, "hasPreviousPage": false }, "totalCount": 2 } }, "cursor": "MA==" }, { "node": { "id": "/api/shop/attribute-options/2", "_id": 2, "adminName": "Green", "sortOrder": 1, "swatchValue": "#155616", "swatchValueUrl": null, "translation": { "id": "/api/attribute_option_translations/2", "_id": 2, "attributeOptionId": "2", "locale": "en", "label": "Green" }, "translations": { "edges": [ { "node": { "id": "/api/attribute_option_translations/85", "_id": 85, "attributeOptionId": "2", "locale": "ar", "label": "" } }, { "node": { "id": "/api/attribute_option_translations/2", "_id": 2, "attributeOptionId": "2", "locale": "en", "label": "Green" } } ], "pageInfo": { "endCursor": "MQ==", "startCursor": "MA==", "hasNextPage": false, "hasPreviousPage": false }, "totalCount": 2 } }, "cursor": "MQ==" }, { "node": { "id": "/api/shop/attribute-options/3", "_id": 3, "adminName": "Yellow", "sortOrder": 2, "swatchValue": "#f6fa00", "swatchValueUrl": null, "translation": { "id": "/api/attribute_option_translations/3", "_id": 3, "attributeOptionId": "3", "locale": "en", "label": "Yellow" }, "translations": { "edges": [ { "node": { "id": "/api/attribute_option_translations/86", "_id": 86, "attributeOptionId": "3", "locale": "ar", "label": "" } }, { "node": { "id": "/api/attribute_option_translations/3", "_id": 3, "attributeOptionId": "3", "locale": "en", "label": "Yellow" } } ], "pageInfo": { "endCursor": "MQ==", "startCursor": "MA==", "hasNextPage": false, "hasPreviousPage": false }, "totalCount": 2 } }, "cursor": "Mg==" }, { "node": { "id": "/api/shop/attribute-options/4", "_id": 4, "adminName": "Black", "sortOrder": 3, "swatchValue": "#000000", "swatchValueUrl": null, "translation": { "id": "/api/attribute_option_translations/4", "_id": 4, "attributeOptionId": "4", "locale": "en", "label": "Black" }, "translations": { "edges": [ { "node": { "id": "/api/attribute_option_translations/87", "_id": 87, "attributeOptionId": "4", "locale": "ar", "label": "" } }, { "node": { "id": "/api/attribute_option_translations/4", "_id": 4, "attributeOptionId": "4", "locale": "en", "label": "Black" } } ], "pageInfo": { "endCursor": "MQ==", "startCursor": "MA==", "hasNextPage": false, "hasPreviousPage": false }, "totalCount": 2 } }, "cursor": "Mw==" }, { "node": { "id": "/api/shop/attribute-options/5", "_id": 5, "adminName": "White", "sortOrder": 4, "swatchValue": "#ffffff", "swatchValueUrl": null, "translation": { "id": "/api/attribute_option_translations/5", "_id": 5, "attributeOptionId": "5", "locale": "en", "label": "White" }, "translations": { "edges": [ { "node": { "id": "/api/attribute_option_translations/88", "_id": 88, "attributeOptionId": "5", "locale": "ar", "label": "" } }, { "node": { "id": "/api/attribute_option_translations/5", "_id": 5, "attributeOptionId": "5", "locale": "en", "label": "White" } } ], "pageInfo": { "endCursor": "MQ==", "startCursor": "MA==", "hasNextPage": false, "hasPreviousPage": false }, "totalCount": 2 } }, "cursor": "NA==" }, { "node": { "id": "/api/shop/attribute-options/39", "_id": 39, "adminName": "Orange", "sortOrder": 5, "swatchValue": "#ff6600", "swatchValueUrl": null, "translation": { "id": "/api/attribute_option_translations/39", "_id": 39, "attributeOptionId": "39", "locale": "en", "label": "Orange" }, "translations": { "edges": [ { "node": { "id": "/api/attribute_option_translations/89", "_id": 89, "attributeOptionId": "39", "locale": "ar", "label": "" } }, { "node": { "id": "/api/attribute_option_translations/39", "_id": 39, "attributeOptionId": "39", "locale": "en", "label": "Orange" } } ], "pageInfo": { "endCursor": "MQ==", "startCursor": "MA==", "hasNextPage": false, "hasPreviousPage": false }, "totalCount": 2 } }, "cursor": "NQ==" }, { "node": { "id": "/api/shop/attribute-options/41", "_id": 41, "adminName": "Blue", "sortOrder": 6, "swatchValue": "#0000ff", "swatchValueUrl": null, "translation": { "id": "/api/attribute_option_translations/41", "_id": 41, "attributeOptionId": "41", "locale": "en", "label": "Blue" }, "translations": { "edges": [ { "node": { "id": "/api/attribute_option_translations/90", "_id": 90, "attributeOptionId": "41", "locale": "ar", "label": "" } }, { "node": { "id": "/api/attribute_option_translations/41", "_id": 41, "attributeOptionId": "41", "locale": "en", "label": "Blue" } } ], "pageInfo": { "endCursor": "MQ==", "startCursor": "MA==", "hasNextPage": false, "hasPreviousPage": false }, "totalCount": 2 } }, "cursor": "Ng==" }, { "node": { "id": "/api/shop/attribute-options/42", "_id": 42, "adminName": "Pink", "sortOrder": 7, "swatchValue": "#e33d94", "swatchValueUrl": null, "translation": { "id": "/api/attribute_option_translations/42", "_id": 42, "attributeOptionId": "42", "locale": "en", "label": "Pink" }, "translations": { "edges": [ { "node": { "id": "/api/attribute_option_translations/91", "_id": 91, "attributeOptionId": "42", "locale": "ar", "label": "" } }, { "node": { "id": "/api/attribute_option_translations/42", "_id": 42, "attributeOptionId": "42", "locale": "en", "label": "Pink" } } ], "pageInfo": { "endCursor": "MQ==", "startCursor": "MA==", "hasNextPage": false, "hasPreviousPage": false }, "totalCount": 2 } }, "cursor": "Nw==" }, { "node": { "id": "/api/shop/attribute-options/43", "_id": 43, "adminName": "Purple", "sortOrder": 8, "swatchValue": "#6611bb", "swatchValueUrl": null, "translation": { "id": "/api/attribute_option_translations/43", "_id": 43, "attributeOptionId": "43", "locale": "en", "label": "Purple" }, "translations": { "edges": [ { "node": { "id": "/api/attribute_option_translations/92", "_id": 92, "attributeOptionId": "43", "locale": "ar", "label": "" } }, { "node": { "id": "/api/attribute_option_translations/43", "_id": 43, "attributeOptionId": "43", "locale": "en", "label": "Purple" } } ], "pageInfo": { "endCursor": "MQ==", "startCursor": "MA==", "hasNextPage": false, "hasPreviousPage": false }, "totalCount": 2 } }, "cursor": "OA==" }, { "node": { "id": "/api/shop/attribute-options/46", "_id": 46, "adminName": "Grey", "sortOrder": 9, "swatchValue": "#949494", "swatchValueUrl": null, "translation": { "id": "/api/attribute_option_translations/46", "_id": 46, "attributeOptionId": "46", "locale": "en", "label": "Grey" }, "translations": { "edges": [ { "node": { "id": "/api/attribute_option_translations/93", "_id": 93, "attributeOptionId": "46", "locale": "ar", "label": "" } }, { "node": { "id": "/api/attribute_option_translations/46", "_id": 46, "attributeOptionId": "46", "locale": "en", "label": "Grey" } } ], "pageInfo": { "endCursor": "MQ==", "startCursor": "MA==", "hasNextPage": false, "hasPreviousPage": false }, "totalCount": 2 } }, "cursor": "OQ==" } ], "pageInfo": { "endCursor": "OQ==", "startCursor": "MA==", "hasNextPage": true, "hasPreviousPage": false }, "totalCount": 12 }, "translations": { "edges": [ { "node": { "id": "/api/attribute_translations/52", "_id": 52, "attributeId": "23", "locale": "ar", "name": "" }, "cursor": "MA==" }, { "node": { "id": "/api/attribute_translations/23", "_id": 23, "attributeId": "23", "locale": "en", "name": "Color" }, "cursor": "MQ==" } ], "pageInfo": { "endCursor": "MQ==", "startCursor": "MA==", "hasNextPage": false, "hasPreviousPage": false }, "totalCount": 2 } } } commonErrors: - error: INVALID_FIELD cause: Requested field does not exist solution: Check available attribute fields in schema --- # Get Attribute ## About The `getAttribute` query retrieves a single attribute by ID with support for nested options, translations, and detailed configuration metadata. This query is essential for: - Building product filter interfaces with attribute options - Displaying attribute details in admin/management interfaces - Creating product configuration forms - Fetching attribute properties and validation rules - Building faceted navigation systems with swatch support The query supports nested pagination for options and translations, making it flexible for various UI requirements. ## Arguments | Argument | Type | Required | Description | |----------|------|----------|-------------| | `id` | `String!` | ✅ Yes | Attribute ID in format `/api/shop/attributes/{id}` or numeric ID | **Supported ID Formats:** ```graphql # Format 1: Full URI query { attribute(id: "/api/shop/attributes/23") { id } } # Format 2: Numeric ID query { attribute(id: "23") { id } } ``` ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `id` | `String!` | Unique attribute identifier in IRI format `/api/shop/attributes/{id}` | | `code` | `String!` | Unique code identifier (e.g., "color", "size", "brand") | | `name` | `String!` | Display name of the attribute | | `type` | `String!` | Attribute type (text, select, date, checkbox, textarea, etc.) | | `sortOrder` | `Int!` | Display order for sorting | | `isFilterable` | `Boolean!` | Can be used for product filtering | | `isSearchable` | `Boolean!` | Can be used in product search | | `isConfigurable` | `Boolean!` | Can be configured for products | | `isVisibleOnFront` | `Boolean!` | Visible to frontend customers | | `isRequired` | `Boolean!` | Required for product assignment | | `defaultValue` | `String` | Default value (nullable) | | `createdAt` | `DateTime!` | Creation timestamp (ISO 8601) | | `updatedAt` | `DateTime!` | Last update timestamp (ISO 8601) | | `options` | `Connection` | Attribute options with pagination support | ## Attribute Fields | Field | Type | Description | |-------|------|-------------| | `id` | `String!` | Unique identifier in format `/api/shop/attributes/{id}` | | `code` | `String!` | Unique code identifier for attribute | | `name` | `String!` | Display name of the attribute | | `type` | `String!` | Attribute type (text, select, date, etc.) | | `sortOrder` | `Int!` | Display sort order | | `isFilterable` | `Boolean!` | Usable for product filtering | | `isSearchable` | `Boolean!` | Usable for search | | `isConfigurable` | `Boolean!` | Can be configured for products | | `isVisibleOnFront` | `Boolean!` | Visible on storefront | | `isRequired` | `Boolean!` | Required for products | | `defaultValue` | `String` | Default value if any | | `createdAt` | `String!` | Creation date | | `updatedAt` | `String!` | Last update date | | `options` | `Connection` | Nested attribute options with pagination | ## Common Use Cases ### Get Attribute for Filter UI ```graphql query GetAttributeFilter($id: String!) { attribute(id: $id) { id code name isFilterable options(first: 100) { edges { node { id adminName translation { label } } } } } } ``` ### Get Attribute with Option Swatches ```graphql query GetColorAttribute($id: String!) { attribute(id: $id) { id code name type options(first: 50) { edges { node { id adminName swatchValue translation { label } } } } } } ``` ### Build Product Configuration Form ```graphql query GetAttributeForForm($id: String!) { attribute(id: $id) { id code name type isRequired defaultValue options(first: 100) { edges { node { id adminName sortOrder translation { label } } } } } } ``` ### Get Multi-language Attribute ```graphql query GetMultiLanguageAttribute($id: String!) { attribute(id: $id) { id code name options(first: 20) { edges { node { id adminName translations(first: 10) { edges { node { locale label } } } } } } } } ``` ## Error Handling ### Missing ID Parameter ```json { "errors": [ { "message": "Field \"attribute\" argument \"id\" of type \"String!\" is required but not provided." } ] } ``` ### Attribute Not Found ```json { "data": { "attribute": null } } ``` ### Invalid ID Format ```json { "data": { "attribute": null } } ``` ## Best Practices 1. **Use Variables** - Always use GraphQL variables for dynamic IDs 2. **Request Specific Fields** - Only fetch fields your UI needs 3. **Handle Pagination** - Use `hasNextPage` and `endCursor` for nested options 4. **Cache Results** - Attributes rarely change, implement caching 5. **Limit Option Requests** - Start with reasonable limits (20-50) then load more on demand ## Related Resources - [Pagination Guide](/api/graphql-api/pagination) - Cursor pagination documentation - [Attribute Collection](/api/graphql-api/shop/queries/get-attributes) - Query multiple attributes - [Attribute Options API](/api/graphql-api/shop/queries/get-attribute-options) - Detailed option queries - [Attributes API](/api/graphql-api/shop/attribute-options) - Full attributes documentation - [Shop API Overview](/api/graphql-api/shop-api) - Overview of Shop API resources --- # Get Attribute Options URL: /api/graphql-api/shop/queries/get-attribute-options --- outline: false examples: - id: get-attribute-options-basic title: Get Attribute Options - Basic description: Retrieve basic attribute options with pagination. query: | query getAttributeOptions($first: Int) { attributeOptions(first: $first) { edges { node { id _id adminName sortOrder swatchValue } } pageInfo { hasNextPage endCursor } } } variables: | { "first": 10 } response: | { "data": { "attributeOptions": { "edges": [ { "node": { "id": "/api/admin/attribute_options/1", "_id": 1, "adminName": "Red", "sortOrder": 0, "swatchValue": "#e10e0e" } }, { "node": { "id": "/api/shop/attribute-options/2", "_id": 2, "adminName": "Green", "sortOrder": 1, "swatchValue": "#155616" } } ], "pageInfo": { "hasNextPage": false, "endCursor": "MQ==" } } } } - id: get-attribute-options-with-translations title: Get Attribute Options with Translations description: Retrieve attribute options with all available translations for multi-language support. query: | query getAttributeOptionsWithTranslations($first: Int) { attributeOptions(first: $first) { edges { node { id adminName sortOrder translations(first: 10) { edges { node { locale label } } } } } } } variables: | { "first": 5 } response: | { "data": { "attributeOptions": { "edges": [ { "node": { "id": "/api/admin/attribute_options/1", "adminName": "Red", "sortOrder": 0, "translations": { "edges": [ { "node": { "locale": "en", "label": "Red" } }, { "node": { "locale": "ar", "label": "أحمر" } }, { "node": { "locale": "fr", "label": "Rouge" } } ] } } } ] } } } commonErrors: - error: NO_TRANSLATIONS cause: Attribute option has no translations solution: Check if translations are configured for this option - id: get-attribute-options-with-swatches title: Get Attribute Options with Swatches description: Retrieve attribute options with color or image swatch information. query: | query getSwatchOptions($first: Int) { attributeOptions(first: $first) { edges { node { id adminName swatchValue swatchValueUrl translation { locale label } } } } } variables: | { "first": 20 } response: | { "data": { "attributeOptions": { "edges": [ { "node": { "id": "/api/admin/attribute_options/10", "adminName": "Pattern1", "swatchValue": null, "swatchValueUrl": "https://api-demo.bagisto.com/storage/swatches/pattern1.png", "translation": { "locale": "en", "label": "Pattern 1" } } }, { "node": { "id": "/api/admin/attribute_options/11", "adminName": "Pattern2", "swatchValue": null, "swatchValueUrl": "https://api-demo.bagisto.com/storage/swatches/pattern2.png", "translation": { "locale": "en", "label": "Pattern 2" } } } ] } } } commonErrors: - error: INVALID_SWATCH_URL cause: Swatch image URL is invalid solution: Verify the swatch image exists and URL is correct - id: get-attribute-option-by-id title: Get Single Attribute Option Detail by Option ID description: Retrieve complete details of a single attribute option including all translations and swatch information. query: | query getAttributeOptionByID ($id: ID!) { attributeOption (id: $id) { id _id adminName sortOrder swatchValue swatchValueUrl translation { id _id attributeOptionId locale label } translations { edges { node { id _id attributeOptionId locale label } } pageInfo { endCursor startCursor hasNextPage hasPreviousPage } totalCount } } } variables: | { "id": "/api/admin/attribute_options/1" } response: | { "data": { "attributeOption": { "id": "/api/admin/attribute_options/1", "_id": 1, "attributeId": 23, "adminName": "Red", "sortOrder": 0, "swatchValue": "#e10e0e", "swatchValueUrl": null, "translation": { "id": "/api/shop/attribute-option-translations/1", "_id": 1, "attributeOptionId": 1, "locale": "en", "label": "Red" }, "translations": { "edges": [ { "node": { "id": "/api/shop/attribute-option-translations/1", "_id": 1, "attributeOptionId": 1, "locale": "en", "label": "Red" } }, { "node": { "id": "/api/shop/attribute-option-translations/2", "_id": 2, "attributeOptionId": 1, "locale": "ar", "label": "أحمر" } }, { "node": { "id": "/api/shop/attribute-option-translations/3", "_id": 3, "attributeOptionId": 1, "locale": "fr", "label": "Rouge" } } ], "pageInfo": { "endCursor": "Mw==", "startCursor": "MA==", "hasNextPage": false, "hasPreviousPage": false }, "totalCount": 3 } } } } commonErrors: - error: OPTION_NOT_FOUND cause: Attribute option with given ID does not exist solution: Verify the option ID is correct - error: INVALID_OPTION_ID cause: Option ID format is invalid solution: Use a valid option ID from the system - id: get-attribute-options-pagination title: Get Attribute Options - Pagination description: Paginate through large sets of attribute options using cursors. query: | query getAttributeOptionsPaginated( $first: Int $after: String ) { attributeOptions( first: $first after: $after ) { edges { node { id adminName sortOrder } cursor } pageInfo { hasNextPage endCursor hasPreviousPage startCursor } } } variables: | { "first": 10, "after": null } response: | { "data": { "attributeOptions": { "edges": [ { "node": { "id": "/api/admin/attribute_options/1", "adminName": "Red", "sortOrder": 0 }, "cursor": "MA==" }, { "node": { "id": "/api/shop/attribute-options/2", "adminName": "Green", "sortOrder": 1 }, "cursor": "MQ==" } ], "pageInfo": { "hasNextPage": true, "endCursor": "MQ==", "hasPreviousPage": false, "startCursor": "MA==" } } } } commonErrors: - error: INVALID_CURSOR cause: Pagination cursor format is invalid solution: Use cursor values returned from previous requests - id: get-attribute-options-via-attribute title: Get Attribute Options via Attribute description: Retrieve attribute options as a nested resource within an attribute query. query: | query getAttribute($id: ID!, $first: Int) { attribute(id: $id) { id code adminName options(first: $first) { edges { node { id adminName sortOrder swatchValue translation { locale label } } cursor } pageInfo { hasNextPage endCursor } } } } variables: | { "id": "/api/shop/attributes/23", "first": 10 } response: | { "data": { "attribute": { "id": "/api/shop/attributes/23", "code": "color", "adminName": "Color", "options": { "edges": [ { "node": { "id": "/api/admin/attribute_options/1", "adminName": "Red", "sortOrder": 0, "swatchValue": "#e10e0e", "translation": { "locale": "en", "label": "Red" } }, "cursor": "MA==" }, { "node": { "id": "/api/shop/attribute-options/2", "adminName": "Green", "sortOrder": 1, "swatchValue": "#155616", "translation": { "locale": "en", "label": "Green" } }, "cursor": "MQ==" } ], "pageInfo": { "hasNextPage": false, "endCursor": "MQ==" } } } } } commonErrors: - error: ATTRIBUTE_NOT_FOUND cause: Attribute ID does not exist solution: Verify the attribute ID is correct - id: get-color-options-for-display title: Get Color Options for Display description: Get color attribute options optimized for product display with minimal fields. query: | query getColorOptions { attributeOptions(first: 50) { edges { node { adminName swatchValue translation { label } } } } } variables: | { } response: | { "data": { "attributeOptions": { "edges": [ { "node": { "adminName": "Red", "swatchValue": "#e10e0e", "translation": { "label": "Red" } } }, { "node": { "adminName": "Green", "swatchValue": "#155616", "translation": { "label": "Green" } } }, { "node": { "adminName": "Blue", "swatchValue": "#0000ff", "translation": { "label": "Blue" } } } ] } } } commonErrors: - error: ATTRIBUTE_NOT_FOUND cause: Color attribute does not exist solution: Ensure color attribute ID is configured correctly --- # Get Attribute Options ## About The `getAttributeOptions` query retrieves attribute options (values) for a specific attribute. This query is essential for: - Building product filter and search interfaces - Displaying color swatches and size options - Creating configurable product selectors - Building faceted navigation systems - Multi-language product attribute support The query supports cursor-based pagination and optional translation fetching, making it ideal for displaying product attribute values in various UI contexts. ## Arguments | Argument | Type | Required | Description | |----------|------|----------|-------------| | `first` | `Int` | ❌ No | Number of options to retrieve from the start (forward pagination). Max: 100. | | `after` | `String` | ❌ No | Cursor to start after for forward pagination. | | `last` | `Int` | ❌ No | Number of options to retrieve from the end (backward pagination). Max: 100. | | `before` | `String` | ❌ No | Cursor to start before for backward pagination. | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `edges` | `[AttributeOptionEdge!]!` | Array of attribute option edges containing options and cursors. | | `edges.node` | `AttributeOption!` | The actual attribute option object with id, name, swatch, and translations. | | `edges.cursor` | `String!` | Pagination cursor for this option. Use with `after` or `before` arguments. | | `pageInfo` | `PageInfo!` | Pagination metadata object. | | `pageInfo.hasNextPage` | `Boolean!` | Whether more options exist after the current page. | | `pageInfo.hasPreviousPage` | `Boolean!` | Whether options exist before the current page. | | `pageInfo.startCursor` | `String` | Cursor of the first option on the current page. | | `pageInfo.endCursor` | `String` | Cursor of the last option on the current page. | ## AttributeOption Fields | Field | Type | Description | |-------|------|-------------| | `id` | `String!` | Unique option identifier in format `/api/shop/attribute-options/{id}`. | | `_id` | `Int!` | Numeric ID of the option. | | `adminName` | `String!` | Admin-facing name (e.g., "Red", "Large", "Cotton"). | | `sortOrder` | `Int!` | Display order of the option (0, 1, 2, ...). | | `swatchValue` | `String` | Swatch value - hex color code for color attributes or text representation. | | `swatchValueUrl` | `String` | URL to swatch image file for image-based swatches. | | `translation` | `OptionTranslation` | Single translation for the default/current locale. | | `translations` | `[OptionTranslation!]` | Collection of all translations for multi-language support. | ## Translation Fields | Field | Type | Description | |-------|------|-------------| | `id` | `String!` | Translation ID in format `/api/attribute_option_translations/{id}`. | | `_id` | `Int!` | Numeric translation ID. | | `locale` | `String!` | Language locale code (e.g., "en", "ar", "fr", "de"). | | `label` | `String!` | Translated label for the option in the specified locale. | ## Common Use Cases ### Display Color Picker in Product Page ```graphql query ColorPicker { attributeOptions(first: 50) { edges { node { adminName swatchValue translation { label } } } } } ``` ### Build Size Selector with Sorting ```graphql query SizeSelector { attributeOptions(first: 100) { edges { node { adminName sortOrder translation { label } } } } } ``` ### Multi-language Attribute Support ```graphql query MultiLanguageOptions { attributeOptions(first: 20) { edges { node { adminName translations(first: 10) { edges { node { locale label } } } } } } } ``` ## Error Handling ### Missing Attribute ID ```json { "errors": [ { "message": "Field \"attributeOptions\" argument \"attributeId\" of type \"Int!\" is required but not provided." } ] } ``` ### Non-existent Attribute ```json { "data": { "attributeOptions": { "edges": [], "pageInfo": { "hasNextPage": false, "endCursor": null } } } } ``` ### Invalid Pagination Cursor ```json { "errors": [ { "message": "Invalid cursor provided" } ] } ``` ## Best Practices 1. **Use Appropriate Pagination Size** - Request 10-50 options per page 2. **Cache Results** - Attribute options change infrequently, cache them 3. **Request Translations When Needed** - Only fetch translations if supporting multiple languages 4. **Optimize Field Selection** - Request only fields your UI actually needs ## Related Resources - [Pagination Guide](/api/graphql-api/pagination) - Cursor pagination documentation - [Attribute Options API](/api/graphql-api/shop/attribute-options) - Detailed API documentation - [Products API](/api/graphql-api/shop/queries/get-products) - Related product queries - [Shop API Overview](/api/graphql-api/shop-api) - Overview of Shop API resources --- # Get Attributes URL: /api/graphql-api/shop/queries/get-attributes --- outline: false examples: - id: get-attributes-basic title: Get Attributes - Basic description: Retrieve a paginated list of all product attributes with basic information. query: | query getAllAttributes($first: Int, $after: String) { attributes(first: $first, after: $after) { edges { node { id _id code adminName type swatchType position isRequired isConfigurable options { edges { node { id adminName swatchValue } } totalCount } } cursor } pageInfo { endCursor hasNextPage } totalCount } } variables: | { "first": 10 } response: | { "data": { "attributes": { "edges": [ { "node": { "id": "/api/shop/attributes/1", "_id": 1, "code": "sku", "adminName": "SKU", "type": "text", "swatchType": null, "position": 1, "isRequired": "1", "isConfigurable": "0", "options": { "edges": [], "totalCount": 0 } }, "cursor": "MA==" }, { "node": { "id": "/api/shop/attributes/23", "_id": 23, "code": "color", "adminName": "Color", "type": "select", "swatchType": "color", "position": 26, "isRequired": "0", "isConfigurable": "1", "options": { "edges": [ { "node": { "id": "/api/shop/attribute-options/1", "adminName": "Red", "swatchValue": "#e10e0e" } }, { "node": { "id": "/api/shop/attribute-options/2", "adminName": "Green", "swatchValue": "#155616" } } ], "totalCount": 12 } }, "cursor": "MjI=" } ], "pageInfo": { "endCursor": "Mjk=", "hasNextPage": true }, "totalCount": 38 } } } commonErrors: - error: Argument \"first\" must be between 1 and 100 cause: Pagination limit exceeds maximum allowed solution: Use a value between 1 and 100 for first parameter - error: Invalid cursor provided cause: Provided cursor is invalid or expired solution: Use cursors from the pageInfo section of previous responses - id: get-attributes-with-options title: Get Attributes with Full Options and Translations description: Retrieve attributes with complete option details and multi-locale translations. query: | query getAllAttributes($first: Int) { attributes(first: $first) { edges { node { id _id code adminName type swatchType validation regex position isRequired isUnique isFilterable isComparable isConfigurable isUserDefined isVisibleOnFront valuePerLocale valuePerChannel defaultValue enableWysiwyg createdAt updatedAt columnName validations options { edges { node { id _id adminName sortOrder swatchValue swatchValueUrl translation { id _id attributeOptionId locale label } translations { edges { node { id _id attributeOptionId locale label } } pageInfo { endCursor hasNextPage } totalCount } } cursor } pageInfo { endCursor hasNextPage } totalCount } translations { edges { node { id _id attributeId locale name } } pageInfo { endCursor hasNextPage } totalCount } } cursor } pageInfo { endCursor startCursor hasNextPage hasPreviousPage } totalCount } } variables: | { "first": 5 } response: | { "data": { "attributes": { "edges": [ { "node": { "id": "/api/shop/attributes/1", "_id": 1, "code": "sku", "adminName": "SKU", "type": "text", "swatchType": null, "validation": null, "regex": null, "position": 1, "isRequired": "1", "isUnique": "1", "isFilterable": "0", "isComparable": "0", "isConfigurable": "0", "isUserDefined": "0", "isVisibleOnFront": "0", "valuePerLocale": "0", "valuePerChannel": "0", "defaultValue": null, "enableWysiwyg": "0", "createdAt": "2023-11-02T10:30:00+05:30", "updatedAt": "2023-12-06T12:00:00+05:30", "columnName": "text_value", "validations": "{ }", "options": { "edges": [], "pageInfo": { "endCursor": null, "hasNextPage": false }, "totalCount": 0 }, "translations": { "edges": [ { "node": { "id": "/api/attribute_translations/1", "_id": 1, "attributeId": "1", "locale": "en", "name": "SKU" } } ], "pageInfo": { "endCursor": "MA==", "hasNextPage": false }, "totalCount": 1 } }, "cursor": "MA==" }, { "node": { "id": "/api/shop/attributes/23", "_id": 23, "code": "color", "adminName": "Color", "type": "select", "swatchType": "color", "validation": null, "regex": null, "position": 26, "isRequired": "0", "isUnique": "0", "isFilterable": "1", "isComparable": "0", "isConfigurable": "1", "isUserDefined": "1", "isVisibleOnFront": "0", "valuePerLocale": "0", "valuePerChannel": "0", "defaultValue": null, "enableWysiwyg": "0", "createdAt": "2023-11-02T16:40:10+05:30", "updatedAt": "2023-12-06T12:52:51+05:30", "columnName": "integer_value", "validations": "{ }", "options": { "edges": [ { "node": { "id": "/api/shop/attribute-options/1", "_id": 1, "adminName": "Red", "sortOrder": 0, "swatchValue": "#e10e0e", "swatchValueUrl": null, "translation": { "id": "/api/attribute_option_translations/1", "_id": 1, "attributeOptionId": "1", "locale": "en", "label": "Red" }, "translations": { "edges": [ { "node": { "id": "/api/attribute_option_translations/84", "_id": 84, "attributeOptionId": "1", "locale": "ar", "label": "" } }, { "node": { "id": "/api/attribute_option_translations/1", "_id": 1, "attributeOptionId": "1", "locale": "en", "label": "Red" } } ], "pageInfo": { "endCursor": "MQ==", "hasNextPage": false }, "totalCount": 2 } }, "cursor": "MA==" }, { "node": { "id": "/api/shop/attribute-options/2", "_id": 2, "adminName": "Green", "sortOrder": 1, "swatchValue": "#155616", "swatchValueUrl": null, "translation": { "id": "/api/attribute_option_translations/2", "_id": 2, "attributeOptionId": "2", "locale": "en", "label": "Green" }, "translations": { "edges": [ { "node": { "id": "/api/attribute_option_translations/85", "_id": 85, "attributeOptionId": "2", "locale": "ar", "label": "" } }, { "node": { "id": "/api/attribute_option_translations/2", "_id": 2, "attributeOptionId": "2", "locale": "en", "label": "Green" } } ], "pageInfo": { "endCursor": "MQ==", "hasNextPage": false }, "totalCount": 2 } }, "cursor": "MQ==" } ], "pageInfo": { "endCursor": "MjE=", "hasNextPage": true }, "totalCount": 12 }, "translations": { "edges": [ { "node": { "id": "/api/attribute_translations/23", "_id": 23, "attributeId": "23", "locale": "en", "name": "Color" } } ], "pageInfo": { "endCursor": "MA==", "hasNextPage": false }, "totalCount": 1 } }, "cursor": "MjI=" } ], "pageInfo": { "endCursor": "Mjk=", "startCursor": "MA==", "hasNextPage": true, "hasPreviousPage": false }, "totalCount": 38 } } } commonErrors: - error: Argument \"first\" must be between 1 and 100 cause: Pagination limit exceeds maximum allowed solution: Use a value between 1 and 100 for first parameter - error: Invalid cursor provided cause: Provided cursor is invalid or expired solution: Use cursors from the pageInfo section of previous responses --- # Get Attributes Retrieve a paginated collection of all product attributes available in the system. This query includes attribute configurations, validation rules, option values with swatches, and translations for multiple locales. ## Query Structure ```graphql query getAllAttributes { attributes { edges { node { id _id code adminName type swatchType validation regex position isRequired isUnique isFilterable isComparable isConfigurable isUserDefined isVisibleOnFront valuePerLocale valuePerChannel defaultValue enableWysiwyg createdAt updatedAt columnName validations options { edges { node { id _id adminName sortOrder swatchValue swatchValueUrl translation { id _id attributeOptionId locale label } translations { edges { node { id _id attributeOptionId locale label } } pageInfo { endCursor startCursor hasNextPage hasPreviousPage } totalCount } } cursor } pageInfo { endCursor startCursor hasNextPage hasPreviousPage } totalCount } translations { edges { node { id _id attributeId locale name } cursor } pageInfo { endCursor startCursor hasNextPage hasPreviousPage } totalCount } } cursor } pageInfo { endCursor startCursor hasNextPage hasPreviousPage } totalCount } } ``` ## Arguments | Name | Type | Required | Description | |------|------|----------|-------------| | `first` | Int | No | Number of attributes to fetch (default: 10, max: 100) | | `after` | String | No | Cursor for forward pagination | | `last` | Int | No | Number of attributes to fetch backward | | `before` | String | No | Cursor for backward pagination | ## Field Descriptions ### Attribute Fields | Field | Type | Description | |-------|------|-------------| | `id` | String | IRI format ID (`/api/shop/attributes/{id}`) | | `_id` | Int | Numeric attribute ID | | `code` | String | Machine-readable attribute code (e.g., "sku", "color", "size") | | `adminName` | String | Human-readable name shown in admin panel | | `type` | String | Attribute input type: `text`, `textarea`, `select`, `boolean`, `date`, `price` | | `swatchType` | String | Visual representation type: `color`, `text`, or null | | `validation` | String | Validation rule type (e.g., "decimal", "integer") | | `regex` | String | Regular expression pattern for custom validation | | `position` | Int | Display position in forms | | `isRequired` | String | Whether attribute is required ("0" or "1") | | `isUnique` | String | Whether values must be unique ("0" or "1") | | `isFilterable` | String | Can be used in layered navigation ("0" or "1") | | `isComparable` | String | Can be used in product comparison ("0" or "1") | | `isConfigurable` | String | Can configure variations ("0" or "1") | | `isUserDefined` | String | Custom attribute vs system ("0" or "1") | | `isVisibleOnFront` | String | Visible to customers on storefront ("0" or "1") | | `valuePerLocale` | String | Has locale-specific values ("0" or "1") | | `valuePerChannel` | String | Has channel-specific values ("0" or "1") | | `defaultValue` | String | Default value if not specified | | `enableWysiwyg` | String | Enable WYSIWYG editor for textarea ("0" or "1") | | `createdAt` | DateTime | ISO 8601 timestamp when attribute was created | | `updatedAt` | DateTime | ISO 8601 timestamp of last modification | | `columnName` | String | Database column name (e.g., "text_value", "integer_value") | | `validations` | String | JSON string with validation rules | | `options` | Connection | Paginated attribute option values | | `translations` | Connection | Multi-language names for the attribute | ### Attribute Option Fields | Field | Type | Description | |-------|------|-------------| | `id` | String | IRI format option ID | | `_id` | Int | Numeric option ID | | `adminName` | String | Option label in admin | | `sortOrder` | Int | Display order among options | | `swatchValue` | String | Color code (for color swatches) or text value | | `swatchValueUrl` | String | Image URL (for image swatches) | | `translation` | Object | Default locale translation | | `translations` | Connection | All locale translations for this option | ### Translation Fields | Field | Type | Description | |-------|------|-------------| | `id` | String | IRI format translation ID | | `_id` | Int | Numeric translation ID | | `attributeId` | String | Parent attribute ID | | `attributeOptionId` | String | Parent option ID (for options) | | `locale` | String | Locale code (e.g., "en", "ar") | | `name` | String | Attribute name in locale | | `label` | String | Option label in locale | ## Response Format The response follows cursor-based pagination with the following structure: ```json { "data": { "attributes": { "edges": [ { "node": { "id": "/api/shop/attributes/1", "_id": 1, "code": "sku", "adminName": "SKU", // ... attribute fields "options": { "edges": [], "pageInfo": { /* pagination */ }, "totalCount": 0 }, "translations": { "edges": [ /* translations */ ], "pageInfo": { /* pagination */ }, "totalCount": 1 } }, "cursor": "MA==" } ], "pageInfo": { "endCursor": "Mjk=", "startCursor": "MA==", "hasNextPage": true, "hasPreviousPage": false }, "totalCount": 38 } } } ``` ## Examples ### Example 1: Fetch First 10 Attributes Retrieve the first 10 attributes with their options and translations: :::: code-group ::: code-group-item Query ```graphql query { attributes(first: 10) { edges { node { id _id code adminName type position isRequired isConfigurable options { edges { node { adminName swatchValue } } totalCount } translations { edges { node { locale name } } totalCount } } cursor } pageInfo { hasNextPage endCursor } totalCount } } ``` ::: ::: code-group-item Response ```json { "data": { "attributes": { "edges": [ { "node": { "id": "/api/shop/attributes/1", "_id": 1, "code": "sku", "adminName": "SKU", "type": "text", "position": 1, "isRequired": "1", "isConfigurable": "0", "options": { "edges": [], "totalCount": 0 }, "translations": { "edges": [ { "node": { "locale": "en", "name": "SKU" } } ], "totalCount": 1 } }, "cursor": "MA==" }, { "node": { "id": "/api/shop/attributes/2", "_id": 2, "code": "name", "adminName": "Name", "type": "text", "position": 3, "isRequired": "1", "isConfigurable": "0", "options": { "edges": [], "totalCount": 0 }, "translations": { "edges": [ { "node": { "locale": "en", "name": "Name" } } ], "totalCount": 1 } }, "cursor": "MQ==" }, { "node": { "id": "/api/shop/attributes/23", "_id": 23, "code": "color", "adminName": "Color", "type": "select", "position": 26, "isRequired": "0", "isConfigurable": "1", "options": { "edges": [ { "node": { "adminName": "Red", "swatchValue": "#e10e0e" } }, { "node": { "adminName": "Green", "swatchValue": "#155616" } }, { "node": { "adminName": "Blue", "swatchValue": "#0000ff" } } ], "totalCount": 12 }, "translations": { "edges": [ { "node": { "locale": "en", "name": "Color" } } ], "totalCount": 2 } }, "cursor": "MjI=" } ], "pageInfo": { "hasNextPage": true, "endCursor": "MTM=" }, "totalCount": 38 } } } ``` ::: :::: ### Example 2: Fetch Configurable Attributes with Options Retrieve configurable attributes (used for product variations) with complete option details: :::: code-group ::: code-group-item Query ```graphql query { attributes(first: 20) { edges { node { id _id code adminName type swatchType isConfigurable options { edges { node { id _id adminName sortOrder swatchValue swatchValueUrl translation { locale label } } } totalCount } } cursor } pageInfo { hasNextPage endCursor } totalCount } } ``` ::: ::: code-group-item Response ```json { "data": { "attributes": { "edges": [ { "node": { "id": "/api/shop/attributes/23", "_id": 23, "code": "color", "adminName": "Color", "type": "select", "swatchType": "color", "isConfigurable": "1", "options": { "edges": [ { "node": { "id": "/api/shop/attribute-options/1", "_id": 1, "adminName": "Red", "sortOrder": 0, "swatchValue": "#e10e0e", "swatchValueUrl": null, "translation": { "locale": "en", "label": "Red" } } }, { "node": { "id": "/api/shop/attribute-options/2", "_id": 2, "adminName": "Green", "sortOrder": 1, "swatchValue": "#155616", "swatchValueUrl": null, "translation": { "locale": "en", "label": "Green" } } }, { "node": { "id": "/api/shop/attribute-options/3", "_id": 3, "adminName": "Yellow", "sortOrder": 2, "swatchValue": "#f6fa00", "swatchValueUrl": null, "translation": { "locale": "en", "label": "Yellow" } } } ], "totalCount": 12 } }, "cursor": "MjI=" }, { "node": { "id": "/api/shop/attributes/24", "_id": 24, "code": "size", "adminName": "Size", "type": "select", "swatchType": "text", "isConfigurable": "1", "options": { "edges": [ { "node": { "id": "/api/shop/attribute-options/6", "_id": 6, "adminName": "S", "sortOrder": 0, "swatchValue": "S", "swatchValueUrl": null, "translation": { "locale": "en", "label": "S" } } }, { "node": { "id": "/api/shop/attribute-options/7", "_id": 7, "adminName": "M", "sortOrder": 1, "swatchValue": "M", "swatchValueUrl": null, "translation": { "locale": "en", "label": "M" } } }, { "node": { "id": "/api/shop/attribute-options/8", "_id": 8, "adminName": "L", "sortOrder": 2, "swatchValue": "L", "swatchValueUrl": null, "translation": { "locale": "en", "label": "L" } } } ], "totalCount": 6 } }, "cursor": "MjM=" } ], "pageInfo": { "hasNextPage": true, "endCursor": "MjM=" }, "totalCount": 38 } } } ``` ::: :::: ### Example 3: Fetch Attributes with Multi-locale Translations Retrieve attributes with complete translation information across all locales: :::: code-group ::: code-group-item Query ```graphql query { attributes(first: 5) { edges { node { id code adminName type validations translations { edges { node { locale name } } totalCount } options { edges { node { adminName translations { edges { node { locale label } } totalCount } } } totalCount } } } pageInfo { totalCount hasNextPage } } } ``` ::: ::: code-group-item Response ```json { "data": { "attributes": { "edges": [ { "node": { "id": "/api/shop/attributes/9", "code": "short_description", "adminName": "Short Description", "type": "textarea", "validations": "{ required: true }", "translations": { "edges": [ { "node": { "locale": "ar", "name": "" } }, { "node": { "locale": "en", "name": "Short Description" } } ], "totalCount": 2 }, "options": { "edges": [], "totalCount": 0 } } }, { "node": { "id": "/api/shop/attributes/23", "code": "color", "adminName": "Color", "type": "select", "validations": "{ }", "translations": { "edges": [ { "node": { "locale": "ar", "name": "" } }, { "node": { "locale": "en", "name": "Color" } } ], "totalCount": 2 }, "options": { "edges": [ { "node": { "adminName": "Red", "translations": { "edges": [ { "node": { "locale": "ar", "label": "" } }, { "node": { "locale": "en", "label": "Red" } } ], "totalCount": 2 } } } ], "totalCount": 12 } } } ], "pageInfo": { "totalCount": 38, "hasNextPage": true } } } } ``` ::: :::: ## Pagination Details Attributes use cursor-based pagination for efficient data retrieval: ### Pagination Arguments - **`first`**: Number of records to fetch forward (max: 100) - **`after`**: Cursor position to start from (from `endCursor` of previous request) - **`last`**: Number of records to fetch backward (max: 100) - **`before`**: Cursor position to end at (from `startCursor` of previous request) ### Pagination Example ```graphql # Get next page of attributes query { attributes(first: 10, after: "Mjk=") { edges { node { id code adminName } } pageInfo { hasNextPage endCursor } } } ``` ## Attribute Types Reference | Type | Behavior | Examples | |------|----------|----------| | `text` | Single-line text input | SKU, Product Number, Name | | `textarea` | Multi-line text editor | Description, Short Description | | `select` | Dropdown list with predefined options | Color, Size, Brand | | `boolean` | Yes/No toggle | Status, Visible Individually, New | | `date` | Date picker | Special Price From/To | | `price` | Decimal number for pricing | Price, Cost, Special Price | ## Swatch Types Reference | Type | Display | Use Cases | |------|---------|-----------| | `color` | Color square with hex code | Color attribute visualization | | `text` | Text label | Size attribute swatches | | `null` | No visual representation | Regular dropdown options | ## Validation Rules Boolean fields return string values ("0" or "1"): | Field | Meaning | |-------|---------| | `"0"` | Feature disabled/false | | `"1"` | Feature enabled/true | The `validations` field contains JSON with rule definitions, for example: - `{ required: true }` - Field is mandatory - `{ decimal: true }` - Must be decimal number - `{ decimal: true, decimal: true }` - Multiple validation rules ## Common Use Cases ### 1. **Product Creation Form** Fetch all required attributes with their validation rules to build dynamic product creation forms. ```graphql query { attributes(first: 100) { edges { node { code adminName type isRequired position validations } } } } ``` ### 2. **Variation Selection** Get configurable attributes (for product variants) with their options for customer selection. ```graphql query { attributes(first: 100) { edges { node { code isConfigurable options { edges { node { adminName swatchValue swatchValueUrl } } } } } } } ``` ### 3. **Layered Navigation** Retrieve filterable attributes with options to build shopping filters. ```graphql query { attributes(first: 100) { edges { node { code isFilterable options { edges { node { translation { label } } } totalCount } } } } } ``` ### 4. **Multi-language Store** Load attributes with complete translations for all supported locales. ```graphql query { attributes(first: 100) { edges { node { code translations { edges { node { locale name } } } } } } } ``` ## Error Scenarios ### Invalid Pagination Arguments ```json { "errors": [ { "message": "Argument \"first\" on field \"attributes\" must be between 1 and 100", "extensions": { "code": "GRAPHQL_VALIDATION_FAILED" } } ] } ``` **Solution**: Ensure `first` and `last` values don't exceed 100. ### Invalid Cursor ```json { "errors": [ { "message": "Invalid cursor provided", "extensions": { "code": "CURSOR_INVALID" } } ] } ``` **Solution**: Use cursors obtained from previous pagination responses only. ## Best Practices 1. **Pagination Limit**: Use `first: 50` for balanced performance and response size. 2. **Nested Pagination**: Always paginate nested collections (`options`, `translations`) for large datasets to avoid overwhelming responses. 3. **Swatch Handling**: Check `swatchType` before displaying swatches - use `swatchValue` for colors/text, `swatchValueUrl` for images. 4. **Locale-specific Fields**: For multi-language stores, filter translations by locale code to load only required languages. 5. **Caching**: Cache attribute data with a reasonable TTL (15-60 minutes) as attributes rarely change frequently. 6. **Boolean Field Handling**: Always compare boolean fields as strings ("0" vs "1") rather than truthy/falsy values. 7. **Validation Rules**: Parse `validations` JSON field carefully for form validation on the frontend. 8. **Channel/Locale Values**: When `valuePerChannel` or `valuePerLocale` is "1", expect different values across channels/locales in product data. --- # Get Booking Slots URL: /api/graphql-api/shop/queries/get-booking-slots --- outline: false examples: - id: get-booking-slots-non-rental title: Get Booking Slots (Default / Appointment / Table / Event) description: | Retrieve available time slots for non-rental booking products. For default, appointment, table, and event booking types, slots are returned as a flat list with `from`, `to`, `timestamp`, and `qty` fields. The `id` parameter is the `bookingProductId` obtained from the product query's `bookingProducts` relationship. query: | query { bookingSlots(id: 1, date: "2026-03-26") { slotId from to timestamp qty } } variables: | {} response: | { "data": { "bookingSlots": [ { "slotId": null, "from": "10:00 AM", "to": "11:00 AM", "timestamp": "1774413000-1774416600", "qty": "1" }, { "slotId": null, "from": "11:00 AM", "to": "12:00 PM", "timestamp": "1774416600-1774420200", "qty": "1" }, { "slotId": null, "from": "12:00 PM", "to": "01:00 PM", "timestamp": "1774420200-1774423800", "qty": "1" }, { "slotId": null, "from": "01:00 PM", "to": "02:00 PM", "timestamp": "1774423800-1774427400", "qty": "1" }, { "slotId": null, "from": "02:00 PM", "to": "03:00 PM", "timestamp": "1774427400-1774431000", "qty": "1" } ] } } commonErrors: - error: PRODUCT_NOT_FOUND cause: The bookingProductId does not exist solution: Use the bookingProductId from the product query's bookingProducts relationship - error: NO_SLOTS_AVAILABLE cause: No slots are configured or available for the selected date solution: Try a different date — the product may not have slots on this day of the week - id: get-booking-slots-rental-hourly title: Get Booking Slots (Rental - Hourly) description: | Retrieve available time slots for a rental booking product with hourly renting type. Unlike other booking types, rental slots are returned in a **grouped structure** — slots are nested inside time range groups using the `time` and `slots` fields. Each group has a `time` label (e.g., "10:00 AM - 12:00 PM") containing individual hourly slots within that range. query: | query { bookingSlots(id: 5, date: "2026-03-26") { slotId time slots } } variables: | {} response: | { "data": { "bookingSlots": [ { "slotId": null, "time": "10:00 AM - 12:00 PM", "slots": [ { "from": "10:00 AM", "to": "11:00 AM", "timestamp": "1774499400-1774503000", "qty": "1" }, { "from": "11:00 AM", "to": "12:00 PM", "timestamp": "1774503000-1774506600", "qty": "1" } ] }, { "slotId": null, "time": "12:00 PM - 09:00 PM", "slots": [ { "from": "12:00 PM", "to": "01:00 PM", "timestamp": "1774506600-1774510200", "qty": "1" }, { "from": "01:00 PM", "to": "02:00 PM", "timestamp": "1774510200-1774513800", "qty": "1" }, { "from": "02:00 PM", "to": "03:00 PM", "timestamp": "1774513800-1774517400", "qty": "1" }, { "from": "03:00 PM", "to": "04:00 PM", "timestamp": "1774517400-1774521000", "qty": "1" }, { "from": "04:00 PM", "to": "05:00 PM", "timestamp": "1774521000-1774524600", "qty": "1" }, { "from": "05:00 PM", "to": "06:00 PM", "timestamp": "1774524600-1774528200", "qty": "1" }, { "from": "06:00 PM", "to": "07:00 PM", "timestamp": "1774528200-1774531800", "qty": "1" }, { "from": "07:00 PM", "to": "08:00 PM", "timestamp": "1774531800-1774535400", "qty": "1" }, { "from": "08:00 PM", "to": "09:00 PM", "timestamp": "1774535400-1774539000", "qty": "1" } ] } ] } } commonErrors: - error: PRODUCT_NOT_FOUND cause: The bookingProductId does not exist solution: Use the bookingProductId from the product query's bookingProducts relationship - error: NO_SLOTS_AVAILABLE cause: No rental slots configured for the selected date solution: Try a different date — check the product's configured slot days --- # Get Booking Slots ## About The `bookingSlots` query retrieves available time slots for a booking product on a specific date. This query is essential for building the booking UI — when a customer selects a date, you use this query to fetch and display the available slots they can choose from before adding the product to cart. ### Why This Query Is Needed When adding a booking product to the cart, the `booking` input requires a specific time slot (e.g., `"slot": "12:00 PM - 01:00 PM"`). But the available slots depend on the product's configuration (duration, break time, operating hours) and the selected date (day-of-week availability, existing bookings). This query resolves all of that and returns only the slots that are actually available for selection. The typical flow is: 1. **Query the product** to get `bookingProductId` from the `bookingProducts` relationship 2. **Customer selects a date** on the frontend 3. **Query `bookingSlots`** with the `bookingProductId` and selected date to get available slots 4. **Customer picks a slot** from the results 5. **Add to cart** using the selected slot value in the `booking` JSON ::: info Different Response Structures The response structure differs based on the booking type: - **Default, Appointment, Table, Event** — Slots are returned as a **flat list**. Each item has `from`, `to`, `timestamp`, and `qty` fields directly on it. - **Rental (Hourly)** — Slots are returned in a **grouped structure**. Each item has a `time` field (the time range group label) and a `slots` array containing the individual hourly slots within that group. - **Rental (Daily)** and **Event** — These types typically don't use time slots (daily rentals use date ranges, events use ticket quantities), so this query may return an empty array for them. ::: ## Arguments | Argument | Type | Required | Description | |----------|------|----------|-------------| | `id` | `Int!` | Yes | The `bookingProductId` — obtained from the product query's `bookingProducts` relationship (not the product ID). | | `date` | `String!` | Yes | The date to check for available slots in `YYYY-MM-DD` format. | ## Response Fields ### Non-Rental Booking Types (Flat Structure) | Field | Type | Description | |-------|------|-------------| | `slotId` | `Int` | Slot identifier (may be null). | | `from` | `String` | Slot start time (e.g., "10:00 AM"). | | `to` | `String` | Slot end time (e.g., "11:00 AM"). | | `timestamp` | `String` | Unix timestamp range in format `"from-to"` (e.g., "1774413000-1774416600"). | | `qty` | `String` | Number of available bookings for this slot. | ### Rental Hourly Booking Type (Grouped Structure) | Field | Type | Description | |-------|------|-------------| | `slotId` | `Int` | Slot identifier (may be null). | | `time` | `String` | Time range group label (e.g., "10:00 AM - 12:00 PM"). | | `slots` | `[Slot]` | Array of individual hourly slots within the time range group. | | `slots[].from` | `String` | Individual slot start time (e.g., "10:00 AM"). | | `slots[].to` | `String` | Individual slot end time (e.g., "11:00 AM"). | | `slots[].timestamp` | `String` | Unix timestamp range for the individual slot. | | `slots[].qty` | `String` | Number of available bookings for this individual slot. | ## Response by Booking Type | Booking Type | Response Structure | Fields to Query | |---|---|---| | **Default** | Flat list | `slotId`, `from`, `to`, `timestamp`, `qty` | | **Appointment** | Flat list | `slotId`, `from`, `to`, `timestamp`, `qty` | | **Table** | Flat list | `slotId`, `from`, `to`, `timestamp`, `qty` | | **Event** | Empty array (events use ticket quantities, not time slots) | — | | **Rental (Hourly)** | Grouped by time ranges | `slotId`, `time`, `slots` | | **Rental (Daily)** | Empty array (daily rentals use date ranges, not time slots) | — | ## How to Get the `bookingProductId` The `id` parameter for this query is **not** the product ID — it is the `bookingProductId` from the product's `bookingProducts` relationship. Query it like this: ```graphql query getProduct($id: ID!) { product(id: $id) { id name bookingProducts { edges { node { _id # This is the bookingProductId to use type # default, appointment, rental, table, event } } } } } ``` Use the `_id` value from `bookingProducts.edges.node` as the `id` argument in the `bookingSlots` query. ## Common Use Cases ### Build a Date + Slot Picker (Non-Rental) ```graphql # Step 1: User selects a date, fetch available slots query { bookingSlots(id: 1, date: "2026-03-26") { from to timestamp qty } } ``` Then use the `from` and `to` values to construct the `slot` field for the add-to-cart mutation: ```json { "booking": "{\"type\":\"appointment\",\"date\":\"2026-03-26\",\"slot\":\"10:00 AM - 11:00 AM\"}" } ``` ### Build an Hourly Rental Slot Picker ```graphql # Fetch grouped rental slots query { bookingSlots(id: 5, date: "2026-03-26") { time slots } } ``` Display the `time` groups as headers and individual `slots` as selectable options. Then use the selected slot for add-to-cart: ```json { "booking": "{\"type\":\"rental\",\"renting_type\":\"hourly\",\"date\":\"2026-03-26\",\"slot\":\"10:00 AM - 11:00 AM\"}" } ``` ## Best Practices 1. **Use `bookingProductId`, Not Product ID** — The `id` argument must be the `_id` from the `bookingProducts` relationship, not the main product ID 2. **Check for Empty Results** — An empty array means no slots are available for that date; prompt the user to select a different date 3. **Check `qty`** — Only display slots where `qty` is greater than 0 4. **Handle Both Structures** — Use the product's booking `type` to determine whether to expect flat slots or grouped rental slots 5. **Refresh on Date Change** — Re-query whenever the user changes the selected date ## Related Resources - [Single Product](/api/graphql-api/shop/queries/get-product) - Get product details including `bookingProducts` relationship - [Add to Cart](/api/graphql-api/shop/mutations/add-to-cart) - Add booking product to cart with selected slot --- # Get Cart URL: /api/graphql-api/shop/queries/get-cart --- outline: false examples: - id: get-cart-details title: Get Cart Details description: Retrieve the current shopping cart with all items. query: | mutation readCart { createReadCart(input: {}) { readCart { id _id grandTotal discountAmount cartToken customerId channelId subtotal baseSubtotal discountAmount baseDiscountAmount taxAmount baseTaxAmount shippingAmount baseShippingAmount grandTotal baseGrandTotal formattedSubtotal formattedDiscountAmount formattedTaxAmount formattedShippingAmount formattedGrandTotal couponCode items { totalCount pageInfo { startCursor endCursor hasNextPage hasPreviousPage } edges { cursor node { id cartId productId name sku quantity price basePrice total baseImage baseTotal discountAmount baseDiscountAmount taxAmount baseTaxAmount type formattedPrice formattedTotal priceInclTax basePriceInclTax formattedPriceInclTax totalInclTax baseTotalInclTax formattedTotalInclTax productUrlKey canChangeQty options } } } success message sessionToken isGuest itemsQty itemsCount haveStockableItems paymentMethod paymentMethodTitle subTotalInclTax baseSubTotalInclTax formattedSubTotalInclTax taxTotal formattedTaxTotal shippingAmountInclTax baseShippingAmountInclTax formattedShippingAmountInclTax } } } response: | { "data": { "createReadCart": { "readCart": { "id": "4484", "_id": 4484, "grandTotal": 4500, "discountAmount": 0, "cartToken": "4484", "customerId": 122, "channelId": 1, "subtotal": 4500, "baseSubtotal": 4500, "baseDiscountAmount": 0, "taxAmount": 0, "baseTaxAmount": 0, "shippingAmount": 0, "baseShippingAmount": 0, "baseGrandTotal": 4500, "formattedSubtotal": "$4,500.00", "formattedDiscountAmount": "$0.00", "formattedTaxAmount": "$0.00", "formattedShippingAmount": "$0.00", "formattedGrandTotal": "$4,500.00", "couponCode": null, "items": { "totalCount": 1, "pageInfo": { "startCursor": "MA==", "endCursor": "MA==", "hasNextPage": false, "hasPreviousPage": false }, "edges": [ { "cursor": "MA==", "node": { "id": "5648", "cartId": 4484, "productId": 2394, "name": "Verdant Luxe 2-Seater Velvet Sofa Green", "sku": "sku-234235345346-variant-2", "quantity": 9, "price": 500, "basePrice": 500, "total": 4500, "baseTotal": 4500, "discountAmount": 0, "baseDiscountAmount": 0, "taxAmount": 0, "baseTaxAmount": 0, "type": "simple", "formattedPrice": "$500.00", "formattedTotal": "$4,500.00", "priceInclTax": 500, "basePriceInclTax": 500, "formattedPriceInclTax": "$500.00", "totalInclTax": 4500, "baseTotalInclTax": 4500, "formattedTotalInclTax": "$4,500.00", "productUrlKey": "sku-234235345346-variant-2", "canChangeQty": true } } ] }, "success": null, "message": null, "sessionToken": null, "isGuest": false, "itemsQty": 9, "itemsCount": 1, "haveStockableItems": true, "paymentMethod": null, "paymentMethodTitle": null, "subTotalInclTax": 4500, "baseSubTotalInclTax": 4500, "formattedSubTotalInclTax": "$4,500.00", "taxTotal": 0, "formattedTaxTotal": "$0.00", "shippingAmountInclTax": 0, "baseShippingAmountInclTax": 0, "formattedShippingAmountInclTax": "$0.00" } } } } commonErrors: - error: CART_NOT_FOUND cause: Cart is empty or no active cart session exists solution: Add at least one product to the cart before calling this query - error: INVALID_CART_ID cause: Invalid cart ID format solution: Provide valid cart ID --- # Get Cart ## About The `getCart` query retrieves the contents and summary information for a customer's shopping cart. Use this query to: - Display cart previews in sidebars and mini carts - Render the full shopping cart page - Calculate cart totals with applied discounts and taxes - Show line item details and product information - Track cart state throughout the checkout process - Sync cart data with external inventory systems This query returns complete cart information including all items, quantities, prices, and applicable discounts/taxes needed for checkout and order processing. > **Note:** A cart session is only created when a product is added to the cart. If the cart is empty or no cart session exists, this query will return a `"Cart not found"` error. Make sure to add at least one product to the cart before calling this query. ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `id` | `String!` | Unique cart identifier/token. | | `items` | `[CartItem!]!` | Array of products in the cart. | | `items.id` | `String!` | Unique cart line item ID. | | `items.product` | `Product!` | Product object with id, name, sku. | | `items.quantity` | `Int!` | Number of items ordered. | | `items.price` | `Float!` | Unit price at time of adding to cart. | | `items.lineTotal` | `Float!` | Total for line item (price × quantity). | | `itemsCount` | `Int!` | Total number of line items in cart. | | `itemsQuantity` | `Int!` | Total quantity of all items. | | `subTotal` | `Float!` | Cart subtotal before discounts and taxes. | | `discountAmount` | `Float` | Total discount applied to cart. | | `taxAmount` | `Float!` | Calculated tax on cart. | | `shippingAmount` | `Float!` | Selected shipping cost. | | `total` | `Float!` | Grand total (subtotal + tax + shipping - discounts). | | `appliedCoupons` | `[CouponCode!]` | Active coupon codes applied to cart. | | `shippingMethods` | `[ShippingMethod!]` | Available shipping options and costs. | | `currency` | `String!` | Cart currency code (e.g., USD, EUR). | | `createdAt` | `DateTime!` | Cart creation timestamp. | | `updatedAt` | `DateTime!` | Last update timestamp. | --- # Get Category URL: /api/graphql-api/shop/queries/get-category --- outline: false examples: - id: get-category-by-id-basic title: Get Category by ID - Basic description: Retrieve basic information for a single category by its ID. query: | query getCategoryByID($id: ID!) { category(id: $id) { id _id position status translation { name slug urlPath description } } } variables: | { "id": "/api/shop/categories/23" } response: | { "data": { "category": { "id": "/api/shop/categories/23", "_id": 23, "position": 3, "status": "1", "translation": { "name": "Furniture", "slug": "furniture", "urlPath": "", "description": "

Discover our wide range of furniture designed to bring comfort, style, and functionality to every corner of your home. From elegant sofas and sturdy wooden tables to cozy beds and smart storage solutions, each piece is crafted with quality and care to suit your lifestyle.

" } } } } commonErrors: - error: id-required cause: Category ID parameter is missing solution: Provide the category ID as a required parameter - error: invalid-id-format cause: Invalid ID format. Expected IRI format like "/api/shop/categories/1" or numeric ID solution: Use either numeric ID (1) or IRI format (/api/shop/categories/1) - error: not-found cause: Category with given ID does not exist solution: Verify the category ID is correct and the category is active - id: get-category-by-numeric-id title: Get Category by Numeric ID description: Retrieve category using numeric ID format instead of IRI. query: | query getCategoryByID($id: ID!) { category(id: $id) { id _id position status translation { name slug urlPath } } } variables: | { "id": 23 } response: | { "data": { "category": { "id": "/api/shop/categories/23", "_id": 23, "position": 3, "status": "1", "translation": { "name": "Furniture", "slug": "furniture", "urlPath": "" } } } } commonErrors: - error: id-required cause: Category ID parameter is missing solution: Provide the category ID as a required parameter - error: invalid-id-format cause: Invalid ID format. Expected IRI format like "/api/shop/categories/1" or numeric ID solution: Use either numeric ID (1) or IRI format (/api/shop/categories/1) - error: not-found cause: Category with given ID does not exist solution: Verify the category ID is correct and the category is active - id: get-category-complete title: Get Category - Complete Details description: Retrieve complete category information including logos, banners, translations, and children. query: | query getCategoryByID($id: ID!) { category(id: $id) { id _id position logoPath logoUrl status displayMode _lft _rgt additional bannerPath bannerUrl translation { id _id categoryId name slug urlPath description metaTitle metaDescription metaKeywords localeId locale } translations { edges { node { id _id categoryId name slug urlPath description metaTitle metaDescription metaKeywords localeId locale } cursor } pageInfo { endCursor startCursor hasNextPage hasPreviousPage } totalCount } filterableAttributes { edges { node { id _id code adminName type } } } createdAt updatedAt url children { edges { node { id _id position logoUrl status translation { name slug } } } pageInfo { hasNextPage endCursor startCursor hasPreviousPage } totalCount } } } variables: | { "id": "/api/shop/categories/23" } response: | { "data": { "category": { "id": "/api/shop/categories/23", "_id": 23, "position": 3, "logoPath": "category/23/GuIZOJY3oW09ku4zqxIfKvtXho9gOnq4eCl0HmOW.webp", "logoUrl": "https://api-demo.bagisto.com/storage/category/23/GuIZOJY3oW09ku4zqxIfKvtXho9gOnq4eCl0HmOW.webp", "status": "1", "displayMode": "products_and_description", "_lft": "18", "_rgt": "25", "additional": null, "bannerPath": null, "bannerUrl": null, "translation": { "id": "/api/shop/category_translations/195", "_id": 195, "categoryId": "23", "name": "Furniture", "slug": "furniture", "urlPath": "", "description": "

Discover our wide range of furniture designed to bring comfort, style, and functionality to every corner of your home.

", "metaTitle": "", "metaDescription": "", "metaKeywords": "", "localeId": "1", "locale": "en" }, "translations": { "edges": [ { "node": { "id": "/api/shop/category_translations/195", "_id": 195, "categoryId": "23", "name": "Furniture", "slug": "furniture", "urlPath": "", "description": "

Discover our wide range of furniture designed to bring comfort, style, and functionality to every corner of your home.

", "metaTitle": "", "metaDescription": "", "metaKeywords": "", "localeId": "1", "locale": "en" }, "cursor": "MA==" }, { "node": { "id": "/api/shop/category_translations/204", "_id": 204, "categoryId": "23", "name": "الأثاث", "slug": "الأثاث", "urlPath": "", "description": "

اكتشف مجموعتنا الواسعة من الأثاث المصمم ليمنح كل زاوية في منزلك الراحة، والأناقة، والعملية.

", "metaTitle": "", "metaDescription": "", "metaKeywords": "", "localeId": null, "locale": "AR" }, "cursor": "MQ==" } ], "pageInfo": { "endCursor": "MQ==", "startCursor": "MA==", "hasNextPage": false, "hasPreviousPage": false }, "totalCount": 2 }, "filterableAttributes": { "edges": [ { "node": { "id": "/api/shop/attributes/3", "_id": 3, "code": "material", "adminName": "Material", "type": "select" } }, { "node": { "id": "/api/shop/attributes/1", "_id": 1, "code": "color", "adminName": "Color", "type": "select" } } ] }, "createdAt": "2025-09-03T12:43:50+05:30", "updatedAt": "2025-09-03T18:26:45+05:30", "url": "https://api-demo.bagisto.com/furniture", "children": { "edges": [ { "node": { "id": "/api/shop/categories/19", "_id": 19, "position": 7, "logoUrl": "https://api-demo.bagisto.com/storage/category/19/pmfWVVuhj7VK4dXFZG1ZlBeaUPwLrE4Ua99oer9l.webp", "status": "1", "translation": { "name": "Plastic Sofa", "slug": "plastic-sofa" } } }, { "node": { "id": "/api/shop/categories/20", "_id": 20, "position": 6, "logoUrl": "https://api-demo.bagisto.com/storage/category/20/WO71UuFFtbSRbjZVr7QUbNuMZM4PRSIAjHSLqUUY.webp", "status": "1", "translation": { "name": "Wooden Sofa", "slug": "wooden-sofa" } } }, { "node": { "id": "/api/shop/categories/21", "_id": 21, "position": 5, "logoUrl": "https://api-demo.bagisto.com/storage/category/21/Q8Z5RUYiBwPVKVkJNJ0XOfWitDiqP7admksTYxKm.webp", "status": "1", "translation": { "name": "Leather Sofa", "slug": "leather-sofa" } } } ], "pageInfo": { "hasNextPage": false, "endCursor": "Mg==", "startCursor": "MA==", "hasPreviousPage": false }, "totalCount": 3 } } } } commonErrors: - error: id-required cause: Category ID parameter is missing solution: Provide the category ID as a required parameter - error: not-found cause: Category with given ID does not exist solution: Verify the category ID is correct and the category is active - id: get-category-with-seo title: Get Category with SEO Data description: Retrieve category with complete SEO metadata for search engine optimization. query: | query getCategoryByID($id: ID!) { category(id: $id) { id _id url translation { name slug urlPath description metaTitle metaDescription metaKeywords } translations { edges { node { name slug metaTitle metaDescription metaKeywords locale } } totalCount } } } variables: | { "id": "/api/shop/categories/23" } response: | { "data": { "category": { "id": "/api/shop/categories/23", "_id": 23, "url": "https://api-demo.bagisto.com/furniture", "translation": { "name": "Furniture", "slug": "furniture", "urlPath": "", "description": "

Discover our wide range of furniture designed to bring comfort, style, and functionality to every corner of your home. From elegant sofas and sturdy wooden tables to cozy beds and smart storage solutions, each piece is crafted with quality and care to suit your lifestyle.

", "metaTitle": "", "metaDescription": "", "metaKeywords": "" }, "translations": { "edges": [ { "node": { "name": "Furniture", "slug": "furniture", "metaTitle": "", "metaDescription": "", "metaKeywords": "", "locale": "en" } }, { "node": { "name": "الأثاث", "slug": "الأثاث", "metaTitle": "", "metaDescription": "", "metaKeywords": "", "locale": "AR" } } ], "totalCount": 2 } } } } commonErrors: - error: MISSING_SEO_DATA cause: Category has no SEO metadata solution: Add SEO information in admin panel - id: get-category-display-settings title: Get Category Display Settings description: Retrieve category display configuration including mode, logos, and banners. query: | query getCategoryByID($id: ID!) { category(id: $id) { id _id position logoPath logoUrl bannerPath bannerUrl displayMode status _lft _rgt translation { name slug description } children { totalCount } } } variables: | { "id": "/api/shop/categories/23" } response: | { "data": { "category": { "id": "/api/shop/categories/23", "_id": 23, "position": 3, "logoPath": "category/23/GuIZOJY3oW09ku4zqxIfKvtXho9gOnq4eCl0HmOW.webp", "logoUrl": "https://api-demo.bagisto.com/storage/category/23/GuIZOJY3oW09ku4zqxIfKvtXho9gOnq4eCl0HmOW.webp", "bannerPath": null, "bannerUrl": null, "displayMode": "products_and_description", "status": "1", "_lft": "18", "_rgt": "25", "translation": { "name": "Furniture", "slug": "furniture", "description": "

Discover our wide range of furniture designed to bring comfort, style, and functionality to every corner of your home. From elegant sofas and sturdy wooden tables to cozy beds and smart storage solutions, each piece is crafted with quality and care to suit your lifestyle.

" }, "children": { "totalCount": 3 } } } } commonErrors: - error: INVALID_DISPLAY_MODE cause: Display mode value is not supported solution: Use products_only, category_and_products, or products_and_description - id: get-category-with-children title: Get Category with All Children description: Retrieve category with complete information about all child categories. query: | query getCategoryByID($id: ID!) { category(id: $id) { id _id translation { name slug } url children { edges { node { id _id position translation { name slug } logoUrl status children { totalCount } } } pageInfo { hasNextPage endCursor } totalCount } } } variables: | { "id": "/api/shop/categories/23" } response: | { "data": { "category": { "id": "/api/shop/categories/23", "_id": 23, "translation": { "name": "Furniture", "slug": "furniture" }, "url": "https://api-demo.bagisto.com/furniture", "children": { "edges": [ { "node": { "id": "/api/shop/categories/19", "_id": 19, "position": 7, "translation": { "name": "Plastic Sofa", "slug": "plastic-sofa" }, "logoUrl": "https://api-demo.bagisto.com/storage/category/19/pmfWVVuhj7VK4dXFZG1ZlBeaUPwLrE4Ua99oer9l.webp", "status": "1", "children": { "totalCount": 0 } } }, { "node": { "id": "/api/shop/categories/20", "_id": 20, "position": 6, "translation": { "name": "Wooden Sofa", "slug": "wooden-sofa" }, "logoUrl": "https://api-demo.bagisto.com/storage/category/20/WO71UuFFtbSRbjZVr7QUbNuMZM4PRSIAjHSLqUUY.webp", "status": "1", "children": { "totalCount": 0 } } }, { "node": { "id": "/api/shop/categories/21", "_id": 21, "position": 5, "translation": { "name": "Leather Sofa", "slug": "leather-sofa" }, "logoUrl": "https://api-demo.bagisto.com/storage/category/21/Q8Z5RUYiBwPVKVkJNJ0XOfWitDiqP7admksTYxKm.webp", "status": "1", "children": { "totalCount": 0 } } } ], "pageInfo": { "hasNextPage": false, "endCursor": "Mg==" }, "totalCount": 3 } } } } commonErrors: - error: NO_CHILDREN cause: Category has no child categories solution: This is normal for leaf categories --- # Get Category ## About The `category` query retrieves detailed information about a single category by its ID. Use this query to: - Display category detail pages with complete information - Show category images, descriptions, and SEO metadata - Retrieve display settings and configuration options - Build breadcrumb navigation - Get category hierarchy information - Access all translations for multi-language support - Display category children and sub-categories - Render category-specific layouts and content This query returns comprehensive category data including logos, banners, all translations, display modes, and child category information. ## Arguments | Argument | Type | Description | |----------|------|-------------| | `id` | `ID!` | Category ID. Supports two formats: numeric ID (e.g., `1`) or IRI format (e.g., `/api/shop/categories/1`). Required. | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `id` | `ID!` | Unique category API identifier. | | `_id` | `Int!` | Numeric category ID. | | `position` | `Int` | Display position among siblings. | | `logoPath` | `String` | File path to category logo. | | `logoUrl` | `String` | Full URL to category logo image. | | `bannerPath` | `String` | File path to category banner. | | `bannerUrl` | `String` | Full URL to category banner image. | | `status` | `Int` | Category status (0 = inactive, 1 = active). | | `displayMode` | `String` | Display mode: `products_only`, `category_and_products`, `products_and_description`. | | `_lft` | `Int` | Left value for nested set tree structure. | | `_rgt` | `Int` | Right value for nested set tree structure. | | `additional` | `String` | Additional metadata (JSON format). | | `translation` | `CategoryTranslation!` | Default locale translation. | | `translation.id` | `ID!` | Translation identifier. | | `translation._id` | `Int!` | Numeric translation ID. | | `translation.categoryId` | `Int!` | Associated category ID. | | `translation.name` | `String!` | Category name in current language. | | `translation.slug` | `String!` | URL slug for the category. | | `translation.urlPath` | `String!` | Full URL path including hierarchy. | | `translation.description` | `String` | Category description text. | | `translation.metaTitle` | `String` | SEO meta title tag. | | `translation.metaDescription` | `String` | SEO meta description. | | `translation.metaKeywords` | `String` | SEO keywords. | | `translation.localeId` | `Int` | Locale identifier. | | `translation.locale` | `String!` | Language locale code (e.g., 'en', 'ar', 'fr'). | | `translations` | `CategoryTranslationCollection!` | All available translations. | | `translations.edges` | `[Edge!]!` | Translation edges with cursors. | | `translations.edges.node` | `CategoryTranslation!` | Individual translation. | | `translations.edges.cursor` | `String!` | Pagination cursor for this translation. | | `translations.pageInfo` | `PageInfo!` | Pagination info for translations. | | `translations.pageInfo.hasNextPage` | `Boolean!` | More translations available. | | `translations.pageInfo.hasPreviousPage` | `Boolean!` | Previous translations available. | | `translations.pageInfo.startCursor` | `String` | First translation cursor. | | `translations.pageInfo.endCursor` | `String` | Last translation cursor. | | `translations.totalCount` | `Int!` | Total translations for this category. | | `createdAt` | `DateTime!` | Category creation timestamp. | | `updatedAt` | `DateTime!` | Last update timestamp. | | `url` | `String` | Full category URL. | | `filterableAttributes` | `AttributeCollection!` | Attributes configured for filtering in this category. | | `filterableAttributes.edges` | `[Edge!]!` | Attribute edges. | | `filterableAttributes.edges.node.id` | `ID!` | Attribute API identifier. | | `filterableAttributes.edges.node._id` | `Int!` | Numeric attribute ID. | | `filterableAttributes.edges.node.code` | `String!` | Attribute code (e.g., `color`, `size`). | | `filterableAttributes.edges.node.adminName` | `String!` | Attribute label shown in admin panel. | | `filterableAttributes.edges.node.type` | `String!` | Attribute input type (e.g., `select`, `multiselect`, `boolean`). | | `children` | `CategoryCollection!` | Child categories. | | `children.edges` | `[Edge!]!` | Child category edges. | | `children.edges.node` | `Category!` | Individual child category. | | `children.pageInfo` | `PageInfo!` | Pagination info for children. | | `children.totalCount` | `Int!` | Total child categories. | ## Display Modes | Mode | Description | |------|-------------| | `products_only` | Show only products, no category description. | | `category_and_products` | Show category and products together. | | `products_and_description` | Show products with category description. | ## Use Cases ### 1. Category Detail Page Use the "Complete Details" example to display a full category page with all information. ### 2. SEO Optimization Use the "With SEO Data" example to get all metadata for search engines. ### 3. Category Display Settings Use the "Display Settings" example to configure how the category should be rendered. ### 4. Category Hierarchy Use the "With All Children" example to show subcategories. ## Best Practices 1. **Cache Category Data** - Categories change infrequently, cache the response 2. **Include All Translations** - Fetch all translations for multi-language support 3. **Use Correct ID Format** - Use `/api/shop/categories/{id}` format when available 4. **Optimize Field Selection** - Request only fields your UI needs 5. **Include SEO Data** - Always fetch meta tags for optimization 6. **Check Status** - Verify category is active before displaying ## Related Resources - [Categories](/api/graphql-api/shop/queries/categories) - Get all categories with pagination - [Tree Categories](/api/graphql-api/shop/queries/tree-categories) - Get hierarchical category tree - [Get Products](/api/graphql-api/shop/queries/get-products) - Query products within a category - [Pagination Guide](/api/graphql-api/pagination) - Cursor pagination documentation - [Shop API Overview](/api/graphql-api/shop-api) - Overview of Shop API resources --- # Get Channel URL: /api/graphql-api/shop/queries/get-channel --- outline: false examples: - id: get-channel-by-id-basic title: Get Channel by ID - Basic description: Retrieve basic information for a single channel by its ID. query: | query getChannelByID($id: ID!) { channel(id: $id) { id _id code hostname timezone } } variables: | { "id": "/api/shop/channels/1" } response: | { "data": { "channel": { "id": "/api/shop/channels/1", "_id": 1, "code": "default", "hostname": "https://api-demo.bagisto.com", "timezone": null } } } commonErrors: - error: CHANNEL_NOT_FOUND cause: Channel with given ID does not exist solution: Verify the channel ID is correct - error: INVALID_ID_FORMAT cause: Invalid channel ID format solution: Use a valid channel ID like /api/shop/channels/1 - id: get-channel-complete title: Get Channel - Complete Details description: Retrieve complete channel information including logos, themes, and all translations. query: | query getChannelByID($id: ID!) { channel(id: $id) { id _id code timezone theme hostname logo favicon isMaintenanceOn allowedIps createdAt updatedAt logoUrl faviconUrl translation { id _id channelId locale name description maintenanceModeText createdAt updatedAt } translations { edges { node { id _id channelId locale name description maintenanceModeText createdAt updatedAt } cursor } pageInfo { endCursor startCursor hasNextPage hasPreviousPage } totalCount } } } variables: | { "id": "/api/shop/channels/1" } response: | { "data": { "channel": { "id": "/api/shop/channels/1", "_id": 1, "code": "default", "timezone": null, "theme": "default", "hostname": "https://api-demo.bagisto.com", "logo": "channels/default-logo.png", "favicon": "channels/default-favicon.ico", "isMaintenanceOn": "0", "allowedIps": "", "createdAt": null, "updatedAt": "2026-04-07T13:10:17+05:30", "logoUrl": "https://api-demo.bagisto.com/storage/channels/default-logo.png", "faviconUrl": "https://api-demo.bagisto.com/storage/channels/default-favicon.ico", "translation": { "id": "/api/shop/channel_translations/1", "_id": 1, "channelId": "1", "locale": "en", "name": "bagisto store", "description": "", "maintenanceModeText": "", "createdAt": null, "updatedAt": "2026-04-07T13:10:17+05:30" }, "translations": { "edges": [ { "node": { "id": "/api/shop/channel_translations/1", "_id": 1, "channelId": "1", "locale": "en", "name": "bagisto store", "description": "", "maintenanceModeText": "", "createdAt": null, "updatedAt": "2026-04-07T13:10:17+05:30" }, "cursor": "MA==" }, { "node": { "id": "/api/shop/channel_translations/5", "_id": 5, "channelId": "1", "locale": "es", "name": "Default", "description": null, "maintenanceModeText": null, "createdAt": null, "updatedAt": null }, "cursor": "MQ==" }, { "node": { "id": "/api/shop/channel_translations/2", "_id": 2, "channelId": "1", "locale": "fr", "name": "Default", "description": null, "maintenanceModeText": null, "createdAt": null, "updatedAt": null }, "cursor": "Mg==" }, { "node": { "id": "/api/shop/channel_translations/3", "_id": 3, "channelId": "1", "locale": "nl", "name": "Default", "description": null, "maintenanceModeText": null, "createdAt": null, "updatedAt": null }, "cursor": "Mw==" }, { "node": { "id": "/api/shop/channel_translations/4", "_id": 4, "channelId": "1", "locale": "tr", "name": "Default", "description": null, "maintenanceModeText": null, "createdAt": null, "updatedAt": null }, "cursor": "NA==" } ], "pageInfo": { "endCursor": "NA==", "startCursor": "MA==", "hasNextPage": false, "hasPreviousPage": false }, "totalCount": 5 } } } } commonErrors: - error: CHANNEL_NOT_FOUND cause: Channel with given ID does not exist solution: Verify the channel ID and check if it's active - error: UNAUTHORIZED cause: User is not authenticated solution: Provide valid authentication credentials - id: get-channel-with-branding title: Get Channel with Branding Assets description: Retrieve channel branding information including logos, favicons, and theme configuration. query: | query getChannelByID($id: ID!) { channel(id: $id) { id _id code hostname theme logo favicon logoUrl faviconUrl translation { name description } } } variables: | { "id": "/api/shop/channels/1" } response: | { "data": { "channel": { "id": "/api/shop/channels/1", "_id": 1, "code": "default", "hostname": "https://api-demo.bagisto.com", "theme": "default", "logo": "channels/default-logo.png", "favicon": "channels/default-favicon.ico", "logoUrl": "https://api-demo.bagisto.com/storage/channels/default-logo.png", "faviconUrl": "https://api-demo.bagisto.com/storage/channels/default-favicon.ico", "translation": { "name": "bagisto store", "description": "" } } } } commonErrors: - error: MISSING_BRANDING_ASSETS cause: Channel logo or favicon is not configured solution: Upload branding assets in admin panel - id: get-channel-maintenance-details title: Get Channel Maintenance Mode Details description: Retrieve channel maintenance mode status and configuration with custom messages. query: | query getChannelByID($id: ID!) { channel(id: $id) { id _id code hostname isMaintenanceOn allowedIps translation { locale name maintenanceModeText } translations { edges { node { locale maintenanceModeText } } totalCount } } } variables: | { "id": "/api/shop/channels/1" } response: | { "data": { "channel": { "id": "/api/shop/channels/1", "_id": 1, "code": "default", "hostname": "https://api-demo.bagisto.com", "isMaintenanceOn": "1", "allowedIps": "192.168.45.51", "translation": { "locale": "en", "name": "bagisto store", "maintenanceModeText": "Maintenance Mode" }, "translations": { "edges": [ { "node": { "locale": "en", "maintenanceModeText": "Maintenance Mode" } }, { "node": { "locale": "es", "maintenanceModeText": null } }, { "node": { "locale": "fr", "maintenanceModeText": null } }, { "node": { "locale": "nl", "maintenanceModeText": null } }, { "node": { "locale": "tr", "maintenanceModeText": null } } ], "totalCount": 5 } } } } commonErrors: - error: INVALID_IP_FORMAT cause: Allowed IPs format is invalid solution: Use comma-separated IPs or CIDR notation - id: get-channel-with-relationships title: Get Channel with Relationships description: Retrieve a channel with its related locales, currencies, default locale, and base currency. query: | query getChannelByID($id: ID!) { channel(id: $id) { id _id code hostname theme timezone homeSeo logoUrl faviconUrl locales { edges { node { id _id code name direction } } } currencies { edges { node { id _id code name symbol } } } defaultLocale { id _id code name direction } baseCurrency { id _id code name symbol } } } variables: | { "id": "/api/shop/channels/1" } response: | { "data": { "channel": { "id": "/api/shop/channels/1", "_id": 1, "code": "default", "hostname": "https://api-demo.bagisto.com", "theme": "default", "timezone": null, "homeSeo": { "meta_title": "Demo store", "meta_keywords": "Demo store meta keyword", "meta_description": "Demo store meta description" }, "logoUrl": null, "faviconUrl": null, "locales": { "edges": [ { "node": { "id": "/api/shop/locales/1", "_id": 1, "code": "en", "name": "English", "direction": "ltr" } }, { "node": { "id": "/api/shop/locales/10", "_id": 10, "code": "AR", "name": "Arabic", "direction": "rtl" } } ] }, "currencies": { "edges": [ { "node": { "id": "/api/shop/currencies/1", "_id": 1, "code": "USD", "name": "US Dollar", "symbol": "$" } }, { "node": { "id": "/api/shop/currencies/3", "_id": 3, "code": "AED", "name": "Dirham", "symbol": "د.إ" } } ] }, "defaultLocale": { "id": "/api/shop/locales/1", "_id": 1, "code": "en", "name": "English", "direction": "ltr" }, "baseCurrency": { "id": "/api/shop/currencies/1", "_id": 1, "code": "USD", "name": "US Dollar", "symbol": "$" } } } } commonErrors: - error: CHANNEL_NOT_FOUND cause: Channel with given ID does not exist solution: Verify the channel ID is correct - error: UNAUTHORIZED cause: User is not authenticated solution: Provide valid authentication credentials - id: get-channel-with-translations title: Get Channel with All Translations description: Retrieve channel with complete translation information for all languages. query: | query getChannelByID($id: ID!) { channel(id: $id) { id _id code hostname timezone translations { edges { node { id locale name description maintenanceModeText } cursor } pageInfo { hasNextPage endCursor } totalCount } } } variables: | { "id": "/api/shop/channels/1" } response: | { "data": { "channel": { "id": "/api/shop/channels/1", "_id": 1, "code": "default", "hostname": "https://api-demo.bagisto.com", "timezone": null, "translations": { "edges": [ { "node": { "id": "/api/shop/channel_translations/1", "locale": "en", "name": "bagisto store", "description": "", "maintenanceModeText": "" }, "cursor": "MA==" }, { "node": { "id": "/api/shop/channel_translations/5", "locale": "es", "name": "Default", "description": null, "maintenanceModeText": null }, "cursor": "MQ==" }, { "node": { "id": "/api/shop/channel_translations/2", "locale": "fr", "name": "Default", "description": null, "maintenanceModeText": null }, "cursor": "Mg==" }, { "node": { "id": "/api/shop/channel_translations/3", "locale": "nl", "name": "Default", "description": null, "maintenanceModeText": null }, "cursor": "Mw==" }, { "node": { "id": "/api/shop/channel_translations/4", "locale": "tr", "name": "Default", "description": null, "maintenanceModeText": null }, "cursor": "NA==" } ], "pageInfo": { "hasNextPage": false, "endCursor": "NA==" }, "totalCount": 5 } } } } commonErrors: - error: NO_TRANSLATIONS cause: Channel has no translations configured solution: Add translations in the admin panel --- # Get Channel ## About The `channel` query retrieves detailed information about a single channel by its ID. Use this query to: - Display channel-specific information and branding - Fetch channel configuration for your application - Get channel translations for multi-language support - Check maintenance mode status and allowed IPs - Retrieve channel themes and display settings - Access channel logos and branding assets - Build channel-specific pages and configurations - Handle channel-based routing and localization This query returns comprehensive channel data including logos, favicons, themes, all translations, and maintenance mode settings. ## Arguments | Argument | Type | Description | |----------|------|-------------| | `id` | `ID!` | Channel ID in format `/api/shop/channels/{id}` or numeric ID. | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `id` | `ID!` | Unique channel API identifier. | | `_id` | `Int!` | Numeric channel ID. | | `code` | `String!` | Unique channel code (e.g., 'default', 'mobile'). | | `timezone` | `String!` | Channel timezone (e.g., 'UTC', 'US/Eastern'). | | `theme` | `String` | Theme name assigned to this channel. | | `hostname` | `String!` | Channel hostname/domain. | | `logo` | `String` | File path to channel logo. | | `favicon` | `String` | File path to channel favicon. | | `isMaintenanceOn` | `Boolean!` | Whether maintenance mode is enabled. | | `allowedIps` | `String` | Comma-separated IPs allowed during maintenance (CIDR notation supported). | | `createdAt` | `DateTime!` | Channel creation timestamp. | | `updatedAt` | `DateTime!` | Last update timestamp. | | `logoUrl` | `String` | Full URL to channel logo image. | | `faviconUrl` | `String` | Full URL to channel favicon image. | | `translation` | `ChannelTranslation!` | Default locale translation. | | `translation.id` | `ID!` | Translation identifier. | | `translation._id` | `Int!` | Numeric translation ID. | | `translation.channelId` | `Int!` | Associated channel ID. | | `translation.locale` | `String!` | Language locale code (e.g., 'en', 'ar', 'fr'). | | `translation.name` | `String!` | Channel name in current language. | | `translation.description` | `String` | Channel description. | | `translation.maintenanceModeText` | `String` | Custom maintenance mode message. | | `translation.createdAt` | `DateTime!` | Translation creation timestamp. | | `translation.updatedAt` | `DateTime!` | Translation update timestamp. | | `homeSeo` | `String` | JSON string containing SEO metadata (meta_title, meta_description, meta_keywords). | | `locales` | `LocaleCollection!` | All locales (languages) enabled for this channel. | | `currencies` | `CurrencyCollection!` | All currencies enabled for this channel. | | `defaultLocale` | `Locale!` | The default locale used when no language is explicitly selected. | | `baseCurrency` | `Currency!` | The base currency used for pricing on this channel. | | `translations` | `ChannelTranslationCollection!` | All available translations. | | `translations.edges` | `[Edge!]!` | Translation edges with cursors. | | `translations.edges.node` | `ChannelTranslation!` | Individual translation. | | `translations.edges.cursor` | `String!` | Pagination cursor for this translation. | | `translations.pageInfo` | `PageInfo!` | Pagination information. | | `translations.pageInfo.hasNextPage` | `Boolean!` | More translations available. | | `translations.pageInfo.hasPreviousPage` | `Boolean!` | Previous translations available. | | `translations.pageInfo.startCursor` | `String` | First translation cursor. | | `translations.pageInfo.endCursor` | `String` | Last translation cursor. | | `translations.totalCount` | `Int!` | Total translations for this channel. | ## Channel Relationships The **Get Channel with Relationships** variant fetches the channel along with its associated configuration objects — locales, currencies, default locale, and base currency. These are not simple scalar fields; they are linked resources that define how the channel behaves for different regions and markets. | Relationship | Type | Description | |---|---|---| | `locales` | Collection | All languages enabled for this channel. Use this to build a language switcher or determine which locales the channel supports. | | `currencies` | Collection | All currencies enabled for this channel. Use this to display price selectors or determine accepted currencies. | | `defaultLocale` | Single object | The fallback language used when no locale is explicitly selected by the user. | | `baseCurrency` | Single object | The primary currency used for pricing and calculations on this channel. | **When to use this variant:** - Building a storefront that needs to know which languages and currencies are available - Rendering a locale or currency switcher in the header - Determining the channel's default display language for first-time visitors - Resolving the base currency before formatting prices ## IP Allowlist Format The `allowedIps` field supports multiple formats: | Format | Example | Description | |--------|---------|-------------| | Single IP | `192.168.1.1` | Exact IP address | | Multiple IPs | `192.168.1.1,192.168.1.2` | Comma-separated addresses | | CIDR Notation | `192.168.1.0/24` | Subnet range | | Mixed | `127.0.0.1,10.0.0.0/8` | Combination of formats | ## Use Cases ### 1. Channel Detail Page Use the "Complete Details" example to display full channel information with all metadata. ### 2. Branding Configuration Use the "With Branding Assets" example to get logos and favicons for dynamic theming. ### 3. Maintenance Mode Use the "Maintenance Details" example to check if channel is under maintenance and display appropriate messages. ### 4. Multi-Language Support Use the "With All Translations" example to display channel information in all languages. ### 5. Channel Relationships (Locales, Currencies) Use the "With Relationships" example to get all locales, currencies, default locale, and base currency associated with the channel. ### 6. Channel Routing Use the basic query to get channel hostname and timezone for routing and localization. ## Best Practices 1. **Cache Channel Data** - Channels change infrequently, cache the response per channel 2. **Check Maintenance Mode** - Always verify `isMaintenanceOn` status before displaying content 3. **Include Translations** - Fetch all translations for multi-language support 4. **Validate IP Allowlist** - Verify user IP against `allowedIps` during maintenance 5. **Use Correct ID Format** - Use `/api/shop/channels/{id}` format when available 6. **Apply Theme Dynamically** - Use `theme` field to load theme-specific CSS/configuration 7. **Display Localized Messages** - Use translations for maintenance messages in user's language ## Maintenance Mode Details During maintenance mode: - Set `isMaintenanceOn` to `true` - Configure `allowedIps` to allow specific IP addresses - Display localized `maintenanceModeText` to users - Redirect non-allowed users to maintenance page - Support CIDR notation for flexible IP ranges ## Related Resources - [Channels](/api/graphql-api/shop/queries/get-channels) - Get all channels with pagination - [Pagination Guide](/api/graphql-api/pagination) - Cursor pagination documentation - [Shop API Overview](/api/graphql-api/shop-api) - Overview of Shop API resources --- # Get Channels URL: /api/graphql-api/shop/queries/get-channels --- outline: false examples: - id: get-channels-basic title: Get Channels - Basic description: Retrieve all store channels with basic information. query: | query getChannels { channels { edges { node { id _id code hostname timezone } } pageInfo { hasNextPage endCursor } } } variables: | {} response: | { "data": { "channels": { "edges": [ { "node": { "id": "/api/shop/channels/1", "_id": 1, "code": "default", "hostname": "https://api-demo.bagisto.com", "timezone": null } } ], "pageInfo": { "hasNextPage": false, "endCursor": "MA==" } } } } commonErrors: - error: UNAUTHORIZED cause: Invalid or missing authentication token solution: Provide valid authentication credentials - error: NO_CHANNELS cause: No channels configured in the system solution: Create channels in the admin panel - id: get-channels-complete title: Get Channels - Complete Details description: Retrieve all channels with complete information including logos, themes, and translations. query: | query getChannels { channels { edges { node { id _id code timezone theme hostname logo favicon isMaintenanceOn allowedIps createdAt updatedAt logoUrl faviconUrl translation { id _id channelId locale name description maintenanceModeText createdAt updatedAt } translations { edges { node { id _id channelId locale name description maintenanceModeText createdAt updatedAt } cursor } pageInfo { endCursor startCursor hasNextPage hasPreviousPage } totalCount } } cursor } pageInfo { endCursor startCursor hasNextPage hasPreviousPage } totalCount } } variables: | {} response: | { "data": { "channels": { "edges": [ { "node": { "id": "/api/shop/channels/1", "_id": 1, "code": "default", "timezone": null, "theme": "default", "hostname": "https://api-demo.bagisto.com", "logo": null, "favicon": null, "isMaintenanceOn": "0", "allowedIps": "", "createdAt": null, "updatedAt": "2026-04-06T14:02:08+05:30", "logoUrl": null, "faviconUrl": null, "translation": { "id": "/api/shop/channel_translations/1", "_id": 1, "channelId": "1", "locale": "en", "name": "bagisto store", "description": "", "maintenanceModeText": "", "createdAt": null, "updatedAt": "2026-04-06T14:02:09+05:30" }, "translations": { "edges": [ { "node": { "id": "/api/shop/channel_translations/1", "_id": 1, "channelId": "1", "locale": "en", "name": "bagisto store", "description": "", "maintenanceModeText": "", "createdAt": null, "updatedAt": "2026-04-06T14:02:09+05:30" }, "cursor": "MA==" }, { "node": { "id": "/api/shop/channel_translations/5", "_id": 5, "channelId": "1", "locale": "es", "name": "Default", "description": null, "maintenanceModeText": null, "createdAt": null, "updatedAt": null }, "cursor": "MQ==" }, { "node": { "id": "/api/shop/channel_translations/2", "_id": 2, "channelId": "1", "locale": "fr", "name": "Default", "description": null, "maintenanceModeText": null, "createdAt": null, "updatedAt": null }, "cursor": "Mg==" }, { "node": { "id": "/api/shop/channel_translations/3", "_id": 3, "channelId": "1", "locale": "nl", "name": "Default", "description": null, "maintenanceModeText": null, "createdAt": null, "updatedAt": null }, "cursor": "Mw==" }, { "node": { "id": "/api/shop/channel_translations/4", "_id": 4, "channelId": "1", "locale": "tr", "name": "Default", "description": null, "maintenanceModeText": null, "createdAt": null, "updatedAt": null }, "cursor": "NA==" } ], "pageInfo": { "endCursor": "NA==", "startCursor": "MA==", "hasNextPage": false, "hasPreviousPage": false }, "totalCount": 5 } }, "cursor": "MA==" } ], "pageInfo": { "endCursor": "MA==", "startCursor": "MA==", "hasNextPage": false, "hasPreviousPage": false }, "totalCount": 1 } } } commonErrors: - error: UNAUTHORIZED cause: Invalid or missing authentication token solution: Provide valid authentication credentials - id: get-channels-with-pagination title: Get Channels with Pagination description: Retrieve channels using cursor-based pagination for handling large channel lists. query: | query getChannels($first: Int, $after: String) { channels(first: $first, after: $after) { edges { node { id _id code hostname translation { name description } logoUrl } cursor } pageInfo { endCursor startCursor hasNextPage hasPreviousPage } totalCount } } variables: | { "first": 10, "after": null } response: | { "data": { "channels": { "edges": [ { "node": { "id": "/api/shop/channels/1", "_id": 1, "code": "default", "hostname": "https://api-demo.bagisto.com", "translation": { "name": "bagisto store", "description": "" }, "logoUrl": null }, "cursor": "MA==" } ], "pageInfo": { "endCursor": "MA==", "startCursor": "MA==", "hasNextPage": false, "hasPreviousPage": false }, "totalCount": 1 } } } commonErrors: - error: INVALID_CURSOR cause: Pagination cursor format is invalid solution: Use cursor values from previous response - id: get-channels-with-translations title: Get Channels with All Translations description: Retrieve channels with complete translation information for multi-language support. query: | query getChannels { channels { edges { node { id _id code hostname timezone translations { edges { node { id locale name description maintenanceModeText } } totalCount } } } totalCount } } variables: | {} response: | { "data": { "channels": { "edges": [ { "node": { "id": "/api/shop/channels/1", "_id": 1, "code": "default", "hostname": "https://api-demo.bagisto.com", "timezone": null, "translations": { "edges": [ { "node": { "id": "/api/shop/channel_translations/1", "locale": "en", "name": "bagisto store", "description": "", "maintenanceModeText": "" } }, { "node": { "id": "/api/shop/channel_translations/5", "locale": "es", "name": "Default", "description": null, "maintenanceModeText": null } }, { "node": { "id": "/api/shop/channel_translations/2", "locale": "fr", "name": "Default", "description": null, "maintenanceModeText": null } }, { "node": { "id": "/api/shop/channel_translations/3", "locale": "nl", "name": "Default", "description": null, "maintenanceModeText": null } }, { "node": { "id": "/api/shop/channel_translations/4", "locale": "tr", "name": "Default", "description": null, "maintenanceModeText": null } } ], "totalCount": 5 } } } ], "totalCount": 1 } } } commonErrors: - error: NO_TRANSLATIONS cause: Channel has no translations solution: Add translations in the admin panel - id: get-channels-maintenance-mode title: Get Channels with Maintenance Mode Info description: Retrieve channels to check maintenance status and display custom maintenance messages. query: | query getChannels { channels { edges { node { id _id code hostname isMaintenanceOn allowedIps translation { name maintenanceModeText } } } totalCount } } variables: | {} response: | { "data": { "channels": { "edges": [ { "node": { "id": "/api/shop/channels/1", "_id": 1, "code": "default", "hostname": "https://api-demo.bagisto.com", "isMaintenanceOn": "0", "allowedIps": "", "translation": { "name": "bagisto store", "maintenanceModeText": "" } } } ], "totalCount": 1 } } } commonErrors: - error: INVALID_IP_FORMAT cause: Allowed IPs format is invalid solution: Use comma-separated IPs or CIDR notation --- # Get Channels ## About The `channels` query retrieves all store channels configured in your Bagisto instance. Use this query to: - Fetch all available sales channels for your store - Display channel-specific information and branding - Get channel translations for multi-language support - Check maintenance mode status and allowed IPs - Retrieve channel themes and configuration - Build channel switcher or store selector features - Access channel logos and branding assets - Manage multi-channel deployments This query returns comprehensive channel information including logos, favicons, themes, translations, and maintenance mode settings. ## Arguments | Argument | Type | Description | |----------|------|-------------| | `first` | `Int` | Number of channels to return (forward pagination). Max: 100. | | `after` | `String` | Pagination cursor for forward navigation. | | `last` | `Int` | Number of channels for backward pagination. Max: 100. | | `before` | `String` | Pagination cursor for backward navigation. | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `id` | `ID!` | Unique channel API identifier. | | `_id` | `Int!` | Numeric channel ID. | | `code` | `String!` | Unique channel code (e.g., 'default', 'mobile'). | | `timezone` | `String!` | Channel timezone (e.g., 'UTC', 'US/Eastern'). | | `theme` | `String` | Theme name assigned to this channel. | | `hostname` | `String!` | Channel hostname/domain. | | `logo` | `String` | File path to channel logo. | | `favicon` | `String` | File path to channel favicon. | | `isMaintenanceOn` | `Boolean!` | Whether maintenance mode is enabled. | | `allowedIps` | `String` | Comma-separated IPs allowed during maintenance. | | `createdAt` | `DateTime!` | Channel creation timestamp. | | `updatedAt` | `DateTime!` | Last update timestamp. | | `logoUrl` | `String` | Full URL to channel logo image. | | `faviconUrl` | `String` | Full URL to channel favicon image. | | `translation` | `ChannelTranslation!` | Default locale translation. | | `translation.id` | `ID!` | Translation identifier. | | `translation._id` | `Int!` | Numeric translation ID. | | `translation.channelId` | `Int!` | Associated channel ID. | | `translation.locale` | `String!` | Language locale code (e.g., 'en', 'ar', 'fr'). | | `translation.name` | `String!` | Channel name in current language. | | `translation.description` | `String` | Channel description. | | `translation.maintenanceModeText` | `String` | Custom maintenance mode message. | | `translation.createdAt` | `DateTime!` | Translation creation timestamp. | | `translation.updatedAt` | `DateTime!` | Translation update timestamp. | | `translations` | `ChannelTranslationCollection!` | All available translations. | | `translations.edges` | `[Edge!]!` | Translation edges with cursors. | | `translations.edges.node` | `ChannelTranslation!` | Individual translation. | | `translations.edges.cursor` | `String!` | Pagination cursor. | | `translations.pageInfo` | `PageInfo!` | Pagination information. | | `translations.totalCount` | `Int!` | Total translations for this channel. | | `pageInfo` | `PageInfo!` | Pagination information. | | `pageInfo.hasNextPage` | `Boolean!` | More channels available. | | `pageInfo.hasPreviousPage` | `Boolean!` | Previous channels available. | | `pageInfo.startCursor` | `String` | First channel cursor. | | `pageInfo.endCursor` | `String` | Last channel cursor. | | `totalCount` | `Int!` | Total channels in system. | ## Use Cases ### 1. Multi-Channel Support Use the "Complete Details" example to get all channel information for multi-channel deployments. ### 2. Channel Selection Use the "Basic" example for rendering a channel/store selector in your frontend. ### 3. Maintenance Mode Use the "Maintenance Mode Info" example to check if a channel is under maintenance and display appropriate messages. ### 4. Multi-Language Support Use the "With All Translations" example to display channel information in all languages. ### 5. Channel Branding Use the complete details example to fetch logos and favicons for channel-specific branding. ## Best Practices 1. **Cache Channel List** - Channels change infrequently, cache the entire list 2. **Check Maintenance Mode** - Always verify `isMaintenanceOn` status before redirecting users 3. **Include Translations** - Fetch all translations for multi-language support 4. **Validate Hostname** - Use `hostname` to route requests to correct channel 5. **Use Brand Assets** - Fetch `logoUrl` and `faviconUrl` for consistent branding 6. **Respect Timezones** - Use channel `timezone` for displaying dates and times ## Related Resources - [Get Channel](/api/graphql-api/shop/queries/get-channel) - Get single channel by ID - [Pagination Guide](/api/graphql-api/pagination) - Cursor pagination documentation - [Shop API Overview](/api/graphql-api/shop-api) - Overview of Shop API resources --- # Get Compare Item URL: /api/graphql-api/shop/queries/get-compare-item --- outline: false examples: - id: get-compare-item-by-id title: Get Single Compare Item description: Retrieve a specific compare item by its IRI. query: | query GetCompareItem($id: ID!) { compareItem(id: $id) { id _id product { id _id sku type name description shortDescription price formattedPrice minimumPrice formattedMinimumPrice maximumPrice formattedMaximumPrice guestCheckout locale channel reviews { edges { node { name id title rating } } totalCount } } customer { id firstName lastName gender dateOfBirth } createdAt updatedAt } } variables: | { "id": "/api/shop/compare-items/37" } response: | { "data": { "compareItem": { "id": "/api/shop/compare_items/37", "_id": 37, "product": { "id": "/api/shop/products/2500", "_id": 2500, "sku": "MINT-BLAZER-001", "type": "configurable", "name": "Mint Axis Unisex Tailored Blazer", "description": "The Mint Axis Unisex Tailored Blazer is built for those who lead with style, not trends. Featuring a structured yet comfortable silhouette, this blazer balances precision tailoring with a contemporary mint tone.", "shortDescription": "A modern mint blazer with a sharp tailored fit, designed for confident, gender-neutral styling and effortless statement wear.", "price": "0", "formattedPrice": "$0.00", "minimumPrice": "544", "formattedMinimumPrice": "$544.00", "maximumPrice": "544", "formattedMaximumPrice": "$544.00", "guestCheckout": "1", "locale": null, "channel": null, "reviews": { "edges": [ { "node": { "name": "Gerson Rivera", "id": "/api/shop/reviews/8", "title": "Velvet sofa", "rating": 4 } } ], "totalCount": 1 } }, "customer": { "id": "/api/shop/customers/122", "firstName": "John", "lastName": "Doe", "gender": "Male", "dateOfBirth": "1990-01-15" }, "createdAt": "2026-04-06T18:47:53+05:30", "updatedAt": "2026-04-06T18:47:53+05:30" } } } commonErrors: - error: ITEM_NOT_FOUND cause: Compare item ID does not exist or does not belong to the customer solution: Use a valid compare item IRI from the compareItems collection query - error: UNAUTHENTICATED cause: Missing or invalid Bearer token solution: Login and provide a valid customer authentication token --- # Get Compare Item ## About The `compareItem` query retrieves a single compare item by its IRI identifier. Use this query to: - Fetch detailed information about a specific compare item - Display product comparison details on a detail page - Verify a product exists in the customer's comparison list ## Authentication This query requires customer authentication: - **Authenticated customers**: Provide a valid customer authentication token in the `Authorization` header. Obtain this token via the [Customer Login API](/api/graphql-api/shop/mutations/customer-login). ``` Authorization: Bearer ``` ## Arguments | Argument | Type | Description | |----------|------|-------------| | `id` | `ID!` | The IRI of the compare item (e.g. `/api/shop/compare-items/606`). | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `id` | `ID!` | IRI identifier (e.g. `/api/shop/compare-items/606`). | | `_id` | `Int!` | Numeric identifier. | | `product` | `Product!` | Associated product with id, name, sku, price, description, etc. | | `product.reviews` | `ReviewCollection!` | Customer reviews left on the product. | | `product.reviews.edges` | `[ReviewEdge!]!` | Review edges. | | `product.reviews.edges.node.id` | `ID!` | Review IRI identifier. | | `product.reviews.edges.node.name` | `String` | Reviewer's name. | | `product.reviews.edges.node.title` | `String` | Review title. | | `product.reviews.edges.node.rating` | `Int!` | Rating value given by the customer (1–5). | | `product.reviews.totalCount` | `Int!` | Total number of reviews on this product. | | `customer` | `Customer!` | Associated customer with id, email, firstName, lastName. | | `createdAt` | `String` | Timestamp when the item was added. | | `updatedAt` | `String` | Timestamp when the item was last updated. | --- # Get Compare Items URL: /api/graphql-api/shop/queries/get-compare-items --- outline: false examples: - id: get-compare-items-basic title: Get All Compare Items description: Retrieve paginated compare items for the authenticated customer using cursor-based pagination. query: | query GetCompareItems($first: Int, $after: String) { compareItems(first: $first, after: $after) { edges { cursor node { id _id product { id _id sku type name description shortDescription price formattedPrice minimumPrice formattedMinimumPrice maximumPrice formattedMaximumPrice guestCheckout locale channel reviews { edges { node { name id title rating } } totalCount } } customer { id firstName lastName gender dateOfBirth } createdAt updatedAt } } pageInfo { endCursor startCursor hasNextPage hasPreviousPage } totalCount } } variables: | { "first": 10, "after": null } response: | { "data": { "compareItems": { "edges": [ { "cursor": "MA==", "node": { "id": "/api/shop/compare_items/36", "_id": 36, "product": { "id": "/api/shop/products/2495", "_id": 2495, "sku": "IVORY-OVERCOAT-001", "type": "configurable", "name": "Ivory Frost Classic Overcoat", "description": "The Ivory Frost Classic Overcoat blends modern simplicity with timeless winter design. Crafted in a smooth, insulating fabric, it offers dependable warmth while maintaining a lightweight, structured feel.", "shortDescription": "A sleek ivory overcoat with a tailored fit and soft warmth, designed to elevate everyday winter styling with minimal effort.", "price": "0", "formattedPrice": "$0.00", "minimumPrice": "500", "formattedMinimumPrice": "$500.00", "maximumPrice": "500", "formattedMaximumPrice": "$500.00", "guestCheckout": "1", "locale": null, "channel": null, "reviews": { "edges": [ { "node": { "name": "Gerson Rivera", "id": "/api/shop/reviews/7", "title": "Royal Sofa", "rating": 5 } } ], "totalCount": 1 } }, "customer": { "id": "/api/shop/customers/122", "firstName": "John", "lastName": "Doe", "gender": "Male", "dateOfBirth": "1990-01-15" }, "createdAt": "2026-04-06T18:47:53+05:30", "updatedAt": "2026-04-06T18:47:53+05:30" } }, { "cursor": "MQ==", "node": { "id": "/api/shop/compare_items/37", "_id": 37, "product": { "id": "/api/shop/products/2500", "_id": 2500, "sku": "MINT-BLAZER-001", "type": "configurable", "name": "Mint Axis Unisex Tailored Blazer", "description": "The Mint Axis Unisex Tailored Blazer is built for those who lead with style, not trends. Featuring a structured yet comfortable silhouette, this blazer balances precision tailoring with a contemporary mint tone.", "shortDescription": "A modern mint blazer with a sharp tailored fit, designed for confident, gender-neutral styling and effortless statement wear.", "price": "0", "formattedPrice": "$0.00", "minimumPrice": "544", "formattedMinimumPrice": "$544.00", "maximumPrice": "544", "formattedMaximumPrice": "$544.00", "guestCheckout": "1", "locale": null, "channel": null, "reviews": { "edges": [], "totalCount": 0 } }, "customer": { "id": "/api/shop/customers/122", "firstName": "John", "lastName": "Doe", "gender": "Male", "dateOfBirth": "1990-01-15" }, "createdAt": "2026-04-06T18:47:53+05:30", "updatedAt": "2026-04-06T18:47:53+05:30" } } ], "pageInfo": { "endCursor": "MQ==", "startCursor": "MA==", "hasNextPage": false, "hasPreviousPage": false }, "totalCount": 2 } } } commonErrors: - error: UNAUTHENTICATED cause: Missing or invalid Bearer token solution: Login and provide a valid customer authentication token - id: get-compare-items-paginated title: Get Compare Items - Next Page description: Fetch the next page of compare items using the endCursor from the previous response. query: | query GetCompareItems($first: Int, $after: String) { compareItems(first: $first, after: $after) { edges { cursor node { id _id product { id _id sku type name price formattedPrice minimumPrice formattedMinimumPrice } createdAt } } pageInfo { endCursor hasNextPage } totalCount } } variables: | { "first": 10, "after": "MQ==" } response: | { "data": { "compareItems": { "edges": [], "pageInfo": { "endCursor": null, "hasNextPage": false }, "totalCount": 1 } } } commonErrors: - error: INVALID_CURSOR cause: The cursor value is invalid or expired solution: Use a valid cursor from a previous response's pageInfo --- # Get Compare Items ## About The `compareItems` query retrieves a paginated list of products in the authenticated customer's comparison list. Use this query to: - Display the customer's product comparison list - Build comparison tables with product details - Implement pagination for large comparison lists - Show compare item counts and status This query uses cursor-based pagination and returns compare items with their associated product and customer data. ## Authentication This query requires customer authentication: - **Authenticated customers**: Provide a valid customer authentication token in the `Authorization` header. Obtain this token via the [Customer Login API](/api/graphql-api/shop/mutations/customer-login). ``` Authorization: Bearer ``` ## Arguments | Argument | Type | Description | |----------|------|-------------| | `first` | `Int` | Number of items to return per page. | | `after` | `String` | Cursor for pagination. Use `endCursor` from previous response. | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `edges` | `[CompareItemEdge!]` | Array of compare item edges with cursor and node. | | `edges.cursor` | `String!` | Cursor for this edge, used in pagination. | | `edges.node` | `CompareItem!` | The compare item object. | | `edges.node.id` | `ID!` | IRI identifier (e.g. `/api/shop/compare-items/606`). | | `edges.node._id` | `Int!` | Numeric identifier. | | `edges.node.product` | `Product!` | Associated product with id, name, sku, price, etc. | | `edges.node.product.reviews` | `ReviewCollection!` | Customer reviews left on the product. | | `edges.node.product.reviews.edges` | `[ReviewEdge!]!` | Review edges. | | `edges.node.product.reviews.edges.node.id` | `ID!` | Review IRI identifier. | | `edges.node.product.reviews.edges.node.name` | `String` | Reviewer's name. | | `edges.node.product.reviews.edges.node.title` | `String` | Review title. | | `edges.node.product.reviews.edges.node.rating` | `Int!` | Rating value given by the customer (1–5). | | `edges.node.product.reviews.totalCount` | `Int!` | Total number of reviews on this product. | | `edges.node.customer` | `Customer!` | Associated customer with id, email, firstName, lastName. | | `edges.node.createdAt` | `String` | Timestamp when the item was added. | | `edges.node.updatedAt` | `String` | Timestamp when the item was last updated. | | `pageInfo` | `PageInfo!` | Pagination metadata. | | `totalCount` | `Int!` | Total number of compare items. | --- # Get Countries URL: /api/graphql-api/shop/queries/get-countries --- outline: false examples: - id: get-countries-basic title: Get Countries - Basic description: Retrieve all available countries with basic information. query: | query countries { countries { edges { node { id _id code name } } pageInfo { hasNextPage endCursor } totalCount } } variables: | {} response: | { "data": { "countries": { "edges": [ { "node": { "id": "/api/shop/countries/1", "_id": 1, "code": "US", "name": "United States" } }, { "node": { "id": "/api/shop/countries/2", "_id": 2, "code": "GB", "name": "United Kingdom" } }, { "node": { "id": "/api/shop/countries/3", "_id": 3, "code": "AE", "name": "United Arab Emirates" } } ], "pageInfo": { "hasNextPage": true, "endCursor": "Mg==" }, "totalCount": 250 } } } commonErrors: - error: UNAUTHORIZED cause: Invalid or missing authentication token solution: Provide valid authentication credentials - error: NO_COUNTRIES cause: No countries configured in the system solution: Configure countries in the admin panel - id: get-countries-with-states title: Get Countries with States description: Retrieve all countries with their associated states/provinces and translations. query: | query countries { countries { edges { node { id _id code name states { edges { node { id _id code defaultName countryId countryCode translation { id locale defaultName } } } pageInfo { hasNextPage endCursor } totalCount } translations { edges { node { id locale name } } totalCount } } } pageInfo { hasNextPage endCursor } totalCount } } variables: | {} response: | { "data": { "countries": { "edges": [ { "node": { "id": "/api/shop/countries/1", "_id": 1, "code": "US", "name": "United States", "states": { "edges": [ { "node": { "id": "/api/shop/country-states/1", "_id": 1, "code": "AL", "defaultName": "Alabama", "countryId": 1, "countryCode": "US", "translation": { "id": "/api/shop/country-state-translations/1", "locale": "en", "defaultName": "Alabama" } } }, { "node": { "id": "/api/shop/country-states/2", "_id": 2, "code": "AK", "defaultName": "Alaska", "countryId": 1, "countryCode": "US", "translation": { "id": "/api/shop/country-state-translations/2", "locale": "en", "defaultName": "Alaska" } } } ], "pageInfo": { "hasNextPage": true, "endCursor": "MQ==" }, "totalCount": 50 }, "translations": { "edges": [ { "node": { "id": "/api/shop/country-translations/1", "locale": "en", "name": "United States" } }, { "node": { "id": "/api/shop/country-translations/2", "locale": "ar", "name": "الولايات المتحدة الأمريكية" } } ], "totalCount": 2 } } }, { "node": { "id": "/api/shop/countries/2", "_id": 2, "code": "GB", "name": "United Kingdom", "states": { "edges": [ { "node": { "id": "/api/shop/country-states/51", "_id": 51, "code": "ENG", "defaultName": "England", "countryId": 2, "countryCode": "GB", "translation": { "id": "/api/shop/country-state-translations/51", "locale": "en", "defaultName": "England" } } } ], "pageInfo": { "hasNextPage": false, "endCursor": "NTA==" }, "totalCount": 4 }, "translations": { "edges": [ { "node": { "id": "/api/shop/country-translations/3", "locale": "en", "name": "United Kingdom" } } ], "totalCount": 1 } } } ], "pageInfo": { "hasNextPage": true, "endCursor": "MQ==" }, "totalCount": 250 } } } commonErrors: - error: NO_STATES cause: Country has no states configured solution: Add states for the country in admin panel - id: get-countries-with-pagination title: Get Countries with Pagination description: Paginate through countries using cursor-based pagination. query: | query countries($first: Int, $after: String) { countries(first: $first, after: $after) { edges { node { id _id code name } cursor } pageInfo { endCursor startCursor hasNextPage hasPreviousPage } totalCount } } variables: | { "first": 10, "after": null } response: | { "data": { "countries": { "edges": [ { "node": { "id": "/api/shop/countries/1", "_id": 1, "code": "US", "name": "United States" }, "cursor": "MA==" }, { "node": { "id": "/api/shop/countries/2", "_id": 2, "code": "GB", "name": "United Kingdom" }, "cursor": "MQ==" }, { "node": { "id": "/api/shop/countries/3", "_id": 3, "code": "AE", "name": "United Arab Emirates" }, "cursor": "Mg==" } ], "pageInfo": { "endCursor": "Mg==", "startCursor": "MA==", "hasNextPage": true, "hasPreviousPage": false }, "totalCount": 250 } } } commonErrors: - error: INVALID_CURSOR cause: Pagination cursor format is invalid solution: Use cursor values from previous response - id: get-countries-with-translations title: Get Countries with All Translations description: Retrieve countries with complete translation information for multi-language support. query: | query countries { countries { edges { node { id _id code translations { edges { node { id locale name } } totalCount } } } totalCount } } variables: | {} response: | { "data": { "countries": { "edges": [ { "node": { "id": "/api/shop/countries/1", "_id": 1, "code": "US", "translations": { "edges": [ { "node": { "id": "/api/shop/country-translations/1", "locale": "en", "name": "United States" } }, { "node": { "id": "/api/shop/country-translations/2", "locale": "ar", "name": "الولايات المتحدة الأمريكية" } }, { "node": { "id": "/api/shop/country-translations/3", "locale": "fr", "name": "États-Unis" } } ], "totalCount": 3 } } }, { "node": { "id": "/api/shop/countries/2", "_id": 2, "code": "GB", "translations": { "edges": [ { "node": { "id": "/api/shop/country-translations/4", "locale": "en", "name": "United Kingdom" } }, { "node": { "id": "/api/shop/country-translations/5", "locale": "ar", "name": "المملكة المتحدة" } } ], "totalCount": 2 } } } ], "totalCount": 250 } } } commonErrors: - error: NO_TRANSLATIONS cause: Country has no translations configured solution: Add translations in the admin panel - id: get-countries-for-address-form title: Get Countries for Address Form description: Retrieve countries with their states optimized for checkout address forms. query: | query countries { countries { edges { node { id _id code name states { edges { node { id _id code defaultName } } totalCount } } cursor } pageInfo { hasNextPage endCursor } totalCount } } variables: | {} response: | { "data": { "countries": { "edges": [ { "node": { "id": "/api/shop/countries/1", "_id": 1, "code": "US", "name": "United States", "states": { "edges": [ { "node": { "id": "/api/shop/country-states/1", "_id": 1, "code": "AL", "defaultName": "Alabama" } }, { "node": { "id": "/api/shop/country-states/2", "_id": 2, "code": "AK", "defaultName": "Alaska" } }, { "node": { "id": "/api/shop/country-states/3", "_id": 3, "code": "AZ", "defaultName": "Arizona" } } ], "totalCount": 50 } }, "cursor": "MA==" }, { "node": { "id": "/api/shop/countries/2", "_id": 2, "code": "GB", "name": "United Kingdom", "states": { "edges": [ { "node": { "id": "/api/shop/country-states/51", "_id": 51, "code": "ENG", "defaultName": "England" } } ], "totalCount": 4 } }, "cursor": "MQ==" } ], "pageInfo": { "hasNextPage": true, "endCursor": "MQ==" }, "totalCount": 250 } } } commonErrors: - error: EMPTY_COUNTRIES_LIST cause: No countries are available solution: Ensure countries are configured in admin panel --- # Get Countries ## About The `countries` query retrieves all available countries and their states/provinces configured in your Bagisto store. Use this query to: - Build country/region selector dropdowns for addresses - Display available shipping destinations - Create address form fields with country and state options - Implement multi-language country names and state names - Display shipping country restrictions - Build geographic-based features and configurations - Populate checkout address forms with country and state data - Support currency and language selection based on country This query returns comprehensive country data including all states, translations for countries and states, and pagination support for large country lists. ## Arguments | Argument | Type | Description | |----------|------|-------------| | `first` | `Int` | Number of countries to return (forward pagination). Max: 100. | | `after` | `String` | Pagination cursor for forward navigation. | | `last` | `Int` | Number of countries for backward pagination. Max: 100. | | `before` | `String` | Pagination cursor for backward navigation. | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `id` | `ID!` | Unique country API identifier. | | `_id` | `Int!` | Numeric country ID. | | `code` | `String!` | ISO 3166-1 alpha-2 country code (e.g., 'US', 'GB', 'AE'). | | `name` | `String!` | Country name in default language. | | `states` | `CountryStateCollection!` | States/provinces for this country. | | `states.edges` | `[Edge!]!` | State edges with cursors. | | `states.edges.node` | `CountryState!` | Individual state information. | | `states.edges.node.id` | `ID!` | State API identifier. | | `states.edges.node._id` | `Int!` | Numeric state ID. | | `states.edges.node.code` | `String` | State code (e.g., 'CA' for California). | | `states.edges.node.defaultName` | `String!` | State name in default language. | | `states.edges.node.countryId` | `Int!` | Associated country ID. | | `states.edges.node.countryCode` | `String!` | Associated country code. | | `states.edges.node.translation` | `CountryStateTranslation!` | State translation in current locale. | | `states.edges.node.translation.id` | `ID!` | Translation identifier. | | `states.edges.node.translation._id` | `Int!` | Numeric translation ID. | | `states.edges.node.translation.countryStateId` | `Int!` | Associated state ID. | | `states.edges.node.translation.locale` | `String!` | Language locale code. | | `states.edges.node.translation.defaultName` | `String!` | State name in locale language. | | `states.edges.node.translations` | `StateTranslationCollection!` | All state translations. | | `states.edges.node.translations.edges` | `[Edge!]!` | Translation edges. | | `states.edges.node.translations.edges.node.id` | `ID!` | Translation ID. | | `states.edges.node.translations.edges.node.locale` | `String!` | Locale code. | | `states.edges.node.translations.edges.node.defaultName` | `String!` | Translated state name. | | `states.pageInfo` | `PageInfo!` | Pagination info for states. | | `states.pageInfo.hasNextPage` | `Boolean!` | More states available. | | `states.pageInfo.hasPreviousPage` | `Boolean!` | Previous states available. | | `states.pageInfo.startCursor` | `String` | First state cursor. | | `states.pageInfo.endCursor` | `String` | Last state cursor. | | `states.totalCount` | `Int!` | Total states for this country. | | `translations` | `CountryTranslationCollection!` | All country translations. | | `translations.edges` | `[Edge!]!` | Translation edges with cursors. | | `translations.edges.node` | `CountryTranslation!` | Individual translation. | | `translations.edges.node.id` | `ID!` | Translation identifier. | | `translations.edges.node._id` | `Int!` | Numeric translation ID. | | `translations.edges.node.countryId` | `Int!` | Associated country ID. | | `translations.edges.node.locale` | `String!` | Language locale code (e.g., 'en', 'ar', 'fr'). | | `translations.edges.node.name` | `String!` | Country name in locale language. | | `translations.edges.cursor` | `String!` | Pagination cursor for this translation. | | `translations.pageInfo` | `PageInfo!` | Pagination information for translations. | | `translations.totalCount` | `Int!` | Total translations for this country. | | `pageInfo` | `PageInfo!` | Pagination information. | | `pageInfo.hasNextPage` | `Boolean!` | More countries available. | | `pageInfo.hasPreviousPage` | `Boolean!` | Previous countries available. | | `pageInfo.startCursor` | `String` | First country cursor. | | `pageInfo.endCursor` | `String` | Last country cursor. | | `totalCount` | `Int!` | Total countries in system. | ## Use Cases ### 1. Address Form Use the "For Address Form" example to populate country and state dropdowns in checkout forms. ### 2. Multi-Language Support Use the "With All Translations" example to display country names in all supported languages. ### 3. Shipping Configuration Use the "Basic" example to get all available shipping destinations. ### 4. Pagination Use the "With Pagination" example to handle large country lists efficiently. ### 5. Complete Data Use the "With States" example to get all country and state information at once. ## Best Practices 1. **Cache Countries Data** - Countries rarely change, cache the entire list 2. **Load Countries on App Init** - Fetch and store countries when application starts 3. **Support State Selection** - Always fetch states for proper address forms 4. **Use Translations** - Fetch translations for all supported languages 5. **Optimize Field Selection** - Request only fields needed for your use case 6. **Pagination for Mobile** - Use pagination on mobile to reduce data transfer 7. **Sort by Frequency** - Display most-used countries first in UI ## State Availability Some countries may have states configured, while others may not: - **Countries with states**: US (50+), Canada, India, etc. - **Countries without states**: Many smaller countries return empty states collection - Always check `totalCount` to determine if states are available ## Related Resources - [Get Country](/api/graphql-api/shop/queries/get-country) - Get single country by code - [Country States](/api/graphql-api/shop/queries/get-country-states) - Get states for specific country - [Pagination Guide](/api/graphql-api/pagination) - Cursor pagination documentation - [Shop API Overview](/api/graphql-api/shop-api) - Overview of Shop API resources --- # Get Country URL: /api/graphql-api/shop/queries/get-country --- outline: false examples: - id: get-country-by-id-basic title: Get Country by ID - Basic description: Retrieve basic information for a single country by its ID. query: | query getSingleCountry($id: ID!) { country(id: $id) { id _id code name } } variables: | { "id": "106" } response: | { "data": { "country": { "id": "/api/shop/countries/106", "_id": 106, "code": "AE", "name": "United Arab Emirates" } } } commonErrors: - error: COUNTRY_NOT_FOUND cause: Country with given ID does not exist solution: Verify the country ID is correct - error: INVALID_ID_FORMAT cause: Invalid country ID format solution: Use a valid country ID like "106" or "/api/shop/countries/106" - id: get-country-with-states title: Get Country with States description: Retrieve country information with all associated states and translations. query: | query getSingleCountry($id: ID!) { country(id: $id) { id _id code name states { edges { node { id _id code defaultName countryId countryCode translations { edges { node { id locale defaultName } } } } } pageInfo { hasNextPage endCursor } totalCount } } } variables: | { "id": "1" } response: | { "data": { "country": { "id": "/api/shop/countries/1", "_id": 1, "code": "US", "name": "United States", "states": { "edges": [ { "node": { "id": "/api/shop/country-states/1", "_id": 1, "code": "AL", "defaultName": "Alabama", "countryId": 1, "countryCode": "US", "translations": { "edges": [ { "node": { "id": "/api/shop/country-state-translations/1", "locale": "en", "defaultName": "Alabama" } }, { "node": { "id": "/api/shop/country-state-translations/2", "locale": "ar", "defaultName": "ألاباما" } } ] } } }, { "node": { "id": "/api/shop/country-states/2", "_id": 2, "code": "AK", "defaultName": "Alaska", "countryId": 1, "countryCode": "US", "translations": { "edges": [ { "node": { "id": "/api/shop/country-state-translations/3", "locale": "en", "defaultName": "Alaska" } } ] } } } ], "pageInfo": { "hasNextPage": true, "endCursor": "MQ==" }, "totalCount": 50 } } } } commonErrors: - error: NO_STATES cause: Country has no states configured solution: Add states for the country in admin panel - id: get-country-with-translations title: Get Country with Translations description: Retrieve country with complete translation information for all languages. query: | query getSingleCountry($id: ID!) { country(id: $id) { id _id code name translations { edges { node { id _id locale name } cursor } pageInfo { hasNextPage endCursor } totalCount } } } variables: | { "id": "1" } response: | { "data": { "country": { "id": "/api/shop/countries/1", "_id": 1, "code": "US", "name": "United States", "translations": { "edges": [ { "node": { "id": "/api/shop/country-translations/1", "_id": 1, "locale": "en", "name": "United States" }, "cursor": "MA==" }, { "node": { "id": "/api/shop/country-translations/2", "_id": 2, "locale": "ar", "name": "الولايات المتحدة الأمريكية" }, "cursor": "MQ==" }, { "node": { "id": "/api/shop/country-translations/3", "_id": 3, "locale": "fr", "name": "États-Unis" }, "cursor": "Mg==" } ], "pageInfo": { "hasNextPage": false, "endCursor": "Mg==" }, "totalCount": 3 } } } } commonErrors: - error: NO_TRANSLATIONS cause: Country has no translations configured solution: Add translations in the admin panel - id: get-country-complete title: Get Country - Complete Details description: Retrieve complete country information with all states and translations. query: | query getSingleCountry($id: ID!) { country(id: $id) { id _id code name states { edges { node { id _id code defaultName countryId countryCode translations { edges { node { id locale defaultName } } } } } pageInfo { hasNextPage endCursor } totalCount } translations { edges { node { id _id locale name } } pageInfo { hasNextPage endCursor } totalCount } } } variables: | { "id": "106" } response: | { "data": { "country": { "id": "/api/shop/countries/106", "_id": 106, "code": "AE", "name": "United Arab Emirates", "states": { "edges": [ { "node": { "id": "/api/shop/country-states/750", "_id": 750, "code": "AD", "defaultName": "Abu Dhabi", "countryId": 106, "countryCode": "AE", "translations": { "edges": [ { "node": { "id": "/api/shop/country-state-translations/750", "locale": "en", "defaultName": "Abu Dhabi" } } ] } } } ], "pageInfo": { "hasNextPage": false, "endCursor": "NzQ5" }, "totalCount": 7 }, "translations": { "edges": [ { "node": { "id": "/api/shop/country-translations/106", "_id": 106, "locale": "en", "name": "United Arab Emirates" } }, { "node": { "id": "/api/shop/country-translations/107", "_id": 107, "locale": "ar", "name": "الإمارات العربية المتحدة" } } ], "pageInfo": { "hasNextPage": false, "endCursor": "MTA3" }, "totalCount": 2 } } } } commonErrors: - error: COUNTRY_NOT_FOUND cause: Country with given ID does not exist solution: Verify the country ID is correct --- # Get Country ## About The `country` query retrieves detailed information about a single country by its ID. Use this query to: - Display country-specific information and settings - Fetch all states/provinces for a country - Get country translations for multi-language support - Populate address forms with state options for a selected country - Build country-specific pages and configurations - Display country-specific shipping rules - Render localized country and state names This query returns comprehensive country data including all states with translations and country translations for multiple languages. ## Arguments | Argument | Type | Description | |----------|------|-------------| | `id` | `ID!` | Country ID in format "106" or "/api/shop/countries/106". | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `id` | `ID!` | Unique country API identifier. | | `_id` | `Int!` | Numeric country ID. | | `code` | `String!` | ISO 3166-1 alpha-2 country code (e.g., 'US', 'GB', 'AE'). | | `name` | `String!` | Country name in default language. | | `states` | `CountryStateCollection!` | States/provinces for this country. | | `states.edges` | `[Edge!]!` | State edges with cursors. | | `states.edges.node` | `CountryState!` | Individual state information. | | `states.edges.node.id` | `ID!` | State API identifier. | | `states.edges.node._id` | `Int!` | Numeric state ID. | | `states.edges.node.code` | `String` | State code (e.g., 'CA' for California). | | `states.edges.node.defaultName` | `String!` | State name in default language. | | `states.edges.node.countryId` | `Int!` | Associated country ID. | | `states.edges.node.countryCode` | `String!` | Associated country code. | | `states.edges.node.translations` | `StateTranslationCollection!` | All state translations. | | `states.edges.node.translations.edges` | `[Edge!]!` | Translation edges. | | `states.edges.node.translations.edges.node.id` | `ID!` | Translation ID. | | `states.edges.node.translations.edges.node.locale` | `String!` | Language locale code. | | `states.edges.node.translations.edges.node.defaultName` | `String!` | Translated state name. | | `states.pageInfo` | `PageInfo!` | Pagination info for states. | | `states.pageInfo.hasNextPage` | `Boolean!` | More states available. | | `states.pageInfo.endCursor` | `String` | Last state cursor. | | `states.totalCount` | `Int!` | Total states for this country. | | `translations` | `CountryTranslationCollection!` | All country translations. | | `translations.edges` | `[Edge!]!` | Translation edges with cursors. | | `translations.edges.node` | `CountryTranslation!` | Individual translation. | | `translations.edges.node.id` | `ID!` | Translation identifier. | | `translations.edges.node._id` | `Int!` | Numeric translation ID. | | `translations.edges.node.locale` | `String!` | Language locale code. | | `translations.edges.node.name` | `String!` | Country name in locale language. | | `translations.edges.cursor` | `String!` | Pagination cursor. | | `translations.pageInfo` | `PageInfo!` | Pagination info for translations. | | `translations.totalCount` | `Int!` | Total translations for this country. | ## Use Cases ### 1. Address Form State Selection Use the "With States" example to populate state dropdown when user selects a country. ### 2. Multi-Language Display Use the "With Translations" example to display country name in user's language. ### 3. Country Details Page Use the "Complete Details" example for a comprehensive country information display. ## Best Practices 1. **Cache Country Data** - Country information rarely changes, cache the response 2. **Load States Dynamically** - Fetch states when user selects a country in forms 3. **Include Translations** - Always fetch translations for multi-language support 4. **Handle No States** - Some countries have no states configured, handle gracefully 5. **Use Correct ID** - Use numeric ID format "106" or full API path ## Related Resources - [Countries](/api/graphql-api/shop/queries/get-countries) - Get all countries with pagination - [Country States](/api/graphql-api/shop/queries/get-country-states) - Get states for a country - [Country State](/api/graphql-api/shop/queries/get-country-state) - Get single state by ID - [Pagination Guide](/api/graphql-api/pagination) - Cursor pagination documentation - [Shop API Overview](/api/graphql-api/shop-api) - Overview of Shop API resources --- # Get Country State URL: /api/graphql-api/shop/queries/get-country-state --- outline: false examples: - id: get-country-state-basic title: Get Country State - Basic description: Retrieve a single state by ID with basic information. query: | query getCountryState($id: ID!) { countryState(id: $id) { id _id code defaultName countryId countryCode } } variables: | { "id": "16" } response: | { "data": { "countryState": { "id": "/api/shop/country-states/1", "_id": 1, "code": "AL", "defaultName": "Alabama", "countryId": 1, "countryCode": "US" } } } commonErrors: - error: NOT_FOUND cause: State with provided ID does not exist solution: Provide a valid state ID - error: INVALID_ID_FORMAT cause: ID format is invalid solution: Use numeric ID or API path format (e.g., "1" or "/api/shop/country-states/1") - id: get-country-state-with-translations title: Get Country State with Translations description: Retrieve a state with complete translation information in multiple languages. query: | query getCountryState($id: ID!) { countryState(id: $id) { id _id code defaultName countryId countryCode translations { edges { node { id locale defaultName } } totalCount } } } variables: | { "id": "750" } response: | { "data": { "countryState": { "id": "/api/shop/country-states/750", "_id": 750, "code": "AD", "defaultName": "Abu Dhabi", "countryId": 106, "countryCode": "AE", "translations": { "edges": [ { "node": { "id": "/api/shop/country-state-translations/750-en", "locale": "en", "defaultName": "Abu Dhabi" } }, { "node": { "id": "/api/shop/country-state-translations/750-ar", "locale": "ar", "defaultName": "أبو ظبي" } }, { "node": { "id": "/api/shop/country-state-translations/750-fr", "locale": "fr", "defaultName": "Abou Dabi" } } ], "totalCount": 3 } } } } commonErrors: - error: NO_TRANSLATIONS cause: State has no translations configured solution: Add translations in the admin panel - id: get-country-state-for-address-validation title: Get Country State for Address Validation description: Query to validate state information when processing customer addresses. query: | query getCountryState($id: ID!) { countryState(id: $id) { id _id code defaultName countryId countryCode } } variables: | { "id": "/api/shop/country-states/16" } response: | { "data": { "countryState": { "id": "/api/shop/country-states/16", "_id": 16, "code": "DC", "defaultName": "District of Columbia", "countryId": "244", "countryCode": "US" } } } commonErrors: - error: INVALID_STATE_FOR_COUNTRY cause: State does not belong to the specified country solution: Verify state-country relationship before saving address - id: get-country-state-complete title: Get Country State - Complete Details description: Fetch complete state information with all available fields for comprehensive data needs. query: | query getCountryState($id: ID!) { countryState(id: $id) { id _id code defaultName countryId countryCode translations { edges { node { id locale defaultName } } totalCount } } } variables: | { "id": "1" } response: | { "data": { "countryState": { "id": "/api/shop/country-states/1", "_id": 1, "code": "AL", "defaultName": "Alabama", "countryId": 1, "countryCode": "US", "translations": { "edges": [ { "node": { "id": "/api/shop/country-state-translations/1-en", "locale": "en", "defaultName": "Alabama" } }, { "node": { "id": "/api/shop/country-state-translations/1-es", "locale": "es", "defaultName": "Alabama" } }, { "node": { "id": "/api/shop/country-state-translations/1-fr", "locale": "fr", "defaultName": "Alabama" } }, { "node": { "id": "/api/shop/country-state-translations/1-ar", "locale": "ar", "defaultName": "ألاباما" } } ], "totalCount": 4 } } } } commonErrors: - error: STATE_NOT_AVAILABLE cause: State exists but is not available for transactions solution: Check administrative settings for state --- # Get Country State ## About The `countryState` query retrieves detailed information about a single state/province by its ID. Use this query to: - Fetch specific state information by ID - Validate state information in address forms - Get state translations for multi-language displays - Retrieve complete state details for order processing - Verify state belongs to correct country - Display state information in customer dashboards - Support state-specific business logic - Process returns and address updates This query returns comprehensive details about a single state including all translations. ## Arguments | Argument | Type | Description | |----------|------|-------------| | `id` | `ID!` | State ID (numeric or API path format, e.g., "1" or "/api/shop/country-states/1"). | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `id` | `ID!` | Unique state API identifier. | | `_id` | `Int!` | Numeric state ID. | | `code` | `String!` | State code (e.g., 'CA' for California, 'ON' for Ontario). | | `defaultName` | `String!` | State name in default language. | | `countryId` | `Int!` | Associated country ID. | | `countryCode` | `String!` | Associated country code (e.g., 'US', 'CA'). | | `translations` | `StateTranslationCollection!` | All state translations. | | `translations.edges` | `[Edge!]!` | Translation edges. | | `translations.edges.node` | `StateTranslation!` | Individual translation. | | `translations.edges.node.id` | `ID!` | Translation ID. | | `translations.edges.node.locale` | `String!` | Language locale code (e.g., 'en', 'ar', 'fr'). | | `translations.edges.node.defaultName` | `String!` | Translated state name. | | `translations.totalCount` | `Int!` | Total translations for this state. | ## Use Cases ### 1. Address Validation Validate that selected state exists and belongs to the correct country. ### 2. Multi-Language Display Show state name in customer's preferred language. ### 3. Order Processing Retrieve complete state information for order confirmation and shipping. ### 4. State Information Display Display state code and name in customer account or dashboard. ### 5. Checkout Confirmation Confirm state information before finalizing order. ## Best Practices 1. **Include Translations** - Always fetch translations for accurate multi-language support 2. **Validate Country Match** - Ensure state countryId matches selected country 3. **Store State Code** - Use state code for data consistency across systems 4. **Cache by ID** - Cache individual states to reduce API calls 5. **Handle Missing States** - Gracefully handle not found errors 6. **Use Default Name Fallback** - Fall back to defaultName if translation unavailable 7. **Verify Data Format** - Accept both numeric and path-format IDs ## ID Format The `id` argument accepts either format: ```graphql # Numeric format countryState(id: "1") # API path format countryState(id: "/api/shop/country-states/1") ``` Both formats return identical results. ## State Code Reference State codes vary by country: - **US States**: 2-letter codes (AL, AK, AZ, CA, CO, etc.) - **Canadian Provinces**: 2-letter codes (ON, BC, QC, AB, etc.) - **European Regions**: Varies by country - **Asian States**: Varies by country - **UAE Emirates**: 2-letter codes (AD, AJ, DU, etc.) ## Related Resources - [Countries](/api/graphql-api/shop/queries/get-countries) - Get all countries - [Country](/api/graphql-api/shop/queries/get-country) - Get single country with states - [Country States](/api/graphql-api/shop/queries/get-country-states) - Get all states for a country - [Shop API Overview](/api/graphql-api/shop-api) - Overview of Shop API resources --- # Get Country States URL: /api/graphql-api/shop/queries/get-country-states --- outline: false examples: - id: get-country-states-basic title: Get Country States - Basic description: Retrieve all states for a specific country with basic information. query: | query getCountryStates($countryId: Int!) { countryStates(countryId: $countryId) { edges { node { id _id code defaultName countryId countryCode } } totalCount } } variables: | { "countryId": 16 } response: | { "data": { "countryStates": { "edges": [ { "node": { "id": "/api/shop/country-states/95", "_id": 95, "code": "WI", "defaultName": "Wien", "countryId": "16", "countryCode": "AT" } }, { "node": { "id": "/api/shop/country-states/96", "_id": 96, "code": "NO", "defaultName": "Niederösterreich", "countryId": "16", "countryCode": "AT" } }, { "node": { "id": "/api/shop/country-states/97", "_id": 97, "code": "OO", "defaultName": "Oberösterreich", "countryId": "16", "countryCode": "AT" } } ], "totalCount": 9 } } } commonErrors: - error: INVALID_COUNTRY_ID cause: Country ID is invalid or does not exist solution: Provide a valid country ID - error: NO_STATES cause: Country has no states configured solution: This is normal for countries without states - id: get-country-states-with-translations title: Get Country States with Translations description: Retrieve all states for a country with complete translation information. query: | query getCountryStates($countryId: Int!) { countryStates(countryId: $countryId) { edges { node { id _id code defaultName countryId countryCode translations { edges { node { id locale defaultName } } totalCount } } } totalCount } } variables: | { "countryId": 16 } response: | { "data": { "countryStates": { "edges": [ { "node": { "id": "/api/shop/country-states/1", "_id": 1, "code": "AL", "defaultName": "Alabama", "countryId": 16, "countryCode": "US", "translations": { "edges": [ { "node": { "id": "/api/shop/country-state-translations/1", "locale": "en", "defaultName": "Alabama" } }, { "node": { "id": "/api/shop/country-state-translations/2", "locale": "ar", "defaultName": "ألاباما" } }, { "node": { "id": "/api/shop/country-state-translations/3", "locale": "fr", "defaultName": "Alabama" } } ], "totalCount": 3 } } }, { "node": { "id": "/api/shop/country-states/2", "_id": 2, "code": "AK", "defaultName": "Alaska", "countryId": 16, "countryCode": "US", "translations": { "edges": [ { "node": { "id": "/api/shop/country-state-translations/4", "locale": "en", "defaultName": "Alaska" } }, { "node": { "id": "/api/shop/country-state-translations/5", "locale": "ar", "defaultName": "ألاسكا" } } ], "totalCount": 2 } } } ], "totalCount": 50 } } } commonErrors: - error: NO_TRANSLATIONS cause: States have no translations configured solution: Add translations in the admin panel - id: get-country-states-with-pagination title: Get Country States with Pagination description: Retrieve states with pagination for countries with many states. query: | query getCountryStates($countryId: Int!, $first: Int, $after: String) { countryStates(countryId: $countryId, first: $first, after: $after) { edges { node { id _id code defaultName countryId countryCode } cursor } pageInfo { endCursor startCursor hasNextPage hasPreviousPage } totalCount } } variables: | { "countryId": 16, "first": 10, "after": null } response: | { "data": { "countryStates": { "edges": [ { "node": { "id": "/api/shop/country-states/1", "_id": 1, "code": "AL", "defaultName": "Alabama", "countryId": 16, "countryCode": "US" }, "cursor": "MA==" }, { "node": { "id": "/api/shop/country-states/2", "_id": 2, "code": "AK", "defaultName": "Alaska", "countryId": 16, "countryCode": "US" }, "cursor": "MQ==" } ], "pageInfo": { "endCursor": "MQ==", "startCursor": "MA==", "hasNextPage": true, "hasPreviousPage": false }, "totalCount": 50 } } } commonErrors: - error: INVALID_CURSOR cause: Pagination cursor format is invalid solution: Use cursor values from previous response - id: get-country-states-for-dropdown title: Get Country States for Dropdown Form description: Query optimized for populating state dropdown in address forms. query: | query getCountryStates($countryId: Int!) { countryStates(countryId: $countryId) { edges { node { id _id code defaultName } } totalCount } } variables: | { "countryId": 106 } response: | { "data": { "countryStates": { "edges": [ { "node": { "id": "/api/shop/country-states/750", "_id": 750, "code": "AD", "defaultName": "Abu Dhabi" } }, { "node": { "id": "/api/shop/country-states/751", "_id": 751, "code": "AJ", "defaultName": "Ajman" } }, { "node": { "id": "/api/shop/country-states/752", "_id": 752, "code": "DU", "defaultName": "Dubai" } } ], "totalCount": 7 } } } commonErrors: - error: EMPTY_STATES_LIST cause: Country has no states solution: Some countries do not have state divisions --- # Get Country States ## About The `countryStates` query retrieves all states/provinces for a specific country. Use this query to: - Populate state/province dropdowns in address forms - Display available states for a selected country - Get state translations for multi-language support - Build location-based features and configurations - Retrieve state codes and names for validation - Support dynamic form field population - Display state-specific shipping and tax information This query returns all states for a given country with optional translations and pagination support. ## Arguments | Argument | Type | Description | |----------|------|-------------| | `countryId` | `Int!` | Numeric country ID to fetch states for. | | `first` | `Int` | Number of states to return (forward pagination). Max: 100. | | `after` | `String` | Pagination cursor for forward navigation. | | `last` | `Int` | Number of states for backward pagination. Max: 100. | | `before` | `String` | Pagination cursor for backward navigation. | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `id` | `ID!` | Unique state API identifier. | | `_id` | `Int!` | Numeric state ID. | | `code` | `String` | State code (e.g., 'CA' for California, 'ON' for Ontario). | | `defaultName` | `String!` | State name in default language. | | `countryId` | `Int!` | Associated country ID. | | `countryCode` | `String!` | Associated country code. | | `translations` | `StateTranslationCollection!` | All state translations. | | `translations.edges` | `[Edge!]!` | Translation edges. | | `translations.edges.node` | `StateTranslation!` | Individual translation. | | `translations.edges.node.id` | `ID!` | Translation ID. | | `translations.edges.node.locale` | `String!` | Language locale code. | | `translations.edges.node.defaultName` | `String!` | Translated state name. | | `translations.totalCount` | `Int!` | Total translations for this state. | | `edges` | `[Edge!]!` | State edges with cursors. | | `edges.node` | `CountryState!` | Individual state. | | `edges.cursor` | `String!` | Pagination cursor. | | `pageInfo` | `PageInfo!` | Pagination information. | | `pageInfo.hasNextPage` | `Boolean!` | More states available. | | `pageInfo.hasPreviousPage` | `Boolean!` | Previous states available. | | `pageInfo.startCursor` | `String` | First state cursor. | | `pageInfo.endCursor` | `String` | Last state cursor. | | `totalCount` | `Int!` | Total states for this country. | ## Use Cases ### 1. Address Form State Dropdown Use the "For Dropdown Form" example to populate state dropdown when user selects a country. ### 2. Multi-Language Support Use the "With Translations" example to display state names in user's language. ### 3. Large State Lists Use the "With Pagination" example for countries with many states (e.g., US with 50 states). ## Best Practices 1. **Fetch on Country Selection** - Load states dynamically when user selects a country 2. **Cache Per Country** - Cache states for each country to reduce API calls 3. **Include Translations** - Always fetch translations for multi-language forms 4. **Handle Empty States** - Some countries have no states, handle gracefully 5. **Use State Code** - Store state code, not just name, for consistency 6. **Sort Alphabetically** - Present states in alphabetical order in dropdowns 7. **Minimize Fields** - Request only code and name for dropdown optimization ## State Codes Common state code formats: - **US States**: 2-letter codes (AL, AK, AZ) - **Canadian Provinces**: 2-letter codes (ON, BC, QC) - **European Regions**: Various formats - **Other Countries**: May vary by country ## Related Resources - [Countries](/api/graphql-api/shop/queries/get-countries) - Get all countries with pagination - [Country](/api/graphql-api/shop/queries/get-country) - Get single country with states - [Country State](/api/graphql-api/shop/queries/get-country-state) - Get single state by ID - [Pagination Guide](/api/graphql-api/pagination) - Cursor pagination documentation - [Shop API Overview](/api/graphql-api/shop-api) - Overview of Shop API resources --- # Get Currencies URL: /api/graphql-api/shop/queries/get-currencies --- outline: false examples: - id: get-currencies-basic title: Get Currencies - Basic description: Retrieve all store currencies with basic information. query: | query allCurrency { currencies { edges { node { id _id code name symbol } } pageInfo { hasNextPage endCursor } } } variables: | {} response: | { "data": { "currencies": { "edges": [ { "node": { "id": "/api/shop/currencies/1", "_id": 1, "code": "USD", "name": "US Dollar", "symbol": "$" } }, { "node": { "id": "/api/shop/currencies/3", "_id": 3, "code": "AED", "name": "Dirham", "symbol": "د.إ" } } ], "pageInfo": { "hasNextPage": false, "endCursor": "MQ==" } } } } commonErrors: - error: UNAUTHORIZED cause: Invalid or missing authentication token solution: Provide valid authentication credentials - error: NO_CURRENCIES cause: No currencies configured in the system solution: Create currencies in the admin panel - id: get-currencies-complete title: Get Currencies - Complete Details description: Retrieve all currencies with complete information including formatting options. query: | query allCurrency { currencies { edges { cursor node { id _id code name symbol decimal groupSeparator decimalSeparator currencyPosition } } pageInfo { endCursor startCursor hasNextPage hasPreviousPage } totalCount } } variables: | {} response: | { "data": { "currencies": { "edges": [ { "cursor": "MA==", "node": { "id": "/api/shop/currencies/1", "_id": 1, "code": "USD", "name": "US Dollar", "symbol": "$", "decimal": "2", "groupSeparator": ",", "decimalSeparator": ".", "currencyPosition": null } }, { "cursor": "MQ==", "node": { "id": "/api/shop/currencies/3", "_id": 3, "code": "AED", "name": "Dirham", "symbol": "د.إ", "decimal": "2", "groupSeparator": ",", "decimalSeparator": ".", "currencyPosition": "left_with_space" } } ], "pageInfo": { "endCursor": "MQ==", "startCursor": "MA==", "hasNextPage": false, "hasPreviousPage": false }, "totalCount": 2 } } } commonErrors: - error: INVALID_PAGINATION cause: Invalid pagination parameters provided solution: Ensure first/last are positive integers and cursors are valid - error: INVALID_CURSOR cause: Pagination cursor is invalid or expired solution: Use cursor values from the previous response - id: get-currencies-with-pagination title: Get Currencies with Pagination description: Retrieve currencies with cursor-based pagination for handling large datasets. query: | query getCurrenciesWithPagination($first: Int, $after: String) { currencies(first: $first, after: $after) { edges { cursor node { id _id code name symbol } } pageInfo { endCursor startCursor hasNextPage hasPreviousPage } totalCount } } variables: | { "first": 10, "after": null } response: | { "data": { "currencies": { "edges": [ { "cursor": "MA==", "node": { "id": "/api/shop/currencies/1", "_id": 1, "code": "USD", "name": "US Dollar", "symbol": "$" } }, { "cursor": "MQ==", "node": { "id": "/api/shop/currencies/3", "_id": 3, "code": "AED", "name": "Dirham", "symbol": "د.إ" } } ], "pageInfo": { "endCursor": "MQ==", "startCursor": "MA==", "hasNextPage": false, "hasPreviousPage": false }, "totalCount": 2 } } } commonErrors: - error: INVALID_FIRST_VALUE cause: The first argument exceeds maximum allowed value solution: Use first value between 1 and 100 - error: INVALID_CURSOR cause: The provided cursor is invalid solution: Use a valid cursor from a previous response --- # Get Currencies ## About The `currencies` query retrieves currency information from your store with support for pagination and detailed field access. This query is essential for: - Displaying available currency options for store visitors - Building currency selector/switcher interfaces - Retrieving currency formatting details (symbol, position, separators) - Managing multi-currency pricing configurations - Formatting prices correctly based on currency settings The query supports cursor-based pagination and allows you to fetch all currencies with full field access. ## Arguments | Argument | Type | Required | Description | |----------|------|----------|-------------| | `first` | `Int` | No | Number of currencies to retrieve from the start (forward pagination). Max: 100. | | `after` | `String` | No | Cursor to start after for forward pagination. | | `last` | `Int` | No | Number of currencies to retrieve from the end (backward pagination). Max: 100. | | `before` | `String` | No | Cursor to start before for backward pagination. | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `edges` | `[CurrencyEdge!]!` | Array of currency edges containing currencies and cursors. | | `edges.node` | `Currency!` | The actual currency object with id, code, name, symbol, and other fields. | | `edges.cursor` | `String!` | Pagination cursor for this currency. Use with `after` or `before` arguments. | | `pageInfo` | `PageInfo!` | Pagination metadata object. | | `pageInfo.hasNextPage` | `Boolean!` | Whether more currencies exist after the current page. | | `pageInfo.hasPreviousPage` | `Boolean!` | Whether currencies exist before the current page. | | `pageInfo.startCursor` | `String` | Cursor of the first currency on the current page. | | `pageInfo.endCursor` | `String` | Cursor of the last currency on the current page. | | `totalCount` | `Int!` | Total number of currencies available. | ## Currency Fields | Field | Type | Description | |-------|------|-------------| | `id` | `String!` | Unique identifier in format `/api/shop/currencies/{id}` | | `_id` | `Int!` | Numeric identifier for the currency | | `code` | `String!` | ISO 4217 currency code (e.g., "USD", "EUR", "INR") | | `name` | `String!` | Display name of the currency (e.g., "US Dollar", "Euro") | | `symbol` | `String!` | Currency symbol (e.g., "$", "€", "₹") | | `decimal` | `String` | Number of decimal places for the currency (e.g., `"2"`) | | `groupSeparator` | `String` | Thousands group separator character (e.g., `","`) | | `decimalSeparator` | `String` | Decimal separator character (e.g., `"."`) | | `currencyPosition` | `String` | Position of currency symbol relative to the amount: `"left"`, `"left_with_space"`, `"right"`, `"right_with_space"`, or `null` (use system default) | ## Common Use Cases ### Display All Available Currencies ```graphql query GetAllCurrencies { currencies { edges { node { id code name symbol } } } } ``` ### Build Currency Selector ```graphql query GetCurrenciesForSelector { currencies { edges { node { code name symbol currencyPosition } } } } ``` ### Get Currency Formatting Details ```graphql query GetCurrencyFormatting { currencies { edges { node { code symbol decimal groupSeparator decimalSeparator currencyPosition } } totalCount } } ``` ### Get Currencies with Pagination ```graphql query GetCurrenciesWithPagination($first: Int!) { currencies(first: $first) { edges { cursor node { id code name symbol } } pageInfo { hasNextPage hasPreviousPage startCursor endCursor } totalCount } } ``` ## Error Handling ### Missing Currencies Configuration ```json { "data": { "currencies": { "edges": [], "pageInfo": { "hasNextPage": false, "hasPreviousPage": false, "startCursor": null, "endCursor": null }, "totalCount": 0 } } } ``` ### Invalid Pagination Parameters ```json { "errors": [ { "message": "Argument \"first\" must be between 1 and 100" } ] } ``` ### Invalid Cursor ```json { "errors": [ { "message": "Invalid cursor provided" } ] } ``` ## Best Practices 1. **Cache Currencies** - Currencies change infrequently; implement client-side caching 2. **Use Formatting Fields** - Always use `decimal`, `groupSeparator`, `decimalSeparator`, and `currencyPosition` for correct price formatting 3. **Request Only Needed Fields** - Reduce payload by selecting specific fields 4. **Display Symbol Correctly** - Use `currencyPosition` to place symbol on the correct side of the amount 5. **Paginate When Needed** - For systems with many currencies, use pagination 6. **Use Variables** - Use GraphQL variables for dynamic currency queries ## Related Resources - [Get Currency](/api/graphql-api/shop/queries/get-currency) - Retrieve a single currency by ID - [Get Channel](/api/graphql-api/shop/queries/get-channel) - Channel includes base currency and supported currencies - [Pagination Guide](/api/graphql-api/pagination) - Cursor pagination documentation - [Shop API Overview](/api/graphql-api/shop-api) - Overview of Shop API resources --- # Get Single Currency URL: /api/graphql-api/shop/queries/get-currency --- outline: false examples: - id: get-currency-basic title: Get Single Currency - Basic description: Retrieve a single currency by ID with basic information. query: | query getCurrencyByID($id: ID!) { currency(id: $id) { id _id code name symbol } } variables: | { "id": "/api/shop/currencies/3" } response: | { "data": { "currency": { "id": "/api/shop/currencies/3", "_id": 3, "code": "AED", "name": "Dirham", "symbol": "د.إ" } } } commonErrors: - error: Variable "$id" of required type "ID!" was not provided. cause: Currency ID parameter is required solution: Provide a valid currency ID in format /api/shop/currencies/{id} or numeric ID - error: Invalid ID format. Expected IRI format like "/api/shop/currencies/1" or numeric ID cause: Currency ID is not in valid format solution: Verify the currency ID is in correct format, use "/api/shop/currencies/1" or "1" - error: Currency not found cause: Currency ID does not exist in the system solution: Verify the currency ID is correct and exists - id: get-currency-complete title: Get Single Currency - Complete Details description: Retrieve a single currency with all fields including formatting options. query: | query getCurrencyByID($id: ID!) { currency(id: $id) { id _id code name symbol decimal groupSeparator decimalSeparator currencyPosition } } variables: | { "id": "/api/shop/currencies/3" } response: | { "data": { "currency": { "id": "/api/shop/currencies/3", "_id": 3, "code": "AED", "name": "Dirham", "symbol": "د.إ", "decimal": "2", "groupSeparator": ",", "decimalSeparator": ".", "currencyPosition": "left_with_space" } } } commonErrors: - error: Currency not found cause: The provided currency ID does not exist solution: Use a valid currency ID from the get-currencies query - error: Invalid ID format. Expected IRI format like "/api/shop/currencies/1" or numeric ID cause: Invalid ID format provided solution: Provide valid currency ID in format /api/shop/currencies/1 or numeric ID like "1" - id: get-currency-by-numeric-id title: Get Single Currency - Using Numeric ID description: Retrieve a single currency by its numeric ID instead of IRI format. query: | query getCurrencyByID($id: ID!) { currency(id: $id) { id _id code name symbol decimal groupSeparator decimalSeparator currencyPosition } } variables: | { "id": "3" } response: | { "data": { "currency": { "id": "/api/shop/currencies/3", "_id": 3, "code": "AED", "name": "Dirham", "symbol": "د.إ", "decimal": "2", "groupSeparator": ",", "decimalSeparator": ".", "currencyPosition": "left_with_space" } } } commonErrors: - error: Invalid ID format. Expected IRI format like "/api/shop/currencies/1" or numeric ID cause: ID format is not recognized solution: Use either numeric ID like "1" or IRI format like /api/shop/currencies/1 - id: get-currency-formatting title: Get Currency Formatting Details description: Retrieve a currency with all formatting fields for correct price display. query: | query getCurrencyByID($id: ID!) { currency(id: $id) { id _id code name symbol decimal groupSeparator decimalSeparator currencyPosition } } variables: | { "id": "/api/shop/currencies/3" } response: | { "data": { "currency": { "id": "/api/shop/currencies/3", "_id": 3, "code": "AED", "name": "Dirham", "symbol": "د.إ", "decimal": "2", "groupSeparator": ",", "decimalSeparator": ".", "currencyPosition": "left_with_space" } } } commonErrors: - error: Currency not found cause: The provided currency ID does not exist solution: Use a valid currency ID from the get-currencies query --- # Get Single Currency ## About The `currency` query retrieves a single currency by ID with support for detailed field access. This query is essential for: - Fetching specific currency details for price formatting - Retrieving currency symbol and position for display - Getting formatting details (decimal places, separators) - Validating currency existence before operations - Building currency detail pages - Configuring currency-specific price formatting The query allows you to fetch a specific currency with all its properties. ## Arguments | Argument | Type | Required | Description | |----------|------|----------|-------------| | `id` | `ID!` | Yes | The unique identifier of the currency. Can be either numeric ID or IRI format (`/api/shop/currencies/{id}`). | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `currency` | `Currency` | The requested currency object, or null if not found. | ## Currency Fields | Field | Type | Description | |-------|------|-------------| | `id` | `String!` | Unique identifier in format `/api/shop/currencies/{id}` | | `_id` | `Int!` | Numeric identifier for the currency | | `code` | `String!` | ISO 4217 currency code (e.g., "USD", "EUR", "INR") | | `name` | `String!` | Display name of the currency (e.g., "US Dollar", "Euro") | | `symbol` | `String!` | Currency symbol (e.g., "$", "€", "₹") | | `decimal` | `String` | Number of decimal places for the currency (e.g., `"2"`) | | `groupSeparator` | `String` | Thousands group separator character (e.g., `","`) | | `decimalSeparator` | `String` | Decimal separator character (e.g., `"."`) | | `currencyPosition` | `String` | Position of currency symbol relative to the amount: `"left"`, `"left_with_space"`, `"right"`, `"right_with_space"`, or `null` (use system default) | ## Common Use Cases ### Get Currency Details by IRI ID ```graphql query GetCurrencyByIRI($id: ID!) { currency(id: $id) { id _id code name symbol currencyPosition } } ``` Variables: ```json { "id": "/api/shop/currencies/1" } ``` ### Get Currency Formatting Details ```graphql query GetCurrencyFormatting($id: ID!) { currency(id: $id) { code symbol decimal groupSeparator decimalSeparator currencyPosition } } ``` ### Get Currency Symbol and Position ```graphql query GetCurrencyDisplay($id: ID!) { currency(id: $id) { code name symbol currencyPosition } } ``` ### Validate Currency Existence ```graphql query ValidateCurrency($id: ID!) { currency(id: $id) { id code } } ``` ## Error Handling ### Currency Not Found ```json { "data": { "currency": null } } ``` ### Missing Required ID Parameter ```json { "errors": [ { "message": "Field \"currency\" argument \"id\" of type \"ID!\" is required but not provided." } ] } ``` ### Invalid ID Format ```json { "errors": [ { "message": "Invalid ID format. Expected IRI format like \"/api/shop/currencies/1\" or numeric ID" } ] } ``` ## Best Practices 1. **Always Provide ID** - The ID parameter is required for this query 2. **Check for Null** - Handle the case when currency is not found (returns null) 3. **Use Formatting Fields** - Always use `decimal`, `groupSeparator`, `decimalSeparator`, and `currencyPosition` for correct price display 4. **Cache Results** - Currencies change infrequently; implement caching 5. **Validate Before Using** - Verify currency exists before using in operations 6. **Use Variables** - Use GraphQL variables for dynamic currency queries 7. **Request Needed Fields** - Only request fields you'll actually use 8. **Display Symbol Correctly** - Use `currencyPosition` to place symbol on the correct side ## Related Resources - [Get Currencies](/api/graphql-api/shop/queries/get-currencies) - Retrieve all currencies with pagination - [Get Channel](/api/graphql-api/shop/queries/get-channel) - Channel includes base currency and supported currencies - [Pagination Guide](/api/graphql-api/pagination) - Cursor pagination documentation - [Shop API Overview](/api/graphql-api/shop-api) - Overview of Shop API resources --- # Get Customer Addresses URL: /api/graphql-api/shop/queries/get-customer-addresses --- outline: false examples: - id: get-customer-addresses title: Get Customer Addresses description: Retrieve all saved addresses for the authenticated customer with cursor-based pagination. query: | query getCustomerAddresses($first: Int, $after: String) { getCustomerAddresses(first: $first, after: $after) { edges { cursor node { id _id addressType companyName name firstName lastName email address city state country postcode phone vatId defaultAddress useForShipping additional createdAt updatedAt } } pageInfo { hasNextPage hasPreviousPage startCursor endCursor } totalCount } } variables: | { "first": 10 } response: | { "data": { "getCustomerAddresses": { "edges": [ { "cursor": "MQ==", "node": { "id": "/api/shop/customer-addresses/2829", "_id": 2829, "addressType": "customer", "companyName": "ABC Retail Solutions", "name": "John Doe", "firstName": "John", "lastName": "Doe", "email": "john.doe@example.com", "address": "123 Maple Street, Apt 4B", "city": "Springfield", "state": "IL", "country": "US", "postcode": "62704", "phone": "+15551234567", "vatId": "", "defaultAddress": true, "useForShipping": false, "additional": null, "createdAt": "2026-01-28T18:47:54+05:30", "updatedAt": "2026-01-28T18:47:54+05:30" } }, { "cursor": "Mg==", "node": { "id": "/api/shop/customer-addresses/2830", "_id": 2830, "addressType": "customer", "companyName": null, "name": "John Doe", "firstName": "John", "lastName": "Doe", "email": "john.doe@example.com", "address": "456 Oak Avenue, Suite 12", "city": "Los Angeles", "state": "CA", "country": "US", "postcode": "90001", "phone": "+15559876543", "vatId": null, "defaultAddress": false, "useForShipping": true, "additional": null, "createdAt": "2026-02-05T10:15:00+05:30", "updatedAt": "2026-02-05T10:15:00+05:30" } } ], "pageInfo": { "hasNextPage": false, "hasPreviousPage": false, "startCursor": "MQ==", "endCursor": "Mg==" }, "totalCount": 2 } } } commonErrors: - error: Unauthenticated cause: Missing or invalid Bearer token solution: Login and provide a valid customer authentication token - error: Invalid storefront key cause: Storefront key is missing or invalid solution: Provide a valid X-STOREFRONT-KEY header - id: get-customer-addresses-paginated title: Paginated Addresses (Forward) description: Retrieve customer addresses one page at a time using cursor-based pagination. query: | query getCustomerAddresses { getCustomerAddresses(first: 2) { edges { cursor node { _id firstName lastName address city state country postcode defaultAddress } } pageInfo { hasNextPage endCursor } totalCount } } variables: | {} response: | { "data": { "getCustomerAddresses": { "edges": [ { "cursor": "MQ==", "node": { "_id": 2829, "firstName": "John", "lastName": "Doe", "address": "123 Maple Street, Apt 4B", "city": "Springfield", "state": "IL", "country": "US", "postcode": "62704", "defaultAddress": true } }, { "cursor": "Mg==", "node": { "_id": 2830, "firstName": "John", "lastName": "Doe", "address": "456 Oak Avenue, Suite 12", "city": "Los Angeles", "state": "CA", "country": "US", "postcode": "90001", "defaultAddress": false } } ], "pageInfo": { "hasNextPage": true, "endCursor": "Mg==" }, "totalCount": 5 } } } - id: get-customer-addresses-next-page title: Paginated Addresses — Next Page description: Fetch the next page of addresses using the `after` cursor from a previous response. query: | query getCustomerAddresses { getCustomerAddresses(first: 2, after: "Mg==") { edges { cursor node { _id firstName lastName address city state country postcode defaultAddress } } pageInfo { hasNextPage endCursor } totalCount } } variables: | {} response: | { "data": { "getCustomerAddresses": { "edges": [ { "cursor": "Mw==", "node": { "_id": 2831, "firstName": "John", "lastName": "Doe", "address": "789 Pine Road", "city": "Chicago", "state": "IL", "country": "US", "postcode": "60601", "defaultAddress": false } }, { "cursor": "NA==", "node": { "_id": 2832, "firstName": "John", "lastName": "Doe", "address": "321 Elm Boulevard", "city": "Houston", "state": "TX", "country": "US", "postcode": "77001", "defaultAddress": false } } ], "pageInfo": { "hasNextPage": true, "endCursor": "NA==" }, "totalCount": 5 } } } - id: get-customer-addresses-minimal title: Minimal Address Fields description: Fetch only essential address fields for a lightweight dropdown or address selector. query: | query getCustomerAddresses { getCustomerAddresses(first: 50) { edges { node { _id name address city state country postcode defaultAddress } } totalCount } } variables: | {} response: | { "data": { "getCustomerAddresses": { "edges": [ { "node": { "_id": 2829, "name": "John Doe", "address": "123 Maple Street, Apt 4B", "city": "Springfield", "state": "IL", "country": "US", "postcode": "62704", "defaultAddress": true } }, { "node": { "_id": 2830, "name": "John Doe", "address": "456 Oak Avenue, Suite 12", "city": "Los Angeles", "state": "CA", "country": "US", "postcode": "90001", "defaultAddress": false } } ], "totalCount": 2 } } } - id: get-customer-address-by-id title: Get Customer Address by ID description: Retrieve a single saved address by its ID for the authenticated customer. query: | query customerAddress($id: ID!) { customerAddress(id: $id) { id _id addressType companyName name firstName lastName email address city state country postcode phone vatId defaultAddress useForShipping additional createdAt updatedAt } } variables: | { "id": "777" } response: | { "data": { "customerAddress": { "id": "/api/shop/customer-addresses/777", "_id": 777, "addressType": "customer", "companyName": "Webkul software limited company", "name": "abhishek kumar", "firstName": "abhishek", "lastName": "kumar", "email": "abhisheksinghsci123@gmail.com", "address": "pavitra marriage home NH-2 road tundla firozabad", "city": "34234", "state": "Salta", "country": "AR", "postcode": "34234234", "phone": "7906948573", "vatId": null, "defaultAddress": false, "useForShipping": false, "additional": null, "createdAt": "2025-05-21T20:04:48+05:30", "updatedAt": "2025-05-21T20:04:48+05:30" } } } commonErrors: - error: Unauthenticated cause: Missing or invalid Bearer token solution: Login and provide a valid customer authentication token - error: Item not found cause: Address ID does not exist or does not belong to the authenticated customer solution: Verify the address ID and ensure it belongs to the logged-in customer - id: get-customer-addresses-with-company title: Addresses with Company & VAT Details description: Retrieve addresses including business-specific fields like company name and VAT ID. query: | query getCustomerAddresses { getCustomerAddresses(first: 10) { edges { node { _id companyName firstName lastName email address city state country postcode phone vatId defaultAddress addressType } } totalCount } } variables: | {} response: | { "data": { "getCustomerAddresses": { "edges": [ { "node": { "_id": 2829, "companyName": "ABC Retail Solutions", "firstName": "John", "lastName": "Doe", "email": "john.doe@example.com", "address": "123 Maple Street, Apt 4B", "city": "Springfield", "state": "IL", "country": "US", "postcode": "62704", "phone": "+15551234567", "vatId": "US123456789", "defaultAddress": true, "addressType": "customer" } }, { "node": { "_id": 2833, "companyName": null, "firstName": "John", "lastName": "Doe", "email": "john.doe@example.com", "address": "789 Residential Lane", "city": "Miami", "state": "FL", "country": "US", "postcode": "33101", "phone": "+15559991234", "vatId": null, "defaultAddress": false, "addressType": "customer" } } ], "totalCount": 2 } } } --- # Get Customer Addresses Retrieve all saved addresses for the authenticated customer. Addresses are used for billing and shipping during checkout, and can be managed from the customer's account dashboard. ## Query ```graphql query getCustomerAddresses($first: Int, $after: String) { getCustomerAddresses(first: $first, after: $after) { edges { cursor node { id _id addressType companyName name firstName lastName email address city state country postcode phone vatId defaultAddress useForShipping additional createdAt updatedAt } } pageInfo { hasNextPage hasPreviousPage startCursor endCursor } totalCount } } ``` ## Authentication This query requires a valid customer authentication token in the `Authorization` header. Use the [Customer Login API](/api/graphql-api/shop/mutations/customer-login) to retrieve the token. ## Request Headers | Header | Value | Required | Description | |--------|-------|----------|-------------| | `Content-Type` | `application/json` | Yes | Request content type | | `X-STOREFRONT-KEY` | `pk_storefront_xxx` | Yes | Storefront API key | | `Authorization` | `Bearer {token}` | Yes | Customer authentication token | ## Arguments | Argument | Type | Required | Description | |----------|------|----------|-------------| | `first` | Int | No | Number of addresses to fetch (default: 10, max: 100) | | `after` | String | No | Cursor for forward pagination | | `last` | Int | No | Number of addresses to fetch backward | | `before` | String | No | Cursor for backward pagination | ## Response Fields | Field | Type | Description | |-------|------|-------------| | `id` | String | IRI identifier (e.g., `/api/shop/customer-addresses/2829`) | | `_id` | Int | Numeric database ID | | `addressType` | String | Address type (e.g., `customer`) | | `companyName` | String | Company name (nullable) | | `name` | String | Full name (computed from first + last name) | | `firstName` | String | First name | | `lastName` | String | Last name | | `email` | String | Email address | | `address` | String | Street address | | `city` | String | City | | `state` | String | State/Province code | | `country` | String | Country code (ISO 3166-1 alpha-2) | | `postcode` | String | Postal/Zip code | | `phone` | String | Phone number | | `vatId` | String | VAT identification number (nullable) | | `defaultAddress` | Boolean | Whether this is the default address | | `useForShipping` | Boolean | Whether this address is used for shipping | | `additional` | JSON | Additional address data (nullable) | | `createdAt` | DateTime | Creation timestamp | | `updatedAt` | DateTime | Last update timestamp | ## cURL Example ```bash curl -X POST "https://api-demo.bagisto.com/api/graphql" \ -H "X-STOREFRONT-KEY: pk_storefront_your_key_here" \ -H "Authorization: Bearer YOUR_CUSTOMER_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "query": "query { getCustomerAddresses(first: 10) { edges { cursor node { _id firstName lastName address city state country postcode phone defaultAddress } } pageInfo { hasNextPage endCursor } totalCount } }" }' ``` ## Pagination Uses cursor-based pagination: - **Forward:** `first` + `after` — use `endCursor` from `pageInfo` as the next `after` value - **Backward:** `last` + `before` — use `startCursor` from `pageInfo` as the next `before` value - **Check for more:** read `pageInfo.hasNextPage` or `pageInfo.hasPreviousPage` ## Empty Response When the customer has no saved addresses: ```json { "data": { "getCustomerAddresses": { "edges": [], "pageInfo": { "hasNextPage": false, "hasPreviousPage": false, "startCursor": null, "endCursor": null }, "totalCount": 0 } } } ``` ## Use Cases | Use Case | Approach | |----------|----------| | **Address book** | Fetch all addresses with `first: 50` for account dashboard | | **Checkout selector** | Fetch minimal fields (`_id`, `name`, `address`, `city`, `defaultAddress`) for an address dropdown | | **Default address** | Look for the entry with `defaultAddress: true` | | **Business addresses** | Include `companyName` and `vatId` fields for B2B customers | | **Pre-fill checkout** | Use `defaultAddress: true` entry to pre-populate shipping/billing forms | ## Error Responses ### Unauthenticated ```json { "errors": [ { "message": "Unauthenticated. Please login to perform this action", "locations": [{ "line": 2, "column": 3 }], "path": ["getCustomerAddresses"] } ] } ``` ## Get a Single Customer Address To fetch one specific address by its ID, use the `customerAddress` query. This is the single-item counterpart to `getCustomerAddresses` and is useful for edit screens, address detail pages, or pre-filling a form. ### Query ```graphql query customerAddress($id: ID!) { customerAddress(id: $id) { id _id addressType companyName name firstName lastName email address city state country postcode phone vatId defaultAddress useForShipping additional createdAt updatedAt } } ``` ### Variables ```json { "id": "777" } ``` The `id` accepts either a numeric address ID (`"777"`) or the full IRI (`"/api/shop/customer-addresses/777"`). ### Arguments | Argument | Type | Required | Description | |----------|------|----------|-------------| | `id` | ID | ✅ Yes | The numeric ID or IRI of the address to fetch | ### Response The response shape matches a single `node` from `getCustomerAddresses` (same fields, no `edges` / `pageInfo` / `totalCount` wrapper). ### Error Responses - **Unauthenticated** — Missing or invalid Bearer token. - **Item not found** — The address does not exist or does not belong to the authenticated customer. ## Related Documentation - [Create Customer Address](/api/graphql-api/shop/mutations/create-customer-address) - [Update Customer Address](/api/graphql-api/shop/mutations/update-customer-address) - [Delete Customer Address](/api/graphql-api/shop/mutations/delete-customer-address) --- # Get Customer Downloadable Product URL: /api/graphql-api/shop/queries/get-customer-downloadable-product --- outline: false examples: - id: get-customer-downloadable-product title: Get Single Customer Downloadable Product description: Retrieve details of a specific downloadable product purchase by its IRI identifier. query: | query GetCustomerDownloadableProduct { customerDownloadableProduct(id: "/api/shop/customer-downloadable-products/4") { _id productName name fileName type downloadBought downloadUsed downloadCanceled status downloadUrl remainingDownloads order { _id incrementId status grandTotal } createdAt updatedAt } } variables: | {} response: | { "data": { "customerDownloadableProduct": { "_id": 4, "productName": "Complete Personal Finance Guide (eBook PDF)", "name": "Full eBook PDF", "fileName": null, "type": "url", "downloadBought": 10, "downloadUsed": 1, "downloadCanceled": 0, "status": "available", "downloadUrl": "https://api-demo.bagisto.com/api/shop/customer-downloadable-products/4/download", "remainingDownloads": "9", "order": { "_id": 531, "incrementId": "531", "status": "completed", "grandTotal": 138 }, "createdAt": "2026-04-02T18:54:41+05:30", "updatedAt": "2026-04-02T18:55:29+05:30" } } } commonErrors: - error: Unauthenticated cause: Missing or invalid Bearer token solution: Login and provide a valid customer authentication token - error: Not Found cause: The downloadable product purchase ID does not exist or belongs to another customer solution: Verify the purchase ID and ensure you are authenticated as the correct customer --- # Get Customer Downloadable Product Retrieve details of a specific downloadable product purchase by its IRI identifier. This is a **read-only** API — customers can view a single purchased downloadable link, check its download status, and see remaining downloads. ## Query ```graphql query GetCustomerDownloadableProduct { customerDownloadableProduct(id: "/api/shop/customer-downloadable-products/1") { _id productName name fileName type downloadBought downloadUsed downloadCanceled status downloadUrl remainingDownloads order { _id incrementId status grandTotal } createdAt updatedAt } } ``` ## Arguments | Argument | Type | Required | Description | |----------|------|----------|-------------| | `id` | ID! | Yes | The IRI of the downloadable product purchase (e.g., `/api/shop/customer-downloadable-products/1`). The numeric ID used in this IRI is the `_id` field from the [Get Downloadable Products](/api/graphql-api/shop/queries/get-customer-downloadable-products) query. | ## Response Fields | Field | Type | Description | |-------|------|-------------| | `_id` | Int | Downloadable link purchase ID | | `productName` | String | Name of the purchased product | | `name` | String | Name of the downloadable link | | `fileName` | String | Display name of the file | | `type` | String | Link type: `file` or `url` | | `downloadBought` | Int | Total number of allowed downloads | | `downloadUsed` | Int | Number of times downloaded | | `downloadCanceled` | Int | Number of canceled downloads | | `status` | String | Purchase status: `available`, `expired`, or `pending` | | `downloadUrl` | String | Direct REST API URL to download the purchased file. Use this URL with a GET request and customer authentication to download the file. See [Download Downloadable Product](/api/graphql-api/shop/queries/download-downloadable-product). | | `remainingDownloads` | Int | Computed remaining downloads (`null` if unlimited) | | `order` | Object | Associated order details | | `createdAt` | DateTime | Purchase creation date | | `updatedAt` | DateTime | Purchase last update date | ## Request Headers | Header | Value | Required | Description | |--------|-------|----------|-------------| | `Content-Type` | `application/json` | Yes | Request content type | | `X-STOREFRONT-KEY` | `pk_storefront_xxx` | Yes | Storefront API key | | `Authorization` | `Bearer {token}` | Yes | Customer authentication token | ## cURL Example ```bash curl -X POST "https://api-demo.bagisto.com/api/graphql" \ -H "X-STOREFRONT-KEY: pk_storefront_your_key_here" \ -H "Authorization: Bearer YOUR_CUSTOMER_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "query": "query { customerDownloadableProduct(id: \"/api/shop/customer-downloadable-products/1\") { _id productName name fileName type downloadBought downloadUsed status downloadUrl remainingDownloads createdAt } }" }' ``` ## Error Responses ### Item Not Found ```json { "errors": [ { "message": "Customer downloadable product with ID \"999\" not found", "locations": [{ "line": 2, "column": 3 }], "path": ["customerDownloadableProduct"] } ] } ``` ## Notes - **IRI format:** The `id` argument uses the IRI format `/api/shop/customer-downloadable-products/{id}`. - **Customer isolation:** A customer can only access their own purchases. Requesting another customer's purchase returns a not found error. - **Computed field:** `remainingDownloads` is calculated as `downloadBought - downloadUsed - downloadCanceled`. Returns `null` for unlimited downloads. --- # Get Customer Downloadable Products URL: /api/graphql-api/shop/queries/get-customer-downloadable-products --- outline: false examples: - id: get-customer-downloadable-products-basic title: Get All Customer Downloadable Products description: Retrieve a paginated list of downloadable product purchases for the authenticated customer using cursor-based pagination. query: | query GetCustomerDownloadableProducts { customerDownloadableProducts(first: 10) { edges { cursor node { _id productName name fileName type downloadBought downloadUsed downloadCanceled status downloadUrl remainingDownloads order { _id incrementId status } createdAt updatedAt } } pageInfo { endCursor startCursor hasNextPage hasPreviousPage } totalCount } } variables: | {} response: | { "data": { "customerDownloadableProducts": { "edges": [ { "cursor": "MA==", "node": { "_id": 4, "productName": "Complete Personal Finance Guide (eBook PDF)", "name": "Full eBook PDF", "fileName": null, "type": "url", "downloadBought": 10, "downloadUsed": 4, "downloadCanceled": 0, "status": "available", "downloadUrl": "https://api-demo.bagisto.com/api/shop/customer-downloadable-products/4/download", "remainingDownloads": "6", "order": { "_id": 531, "incrementId": "531", "status": "completed" }, "createdAt": "2026-04-02T18:54:41+05:30", "updatedAt": "2026-04-06T17:39:56+05:30" } }, { "cursor": "MQ==", "node": { "_id": 2, "productName": "Dummy Download", "name": "link", "fileName": "Game 2.jpg", "type": "file", "downloadBought": 20, "downloadUsed": 15, "downloadCanceled": 0, "status": "available", "downloadUrl": "https://api-demo.bagisto.com/api/shop/customer-downloadable-products/2/download", "remainingDownloads": "5", "order": { "_id": 150, "incrementId": "150", "status": "completed" }, "createdAt": "2025-05-19T16:55:59+05:30", "updatedAt": "2026-04-06T17:39:48+05:30" } }, { "cursor": "Mg==", "node": { "_id": 1, "productName": "Dummy Download", "name": "link", "fileName": "Game 2.jpg", "type": "file", "downloadBought": 10, "downloadUsed": 10, "downloadCanceled": 0, "status": "expired", "downloadUrl": "https://api-demo.bagisto.com/api/shop/customer-downloadable-products/1/download", "remainingDownloads": "0", "order": { "_id": 149, "incrementId": "149", "status": "completed" }, "createdAt": "2025-05-19T13:07:35+05:30", "updatedAt": "2025-05-19T16:53:40+05:30" } } ], "pageInfo": { "endCursor": "Mg==", "startCursor": "MA==", "hasNextPage": false, "hasPreviousPage": false }, "totalCount": 3 } } } commonErrors: - error: Unauthenticated cause: Missing or invalid Bearer token solution: Login and provide a valid customer authentication token - error: Invalid storefront key cause: Storefront key is missing or invalid solution: Provide a valid X-STOREFRONT-KEY header - id: get-customer-downloadable-products-filter-status title: Filter by Status description: Filter downloadable product purchases by status (available, expired, or pending). query: | query GetAvailableDownloads { customerDownloadableProducts(first: 10, status: "available") { edges { cursor node { _id productName name status downloadUrl downloadBought downloadUsed remainingDownloads } } totalCount } } variables: | {} response: | { "data": { "customerDownloadableProducts": { "edges": [ { "cursor": "MQ==", "node": { "_id": 1, "productName": "Laravel E-Book", "name": "PDF Download", "status": "available", "downloadUrl": "https://your-domain.com/api/shop/customer-downloadable-products/1/download", "downloadBought": 5, "downloadUsed": 1, "remainingDownloads": 4 } } ], "totalCount": 1 } } } - id: get-customer-downloadable-products-pagination title: Pagination — Forward description: Use the `after` cursor to paginate forward through downloadable product results. query: | query GetNextPage { customerDownloadableProducts(first: 5, after: "MQ==") { edges { cursor node { _id productName name status } } pageInfo { endCursor hasNextPage } totalCount } } variables: | {} response: | { "data": { "customerDownloadableProducts": { "edges": [ { "cursor": "Mg==", "node": { "_id": 2, "productName": "Stock Photo Pack", "name": "High-Res Bundle", "status": "expired" } } ], "pageInfo": { "endCursor": "Mg==", "hasNextPage": false }, "totalCount": 2 } } } --- # Get Customer Downloadable Products Retrieve a paginated list of downloadable product purchases belonging to the authenticated customer. This is a **read-only** API — customers can view their purchased downloadable links, check download status, and see remaining downloads. ## Query ```graphql query GetCustomerDownloadableProducts { customerDownloadableProducts(first: 10) { edges { cursor node { _id productName name fileName type downloadBought downloadUsed downloadCanceled status downloadUrl remainingDownloads order { _id incrementId status } createdAt updatedAt } } pageInfo { endCursor startCursor hasNextPage hasPreviousPage } totalCount } } ``` ## Arguments | Argument | Type | Required | Description | |----------|------|----------|-------------| | `first` | Int | No | Number of items to return from the start (default: 10) | | `last` | Int | No | Number of items to return from the end | | `after` | String | No | Cursor to start pagination after | | `before` | String | No | Cursor to start pagination before | | `status` | String | No | Filter by status: `available`, `expired`, or `pending` | ## Response Fields | Field | Type | Description | |-------|------|-------------| | `_id` | Int | Downloadable link purchase ID | | `productName` | String | Name of the purchased product | | `name` | String | Name of the downloadable link | | `fileName` | String | Display name of the file | | `type` | String | Link type: `file` or `url` | | `downloadBought` | Int | Total number of allowed downloads | | `downloadUsed` | Int | Number of times downloaded | | `downloadCanceled` | Int | Number of canceled downloads | | `status` | String | Purchase status: `available`, `expired`, or `pending` | | `downloadUrl` | String | Direct REST API URL to download the purchased file. Use this URL with a GET request and customer authentication to download the file. See [Download Downloadable Product](/api/graphql-api/shop/queries/download-downloadable-product). | | `remainingDownloads` | Int | Computed remaining downloads (`null` if unlimited) | | `order` | Object | Associated order details | | `createdAt` | DateTime | Purchase creation date | | `updatedAt` | DateTime | Purchase last update date | ### Status Values | Status | Description | |--------|-------------| | `available` | Download link is active and can be used | | `pending` | Order has not been invoiced yet; download is not available | | `expired` | All downloads have been used or the link has expired | ## Request Headers | Header | Value | Required | Description | |--------|-------|----------|-------------| | `Content-Type` | `application/json` | Yes | Request content type | | `X-STOREFRONT-KEY` | `pk_storefront_xxx` | Yes | Storefront API key | | `Authorization` | `Bearer {token}` | Yes | Customer authentication token | ## cURL Example ```bash curl -X POST "https://api-demo.bagisto.com/api/graphql" \ -H "X-STOREFRONT-KEY: pk_storefront_your_key_here" \ -H "Authorization: Bearer YOUR_CUSTOMER_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "query": "query { customerDownloadableProducts(first: 10) { edges { cursor node { _id productName name fileName type downloadBought downloadUsed status downloadUrl remainingDownloads createdAt } } pageInfo { endCursor hasNextPage } totalCount } }" }' ``` ## Notes - **Read-only API:** Only `GET` / query operations are available. - **Customer isolation:** Purchases are automatically filtered by the authenticated customer. A customer can never see another customer's purchases. - **Status filtering:** Use the `status` parameter to filter by `available`, `expired`, or `pending`. - **Cursor pagination:** Uses cursor-based pagination. Use `first`/`after` for forward pagination and `last`/`before` for backward pagination. - **Computed field:** `remainingDownloads` is calculated as `downloadBought - downloadUsed - downloadCanceled`. Returns `null` for unlimited downloads. --- # Get Customer Invoice URL: /api/graphql-api/shop/queries/get-customer-invoice --- outline: false examples: - id: get-customer-invoice-basic title: Get Single Customer Invoice description: Retrieve details of a specific invoice by its IRI identifier. query: | query GetCustomerInvoice { customerInvoice(id: "/api/shop/customer-invoices/532") { incrementId state totalQty grandTotal baseGrandTotal subTotal baseSubTotal shippingAmount baseShippingAmount taxAmount baseTaxAmount discountAmount baseDiscountAmount shippingTaxAmount subTotalInclTax shippingAmountInclTax baseCurrencyCode channelCurrencyCode orderCurrencyCode transactionId emailSent reminders createdAt updatedAt downloadUrl items { edges { node { id sku name qty price total } } } } } variables: | {} response: | { "data": { "customerInvoice": { "incrementId": "532", "state": "paid", "totalQty": 1, "grandTotal": 4810, "baseGrandTotal": 4810, "subTotal": 5000, "baseSubTotal": 5000, "shippingAmount": 10, "baseShippingAmount": 10, "taxAmount": 0, "baseTaxAmount": 0, "discountAmount": 200, "baseDiscountAmount": 200, "shippingTaxAmount": 0, "subTotalInclTax": 5000, "shippingAmountInclTax": 10, "baseCurrencyCode": "USD", "channelCurrencyCode": "USD", "orderCurrencyCode": "USD", "transactionId": null, "emailSent": true, "reminders": 0, "createdAt": "2026-04-10T18:43:10+05:30", "updatedAt": "2026-04-10T18:43:11+05:30", "downloadUrl": "https://api-demo.bagisto.com/api/shop/customer-invoices/532/pdf", "items": { "edges": [ { "node": { "id": "/api/customer_invoice_items/803", "sku": "AURORA-BLAZER-001", "name": "Aurora Cream Winter Blazer Coat", "qty": 1, "price": 5000, "total": 5000 } } ] } } } } commonErrors: - error: UNAUTHENTICATED cause: Missing or invalid Bearer token solution: Login and provide a valid customer authentication token - error: NOT_FOUND cause: Invoice with specified ID does not exist or does not belong to the customer solution: Verify the invoice ID and ensure it belongs to the authenticated customer's orders - error: MISSING_ID cause: Invoice ID not provided solution: Provide a valid invoice IRI identifier --- # Get Customer Invoice ## About The `customerInvoice` query retrieves detailed information for a specific invoice by its IRI identifier. Customers can only access invoices from their own orders — requesting another customer's invoice returns a not found error, preventing enumeration attacks. Use this query to: - Display detailed invoice information - Show invoice summary with line items and totals - Track payment state and transaction details - View tax, shipping, and discount breakdowns - Display financial details for a specific invoice ## Authentication This query requires customer authentication: - **Authenticated customers**: Provide a valid customer authentication token in the `Authorization` header. Obtain this token via the [Customer Login API](/api/graphql-api/shop/mutations/customer-login). ``` Authorization: Bearer X-STOREFRONT-KEY: ``` ## Arguments | Argument | Type | Required | Description | |----------|------|----------|-------------| | `id` | `ID!` | ✅ Yes | The IRI identifier of the customer invoice (e.g. `/api/shop/customer-invoices/1`). | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `_id` | `Int!` | Numeric invoice ID. | | `incrementId` | `String!` | Human-readable invoice number (e.g. `INV-001`). | | `state` | `String!` | Invoice state: `pending`, `pending_payment`, `paid`, `overdue`, `refunded`. | | `totalQty` | `Int!` | Total quantity of items in the invoice. | | `emailSent` | `Boolean` | Whether the invoice email was sent. | | `grandTotal` | `Float!` | Grand total. | | `baseGrandTotal` | `Float!` | Base grand total. | | `subTotal` | `Float!` | Sub total. | | `baseSubTotal` | `Float!` | Base sub total. | | `shippingAmount` | `Float` | Shipping amount. | | `baseShippingAmount` | `Float` | Base shipping amount. | | `taxAmount` | `Float` | Tax amount. | | `baseTaxAmount` | `Float` | Base tax amount. | | `discountAmount` | `Float` | Discount amount. | | `baseDiscountAmount` | `Float` | Base discount amount. | | `shippingTaxAmount` | `Float` | Shipping tax amount. | | `baseShippingTaxAmount` | `Float` | Base shipping tax amount. | | `subTotalInclTax` | `Float` | Sub total including tax. | | `baseSubTotalInclTax` | `Float` | Base sub total including tax. | | `shippingAmountInclTax` | `Float` | Shipping amount including tax. | | `baseShippingAmountInclTax` | `Float` | Base shipping amount including tax. | | `baseCurrencyCode` | `String!` | Base currency code (e.g. `USD`). | | `channelCurrencyCode` | `String` | Channel currency code. | | `orderCurrencyCode` | `String!` | Order currency code. | | `transactionId` | `String` | Payment transaction ID. | | `reminders` | `Int` | Number of reminders sent. | | `nextReminderAt` | `DateTime` | Next reminder scheduled date. | | `createdAt` | `DateTime!` | Invoice creation timestamp. | | `updatedAt` | `DateTime!` | Invoice last update timestamp. | | `downloadUrl` | `String` | URL to download the invoice as PDF. | ## Error Handling ### Invoice Not Found ```json { "errors": [ { "message": "Customer invoice with ID \"999\" not found.", "locations": [{ "line": 2, "column": 3 }], "path": ["customerInvoice"] } ] } ``` ### Unauthenticated Request ```json { "errors": [ { "message": "Customer is not logged in.", "locations": [{ "line": 2, "column": 3 }], "path": ["customerInvoice"] } ] } ``` ## Use Cases - Display detailed invoice page in customer account - Show full financial breakdown of an invoice - Track payment state and transaction ID - View tax and shipping details - Check if invoice email was sent ## Notes - **Customer isolation:** Invoices are scoped through the order relationship. A customer can only access invoices from their own orders. - **Read-only:** Only query operations are available. Invoices cannot be modified through this API. ## Related Resources - [Get All Customer Invoices](/api/graphql-api/shop/queries/get-customer-invoices) — Query all customer invoices - [Get Customer Orders](/api/graphql-api/shop/queries/get-customer-orders) — Query customer orders - [Get Customer Profile](/api/graphql-api/shop/queries/get-customer-profile) — Query customer profile --- # Get Customer Invoices URL: /api/graphql-api/shop/queries/get-customer-invoices --- outline: false examples: - id: get-customer-invoices-by-order-with-items title: Get Customer Invoice by Order ID with Items description: Retrieve the invoice for a specific order with detailed invoice line items. Each order has exactly one invoice. query: | query customerInvoices { customerInvoices(first: 1, orderId: 590) { edges { node { orderCurrencyCode grandTotal downloadUrl items { edges { node { id _id sku parentId name price qty total basePrice description baseTotal taxAmount baseTaxAmount discountPercent discountAmount baseDiscountAmount priceInclTax basePriceInclTax totalInclTax baseTotalInclTax productId productType orderItemId invoiceId createdAt updatedAt } } } } } } } variables: | {} response: | { "data": { "customerInvoices": { "edges": [ { "node": { "orderCurrencyCode": "USD", "grandTotal": 4810, "downloadUrl": "https://api-demo.bagisto.com/api/shop/customer-invoices/532/pdf", "items": { "edges": [ { "node": { "id": "/api/customer_invoice_items/803", "_id": 803, "sku": "AURORA-BLAZER-001", "parentId": null, "name": "Aurora Cream Winter Blazer Coat", "price": 5000, "qty": 1, "total": 5000, "basePrice": 5000, "description": null, "baseTotal": 5000, "taxAmount": 0, "baseTaxAmount": 0, "discountPercent": 0, "discountAmount": 200, "baseDiscountAmount": 200, "priceInclTax": "5000.0000", "basePriceInclTax": "5000.0000", "totalInclTax": "5000.0000", "baseTotalInclTax": "5000.0000", "productId": "2493", "productType": "Webkul\\Product\\Models\\Product", "orderItemId": 871, "invoiceId": 532, "createdAt": "2026-04-10T18:43:10+05:30", "updatedAt": "2026-04-10T18:43:10+05:30" } } ] } } } ] } } } commonErrors: - error: invalid-order-id cause: Order ID does not exist or does not belong to the customer solution: Verify the order ID belongs to the authenticated customer - id: get-customer-invoices-basic title: Get All Customer Invoices description: Retrieve a paginated list of invoices for the authenticated customer using cursor-based pagination. query: | query GetCustomerInvoices { customerInvoices(first: 10, after: null) { edges { cursor node { _id incrementId state totalQty grandTotal baseGrandTotal subTotal baseSubTotal shippingAmount baseShippingAmount taxAmount baseTaxAmount discountAmount baseDiscountAmount baseCurrencyCode orderCurrencyCode downloadUrl createdAt updatedAt } } pageInfo { endCursor startCursor hasNextPage hasPreviousPage } totalCount } } variables: | {} response: | { "data": { "customerInvoices": { "edges": [ { "cursor": "Mg==", "node": { "_id": 2, "incrementId": "INV-002", "state": "pending", "totalQty": 1, "grandTotal": 55.00, "baseGrandTotal": 55.00, "subTotal": 50.00, "baseSubTotal": 50.00, "shippingAmount": 5.00, "baseShippingAmount": 5.00, "taxAmount": 0.00, "baseTaxAmount": 0.00, "discountAmount": 0.00, "baseDiscountAmount": 0.00, "baseCurrencyCode": "USD", "orderCurrencyCode": "USD", "downloadUrl": "/customer/invoices/download/2", "createdAt": "2025-02-10T14:00:00+00:00", "updatedAt": "2025-02-10T14:00:00+00:00" } }, { "cursor": "MQ==", "node": { "_id": 1, "incrementId": "INV-001", "state": "paid", "totalQty": 2, "grandTotal": 110.00, "baseGrandTotal": 110.00, "subTotal": 100.00, "baseSubTotal": 100.00, "shippingAmount": 5.00, "baseShippingAmount": 5.00, "taxAmount": 5.00, "baseTaxAmount": 5.00, "discountAmount": 0.00, "baseDiscountAmount": 0.00, "baseCurrencyCode": "USD", "orderCurrencyCode": "USD", "downloadUrl": "/customer/invoices/download/1", "createdAt": "2025-02-10T10:30:00+00:00", "updatedAt": "2025-02-10T10:30:00+00:00" } } ], "pageInfo": { "endCursor": "MQ==", "startCursor": "Mg==", "hasNextPage": false, "hasPreviousPage": false }, "totalCount": 2 } } } commonErrors: - error: UNAUTHENTICATED cause: Missing or invalid Bearer token solution: Login and provide a valid customer authentication token - error: invalid-pagination cause: Invalid pagination arguments solution: Use valid first/after or last/before combinations with max value 100 - id: get-customer-invoices-by-order title: Get Customer Invoice - Filter by Order ID description: Retrieve the invoice for a specific order. Each order has exactly one invoice, so this will always return a single result. query: | query GetInvoicesByOrder($orderId: Int) { customerInvoices(first: 10, orderId: $orderId) { edges { node { _id incrementId state grandTotal downloadUrl createdAt } } totalCount } } variables: | { "orderId": 1 } response: | { "data": { "customerInvoices": { "edges": [ { "node": { "_id": 1, "incrementId": "INV-001", "state": "paid", "grandTotal": 110.00, "downloadUrl": "/customer/invoices/download/1", "createdAt": "2025-02-10T10:30:00+00:00" } } ], "totalCount": 1 } } } commonErrors: - error: invalid-order-id cause: Order ID does not exist or does not belong to the customer solution: Verify the order ID belongs to the authenticated customer - id: get-customer-invoices-by-state title: Get Customer Invoices - Filter by State description: Retrieve invoices filtered by payment state. query: | query GetPaidInvoices { customerInvoices(first: 10, state: "paid") { edges { node { _id incrementId state grandTotal downloadUrl createdAt } } totalCount } } variables: | {} response: | { "data": { "customerInvoices": { "edges": [ { "node": { "_id": 1, "incrementId": "INV-001", "state": "paid", "grandTotal": 110.00, "downloadUrl": "/customer/invoices/download/1", "createdAt": "2025-02-10T10:30:00+00:00" } } ], "totalCount": 1 } } } commonErrors: - error: invalid-state cause: Invalid state value provided solution: Use one of pending, pending_payment, paid, overdue, refunded - id: get-customer-invoices-pagination title: Get Customer Invoices - Forward Pagination description: Paginate through customer invoices using cursor-based pagination with the after argument. query: | query GetNextPage { customerInvoices(first: 5, after: "Mg==") { edges { cursor node { _id incrementId state grandTotal downloadUrl createdAt } } pageInfo { endCursor hasNextPage } totalCount } } variables: | {} response: | { "data": { "customerInvoices": { "edges": [ { "cursor": "MQ==", "node": { "_id": 1, "incrementId": "INV-001", "state": "paid", "grandTotal": 110.00, "downloadUrl": "/customer/invoices/download/1", "createdAt": "2025-02-10T10:30:00+00:00" } } ], "pageInfo": { "endCursor": "MQ==", "hasNextPage": false }, "totalCount": 2 } } } commonErrors: - error: INVALID_CURSOR cause: The cursor value is invalid or expired solution: Use a valid cursor from a previous response's pageInfo --- # Get Customer Invoices ## About The `customerInvoices` query retrieves a paginated list of invoices belonging to the authenticated customer's orders. This is a **read-only** API — customers can only view their own invoices. Use this query to: - Display invoice history in the customer's account dashboard - Show invoice list with state, totals, and dates - Filter invoices by order ID or payment state - Implement pagination for customers with many invoices - Build invoice tracking interfaces Invoices are automatically scoped to the authenticated customer via the order relationship. ## Authentication This query requires customer authentication: - **Authenticated customers**: Provide a valid customer authentication token in the `Authorization` header. Obtain this token via the [Customer Login API](/api/graphql-api/shop/mutations/customer-login). ``` Authorization: Bearer X-STOREFRONT-KEY: ``` ## Arguments | Argument | Type | Required | Description | |----------|------|----------|-------------| | `first` | `Int` | ❌ No | Number of items to return (forward pagination). Max: 100. | | `after` | `String` | ❌ No | Cursor for forward pagination. Use `endCursor` from previous response. | | `last` | `Int` | ❌ No | Number of items for backward pagination. Max: 100. | | `before` | `String` | ❌ No | Cursor for backward pagination. Use `startCursor` from previous response. | | `orderId` | `Int` | ❌ No | Filter invoices by order ID. | | `state` | `String` | ❌ No | Filter by invoice state: `pending`, `pending_payment`, `paid`, `overdue`, `refunded`. | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `edges` | `[CustomerInvoiceEdge!]` | Array of invoice edges with cursor and node. | | `edges.cursor` | `String!` | Cursor for this edge, used in pagination. | | `edges.node` | `CustomerInvoice!` | The customer invoice object. | | `edges.node._id` | `Int!` | Numeric invoice ID. | | `edges.node.incrementId` | `String!` | Human-readable invoice number (e.g. `INV-001`). | | `edges.node.state` | `String!` | Invoice state: `pending`, `pending_payment`, `paid`, `overdue`, `refunded`. | | `edges.node.totalQty` | `Int!` | Total quantity of items in the invoice. | | `edges.node.emailSent` | `Boolean` | Whether the invoice email was sent. | | `edges.node.grandTotal` | `Float!` | Grand total. | | `edges.node.baseGrandTotal` | `Float!` | Base grand total. | | `edges.node.subTotal` | `Float!` | Sub total. | | `edges.node.baseSubTotal` | `Float!` | Base sub total. | | `edges.node.shippingAmount` | `Float` | Shipping amount. | | `edges.node.baseShippingAmount` | `Float` | Base shipping amount. | | `edges.node.taxAmount` | `Float` | Tax amount. | | `edges.node.baseTaxAmount` | `Float` | Base tax amount. | | `edges.node.discountAmount` | `Float` | Discount amount. | | `edges.node.baseDiscountAmount` | `Float` | Base discount amount. | | `edges.node.shippingTaxAmount` | `Float` | Shipping tax amount. | | `edges.node.baseShippingTaxAmount` | `Float` | Base shipping tax amount. | | `edges.node.subTotalInclTax` | `Float` | Sub total including tax. | | `edges.node.baseSubTotalInclTax` | `Float` | Base sub total including tax. | | `edges.node.shippingAmountInclTax` | `Float` | Shipping amount including tax. | | `edges.node.baseShippingAmountInclTax` | `Float` | Base shipping amount including tax. | | `edges.node.baseCurrencyCode` | `String!` | Base currency code (e.g. `USD`). | | `edges.node.channelCurrencyCode` | `String` | Channel currency code. | | `edges.node.orderCurrencyCode` | `String!` | Order currency code. | | `edges.node.transactionId` | `String` | Payment transaction ID. | | `edges.node.reminders` | `Int` | Number of reminders sent. | | `edges.node.nextReminderAt` | `DateTime` | Next reminder scheduled date. | | `edges.node.createdAt` | `DateTime!` | Invoice creation timestamp. | | `edges.node.updatedAt` | `DateTime!` | Invoice last update timestamp. | | `pageInfo` | `PageInfo!` | Pagination metadata. | | `pageInfo.hasNextPage` | `Boolean!` | Whether more pages exist forward. | | `pageInfo.hasPreviousPage` | `Boolean!` | Whether more pages exist backward. | | `pageInfo.startCursor` | `String` | Cursor for first item in page. | | `pageInfo.endCursor` | `String` | Cursor for last item in page. | | `totalCount` | `Int!` | Total invoices matching filters. | ## Invoice State Values | State | Description | |-------|-------------| | `pending` | Invoice created, awaiting action | | `pending_payment` | Payment initiated but not yet confirmed | | `paid` | Payment received and confirmed | | `overdue` | Payment past due date | | `refunded` | Invoice has been refunded | ## Use Cases ### 1. Invoice History Dashboard Fetch all customer invoices to display in the account dashboard with pagination. ### 2. Order-Specific Invoice Filter by `orderId` to retrieve the invoice for a specific order. Each order has exactly one invoice, so this will always return a single result. ### 3. Paid Invoices Filter by `state: "paid"` to show confirmed payment history. ### 4. Pending Payments Filter by `state: "pending"` or `state: "pending_payment"` to show outstanding invoices. ## Best Practices 1. **Use Pagination** — Always implement pagination for better performance 2. **Show State** — Display the invoice state prominently so customers can track payment status 3. **Filter by Order** — Use `orderId` filter when showing invoices within an order detail page 4. **Cache Results** — Cache invoice lists for better performance 5. **Handle Empty States** — Provide helpful UI when the customer has no invoices ## Error Handling ### Unauthenticated Request ```json { "errors": [ { "message": "Customer is not logged in.", "locations": [{ "line": 2, "column": 3 }], "path": ["customerInvoices"] } ] } ``` ## Related Resources - [Get Single Customer Invoice](/api/graphql-api/shop/queries/get-customer-invoice) — Query individual invoice details - [Get Customer Orders](/api/graphql-api/shop/queries/get-customer-orders) — Query customer orders - [Get Customer Profile](/api/graphql-api/shop/queries/get-customer-profile) — Query customer profile - [Pagination Guide](/api/graphql-api/pagination) — Cursor pagination documentation --- # Get Customer Order URL: /api/graphql-api/shop/queries/get-customer-order --- outline: false examples: - id: get-customer-order-basic title: Get Single Customer Order description: Retrieve details of a specific customer order by its IRI identifier. query: | query GetCustomerOrder { customerOrder(id: "/api/shop/customer-orders/1") { incrementId status channelName customerEmail customerFirstName customerLastName shippingMethod shippingTitle couponCode totalItemCount totalQtyOrdered grandTotal baseGrandTotal grandTotalInvoiced grandTotalRefunded subTotal baseSubTotal taxAmount baseTaxAmount discountAmount baseDiscountAmount shippingAmount baseShippingAmount baseCurrencyCode channelCurrencyCode orderCurrencyCode items { edges { node { id sku name additional qtyOrdered qtyShipped qtyInvoiced qtyCanceled qtyRefunded } } } addresses { edges { node { id _id addressType parentAddressId customerId cartId orderId name firstName lastName companyName address city state country postcode useForShipping email phone gender vatId defaultAddress createdAt updatedAt } } } createdAt updatedAt } } variables: | {} response: | { "data": { "customerOrder": { "incrementId": "1", "status": "pending", "channelName": "Default", "customerEmail": "customer@example.com", "customerFirstName": "John", "customerLastName": "Doe", "shippingMethod": "flatrate_flatrate", "shippingTitle": "Flat Rate - Flat Rate", "couponCode": null, "totalItemCount": 1, "totalQtyOrdered": 2, "grandTotal": 150.00, "baseGrandTotal": 150.00, "grandTotalInvoiced": 150.00, "grandTotalRefunded": 0.00, "subTotal": 140.00, "baseSubTotal": 140.00, "taxAmount": 0.00, "baseTaxAmount": 0.00, "discountAmount": 0.00, "baseDiscountAmount": 0.00, "shippingAmount": 10.00, "baseShippingAmount": 10.00, "baseCurrencyCode": "USD", "channelCurrencyCode": "USD", "orderCurrencyCode": "USD", "items": { "edges": [ { "node": { "id": "/api/shop/order-items/1", "sku": "ACME-DRAWBAG-001", "name": "Acme Drawstring Bag", "additional": "{\"locale\": \"en\", \"quantity\": 2, \"attributes\": [{\"option_id\": \"2\", \"option_label\": \"Blue\", \"attribute_name\": \"Color\"}, {\"option_id\": \"7\", \"option_label\": \"M\", \"attribute_name\": \"Size\"}], \"is_buy_now\": \"0\", \"product_id\": \"22\"}", "qtyOrdered": 2, "qtyShipped": 0, "qtyInvoiced": 2, "qtyCanceled": 0, "qtyRefunded": 0 } } ] }, "addresses": { "edges": [ { "node": { "id": "/api/shop/customer-addresses/1", "_id": "1", "addressType": "billing", "parentAddressId": null, "customerId": "1", "cartId": null, "orderId": "5", "name": "John Doe", "firstName": "John", "lastName": "Doe", "companyName": "Acme Corp", "address": "123 Main St", "city": "New York", "state": "NY", "country": "US", "postcode": "10001", "useForShipping": true, "email": "john@example.com", "phone": "+1234567890", "gender": "male", "vatId": null, "defaultAddress": true, "createdAt": "2025-01-10T08:15:00+00:00", "updatedAt": "2025-01-15T10:30:00+00:00" } } ] }, "createdAt": "2025-01-15T10:30:00+00:00", "updatedAt": "2025-01-15T10:30:00+00:00" } } } commonErrors: - error: UNAUTHENTICATED cause: Missing or invalid Bearer token solution: Login and provide a valid customer authentication token - error: NOT_FOUND cause: Order with specified ID does not exist or does not belong to the customer solution: Verify the order ID and ensure it belongs to the authenticated customer - error: MISSING_ID cause: Order ID not provided solution: Provide a valid order IRI identifier - id: get-customer-order-with-shipments title: Get Customer Order with Shipments description: Retrieve customer order details including shipment information and tracking details. query: | query getCustomerOrder { customerOrder(id: "/api/shop/customer-orders/3") { _id incrementId status shipments { edges { node { _id status totalQty totalWeight carrierCode carrierTitle trackNumber emailSent shippingNumber createdAt items { edges { node { _id sku name qty weight } } } } } pageInfo { endCursor startCursor hasNextPage hasPreviousPage } totalCount } } } variables: | {} response: | { "data": { "customerOrder": { "_id": 3, "incrementId": "3", "status": "shipped", "shipments": { "edges": [ { "node": { "_id": 1, "status": "shipped", "totalQty": 2, "totalWeight": 5.5, "carrierCode": "fedex", "carrierTitle": "FedEx", "trackNumber": "794698949845", "emailSent": true, "shippingNumber": "SH-001", "createdAt": "2025-01-20T14:30:00+00:00", "items": { "edges": [ { "node": { "_id": 1, "sku": "ACME-DRAWBAG-001", "name": "Acme Drawstring Bag", "qty": 2, "weight": 5.5 } } ] } } } ], "pageInfo": { "endCursor": "MQ==", "startCursor": "MQ==", "hasNextPage": false, "hasPreviousPage": false }, "totalCount": 1 } } } } commonErrors: - error: UNAUTHENTICATED cause: Missing or invalid Bearer token solution: Login and provide a valid customer authentication token - error: NOT_FOUND cause: Order with specified ID does not exist or does not belong to the customer solution: Verify the order ID and ensure it belongs to the authenticated customer --- # Get Customer Order ## About The `customerOrder` query retrieves detailed information for a specific order by its IRI identifier. Customers can only access their own orders — requesting another customer's order returns a not found error, preventing enumeration attacks. Use this query to: - Display detailed order information - Show order summary with line items and totals - Track order status and shipping details - View applied coupons and discounts - Display invoiced and refunded amounts ## Authentication This query requires customer authentication: - **Authenticated customers**: Provide a valid customer authentication token in the `Authorization` header. Obtain this token via the [Customer Login API](/api/graphql-api/shop/mutations/customer-login). ``` Authorization: Bearer X-STOREFRONT-KEY: ``` ## Arguments | Argument | Type | Required | Description | |----------|------|----------|-------------| | `id` | `ID!` | ✅ Yes | The IRI identifier of the customer order (e.g. `/api/shop/customer-orders/1`). | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `incrementId` | `String!` | Human-readable order number. | | `status` | `String!` | Order status: `pending`, `processing`, `completed`, `canceled`, `closed`, `fraud`. | | `channelName` | `String!` | Channel the order was placed on. | | `isGuest` | `Int` | Whether the order was placed as guest. | | `customerEmail` | `String!` | Customer email address. | | `customerFirstName` | `String!` | Customer first name. | | `customerLastName` | `String!` | Customer last name. | | `shippingMethod` | `String` | Shipping method code. | | `shippingTitle` | `String` | Shipping method display name. | | `couponCode` | `String` | Applied coupon code. | | `isGift` | `Int` | Whether the order is a gift. | | `totalItemCount` | `Int!` | Number of distinct items. | | `totalQtyOrdered` | `Int!` | Total quantity ordered. | | `grandTotal` | `Float!` | Grand total. | | `baseGrandTotal` | `Float!` | Base grand total. | | `grandTotalInvoiced` | `Float` | Grand total invoiced. | | `baseGrandTotalInvoiced` | `Float` | Base grand total invoiced. | | `grandTotalRefunded` | `Float` | Grand total refunded. | | `baseGrandTotalRefunded` | `Float` | Base grand total refunded. | | `subTotal` | `Float!` | Sub total. | | `baseSubTotal` | `Float!` | Base sub total. | | `taxAmount` | `Float` | Tax amount. | | `baseTaxAmount` | `Float` | Base tax amount. | | `discountAmount` | `Float` | Discount amount. | | `baseDiscountAmount` | `Float` | Base discount amount. | | `shippingAmount` | `Float` | Shipping amount. | | `baseShippingAmount` | `Float` | Base shipping amount. | | `baseCurrencyCode` | `String!` | Base currency code. | | `channelCurrencyCode` | `String` | Channel currency code. | | `orderCurrencyCode` | `String!` | Order currency code. | | `items` | `OrderItemConnection` | Paginated list of order line items. | | `items.edges.node.id` | `ID!` | IRI identifier of the order item. | | `items.edges.node.sku` | `String!` | Product SKU. | | `items.edges.node.name` | `String!` | Product name at time of order. | | `items.edges.node.additional` | `String (JSON)` | JSON-encoded string containing additional details captured at order time — selected attributes (e.g. color, size), booking slots, event tickets, configurable options, etc. The structure varies by product type. | | `items.edges.node.qtyOrdered` | `Int!` | Quantity ordered. | | `items.edges.node.qtyShipped` | `Int!` | Quantity shipped. | | `items.edges.node.qtyInvoiced` | `Int!` | Quantity invoiced. | | `items.edges.node.qtyCanceled` | `Int!` | Quantity canceled. | | `items.edges.node.qtyRefunded` | `Int!` | Quantity refunded. | | `addresses` | `OrderAddressConnection` | Paginated list of order addresses. | | `addresses.edges.node.id` | `ID!` | IRI identifier of the address. | | `addresses.edges.node._id` | `ID!` | Internal address identifier. | | `addresses.edges.node.addressType` | `String!` | Address type: `billing` or `shipping`. | | `addresses.edges.node.parentAddressId` | `ID` | Parent address ID if applicable. | | `addresses.edges.node.customerId` | `ID` | Associated customer ID. | | `addresses.edges.node.cartId` | `ID` | Associated cart ID if applicable. | | `addresses.edges.node.orderId` | `ID` | Associated order ID. | | `addresses.edges.node.name` | `String` | Full name for the address. | | `addresses.edges.node.firstName` | `String!` | First name. | | `addresses.edges.node.lastName` | `String!` | Last name. | | `addresses.edges.node.companyName` | `String` | Company name. | | `addresses.edges.node.address` | `String!` | Street address. | | `addresses.edges.node.city` | `String!` | City name. | | `addresses.edges.node.state` | `String` | State/Province code. | | `addresses.edges.node.country` | `String!` | Country code. | | `addresses.edges.node.postcode` | `String` | Postal code. | | `addresses.edges.node.useForShipping` | `Boolean` | Whether address is used for shipping. | | `addresses.edges.node.email` | `String` | Email address. | | `addresses.edges.node.phone` | `String` | Phone number. | | `addresses.edges.node.gender` | `String` | Gender. | | `addresses.edges.node.vatId` | `String` | VAT ID for the address. | | `addresses.edges.node.defaultAddress` | `Boolean` | Whether this is the default address. | | `createdAt` | `DateTime!` | Order creation timestamp. | | `updatedAt` | `DateTime!` | Order last update timestamp. | ## Order Item `additional` Field The `additional` field on each order item is a JSON-encoded string that contains all the product-specific details captured at the moment of purchase. This is essential for displaying exactly what the customer selected on order confirmation, invoice, and order history pages. The structure of this field varies depending on the product type. | Product Type | `additional` Contains | |---|---| | **Configurable** | Selected attributes (e.g. Color, Size) with `option_id`, `option_label`, and `attribute_name` | | **Booking - Appointment / Default** | Booking date, slot range (e.g. "28 April, 2026 11:00 AM"), and time slot timestamps | | **Booking - Rental** | Rental period (date_from, date_to) or hourly slot details | | **Booking - Event** | Selected event ticket types and quantities | | **Booking - Table** | Reservation date, slot, and booking note | | **Downloadable** | Selected downloadable link IDs and titles | | **Bundle** | Selected bundle option products and quantities | ### Example — Configurable Product ```json { "locale": "en", "quantity": 2, "attributes": [ { "option_id": "2", "option_label": "Blue", "attribute_name": "Color" }, { "option_id": "7", "option_label": "M", "attribute_name": "Size" } ], "is_buy_now": "0", "product_id": "22" } ``` ### Example — Booking Appointment ```json { "locale": "en", "booking": { "date": "2026-04-28", "slot": "1777354200-1777356900" }, "cart_id": 5144, "quantity": 1, "attributes": [ { "option_id": 0, "option_label": "28 April, 2026 11:00 AM", "attribute_name": "Booking From" }, { "option_id": 0, "option_label": "28 April, 2026 11:45 AM", "attribute_name": "Booking Till" } ], "is_buy_now": "0", "product_id": "2509" } ``` ### Example — Booking Event ```json { "locale": "en", "booking": { "qty": { "7": "1", "8": "1" }, "slot": "1775457000-1777530600", "ticket_id": 7 }, "cart_id": 5141, "quantity": 1, "attributes": [ { "option_id": 0, "option_label": "Standard Entry Ticket", "attribute_name": "Event Ticket" }, { "option_id": 0, "option_label": "06 April, 2026", "attribute_name": "Event From" }, { "option_id": 0, "option_label": "30 April, 2026", "attribute_name": "Event Till" } ], "is_buy_now": "0", "product_id": "2508" } ``` > Always parse this field as JSON in your application. Use the `attributes` array to render selected options on order detail pages, and the `booking` object for booking-specific products. ## Error Handling ### Order Not Found ```json { "errors": [ { "message": "Customer order with ID \"999\" not found.", "locations": [{ "line": 2, "column": 3 }], "path": ["customerOrder"] } ] } ``` ### Unauthenticated Request ```json { "errors": [ { "message": "Customer is not logged in.", "locations": [{ "line": 2, "column": 3 }], "path": ["customerOrder"] } ] } ``` ## Use Cases - Display detailed order page in customer account - Show order summary with all financial details - Track shipping method and status - View applied coupons and discounts - Display invoiced and refunded amounts for order history ## Notes - **Customer isolation:** A customer can never see another customer's orders. Requesting another customer's order returns a 404, preventing enumeration attacks. - **Read-only:** Only `GET` operations are available. Orders cannot be modified through this API. - **Channel scoping:** Orders are filtered by the customer's channel for multi-tenant isolation. ## Related Resources - [Get All Customer Orders](/api/graphql-api/shop/queries/get-customer-orders) — Query all customer orders - [Place Order](/api/graphql-api/shop/mutations/place-order) — Place a new order - [Get Customer Profile](/api/graphql-api/shop/queries/get-customer-profile) — Query customer profile --- # Get Customer Order Shipment URL: /api/graphql-api/shop/queries/get-customer-order-shipment --- outline: false examples: - id: get-customer-order-shipment-basic title: Get Single Customer Order Shipment description: Retrieve details of a specific shipment by its ID with tracking information and line items. query: | query getOrderShipment { customerOrderShipment(id: 5) { id _id status trackNumber carrierTitle totalQty createdAt items { edges { node { id name sku qty } } } shippingNumber } } variables: | {} response: | { "data": { "customerOrderShipment": { "id": "/api/shop/shipments/5", "_id": 5, "status": "shipped", "trackNumber": "794698949845", "carrierTitle": "FedEx", "totalQty": 2, "createdAt": "2025-01-20T14:30:00+00:00", "items": { "edges": [ { "node": { "id": "/api/shop/shipment-items/1", "name": "Acme Drawstring Bag", "sku": "ACME-DRAWBAG-001", "qty": 2 } } ] }, "shippingNumber": "SH-005" } } } commonErrors: - error: UNAUTHENTICATED cause: Missing or invalid Bearer token solution: Login and provide a valid customer authentication token - error: NOT_FOUND cause: Shipment with specified ID does not exist or does not belong to the customer's order solution: Verify the shipment ID and ensure it belongs to one of the authenticated customer's orders - error: INVALID_ID cause: Invalid or missing shipment ID solution: Provide a valid shipment ID as an integer --- # Get Customer Order Shipment ## About The `customerOrderShipment` query retrieves detailed information for a specific shipment identified by its ID. Customers can only access shipments from their own orders — requesting another customer's shipment returns a not found error, preventing enumeration attacks. Use this query to: - Display shipment details on a tracking page - Show tracking number and carrier information - View shipped items and quantities - Track individual shipment status - Display shipment creation and tracking details - Build shipment tracking interfaces ## Authentication This query requires customer authentication: - **Authenticated customers**: Provide a valid customer authentication token in the `Authorization` header. Obtain this token via the [Customer Login API](/api/graphql-api/shop/mutations/customer-login). ``` Authorization: Bearer X-STOREFRONT-KEY: ``` ## Arguments | Argument | Type | Required | Description | |----------|------|----------|-------------| | `id` | `Int!` | ✅ Yes | The numeric ID of the shipment to retrieve. | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `id` | `ID!` | IRI identifier of the shipment. | | `_id` | `ID!` | Numeric shipment ID. | | `status` | `String!` | Shipment status: `pending`, `shipped`, `delivered`, `cancelled`. | | `trackNumber` | `String` | Carrier tracking number for shipment tracking. | | `carrierTitle` | `String` | Carrier name (e.g. `FedEx`, `UPS`, `Standard Post`). | | `totalQty` | `Int!` | Total quantity of items in the shipment. | | `createdAt` | `DateTime!` | Shipment creation timestamp. | | `items` | `ShipmentItemConnection` | Paginated list of shipment line items. | | `items.edges.node.id` | `ID!` | IRI identifier of the shipment item. | | `items.edges.node.name` | `String!` | Product name in the shipment. | | `items.edges.node.sku` | `String!` | Product SKU. | | `items.edges.node.qty` | `Int!` | Quantity shipped. | | `shippingNumber` | `String` | Unique shipping number for the shipment. | ## Error Handling ### Shipment Not Found ```json { "errors": [ { "message": "Customer shipment with ID \"999\" not found.", "locations": [{ "line": 2, "column": 3 }], "path": ["customerOrderShipment"] } ] } ``` ### Unauthenticated Request ```json { "errors": [ { "message": "Customer is not logged in.", "locations": [{ "line": 2, "column": 3 }], "path": ["customerOrderShipment"] } ] } ``` ### Invalid Shipment ID ```json { "errors": [ { "message": "Invalid shipment ID provided.", "locations": [{ "line": 2, "column": 3 }], "path": ["customerOrderShipment"] } ] } ``` ## Use Cases - Display single shipment tracking details on a dedicated page - Show tracking number for carrier tracking websites - View shipped items and quantities for a specific shipment - Track individual shipment status and delivery progress - Build detailed shipment information pages - Integrate with carrier tracking APIs using tracking numbers ## Notes - **Customer isolation:** A customer can only view shipments from their own orders. Requesting another customer's shipment returns a 404, preventing enumeration attacks. - **Read-only:** Only `GET` operations are available. Shipments cannot be modified through this API. - **Tracking information:** The `trackNumber` field can be used to track the shipment on the carrier's website. - **Item-level details:** Each shipment item includes SKU and quantity for order fulfillment verification. - **Status tracking:** Monitor shipment status changes from `pending` to `shipped` to `delivered`. ## Related Resources - [Get Customer Order Shipments](/api/graphql-api/shop/queries/get-customer-order-shipments) — Query all shipments for a specific order - [Get Customer Order](/api/graphql-api/shop/queries/get-customer-order) — Query a specific order with shipment summary - [Get All Customer Orders](/api/graphql-api/shop/queries/get-customer-orders) — Query all customer orders - [Get Customer Profile](/api/graphql-api/shop/queries/get-customer-profile) — Query customer profile --- # Get Customer Order Shipments URL: /api/graphql-api/shop/queries/get-customer-order-shipments --- outline: false examples: - id: get-customer-order-shipments-basic title: Get Customer Order Shipments by Order ID description: Retrieve all shipments for a specific customer order with tracking details and line items. query: | query getOrderShipments { customerOrderShipments(orderId: 3) { edges { node { id _id status trackNumber carrierTitle totalQty createdAt items { edges { node { id name sku qty } } } shippingNumber } } totalCount } } variables: | {} response: | { "data": { "customerOrderShipments": { "edges": [ { "node": { "id": "/api/shop/shipments/1", "_id": 1, "status": "shipped", "trackNumber": "794698949845", "carrierTitle": "FedEx", "totalQty": 2, "createdAt": "2025-01-20T14:30:00+00:00", "items": { "edges": [ { "node": { "id": "/api/shop/shipment-items/1", "name": "Acme Drawstring Bag", "sku": "ACME-DRAWBAG-001", "qty": 2 } } ] }, "shippingNumber": "SH-001" } } ], "totalCount": 1 } } } commonErrors: - error: UNAUTHENTICATED cause: Missing or invalid Bearer token solution: Login and provide a valid customer authentication token - error: NOT_FOUND cause: Order with specified ID does not exist or does not belong to the customer solution: Verify the order ID and ensure it belongs to the authenticated customer - error: INVALID_ORDER_ID cause: Invalid or missing order ID solution: Provide a valid order ID as an integer --- # Get Customer Order Shipments ## About The `customerOrderShipments` query retrieves all shipments for a specific customer order identified by its order ID. Customers can only access shipments from their own orders — requesting another customer's shipments returns a not found error, preventing enumeration attacks. Use this query to: - Display shipment tracking information - Show shipped items and quantities - Track shipping status and carrier details - View tracking numbers and shipping numbers - Display shipment creation timestamps - Show item-level shipment details ## Authentication This query requires customer authentication: - **Authenticated customers**: Provide a valid customer authentication token in the `Authorization` header. Obtain this token via the [Customer Login API](/api/graphql-api/shop/mutations/customer-login). ``` Authorization: Bearer X-STOREFRONT-KEY: ``` ## Arguments | Argument | Type | Required | Description | |----------|------|----------|-------------| | `orderId` | `Int!` | ✅ Yes | The numeric order ID for which to retrieve shipments. | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `id` | `ID!` | IRI identifier of the shipment. | | `_id` | `ID!` | Numeric shipment ID. | | `status` | `String!` | Shipment status: `pending`, `shipped`, `delivered`, `cancelled`. | | `trackNumber` | `String` | Carrier tracking number. | | `carrierTitle` | `String` | Carrier name (e.g. `FedEx`, `UPS`, `Standard Post`). | | `totalQty` | `Int!` | Total quantity of items in the shipment. | | `createdAt` | `DateTime!` | Shipment creation timestamp. | | `items` | `ShipmentItemConnection` | Paginated list of shipment line items. | | `items.edges.node.id` | `ID!` | IRI identifier of the shipment item. | | `items.edges.node.name` | `String!` | Product name in the shipment. | | `items.edges.node.sku` | `String!` | Product SKU. | | `items.edges.node.qty` | `Int!` | Quantity shipped. | | `shippingNumber` | `String` | Unique shipping number for the shipment. | | `totalCount` | `Int!` | Total number of shipments for the order. | ## Error Handling ### Order Not Found ```json { "errors": [ { "message": "Customer order with ID \"999\" not found.", "locations": [{ "line": 2, "column": 3 }], "path": ["customerOrderShipments"] } ] } ``` ### Unauthenticated Request ```json { "errors": [ { "message": "Customer is not logged in.", "locations": [{ "line": 2, "column": 3 }], "path": ["customerOrderShipments"] } ] } ``` ### Invalid Order ID ```json { "errors": [ { "message": "Invalid order ID provided.", "locations": [{ "line": 2, "column": 3 }], "path": ["customerOrderShipments"] } ] } ``` ## Use Cases - Display shipment tracking page in customer account - Show tracking numbers and carrier information - Display shipped items and quantities - Track shipment status and delivery progress - Build shipment history timeline - Show carrier-specific tracking links ## Notes - **Customer isolation:** A customer can only view shipments from their own orders. Requesting shipments for another customer's order returns a 404, preventing enumeration attacks. - **Read-only:** Only `GET` operations are available. Shipments cannot be modified through this API. - **Tracking information:** Tracking numbers are only available for shipments with carrier information. - **Item-level details:** Each shipment item includes SKU and quantity information for order fulfillment tracking. ## Related Resources - [Get Customer Order](/api/graphql-api/shop/queries/get-customer-order) — Query a specific order with shipment summary - [Get All Customer Orders](/api/graphql-api/shop/queries/get-customer-orders) — Query all customer orders - [Get Customer Profile](/api/graphql-api/shop/queries/get-customer-profile) — Query customer profile --- # Get Customer Orders URL: /api/graphql-api/shop/queries/get-customer-orders --- outline: false examples: - id: get-customer-orders-basic title: Get All Customer Orders description: Retrieve a paginated list of orders for the authenticated customer using cursor-based pagination. query: | query GetCustomerOrders { customerOrders(first: 10, after: null) { edges { cursor node { _id incrementId status channelName customerEmail customerFirstName customerLastName shippingMethod shippingTitle couponCode totalItemCount totalQtyOrdered grandTotal baseGrandTotal subTotal baseSubTotal taxAmount shippingAmount discountAmount baseCurrencyCode orderCurrencyCode createdAt updatedAt } } pageInfo { endCursor startCursor hasNextPage hasPreviousPage } totalCount } } variables: | {} response: | { "data": { "customerOrders": { "edges": [ { "cursor": "MQ==", "node": { "_id": 1, "incrementId": "1", "status": "pending", "channelName": "Default", "customerEmail": "customer@example.com", "customerFirstName": "John", "customerLastName": "Doe", "shippingMethod": "flatrate_flatrate", "shippingTitle": "Flat Rate - Flat Rate", "couponCode": null, "totalItemCount": 1, "totalQtyOrdered": 2, "grandTotal": 150.00, "baseGrandTotal": 150.00, "subTotal": 140.00, "baseSubTotal": 140.00, "taxAmount": 0.00, "shippingAmount": 10.00, "discountAmount": 0.00, "baseCurrencyCode": "USD", "orderCurrencyCode": "USD", "createdAt": "2025-01-15T10:30:00+00:00", "updatedAt": "2025-01-15T10:30:00+00:00" } } ], "pageInfo": { "endCursor": "MQ==", "startCursor": "MQ==", "hasNextPage": false, "hasPreviousPage": false }, "totalCount": 1 } } } commonErrors: - error: UNAUTHENTICATED cause: Missing or invalid Bearer token solution: Login and provide a valid customer authentication token - error: invalid-pagination cause: Invalid pagination arguments solution: Use valid first/after or last/before combinations with max value 100 - id: get-customer-orders-status title: Get Customer Orders - Filter by Status description: Retrieve customer orders filtered by order status. query: | query GetPendingOrders { customerOrders(first: 10, status: "pending") { edges { cursor node { _id incrementId status grandTotal createdAt } } totalCount } } variables: | {} response: | { "data": { "customerOrders": { "edges": [ { "cursor": "MQ==", "node": { "_id": 1, "incrementId": "1", "status": "pending", "grandTotal": 150.00, "createdAt": "2025-01-15T10:30:00+00:00" } } ], "totalCount": 1 } } } commonErrors: - error: invalid-status cause: Invalid status value provided solution: Use one of pending, processing, completed, canceled, closed, fraud - id: get-customer-orders-pagination title: Get Customer Orders - Forward Pagination description: Paginate through customer orders using cursor-based pagination with the after argument. query: | query GetNextPage { customerOrders(first: 5, after: "MQ==") { edges { cursor node { _id incrementId status grandTotal createdAt } } pageInfo { endCursor hasNextPage } totalCount } } variables: | {} response: | { "data": { "customerOrders": { "edges": [ { "cursor": "Mg==", "node": { "_id": 2, "incrementId": "2", "status": "completed", "grandTotal": 250.00, "createdAt": "2025-01-16T14:00:00+00:00" } } ], "pageInfo": { "endCursor": "Mg==", "hasNextPage": false }, "totalCount": 2 } } } commonErrors: - error: INVALID_CURSOR cause: The cursor value is invalid or expired solution: Use a valid cursor from a previous response's pageInfo --- # Get Customer Orders ## About The `customerOrders` query retrieves a paginated list of orders belonging to the authenticated customer. This is a **read-only** API — customers can only view their own orders. Use this query to: - Display order history in the customer's account dashboard - Show order list with status, totals, and dates - Filter orders by status (pending, processing, completed, etc.) - Implement pagination for customers with many orders - Build order tracking interfaces Orders are automatically scoped to the authenticated customer and the current channel. ## Authentication This query requires customer authentication: - **Authenticated customers**: Provide a valid customer authentication token in the `Authorization` header. Obtain this token via the [Customer Login API](/api/graphql-api/shop/mutations/customer-login). ``` Authorization: Bearer X-STOREFRONT-KEY: ``` ## Arguments | Argument | Type | Required | Description | |----------|------|----------|-------------| | `first` | `Int` | ❌ No | Number of items to return (forward pagination). Max: 100. | | `after` | `String` | ❌ No | Cursor for forward pagination. Use `endCursor` from previous response. | | `last` | `Int` | ❌ No | Number of items for backward pagination. Max: 100. | | `before` | `String` | ❌ No | Cursor for backward pagination. Use `startCursor` from previous response. | | `status` | `String` | ❌ No | Filter by order status: `pending`, `processing`, `completed`, `canceled`, `closed`, `fraud`. | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `edges` | `[CustomerOrderEdge!]` | Array of order edges with cursor and node. | | `edges.cursor` | `String!` | Cursor for this edge, used in pagination. | | `edges.node` | `CustomerOrder!` | The customer order object. | | `edges.node._id` | `Int!` | Numeric order ID. | | `edges.node.incrementId` | `String!` | Human-readable order number. | | `edges.node.status` | `String!` | Order status: `pending`, `processing`, `completed`, `canceled`, `closed`, `fraud`. | | `edges.node.channelName` | `String!` | Channel the order was placed on. | | `edges.node.isGuest` | `Int` | Whether the order was placed as guest. | | `edges.node.customerEmail` | `String!` | Customer email address. | | `edges.node.customerFirstName` | `String!` | Customer first name. | | `edges.node.customerLastName` | `String!` | Customer last name. | | `edges.node.shippingMethod` | `String` | Shipping method code. | | `edges.node.shippingTitle` | `String` | Shipping method display name. | | `edges.node.couponCode` | `String` | Applied coupon code. | | `edges.node.isGift` | `Int` | Whether the order is a gift. | | `edges.node.totalItemCount` | `Int!` | Number of distinct items. | | `edges.node.totalQtyOrdered` | `Int!` | Total quantity ordered. | | `edges.node.grandTotal` | `Float!` | Grand total. | | `edges.node.baseGrandTotal` | `Float!` | Base grand total. | | `edges.node.grandTotalInvoiced` | `Float` | Grand total invoiced. | | `edges.node.grandTotalRefunded` | `Float` | Grand total refunded. | | `edges.node.subTotal` | `Float!` | Sub total. | | `edges.node.baseSubTotal` | `Float!` | Base sub total. | | `edges.node.taxAmount` | `Float` | Tax amount. | | `edges.node.baseTaxAmount` | `Float` | Base tax amount. | | `edges.node.discountAmount` | `Float` | Discount amount. | | `edges.node.baseDiscountAmount` | `Float` | Base discount amount. | | `edges.node.shippingAmount` | `Float` | Shipping amount. | | `edges.node.baseShippingAmount` | `Float` | Base shipping amount. | | `edges.node.baseCurrencyCode` | `String!` | Base currency code. | | `edges.node.channelCurrencyCode` | `String` | Channel currency code. | | `edges.node.orderCurrencyCode` | `String!` | Order currency code. | | `edges.node.createdAt` | `DateTime!` | Order creation timestamp. | | `edges.node.updatedAt` | `DateTime!` | Order last update timestamp. | | `pageInfo` | `PageInfo!` | Pagination metadata. | | `pageInfo.hasNextPage` | `Boolean!` | Whether more pages exist forward. | | `pageInfo.hasPreviousPage` | `Boolean!` | Whether more pages exist backward. | | `pageInfo.startCursor` | `String` | Cursor for first item in page. | | `pageInfo.endCursor` | `String` | Cursor for last item in page. | | `totalCount` | `Int!` | Total orders matching filters. | ## Order Status Values | Status | Description | |--------|-------------| | `pending` | Awaiting payment confirmation | | `processing` | Payment confirmed, order being processed | | `completed` | Order fulfilled and delivered | | `canceled` | Order canceled | | `closed` | Order closed | | `fraud` | Flagged as fraudulent | ## Use Cases ### 1. Order History Dashboard Fetch all customer orders to display in the account dashboard with pagination. ### 2. Pending Orders Filter by `status: "pending"` to show orders awaiting payment or processing. ### 3. Order Tracking Display order status and details for customers to track their purchases. ### 4. Completed Orders Filter by `status: "completed"` to show fulfilled orders for reorder functionality. ## Best Practices 1. **Use Pagination** — Always implement pagination for better performance, especially for active customers 2. **Show Status** — Display the order status prominently so customers can track progress 3. **Filter by Status** — Provide status filters so customers can quickly find specific orders 4. **Cache Results** — Cache order lists for better performance 5. **Handle Empty States** — Provide helpful UI when the customer has no orders ## Error Handling ### Unauthenticated Request ```json { "errors": [ { "message": "Customer is not logged in.", "locations": [{ "line": 2, "column": 3 }], "path": ["customerOrders"] } ] } ``` ## Related Resources - [Get Single Customer Order](/api/graphql-api/shop/queries/get-customer-order) — Query individual order details - [Place Order](/api/graphql-api/shop/mutations/place-order) — Place a new order - [Get Customer Profile](/api/graphql-api/shop/queries/get-customer-profile) — Query customer profile - [Pagination Guide](/api/graphql-api/pagination) — Cursor pagination documentation --- # Get Customer Profile URL: /api/graphql-api/shop/queries/get-customer-profile --- outline: false examples: - id: get-customer-profile title: Get Customer Profile description: Retrieve the authenticated customer's profile information. query: | query getCustomerProfile { readCustomerProfile { id firstName lastName email dateOfBirth gender phone status subscribedToNewsLetter isVerified image } } variables: | {} response: | { "data": { "readCustomerProfile": { "id": "/api/shop/customer-profiles/122", "firstName": "John", "lastName": "Doe", "email": "john.doe@example.com", "dateOfBirth": "1990-01-15", "gender": "Male", "phone": "5550123", "status": "1", "subscribedToNewsLetter": true, "isVerified": "1", "image": null } } } --- # Get Customer Profile Retrieve the authenticated customer's profile information. ## Authentication This query requires a valid customer authentication token in the `Authorization` header. Use the [Customer Login API](/api/graphql-api/shop/mutations/customer-login) to retrieve the token. ``` Authorization: Bearer ``` ## Arguments This query has no required arguments. ## Response | Field | Type | Description | |-------|------|-------------| | `id` | String | Customer ID | | `firstName` | String | First name | | `lastName` | String | Last name | | `email` | String | Email address | | `dateOfBirth` | String | Date of birth (YYYY-MM-DD) | | `gender` | String | Gender (male/female/other) | | `phone` | String | Phone number | ## Use Cases - Display customer account information - Show profile on account dashboard - Verify customer information - Pre-fill form fields ## Error Responses ```json { "errors": { "authentication": ["Unauthenticated. Please login to perform this action"] } } ``` ## Related Documentation - [Update Customer Profile](/api/graphql-api/shop/mutations/update-customer-profile) - [Get Customer Orders](/api/graphql-api/shop/queries/get-customer-orders) - [Get Customer Addresses](/api/graphql-api/shop/queries/get-customer-addresses) --- # Get Customer Review URL: /api/graphql-api/shop/queries/get-customer-review --- outline: false examples: - id: get-customer-review-basic title: Get Single Customer Review description: Retrieve a specific product review by ID for the authenticated customer. query: | query getCustomerReview($id: ID!) { customerReview(id: $id) { id _id title comment rating status name product { id _id sku } customer { id _id } createdAt updatedAt } } variables: | { "id": "/api/shop/customer-reviews/1" } response: | { "data": { "customerReview": { "id": "/api/shop/customer-reviews/1", "_id": 1, "title": "Great product", "comment": "Really enjoyed using this product.", "rating": 5, "status": "approved", "name": "John", "product": { "id": "/api/shop/products/2", "_id": 2, "sku": "PUREWHTSNEAK2023" }, "customer": { "id": "/api/shop/customers/1", "_id": 1 }, "createdAt": "2026-02-18T10:30:00+00:00", "updatedAt": "2026-02-18T10:30:00+00:00" } } } commonErrors: - error: UNAUTHENTICATED cause: Missing or invalid Bearer token solution: Login and provide a valid customer authentication token - error: NOT_FOUND cause: Review with specified ID does not exist or does not belong to the customer solution: Verify the review ID and ensure it belongs to the authenticated customer - error: MISSING_ID cause: Review ID not provided solution: Provide a valid review IRI identifier - id: get-customer-review-introspection title: Schema Introspection - CustomerReview description: Inspect the CustomerReview type schema. query: | { __type(name: "CustomerReview") { name kind fields { name type { name kind } } } } variables: | {} response: | { "data": { "__type": { "name": "CustomerReview", "kind": "OBJECT", "fields": [ { "name": "id", "type": { "name": null, "kind": "NON_NULL" } }, { "name": "_id", "type": { "name": "Int", "kind": "SCALAR" } }, { "name": "title", "type": { "name": "String", "kind": "SCALAR" } }, { "name": "comment", "type": { "name": "String", "kind": "SCALAR" } }, { "name": "rating", "type": { "name": "Int", "kind": "SCALAR" } }, { "name": "status", "type": { "name": "String", "kind": "SCALAR" } }, { "name": "name", "type": { "name": "String", "kind": "SCALAR" } }, { "name": "product", "type": { "name": "Product", "kind": "OBJECT" } }, { "name": "customer", "type": { "name": "Customer", "kind": "OBJECT" } }, { "name": "createdAt", "type": { "name": "String", "kind": "SCALAR" } }, { "name": "updatedAt", "type": { "name": "String", "kind": "SCALAR" } } ] } } } commonErrors: [] --- # Get Customer Review ## About The `customerReview` query retrieves a single product review by its ID for the authenticated customer. Customers can only access their own reviews. Use this query to: - Display detailed review information - Show the full review text and rating - Check the approval status of a specific review - Load a review for detailed viewing in the customer dashboard ## Authentication This query requires customer authentication: - **Authenticated customers**: Provide a valid customer authentication token in the `Authorization` header. Obtain this token via the [Customer Login API](/api/graphql-api/shop/mutations/customer-login). ``` Authorization: Bearer X-STOREFRONT-KEY: ``` ## Arguments | Argument | Type | Required | Description | |----------|------|----------|-------------| | `id` | `ID!` | ✅ Yes | The IRI identifier of the customer review (e.g. `/api/shop/customer-reviews/1`). | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `id` | `ID!` | IRI identifier (e.g. `/api/shop/customer-reviews/1`). | | `_id` | `Int!` | Numeric database ID. | | `title` | `String!` | Review title. | | `comment` | `String!` | Review body text. | | `rating` | `Int!` | Star rating (1–5). | | `status` | `String!` | Review status: `pending`, `approved`, or `rejected`. | | `name` | `String!` | Reviewer display name. | | `product` | `Product!` | Associated product with id, _id, sku. | | `customer` | `Customer!` | Customer who wrote the review with id, _id. | | `createdAt` | `DateTime!` | ISO 8601 creation timestamp. | | `updatedAt` | `DateTime!` | ISO 8601 last update timestamp. | ## Error Responses | Error | Cause | |-------|-------| | `Unauthenticated` | Missing or invalid Bearer token | | `Customer review ID is required` | ID not provided for single-item query | | `Customer review with ID ":id" not found` | Review doesn't exist or doesn't belong to the customer | ## Use Cases - Display individual review details in customer account - Show review with full context and approval status - Load specific review for viewing or tracking - Check if a submitted review has been approved ## Related Resources - [Get All Customer Reviews](/api/graphql-api/shop/queries/get-customer-reviews) — Query all customer reviews - [Get Product Reviews](/api/graphql-api/shop/queries/get-product-reviews) — Query all product reviews - [Create Product Review](/api/graphql-api/shop/mutations/create-product-review) — Submit a new product review --- # Get Customer Reviews URL: /api/graphql-api/shop/queries/get-customer-reviews --- outline: false examples: - id: get-customer-reviews-basic title: Get Customer Reviews - Basic description: Retrieve all product reviews submitted by the authenticated customer with pagination. query: | query getCustomerReviews($first: Int, $after: String) { customerReviews(first: $first, after: $after) { edges { cursor node { id _id title comment rating status name product { id _id sku type } customer { id _id } createdAt updatedAt } } pageInfo { endCursor startCursor hasNextPage hasPreviousPage } totalCount } } variables: | { "first": 10 } response: | { "data": { "customerReviews": { "edges": [ { "cursor": "MA==", "node": { "id": "/api/shop/customer-reviews/1", "_id": 1, "title": "Great product", "comment": "Really enjoyed using this product.", "rating": 5, "status": "approved", "name": "John", "product": { "id": "/api/shop/products/2", "_id": 2, "sku": "PUREWHTSNEAK2023", "type": "simple" }, "customer": { "id": "/api/shop/customers/1", "_id": 1 }, "createdAt": "2026-02-18T10:30:00+00:00", "updatedAt": "2026-02-18T10:30:00+00:00" } } ], "pageInfo": { "endCursor": "MA==", "startCursor": "MA==", "hasNextPage": false, "hasPreviousPage": false }, "totalCount": 1 } } } commonErrors: - error: UNAUTHENTICATED cause: Missing or invalid Bearer token solution: Login and provide a valid customer authentication token - error: invalid-pagination cause: Invalid pagination arguments solution: Use valid first/after or last/before combinations with max value 100 - id: get-customer-reviews-status title: Get Customer Reviews - Filter by Status description: Retrieve customer reviews filtered by approval status. query: | query getApprovedReviews($first: Int, $status: String) { customerReviews(first: $first, status: $status) { edges { node { _id title rating status } } totalCount } } variables: | { "first": 10, "status": "approved" } response: | { "data": { "customerReviews": { "edges": [ { "node": { "_id": 1, "title": "Great product", "rating": 5, "status": "approved" } } ], "totalCount": 1 } } } commonErrors: - error: invalid-status cause: Invalid status value provided solution: Use one of pending, approved, or rejected - id: get-customer-reviews-rating title: Get Customer Reviews - Filter by Rating description: Retrieve customer reviews filtered by star rating. query: | query get5StarReviews($first: Int, $rating: Int) { customerReviews(first: $first, rating: $rating) { edges { node { _id title rating status } } totalCount } } variables: | { "first": 10, "rating": 5 } response: | { "data": { "customerReviews": { "edges": [ { "node": { "_id": 1, "title": "Great product", "rating": 5, "status": "approved" } } ], "totalCount": 1 } } } commonErrors: - error: invalid-rating cause: Rating value is out of valid range solution: Use a rating between 1 and 5 - id: get-customer-reviews-combined title: Get Customer Reviews - Combined Filters description: Retrieve customer reviews filtered by both status and rating. query: | query getApproved5StarReviews($first: Int, $status: String, $rating: Int) { customerReviews(first: $first, status: $status, rating: $rating) { edges { node { _id title comment rating status } } totalCount } } variables: | { "first": 10, "status": "approved", "rating": 5 } response: | { "data": { "customerReviews": { "edges": [ { "node": { "_id": 1, "title": "Great product", "comment": "Really enjoyed using this product.", "rating": 5, "status": "approved" } } ], "totalCount": 1 } } } commonErrors: - error: UNAUTHENTICATED cause: Missing or invalid Bearer token solution: Login and provide a valid customer authentication token - id: get-customer-reviews-pagination title: Get Customer Reviews - Pagination description: Paginate through customer reviews using cursor-based pagination. query: | query getNextPage($first: Int, $after: String) { customerReviews(first: $first, after: $after) { edges { cursor node { _id title } } pageInfo { endCursor hasNextPage } } } variables: | { "first": 5, "after": "MQ==" } response: | { "data": { "customerReviews": { "edges": [], "pageInfo": { "endCursor": null, "hasNextPage": false } } } } commonErrors: - error: INVALID_CURSOR cause: The cursor value is invalid or expired solution: Use a valid cursor from a previous response's pageInfo --- # Get Customer Reviews ## About The `customerReviews` query retrieves a paginated list of product reviews submitted by the authenticated customer. This is a **read-only, customer-scoped** resource — customers can only see their own reviews. Use this query to: - Display the customer's review history in their account dashboard - Show pending reviews awaiting approval - Filter reviews by status or star rating - Build review management UI for customers - Implement pagination for customers with many reviews This query uses cursor-based pagination and returns reviews with their associated product and customer data. ## Authentication This query requires customer authentication: - **Authenticated customers**: Provide a valid customer authentication token in the `Authorization` header. Obtain this token via the [Customer Login API](/api/graphql-api/shop/mutations/customer-login). ``` Authorization: Bearer X-STOREFRONT-KEY: ``` ## Arguments | Argument | Type | Required | Description | |----------|------|----------|-------------| | `first` | `Int` | ❌ No | Number of items to return (forward pagination). Max: 100. | | `after` | `String` | ❌ No | Cursor for forward pagination. Use `endCursor` from previous response. | | `last` | `Int` | ❌ No | Number of items for backward pagination. Max: 100. | | `before` | `String` | ❌ No | Cursor for backward pagination. Use `startCursor` from previous response. | | `status` | `String` | ❌ No | Filter by review status: `pending`, `approved`, or `rejected`. | | `rating` | `Int` | ❌ No | Filter by star rating (1–5). | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `edges` | `[CustomerReviewEdge!]` | Array of review edges with cursor and node. | | `edges.cursor` | `String!` | Cursor for this edge, used in pagination. | | `edges.node` | `CustomerReview!` | The customer review object. | | `edges.node.id` | `ID!` | IRI identifier (e.g. `/api/shop/customer-reviews/1`). | | `edges.node._id` | `Int!` | Numeric database ID. | | `edges.node.title` | `String!` | Review title. | | `edges.node.comment` | `String!` | Review body text. | | `edges.node.rating` | `Int!` | Star rating (1–5). | | `edges.node.status` | `String!` | Review status: `pending`, `approved`, or `rejected`. | | `edges.node.name` | `String!` | Reviewer display name. | | `edges.node.product` | `Product!` | Associated product with id, _id, sku, type. | | `edges.node.customer` | `Customer!` | Customer who wrote the review with id, _id. | | `edges.node.createdAt` | `DateTime!` | ISO 8601 creation timestamp. | | `edges.node.updatedAt` | `DateTime!` | ISO 8601 last update timestamp. | | `pageInfo` | `PageInfo!` | Pagination metadata. | | `pageInfo.hasNextPage` | `Boolean!` | Whether more pages exist forward. | | `pageInfo.hasPreviousPage` | `Boolean!` | Whether more pages exist backward. | | `pageInfo.startCursor` | `String` | Cursor for first item in page. | | `pageInfo.endCursor` | `String` | Cursor for last item in page. | | `totalCount` | `Int!` | Total reviews matching filters. | ## Filter Parameters | Parameter | Type | Values | Description | |-----------|------|--------|-------------| | `status` | String | `pending`, `approved`, `rejected` | Filter by review approval status | | `rating` | Int | `1`–`5` | Filter by star rating | ## Pagination Parameters | Parameter | Type | Description | |-----------|------|-------------| | `first` | Int | Return the first N items | | `last` | Int | Return the last N items | | `after` | String | Cursor — return items after this cursor | | `before` | String | Cursor — return items before this cursor | ## Use Cases ### 1. Customer Review History Fetch all reviews a customer has submitted to display in their account dashboard. ### 2. Pending Reviews Filter by `status: "pending"` to show the customer which reviews are still awaiting approval. ### 3. Top-Rated Reviews Filter by `rating: 5` to highlight the customer's highest-rated reviews. ### 4. Review Status Tracking Combine status and rating filters to help customers manage and track their reviews. ## Best Practices 1. **Use Pagination** — Always implement pagination for better performance, especially for active reviewers 2. **Show Status** — Display the review status so customers know which reviews are live 3. **Cache Results** — Cache review lists as they change infrequently 4. **Handle Empty States** — Provide helpful UI when the customer has no reviews 5. **Filter by Status** — Allow customers to filter by pending/approved/rejected for easy management ## Related Resources - [Get Single Customer Review](/api/graphql-api/shop/queries/get-customer-review) — Query individual customer review details - [Get Product Reviews](/api/graphql-api/shop/queries/get-product-reviews) — Query all product reviews - [Create Product Review](/api/graphql-api/shop/mutations/create-product-review) — Submit a new product review - [Pagination Guide](/api/graphql-api/pagination) — Cursor pagination documentation --- # Single CMS Page URL: /api/graphql-api/shop/queries/get-page --- outline: false examples: - id: get-cms-page-by-id title: Get Single CMS Page description: Retrieve a single CMS page by its IRI-style ID, including full translation details. query: | query getCmsPageDetail { page(id: "/api/shop/pages/1") { id _id layout createdAt updatedAt translation { id _id pageTitle urlKey htmlContent metaTitle metaDescription metaKeywords locale } } } variables: | {} response: | { "data": { "page": { "id": "/api/shop/pages/1", "_id": 1, "layout": null, "createdAt": "2024-04-16T16:14:17+05:30", "updatedAt": "2024-04-16T16:14:17+05:30", "translation": { "id": "/api/shop/page_translations/1", "_id": 1, "pageTitle": "About Us", "urlKey": "about-us", "htmlContent": "
We are dedicated to providing high-quality products and services to our customers. Our team is passionate about innovation and customer satisfaction. We believe in transparency, integrity, and building long-term relationships with our users.
", "metaTitle": "about us", "metaDescription": "", "metaKeywords": "aboutus", "locale": "en" } } } } commonErrors: - error: PAGE_NOT_FOUND cause: No CMS page exists for the provided ID solution: Verify the page ID using the pages query before fetching a single page - error: INVALID_ID_FORMAT cause: The ID is not a valid IRI-style string solution: Use the full IRI format, e.g. "/api/shop/pages/1" - error: UNAUTHENTICATED cause: Request is missing a valid authentication token solution: Include a valid Bearer token in the Authorization header --- # Single CMS Page ## About The `page(id:)` query retrieves a single CMS (Content Management System) page by its IRI-style ID. Use this query to: - Render a specific CMS page (e.g. About Us, Privacy Policy) in the storefront - Retrieve the full HTML content of a page for display - Access SEO metadata (meta title, description, keywords) for a specific page - Fetch locale-specific translation data for a page - Build dynamic page routes using `urlKey` - Validate whether a page exists before rendering This query returns a single page with its active locale translation. Use the `pages` query to list all available pages. > **Note:** The `translation` field returns content for the locale specified in the request header. If a translation does not exist for the selected locale, the content will automatically fall back to the store's default language. ## Arguments | Argument | Type | Required | Description | |----------|------|----------|-------------| | `id` | `ID!` | ✅ Yes | IRI-style identifier of the page (e.g. `/api/shop/pages/1`). | ## Possible Returns ### Page Fields | Field | Type | Description | |-------|------|-------------| | `id` | `ID!` | IRI-style unique identifier (e.g. `/api/shop/pages/1`). | | `_id` | `Int!` | Numeric database ID. | | `layout` | `String` | Page layout template name (e.g. `default`). | | `createdAt` | `DateTime!` | Timestamp when the page was created. | | `updatedAt` | `DateTime!` | Timestamp when the page was last updated. | | `translation` | `PageTranslation` | Active locale translation for this page. | ### PageTranslation Fields | Field | Type | Description | |-------|------|-------------| | `id` | `ID!` | IRI-style ID of the translation record. | | `_id` | `Int!` | Numeric translation record ID. | | `pageTitle` | `String!` | Display title of the CMS page. | | `urlKey` | `String!` | URL slug used to access the page (e.g. `about-us`). | | `htmlContent` | `String` | Full HTML body content of the page. | | `metaTitle` | `String` | SEO meta title tag. | | `metaDescription` | `String` | SEO meta description tag. | | `metaKeywords` | `String` | SEO meta keywords. | | `locale` | `String!` | Locale code for this translation (e.g. `en`, `fr`). | ## Use Cases ### 1. Render a CMS Page Fetch a specific page by ID and render its `htmlContent` on a dedicated page route in the storefront. ### 2. SEO Head Tags Use `metaTitle`, `metaDescription`, and `metaKeywords` from the translation to populate `` tags dynamically. ### 3. Page Validation Query a page before rendering to check whether it exists; handle null responses with a 404 page. ### 4. Multi-language Content Use the `locale` field in `translation` to display locale-aware page content in multilingual storefronts. ### 5. Dynamic Page Routing Use `urlKey` to implement client-side routing so users access pages via human-readable URLs (e.g. `/about-us`). ## Best Practices 1. **Use the IRI format** — Always pass the full IRI string (e.g. `/api/shop/pages/1`) as the `id` argument 2. **Handle null gracefully** — If `page` returns `null`, show a 404 response to the user 3. **Sanitize `htmlContent`** — Always sanitize the `htmlContent` field before rendering to prevent XSS vulnerabilities 4. **Cache individual pages** — Cache page responses keyed by ID; invalidate the cache when content is updated 5. **Use `urlKey` for routing** — Build page URLs from `urlKey` for SEO-friendly, human-readable links 6. **Fetch only needed fields** — Omit `htmlContent` when only metadata (title, URL) is needed to reduce response size ## Related Resources - [Get All CMS Pages](/api/graphql-api/shop/queries/get-pages) - Query all CMS pages - [Theme Customisations](/api/graphql-api/shop/queries/theme-customisations) - Query storefront theme customisations - [Pagination Guide](/api/graphql-api/pagination) - Cursor pagination documentation - [Shop API Overview](/api/graphql-api/shop-api) - Overview of Shop API resources --- # CMS Pages URL: /api/graphql-api/shop/queries/get-pages --- outline: false examples: - id: get-cms-pages-basic title: Get All CMS Pages description: Retrieve all CMS pages with their translation details using cursor-based pagination. query: | query getCmsPagesDetails { pages { edges { node { id _id layout createdAt updatedAt translation { id _id pageTitle urlKey htmlContent metaTitle metaDescription metaKeywords locale } } } } } variables: | {} response: | { "data": { "pages": { "edges": [ { "node": { "id": "/api/shop/pages/1", "_id": 1, "layout": null, "createdAt": "2024-04-16T16:14:17+05:30", "updatedAt": "2024-04-16T16:14:17+05:30", "translation": { "id": "/api/shop/page_translations/1", "_id": 1, "pageTitle": "About Us", "urlKey": "about-us", "htmlContent": "
We are dedicated to providing high-quality products and services to our customers...
", "metaTitle": "about us", "metaDescription": "", "metaKeywords": "aboutus", "locale": "en" } } }, { "node": { "id": "/api/shop/pages/2", "_id": 2, "layout": null, "createdAt": "2024-04-16T16:14:17+05:30", "updatedAt": "2024-04-16T16:14:17+05:30", "translation": { "id": "/api/shop/page_translations/2", "_id": 2, "pageTitle": "Return Policy", "urlKey": "return-policy", "htmlContent": "
If you are not satisfied with your purchase, you can return it within 30 days...
", "metaTitle": "return policy", "metaDescription": "", "metaKeywords": "return, policy", "locale": "en" } } }, { "node": { "id": "/api/shop/pages/3", "_id": 3, "layout": null, "createdAt": "2024-04-16T16:14:17+05:30", "updatedAt": "2024-04-16T16:14:17+05:30", "translation": { "id": "/api/shop/page_translations/3", "_id": 3, "pageTitle": "Refund Policy", "urlKey": "refund-policy", "htmlContent": "
We offer a 30-day refund policy on eligible products...
", "metaTitle": "Refund policy", "metaDescription": "", "metaKeywords": "refund, policy", "locale": "en" } } }, { "node": { "id": "/api/shop/pages/4", "_id": 4, "layout": null, "createdAt": "2024-04-16T16:14:17+05:30", "updatedAt": "2024-04-16T16:14:17+05:30", "translation": { "id": "/api/shop/page_translations/4", "_id": 4, "pageTitle": "Terms & Conditions", "urlKey": "terms-conditions", "htmlContent": "
These terms and conditions govern your use of our website...
", "metaTitle": "Terms & Conditions", "metaDescription": "", "metaKeywords": "term, conditions", "locale": "en" } } }, { "node": { "id": "/api/shop/pages/5", "_id": 5, "layout": null, "createdAt": "2024-04-16T16:14:17+05:30", "updatedAt": "2024-04-16T16:14:17+05:30", "translation": { "id": "/api/shop/page_translations/5", "_id": 5, "pageTitle": "Terms of use", "urlKey": "terms-of-use", "htmlContent": "
By accessing this website, you agree to follow our terms and conditions...
", "metaTitle": "Terms of use", "metaDescription": "", "metaKeywords": "term, use", "locale": "en" } } }, { "node": { "id": "/api/shop/pages/7", "_id": 7, "layout": null, "createdAt": "2024-04-16T16:14:17+05:30", "updatedAt": "2024-04-16T16:14:17+05:30", "translation": { "id": "/api/shop/page_translations/7", "_id": 7, "pageTitle": "Customer Service", "urlKey": "cutomer-service", "htmlContent": "
Our customer support team is available 24/7 to assist you...
", "metaTitle": "Customer Service", "metaDescription": "", "metaKeywords": "customer, service", "locale": "en" } } }, { "node": { "id": "/api/shop/pages/9", "_id": 9, "layout": null, "createdAt": "2024-04-16T16:14:17+05:30", "updatedAt": "2024-04-16T16:14:17+05:30", "translation": { "id": "/api/shop/page_translations/9", "_id": 9, "pageTitle": "Payment Policy", "urlKey": "payment-policy", "htmlContent": "
We accept multiple payment methods including credit/debit cards, digital wallets, and net banking...
", "metaTitle": "Payment Policy", "metaDescription": "", "metaKeywords": "payment, policy", "locale": "en" } } }, { "node": { "id": "/api/shop/pages/10", "_id": 10, "layout": null, "createdAt": "2024-04-16T16:14:17+05:30", "updatedAt": "2024-04-16T16:14:17+05:30", "translation": { "id": "/api/shop/page_translations/10", "_id": 10, "pageTitle": "Shipping Policy", "urlKey": "shipping-policy", "htmlContent": "
We offer fast and reliable shipping worldwide. Orders are processed within 1-2 business days...
", "metaTitle": "Shipping Policy", "metaDescription": "", "metaKeywords": "shipping, policy", "locale": "en" } } } ] } } } commonErrors: - error: UNAUTHENTICATED cause: Request is missing a valid authentication token solution: Include a valid Bearer token in the Authorization header - error: INTERNAL_SERVER_ERROR cause: Unexpected server-side error solution: Check server logs; retry the request or contact support --- # CMS Pages ## About The `pages` query retrieves a collection of all CMS (Content Management System) pages from your Bagisto store. Use this query to: - List all static content pages (e.g. About Us, Privacy Policy, FAQ) - Render CMS page listings in the storefront footer or navigation - Build a sitemap of all content pages - Access locale-specific page titles, slugs, and HTML content - Retrieve SEO metadata (meta title, description, keywords) for each page - Support multi-language storefronts with locale-aware translation data This query returns all pages with their active locale translation. Use the `page(id:)` query to fetch a single page by ID. > **Note:** The `translation` field returns content for the locale specified in the request header. If a translation does not exist for the selected locale, the content will automatically fall back to the store's default language. ## Arguments | Argument | Type | Required | Description | |----------|------|----------|-------------| | `first` | `Int` | ❌ No | Number of results to return (forward pagination). | | `after` | `String` | ❌ No | Cursor for forward pagination. Use with `first`. | | `last` | `Int` | ❌ No | Number of results for backward pagination. | | `before` | `String` | ❌ No | Cursor for backward pagination. Use with `last`. | ## Possible Returns ### Page Fields | Field | Type | Description | |-------|------|-------------| | `id` | `ID!` | IRI-style unique identifier (e.g. `/api/shop/pages/1`). | | `_id` | `Int!` | Numeric database ID. | | `layout` | `String` | Page layout template name (e.g. `default`). | | `createdAt` | `DateTime!` | Timestamp when the page was created. | | `updatedAt` | `DateTime!` | Timestamp when the page was last updated. | | `translation` | `PageTranslation` | Active locale translation for this page. | ### PageTranslation Fields | Field | Type | Description | |-------|------|-------------| | `id` | `ID!` | IRI-style ID of the translation record. | | `_id` | `Int!` | Numeric translation record ID. | | `pageTitle` | `String!` | Display title of the CMS page. | | `urlKey` | `String!` | URL slug used to access the page (e.g. `about-us`). | | `htmlContent` | `String` | Full HTML body content of the page. | | `metaTitle` | `String` | SEO meta title tag. | | `metaDescription` | `String` | SEO meta description tag. | | `metaKeywords` | `String` | SEO meta keywords. | | `locale` | `String!` | Locale code for this translation (e.g. `en`, `fr`). | ## Use Cases ### 1. Footer Navigation Links Fetch all CMS pages and use `urlKey` and `pageTitle` to build footer navigation links dynamically. ### 2. CMS Page Sitemap Retrieve all pages to generate a sitemap or page index for SEO crawlers. ### 3. Legal Pages Listing Filter the result client-side to display legal pages such as Privacy Policy and Terms & Conditions. ### 4. Multi-language Storefront Use the `locale` field in `translation` to display locale-aware page titles and content. ### 5. Admin Content Overview List all CMS pages in an admin dashboard for quick access and management. ## Best Practices 1. **Use `urlKey` for routing** — Build page URLs using `urlKey` rather than the numeric `_id` for human-readable and SEO-friendly URLs 2. **Cache responses** — CMS pages change infrequently; cache the response at the CDN or application level to reduce API calls 3. **Sanitize `htmlContent`** — Always sanitize the `htmlContent` field before rendering to prevent XSS vulnerabilities 4. **Use translations** — Always read content from the `translation` object to ensure locale-aware display 5. **Paginate for large stores** — Even though CMS pages are typically few, use `first` to limit payload size ## Related Resources - [Get Single CMS Page](/api/graphql-api/shop/queries/get-page) - Query a single page by ID - [Theme Customisations](/api/graphql-api/shop/queries/theme-customisations) - Query storefront theme customisations - [Pagination Guide](/api/graphql-api/pagination) - Cursor pagination documentation - [Shop API Overview](/api/graphql-api/shop-api) - Overview of Shop API resources --- # Get Payment Methods URL: /api/graphql-api/shop/queries/get-payment-methods --- outline: false examples: - id: get-payment-methods title: Get Payment Methods description: Retrieve available payment methods for checkout. query: | query checkoutPaymentMethods { collectionPaymentMethods { id _id method title description icon isAllowed } } response: | { "data": { "collectionPaymentMethods": [ { "id": "/api/.well-known/genid/0b8f9e6495ca9fce8943", "_id": "moneytransfer", "method": "moneytransfer", "title": "Money Transfer", "description": "Money Transfer", "icon": "https://api-demo.bagisto.com/themes/shop/default/build/assets/money-transfer-BNjtOcYo.png", "isAllowed": null } ] } } --- # Get Payment Methods Retrieve available payment methods for checkout. ## Authentication This query supports both authenticated customers and guest users: - **Authenticated customers**: Provide a valid customer authentication token in the `Authorization` header. Obtain this token via the [Customer Login API](/api/graphql-api/shop/mutations/customer-login). - **Guest users**: Provide the Guest Cart Token `cartToken` obtained from the [Create Cart mutation](/api/graphql-api/shop/mutations/create-cart). ``` Authorization: Bearer ``` ## Response Fields | Field | Type | Description | |-------|------|-------------| | `id` | String | API resource identifier | | `_id` | String | Payment method identifier | | `method` | String | Payment method code (for setting) | | `title` | String | Display name | | `description` | String | Method description | | `icon` | String | Payment method icon URL | | `isAllowed` | Boolean \| null | Whether method is allowed for current cart | ## Common Methods | Method | Title | Description | |--------|-------|-------------| | `moneytransfer` | Money Transfer | Direct money transfer payment | | `paypal` | PayPal | PayPal online payment | | `stripe` | Stripe | Credit/debit card via Stripe | | `cash_on_delivery` | Cash on Delivery | Pay when item is delivered | | `bank_transfer` | Bank Transfer | Manual bank transfer | ## Method Availability Available payment methods depend on: - Store configuration - Customer country - Cart total - Order fulfillment location ## Use Cases - Display payment options during checkout - Show available methods based on customer - Check if method is active - Build payment selection UI ## Error Responses ```json { "errors": { "general": ["No payment methods available."] } } ``` ## Related Documentation - [Set Payment Method](/api/graphql-api/shop/mutations/set-payment-method) - [Place Order](/api/graphql-api/shop/mutations/place-order) - [Checkout Flow](/api/graphql-api/shop/checkout) --- # Single Product URL: /api/graphql-api/shop/queries/get-product --- outline: false examples: - id: get-product-by-id title: Get Product by ID description: Retrieve product information using the product ID. `isInWishlist` and `isInCompare` indicate whether the signed-in customer has this product in their wishlist / compare list (`1` = yes, `0` = no) — send the customer Bearer token (both are `0` for guests). Over GraphQL these flags are returned as `"1"` / `"0"` strings; the REST API returns `1` / `0` integers. query: | query getProduct($id: ID!) { product(id: $id) { id name sku urlKey price isInWishlist isInCompare } } variables: | { "id": 2499 } response: | { "data": { "product": { "id": "/api/shop/products/2499", "name": "Ivory Frost Classic Overcoat XL", "sku": "sku-345346346-variant-9", "urlKey": "sku-345346346-variant-9", "price": "500", "isInWishlist": "1", "isInCompare": "0" } } } commonErrors: - error: PRODUCT_NOT_FOUND cause: Provided product ID does not exist solution: Verify the product ID exists - error: INVALID_ID cause: Invalid ID format solution: Use valid numeric or string ID - id: get-product-by-sku title: Get Product by SKU description: Retrieve product using the product SKU (Stock Keeping Unit). query: | query getProduct($sku: String!) { product(sku: $sku) { id name sku urlKey price } } variables: | { "sku": "sku-345346346-variant-9" } response: | { "data": { "product": { "id": "/api/shop/products/2499", "name": "Ivory Frost Classic Overcoat XL", "sku": "sku-345346346-variant-9", "urlKey": "sku-345346346-variant-9", "price": "500" } } } commonErrors: - error: PRODUCT_NOT_FOUND cause: SKU does not exist solution: Check product SKU spelling - id: get-product-with-variants title: Get Product with Variants description: Retrieve a configurable product with its variant options and superAttributeOptions for building variant selectors. query: | query getProduct($id: ID!) { product(id: $id) { id name sku urlKey price superAttributeOptions variants { edges { node { id name sku price attributeValues { edges { node { value attribute { code adminName } } } } } } } } } variables: | { "id": 2468 } response: | { "data": { "product": { "id": "/api/shop/products/2468", "name": "Minimalist Cotton Shirt", "sku": "MINIMAL-COTTON-001", "urlKey": "minimalist-cotton-shirt", "price": "0", "superAttributeOptions": "[{\"id\":23,\"code\":\"color\",\"label\":\"Color\",\"options\":[{\"id\":3,\"label\":\"Yellow\"},{\"id\":26,\"label\":\"Lavender Grey\"},{\"id\":27,\"label\":\"Charcoal\"}]},{\"id\":24,\"code\":\"size\",\"label\":\"Size\",\"options\":[{\"id\":6,\"label\":\"S\"},{\"id\":7,\"label\":\"M\"}]}]", "variants": { "edges": [ { "node": { "id": "/api/shop/products/2469", "name": "Minimalist Cotton Shirt Yellow S", "sku": "MINIMAL-COTTON-YEL-S", "price": "463", "attributeValues": { "edges": [ { "node": { "value": "MINIMAL-COTTON-YEL-S", "attribute": { "code": "sku", "adminName": "SKU" } } }, { "node": { "value": "Minimalist Cotton Shirt Yellow S", "attribute": { "code": "name", "adminName": "Name" } } }, { "node": { "value": "463.0000", "attribute": { "code": "price", "adminName": "Price" } } }, { "node": { "value": "3", "attribute": { "code": "color", "adminName": "Color" } } } ] } } }, { "node": { "id": "/api/shop/products/2470", "name": "Minimalist Cotton Shirt Lavender Grey S", "sku": "MINIMAL-COTTON-LAV-S", "price": "463", "attributeValues": { "edges": [ { "node": { "value": "MINIMAL-COTTON-LAV-S", "attribute": { "code": "sku", "adminName": "SKU" } } }, { "node": { "value": "Minimalist Cotton Shirt Lavender Grey S", "attribute": { "code": "name", "adminName": "Name" } } }, { "node": { "value": "463.0000", "attribute": { "code": "price", "adminName": "Price" } } }, { "node": { "value": "26", "attribute": { "code": "color", "adminName": "Color" } } } ] } } }, { "node": { "id": "/api/shop/products/2471", "name": "Minimalist Cotton Shirt Charcoal S", "sku": "MINIMAL-COTTON-CHAR-S", "price": "463", "attributeValues": { "edges": [ { "node": { "value": "MINIMAL-COTTON-CHAR-S", "attribute": { "code": "sku", "adminName": "SKU" } } }, { "node": { "value": "Minimalist Cotton Shirt Charcoal S", "attribute": { "code": "name", "adminName": "Name" } } }, { "node": { "value": "463.0000", "attribute": { "code": "price", "adminName": "Price" } } }, { "node": { "value": "27", "attribute": { "code": "color", "adminName": "Color" } } } ] } } }, { "node": { "id": "/api/shop/products/2472", "name": "Minimalist Cotton Shirt Yellow M", "sku": "MINIMAL-COTTON-YEL-M", "price": "463", "attributeValues": { "edges": [ { "node": { "value": "MINIMAL-COTTON-YEL-M", "attribute": { "code": "sku", "adminName": "SKU" } } }, { "node": { "value": "Minimalist Cotton Shirt Yellow M", "attribute": { "code": "name", "adminName": "Name" } } }, { "node": { "value": "463.0000", "attribute": { "code": "price", "adminName": "Price" } } }, { "node": { "value": "3", "attribute": { "code": "color", "adminName": "Color" } } } ] } } }, { "node": { "id": "/api/shop/products/2473", "name": "Minimalist Cotton Shirt Lavender Grey M", "sku": "MINIMAL-COTTON-LAV-M", "price": "463", "attributeValues": { "edges": [ { "node": { "value": "MINIMAL-COTTON-LAV-M", "attribute": { "code": "sku", "adminName": "SKU" } } }, { "node": { "value": "Minimalist Cotton Shirt Lavender Grey M", "attribute": { "code": "name", "adminName": "Name" } } }, { "node": { "value": "463.0000", "attribute": { "code": "price", "adminName": "Price" } } }, { "node": { "value": "26", "attribute": { "code": "color", "adminName": "Color" } } } ] } } }, { "node": { "id": "/api/shop/products/2474", "name": "Minimalist Cotton Shirt Charcoal M", "sku": "MINIMAL-COTTON-CHAR-M", "price": "463", "attributeValues": { "edges": [ { "node": { "value": "MINIMAL-COTTON-CHAR-M", "attribute": { "code": "sku", "adminName": "SKU" } } }, { "node": { "value": "Minimalist Cotton Shirt Charcoal M", "attribute": { "code": "name", "adminName": "Name" } } }, { "node": { "value": "463.0000", "attribute": { "code": "price", "adminName": "Price" } } }, { "node": { "value": "27", "attribute": { "code": "color", "adminName": "Color" } } } ] } } } ] } } } } commonErrors: - error: NO_VARIANTS cause: Product has no variants solution: Use simple product query - id: get-product-details-full title: Get Full Product Details description: Retrieve complete product information including attributes, images, descriptions, pricing, and all formatted price fields. query: | query getProduct($id: ID!) { product(id: $id) { id name sku type urlKey locale channel status description shortDescription color size featured new guestCheckout isSaleable price specialPrice minimumPrice maximumPrice regularMinimumPrice regularMaximumPrice formattedPrice formattedSpecialPrice formattedMinimumPrice formattedMaximumPrice formattedRegularMinimumPrice formattedRegularMaximumPrice superAttributeOptions combinations images { edges { node { id publicPath position } } } attributeValues { edges { node { value attribute { code adminName } } } } variants { edges { node { id name sku price attributeValues { edges { node { value attribute { code adminName } } } } } } } categories { edges { node { id translation { name } } } } } } variables: | { "id": 2478 } response: | { "data": { "product": { "id": "/api/shop/products/2478", "name": "Contemporary Fit Cut-Out Top", "sku": "CUTOUT-TOP-001", "type": "configurable", "urlKey": "contemporary-fit-cut-out-top", "locale": "en", "channel": null, "status": "1", "description": "This modern cut-out tank top is designed for those who prefer bold simplicity with a contemporary edge. Crafted with a body-hugging fit and subtle ribbed texture, it enhances the silhouette while keeping the look clean and refined.", "shortDescription": "A sleek ribbed tank top with a bold cut-out detail, designed to make a confident style statement with minimal effort.", "color": "", "size": "", "featured": "", "new": "1", "guestCheckout": "1", "isSaleable": "1", "price": "0", "specialPrice": null, "minimumPrice": "435", "maximumPrice": "435", "regularMinimumPrice": "435", "regularMaximumPrice": "435", "formattedPrice": "$0.00", "formattedSpecialPrice": null, "formattedMinimumPrice": "$435.00", "formattedMaximumPrice": "$435.00", "formattedRegularMinimumPrice": "$435.00", "formattedRegularMaximumPrice": "$435.00", "superAttributeOptions": "[{\"id\":23,\"code\":\"color\",\"label\":\"Color\",\"options\":[{\"id\":1,\"label\":\"Red\"},{\"id\":5,\"label\":\"White\"},{\"id\":20,\"label\":\"Ash grey\"}]},{\"id\":24,\"code\":\"size\",\"label\":\"Size\",\"options\":[{\"id\":6,\"label\":\"S\"}]}]", "combinations": "{\"2479\":{\"color\":1,\"size\":6},\"2480\":{\"color\":5,\"size\":6},\"2482\":{\"color\":20,\"size\":6}}", "images": { "edges": [ { "node": { "id": "/api/admin/images/678", "publicPath": "https://api-demo.bagisto.com/storage/product/2478/AA1X8qJMtgi3HKHGiwmV1LEPFrQk6Z8aYPc137Y0.webp", "position": "1" } }, { "node": { "id": "/api/admin/images/679", "publicPath": "https://api-demo.bagisto.com/storage/product/2478/LXF5IyrOREvTpNl0mMMeWgMLNFFWb7LqQjfn21H6.webp", "position": "2" } } ] }, "attributeValues": { "edges": [ { "node": { "value": "CUTOUT-TOP-001", "attribute": { "code": "sku", "adminName": "SKU" } } }, { "node": { "value": "Contemporary Fit Cut-Out Top", "attribute": { "code": "name", "adminName": "Name" } } }, { "node": { "value": "contemporary-fit-cut-out-top", "attribute": { "code": "url_key", "adminName": "URL Key" } } } ] }, "variants": { "edges": [ { "node": { "id": "/api/shop/products/2479", "name": "Contemporary Fit Cut-Out Top Red S", "sku": "CUTOUT-TOP-RED-S", "price": "435", "attributeValues": { "edges": [ { "node": { "value": "CUTOUT-TOP-RED-S", "attribute": { "code": "sku", "adminName": "SKU" } } }, { "node": { "value": "Contemporary Fit Cut-Out Top Red S", "attribute": { "code": "name", "adminName": "Name" } } }, { "node": { "value": "435.0000", "attribute": { "code": "price", "adminName": "Price" } } }, { "node": { "value": "1", "attribute": { "code": "color", "adminName": "Color" } } } ] } } }, { "node": { "id": "/api/shop/products/2480", "name": "Contemporary Fit Cut-Out Top White S", "sku": "CUTOUT-TOP-WHT-S", "price": "435", "attributeValues": { "edges": [ { "node": { "value": "CUTOUT-TOP-WHT-S", "attribute": { "code": "sku", "adminName": "SKU" } } }, { "node": { "value": "Contemporary Fit Cut-Out Top White S", "attribute": { "code": "name", "adminName": "Name" } } }, { "node": { "value": "435.0000", "attribute": { "code": "price", "adminName": "Price" } } }, { "node": { "value": "5", "attribute": { "code": "color", "adminName": "Color" } } } ] } } }, { "node": { "id": "/api/shop/products/2482", "name": "Contemporary Fit Cut-Out Top Ash Grey S", "sku": "CUTOUT-TOP-AGREY-S", "price": "435", "attributeValues": { "edges": [ { "node": { "value": "CUTOUT-TOP-AGREY-S", "attribute": { "code": "sku", "adminName": "SKU" } } }, { "node": { "value": "Contemporary Fit Cut-Out Top Ash Grey S", "attribute": { "code": "name", "adminName": "Name" } } }, { "node": { "value": "435.0000", "attribute": { "code": "price", "adminName": "Price" } } }, { "node": { "value": "20", "attribute": { "code": "color", "adminName": "Color" } } } ] } } } ] }, "categories": { "edges": [ { "node": { "id": "/api/shop/categories/22", "translation": { "name": "Fashion" } } } ] } } } } commonErrors: - error: PRODUCT_NOT_FOUND cause: Product ID does not exist solution: Verify the product ID is correct - id: get-downloadable-product-samples title: Get Downloadable Product with Samples description: Retrieve a downloadable product with its downloadable links and product-level samples. Use the _id from each link or sample to download the corresponding file. query: | query getProduct($id: ID!) { product(id: $id) { _id name sku type price downloadableLinks { edges { node { _id type translation { title } price formattedPrice sampleType sampleFile sampleFileUrl sampleUrl } } } downloadableSamples { edges { node { _id type file fileUrl url translation { title } } } } } } variables: | { "id": 2506 } response: | { "data": { "product": { "_id": 2506, "name": "Complete Personal Finance Guide (eBook PDF)", "sku": "COMPLETE-PERSONAL-FINANCE-GUIDE-EBOOK", "type": "downloadable", "price": "70", "downloadableLinks": { "edges": [ { "node": { "_id": 2, "type": "url", "translation": { "title": "Full eBook PDF" }, "price": "69", "formattedPrice": "$69.00", "sampleType": "file", "sampleFile": "product_downloadable_links/2506/4aUxeYumTemSR3QwHHHGmdiHBG2qWek3KDR8fhYK.pdf", "sampleFileUrl": "https://api-demo.bagisto.com/api/downloadable/download-sample/link/2", "sampleUrl": null } } ] }, "downloadableSamples": { "edges": [ { "node": { "_id": 1, "type": "file", "file": "product_downloadable_links/2506/1apTXUkt2ugCISKHadT5Fmp4EwU7YeWYY2wb4mNs.pdf", "fileUrl": "https://api-demo.bagisto.com/api/downloadable/download-sample/sample/1", "url": null, "translation": { "title": "" } } } ] } } } } commonErrors: - error: PRODUCT_NOT_FOUND cause: Product ID does not exist solution: Verify the product ID is correct - id: get-grouped-product title: Get Grouped Product description: Retrieve a grouped product with its associated child products and quantities. Grouped products bundle multiple simple products together, each with a default quantity. query: | query getProduct($id: ID!) { product(id: $id) { id name sku type urlKey locale channel status description shortDescription featured new guestCheckout isSaleable price specialPrice minimumPrice maximumPrice regularMinimumPrice regularMaximumPrice formattedPrice formattedSpecialPrice formattedMinimumPrice formattedMaximumPrice formattedRegularMinimumPrice formattedRegularMaximumPrice groupedProducts { edges { node { id qty sortOrder associatedProduct { id name sku price formattedPrice specialPrice formattedSpecialPrice images(first: 3) { edges { node { id publicPath } } } } } } } images { edges { node { id publicPath position } } } categories { edges { node { id translation { name } } } } } } variables: | { "id": 2516 } response: | { "data": { "product": { "id": "/api/shop/products/2516", "name": "Arctic Frost Winter Accessories", "sku": "GP-001", "type": "grouped", "urlKey": "arctic-frost-winter-accessories", "locale": "en", "channel": null, "status": "1", "description": "Introducing the Arctic Frost Winter Accessories Bundle, your go-to solution for staying warm, stylish, and connected during the chilly winter days. This thoughtfully curated set brings together Four essential winter accessories to create a harmonious ensemble. The luxurious scarf, woven from a blend of acrylic and wool, not only adds a layer of warmth but also brings a touch of elegance to your winter wardrobe. The soft knit beanie, crafted with care, promises to keep you cozy while adding a fashionable flair to your look. But it doesn't end there – our bundle also includes touchscreen-compatible gloves. Stay connected without sacrificing warmth as you navigate your devices effortlessly. Whether you're answering calls, sending messages, or capturing winter moments on your smartphone, these gloves ensure convenience without compromising style. The soft and cozy texture of the socks offers a luxurious feel against your skin. Say goodbye to chilly feet as you embrace the plush warmth provided by these wool blend socks. The Arctic Frost Winter Accessories Bundle is not just about functionality; it's a statement of winter fashion. Each piece is designed not only to protect you from the cold but also to elevate your style during the frosty season. The materials chosen for this bundle prioritize both durability and comfort, ensuring that you can enjoy the winter wonderland in style. Whether you're treating yourself or searching for the perfect gift, the Arctic Frost Winter Accessories Bundle is a versatile choice. Delight someone special during the holiday season or elevate your own winter wardrobe with this stylish and functional ensemble. Embrace the frost with confidence, knowing that you have the perfect accessories to keep you warm and chic.", "shortDescription": "Embrace the winter chill with our Arctic Frost Winter Accessories Bundle. This curated set includes a luxurious scarf, a cozy beanie, touchscreen-compatible gloves and wool Blend Socks. Stylish and functional, this ensemble is crafted from high-quality materials, ensuring both durability and comfort. Elevate your winter wardrobe or delight someone special with this perfect gifting option.", "featured": "1", "new": "1", "guestCheckout": "1", "isSaleable": "1", "price": "0", "specialPrice": null, "minimumPrice": "14", "maximumPrice": "21", "regularMinimumPrice": "14", "regularMaximumPrice": "21", "formattedPrice": "$0.00", "formattedSpecialPrice": null, "formattedMinimumPrice": "$14.00", "formattedMaximumPrice": "$21.00", "formattedRegularMinimumPrice": "$14.00", "formattedRegularMaximumPrice": "$21.00", "groupedProducts": { "edges": [ { "node": { "id": "/api/shop/product_grouped_products/1", "qty": 1, "sortOrder": 0, "associatedProduct": { "id": "/api/shop/products/2512", "name": "Arctic Cozy Knit Unisex Beanie", "sku": "SP-001", "price": "14", "formattedPrice": "$14.00", "specialPrice": null, "formattedSpecialPrice": null, "images": { "edges": [ { "node": { "id": "/api/admin/images/851", "publicPath": "https://api-demo.bagisto.com/storage/product/2512/Muc0qeWks34MTZaxf38s6DBmfqMqrCxku81Uo8EB.webp" } } ] } } } }, { "node": { "id": "/api/shop/product_grouped_products/2", "qty": 1, "sortOrder": 1, "associatedProduct": { "id": "/api/shop/products/2514", "name": "Arctic Touchscreen Winter Gloves", "sku": "SP-003", "price": "21", "formattedPrice": "$21.00", "specialPrice": "17", "formattedSpecialPrice": "$17.00", "images": { "edges": [ { "node": { "id": "/api/admin/images/853", "publicPath": "https://api-demo.bagisto.com/storage/product/2514/g8lR0Ity8HcpE20A4yAkX5wvLY5RlTC67NJKyyg6.webp" } } ] } } } }, { "node": { "id": "/api/shop/product_grouped_products/3", "qty": 1, "sortOrder": 2, "associatedProduct": { "id": "/api/shop/products/2515", "name": "Arctic Warmth Wool Blend Socks", "sku": "SP-004", "price": "21", "formattedPrice": "$21.00", "specialPrice": null, "formattedSpecialPrice": null, "images": { "edges": [ { "node": { "id": "/api/admin/images/854", "publicPath": "https://api-demo.bagisto.com/storage/product/2515/442ouyaT1K4weKSZGhSDtSKDBbrhiH0aWWwGcFW0.webp" } } ] } } } } ] }, "images": { "edges": [ { "node": { "id": "/api/admin/images/855", "publicPath": "https://api-demo.bagisto.com/storage/product/2516/5Kgto6KVm6FLMaaDEY6pwCcVoTIhX03D3OGDzwbf.webp", "position": "1" } } ] }, "categories": { "edges": [] } } } } commonErrors: - error: PRODUCT_NOT_FOUND cause: Product ID does not exist solution: Verify the product ID is correct - id: get-appointment-booking-product title: Get Appointment Booking Product description: Retrieve an appointment booking product with its slot configuration. Appointment bookings use the `appointmentSlot` relationship which includes duration, break time, and time slot availability per day. query: | query getProduct($id: ID!) { product(id: $id) { id name sku urlKey price bookingProducts { edges { node { _id type availableFrom availableTo qty location availableEveryWeek appointmentSlot { id _id bookingProductId duration breakTime sameSlotAllDays slots } } } } } } variables: | { "id": 2509 } response: | { "data": { "product": { "id": "/api/shop/products/2509", "name": "Men's Haircut Appointment", "sku": "SALON-HAIRCUT-APPOINTMENT", "urlKey": "mens-haircut-appointment", "price": "60", "bookingProducts": { "edges": [ { "node": { "_id": 3, "type": "appointment", "availableFrom": "2026-04-06T06:30:00.000000Z", "availableTo": "2026-12-30T06:30:00.000000Z", "qty": 10, "location": "Noida, Uttar Pradesh", "availableEveryWeek": "0", "appointmentSlot": { "id": "1", "_id": 1, "bookingProductId": "3", "duration": 45, "breakTime": 15, "sameSlotAllDays": "1", "slots": "[{\"to\": \"10:45\", \"from\": \"10:00\"}, {\"to\": \"11:45\", \"from\": \"11:00\"}, {\"to\": \"12:45\", \"from\": \"12:00\"}, {\"to\": \"14:45\", \"from\": \"14:00\"}, {\"to\": \"15:45\", \"from\": \"15:00\"}]" } } } ] } } } } commonErrors: - error: PRODUCT_NOT_FOUND cause: Provided product ID does not exist solution: Verify the product ID exists - error: BOOKING_TYPE_MISMATCH cause: Product is not an appointment booking type solution: Ensure the product has appointment booking enabled - id: get-rental-booking-product title: Get Rental Booking Product description: Retrieve a rental booking product with its rental slot configuration. Rental bookings use the `rentalSlot` relationship which includes renting type (daily/hourly), daily and hourly pricing, and slot availability. query: | query getProduct($id: ID!) { product(id: $id) { id name sku urlKey price bookingProducts { edges { node { _id type availableFrom availableTo qty location availableEveryWeek rentalSlot { id _id bookingProductId rentingType dailyPrice hourlyPrice sameSlotAllDays slots } } } } } } variables: | { "id": 2510 } response: | { "data": { "product": { "id": "/api/shop/products/2510", "name": "Wooden Folding Chair Rental", "sku": "WOODEN-FOLDING-CHAIR-RENTAL", "urlKey": "wooden-folding-chair-rental", "price": "109", "bookingProducts": { "edges": [ { "node": { "_id": 4, "type": "rental", "availableFrom": "2026-04-06T06:30:00.000000Z", "availableTo": "2026-04-30T06:30:00.000000Z", "qty": 150, "location": "Noida, Uttar Pradesh", "availableEveryWeek": "0", "rentalSlot": { "id": "1", "_id": 1, "bookingProductId": "4", "rentingType": "daily_hourly", "dailyPrice": "99", "hourlyPrice": "105", "sameSlotAllDays": "1", "slots": "[{\"to\": \"18:00\", \"from\": \"12:00\"}]" } } } ] } } } } commonErrors: - error: PRODUCT_NOT_FOUND cause: Provided product ID does not exist solution: Verify the product ID exists - error: BOOKING_TYPE_MISMATCH cause: Product is not a rental booking type solution: Ensure the product has rental booking enabled - id: get-default-booking-product title: Get Default Booking Product description: Retrieve a default booking product with its slot configuration. Default bookings use the `defaultSlot` relationship which includes booking type, duration, break time, and day-wise slot availability. query: | query getProduct($id: ID!) { product(id: $id) { id name sku urlKey price bookingProducts { edges { node { _id type availableFrom availableTo qty location availableEveryWeek defaultSlot { id _id bookingType duration breakTime slots } } } } } } variables: | { "id": 2507 } response: | { "data": { "product": { "id": "/api/shop/products/2507", "name": "Professional Photography Session", "sku": "PROFESSIONAL-PHOTOGRAPHY-SESSION", "urlKey": "professional-photography-session", "price": "100", "bookingProducts": { "edges": [ { "node": { "_id": 1, "type": "default", "availableFrom": "2026-04-06T06:30:00.000000Z", "availableTo": "2026-12-31T06:30:00.000000Z", "qty": 150, "location": "Noida, Uttar Pradesh", "availableEveryWeek": null, "defaultSlot": { "id": "1", "_id": 1, "bookingType": "one", "duration": null, "breakTime": null, "slots": "[{\"id\": \"1\", \"to\": \"18:00\", \"from\": \"12:00\", \"to_day\": \"1\", \"from_day\": \"1\"}, {\"id\": \"2\", \"to\": \"18:00\", \"from\": \"12:00\", \"to_day\": \"2\", \"from_day\": \"2\"}, {\"id\": \"1\", \"to\": \"18:00\", \"from\": \"12:00\", \"to_day\": \"3\", \"from_day\": \"3\"}]" } } } ] } } } } commonErrors: - error: PRODUCT_NOT_FOUND cause: Provided product ID does not exist solution: Verify the product ID exists - error: BOOKING_TYPE_MISMATCH cause: Product is not a default booking type solution: Ensure the product has default booking enabled - id: get-table-booking-product title: Get Table Booking Product description: Retrieve a table booking product with its slot configuration. Table bookings use the `tableSlot` relationship which includes price type, guest limit, duration, break time, scheduling restrictions, and day-wise slot availability. query: | query getProduct($id: ID!) { product(id: $id) { id name sku urlKey price bookingProducts { edges { node { _id type availableFrom availableTo qty location availableEveryWeek tableSlot { id _id bookingProductId priceType guestLimit duration breakTime preventSchedulingBefore sameSlotAllDays slots } } } } } } variables: | { "id": 2511 } response: | { "data": { "product": { "id": "/api/shop/products/2511", "name": "Fine Dining Table Reservation", "sku": "FINE-DINING-TABLE-RESERVATION", "urlKey": "fine-dining-table-reservation", "price": "200", "bookingProducts": { "edges": [ { "node": { "_id": 5, "type": "table", "availableFrom": "2026-04-06T06:30:00.000000Z", "availableTo": "2026-04-30T06:30:00.000000Z", "qty": 5, "location": "Mumbai, Maharashtra", "availableEveryWeek": "0", "tableSlot": { "id": "1", "_id": 1, "bookingProductId": "5", "priceType": "guest", "guestLimit": 0, "duration": 45, "breakTime": 15, "preventSchedulingBefore": 2, "sameSlotAllDays": "1", "slots": "[{\"to\": \"12:45\", \"from\": \"12:00\"}, {\"to\": \"13:45\", \"from\": \"13:00\"}, {\"to\": \"14:45\", \"from\": \"14:00\"}, {\"to\": \"19:45\", \"from\": \"19:00\"}, {\"to\": \"20:45\", \"from\": \"20:00\"}, {\"to\": \"21:45\", \"from\": \"21:00\"}]" } } } ] } } } } commonErrors: - error: PRODUCT_NOT_FOUND cause: Provided product ID does not exist solution: Verify the product ID exists - error: BOOKING_TYPE_MISMATCH cause: Product is not a table booking type solution: Ensure the product has table booking enabled - id: get-event-booking-product title: Get Event Booking Product description: Retrieve an event booking product with its ticket configuration. Event bookings use the `eventTickets` relationship which includes ticket pricing, quantity, and special price date ranges — a different structure from slot-based booking types. query: | query getProduct($id: ID!) { product(id: $id) { id name sku urlKey price bookingProducts { edges { node { _id type availableFrom availableTo qty location availableEveryWeek eventTickets { edges { node { id _id bookingProductId price qty specialPrice specialPriceFrom specialPriceTo formattedPrice formattedSpecialPrice translation { locale name description } translations { edges { node { locale name description } } } } } } } } } } } variables: | { "id": 2508 } response: | { "data": { "product": { "id": "/api/shop/products/2508", "name": "Live Music Concert Ticket", "sku": "LIVE-MUSIC-CONCERT-TICKET", "urlKey": "live-music-concert-ticket", "price": "120", "bookingProducts": { "edges": [ { "node": { "_id": 2, "type": "event", "availableFrom": "2026-04-06T06:30:00.000000Z", "availableTo": "2026-04-30T06:30:00.000000Z", "qty": 0, "location": "Noida, Uttar Pradesh", "availableEveryWeek": null, "eventTickets": { "edges": [ { "node": { "id": "7", "_id": 7, "bookingProductId": "2", "price": "120", "qty": 1500, "specialPrice": "115", "specialPriceFrom": "2026-04-06 12:00:00", "specialPriceTo": "2026-04-30 12:00:00", "formattedPrice": "$120.00", "formattedSpecialPrice": "$115.00", "translation": { "locale": "en", "name": "Standard Entry Ticket", "description": "General admission ticket for the event with access to all standard areas and performances." }, "translations": { "edges": [ { "node": { "locale": "en", "name": "Standard Entry Ticket", "description": "General admission ticket for the event with access to all standard areas and performances." } } ] } } }, { "node": { "id": "8", "_id": 8, "bookingProductId": "2", "price": "125", "qty": 150, "specialPrice": "120", "specialPriceFrom": "2026-04-02 12:00:00", "specialPriceTo": "2026-04-30 12:00:00", "formattedPrice": "$125.00", "formattedSpecialPrice": "$120.00", "translation": { "locale": "en", "name": "VIP Access Ticket", "description": "VIP ticket includes priority entry, reserved seating, and access to exclusive areas near the stage." }, "translations": { "edges": [ { "node": { "locale": "en", "name": "VIP Access Ticket", "description": "VIP ticket includes priority entry, reserved seating, and access to exclusive areas near the stage." } } ] } } } ] } } } ] } } } } commonErrors: - error: PRODUCT_NOT_FOUND cause: Provided product ID does not exist solution: Verify the product ID exists - error: BOOKING_TYPE_MISMATCH cause: Product is not an event booking type solution: Ensure the product has event booking enabled --- # Single Product ## About The `product` query retrieves a single product by its unique identifier, SKU, or URL key. Use this query to: - Fetch individual products for detail pages - Look up products by different identifier types (ID, SKU, URL) - Display complete product information including images, variants, and attributes - Show product pricing, descriptions, and SEO metadata - Retrieve inventory and availability status - Build product-specific API integrations - Generate product detail pages with all metadata This query supports multiple lookup methods (ID, SKU, or URL key) and can return minimal data for previews or comprehensive data for full product detail pages, making it flexible for various use cases. ::: info Why Booking Product Types Are Documented Separately All product types (simple, configurable, grouped, bundle, downloadable, virtual) share the same core fields (`name`, `sku`, `price`, `images`, `variants`, `attributeValues`, etc.) and can be queried using the same base query structure. However, **booking products** are documented with separate examples because each booking type (Appointment, Rental, Default, Table, Event) exposes its own unique relationship and slot structure through the `bookingProducts` field. These sub-types have different fields and response shapes (e.g., `appointmentSlot`, `rentalSlot`, `defaultSlot`, `tableSlot`, `eventTickets`), so dedicated examples are provided to show how to query each one correctly. ::: ## Arguments | Argument | Type | Description | |----------|------|-------------| | `id` | `ID` | Product's unique system identifier. Use this for direct lookups. | | `sku` | `String` | Stock Keeping Unit. Alternative identifier for product lookup. | | `urlKey` | `String` | URL-friendly product slug. Alternative lookup method. | | `include_variants` | `Boolean` | Include product variants (colors, sizes, options). Default: `false` | | `include_images` | `Boolean` | Include product images. Default: `false` | | `include_attributes` | `Boolean` | Include custom product attributes. Default: `true` | | `image_resolution` | `String` | Image quality: `thumbnail`, `medium`, `large`, `original`. Default: `large` | | `include_recommendations` | `Boolean` | Include related and recommended products. Default: `false` | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `id` | `ID!` | Unique product identifier. | | `name` | `String!` | Product display name. | | `sku` | `String!` | Stock Keeping Unit for inventory tracking. | | `urlKey` | `String!` | URL-friendly product slug for SEO. | | `type` | `String!` | Product type (simple, configurable, grouped, bundle). | | `description` | `String` | Full product description with formatting. | | `shortDescription` | `String` | Brief product summary. | | `price` | `Float!` | Base product price. | | `specialPrice` | `Float` | Promotional/discounted price if applicable. | | `superAttributeOptions` | `String (JSON)` | JSON-encoded array of configurable attribute options (e.g. color, size) with their available values. Only populated for **configurable** products — returns the attributes and selectable options that define the product's variants. | | `combinations` | `String (JSON)` | JSON-encoded object mapping variant product IDs to their attribute option combinations. Each key is a variant ID and the value contains the attribute option IDs that define that variant. Only populated for **configurable** products. | | `taxClass` | `String` | Tax classification for the product. | | `images` | `[ProductImage!]` | Array of product images with URLs and metadata. | | `images.url` | `String!` | Image URL. | | `images.altText` | `String` | Image alt text for accessibility. | | `images.position` | `Int` | Image order in gallery. | | `images.width` | `Int` | Image width in pixels. | | `images.height` | `Int` | Image height in pixels. | | `attributes` | `[ProductAttribute!]` | Custom product attributes and values. | | `variants` | `[ProductVariant!]` | Product variants (colors, sizes, options). | | `variants.sku` | `String!` | Variant SKU. | | `variants.price` | `Float!` | Variant-specific price. | | `inventory` | `InventoryInfo!` | Stock availability information. | | `inventory.stock` | `Int!` | Current stock quantity. | | `inventory.status` | `String!` | Stock status (in_stock, out_of_stock, low_stock). | | `categories` | `[Category!]!` | Categories this product belongs to. | | `tags` | `[String!]` | Product tags and labels. | | `seo` | `ProductSEO!` | SEO metadata. | | `status` | `String!` | Product status (active, draft, inactive). | | `visibility` | `String!` | Visibility status (visible, not visible, search only). | | `createdAt` | `DateTime!` | Product creation date. | | `updatedAt` | `DateTime!` | Last modification date. | | `isInWishlist` | `Int` | Whether the signed-in customer has this product in their **wishlist** (active channel): `1` = yes, `0` = no. | | `isInCompare` | `Int` | Whether the signed-in customer has this product in their **compare list**: `1` = yes, `0` = no. | > **Wishlist & compare flags:** `isInWishlist` and `isInCompare` let you highlight the wishlist / compare icon directly from the product response instead of separately fetching and cross-referencing those lists. They require the customer Bearer token — for guests both are `0`. The values are `0` / `1`; over GraphQL they are returned as the strings `"1"` (in the list) / `"0"` (not in the list), while the REST API returns them as `1` / `0` integers. ## Configurable Products A **configurable product** is a product that has multiple variants based on attributes like color, size, or material. For example, a T-shirt that comes in 3 colors and 2 sizes would have 6 variants. When querying a configurable product, two additional fields are returned that are essential for building a variant selection UI: ### `superAttributeOptions` This field returns a JSON-encoded string containing the configurable attributes and their selectable options. Each entry includes: - `code` — the attribute code (e.g. `color`, `size`) - `label` — the display label (e.g. `Color`, `Size`) - `options` — an array of available values, each with an `id` and `label` Use this field to render attribute dropdowns (e.g. color picker, size selector) on the product detail page. ### `combinations` This field returns a JSON-encoded object that maps each **variant product ID** to its specific attribute option combination. For example: ```json {"8": {"color": 3, "size": 7}, "9": {"color": 3, "size": 8}} ``` This means variant ID `8` is the product with color option `3` and size option `7`. When a customer selects a color and size from the dropdowns, use this mapping to resolve which variant ID to load (for pricing, stock, images, etc.). ### How they work together 1. Use `superAttributeOptions` to render the attribute dropdowns on your product page 2. When the customer selects options (e.g. Color: Blue, Size: M), match their selection against the `combinations` object to find the corresponding variant ID 3. Use that variant ID to display the correct price, stock status, and images for the selected variant > For non-configurable product types (simple, grouped, bundle, etc.), both `superAttributeOptions` and `combinations` will be `null`. ## Downloadable Products A **downloadable** product contains digital files that customers can download after purchase. Each downloadable product can have two types of sample files: ### `downloadableLinks` These are the individual download links that make up the product (e.g. Track 1, Track 2 for a music album, or Chapter 1, Chapter 2 for an e-book). Each link has its own price and can optionally have a **sample file** attached for preview. The fields `sampleFile`, `sampleFileUrl`, and `sampleUrl` provide details about the sample associated with each link. ### `downloadableSamples` These are **product-level samples** — general preview files for the entire product rather than a specific link. The `_id` from each sample node is used to download the sample via: ``` GET /api/shop/downloadable/download-sample/sample/{_id} ``` ### Downloading purchased files After a customer purchases a downloadable product, the purchased files can be downloaded using the `_id` from the [Get Downloadable Products](/api/graphql-api/shop/queries/get-customer-downloadable-products) query (not the product query). See the [Download Downloadable Product](/api/graphql-api/shop/queries/download-downloadable-product) page for full details. > Sample downloads are free and do not require authentication. Purchased file downloads require customer authentication and have a limited number of downloads. ## Grouped Products A **grouped product** bundles multiple simple products together, allowing customers to purchase them as a set. Unlike a bundle product where the customer selects options, a grouped product presents each child product with a default quantity that the customer can adjust before adding to cart. ### `groupedProducts` This field returns the list of associated child products via the `groupedProducts` connection. Each node contains: - `id` — the grouped product relationship ID - `qty` — the default quantity for that child product in the group - `sortOrder` — the display order of the child product - `associatedProduct` — the full child product details including `id`, `name`, `sku`, `price`, `formattedPrice`, `specialPrice`, `formattedSpecialPrice`, and `images` ### Pricing behavior A grouped product's own `price` is always `0` because it does not have a standalone price. Instead, the price range is derived from its child products: - `minimumPrice` — the lowest priced child product - `maximumPrice` — the highest priced child product The customer's total depends on which child products they select and in what quantities. > For non-grouped product types, the `groupedProducts` field will return an empty edges array. ## Booking Product Types All standard product types (simple, configurable, grouped, bundle, downloadable, virtual) share the same core fields and can be queried using any of the general examples above. Booking products, however, require special attention because each booking type exposes a **different relationship with its own unique fields** through the `bookingProducts` connection. The `bookingProducts` field returns a `type` that determines which slot/ticket relationship contains the data: | Booking Type | Relationship Field | Key Fields | Use Case | |---|---|---|---| | **Appointment** | `appointmentSlot` | `duration`, `breakTime`, `sameSlotAllDays`, `slots` | Salon visits, doctor appointments, consultations | | **Rental** | `rentalSlot` | `rentingType`, `dailyPrice`, `hourlyPrice`, `slots` | Equipment rental, vehicle hire, venue booking | | **Default** | `defaultSlot` | `bookingType`, `duration`, `breakTime`, `slots` | General time-slot bookings | | **Table** | `tableSlot` | `priceType`, `guestLimit`, `duration`, `breakTime`, `preventSchedulingBefore`, `slots` | Restaurant reservations, meeting rooms | | **Event** | `eventTickets` | `price`, `qty`, `specialPrice`, `specialPriceFrom`, `specialPriceTo` | Concerts, workshops, conferences | ::: tip Only the relationship matching the product's booking type will contain data. For example, an appointment booking product will have data in `appointmentSlot` but not in `rentalSlot` or `tableSlot`. Always check the `type` field first to determine which relationship to query. ::: ## Product Fields Reference Below is a complete reference of all available fields on the `Product` type. Use this as a lookup when building your queries — include only the fields you need. ### Basic Information | Field | Type | Description | |-------|------|-------------| | `id` | `ID!` | Unique product identifier in IRI format (e.g. `/api/shop/products/1`) | | `_id` | `Int!` | Numeric database ID | | `sku` | `String!` | Stock Keeping Unit — unique product code for inventory tracking | | `type` | `String!` | Product type: `simple`, `virtual`, `configurable`, `grouped`, `bundle`, `downloadable`, `booking` | | `name` | `String` | Product display name | | `urlKey` | `String` | URL-friendly slug (e.g. `premium-wireless-headphones`) | | `status` | `String` | Product status (`1` = active, `0` = inactive) | | `locale` | `String` | Locale of the returned product data | | `channel` | `String` | Channel the product belongs to | | `productNumber` | `String` | Optional product number assigned by the store | | `additional` | `Iterable` | Additional product data stored as key-value pairs | | `createdAt` | `String` | Product creation timestamp | | `updatedAt` | `String` | Last modification timestamp | ### Descriptions | Field | Type | Description | |-------|------|-------------| | `description` | `String` | Full product description (plain text) | | `descriptionHtml` | `String` | Full product description with HTML formatting preserved | | `shortDescription` | `String` | Brief product summary for listing pages | ### Pricing | Field | Type | Description | |-------|------|-------------| | `price` | `String` | Base catalog price. Reflects active currency conversion. | | `specialPrice` | `String` | Discounted price if set, otherwise `null` | | `specialPriceFrom` | `String` | Start date for the special price (YYYY-MM-DD) | | `specialPriceTo` | `String` | End date for the special price (YYYY-MM-DD) | | `cost` | `String` | Product cost/purchase price (internal use) | | `minimumPrice` | `String` | Lowest effective price — accounts for special price and variant pricing. Used for price sorting. | | `maximumPrice` | `String` | Highest effective price across all variants | | `regularMinimumPrice` | `String` | Regular (non-discounted) minimum price | | `regularMaximumPrice` | `String` | Regular (non-discounted) maximum price | | `formattedPrice` | `String` | `price` with currency symbol (e.g. `$99.00`) | | `formattedSpecialPrice` | `String` | `specialPrice` with currency symbol | | `formattedMinimumPrice` | `String` | `minimumPrice` with currency symbol | | `formattedMaximumPrice` | `String` | `maximumPrice` with currency symbol | | `formattedRegularMinimumPrice` | `String` | `regularMinimumPrice` with currency symbol | | `formattedRegularMaximumPrice` | `String` | `regularMaximumPrice` with currency symbol | | `taxCategoryId` | `String` | Tax category ID assigned to the product | ### Physical Dimensions | Field | Type | Description | |-------|------|-------------| | `weight` | `String` | Product weight | | `length` | `String` | Product length | | `width` | `String` | Product width | | `height` | `String` | Product height | ### Attributes | Field | Type | Description | |-------|------|-------------| | `color` | `String` | Color attribute value (if applicable) | | `size` | `String` | Size attribute value (if applicable) | | `brand` | `String` | Brand attribute value (if applicable) | ### SEO | Field | Type | Description | |-------|------|-------------| | `metaTitle` | `String` | SEO meta title for the product page | | `metaKeywords` | `String` | SEO meta keywords | | `metaDescription` | `String` | SEO meta description | ### Flags & Settings | Field | Type | Description | |-------|------|-------------| | `isSaleable` | `String` | Whether the product can be purchased (`1` = yes) | | `new` | `String` | Whether the product is marked as "new" (`1` = yes) | | `featured` | `String` | Whether the product is featured (`1` = yes) | | `visibleIndividually` | `String` | Whether the product appears in catalog listings (`1` = yes) | | `guestCheckout` | `String` | Whether guest users can purchase this product (`1` = yes) | | `manageStock` | `String` | Whether stock is managed for this product (`1` = yes) | ### Configurable Product Fields | Field | Type | Description | |-------|------|-------------| | `superAttributeOptions` | `String` | JSON-encoded array of configurable attributes and their selectable options. See [Configurable Products](#configurable-products). | | `combinations` | `String` | JSON-encoded object mapping variant IDs to their attribute option combinations. See [Configurable Products](#configurable-products). | ### Media | Field | Type | Description | |-------|------|-------------| | `baseImageUrl` | `String` | URL of the product's primary/base image | ## Product Relationships Reference These are the connection/relationship fields available on the `Product` type. Each returns a paginated cursor connection supporting `first`, `last`, `before`, and `after` arguments. ### Core Relationships | Relationship | Return Type | Description | |---|---|---| | `images` | `ProductImageCursorConnection` | Product gallery images with `id`, `publicPath`, and `position` | | `videos` | `ProductVideoCursorConnection` | Product videos | | `categories` | `CategoryCursorConnection` | Categories the product belongs to, with `translation { name }` | | `attributeValues` | `AttributeValueCursorConnection` | All attribute values with `value` and `attribute { code, adminName }` | | `attributeFamily` | `AttributeFamily!` | The attribute family this product belongs to (not paginated) | | `channels` | `ChannelCursorConnection` | Channels this product is assigned to | ### Variant & Configuration | Relationship | Return Type | Description | |---|---|---| | `variants` | `ProductCursorConnection` | Child variant products (for configurable products). Each variant is a full `Product` with its own `id`, `name`, `sku`, `price`, and `attributeValues`. | | `superAttributes` | `AttributeCursorConnection` | The attributes used for configurable options (e.g. Color, Size) | | `parent` | `Product!` | Parent product (for variant products — returns the configurable parent) | ### Product Type-Specific | Relationship | Return Type | Description | Dropdown Example | |---|---|---|---| | `groupedProducts` | `ProductGroupedProductCursorConnection` | Associated child products with `qty`, `sortOrder`, and `associatedProduct` details | "Get Grouped Product" | | `bundleOptions` | `ProductBundleOptionCursorConnection` | Bundle option groups with selectable products and quantities | — | | `downloadableLinks` | `ProductDownloadableLinkCursorConnection` | Downloadable links with `price`, `formattedPrice`, `translation { title }`, and sample info | "Get Downloadable Product with Samples" | | `downloadableSamples` | `ProductDownloadableSampleCursorConnection` | Product-level sample files with `file`, `fileUrl`, `url`, and `translation { title }` | "Get Downloadable Product with Samples" | | `bookingProducts` | `BookingProductCursorConnection` | Booking configuration with `type`, `availableFrom`, `availableTo`, `qty`, `location`, and type-specific slots | "Get Appointment/Rental/Default/Table/Event Booking Product" | | `customizableOptions` | `ProductCustomizableOptionCursorConnection` | Custom product options (e.g. engraving text, gift wrapping) | — | ### Related & Recommended Products | Relationship | Return Type | Description | |---|---|---| | `relatedProducts` | `ProductCursorConnection` | Products marked as related — typically shown in a "Related Products" section on the product page | | `upSells` | `ProductCursorConnection` | Up-sell products — higher-value alternatives shown on the product page | | `crossSells` | `ProductCursorConnection` | Cross-sell products — complementary items shown during checkout or in the cart | ### Reviews | Relationship | Return Type | Description | |---|---|---| | `reviews` | `ProductReviewCursorConnection` | All reviews for this product (all statuses) | | `approvedReviews` | `ProductReviewCursorConnection` | Only admin-approved reviews — use this for public-facing display | ::: tip All relationship fields support cursor-based pagination with `first`, `last`, `before`, and `after` arguments. For example: `images(first: 5)` returns only the first 5 images. Use the nested `pageInfo { hasNextPage, endCursor }` to paginate through large collections. ::: --- # Product Reviews URL: /api/graphql-api/shop/queries/get-product-reviews --- outline: false examples: - id: get-product-reviews-basic title: Get Product Reviews - Basic description: Retrieve product reviews with basic fields and pagination. query: | query productReviews($first: Int, $after: String) { productReviews(first: $first, after: $after) { edges { node { id _id name title rating comment status createdAt updatedAt } cursor } pageInfo { hasNextPage endCursor } totalCount } } variables: | { "first": 10 } response: | { "data": { "productReviews": { "edges": [ { "node": { "id": "/api/shop/reviews/1", "_id": 1, "name": "Tom Smith", "title": "Incredible Product!", "rating": 5, "comment": "This jacket is incredibly warm and comfortable. I love wearing it on cold days or when I'm going for a hike. It's also very stylish and looks great with a pair of jeans or chinos.", "status": 0, "createdAt": "2023-11-16T12:23:20+05:30", "updatedAt": "2023-12-01T10:44:45+05:30" }, "cursor": "MA==" }, { "node": { "id": "/api/shop/reviews/2", "_id": 2, "name": "Thomas Freeman", "title": "High Quality & Affordable", "rating": 5, "comment": "I can't believe how affordable this jacket is for the quality. It's well-made and looks great. I've already gotten so many compliments on it.", "status": 0, "createdAt": "2023-11-16T12:30:54+05:30", "updatedAt": "2023-11-16T12:31:09+05:30" }, "cursor": "MQ==" }, { "node": { "id": "/api/shop/reviews/3", "_id": 3, "name": "Emma Wilson", "title": "Perfect Winter Essential", "rating": 4, "comment": "Great quality and very comfortable. Highly recommend for anyone looking for a warm jacket.", "status": 0, "createdAt": "2023-11-18T08:15:30+05:30", "updatedAt": "2023-11-18T08:15:30+05:30" }, "cursor": "Mg==" } ], "pageInfo": { "hasNextPage": true, "endCursor": "Mg==" }, "totalCount": 45 } } } commonErrors: - error: invalid-pagination cause: Invalid pagination arguments solution: Use valid first/after or last/before combinations with max value 100 - error: invalid-product-id cause: Product ID is not a valid integer solution: Use a valid numeric product ID - error: invalid-rating cause: Rating value is out of valid range solution: Use rating between 1 and 5 - id: get-product-reviews-by-product-id title: Get Product Reviews - By Product ID description: Retrieve reviews for a specific product using its numeric product ID. query: | query productReviews($productId: Int) { productReviews(product_id: $productId) { edges { node { id _id name title rating comment status attachments createdAt updatedAt } } } } variables: | { "productId": 2446 } response: | { "data": { "productReviews": { "edges": [ { "node": { "id": "/api/shop/reviews/33", "_id": 33, "name": "Hiroshi Tanaka", "title": "Solid Gaming Keyboard", "rating": 5, "comment": "Gaming casual competitive Valorant/CS2. Switches tactile response accurate fast 8k polling proof. RGB control easy app intuitive. Wrist rest supportive gaming sessions. Cons: USB-C port slightly loose after 3 months (QA variance), N-key rollover flawless. Reliable 4 months daily use robust build. Worth ₹11.5k investment.", "status": "approved", "attachments": null, "createdAt": "2026-01-08T13:50:01+05:30", "updatedAt": "2026-01-08T13:50:23+05:30" } } ] } } } commonErrors: - error: invalid-product-id cause: Product ID is not a valid integer or product does not exist solution: Use a valid numeric product ID from your store - error: invalid-pagination cause: Invalid pagination arguments solution: Use valid first/after or last/before combinations with max value 100 - id: get-product-reviews-by-status title: Get Product Reviews - Filtered by Status description: Retrieve reviews filtered by approval status. Use "approved" to show published reviews, "pending" for those awaiting moderation, or "rejected" for declined reviews. query: | query productReviewsByStatus($status: String, $first: Int, $after: String) { productReviews(status: $status, first: $first, after: $after) { edges { node { id _id name title rating comment status createdAt updatedAt } cursor } pageInfo { hasNextPage endCursor } totalCount } } variables: | { "status": "approved", "first": 10 } response: | { "data": { "productReviews": { "edges": [ { "node": { "id": "/api/shop/reviews/4", "_id": 4, "name": "Gerson Rivera", "title": "Earphones", "rating": 5, "comment": "I've been using these earphones for a week now and I'm really impressed. The sound is clear and balanced, with just the right amount of bass.", "status": "approved", "createdAt": "2025-09-03T12:32:39+05:30", "updatedAt": "2025-09-03T12:33:56+05:30" }, "cursor": "MA==" }, { "node": { "id": "/api/shop/reviews/5", "_id": 5, "name": "Gerson Rivera", "title": "Overhead", "rating": 5, "comment": "I've been using these overhead headphones for a while and they feel really solid. The sound quality is excellent – clear vocals, detailed highs, and a deep, punchy bass.", "status": "approved", "createdAt": "2025-09-03T12:33:34+05:30", "updatedAt": "2025-09-03T12:33:56+05:30" }, "cursor": "MQ==" } ], "pageInfo": { "hasNextPage": true, "endCursor": "MQ==" }, "totalCount": 12 } } } commonErrors: - error: invalid-status cause: Status value is not a recognised string solution: Use one of "approved", "pending", or "rejected" - error: invalid-pagination cause: Invalid pagination arguments solution: Use valid first/after or last/before combinations with max value 100 - id: get-product-reviews-by-rating title: Get Product Reviews - Filtered by Rating description: Retrieve reviews filtered by a specific star rating (1–5). Useful for highlighting top-rated feedback or surfacing low-rated reviews for quality control. query: | query productReviewsByRating($rating: Int, $first: Int, $after: String) { productReviews(rating: $rating, first: $first, after: $after) { edges { node { id _id name title rating comment status createdAt updatedAt } cursor } pageInfo { hasNextPage endCursor } totalCount } } variables: | { "rating": 5, "first": 10 } response: | { "data": { "productReviews": { "edges": [ { "node": { "id": "/api/shop/reviews/4", "_id": 4, "name": "Gerson Rivera", "title": "Earphones", "rating": 5, "comment": "I've been using these earphones for a week now and I'm really impressed. The sound is clear and balanced, with just the right amount of bass. Definitely worth it if you're looking for reliable everyday earphones.", "status": "approved", "createdAt": "2025-09-03T12:32:39+05:30", "updatedAt": "2025-09-03T12:33:56+05:30" }, "cursor": "MA==" }, { "node": { "id": "/api/shop/reviews/5", "_id": 5, "name": "Gerson Rivera", "title": "Overhead", "rating": 5, "comment": "I've been using these overhead headphones for a while and they feel really solid. The sound quality is excellent – clear vocals, detailed highs, and a deep, punchy bass that makes music more immersive.", "status": "approved", "createdAt": "2025-09-03T12:33:34+05:30", "updatedAt": "2025-09-03T12:33:56+05:30" }, "cursor": "MQ==" }, { "node": { "id": "/api/shop/reviews/7", "_id": 7, "name": "Gerson Rivera", "title": "Royal Sofa", "rating": 5, "comment": "I recently purchased the royal leather sofa and it truly adds a luxurious touch to the living room. The leather finish feels premium and elegant.", "status": "approved", "createdAt": "2025-09-03T12:36:08+05:30", "updatedAt": "2025-09-03T12:40:50+05:30" }, "cursor": "Mg==" } ], "pageInfo": { "hasNextPage": true, "endCursor": "Mg==" }, "totalCount": 9 } } } commonErrors: - error: invalid-rating cause: Rating value is out of valid range solution: Use an integer between 1 and 5 - error: invalid-pagination cause: Invalid pagination arguments solution: Use valid first/after or last/before combinations with max value 100 # - id: get-product-reviews-filtered # title: Get Product Reviews - Filtered by Product # description: Retrieve product reviews filtered by product ID with optional status and rating filters. # query: | # query productReviews($productId: Int, $status: Int, $rating: Int, $first: Int, $after: String) { # productReviews(productId: $productId, status: $status, rating: $rating, first: $first, after: $after) { # edges { # node { # id # _id # name # title # rating # comment # status # createdAt # updatedAt # productId # } # cursor # } # pageInfo { # hasNextPage # endCursor # startCursor # hasPreviousPage # } # totalCount # } # } # variables: | # { # "productId": 357, # "status": 0, # "rating": 5, # "first": 10 # } # response: | # { # "data": { # "productReviews": { # "edges": [ # { # "node": { # "id": "/api/shop/reviews/1", # "_id": 1, # "name": "Tom Smith", # "title": "Incredible Product!", # "rating": 5, # "comment": "This jacket is incredibly warm and comfortable. I love wearing it on cold days or when I'm going for a hike. It's also very stylish and looks great with a pair of jeans or chinos.", # "status": 0, # "createdAt": "2023-11-16T12:23:20+05:30", # "updatedAt": "2023-12-01T10:44:45+05:30", # }, # "cursor": "MA==" # }, # { # "node": { # "id": "/api/shop/reviews/2", # "_id": 2, # "name": "Thomas Freeman", # "title": "High Quality & Affordable", # "rating": 5, # "comment": "I can't believe how affordable this jacket is for the quality. It's well-made and looks great. I've already gotten so many compliments on it.", # "status": 0, # "createdAt": "2023-11-16T12:30:54+05:30", # "updatedAt": "2023-11-16T12:31:09+05:30", # }, # "cursor": "MQ==" # }, # { # "node": { # "id": "/api/shop/reviews/8", # "_id": 8, # "name": "Sarah Johnson", # "title": "Excellent Value", # "rating": 5, # "comment": "Outstanding quality for the price. Very satisfied with my purchase. Would buy again!", # "status": 0, # "createdAt": "2023-12-02T14:45:20+05:30", # "updatedAt": "2023-12-02T14:45:20+05:30", # "productId": 357 # }, # "cursor": "Mw==" # } # ], # "pageInfo": { # "hasNextPage": false, # "endCursor": "Mw==", # "startCursor": "MA==", # "hasPreviousPage": false # }, # "totalCount": 3 # } # } # } # commonErrors: # - error: invalid-pagination # cause: Invalid pagination arguments or exceeding maximum limit # solution: Use valid first/after or last/before combinations with max value 100 # - error: invalid-product-id # cause: Product ID is not a valid integer # solution: Use a valid numeric product ID # - error: invalid-rating # cause: Rating value is out of valid range # solution: Use rating between 1 and 5 # - error: invalid-status # cause: Status value is not valid # solution: Use status 0 (pending), 1 (approved), or 2 (rejected) # - id: get-product-reviews-complete # title: Get Product Reviews - Complete Details # description: Retrieve all product reviews with complete pagination information and all filters applied. # query: | # query productReviews($productId: Int, $status: Int, $rating: Int, $first: Int, $after: String, $last: Int, $before: String) { # productReviews(productId: $productId, status: $status, rating: $rating, first: $first, after: $after, last: $last, before: $before) { # edges { # node { # id # _id # name # title # rating # comment # status # createdAt # updatedAt # productId # } # cursor # } # pageInfo { # endCursor # startCursor # hasNextPage # hasPreviousPage # } # totalCount # } # } # variables: | # { # "first": 5 # } # response: | # { # "data": { # "productReviews": { # "edges": [ # { # "node": { # "id": "/api/shop/reviews/1", # "_id": 1, # "name": "Tom Smith", # "title": "Incredible Product!", # "rating": 5, # "comment": "This jacket is incredibly warm and comfortable. I love wearing it on cold days or when I'm going for a hike. It's also very stylish and looks great with a pair of jeans or chinos.", # "status": 0, # "createdAt": "2023-11-16T12:23:20+05:30", # "updatedAt": "2023-12-01T10:44:45+05:30", # "productId": 357 # }, # "cursor": "MA==" # }, # { # "node": { # "id": "/api/shop/reviews/2", # "_id": 2, # "name": "Thomas Freeman", # "title": "High Quality & Affordable", # "rating": 5, # "comment": "I can't believe how affordable this jacket is for the quality. It's well-made and looks great. I've already gotten so many compliments on it.", # "status": 0, # "createdAt": "2023-11-16T12:30:54+05:30", # "updatedAt": "2023-11-16T12:31:09+05:30", # "productId": 357 # }, # "cursor": "MQ==" # }, # { # "node": { # "id": "/api/shop/reviews/3", # "_id": 3, # "name": "Emma Wilson", # "title": "Perfect Winter Essential", # "rating": 4, # "comment": "Great quality and very comfortable. Highly recommend for anyone looking for a warm jacket.", # "status": 0, # "createdAt": "2023-11-18T08:15:30+05:30", # "updatedAt": "2023-11-18T08:15:30+05:30", # "productId": 357 # }, # "cursor": "Mg==" # }, # { # "node": { # "id": "/api/shop/reviews/4", # "_id": 4, # "name": "James Brown", # "title": "Good Value", # "rating": 4, # "comment": "Nice jacket, good quality. Would recommend to friends and family.", # "status": 0, # "createdAt": "2023-11-20T16:30:15+05:30", # "updatedAt": "2023-11-20T16:30:15+05:30", # "productId": 357 # }, # "cursor": "Mw==" # }, # { # "node": { # "id": "/api/shop/reviews/5", # "_id": 5, # "name": "Lisa Anderson", # "title": "Excellent Quality", # "rating": 5, # "comment": "Best jacket I've ever owned. Highly recommended for anyone looking for quality and style.", # "status": 0, # "createdAt": "2023-11-22T09:45:22+05:30", # "updatedAt": "2023-11-22T09:45:22+05:30", # "productId": 357 # }, # "cursor": "NA==" # } # ], # "pageInfo": { # "endCursor": "NA==", # "startCursor": "MA==", # "hasNextPage": true, # "hasPreviousPage": false # }, # "totalCount": 45 # } # } # } # commonErrors: # - error: invalid-pagination # cause: Invalid pagination arguments or exceeding maximum limit # solution: Use valid first/after or last/before combinations with max value 100 # - error: invalid-product-id # cause: Product ID is not a valid integer # solution: Use a valid numeric product ID # - error: invalid-rating # cause: Rating value is out of valid range # solution: Use rating between 1 and 5 # - error: invalid-status # cause: Status value is not valid # solution: Use status 0 (pending), 1 (approved), or 2 (rejected) --- # Product Reviews ## About The `productReviews` query retrieves a collection of product reviews with filtering and pagination support. Use this query to: - Display product reviews on product detail pages - Show review statistics and ratings - Filter reviews by product, status, and rating - Build review listing pages with pagination - Display customer feedback and testimonials - Calculate average ratings and review counts - Show pending reviews in admin dashboard - Implement review sorting and filtering This query supports full pagination with cursor-based navigation and flexible filtering options for various use cases. > **Note:** This query only returns reviews that have been **approved by an admin**. Reviews submitted by customers are set to `pending` by default and will not appear in the results until an admin approves them from the admin panel. `pending` and `rejected` reviews are never visible to storefront users. ## Arguments | Argument | Type | Required | Description | |----------|------|----------|-------------| | `status` | `String` | ❌ No | Filter by review status (`"pending"`, `"approved"`, `"rejected"`). | | `rating` | `Int` | ❌ No | Filter by rating value (1-5 stars). | | `first` | `Int` | ❌ No | Number of results to return (forward pagination). Max: 100. | | `after` | `String` | ❌ No | Pagination cursor for forward navigation. Use with `first`. | | `last` | `Int` | ❌ No | Number of results for backward pagination. Max: 100. | | `before` | `String` | ❌ No | Pagination cursor for backward navigation. Use with `last`. | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `id` | `ID!` | Unique review API identifier. | | `_id` | `Int!` | Numeric review ID. | | `name` | `String!` | Customer name who wrote the review. | | `title` | `String!` | Review title/headline. | | `rating` | `Int!` | Star rating (1-5). | | `comment` | `String!` | Review comment/text. | | `status` | `String!` | Review approval status (`"pending"`, `"approved"`, `"rejected"`). | | `createdAt` | `DateTime!` | Review creation timestamp. | | `updatedAt` | `DateTime!` | Last update timestamp. | | `pageInfo` | `PageInfo!` | Pagination information. | | `pageInfo.hasNextPage` | `Boolean!` | Whether more pages exist forward. | | `pageInfo.hasPreviousPage` | `Boolean!` | Whether more pages exist backward. | | `pageInfo.startCursor` | `String` | Cursor for first item in page. | | `pageInfo.endCursor` | `String` | Cursor for last item in page. | | `totalCount` | `Int!` | Total reviews matching filters. | ## Review Status | Status | Description | |--------|-------------| | `"pending"` | Awaiting moderation approval | | `"approved"` | Published and visible on the storefront | | `"rejected"` | Declined and not published | ## Use Cases ### 1. Product Reviews Page Use the "By Product ID" example to display all approved reviews for a specific product. ### 2. Admin Review Management Use the "Filtered by Status" example with `status: "pending"` to show reviews awaiting moderation. ### 3. High-Rated Reviews Use the "Filtered by Rating" example with `rating: 5` to highlight 5-star reviews. ### 4. Customer Testimonials Filter by approved status and high rating to display customer testimonials. ### 5. Review Analytics Use pagination to fetch all reviews for a product and calculate statistics. ## Best Practices 1. **Filter by Status** - Always filter by `status: "approved"` to show only approved reviews to customers 2. **Show Ratings** - Display the rating prominently alongside the review 3. **Use Pagination** - Always implement pagination for better performance 4. **Cache Results** - Cache reviews for better performance as they change infrequently 5. **Sort Reviews** - Display most recent or highest-rated reviews first 6. **Prevent Spam** - Only show approved reviews to maintain quality 7. **Display Author Info** - Show customer name to build trust and authenticity ## Related Resources - [Get Single Product Review](/api/graphql-api/shop/queries/get-product-review) - Query individual review details - [Create Product Review](/api/graphql-api/shop/mutations/create-product-review) - Submit new product review - [Get Product](/api/graphql-api/shop/queries/get-product) - Query product details - [Pagination Guide](/api/graphql-api/pagination) - Cursor pagination documentation - [Shop API Overview](/api/graphql-api/shop-api) - Overview of Shop API resources --- # Get Products URL: /api/graphql-api/shop/queries/get-products --- outline: false examples: - id: get-products-wishlist-compare-flags title: Get Products with Wishlist & Compare Flags description: For a signed-in customer, every product carries `isInWishlist` and `isInCompare`, so you can highlight the wishlist / compare icon directly on each product card without separately fetching and cross-referencing the wishlist or compare lists (which paginate independently of the catalog). Send the customer Bearer token. The flags are `1` (in the list) or `0` (not in the list); over GraphQL they are returned as the strings `"1"` / `"0"`. For guests, both are always `"0"`. query: | query getProductsWithFlags { products(first: 3) { edges { node { id name sku isInWishlist isInCompare } } } } variables: | {} response: | { "data": { "products": { "edges": [ { "node": { "id": "/api/shop/products/1", "name": "Coastal Breeze Men's Blue Zipper Hoodie", "sku": "COASTALBREEZEMENSHOODIE", "isInWishlist": "1", "isInCompare": "0" } }, { "node": { "id": "/api/shop/products/22", "name": "Acme Drawstring Bag", "sku": "ACME-DRAWBAG-001", "isInWishlist": "0", "isInCompare": "1" } }, { "node": { "id": "/api/shop/products/92", "name": "Bagisto Sticker", "sku": "bagisto-sticker", "isInWishlist": "0", "isInCompare": "0" } } ] } } } - id: get-products-currency-formatted-prices title: Get Products with Currency Formatted Prices description: Fetch products with all formatted price fields that reflect the active currency set via the locale header. Use these formatted fields instead of raw price fields when displaying prices to customers, as they include currency conversion and symbol. query: | query getProductsSorted { products(first: 10) { edges { node { id name sku price formattedPrice specialPrice formattedSpecialPrice minimumPrice formattedMinimumPrice maximumPrice formattedMaximumPrice regularMinimumPrice formattedRegularMinimumPrice regularMaximumPrice formattedRegularMaximumPrice } } } } variables: | {} response: | { "data": { "products": { "edges": [ { "node": { "id": "/api/shop/products/1", "name": "Coastal Breeze Men's Blue Zipper Hoodie", "sku": "COASTALBREEZEMENSHOODIE", "price": "100", "formattedPrice": "$100.00", "specialPrice": null, "formattedSpecialPrice": null, "minimumPrice": "100", "formattedMinimumPrice": "$100.00", "maximumPrice": "100", "formattedMaximumPrice": "$100.00", "regularMinimumPrice": "100", "formattedRegularMinimumPrice": "$100.00", "regularMaximumPrice": "100", "formattedRegularMaximumPrice": "$100.00" } }, { "node": { "id": "/api/shop/products/22", "name": "Acme Drawstring Bag", "sku": "ACME-DRAWBAG-001", "price": "3000", "formattedPrice": "$3,000.00", "specialPrice": "2700", "formattedSpecialPrice": "$2,700.00", "minimumPrice": "2700", "formattedMinimumPrice": "$2,700.00", "maximumPrice": "2700", "formattedMaximumPrice": "$2,700.00", "regularMinimumPrice": "3000", "formattedRegularMinimumPrice": "$3,000.00", "regularMaximumPrice": "3000", "formattedRegularMaximumPrice": "$3,000.00" } }, { "node": { "id": "/api/shop/products/92", "name": "Bagisto Sticker", "sku": "bagisto-sticker", "price": "10", "formattedPrice": "$10.00", "specialPrice": "8", "formattedSpecialPrice": "$8.00", "minimumPrice": "8", "formattedMinimumPrice": "$8.00", "maximumPrice": "8", "formattedMaximumPrice": "$8.00", "regularMinimumPrice": "10", "formattedRegularMinimumPrice": "$10.00", "regularMaximumPrice": "10", "formattedRegularMaximumPrice": "$10.00" } }, { "node": { "id": "/api/shop/products/114", "name": "Nike Shoes", "sku": "Nike-Shoes", "price": "200", "formattedPrice": "$200.00", "specialPrice": "123", "formattedSpecialPrice": "$123.00", "minimumPrice": "123", "formattedMinimumPrice": "$123.00", "maximumPrice": "123", "formattedMaximumPrice": "$123.00", "regularMinimumPrice": "200", "formattedRegularMinimumPrice": "$200.00", "regularMaximumPrice": "200", "formattedRegularMaximumPrice": "$200.00" } } ] } } } - id: get-products-type-simple title: Get Products - Simple Type description: Retrieve all simple products. Simple products have no variants and include pricing, images, attributes, and categories. query: | query getAllSimpleProducts { products(filter: "{\"type\": \"simple\"}") { edges { node { id name sku urlKey description shortDescription price specialPrice images(first: 5) { edges { node { id publicPath position } } } attributeValues { edges { node { value attribute { code adminName } } } } categories { edges { node { id translation { name } } } } } } totalCount } } variables: | {} response: | { "data": { "products": { "edges": [ { "node": { "id": "/api/shop/products/1", "name": "Coastal Breeze Men's Blue Zipper Hoodie", "sku": "COASTALBREEZEMENSHOODIE", "urlKey": "coastal-breeze-mens-blue-zipper-hoodie", "description": "The Coastal Breeze Men's Blue Zipper Hoodie is your reliable companion for staying warm, comfortable, and stylish...", "shortDescription": "Stay warm and stylish with the Coastal Breeze Men's Blue Zipper Hoodie. This fashionable hoodie features a modern design, making it ideal for casual and active wear.", "price": "100", "specialPrice": null, "images": { "edges": [ { "node": { "id": "/api/admin/images/967", "publicPath": "https://api-demo.bagisto.com/storage/product/1/zKcWZTLDjcawJmaNg8g1cpARqwVONgEKEflabstT.webp", "position": "1" } } ] }, "attributeValues": { "edges": [ { "node": { "value": "COASTALBREEZEMENSHOODIE", "attribute": { "code": "sku", "adminName": "SKU" } } }, { "node": { "value": "Coastal Breeze Men's Blue Zipper Hoodie", "attribute": { "code": "name", "adminName": "Name" } } }, { "node": { "value": "coastal-breeze-mens-blue-zipper-hoodie", "attribute": { "code": "url_key", "adminName": "URL Key" } } }, { "node": { "value": "1", "attribute": { "code": "manage_stock", "adminName": "Manage Stock" } } } ] }, "categories": { "edges": [] } } }, { "node": { "id": "/api/shop/products/2", "name": "PureStride Men's Classic White Sneakers", "sku": "PUREWHTSNEAK2023", "urlKey": "purestride-mens-classic-white-sneakers", "description": "Introducing PureStride Men's Classic White Sneakers, a perfect blend of style, comfort, and versatility...", "shortDescription": "Step into timeless style and comfort with our PureStride Men's Classic White Sneakers.", "price": "189", "specialPrice": null, "images": { "edges": [ { "node": { "id": "/api/admin/images/969", "publicPath": "https://api-demo.bagisto.com/storage/product/2/XmdfIafCjuYEhHiBkHvzmOuDT0mpGHDTi9QhnUoY.webp", "position": "1" } } ] }, "attributeValues": { "edges": [ { "node": { "value": "PUREWHTSNEAK2023", "attribute": { "code": "sku", "adminName": "SKU" } } }, { "node": { "value": "PureStride Men's Classic White Sneakers", "attribute": { "code": "name", "adminName": "Name" } } } ] }, "categories": { "edges": [] } } }, { "node": { "id": "/api/shop/products/92", "name": "Bagisto Sticker", "sku": "bagisto-sticker", "urlKey": "bagisto-sticker", "description": "A collectible Bagisto branded sticker, perfect for laptops and notebooks.", "shortDescription": "Bagisto branded collectible sticker.", "price": "10", "specialPrice": "8", "images": { "edges": [ { "node": { "id": "/api/admin/images/965", "publicPath": "https://api-demo.bagisto.com/storage/product/92/JUvPvPCFeYnjPdVOr1HThLzHptZ7BZLp2RQPnSG5.webp", "position": "1" } } ] }, "attributeValues": { "edges": [ { "node": { "value": "bagisto-sticker", "attribute": { "code": "sku", "adminName": "SKU" } } }, { "node": { "value": "Bagisto Sticker", "attribute": { "code": "name", "adminName": "Name" } } } ] }, "categories": { "edges": [] } } }, { "node": { "id": "/api/shop/products/91", "name": "Bagisto Keyboard", "sku": "Bagisto-keyboard", "urlKey": "bagisto-keyboard", "description": "A mechanical keyboard designed for productivity and comfort.", "shortDescription": "Mechanical keyboard for everyday computing.", "price": "20", "specialPrice": null, "images": { "edges": [ { "node": { "id": "/api/admin/images/964", "publicPath": "https://api-demo.bagisto.com/storage/product/91/E9jCNFXrqr2PYYLKIXUbjFRBURpkYms3MWKJRIba.webp", "position": "1" } } ] }, "attributeValues": { "edges": [ { "node": { "value": "Bagisto-keyboard", "attribute": { "code": "sku", "adminName": "SKU" } } }, { "node": { "value": "Bagisto Keyboard", "attribute": { "code": "name", "adminName": "Name" } } } ] }, "categories": { "edges": [ { "node": { "id": "/api/shop/categories/8", "translation": { "name": "Electronics" } } } ] } } } ], "totalCount": 45 } } } commonErrors: - error: INVALID_FILTER_FORMAT cause: Filter string is not valid JSON solution: Pass filter as a single-line JSON string with escaped quotes e.g. "{\"type\":\"simple\"}" - id: get-products-type-configurable title: Get Products - Configurable Type description: Retrieve all configurable products. Configurable products have selectable variants (e.g. size, color) and include variants, combinations, and superAttributeOptions. query: | query getAllConfigurableProducts { products(filter: "{\"type\": \"configurable\"}") { edges { node { id name sku type combinations superAttributeOptions variants { edges { node { id name sku price attributeValues { edges { node { value attribute { code adminName } } } } } } } urlKey description shortDescription minimumPrice images(first: 5) { edges { node { id publicPath position } } } attributeValues { edges { node { value attribute { code adminName } } } } categories { edges { node { id translation { name } } } } } } totalCount } } variables: | {} response: | { "data": { "products": { "edges": [ { "node": { "id": "/api/shop/products/123", "name": "Zoe Tank", "sku": "ZOE-TANK-001", "type": "configurable", "combinations": "{\"124\":{\"color\":1,\"size\":6},\"125\":{\"color\":2,\"size\":6},\"129\":{\"color\":1,\"size\":7},\"132\":{\"color\":2,\"size\":7}}", "superAttributeOptions": "[{\"id\":23,\"code\":\"color\",\"label\":\"Color\",\"options\":[{\"id\":1,\"label\":\"Red\"},{\"id\":2,\"label\":\"Green\"}]},{\"id\":24,\"code\":\"size\",\"label\":\"Size\",\"options\":[{\"id\":6,\"label\":\"S\"},{\"id\":7,\"label\":\"M\"}]}]", "variants": { "edges": [ { "node": { "id": "/api/shop/products/124", "name": "Zoe Tank - Red / S", "sku": "ZOE-TANK-RED-S", "price": "2040", "attributeValues": { "edges": [ { "node": { "value": "ZOE-TANK-RED-S", "attribute": { "code": "sku", "adminName": "SKU" } } }, { "node": { "value": "Zoe Tank - Red / S", "attribute": { "code": "name", "adminName": "Name" } } } ] } } }, { "node": { "id": "/api/shop/products/129", "name": "Zoe Tank - Red / M", "sku": "ZOE-TANK-RED-M", "price": "2040", "attributeValues": { "edges": [ { "node": { "value": "ZOE-TANK-RED-M", "attribute": { "code": "sku", "adminName": "SKU" } } }, { "node": { "value": "Zoe Tank - Red / M", "attribute": { "code": "name", "adminName": "Name" } } } ] } } } ] }, "urlKey": "zoe-tank", "description": "A stylish tank top available in multiple colors and sizes, perfect for layering or wearing solo.", "shortDescription": "Stylish tank top with color and size options.", "minimumPrice": "2040", "images": { "edges": [ { "node": { "id": "/api/admin/images/50", "publicPath": "https://api-demo.bagisto.com/storage/product/123/zoe-tank.webp", "position": "1" } } ] }, "attributeValues": { "edges": [ { "node": { "value": "ZOE-TANK-001", "attribute": { "code": "sku", "adminName": "SKU" } } }, { "node": { "value": "Zoe Tank", "attribute": { "code": "name", "adminName": "Name" } } }, { "node": { "value": "zoe-tank", "attribute": { "code": "url_key", "adminName": "URL Key" } } } ] }, "categories": { "edges": [] } } }, { "node": { "id": "/api/shop/products/2495", "name": "Ivory Frost Classic Overcoat", "sku": "IVORY-OVERCOAT-001", "type": "configurable", "combinations": "{\"2496\":{\"color\":19,\"size\":6},\"2497\":{\"color\":19,\"size\":7},\"2498\":{\"color\":20,\"size\":6},\"2499\":{\"color\":20,\"size\":7}}", "superAttributeOptions": "[{\"id\":23,\"code\":\"color\",\"label\":\"Color\",\"options\":[{\"id\":19,\"label\":\"Blue\"},{\"id\":20,\"label\":\"Ash grey\"}]},{\"id\":24,\"code\":\"size\",\"label\":\"Size\",\"options\":[{\"id\":6,\"label\":\"S\"},{\"id\":7,\"label\":\"M\"}]}]", "variants": { "edges": [ { "node": { "id": "/api/shop/products/2496", "name": "Ivory Frost Classic Overcoat - Blue / S", "sku": "IVORY-OVERCOAT-BLUE-S", "price": "500", "attributeValues": { "edges": [ { "node": { "value": "IVORY-OVERCOAT-BLUE-S", "attribute": { "code": "sku", "adminName": "SKU" } } }, { "node": { "value": "Ivory Frost Classic Overcoat - Blue / S", "attribute": { "code": "name", "adminName": "Name" } } } ] } } } ] }, "urlKey": "ivory-frost-classic-overcoat", "description": "The Ivory Frost Classic Overcoat blends modern simplicity with timeless winter design. Crafted in a smooth, insulating fabric, it offers dependable warmth while maintaining a lightweight, structured feel.", "shortDescription": "A sleek ivory overcoat with a tailored fit and soft warmth.", "minimumPrice": "500", "images": { "edges": [ { "node": { "id": "/api/admin/images/950", "publicPath": "https://api-demo.bagisto.com/storage/product/2495/FFHxE9HE2Ezt9aqvr6s3fPPCc1nrjwMNna1o1wTQ.webp", "position": "1" } } ] }, "attributeValues": { "edges": [ { "node": { "value": "IVORY-OVERCOAT-001", "attribute": { "code": "sku", "adminName": "SKU" } } }, { "node": { "value": "Ivory Frost Classic Overcoat", "attribute": { "code": "name", "adminName": "Name" } } } ] }, "categories": { "edges": [] } } } ], "totalCount": 12 } } } commonErrors: - error: INVALID_FILTER_FORMAT cause: Filter string is not valid JSON solution: Pass filter as a single-line JSON string with escaped quotes - id: get-products-type-booking title: Get Products - Booking Type description: Retrieve all booking products. Booking products are time-slot or appointment-based and include bookingProducts connection with availability details. query: | query getAllBookingProducts { products(filter: "{\"type\": \"booking\"}") { edges { node { id name sku type urlKey description shortDescription price specialPrice bookingProducts { edges { node { id type qty location showLocation availableEveryWeek availableFrom availableTo createdAt updatedAt } } } images(first: 5) { edges { node { id publicPath position } } } categories { edges { node { id translation { name } } } } } } totalCount } } variables: | {} response: | { "data": { "products": { "edges": [ { "node": { "id": "/api/shop/products/2507", "name": "Professional Photography Session", "sku": "PROFESSIONAL-PHOTOGRAPHY-SESSION", "type": "booking", "urlKey": "professional-photography-session", "description": "Capture high-quality photos with a professional photography session. Suitable for portraits, events, and product shoots.", "shortDescription": "Book a professional photography session for personal or commercial use.", "price": "100", "specialPrice": "99", "bookingProducts": { "edges": [ { "node": { "id": "/api/shop/booking-products/1", "type": "default", "qty": 150, "location": "Noida, Uttar Pradesh", "showLocation": "0", "availableEveryWeek": null, "availableFrom": "2026-04-06T06:30:00.000000Z", "availableTo": "2026-12-31T06:30:00.000000Z", "createdAt": "2026-04-02T13:24:30.000000Z", "updatedAt": "2026-04-06T10:09:47.000000Z" } } ] }, "images": { "edges": [ { "node": { "id": "/api/admin/images/850", "publicPath": "https://api-demo.bagisto.com/storage/product/2507/1jO3Pb5UA89ZaVsp1cnlICSFgZKlwy6lPlDJynGu.webp", "position": "1" } } ] }, "categories": { "edges": [] } } }, { "node": { "id": "/api/shop/products/2508", "name": "Live Music Concert Ticket", "sku": "LIVE-MUSIC-CONCERT-TICKET", "type": "booking", "urlKey": "live-music-concert-ticket", "description": "Enjoy a live music concert featuring top artists and an energetic crowd.", "shortDescription": "Book tickets for an exciting live music concert experience.", "price": "120", "specialPrice": null, "bookingProducts": { "edges": [ { "node": { "id": "/api/shop/booking-products/2", "type": "event", "qty": 0, "location": "Noida, Uttar Pradesh", "showLocation": "0", "availableEveryWeek": null, "availableFrom": "2026-04-06T06:30:00.000000Z", "availableTo": "2026-04-30T06:30:00.000000Z", "createdAt": "2026-04-02T13:41:33.000000Z", "updatedAt": "2026-04-06T10:01:47.000000Z" } } ] }, "images": { "edges": [ { "node": { "id": "/api/admin/images/846", "publicPath": "https://api-demo.bagisto.com/storage/product/2508/qXYifNamNZcymBWoGmuh3cauzyujPl23mMH1XYPt.webp", "position": "1" } } ] }, "categories": { "edges": [] } } }, { "node": { "id": "/api/shop/products/2509", "name": "Men's Haircut Appointment", "sku": "SALON-HAIRCUT-APPOINTMENT", "type": "booking", "urlKey": "mens-haircut-appointment", "description": "Schedule a haircut with experienced stylists. Choose your preferred date and time slot.", "shortDescription": "Book a professional haircut appointment with flexible time slots.", "price": "60", "specialPrice": "55", "bookingProducts": { "edges": [ { "node": { "id": "/api/shop/booking-products/3", "type": "appointment", "qty": 10, "location": "Noida, Uttar Pradesh", "showLocation": "0", "availableEveryWeek": "0", "availableFrom": "2026-04-06T06:30:00.000000Z", "availableTo": "2026-12-30T06:30:00.000000Z", "createdAt": "2026-04-02T13:52:29.000000Z", "updatedAt": "2026-04-06T10:06:42.000000Z" } } ] }, "images": { "edges": [ { "node": { "id": "/api/admin/images/847", "publicPath": "https://api-demo.bagisto.com/storage/product/2509/JaLDOwXAOLJCecJs7hlPwEiDr2G42WlHIjJxFdxF.webp", "position": "1" } } ] }, "categories": { "edges": [] } } }, { "node": { "id": "/api/shop/products/2510", "name": "Wooden Folding Chair Rental", "sku": "WOODEN-FOLDING-CHAIR-RENTAL", "type": "booking", "urlKey": "wooden-folding-chair-rental", "description": "High-quality wooden folding chairs available for daily rental. Ideal for weddings, parties, and corporate events.", "shortDescription": "Rent durable wooden folding chairs for events and gatherings.", "price": "109", "specialPrice": "99", "bookingProducts": { "edges": [ { "node": { "id": "/api/shop/booking-products/4", "type": "rental", "qty": 150, "location": "Noida, Uttar Pradesh", "showLocation": "0", "availableEveryWeek": "0", "availableFrom": "2026-04-06T06:30:00.000000Z", "availableTo": "2026-04-30T06:30:00.000000Z", "createdAt": "2026-04-02T14:02:30.000000Z", "updatedAt": "2026-04-06T10:02:29.000000Z" } } ] }, "images": { "edges": [ { "node": { "id": "/api/admin/images/848", "publicPath": "https://api-demo.bagisto.com/storage/product/2510/eDIAyoJLDAshEe3AOwhi2sgoFH9sAjpMvoemDVpo.webp", "position": "1" } } ] }, "categories": { "edges": [] } } }, { "node": { "id": "/api/shop/products/2511", "name": "Fine Dining Table Reservation", "sku": "FINE-DINING-TABLE-RESERVATION", "type": "booking", "urlKey": "fine-dining-table-reservation", "description": "Book a table for a fine dining experience with comfortable seating and a premium ambiance.", "shortDescription": "Reserve a table at a premium dining restaurant.", "price": "200", "specialPrice": "195", "bookingProducts": { "edges": [ { "node": { "id": "/api/shop/booking-products/5", "type": "table", "qty": 5, "location": "Mumbai, Maharashtra", "showLocation": "0", "availableEveryWeek": "0", "availableFrom": "2026-04-06T06:30:00.000000Z", "availableTo": "2026-04-30T06:30:00.000000Z", "createdAt": "2026-04-02T14:10:57.000000Z", "updatedAt": "2026-04-06T10:06:05.000000Z" } } ] }, "images": { "edges": [ { "node": { "id": "/api/admin/images/849", "publicPath": "https://api-demo.bagisto.com/storage/product/2511/lw253CbVba9nRZVUGy9atW9t85ADE2UwldssE8t6.webp", "position": "1" } } ] }, "categories": { "edges": [] } } } ], "totalCount": 5 } } } commonErrors: - error: INVALID_FILTER_FORMAT cause: Filter string is not valid JSON solution: Pass filter as a single-line JSON string with escaped quotes - id: get-products-type-virtual title: Get Products - Virtual Type description: Retrieve all virtual products. Virtual products are non-physical items (e.g. services, warranties) that require no shipping. Structurally similar to simple products. query: | query getAllVirtualProducts { products(filter: "{\"type\": \"virtual\"}") { edges { node { id name sku type urlKey description shortDescription price specialPrice images(first: 5) { edges { node { id publicPath position } } } attributeValues { edges { node { value attribute { code adminName } } } } categories { edges { node { id translation { name } } } } } } totalCount } } variables: | {} response: | { "data": { "products": { "edges": [ { "node": { "id": "/api/shop/products/2505", "name": "HD Streaming Subscription - 1 Month Access", "sku": "HD-STREAMING-SUBSCRIPTION-1-MONTH", "type": "virtual", "urlKey": "hd-streaming-subscription-1-month-access", "description": "This 1-month HD streaming subscription gives you unlimited access to a wide range of movies, TV series, and exclusive content across multiple genres.", "shortDescription": "Enjoy unlimited access to movies and TV shows with a 1-month HD streaming plan.", "price": "64", "specialPrice": "59", "images": { "edges": [ { "node": { "id": "/api/admin/images/842", "publicPath": "https://api-demo.bagisto.com/storage/product/2505/sCwS1QRNlJHLPjw5UzxYSR21oqYbMvo4UNRYklME.webp", "position": "1" } } ] }, "attributeValues": { "edges": [ { "node": { "value": "HD-STREAMING-SUBSCRIPTION-1-MONTH", "attribute": { "code": "sku", "adminName": "SKU" } } }, { "node": { "value": "HD Streaming Subscription - 1 Month Access", "attribute": { "code": "name", "adminName": "Name" } } } ] }, "categories": { "edges": [] } } } ], "totalCount": 1 } } } commonErrors: - error: INVALID_FILTER_FORMAT cause: Filter string is not valid JSON solution: Pass filter as a single-line JSON string with escaped quotes - id: get-products-type-grouped title: Get Products - Grouped Type description: Retrieve all grouped products. Grouped products are a collection of simple products sold together. Includes groupedProducts connection with associated product details. query: | query getAllGroupedProducts { products(filter: "{\"type\": \"grouped\"}") { edges { node { id name sku type urlKey description shortDescription groupedProducts { edges { node { id qty sortOrder associatedProduct { id name sku price specialPrice images(first: 3) { edges { node { id publicPath } } } } } } } images(first: 5) { edges { node { id publicPath position } } } categories { edges { node { id translation { name } } } } } } totalCount } } variables: | {} response: | { "data": { "products": { "edges": [ { "node": { "id": "/api/shop/products/2516", "name": "Arctic Frost Winter Accessories", "sku": "GP-001", "type": "grouped", "urlKey": "arctic-frost-winter-accessories", "description": "A thoughtfully curated set of essential winter accessories including a beanie, gloves, and socks.", "shortDescription": "Curated winter accessories set with beanie, gloves, and socks.", "groupedProducts": { "edges": [ { "node": { "id": "/api/shop/product_grouped_products/1", "qty": 1, "sortOrder": 0, "associatedProduct": { "id": "/api/shop/products/2512", "name": "Arctic Cozy Knit Unisex Beanie", "sku": "SP-001", "price": "14", "specialPrice": null, "images": { "edges": [ { "node": { "id": "/api/admin/images/851", "publicPath": "https://api-demo.bagisto.com/storage/product/2512/Muc0qeWks34MTZaxf38s6DBmfqMqrCxku81Uo8EB.webp" } } ] } } } }, { "node": { "id": "/api/shop/product_grouped_products/2", "qty": 1, "sortOrder": 1, "associatedProduct": { "id": "/api/shop/products/2514", "name": "Arctic Touchscreen Winter Gloves", "sku": "SP-003", "price": "21", "specialPrice": "17", "images": { "edges": [ { "node": { "id": "/api/admin/images/853", "publicPath": "https://api-demo.bagisto.com/storage/product/2514/g8lR0Ity8HcpE20A4yAkX5wvLY5RlTC67NJKyyg6.webp" } } ] } } } }, { "node": { "id": "/api/shop/product_grouped_products/3", "qty": 1, "sortOrder": 2, "associatedProduct": { "id": "/api/shop/products/2515", "name": "Arctic Warmth Wool Blend Socks", "sku": "SP-004", "price": "21", "specialPrice": null, "images": { "edges": [ { "node": { "id": "/api/admin/images/854", "publicPath": "https://api-demo.bagisto.com/storage/product/2515/442ouyaT1K4weKSZGhSDtSKDBbrhiH0aWWwGcFW0.webp" } } ] } } } } ] }, "images": { "edges": [ { "node": { "id": "/api/admin/images/855", "publicPath": "https://api-demo.bagisto.com/storage/product/2516/5Kgto6KVm6FLMaaDEY6pwCcVoTIhX03D3OGDzwbf.webp", "position": "1" } } ] }, "categories": { "edges": [] } } } ], "totalCount": 1 } } } commonErrors: - error: INVALID_FILTER_FORMAT cause: Filter string is not valid JSON solution: Pass filter as a single-line JSON string with escaped quotes - id: get-products-type-downloadable title: Get Products - Downloadable Type description: Retrieve all downloadable products. Downloadable products are digital items with file links and optional samples. Includes downloadableLinks and downloadableSamples connections. query: | query getAllDownloadableProducts { products(filter: "{\"type\": \"downloadable\"}") { edges { node { id name sku type urlKey description shortDescription price specialPrice downloadableLinks { edges { node { id type price downloads sortOrder fileUrl sampleFileUrl translation { title } } } } downloadableSamples { edges { node { id type fileUrl sortOrder translation { title } } } } images(first: 5) { edges { node { id publicPath position } } } attributeValues { edges { node { value attribute { code adminName } } } } categories { edges { node { id translation { name } } } } } } totalCount } } variables: | {} response: | { "data": { "products": { "edges": [ { "node": { "id": "/api/shop/products/2506", "name": "Complete Personal Finance Guide (eBook PDF)", "sku": "COMPLETE-PERSONAL-FINANCE-GUIDE-EBOOK", "type": "downloadable", "urlKey": "complete-personal-finance-guide-ebook-pdf", "description": "This comprehensive personal finance eBook helps you take control of your money with practical strategies for budgeting, saving, investing, and debt management.", "shortDescription": "Download a practical guide to managing money, saving, and building long-term wealth.", "price": "70", "specialPrice": "69", "downloadableLinks": { "edges": [ { "node": { "id": "/api/shop/product-downloadable-links/2", "type": "url", "price": "69", "downloads": 10, "sortOrder": 0, "fileUrl": "https://api-demo.bagisto.com/storage/", "sampleFileUrl": "https://api-demo.bagisto.com/api/downloadable/download-sample/link/2", "translation": { "title": "Full eBook PDF" } } } ] }, "downloadableSamples": { "edges": [ { "node": { "id": "/api/shop/product-downloadable-samples/1", "type": "file", "fileUrl": "https://api-demo.bagisto.com/api/downloadable/download-sample/sample/1", "sortOrder": 0, "translation": { "title": "" } } } ] }, "images": { "edges": [ { "node": { "id": "/api/admin/images/843", "publicPath": "https://api-demo.bagisto.com/storage/product/2506/XY0sCaNbWfeXDntNFbYnlL6N5uOJ9tfyR7AtntSf.webp", "position": "1" } } ] }, "attributeValues": { "edges": [ { "node": { "value": "COMPLETE-PERSONAL-FINANCE-GUIDE-EBOOK", "attribute": { "code": "sku", "adminName": "SKU" } } }, { "node": { "value": "Complete Personal Finance Guide (eBook PDF)", "attribute": { "code": "name", "adminName": "Name" } } } ] }, "categories": { "edges": [] } } } ], "totalCount": 1 } } } commonErrors: - error: INVALID_FILTER_FORMAT cause: Filter string is not valid JSON solution: Pass filter as a single-line JSON string with escaped quotes - id: get-products-type-bundle title: Get Products - Bundle Type description: Retrieve all bundle products. Bundle products are composed of selectable options where customers can choose individual components. Includes bundleOptions and bundleOptionProducts connections. query: | query getAllBundleProducts { products(filter: "{\"type\": \"bundle\"}") { edges { node { id name sku type urlKey description shortDescription minimumPrice bundleOptions { edges { node { id type isRequired sortOrder translation { label } bundleOptionProducts { edges { node { id qty isDefault isUserDefined sortOrder product { id name sku price images(first: 3) { edges { node { id publicPath } } } } } } } } } } images(first: 5) { edges { node { id publicPath position } } } categories { edges { node { id translation { name } } } } } } totalCount } } variables: | {} response: | { "data": { "products": { "edges": [ { "node": { "id": "/api/shop/products/2517", "name": "Arctic Frost Winter Accessories Bundle", "sku": "BP-001", "type": "bundle", "urlKey": "arctic-frost-winter-accessories-bundle", "description": "A curated bundle of essential winter accessories. Choose from beanies, scarves, gloves, and socks to build your perfect winter kit.", "shortDescription": "Build your perfect winter accessories bundle with beanie, scarf, gloves, and socks.", "minimumPrice": "69", "bundleOptions": { "edges": [ { "node": { "id": "/api/shop/product_bundle_options/1", "type": "radio", "isRequired": "1", "sortOrder": 0, "translation": { "label": "Bundle Option 1" }, "bundleOptionProducts": { "edges": [ { "node": { "id": "/api/shop/product-bundle-option-products/1", "qty": 1, "isDefault": "1", "isUserDefined": "1", "sortOrder": 0, "product": { "id": "/api/shop/products/2512", "name": "Arctic Cozy Knit Unisex Beanie", "sku": "SP-001", "price": "14", "images": { "edges": [ { "node": { "id": "/api/admin/images/851", "publicPath": "https://api-demo.bagisto.com/storage/product/2512/Muc0qeWks34MTZaxf38s6DBmfqMqrCxku81Uo8EB.webp" } } ] } } } } ] } } }, { "node": { "id": "/api/shop/product_bundle_options/3", "type": "checkbox", "isRequired": "1", "sortOrder": 2, "translation": { "label": "Bundle Option 2" }, "bundleOptionProducts": { "edges": [ { "node": { "id": "/api/shop/product-bundle-option-products/3", "qty": 1, "isDefault": "1", "isUserDefined": "1", "sortOrder": 0, "product": { "id": "/api/shop/products/2514", "name": "Arctic Touchscreen Winter Gloves", "sku": "SP-003", "price": "21", "images": { "edges": [ { "node": { "id": "/api/admin/images/853", "publicPath": "https://api-demo.bagisto.com/storage/product/2514/g8lR0Ity8HcpE20A4yAkX5wvLY5RlTC67NJKyyg6.webp" } } ] } } } } ] } } } ] }, "images": { "edges": [ { "node": { "id": "/api/admin/images/856", "publicPath": "https://api-demo.bagisto.com/storage/product/2517/lW2A3FH3oKBJnnukyUyKUArdrr8dwTJxxDKthSgq.webp", "position": "1" } } ] }, "categories": { "edges": [] } } } ], "totalCount": 1 } } } commonErrors: - error: INVALID_FILTER_FORMAT cause: Filter string is not valid JSON solution: Pass filter as a single-line JSON string with escaped quotes --- # Get Products ## About The `getProducts` query retrieves a paginated list of products from your store with support for advanced sorting and filtering. This query is essential for: - Building product catalog browsing interfaces - Implementing product search, sorting, and filtering experiences - Creating product recommendation systems - Syncing product data with external systems The query supports cursor-based pagination to efficiently handle large product catalogs and includes metadata for: - Basic product information (name, SKU, description, vendor) - Pricing and inventory details - Product images and media - Categories, tags, and custom attributes - Publication and availability status - Created and updated timestamps > **Currency & Formatted Prices:** All price fields reflect the active currency set via the `X-Currency` header — both numeric fields (e.g. `price`, `specialPrice`, `minimumPrice`) and formatted fields (e.g. `formattedPrice`, `formattedMinimumPrice`) return converted values. The difference is that numeric fields return the converted amount as a number, while formatted fields return the converted amount as a string with the currency symbol prefixed (e.g. `"€84.99"`). See the "Get Products with Currency Formatted Prices" dropdown example above for all available price fields. ## Wishlist & Compare Flags Every product in the list carries two per-customer boolean flags so you can render the wishlist and compare icon states directly from the catalog response: | Field | Description | |-------|-------------| | `isInWishlist` | Whether this product is in the signed-in customer's **wishlist** for the active channel. | | `isInCompare` | Whether this product is in the signed-in customer's **compare list**. | Why they exist: the wishlist and compare lists are their own endpoints and paginate independently of the catalog — a product on catalog page 1 may have its wishlist entry on a different wishlist page, so matching the two lists on the client is unreliable. These flags answer the question per product, in the same response, so the wishlist/compare icon can be highlighted without any extra requests. Things to know: - **Authentication is required.** Send the customer Bearer token. For guests (no token) both flags are always `0`. - **The flags are `0` / `1`** — `1` when the product is in the list, `0` when it is not. Over GraphQL they are returned as the strings `"1"` / `"0"` (the REST API returns them as `1` / `0` integers). Either way, `0` is falsy and `1` is truthy. - A product is considered "in the list" when its own product ID is in the customer's wishlist / compare list — so a configurable parent is flagged when the parent itself was added. See the **"Get Products with Wishlist & Compare Flags"** dropdown example above. ## Arguments | Argument | Type | Description | |----------|------|-------------| | `first` | `Int` | The number of products to return per page (max: 250). Used for forward pagination. | | `after` | `String` | The cursor of the product to start after. Used with `first` for pagination. | | `last` | `Int` | The number of products to return in reverse (max: 250). Used for backward pagination. | | `before` | `String` | The cursor to start before. Used with `last` for reverse pagination. | | `sortKey` | `ProductSortKeys` | Field to sort by: `TITLE`, `PRICE`, `CREATED_AT`, `UPDATED_AT`. Default: `TITLE` | | `reverse` | `Boolean` | Reverse the sort order. Default: `false` | | `query` | `String` | Search query string for filtering products. Supports advanced search syntax. | | `filter` | `String` | JSON string for filtering by type, category, attributes, or price. See examples below. | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `edges` | `[ProductEdge!]!` | Array of edges containing products and cursors. Each edge represents a connection between nodes. | | `edges.node` | `Product!` | The actual product object containing id, name, sku, price, and other product fields. | | `edges.cursor` | `String!` | Pagination cursor for this product. Use with `after` or `before` arguments. | | `nodes` | `[Product!]!` | Flattened array of products without edge information. | | `pageInfo` | `PageInfo!` | Pagination metadata object. | | `pageInfo.hasNextPage` | `Boolean!` | Whether there are more products after the current page. | | `pageInfo.hasPreviousPage` | `Boolean!` | Whether there are products before the current page. | | `pageInfo.startCursor` | `String` | Cursor of the first product on the current page. | | `pageInfo.endCursor` | `String` | Cursor of the last product on the current page. | | `totalCount` | `Int!` | Total number of products matching the query criteria. | ## Price Fields | Field | Type | Description | |-------|------|-------------| | `price` | `Float` | Base catalog price. Returns the converted numeric value based on the active currency set via `X-Currency` header. | | `formattedPrice` | `String` | Same as `price` but returned as a string with the currency symbol prefixed (e.g. `"€84.99"`). | | `specialPrice` | `Float` | Discounted price if a special price is set, otherwise `null`. Reflects currency conversion. | | `formattedSpecialPrice` | `String` | Same as `specialPrice` but with the currency symbol prefixed. | | `minimumPrice` | `Float` | The lowest effective price — accounts for special price and configurable variant pricing. Used for price sorting. Reflects currency conversion. | | `formattedMinimumPrice` | `String` | Same as `minimumPrice` but with the currency symbol prefixed. | | `maximumPrice` | `Float` | The highest effective price across all variants or configurations. Reflects currency conversion. | | `formattedMaximumPrice` | `String` | Same as `maximumPrice` but with the currency symbol prefixed. | | `regularMinimumPrice` | `Float` | The regular (non-discounted) minimum price before any special price is applied. Reflects currency conversion. | | `formattedRegularMinimumPrice` | `String` | Same as `regularMinimumPrice` but with the currency symbol prefixed. | | `regularMaximumPrice` | `Float` | The regular (non-discounted) maximum price before any special price is applied. Reflects currency conversion. | | `formattedRegularMaximumPrice` | `String` | Same as `regularMaximumPrice` but with the currency symbol prefixed. | > The difference between numeric and formatted price fields is purely presentational: numeric fields (e.g. `price`) return the converted amount as a number, while formatted fields (e.g. `formattedPrice`) return the same converted amount as a string with the currency symbol (e.g. `"€84.99"`). Both reflect the active currency set via the `X-Currency` header. ## Product Types Use the `filter` argument with `"type"` to fetch products of a specific kind. The filter value must be a single-line JSON string with escaped quotes. | Type | Filter Value | Key Fields | |------|-------------|------------| | Simple | `"{\"type\": \"simple\"}"` | `price`, `specialPrice`, `images`, `attributeValues` | | Configurable | `"{\"type\": \"configurable\"}"` | `variants`, `combinations`, `superAttributeOptions` | | Booking | `"{\"type\": \"booking\"}"` | `bookingProducts` (type, qty, location, availability) | | Virtual | `"{\"type\": \"virtual\"}"` | `price`, `specialPrice`, `attributeValues` | | Grouped | `"{\"type\": \"grouped\"}"` | `groupedProducts` → `associatedProduct` | | Downloadable | `"{\"type\": \"downloadable\"}"` | `downloadableLinks`, `downloadableSamples` | | Bundle | `"{\"type\": \"bundle\"}"` | `bundleOptions` → `bundleOptionProducts` → `product` | --- # Get Shipping Methods URL: /api/graphql-api/shop/queries/get-shipping-methods --- outline: false examples: - id: get-shipping-methods title: Get Shipping Methods description: Retrieve available shipping methods for checkout. query: | query checkoutShippingRates { collectionShippingRates { _id id code label method description price formattedPrice } } response: | { "data": { "collectionShippingRates": [ { "_id": "flatrate_flatrate_flatrate", "id": "/api/.well-known/genid/f14f85f51f8e3efd572e", "code": "flatrate", "label": "Flat Rate", "method": "flatrate_flatrate", "description": "Flat Rate Shipping", "price": 20, "formattedPrice": "$20.00" }, { "_id": "free_free_free", "id": "/api/.well-known/genid/f14f85f51f8e3efd572e", "code": "free", "label": "Free Shipping", "method": "free_free", "description": "Free Shipping", "price": 0, "formattedPrice": "$0.00" } ] } } --- # Get Shipping Methods Retrieve available shipping methods for a cart during checkout. ## Authentication This query supports both authenticated customers and guest users: - **Authenticated customers**: Provide a valid customer authentication token in the `Authorization` header. Obtain this token via the [Customer Login API](/api/graphql-api/shop/mutations/customer-login). - **Guest users**: Provide the Guest Cart Token `cartToken` obtained from the [Create Cart mutation](/api/graphql-api/shop/mutations/create-cart). ``` Authorization: Bearer ``` ## Response Fields | Field | Type | Description | |-------|------|-------------| | `_id` | String | Internal shipping method identifier | | `id` | String | API resource identifier | | `code` | String | Shipping method code (used for selection) | | `description` | String | Human-readable method description | | `method` | String | Shipping method identifier | | `price` | Float | Shipping cost amount | | `formattedPrice` | String | Shipping cost formatted with currency symbol (e.g. `$20.00`) | | `label` | String | Display label for the shipping method | ## Prerequisites - Cart must exist - Shipping address must be set - Available methods depend on: - Cart items - Shipping address - Store configuration ## Common Methods | Code | Method | Label | Price | Description | |------|--------|-------|-------|-------------| | `flatrate` | `flatrate_flatrate` | Flat Rate | 20 | Fixed shipping cost | | `free` | `free_free` | Free Shipping | 0 | No shipping charge | ## Use Cases - Display shipping options during checkout - Calculate total with shipping - Allow customer selection - Show shipping cost estimates ## Error Responses ```json { "errors": { "cartId": ["Cart not found."], "shippingAddress": ["Shipping address must be set first."] } } ``` ## Related Documentation - [Get Cart](/api/graphql-api/shop/queries/get-cart) - [Set Checkout Address](/api/graphql-api/shop/mutations/set-billing-address) - [Set Shipping Method](/api/graphql-api/shop/mutations/set-shipping-method) --- # Get Wishlist Item URL: /api/graphql-api/shop/queries/get-wishlist --- outline: false examples: - id: get-wishlist-by-id title: Get Single Wishlist Item description: Retrieve a specific wishlist item by its IRI. query: | query GetWishlist($id: ID!) { wishlist(id: $id) { id _id product { id name price baseImageUrl } customer { id email } channel { id code } createdAt updatedAt } } variables: | { "id": "/api/shop/wishlists/81" } response: | { "data": { "wishlist": { "id": "/api/shop/wishlists/81", "_id": 81, "product": { "id": "/api/shop/products/122", "name": "Classic Cowboy Hat", "price": "149.99", "baseImageUrl": "https://api-demo.bagisto.com/storage/product/122/P9n1dbmgM4UOBT3zUAEGCn4wpKi0GjPGhgS1jZe7.webp" }, "customer": { "id": "/api/shop/customers/122", "email": "john.doe@example.com" }, "channel": { "id": "/api/shop/channels/1", "code": "default" }, "createdAt": "2026-04-06T18:44:55+05:30", "updatedAt": "2026-04-06T18:44:55+05:30" } } } commonErrors: - error: ITEM_NOT_FOUND cause: Wishlist item ID does not exist or does not belong to the customer solution: Use a valid wishlist item IRI from the wishlists collection query - error: UNAUTHENTICATED cause: Missing or invalid Bearer token solution: Login and provide a valid customer authentication token --- # Get Wishlist Item ## About The `wishlist` query retrieves a single wishlist item by its IRI identifier. Use this query to: - Fetch detailed information about a specific wishlist item - Display wishlist item details on a detail page - Verify a product exists in the customer's wishlist ## Authentication This query requires customer authentication: - **Authenticated customers**: Provide a valid customer authentication token in the `Authorization` header. Obtain this token via the [Customer Login API](/api/graphql-api/shop/mutations/customer-login). ``` Authorization: Bearer ``` ## Arguments | Argument | Type | Description | |----------|------|-------------| | `id` | `ID!` | The IRI of the wishlist item (e.g. `/api/shop/wishlists/69`). | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `id` | `ID!` | IRI identifier (e.g. `/api/shop/wishlists/69`). | | `_id` | `Int!` | Numeric identifier. | | `product` | `Product!` | Associated product with id, name, price, etc. | | `customer` | `Customer!` | Associated customer with id, email. | | `channel` | `Channel!` | Associated channel with id, code. | | `createdAt` | `String` | Timestamp when the item was added. | | `updatedAt` | `String` | Timestamp when the item was last updated. | --- # Get Wishlists URL: /api/graphql-api/shop/queries/get-wishlists --- outline: false examples: - id: get-wishlists-basic title: Get All Wishlist Items description: Retrieve paginated wishlist items for the authenticated customer using cursor-based pagination. query: | query GetAllWishlists($first: Int, $after: String) { wishlists(first: $first, after: $after) { edges { cursor node { id _id product { id name price sku type description baseImageUrl } customer { id email } channel { id code translation { name } } createdAt updatedAt } } pageInfo { endCursor startCursor hasNextPage hasPreviousPage } totalCount } } variables: | { "first": 10, "after": null } response: | { "data": { "wishlists": { "edges": [ { "cursor": "MA==", "node": { "id": "/api/shop/wishlists/78", "_id": 78, "product": { "id": "/api/shop/products/2500", "name": "Mint Axis Unisex Tailored Blazer", "price": "0", "sku": "MINT-BLAZER-001", "type": "configurable", "description": "The Mint Axis Unisex Tailored Blazer is built for those who lead with style, not trends. Featuring a structured yet comfortable silhouette, this blazer balances precision tailoring with a contemporary mint tone.", "baseImageUrl": "https://api-demo.bagisto.com/storage/product/2500/T97yKJVNKlmi6GXoqKl8FNqfM8115Wxo6jw4WhPF.webp" }, "customer": { "id": "/api/shop/customers/122", "email": "john.doe@example.com" }, "channel": { "id": "/api/shop/channels/1", "code": "default", "translation": { "name": "bagisto store" } }, "createdAt": "2026-04-06T18:44:50+05:30", "updatedAt": "2026-04-06T18:44:50+05:30" } }, { "cursor": "MQ==", "node": { "id": "/api/shop/wishlists/79", "_id": 79, "product": { "id": "/api/shop/products/2495", "name": "Ivory Frost Classic Overcoat", "price": "0", "sku": "IVORY-OVERCOAT-001", "type": "configurable", "description": "The Ivory Frost Classic Overcoat blends modern simplicity with timeless winter design. Crafted in a smooth, insulating fabric, it offers dependable warmth while maintaining a lightweight, structured feel.", "baseImageUrl": "https://api-demo.bagisto.com/storage/product/2495/FFHxE9HE2Ezt9aqvr6s3fPPCc1nrjwMNna1o1wTQ.webp" }, "customer": { "id": "/api/shop/customers/122", "email": "john.doe@example.com" }, "channel": { "id": "/api/shop/channels/1", "code": "default", "translation": { "name": "bagisto store" } }, "createdAt": "2026-04-06T18:44:51+05:30", "updatedAt": "2026-04-06T18:44:51+05:30" } }, { "cursor": "Mg==", "node": { "id": "/api/shop/wishlists/80", "_id": 80, "product": { "id": "/api/shop/products/2359", "name": "Horizon Arc 49\" OLED Curved Gaming Monitor", "price": "4000", "sku": "HORIZON-MONITOR-49", "type": "simple", "description": "Lightning-fast 240Hz refresh and 0.03ms GtG response eliminate motion blur, supported by NVIDIA G-Sync and AMD FreeSync Premium Pro for smooth gameplay without stuttering.", "baseImageUrl": "https://api-demo.bagisto.com/storage/product/2359/jTCVFLzBdbUfmJzDnLa6puGF8kRqEk5NNDYPjN09.webp" }, "customer": { "id": "/api/shop/customers/122", "email": "john.doe@example.com" }, "channel": { "id": "/api/shop/channels/1", "code": "default", "translation": { "name": "bagisto store" } }, "createdAt": "2026-04-06T18:44:54+05:30", "updatedAt": "2026-04-06T18:44:54+05:30" } }, { "cursor": "Mw==", "node": { "id": "/api/shop/wishlists/81", "_id": 81, "product": { "id": "/api/shop/products/122", "name": "Classic Cowboy Hat", "price": "149.99", "sku": "COWBOY-HAT-001", "type": "simple", "description": "A timeless cowboy hat crafted from premium materials, perfect for outdoor adventures and western-style fashion.", "baseImageUrl": "https://api-demo.bagisto.com/storage/product/122/P9n1dbmgM4UOBT3zUAEGCn4wpKi0GjPGhgS1jZe7.webp" }, "customer": { "id": "/api/shop/customers/122", "email": "john.doe@example.com" }, "channel": { "id": "/api/shop/channels/1", "code": "default", "translation": { "name": "bagisto store" } }, "createdAt": "2026-04-06T18:44:55+05:30", "updatedAt": "2026-04-06T18:44:55+05:30" } } ], "pageInfo": { "endCursor": "Mw==", "startCursor": "MA==", "hasNextPage": false, "hasPreviousPage": false }, "totalCount": 4 } } } commonErrors: - error: UNAUTHENTICATED cause: Missing or invalid Bearer token solution: Login and provide a valid customer authentication token - id: get-wishlists-paginated title: Get Wishlist Items - Next Page description: Fetch the next page of wishlist items using the endCursor from the previous response. query: | query GetAllWishlists($first: Int, $after: String) { wishlists(first: $first, after: $after) { edges { cursor node { id _id product { id name baseImageUrl } createdAt } } pageInfo { endCursor hasNextPage } totalCount } } variables: | { "first": 10, "after": "MQ==" } response: | { "data": { "wishlists": { "edges": [], "pageInfo": { "endCursor": null, "hasNextPage": false }, "totalCount": 1 } } } commonErrors: - error: INVALID_CURSOR cause: The cursor value is invalid or expired solution: Use a valid cursor from a previous response's pageInfo --- # Get Wishlists ## About The `wishlists` query retrieves a paginated list of the authenticated customer's wishlist items. Use this query to: - Display the customer's wishlist page - Show wishlist item counts in navigation - Build wishlist displays with product details - Implement pagination for large wishlists - Show channel-specific wishlist data This query uses cursor-based pagination and returns wishlist items with their associated product, customer, and channel data. ## Authentication This query requires customer authentication: - **Authenticated customers**: Provide a valid customer authentication token in the `Authorization` header. Obtain this token via the [Customer Login API](/api/graphql-api/shop/mutations/customer-login). ``` Authorization: Bearer ``` ## Arguments | Argument | Type | Description | |----------|------|-------------| | `first` | `Int` | Number of items to return per page. | | `after` | `String` | Cursor for pagination. Use `endCursor` from previous response. | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `edges` | `[WishlistEdge!]` | Array of wishlist edges with cursor and node. | | `edges.cursor` | `String!` | Cursor for this edge, used in pagination. | | `edges.node` | `Wishlist!` | The wishlist item object. | | `edges.node.id` | `ID!` | IRI identifier (e.g. `/api/shop/wishlists/69`). | | `edges.node._id` | `Int!` | Numeric identifier. | | `edges.node.product` | `Product!` | Associated product with id, name, sku, price, type, description, baseImageUrl. | | `edges.node.customer` | `Customer!` | Associated customer with id, email. | | `edges.node.channel` | `Channel!` | Associated channel with id, code, translation. | | `edges.node.createdAt` | `String` | Timestamp when the item was added. | | `edges.node.updatedAt` | `String` | Timestamp when the item was last updated. | | `pageInfo` | `PageInfo!` | Pagination metadata. | | `totalCount` | `Int!` | Total number of wishlist items. | --- # Search Products URL: /api/graphql-api/shop/queries/search-products --- outline: false examples: - id: search-products-with-filter title: Search Products with Search and Filter description: Search products by query term with sorting and filtering options. query: | query getProductsSearchFilter($query: String, $sortKey: String, $reverse: Boolean, $first: Int) { products(query: $query, sortKey: $sortKey, reverse: $reverse, first: $first) { edges { node { id name sku price } } } } variables: | { "query": "shirt", "sortKey": "TITLE", "reverse": false, "first": 10 } response: | { "data": { "products": { "edges": [ { "node": { "id": "/api/shop/products/144", "name": "Augusta Pullover Top Shirt", "sku": "AUGUSTA-PULLOVER-001", "price": "0" } }, { "node": { "id": "/api/shop/products/2439", "name": "Azure Breeze Sleeveless Linen Shirt", "sku": "AZURE-LINEN-SHIRT-001", "price": "0" } }, { "node": { "id": "/api/shop/products/124", "name": "Clean Pink Shirt", "sku": "CLEAN-PINKSHIRT-001", "price": "2040" } }, { "node": { "id": "/api/shop/products/2432", "name": "Coral Drift Linen V-Neck Shirt", "sku": "CORAL-VNECK-001", "price": "0" } }, { "node": { "id": "/api/shop/products/2468", "name": "Minimalist Cotton Shirt", "sku": "MINIMAL-COTTON-001", "price": "0" } } ] } } } commonErrors: - error: INVALID_QUERY cause: Search query is empty or malformed solution: Provide valid search term - error: NO_RESULTS cause: No products match search criteria solution: Try different keywords - id: search-products-by-category title: Search Products by Category ID with Pagination description: Retrieve products filtered by category ID using the filter argument with cursor-based pagination. query: | query getProducts { products(filter: "{\"category_id\": \"22\"}", first: 2, after: "Mg==") { edges { node { id sku price name urlKey baseImageUrl description shortDescription specialPrice } } pageInfo { hasNextPage hasPreviousPage startCursor endCursor } totalCount } } variables: | {} response: | { "data": { "products": { "edges": [ { "node": { "id": "/api/shop/products/229", "sku": "VUPULSE-JEANS-001", "price": "600", "name": "VuPulse High-Waist Wide-Leg Denim Jeans", "urlKey": "vupulse-high-waist-wide-leg-denim-jeans", "baseImageUrl": "https://api-demo.bagisto.com/storage/product/229/XaW3Oqh8DEq6qm4JwNJ7x9EnhjW9CllTuiJthBpX.webp", "description": "Step into timeless style and comfort with our Classic Blue Denim Jeans. Crafted from high-quality, durable cotton denim with a flattering straight-leg fit.", "shortDescription": "Versatile mid-blue distressed denim jeans with high-rise waist and wide straight legs.", "specialPrice": null } }, { "node": { "id": "/api/shop/products/232", "sku": "CHIC-SKIRT-TOP-001", "price": "0", "name": "Chic Skirt Top for Women", "urlKey": "chic-skirt-top-for-women", "baseImageUrl": "https://api-demo.bagisto.com/storage/product/232/2HCv4W2RyB7zFHKanwiZiv1WpMO0sP96oklzJ3RK.webp", "description": "Stylish and comfortable, this skirt top is designed to pair perfectly with any skirt. With a flattering fit and elegant details, it's ideal for both casual and dressy looks.", "shortDescription": "Complete your look with this stylish skirt top, specially designed to complement all types of skirts.", "specialPrice": null } } ], "pageInfo": { "hasNextPage": true, "hasPreviousPage": true, "startCursor": "Mw==", "endCursor": "NA==" }, "totalCount": 19 } } } commonErrors: - error: INVALID_FILTER cause: Filter JSON string is malformed or contains invalid keys solution: Ensure the filter is a valid JSON string with supported keys like category_id - error: INVALID_CATEGORY_ID cause: Category with specified ID does not exist solution: Verify the category ID exists in your store - error: INVALID_CURSOR cause: The cursor value is invalid or expired solution: Use a valid cursor from a previous response's pageInfo - id: filter-by-product-type title: Filter by Product Type description: Retrieve only configurable products using the type filter. query: | query getProducts { products(filter: "{\"type\": \"configurable\"}") { edges { node { id sku } } totalCount } } variables: | {} response: | { "data": { "products": { "edges": [ { "node": { "id": "/api/shop/products/23", "sku": "LUGGAGE-BAGS-001" } }, { "node": { "id": "/api/shop/products/93", "sku": "PUMA-WHITE-001" } }, { "node": { "id": "/api/shop/products/123", "sku": "ZOE-TANK-001" } }, { "node": { "id": "/api/shop/products/2495", "sku": "IVORY-OVERCOAT-001" } }, { "node": { "id": "/api/shop/products/2500", "sku": "MINT-BLAZER-001" } } ], "totalCount": 27 } } } - id: filter-by-attribute title: Filter by Attribute (Color) description: Filter products by a specific attribute value such as color. query: | query getProducts { products(filter: "{\"color\": \"3\"}") { edges { node { id sku } } totalCount } } variables: | {} response: | { "data": { "products": { "edges": [ { "node": { "id": "/api/shop/products/114", "sku": "Nike-Shoes" } }, { "node": { "id": "/api/shop/products/123", "sku": "ZOE-TANK-001" } }, { "node": { "id": "/api/shop/products/280", "sku": "OAKCRAFT-SOFA-001" } }, { "node": { "id": "/api/shop/products/2419", "sku": "SAGE-TSHIRT-001" } }, { "node": { "id": "/api/shop/products/2468", "sku": "MINIMAL-COTTON-001" } } ], "totalCount": 13 } } } - id: filter-by-multiple-attributes title: Filter by Multiple Attributes description: Combine multiple attribute filters (color, size, brand) in a single query. query: | query getProducts { products(filter: "{\"color\": \"5\", \"size\": \"1\", \"brand\": \"5\"}", first: 10) { edges { node { id sku name price } } totalCount } } variables: | {} response: | { "data": { "products": { "edges": [ { "node": { "id": "/api/shop/products/22", "sku": "POLO-BLU-S-022", "name": "Blue Polo Shirt - Small", "price": 34.99 } } ], "totalCount": 1 } } } - id: sort-title-asc title: Sort A–Z by Title description: Sort products alphabetically from A to Z. query: | query getProducts { products(sortKey: "TITLE", reverse: false, first: 5) { edges { node { id name sku } } totalCount } } variables: | {} response: | { "data": { "products": { "edges": [ { "node": { "id": "/api/shop/products/90", "name": "A Best Shoes", "sku": "main-shoes-123" } }, { "node": { "id": "/api/shop/products/120", "name": "Acme Baby Cap", "sku": "ACME-BABYCAP-001" } }, { "node": { "id": "/api/shop/products/22", "name": "Acme Drawstring Bag", "sku": "ACME-DRAWBAG-001" } }, { "node": { "id": "/api/shop/products/2414", "name": "AeroLoom High-Rise Baggy Jeans", "sku": "AEROLOOM-JEANS-001" } }, { "node": { "id": "/api/shop/products/2513", "name": "Arctic Bliss Stylish Winter Scarf", "sku": "SP-002" } } ], "totalCount": 84 } } } - id: sort-title-desc title: Sort Z–A by Title description: Sort products alphabetically from Z to A. query: | query getProducts { products(sortKey: "TITLE", reverse: true, first: 5) { edges { node { id name sku } } totalCount } } variables: | {} response: | { "data": { "products": { "edges": [ { "node": { "id": "/api/shop/products/123", "name": "Zoe Tank", "sku": "ZOE-TANK-001" } }, { "node": { "id": "/api/shop/products/1093", "name": "Wooden Sofa Model 751", "sku": "SOFA-WOODENSOFA-0751" } }, { "node": { "id": "/api/shop/products/2510", "name": "Wooden Folding Chair Rental", "sku": "WOODEN-FOLDING-CHAIR-RENTAL" } }, { "node": { "id": "/api/shop/products/115", "name": "Women Shoulder Bags", "sku": "WOMEN-SHOULDER-001" } }, { "node": { "id": "/api/shop/products/186", "name": "Women Shirt", "sku": "WOMEN-SHIRT-001" } } ], "totalCount": 84 } } } - id: sort-newest-first title: Sort Newest First description: Sort products by creation date, newest first. query: | query getProducts { products(sortKey: "CREATED_AT", reverse: true, first: 10) { edges { node { id name sku price } } totalCount } } variables: | {} response: | { "data": { "products": { "edges": [ { "node": { "id": "/api/shop/products/2517", "name": "Arctic Frost Winter Accessories Bundle", "sku": "BP-001", "price": "0" } }, { "node": { "id": "/api/shop/products/2516", "name": "Arctic Frost Winter Accessories", "sku": "GP-001", "price": "0" } }, { "node": { "id": "/api/shop/products/2515", "name": "Arctic Warmth Wool Blend Socks", "sku": "SP-004", "price": "21" } }, { "node": { "id": "/api/shop/products/2514", "name": "Arctic Touchscreen Winter Gloves", "sku": "SP-003", "price": "21" } }, { "node": { "id": "/api/shop/products/2513", "name": "Arctic Bliss Stylish Winter Scarf", "sku": "SP-002", "price": "17" } }, { "node": { "id": "/api/shop/products/2512", "name": "Arctic Cozy Knit Unisex Beanie", "sku": "SP-001", "price": "14" } }, { "node": { "id": "/api/shop/products/2511", "name": "Fine Dining Table Reservation", "sku": "FINE-DINING-TABLE-RESERVATION", "price": "200" } }, { "node": { "id": "/api/shop/products/2510", "name": "Wooden Folding Chair Rental", "sku": "WOODEN-FOLDING-CHAIR-RENTAL", "price": "109" } }, { "node": { "id": "/api/shop/products/2509", "name": "Men's Haircut Appointment", "sku": "SALON-HAIRCUT-APPOINTMENT", "price": "60" } }, { "node": { "id": "/api/shop/products/2508", "name": "Live Music Concert Ticket", "sku": "LIVE-MUSIC-CONCERT-TICKET", "price": "120" } } ], "totalCount": 84 } } } - id: sort-oldest-first title: Sort Oldest First description: Sort products by creation date, oldest first. query: | query getProducts { products(sortKey: "CREATED_AT", reverse: false, first: 10) { edges { node { id name sku price } } totalCount } } variables: | {} response: | { "data": { "products": { "edges": [ { "node": { "id": "/api/shop/products/1", "name": "Coastal Breeze Men's Blue Zipper Hoodie", "sku": "COASTALBREEZEMENSHOODIE", "price": "100" } }, { "node": { "id": "/api/shop/products/2", "name": "PureStride Men's Classic White Sneakers", "sku": "PUREWHTSNEAK2023", "price": "189" } }, { "node": { "id": "/api/shop/products/3", "name": "Midnight Blossom Women's Black Floral Print Sandals", "sku": "MIDNIGHTBLOSSOMHEELS2023", "price": "204" } }, { "node": { "id": "/api/shop/products/22", "name": "Acme Drawstring Bag", "sku": "ACME-DRAWBAG-001", "price": "3000" } }, { "node": { "id": "/api/shop/products/91", "name": "Bagisto Keyboard", "sku": "Bagisto-keyboard", "price": "20" } }, { "node": { "id": "/api/shop/products/92", "name": "Bagisto Sticker", "sku": "bagisto-sticker", "price": "10" } }, { "node": { "id": "/api/shop/products/114", "name": "Nike Shoes", "sku": "Nike-Shoes", "price": "200" } } ], "totalCount": 84 } } } - id: sort-cheapest-first title: Sort Cheapest First description: Sort products by price, lowest to highest. Sorting is based on the minimumPrice column, which is the price displayed to the customer. This accounts for all price variants — including special price and configurable variant prices — making minimumPrice the primary reference for what the customer actually sees. query: | query getProductsSorted { products(reverse: false, sortKey: "PRICE", first: 10) { edges { node { id name sku minimumPrice price specialPrice } } } } variables: | {} response: | { "data": { "products": { "edges": [ { "node": { "id": "/api/shop/products/3", "name": "Cotton Socks", "sku": "SOC-003", "minimumPrice": 5.99, "price": 5.99, "specialPrice": null } }, { "node": { "id": "/api/shop/products/8", "name": "Basic Tee", "sku": "BAS-008", "minimumPrice": 10.99, "price": 12.99, "specialPrice": 10.99 } } ] } } } - id: sort-expensive-first title: Sort Most Expensive First description: Sort products by price, highest to lowest. query: | query getProducts { products(sortKey: "PRICE", reverse: true, first: 10) { edges { node { id name sku price } } totalCount } } variables: | {} response: | { "data": { "products": { "edges": [ { "node": { "id": "/api/shop/products/25", "name": "Premium Leather Jacket", "sku": "PLJ-025", "price": 499.99 } }, { "node": { "id": "/api/shop/products/30", "name": "Designer Blazer", "sku": "DBL-030", "price": 349.99 } } ], "totalCount": 50 } } } - id: new-products title: Search Products - New Products description: Retrieve products that are flagged as "new" in the catalog, sorted by creation date descending (newest first). Equivalent to the REST endpoint /api/products?new=1&sort=created_at-desc. query: | query getProducts { products(filter: "{\"new\": \"1\"}", sortKey: "CREATED_AT", reverse: true, first: 10) { edges { node { id name sku price urlKey baseImageUrl } } totalCount } } variables: | {} response: | { "data": { "products": { "edges": [ { "node": { "id": "/api/shop/products/48", "name": "Spring Floral Dress", "sku": "SPR-048", "price": 65.99, "urlKey": "spring-floral-dress", "baseImageUrl": "https://api-demo.bagisto.com/storage/product/48/image.jpg" } } ], "totalCount": 8 } } } - id: featured-products title: Search Products - Featured Products description: Retrieve featured products sorted by newest first — equivalent to the REST endpoint /api/products?featured=1&sort=created_at-desc. query: | query getProducts { products(filter: "{\"featured\": \"1\"}", sortKey: "CREATED_AT", reverse: true, first: 12) { edges { node { id name sku price urlKey baseImageUrl } } totalCount } } variables: | {} response: | { "data": { "products": { "edges": [ { "node": { "id": "/api/shop/products/2517", "name": "Arctic Frost Winter Accessories Bundle", "sku": "BP-001", "price": "0", "urlKey": "arctic-frost-winter-accessories-bundle", "baseImageUrl": "https://api-demo.bagisto.com/storage/product/2517/lW2A3FH3oKBJnnukyUyKUArdrr8dwTJxxDKthSgq.webp" } }, { "node": { "id": "/api/shop/products/2516", "name": "Arctic Frost Winter Accessories", "sku": "GP-001", "price": "0", "urlKey": "arctic-frost-winter-accessories", "baseImageUrl": "https://api-demo.bagisto.com/storage/product/2516/5Kgto6KVm6FLMaaDEY6pwCcVoTIhX03D3OGDzwbf.webp" } }, { "node": { "id": "/api/shop/products/2514", "name": "Arctic Touchscreen Winter Gloves", "sku": "SP-003", "price": "21", "urlKey": "arctic-touchscreen-winter-gloves", "baseImageUrl": "https://api-demo.bagisto.com/storage/product/2514/g8lR0Ity8HcpE20A4yAkX5wvLY5RlTC67NJKyyg6.webp" } }, { "node": { "id": "/api/shop/products/2513", "name": "Arctic Bliss Stylish Winter Scarf", "sku": "SP-002", "price": "17", "urlKey": "arctic-bliss-stylish-winter-scarf", "baseImageUrl": "https://api-demo.bagisto.com/storage/product/2513/odMU05kvHJqzuDUdHGy9dlD3qAq1FEUbkQbkg3Wk.webp" } }, { "node": { "id": "/api/shop/products/2512", "name": "Arctic Cozy Knit Unisex Beanie", "sku": "SP-001", "price": "14", "urlKey": "arctic-cozy-knit-unisex-beanie", "baseImageUrl": "https://api-demo.bagisto.com/storage/product/2512/Muc0qeWks34MTZaxf38s6DBmfqMqrCxku81Uo8EB.webp" } }, { "node": { "id": "/api/shop/products/2511", "name": "Fine Dining Table Reservation", "sku": "FINE-DINING-TABLE-RESERVATION", "price": "200", "urlKey": "fine-dining-table-reservation", "baseImageUrl": "https://api-demo.bagisto.com/storage/product/2511/lw253CbVba9nRZVUGy9atW9t85ADE2UwldssE8t6.webp" } }, { "node": { "id": "/api/shop/products/2507", "name": "Professional Photography Session", "sku": "PROFESSIONAL-PHOTOGRAPHY-SESSION", "price": "100", "urlKey": "professional-photography-session", "baseImageUrl": "https://api-demo.bagisto.com/storage/product/2507/1jO3Pb5UA89ZaVsp1cnlICSFgZKlwy6lPlDJynGu.webp" } }, { "node": { "id": "/api/shop/products/2506", "name": "Complete Personal Finance Guide (eBook PDF)", "sku": "COMPLETE-PERSONAL-FINANCE-GUIDE-EBOOK", "price": "70", "urlKey": "complete-personal-finance-guide-ebook-pdf", "baseImageUrl": "https://api-demo.bagisto.com/storage/product/2506/XY0sCaNbWfeXDntNFbYnlL6N5uOJ9tfyR7AtntSf.webp" } }, { "node": { "id": "/api/shop/products/2505", "name": "HD Streaming Subscription - 1 Month Access", "sku": "HD-STREAMING-SUBSCRIPTION-1-MONTH", "price": "64", "urlKey": "hd-streaming-subscription-1-month-access", "baseImageUrl": "https://api-demo.bagisto.com/storage/product/2505/sCwS1QRNlJHLPjw5UzxYSR21oqYbMvo4UNRYklME.webp" } } ], "totalCount": 56 } } } - id: popular-products-by-brand title: Popular Products by Brand description: Retrieve popular products for a specific brand sorted by newest first — equivalent to the REST endpoint /api/products?sort=created_at-desc&brand=25. query: | query getProducts { products(filter: "{\"brand\": \"25\"}", sortKey: "CREATED_AT", reverse: true, first: 12) { edges { node { id name sku price urlKey baseImageUrl } } totalCount } } variables: | {} response: | { "data": { "products": { "edges": [ { "node": { "id": "/api/shop/products/2500", "name": "Mint Axis Unisex Tailored Blazer", "sku": "MINT-BLAZER-001", "price": "0", "urlKey": "mint-axis-unisex-tailored-blazer", "baseImageUrl": "https://api-demo.bagisto.com/storage/product/2500/T97yKJVNKlmi6GXoqKl8FNqfM8115Wxo6jw4WhPF.webp" } }, { "node": { "id": "/api/shop/products/2495", "name": "Ivory Frost Classic Overcoat", "sku": "IVORY-OVERCOAT-001", "price": "0", "urlKey": "ivory-frost-classic-overcoat", "baseImageUrl": "https://api-demo.bagisto.com/storage/product/2495/FFHxE9HE2Ezt9aqvr6s3fPPCc1nrjwMNna1o1wTQ.webp" } }, { "node": { "id": "/api/shop/products/2493", "name": "Aurora Cream Winter Blazer Coat", "sku": "AURORA-BLAZER-001", "price": "5000", "urlKey": "aurora-cream-winter-blazer-coat", "baseImageUrl": "https://api-demo.bagisto.com/storage/product/2493/wnbCVz0T8R3sxMMlcYsTICoNEj8WK4M3mIL62YgE.webp" } } ], "totalCount": 3 } } } --- # Search Products ## About The `searchProducts` query enables advanced product search with support for text queries, filtering, and sorting. Use this query to: - Implement full-text product search functionality - Build auto-complete and suggestion interfaces - Filter products by multiple criteria (price range, categories, attributes) - Sort search results by relevance, price, date, or custom fields - Implement faceted search interfaces - Create advanced query-based product discovery The search supports Bagisto's advanced search syntax for building complex, multi-criteria queries. It efficiently ranks results by relevance while maintaining performance across large product catalogs. ## Arguments | Argument | Type | Description | |----------|------|-------------| | `query` | `String` | Search term or advanced query string. Searches product name, description, SKU, and other fields. | | `first` | `Int` | Maximum number of results per page (default: 20, max: 250). | | `after` | `String` | Cursor for pagination. Returns results after this cursor. | | `last` | `Int` | Maximum results for backward pagination (max: 250). | | `before` | `String` | Cursor for backward pagination. | | `sortKey` | `ProductSortKeys` | Sort results by: `RELEVANCE`, `TITLE`, `PRICE`, `CREATED_AT`, `POPULARITY`. Default: `RELEVANCE` | | `reverse` | `Boolean` | Reverse sort order. Default: `false` | | `filters` | `ProductFilterInput` | Advanced filters for price range, categories, tags, and custom attributes. | | `filter` | `String` | JSON-encoded filter string. Supports keys like `category_id` to filter products by category (e.g. `"{\"category_id\": \"22\"}"`) | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `edges` | `[ProductEdge!]!` | Search result edges containing product nodes and pagination cursors. | | `edges.node` | `Product!` | Product object with all searchable fields (name, description, SKU, tags). | | `edges.node.score` | `Float` | Relevance score of the product match (0-1). Higher scores indicate better matches. | | `edges.cursor` | `String!` | Pagination cursor for this result. | | `nodes` | `[Product!]!` | Simplified array of products without edge wrapping. | | `pageInfo` | `PageInfo!` | Pagination metadata. | | `pageInfo.hasNextPage` | `Boolean!` | True if more results available after current page. | | `pageInfo.endCursor` | `String` | Cursor of last result on page. | | `facets` | `[SearchFacet!]` | Available facets for filtering (categories, price ranges, attributes). | | `facets.name` | `String!` | Facet name (e.g., "category", "price_range"). | | `facets.values` | `[FacetValue!]!` | Available values and counts for this facet. | | `totalCount` | `Int!` | Total matching products across all pages. | ## Filter Reference The `filter` argument accepts a JSON-encoded string. You can combine multiple filters in a single object. ### Available Filter Keys | Filter Key | Type | Description | Example | |------------|------|-------------|---------| | `category_id` | String | Filter by category ID | `"{\"category_id\": \"22\"}"` | | `type` | String | Filter by product type (`simple`, `configurable`, `virtual`, `downloadable`, `grouped`, `bundle`) | `"{\"type\": \"configurable\"}"` | | `color` | String | Filter by color attribute option ID | `"{\"color\": \"3\"}"` | | `size` | String | Filter by size attribute option ID | `"{\"size\": \"1\"}"` | | `brand` | String | Filter by brand attribute option ID | `"{\"brand\": \"5\"}"` | | `new` | String | Filter for new products only | `"{\"new\": \"1\"}"` | | `featured` | String | Filter for featured products only | `"{\"featured\": \"1\"}"` | ### Combining Filters Pass multiple keys in a single JSON object: ```graphql products(filter: "{\"color\": \"5\", \"size\": \"1\", \"brand\": \"5\"}") ``` ## Sorting Reference Use `sortKey` and `reverse` to control result ordering: | Sort | `sortKey` | `reverse` | Description | |------|-----------|-----------|-------------| | A → Z | `"TITLE"` | `false` | Alphabetical ascending | | Z → A | `"TITLE"` | `true` | Alphabetical descending | | Newest First | `"CREATED_AT"` | `true` | Most recently created | | Oldest First | `"CREATED_AT"` | `false` | Earliest created | | Cheapest First | `"PRICE"` | `false` | Lowest price first | | Most Expensive First | `"PRICE"` | `true` | Highest price first | ### How Price Sorting Works When sorting by `PRICE`, the sort is not based on the `price` field alone. Instead, it is based on the `minimumPrice` column — the effective price shown to the customer on the storefront. `minimumPrice` reflects the lowest applicable price for a product after accounting for: - **Special price** — if a discounted price is set, `minimumPrice` will reflect that instead of the regular `price` - **Configurable variants** — for products with multiple variants, `minimumPrice` is the lowest price across all variants - **Regular price** — if no special price or variant pricing applies, `minimumPrice` equals `price` This means when you sort by price, you are sorting by what the customer actually sees — not the base catalog price. Always use `minimumPrice` in your UI when displaying the effective price alongside price-sorted results. ## REST API Equivalents | Use Case | REST Endpoint | GraphQL Equivalent | |----------|---------------|--------------------| | New Products | `/api/products?new=1&sort=created_at-desc&limit=10` | `products(filter: "{\"new\": \"1\"}", sortKey: "CREATED_AT", reverse: true, first: 10)` | | Featured Products | `/api/products?sort=created_at-desc&limit=12` | `products(filter: "{\"featured\": \"1\"}", sortKey: "CREATED_AT", reverse: true, first: 12)` | | Popular by Brand | `/api/products?sort=created_at-desc&brand=25&limit=12` | `products(filter: "{\"brand\": \"25\"}", sortKey: "CREATED_AT", reverse: true, first: 12)` | | All (Price Desc) | `/api/products?sort=price-desc&limit=12` | `products(sortKey: "PRICE", reverse: true, first: 12)` | --- # Get Theme Customisation URL: /api/graphql-api/shop/queries/single-theme-customisation --- outline: false examples: - id: get-theme-customisation-by-id-basic title: Get Theme Customisation by ID - Basic description: Retrieve basic information for a single theme customisation by its ID. query: | query getThemeCustomisation($id: ID!) { themeCustomization(id: $id) { id _id type name status themeCode translation { locale options } } } variables: | { "id": "/api/theme_customizations/1" } response: | { "data": { "themeCustomization": { "id": "/api/shop/theme-customizations/1", "_id": 1, "type": "image_carousel", "name": "Image Carousel", "status": "1", "themeCode": "default", "translation": { "locale": "en", "options": "{\"images\": [{\"link\": \"fashion\", \"image\": \"storage/theme/1/ATTrUoI1AN2s8mR7KdlxfmGPG1eeFV0uwdIPn9fb.webp\", \"title\": \"Fashion\"}, {\"link\": \"furniture\", \"image\": \"storage/theme/1/CoizBehgRZ4vqmV1gw88HiJWnx16BVorCpRxaBSb.webp\", \"title\": \"Furniture\"}, {\"link\": \"electronics\", \"image\": \"storage/theme/1/HRIEAfZ4vTc0hrW5G5L1tK3vzmwBXgZR781tjEwU.webp\", \"title\": \"Electronics\"}]}" } } } } commonErrors: - error: id-required cause: Theme Customization ID parameter is missing solution: Provide the theme customization ID as a required parameter - error: not-found cause: Theme Customization with given ID does not exist solution: Verify the theme customization ID is correct - id: get-theme-customisation-by-numeric-id title: Get Theme Customisation by Numeric ID description: Retrieve a theme customisation using a numeric ID instead of IRI format. query: | query getThemeCustomisation($id: ID!) { themeCustomization(id: $id) { id _id type name status themeCode sortOrder translation { locale options } } } variables: | { "id": 5 } response: | { "data": { "themeCustomization": { "id": "/api/shop/theme-customizations/5", "_id": 5, "type": "static_content", "name": "Top Collections", "status": "1", "themeCode": "default", "sortOrder": 5, "translation": { "locale": "en", "options": "{\"css\": \".top-collection-header {...}\", \"html\": \"

The game with our new additions!

...
\"}" } } } } commonErrors: - error: not-found cause: Theme Customization with given ID does not exist solution: Verify the theme customization ID is correct - id: get-theme-customisation-complete title: Get Theme Customisation - Complete Details description: Retrieve complete theme customisation details including timestamps and all translations. query: | query getThemeCustomisation($id: ID!) { themeCustomization(id: $id) { id _id themeCode type name sortOrder status channelId createdAt updatedAt translation { id _id themeCustomizationId locale options } translations { edges { cursor node { id _id themeCustomizationId locale options } } pageInfo { endCursor startCursor hasNextPage hasPreviousPage } totalCount } } } variables: | { "id": 5 } response: | { "data": { "themeCustomization": { "id": "/api/shop/theme-customizations/5", "_id": 5, "themeCode": "default", "type": "static_content", "name": "Top Collections", "sortOrder": 5, "status": "1", "channelId": "1", "createdAt": "2024-04-16T16:14:15+05:30", "updatedAt": "2026-04-07T12:02:47+05:30", "translation": { "id": "/api/shop/theme_customization_translations/5", "_id": 5, "themeCustomizationId": "5", "locale": "en", "options": "{\"css\": \".top-collection-header {...}\", \"html\": \"

The game with our new additions!

...
\"}" }, "translations": { "edges": [ { "cursor": "MA==", "node": { "id": "/api/shop/theme_customization_translations/5", "_id": 5, "themeCustomizationId": "5", "locale": "en", "options": "{\"css\": \".top-collection-header {...}\", \"html\": \"
...

The game with our new additions!

...
\"}" } }, { "cursor": "MQ==", "node": { "id": "/api/shop/theme_customization_translations/22", "_id": 22, "themeCustomizationId": "5", "locale": "AR", "options": "{\"css\": \".top-collection-header {...}\", \"html\": \"
...

اللعبة مع إضافاتنا الجديدة!

...
\"}" } } ], "pageInfo": { "endCursor": "MQ==", "startCursor": "MA==", "hasNextPage": false, "hasPreviousPage": false }, "totalCount": 2 } } } } commonErrors: - error: id-required cause: Theme Customization ID parameter is missing solution: Provide the theme customization ID as a required parameter - error: not-found cause: Theme Customization with given ID does not exist solution: Verify the theme customization ID is correct --- # Get Theme Customisation ## About The `themeCustomization` query retrieves detailed information about a single theme customisation by its ID. Use this query to: - Display specific theme customisation details - Fetch carousel or slider configurations by ID - Retrieve footer links or static content sections - Access all translations for a specific customisation - Get customisation metadata including timestamps - Display channel-specific customisations - Access complete JSON configuration options This query returns comprehensive customisation data including all translations, display settings, and channel information. ## Arguments | Argument | Type | Required | Description | |----------|------|----------|-------------| | `id` | `ID!` | ✅ Yes | Theme Customization ID. Supports two formats: numeric ID (e.g., `1`) or IRI format (e.g., `/api/theme_customizations/1`). Required. | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `id` | `ID!` | Unique theme customization API identifier. | | `_id` | `Int!` | Numeric customization ID. | | `themeCode` | `String!` | Theme code/identifier (e.g., 'default'). | | `type` | `String!` | Customization type (e.g., 'footer_links', 'image_carousel', 'product_carousel', 'category_carousel', 'static_content'). | | `name` | `String!` | Human-readable name of the customization. | | `sortOrder` | `Int` | Sort order for display. | | `status` | `String` | Status flag (0 = inactive, 1 = active). | | `channelId` | `String` | Associated channel ID. | | `createdAt` | `DateTime!` | Customization creation timestamp. | | `updatedAt` | `DateTime!` | Last update timestamp. | | `translation` | `ThemeCustomizationTranslation!` | Default locale translation. | | `translation.id` | `ID!` | Translation identifier. | | `translation._id` | `Int!` | Numeric translation ID. | | `translation.themeCustomizationId` | `String!` | Associated customization ID. | | `translation.locale` | `String!` | Language locale code (e.g., 'en', 'ar', 'fr'). | | `translation.options` | `String!` | JSON-formatted options/configuration for this translation. | | `translations` | `ThemeCustomizationTranslationCollection!` | All available translations. | | `translations.edges` | `[Edge!]!` | Translation edges with cursors. | | `translations.edges.node` | `ThemeCustomizationTranslation!` | Individual translation. | | `translations.edges.cursor` | `String!` | Pagination cursor for this translation. | | `translations.pageInfo` | `PageInfo!` | Pagination info for translations. | | `translations.pageInfo.hasNextPage` | `Boolean!` | More translations available. | | `translations.pageInfo.hasPreviousPage` | `Boolean!` | Previous translations available. | | `translations.pageInfo.startCursor` | `String` | First translation cursor. | | `translations.pageInfo.endCursor` | `String` | Last translation cursor. | | `translations.totalCount` | `Int!` | Total translations for this customization. | ## Customisation Types | Type | Description | |------|-------------| | `image_carousel` | Image slider/carousel on home page | | `product_carousel` | Product carousel display | | `category_carousel` | Category carousel display | | `static_content` | HTML/CSS static sections | | `footer_links` | Footer navigation links | | `services_content` | Services information blocks | ## Use Cases ### 1. Display Specific Carousel Use the "Basic" example to fetch and display a specific carousel configuration. ### 2. Multi-Language Support Use the "Complete Details" example to get all translations for rendering in different languages. ### 3. Footer Links Display Fetch footer links customisation and display them in the footer section. ### 4. Static Content Sections Retrieve HTML/CSS static content sections for rendering on pages. ## Best Practices 1. **Use Correct ID Format** - Use either numeric ID or IRI format consistently 2. **Cache Results** - Theme customisations change infrequently, cache the response 3. **Parse JSON Options** - The `options` field contains JSON; parse it in your application 4. **Check Status** - Verify status is active before displaying 5. **Handle Multiple Translations** - Fetch all translations for multi-language support 6. **Validate Channel** - Ensure customisation is for the correct channel ## Related Resources - [Theme Customisations](/api/graphql-api/shop/queries/get-theme-customisations) - Get all theme customisations with pagination - [Get Category](/api/graphql-api/shop/queries/get-category) - Query individual category details - [Get Products](/api/graphql-api/shop/queries/get-products) - Query products - [Pagination Guide](/api/graphql-api/pagination) - Cursor pagination documentation - [Shop API Overview](/api/graphql-api/shop-api) - Overview of Shop API resources --- # Theme Customizations URL: /api/graphql-api/shop/queries/theme-customisations --- outline: false examples: - id: get-theme-customizations-basic title: Get Theme Customizations - Basic description: Retrieve theme customizations with basic fields and pagination. query: | query themeCustomizations($first: Int, $after: String) { themeCustomizations(first: $first, after: $after) { edges { node { id _id type name status themeCode sortOrder translation { locale options } } cursor } pageInfo { hasNextPage endCursor } totalCount } } variables: | { "first": 5, "after": null } response: | { "data": { "themeCustomizations": { "edges": [ { "node": { "id": "/api/shop/theme-customizations/1", "_id": 1, "type": "image_carousel", "name": "Image Carousel", "status": "1", "themeCode": "default", "sortOrder": 1, "translation": { "locale": "en", "options": "{\"images\": [{\"link\": \"fashion\", \"image\": \"storage/theme/1/ATTrUoI1AN2s8mR7KdlxfmGPG1eeFV0uwdIPn9fb.webp\", \"title\": \"Fashion\"}, {\"link\": \"furniture\", \"image\": \"storage/theme/1/CoizBehgRZ4vqmV1gw88HiJWnx16BVorCpRxaBSb.webp\", \"title\": \"Furniture\"}, {\"link\": \"electronics\", \"image\": \"storage/theme/1/HRIEAfZ4vTc0hrW5G5L1tK3vzmwBXgZR781tjEwU.webp\", \"title\": \"Electronics\"}]}" } }, "cursor": "MA==" }, { "node": { "id": "/api/shop/theme-customizations/3", "_id": 3, "type": "category_carousel", "name": "Categories Collections", "status": "1", "themeCode": "default", "sortOrder": 3, "translation": { "locale": "en", "options": "{\"filters\": {\"sort\": \"asc\", \"limit\": \"10\", \"parent_id\": \"1\"}}" } }, "cursor": "MQ==" }, { "node": { "id": "/api/shop/theme-customizations/4", "_id": 4, "type": "product_carousel", "name": "New Products", "status": "1", "themeCode": "default", "sortOrder": 4, "translation": { "locale": "en", "options": "{\"title\": \"New Products\", \"filters\": {\"new\": 1, \"sort\": \"asc\", \"limit\": 10}}" } }, "cursor": "Mg==" }, { "node": { "id": "/api/shop/theme-customizations/5", "_id": 5, "type": "static_content", "name": "Top Collections", "status": "1", "themeCode": "default", "sortOrder": 5, "translation": { "locale": "en", "options": "{\"css\": \"...\", \"html\": \"
...
\"}" } }, "cursor": "Mw==" }, { "node": { "id": "/api/shop/theme-customizations/6", "_id": 6, "type": "static_content", "name": "Bold Collections", "status": "1", "themeCode": "default", "sortOrder": 6, "translation": { "locale": "en", "options": "{\"css\": \"...\", \"html\": \"
...
\"}" } }, "cursor": "NA==" } ], "pageInfo": { "hasNextPage": true, "endCursor": "NA==" }, "totalCount": 12 } } } commonErrors: - error: invalid-pagination cause: Invalid pagination arguments solution: Use valid first/after or last/before combinations with max value 100 - id: get-theme-customizations-with-type-filter title: Get Theme Customizations - Filtered by Type description: Retrieve theme customizations filtered by a specific type with translations. query: | query themeCustomizations($type: String) { themeCustomizations(type: $type) { edges { node { id _id type name status themeCode sortOrder translation { id _id themeCustomizationId locale options } translations { edges { node { id _id themeCustomizationId locale options } cursor } pageInfo { endCursor startCursor hasNextPage hasPreviousPage } totalCount } } cursor } pageInfo { endCursor startCursor hasNextPage hasPreviousPage } totalCount } } variables: | { "type": "footer_links" } response: | { "data": { "themeCustomizations": { "edges": [ { "node": { "id": "/api/shop/theme-customizations/11", "_id": 11, "type": "footer_links", "name": "Footer Links", "status": "1", "themeCode": "default", "sortOrder": 11, "translation": { "id": "/api/shop/theme_customization_translations/11", "_id": 11, "themeCustomizationId": "11", "locale": "en", "options": "{\"column_1\": [{\"url\": \"https://api-demo.bagisto.com/page/privacy-policy\", \"title\": \"Privacy policy\", \"sort_order\": \"3\"}, {\"url\": \"https://api-demo.bagisto.com/page/whats-new\", \"title\": \"What's New\", \"sort_order\": \"3\"}, {\"url\": \"https://api-demo.bagisto.com/page/shipping-policy\", \"title\": \"Shipping\", \"sort_order\": \"4\"}], \"column_2\": [{\"url\": \"https://api-demo.bagisto.com/page/about\", \"title\": \"About Us\", \"sort_order\": \"1\"}, {\"url\": \"https://api-demo.bagisto.com/page/cutomer-service\", \"title\": \"Customer Service\", \"sort_order\": \"5\"}]}" }, "translations": { "edges": [ { "node": { "id": "/api/shop/theme_customization_translations/11", "_id": 11, "themeCustomizationId": "11", "locale": "en", "options": "{\"column_1\": [{\"url\": \"https://api-demo.bagisto.com/page/privacy-policy\", \"title\": \"Privacy policy\", \"sort_order\": \"3\"}, {\"url\": \"https://api-demo.bagisto.com/page/whats-new\", \"title\": \"What's New\", \"sort_order\": \"3\"}, {\"url\": \"https://api-demo.bagisto.com/page/shipping-policy\", \"title\": \"Shipping\", \"sort_order\": \"4\"}], \"column_2\": [{\"url\": \"https://api-demo.bagisto.com/page/about\", \"title\": \"About Us\", \"sort_order\": \"1\"}, {\"url\": \"https://api-demo.bagisto.com/page/cutomer-service\", \"title\": \"Customer Service\", \"sort_order\": \"5\"}]}" }, "cursor": "MA==" }, { "node": { "id": "/api/shop/theme_customization_translations/21", "_id": 21, "themeCustomizationId": "11", "locale": "AR", "options": "{\"column_1\": [{\"url\": \"https://api-demo.bagisto.com/page/privacy-policy\", \"title\": \"سياسة الخصوصية\", \"sort_order\": \"3\"}, {\"url\": \"https://api-demo.bagisto.com/page/whats-new\", \"title\": \"ما الجديد\", \"sort_order\": \"3\"}, {\"url\": \"https://api-demo.bagisto.com/page/shipping-policy\", \"title\": \"سياسة الشحن\", \"sort_order\": \"4\"}], \"column_2\": [{\"url\": \"https://api-demo.bagisto.com/page/about\", \"title\": \"من نحن\", \"sort_order\": \"1\"}, {\"url\": \"https://api-demo.bagisto.com/page/cutomer-service\", \"title\": \"خدمة العملاء\", \"sort_order\": \"5\"}]}" }, "cursor": "MQ==" } ], "pageInfo": { "endCursor": "MQ==", "startCursor": "MA==", "hasNextPage": false, "hasPreviousPage": false }, "totalCount": 2 } }, "cursor": "MA==" } ], "pageInfo": { "endCursor": "MA==", "startCursor": "MA==", "hasNextPage": false, "hasPreviousPage": false }, "totalCount": 1 } } } commonErrors: - error: invalid-pagination cause: Invalid pagination arguments solution: Use valid first/after or last/before combinations with max value 100 - error: invalid-type-filter cause: Invalid type filter value solution: Use valid type values like footer_links, image_carousel, product_carousel, etc. - id: get-theme-customizations-complete title: Get Theme Customizations - Complete Details description: Retrieve all theme customizations with complete fields including timestamps and all translations. query: | query themeCustomizations($first: Int, $after: String, $last: Int, $before: String, $type: String) { themeCustomizations(first: $first, after: $after, last: $last, before: $before, type: $type) { edges { node { id _id themeCode type name sortOrder status channelId createdAt updatedAt translation { id _id themeCustomizationId locale options } translations { edges { cursor node { id _id themeCustomizationId locale options } } pageInfo { endCursor startCursor hasNextPage hasPreviousPage } totalCount } } cursor } pageInfo { endCursor startCursor hasNextPage hasPreviousPage } totalCount } } variables: | { "first": 2, "after": null, "type": "static_content" } response: | { "data": { "themeCustomizations": { "edges": [ { "node": { "id": "/api/shop/theme-customizations/5", "_id": 5, "themeCode": "default", "type": "static_content", "name": "Top Collections", "sortOrder": 5, "status": "1", "channelId": "1", "createdAt": "2024-04-16T16:14:15+05:30", "updatedAt": "2026-04-07T12:02:47+05:30", "translation": { "id": "/api/shop/theme_customization_translations/5", "_id": 5, "themeCustomizationId": "5", "locale": "en", "options": "{\"css\": \".top-collection-header {...}\", \"html\": \"

The game with our new additions!

...
\"}" }, "translations": { "edges": [ { "cursor": "MA==", "node": { "id": "/api/shop/theme_customization_translations/5", "_id": 5, "themeCustomizationId": "5", "locale": "en", "options": "{\"css\": \".top-collection-header {...}\", \"html\": \"
...
\"}" } }, { "cursor": "MQ==", "node": { "id": "/api/shop/theme_customization_translations/22", "_id": 22, "themeCustomizationId": "5", "locale": "AR", "options": "{\"css\": \".top-collection-header {...}\", \"html\": \"

اللعبة مع إضافاتنا الجديدة!

...
\"}" } } ], "pageInfo": { "endCursor": "MQ==", "startCursor": "MA==", "hasNextPage": false, "hasPreviousPage": false }, "totalCount": 2 } }, "cursor": "MA==" }, { "node": { "id": "/api/shop/theme-customizations/6", "_id": 6, "themeCode": "default", "type": "static_content", "name": "Bold Collections", "sortOrder": 6, "status": "1", "channelId": "1", "createdAt": "2024-04-16T16:14:15+05:30", "updatedAt": "2026-04-07T12:08:41+05:30", "translation": { "id": "/api/shop/theme_customization_translations/6", "_id": 6, "themeCustomizationId": "6", "locale": "en", "options": "{\"css\": \".section-gap{margin-top:80px} ...\", \"html\": \"
...

Get Ready for our new Bold Collections!

...
\"}" }, "translations": { "edges": [ { "cursor": "MA==", "node": { "id": "/api/shop/theme_customization_translations/6", "_id": 6, "themeCustomizationId": "6", "locale": "en", "options": "{\"css\": \".section-gap{margin-top:80px} ...\", \"html\": \"
...

Get Ready for our new Bold Collections!

...
\"}" } }, { "cursor": "MQ==", "node": { "id": "/api/shop/theme_customization_translations/23", "_id": 23, "themeCustomizationId": "6", "locale": "AR", "options": "{\"css\": \".section-gap{margin-top:80px} ...\", \"html\": \"
...

استعدوا لمجموعاتنا الجديدة الجريئة!

...
\"}" } } ], "pageInfo": { "endCursor": "MQ==", "startCursor": "MA==", "hasNextPage": false, "hasPreviousPage": false }, "totalCount": 2 } }, "cursor": "MQ==" } ], "pageInfo": { "endCursor": "MQ==", "startCursor": "MA==", "hasNextPage": true, "hasPreviousPage": false }, "totalCount": 4 } } } commonErrors: - error: invalid-pagination cause: Invalid pagination arguments or exceeding maximum limit solution: Use valid first/after or last/before combinations with max value 100 - error: invalid-type-filter cause: Invalid type filter value solution: Use valid type values like footer_links, image_carousel, product_carousel, category_carousel, static_content, etc. --- # Theme Customizations ## About The `themeCustomizations` query retrieves configurable theme data for the storefront. Use this query to: - Fetch home page sliders and carousels - Display footer links and static content sections - Retrieve category and product carousel configurations - Get theme-specific customization options - Access multi-language translations for theme content - Implement dynamic theme content based on type filters - Display theme customizations with complete metadata This query supports pagination with cursor-based navigation and type-based filtering for retrieving specific customization categories. ## Arguments | Argument | Type | Required | Description | |----------|------|----------|-------------| | `first` | `Int` | ❌ No | Number of results to return (forward pagination). Max: 100. | | `after` | `String` | ❌ No | Pagination cursor for forward navigation. Use with `first`. | | `last` | `Int` | ❌ No | Number of results for backward pagination. Max: 100. | | `before` | `String` | ❌ No | Pagination cursor for backward navigation. Use with `last`. | | `type` | `String` | ❌ No | Filter by customization type (e.g., `footer_links`, `image_carousel`, `product_carousel`, `category_carousel`, `static_content`). | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `id` | `ID!` | Unique theme customization API identifier. | | `_id` | `Int!` | Numeric customization ID. | | `themeCode` | `String!` | Theme code/identifier (e.g., 'default'). | | `type` | `String!` | Customization type (e.g., 'footer_links', 'image_carousel', 'product_carousel', 'category_carousel', 'static_content'). | | `name` | `String!` | Human-readable name of the customization. | | `sortOrder` | `Int` | Sort order for display. | | `status` | `String` | Status flag (0 = inactive, 1 = active). | | `channelId` | `String` | Associated channel ID. | | `createdAt` | `DateTime!` | Customization creation timestamp. | | `updatedAt` | `DateTime!` | Last update timestamp. | | `translation` | `ThemeCustomizationTranslation!` | Default locale translation. | | `translation.id` | `ID!` | Translation identifier. | | `translation._id` | `Int!` | Numeric translation ID. | | `translation.themeCustomizationId` | `String!` | Associated customization ID. | | `translation.locale` | `String!` | Language locale code (e.g., 'en', 'ar', 'fr'). | | `translation.options` | `String!` | JSON-formatted options/configuration for this translation. | | `translations` | `ThemeCustomizationTranslationCollection!` | All available translations. | | `translations.edges` | `[Edge!]!` | Translation edges with cursors. | | `translations.pageInfo` | `PageInfo!` | Pagination info for translations. | | `translations.totalCount` | `Int!` | Total translations for this customization. | | `pageInfo` | `PageInfo!` | Pagination information. | | `pageInfo.hasNextPage` | `Boolean!` | Whether more pages exist forward. | | `pageInfo.hasPreviousPage` | `Boolean!` | Whether more pages exist backward. | | `pageInfo.startCursor` | `String` | Cursor for first item in page. | | `pageInfo.endCursor` | `String` | Cursor for last item in page. | | `totalCount` | `Int!` | Total customizations matching filters. | ## Common Customization Types | Type | Description | Usage | |------|-------------|-------| | `image_carousel` | Image slider/carousel on home page | Homepage promotions and banners | | `product_carousel` | Product carousel display | Featured, new, or special products | | `category_carousel` | Category carousel display | Category promotions | | `static_content` | HTML/CSS static sections | Custom HTML blocks | | `footer_links` | Footer navigation links | Footer menu items | | `services_content` | Services information | Additional service blocks | ## Use Cases ### 1. Home Page Sliders Use the "Filtered by Type" example with `type: "image_carousel"` to fetch home page sliders. ### 2. Footer Links Use the "Filtered by Type" example with `type: "footer_links"` to display footer links. ### 3. Product Carousels Use the "Filtered by Type" example with `type: "product_carousel"` to display featured products. ### 4. Multi-Language Support Use the "Complete Details" example to get all translations for any customization type. ### 5. Paginated List Use the "Basic" example with pagination arguments to load customizations progressively. ## Best Practices 1. **Use Type Filters** - Always filter by type when you only need specific customizations 2. **Paginate Results** - Use pagination for better performance with large datasets 3. **Request Only Needed Fields** - Minimize data transfer by selecting only required fields 4. **Cache Translations** - Theme customizations change infrequently, cache the full response 5. **Parse JSON Options** - The `options` field contains JSON; parse it in your application 6. **Check Status** - Verify status is active before displaying in frontend ## Related Resources - [Get Category](/api/graphql-api/shop/queries/get-category) - Query individual category details - [Get Categories](/api/graphql-api/shop/queries/categories) - Query paginated categories - [Get Products](/api/graphql-api/shop/queries/get-products) - Query products for carousels - [Pagination Guide](/api/graphql-api/pagination) - Cursor pagination documentation - [Shop API Overview](/api/graphql-api/shop-api) - Overview of Shop API resources --- # Tree Categories URL: /api/graphql-api/shop/queries/tree-categories --- outline: false examples: - id: tree-categories-basic title: Tree Categories - Basic description: Retrieve root and child categories with their basic hierarchical structure. query: | query treeCategories { treeCategories(parentId: 1) { id _id position status translation { name slug urlPath } children(first: 100) { edges { node { id _id position status translation { name slug urlPath } } } } } } variables: | {} response: | { "data": { "treeCategories": [ { "id": "/api/shop/categories/8", "_id": 8, "position": 2, "status": "1", "translation": { "name": "Electronics", "slug": "electronics", "urlPath": "electronics" }, "children": { "edges": [] } }, { "id": "/api/shop/categories/23", "_id": 23, "position": 3, "status": "1", "translation": { "name": "Furniture", "slug": "furniture", "urlPath": "" }, "children": { "edges": [ { "node": { "id": "/api/shop/categories/19", "_id": 19, "position": 7, "status": "1", "translation": { "name": "Plastic Sofa", "slug": "plastic-sofa", "urlPath": "" } } }, { "node": { "id": "/api/shop/categories/20", "_id": 20, "position": 6, "status": "1", "translation": { "name": "Wooden Sofa", "slug": "wooden-sofa", "urlPath": "" } } }, { "node": { "id": "/api/shop/categories/21", "_id": 21, "position": 5, "status": "1", "translation": { "name": "Leather Sofa", "slug": "leather-sofa", "urlPath": "" } } } ] } }, { "id": "/api/shop/categories/22", "_id": 22, "position": 4, "status": "1", "translation": { "name": "Fashion", "slug": "fashion", "urlPath": "" }, "children": { "edges": [] } } ] } } commonErrors: - error: INVALID_PARENT_ID cause: Parent category ID format is invalid or does not exist solution: Provide a valid parent category ID - id: tree-categories-complete title: Tree Categories - Complete Details description: Retrieve categories with all fields including logos, banners, display mode, and translations. query: | query treeCategories { treeCategories(parentId: 1) { id _id position status logoPath displayMode _lft _rgt additional bannerPath createdAt updatedAt url logoUrl bannerUrl translation { name slug urlPath } children(first: 100) { edges { node { id _id position status translation { name slug urlPath } } } } translations(first: 1) { edges { node { id _id categoryId name slug urlPath description metaTitle metaDescription metaKeywords localeId locale } cursor } pageInfo { endCursor startCursor hasNextPage hasPreviousPage } totalCount } } } variables: | {} response: | { "data": { "treeCategories": [ { "id": "/api/shop/categories/8", "_id": 8, "position": 2, "status": "1", "logoPath": "category/8/Vk59z6w128ExCrY3lwlSYWhVrYenucFhTuick0VD.webp", "displayMode": "products_and_description", "_lft": "14", "_rgt": "15", "additional": null, "bannerPath": null, "createdAt": "2024-04-19T13:36:12+05:30", "updatedAt": "2026-01-02T19:23:45+05:30", "url": "https://api-demo.bagisto.com/electronics", "logoUrl": "https://api-demo.bagisto.com/storage/category/8/Vk59z6w128ExCrY3lwlSYWhVrYenucFhTuick0VD.webp", "bannerUrl": null, "translation": { "name": "Electronics", "slug": "electronics", "urlPath": "electronics" }, "children": { "edges": [] }, "translations": { "edges": [ { "node": { "id": "/api/shop/category_translations/60", "_id": 60, "categoryId": "8", "name": "Electronics", "slug": "electronics", "urlPath": "electronics", "description": "

Discover a wide range of cutting-edge electronics, from smartphones and laptops to home appliances and gadgets.

", "metaTitle": "Electronics", "metaDescription": "", "metaKeywords": "electronics, electronics-keyboard", "localeId": "1", "locale": "en" }, "cursor": "MA==" } ], "pageInfo": { "endCursor": "MA==", "startCursor": "MA==", "hasNextPage": true, "hasPreviousPage": false }, "totalCount": 2 } }, { "id": "/api/shop/categories/23", "_id": 23, "position": 3, "status": "1", "logoPath": "category/23/GuIZOJY3oW09ku4zqxIfKvtXho9gOnq4eCl0HmOW.webp", "displayMode": "products_and_description", "_lft": "18", "_rgt": "25", "additional": null, "bannerPath": null, "createdAt": "2025-09-03T12:43:50+05:30", "updatedAt": "2025-09-03T18:26:45+05:30", "url": "https://api-demo.bagisto.com/furniture", "logoUrl": "https://api-demo.bagisto.com/storage/category/23/GuIZOJY3oW09ku4zqxIfKvtXho9gOnq4eCl0HmOW.webp", "bannerUrl": null, "translation": { "name": "Furniture", "slug": "furniture", "urlPath": "" }, "children": { "edges": [ { "node": { "id": "/api/shop/categories/19", "_id": 19, "position": 7, "status": "1", "translation": { "name": "Plastic Sofa", "slug": "plastic-sofa", "urlPath": "" } } }, { "node": { "id": "/api/shop/categories/20", "_id": 20, "position": 6, "status": "1", "translation": { "name": "Wooden Sofa", "slug": "wooden-sofa", "urlPath": "" } } }, { "node": { "id": "/api/shop/categories/21", "_id": 21, "position": 5, "status": "1", "translation": { "name": "Leather Sofa", "slug": "leather-sofa", "urlPath": "" } } } ] }, "translations": { "edges": [ { "node": { "id": "/api/shop/category_translations/195", "_id": 195, "categoryId": "23", "name": "Furniture", "slug": "furniture", "urlPath": "", "description": "

Discover our wide range of furniture designed to bring comfort, style, and functionality to every corner of your home.

", "metaTitle": "", "metaDescription": "", "metaKeywords": "", "localeId": "1", "locale": "en" }, "cursor": "MA==" } ], "pageInfo": { "endCursor": "MA==", "startCursor": "MA==", "hasNextPage": true, "hasPreviousPage": false }, "totalCount": 2 } }, { "id": "/api/shop/categories/22", "_id": 22, "position": 4, "status": "1", "logoPath": "category/22/MDbnYET88gzG1ipz3ClxiKSO2wOybEzESa0o0jHc.webp", "displayMode": "products_and_description", "_lft": "16", "_rgt": "17", "additional": null, "bannerPath": null, "createdAt": "2025-08-28T18:52:22+05:30", "updatedAt": "2026-01-02T19:24:08+05:30", "url": "https://api-demo.bagisto.com/fashion", "logoUrl": "https://api-demo.bagisto.com/storage/category/22/MDbnYET88gzG1ipz3ClxiKSO2wOybEzESa0o0jHc.webp", "bannerUrl": null, "translation": { "name": "Fashion", "slug": "fashion", "urlPath": "" }, "children": { "edges": [] }, "translations": { "edges": [ { "node": { "id": "/api/shop/category_translations/186", "_id": 186, "categoryId": "22", "name": "Fashion", "slug": "fashion", "urlPath": "", "description": "

Explore the latest trends in fashion with our curated collection of clothing, accessories, and footwear.

", "metaTitle": "", "metaDescription": "", "metaKeywords": "", "localeId": "1", "locale": "en" }, "cursor": "MA==" } ], "pageInfo": { "endCursor": "MA==", "startCursor": "MA==", "hasNextPage": true, "hasPreviousPage": false }, "totalCount": 2 } } ] } } commonErrors: - error: INVALID_PARENT_ID cause: Parent category ID is invalid or does not exist solution: Use a valid parent category ID - id: tree-categories-with-parent-filter title: Tree Categories - Filter by Parent ID description: Retrieve categories filtered by a specific parent ID to get a subtree. query: | query treeCategories { treeCategories(parentId: 23) { id _id position status logoPath displayMode translation { name slug urlPath } children(first: 50) { edges { node { id _id position status translation { name slug urlPath } } } } } } variables: | {} response: | { "data": { "treeCategories": [ { "id": "/api/shop/categories/21", "_id": 21, "position": 5, "status": "1", "logoPath": "category/21/Q8Z5RUYiBwPVKVkJNJ0XOfWitDiqP7admksTYxKm.webp", "displayMode": "products_and_description", "translation": { "name": "Leather Sofa", "slug": "leather-sofa", "urlPath": "" }, "children": { "edges": [] } }, { "id": "/api/shop/categories/20", "_id": 20, "position": 6, "status": "1", "logoPath": "category/20/WO71UuFFtbSRbjZVr7QUbNuMZM4PRSIAjHSLqUUY.webp", "displayMode": "products_and_description", "translation": { "name": "Wooden Sofa", "slug": "wooden-sofa", "urlPath": "" }, "children": { "edges": [] } }, { "id": "/api/shop/categories/19", "_id": 19, "position": 7, "status": "1", "logoPath": "category/19/pmfWVVuhj7VK4dXFZG1ZlBeaUPwLrE4Ua99oer9l.webp", "displayMode": "products_and_description", "translation": { "name": "Plastic Sofa", "slug": "plastic-sofa", "urlPath": "" }, "children": { "edges": [] } } ] } } commonErrors: - error: INVALID_PARENT_ID cause: Parent category ID is invalid solution: Provide a valid parent category ID that exists in the system --- # Tree Categories ## About The `treeCategories` query retrieves categories in a hierarchical tree structure, useful for navigation menus and category browsing. This query is essential for: - Building category navigation menus - Displaying category hierarchies for storefront - Managing nested category structures - Fetching categories with their parent-child relationships - Building breadcrumb navigation - Creating category tree widgets The query returns an array of categories (not a paginated connection) with nested children and translations. Use the `parentId` argument to filter categories by their parent. ## Arguments | Argument | Type | Required | Description | |----------|------|----------|-------------| | `parentId` | `Int` | ✅ Yes | The numeric ID of the parent category to filter by. Specifies which level of the tree to retrieve. | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `treeCategories` | `[Category!]!` | Array of category objects matching the parent ID filter. | ## Category Fields | Field | Type | Description | |-------|------|-------------| | `id` | `String!` | Unique identifier in format `/api/shop/categories/{id}` | | `_id` | `Int!` | Numeric identifier for the category | | `position` | `Int!` | Display order position of the category | | `status` | `String!` | Status of the category ("1" for active, "0" for inactive) | | `logoPath` | `String` | File path to the category logo | | `displayMode` | `String` | Display mode (e.g., "products_and_description", "products_only") | | `_lft` | `String` | Left pointer for nested set tree structure | | `_rgt` | `String` | Right pointer for nested set tree structure | | `additional` | `Mixed` | Additional category attributes | | `bannerPath` | `String` | File path to the category banner | | `createdAt` | `String!` | Creation timestamp (ISO 8601 format) | | `updatedAt` | `String!` | Last update timestamp (ISO 8601 format) | | `url` | `String` | Full URL to the category page | | `logoUrl` | `String` | Full URL to the category logo image | | `bannerUrl` | `String` | Full URL to the category banner image | | `translation` | `CategoryTranslation` | Default translation object with name, slug, and urlPath | | `translations` | `Connection` | Paginated translations for all locales | | `children` | `Connection` | Paginated child categories with their details | ## Common Use Cases ### Get Root Categories for Main Menu ```graphql query GetRootCategories { treeCategories(parentId: 1) { id _id position status translation { name slug urlPath } logoUrl children(first: 50) { edges { node { id position translation { name slug } } } } } } ``` ### Get Category Tree with Full Details ```graphql query GetCategoryTree { treeCategories(parentId: 1) { id _id position status logoPath logoUrl bannerUrl displayMode url translation { name slug description } children(first: 100) { edges { node { id position translation { name slug urlPath } } } } } } ``` ### Get Specific Subtree by Parent ```graphql query GetCategorySubtree { treeCategories(parentId: 2) { id position translation { name slug } children(first: 50) { edges { node { id position translation { name slug urlPath } } } } } } ``` ### Get Categories with Translations ```graphql query GetCategoriesWithTranslations { treeCategories(parentId: 1) { id position translation { name slug description } translations(first: 10) { edges { node { locale name slug description } } totalCount } children(first: 100) { edges { node { id position translation { name } } } } } } ``` ## Error Handling ### Invalid Parent ID - Non-integer Value ```json { "errors": [ { "message": "Int cannot represent non-integer value: dffddf" } ] } ``` ### Invalid Parent ID - String Instead of Integer ```json { "errors": [ { "message": "Int cannot represent non-integer value: \"1111\"" } ] } ``` ### Parent ID Not Found ```json { "data": { "treeCategories": [] } } ``` ## Best Practices 1. **Always Provide parentId** - The parentId parameter is required 2. **Use First for Children Pagination** - Limit child categories with the `first` argument (e.g., `first: 100`) 3. **Request Only Needed Fields** - Reduce payload by selecting specific fields 4. **Cache Navigation Data** - Categories change infrequently; implement caching 5. **Handle Status Filtering** - Filter by status="1" on client side if needed 6. **Use Translation Fields** - Include translation data for multi-language support 7. **Paginate Nested Collections** - Use `first` argument for children and translations 8. **Use Position Field** - Order results by position field for proper display ## Related Resources - [Get Single Category](/api/graphql-api/shop/queries/get-category) - Retrieve a single category by ID - [Get Categories](/api/graphql-api/shop/queries/categories) - List all categories with pagination - [Pagination Guide](/api/graphql-api/pagination) - Cursor pagination documentation - [Shop API Overview](/api/graphql-api/shop-api) - Overview of Shop API resources --- # Shop API - Reviews URL: /api/graphql-api/shop/reviews # Shop API - Reviews Manage and view product reviews and ratings. ## Get Product Reviews Retrieve all reviews for a specific product. ```graphql query GetReviews($productId: String!, $first: Int!) { reviews(productId: $productId, first: $first) { pageInfo { hasNextPage endCursor } edges { node { id title rating comment customerName email status createdAt } } } } ``` **Variables:** ```json { "productId": "1", "first": 10 } ``` ## Get Single Review Retrieve details of a specific review. ```graphql query GetReview($id: String!) { review(id: $id) { id title rating comment customerName email createdAt } } ``` ## Create Product Review Submit a new product review. ```graphql mutation CreateReview($input: CreateReviewInput!) { createReview(input: $input) { review { id title rating comment status createdAt } } } ``` **Variables:** ```json { "input": { "productId": "1", "title": "Great Product!", "rating": 5, "comment": "Excellent quality and fast shipping.", "name": "John Doe", "email": "john@example.com" } } ``` ## Related Resources - [Products](/api/graphql-api/shop/products) - [Orders](/api/graphql-api/shop/orders) --- # Integration Guides URL: /api/integrations # Integration Guides Real-world examples and step-by-step guides for integrating Bagisto APIs into your application architecture. Choose the integration pattern that best fits your use case. ## Common Integration Patterns ### 1. Headless Commerce (React/Vue Frontend) Build a modern storefront using your preferred frontend framework. **Architecture:** ``` Frontend (React/Vue/Next.js) ↓ Bagisto GraphQL API ↓ Bagisto Backend ``` **Steps:** 1. **Install Apollo Client (React example)** ```bash npm install @apollo/client graphql ``` 2. **Setup Apollo Client** ```javascript import ApolloClient from '@apollo/client'; import { createHttpLink } from '@apollo/client'; const httpLink = createHttpLink({ uri: 'https://your-domain.com/api/graphql', credentials: 'include', }); const client = new ApolloClient({ link: httpLink, cache: new InMemoryCache(), }); ``` 3. **Fetch Products** ```javascript import { gql, useQuery } from '@apollo/client'; const GET_PRODUCTS = gql` query { products(first: 10) { edges { node { id name price image } } } } `; function ProductList() { const { loading, error, data } = useQuery(GET_PRODUCTS); if (loading) return
Loading...
; if (error) return
Error: {error.message}
; return (
{data.products.edges.map(({ node }) => (

{node.name}

${node.price}

))}
); } ``` **Best Practices:** - Cache responses for better performance - Use lazy loading for images - Implement error boundaries - Use real-time subscriptions for inventory updates --- ### 2. Mobile App Integration (React Native/Flutter) Connect your mobile app to Bagisto APIs. **Architecture:** ``` Mobile App (React Native/Flutter) ↓ REST or GraphQL API ↓ Bagisto Backend ``` **REST API Example (React Native):** ```javascript // Auth Service class AuthService { async login(email, password) { const response = await fetch('https://your-domain.com/api/shop/customer/login', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-STOREFRONT-KEY': process.env.STOREFRONT_KEY, }, body: JSON.stringify({ email, password }), }); const data = await response.json(); if (response.ok) { // Store token securely await AsyncStorage.setItem('auth_token', data.token); return data; } throw new Error(data.message); } async getProfile() { const token = await AsyncStorage.getItem('auth_token'); const response = await fetch('https://your-domain.com/api/shop/customers', { headers: { 'Authorization': `Bearer ${token}`, 'X-STOREFRONT-KEY': process.env.STOREFRONT_KEY, }, }); return response.json(); } } ``` **GraphQL Example (Apollo Client):** ```javascript import AsyncStorage from '@react-native-async-storage/async-storage'; const authLink = setContext(async (_, { headers }) => { const token = await AsyncStorage.getItem('auth_token'); return { headers: { ...headers, authorization: token ? `Bearer ${token}` : '', 'X-STOREFRONT-KEY': process.env.STOREFRONT_KEY, }, }; }); const httpLink = createHttpLink({ uri: 'https://your-domain.com/api/graphql', }); const client = new ApolloClient({ link: authLink.concat(httpLink), cache: new InMemoryCache(), }); ``` **Best Practices:** - Store tokens securely using device secure storage - Implement token refresh mechanism - Handle offline state gracefully - Optimize images for mobile networks --- ### 3. Third-Party Integration Integrate Bagisto with external services (ERP, CRM, Payment Gateway). **Architecture:** ``` External Service (Shopify, WooCommerce, etc.) ↓ REST API (for webhook handling) ↓ Bagisto Backend ``` **Webhook Handler Example:** ```php // routes/api.php Route::post('/webhooks/product-sync', 'WebhookController@productSync'); // App/Http/Controllers/WebhookController.php namespace App\Http\Controllers; use Illuminate\Http\Request; class WebhookController extends Controller { public function productSync(Request $request) { $externalProduct = $request->validate([ 'id' => 'required|string', 'name' => 'required|string', 'sku' => 'required|string', 'price' => 'required|numeric', ]); // Create or update product in Bagisto $product = Product::updateOrCreate( ['sku' => $externalProduct['sku']], [ 'name' => $externalProduct['name'], 'price' => $externalProduct['price'], ] ); return response()->json(['success' => true, 'id' => $product->id]); } } ``` **Sync Products Periodically:** ```php // Console/Commands/SyncExternalProducts.php namespace App\Console\Commands; use Illuminate\Console\Command; class SyncExternalProducts extends Command { protected $signature = 'products:sync-external'; public function handle() { $externalAPI = 'https://external-service.com/api/products'; $response = Http::get($externalAPI, [ 'api_key' => config('services.external.key'), ]); foreach ($response->json('products') as $product) { Product::updateOrCreate( ['external_id' => $product['id']], [ 'name' => $product['name'], 'price' => $product['price'], 'sku' => $product['sku'], ] ); } $this->info('Products synced successfully'); } } // Schedule in app/Console/Kernel.php protected function schedule(Schedule $schedule) { $schedule->command('products:sync-external')->hourly(); } ``` **Best Practices:** - Implement retry logic for failed requests - Log all sync operations for audit trail - Handle duplicate data gracefully - Use batch operations for bulk updates --- ### 4. Admin Dashboard Integration Build a custom admin dashboard for inventory and order management. **Architecture:** ``` Custom Admin Dashboard (Vue/React) ↓ Admin API (REST/GraphQL) ↓ Bagisto Backend (with Admin Auth) ``` **Vue.js Dashboard Example:** ```javascript // Dashboard.vue ``` **Best Practices:** - Implement role-based access control - Cache frequently accessed data - Use websockets for real-time updates - Validate all user input server-side --- ## Quick Decision Guide | Use Case | Recommended API | Why | |----------|-----------------|-----| | **Frontend Website** | GraphQL | Flexible data fetching, reduce overfetching | | **Mobile App** | REST or GraphQL | Simpler than REST, smaller payloads | | **Third-Party Sync** | REST | Standard HTTP methods, easier webhooks | | **Admin Dashboard** | GraphQL or REST | Both work, GraphQL is faster for complex queries | | **IoT Devices** | REST | Lightweight, minimal dependencies | --- ## Error Handling Always implement proper error handling in your integrations: **REST API Error Handling:** ```javascript async function apiCall(endpoint, options = {}) { try { const response = await fetch(endpoint, options); if (!response.ok) { const error = await response.json(); throw new Error(error.message || 'API Error'); } return await response.json(); } catch (error) { console.error('API Error:', error.message); // Implement retry logic or user notification throw error; } } ``` **GraphQL Error Handling:** ```javascript const errorLink = onError(({ graphQLErrors, networkError }) => { if (graphQLErrors) { graphQLErrors.forEach(({ message, locations, path }) => { console.error(`GraphQL Error: ${message}`, { locations, path }); }); } if (networkError) { console.error('Network Error:', networkError); } }); const client = new ApolloClient({ link: errorLink.concat(httpLink), cache: new InMemoryCache(), }); ``` --- ## Performance Optimization ### Caching Strategy ```javascript // REST API with caching const cache = new Map(); const CACHE_DURATION = 5 * 60 * 1000; // 5 minutes async function cachedFetch(url) { const cached = cache.get(url); if (cached && Date.now() - cached.time < CACHE_DURATION) { return cached.data; } const data = await fetch(url).then(r => r.json()); cache.set(url, { data, time: Date.now() }); return data; } ``` ### Pagination ```graphql # Efficient pagination with cursors query { products(first: 20, after: "cursor-value") { pageInfo { hasNextPage endCursor } edges { node { id name } } } } ``` --- ## Security Best Practices 1. **Never expose API keys in client code** ```javascript // ❌ Bad const KEY = 'sk_live_xxxxx'; // Visible in browser // ✅ Good const KEY = process.env.REACT_APP_API_KEY; // From .env file ``` 2. **Always use HTTPS** - All API requests must use `https://` - Never send sensitive data over HTTP 3. **Validate tokens** ```javascript // Check token expiration before use if (token && !isTokenExpired(token)) { // Make API call } ``` 4. **Implement rate limiting awareness** - Respect rate limit headers from API - Implement exponential backoff for retries --- ## What's Next? - 🔐 [Authentication Guide](./authentication) - Learn detailed auth methods - 🔗 [REST API Reference](./rest-api/introduction.html) - REST API documentation - ⚡ [GraphQL API Reference](./graphql-api/introduction.html) - GraphQL documentation - 🔑 [API Key Management](./storefront-api-key-management-guide) - Manage your API keys ## Support & Resources - 🌐 [GitHub Repository](https://github.com/bagisto/bagisto-api) - 💬 [Community Forum](https://forums.bagisto.com) - 🐛 [Issue Tracker](https://github.com/bagisto/bagisto-api/issues) - 📧 [Contact Support](https://bagisto.com/en/contacts/) --- # Introduction URL: /api/introduction # Introduction Bagisto provides comprehensive API solutions to help developers integrate and extend the platform's functionality. Whether you're building mobile apps, third-party integrations, or headless commerce solutions, our APIs offer the flexibility and power you need. ## Available API Types ### GraphQL API The Bagisto GraphQL API enables flexible, efficient data fetching with a single endpoint. Ideal for: - **Headless Commerce** - Power modern frontend frameworks - **Mobile Apps** - Reduce bandwidth with precise data queries - **Custom Storefronts** - Build unique shopping experiences - **Real-time Applications** - Efficient data synchronization **Key Features:** - Single endpoint for all operations - Flexible query structure - fetch exactly what you need - Real-time subscriptions support - Built on Platform API Laravel - Type-safe schema with introspection ::: tip Modern Development GraphQL is perfect for modern frontend frameworks like React, Vue, and React Native. Check out our [GraphQL API Guide](./graphql-api) to get started. ::: ### REST API The Bagisto REST API follows RESTful principles and provides complete access to CRUD operations across all Bagisto features. Perfect for: - **Mobile Applications** - Build native iOS/Android shopping apps - **Third-party Integrations** - Connect with external systems and services - **Progressive Web Apps (PWA)** - Create fast, app-like web experiences - **Custom Admin Interfaces** - Build specialized admin tools **Key Features:** - Full CRUD operations support - Built-in pagination for performance - Comprehensive documentation with interactive testing - Laravel Sanctum authentication ::: tip Getting Started New to REST APIs? Start with our [REST API Guide](./rest-api) for installation steps and examples. ::: ## What's Next? Ready to start building? Choose your preferred API approach: - ⚡ [GraphQL API Guide](./graphql-api/introduction.html) - Modern GraphQL for flexible queries - 🔗 [REST API Guide](./rest-api/introduction.html) - RESTful API for traditional integrations - 📚 [Installation](./setup) - Installation and setup - 🔐 [Authentication](./authentication) - Authentication methods - 📊 [Rate Limiting](./rate-limiting) - Understanding API rate limits - 🚀 [Integration Guides](./integrations) - Real-world integration examples --- # Rate Limiting URL: /api/rate-limiting # Rate Limiting Bagisto APIs implement rate limiting to ensure fair usage and protect infrastructure from abuse. This guide explains how rate limiting works and how to handle rate limit responses. ## Overview Rate limiting protects the API from excessive requests by restricting the number of API calls a client can make in a given time window. Each Storefront API Key has its own rate limit quota. **Default Limits:** - **Public APIs (Shop)**: 100 requests per minute per API key - **Customer APIs**: 100 requests per minute per API key - **Admin APIs**: Configurable (higher limits for admin operations) ## Rate Limit Headers Every API response includes rate limit information in the response headers: ``` X-RateLimit-Limit: 100 X-RateLimit-Remaining: 95 X-RateLimit-Reset: 1767967200 ``` | Header | Description | Type | Example | |--------|-------------|------|---------| | `X-RateLimit-Limit` | Maximum requests allowed in the current window | integer | `100` | | `X-RateLimit-Remaining` | Number of requests remaining in current window | integer | `95` | | `X-RateLimit-Reset` | Unix timestamp when the rate limit window resets | integer | `1767967200` | ## Rate Limit Window Rate limits are calculated on a **per-minute basis**: - The window resets every 60 seconds - `X-RateLimit-Reset` shows when the current window expires (Unix timestamp) - After the reset time, your request quota is refreshed **Example Timeline:** ``` Time: 13:00:00 UTC Requests made: 5 Remaining: 95 Reset at: 13:01:00 UTC (Unix: 1767967200) Time: 13:00:30 UTC (30 seconds later) Requests made: 15 (total: 20) Remaining: 80 Reset at: 13:01:00 UTC (Unix: 1767967200) Time: 13:01:00 UTC (reset) Requests made: 0 Remaining: 100 (quota refreshed!) Reset at: 13:02:00 UTC (Unix: 1767967200 + 60) ``` ## Handling Rate Limit Exceeded When you exceed the rate limit, you'll receive a **429 Too Many Requests** response: ```http HTTP/1.1 429 Too Many Requests Content-Type: application/json X-RateLimit-Limit: 100 X-RateLimit-Remaining: 0 X-RateLimit-Reset: 1767967200 Retry-After: 52 ``` **Response Body:** ```json { "message": "Rate limit exceeded", "error": "rate_limit_exceeded", "retry_after": 52 } ``` | Field | Description | Example | |-------|-------------|---------| | `message` | Human-readable error message | `Rate limit exceeded` | | `error` | Machine-readable error code | `rate_limit_exceeded` | | `retry_after` | Seconds to wait before retrying | `52` | ## Configuring Rate Limits ### Per-API Key Configuration Set custom rate limits when creating a Storefront API Key: ```bash # Create key with custom rate limit (requests per minute) php artisan bagisto-api:generate-key --name="Mobile App" --rate-limit=500 # Output: # ✓ API key generated successfully! # Key: sk_live_xxxxxxxxxxxxx # Name: Mobile App # Rate Limit: 500 requests/minute # Status: Active ``` ### Environment Configuration Set the default rate limit in your `.env` file: ```bash # .env # Default rate limit for all new API keys (requests per minute) BAGISTO_API_RATE_LIMIT=100 # Or configure per API type: BAGISTO_API_RATE_LIMIT_PUBLIC=100 BAGISTO_API_RATE_LIMIT_CUSTOMER=100 BAGISTO_API_RATE_LIMIT_ADMIN=500 ``` ### Configuration File Configure rate limiting in `config/bagisto-api.php`: ```php return [ 'rate_limit' => [ // Default rate limit for new keys (requests per minute) 'default' => env('BAGISTO_API_RATE_LIMIT', 100), // Per API type limits 'shop' => env('BAGISTO_API_RATE_LIMIT_PUBLIC', 100), 'customer' => env('BAGISTO_API_RATE_LIMIT_CUSTOMER', 100), 'admin' => env('BAGISTO_API_RATE_LIMIT_ADMIN', 500), // Window duration (in seconds) 'window' => 60, // Enable rate limiting 'enabled' => true, ], ]; ``` ## Checking Current Rate Limit Use the rate limit headers to monitor your quota: ```javascript // JavaScript/Fetch fetch('https://your-domain.com/api/shop/products', { headers: { 'X-STOREFRONT-KEY': 'pk_storefront_xxxxx' } }) .then(response => { const limit = response.headers.get('X-RateLimit-Limit'); const remaining = response.headers.get('X-RateLimit-Remaining'); const reset = response.headers.get('X-RateLimit-Reset'); console.log(`Limit: ${limit}, Remaining: ${remaining}, Reset: ${reset}`); return response.json(); }) .catch(error => console.error(error)); ``` ## Best Practices ### 1. Monitor Rate Limit Headers Always check the `X-RateLimit-Remaining` header to know when you're approaching the limit: ```javascript async function makeRequest(url, options) { const response = await fetch(url, options); const limit = parseInt(response.headers.get('X-RateLimit-Limit')); const remaining = parseInt(response.headers.get('X-RateLimit-Remaining')); if (remaining < limit * 0.1) { console.warn(`Warning: Only ${remaining} requests remaining`); } return response.json(); } ``` ### 2. Implement Exponential Backoff When rate limited, use exponential backoff before retrying: ```javascript async function makeRequestWithRetry(url, options, maxRetries = 3) { let attempt = 0; while (attempt < maxRetries) { try { const response = await fetch(url, options); if (response.status === 429) { const retryAfter = parseInt(response.headers.get('Retry-After')) || 60; const delay = Math.pow(2, attempt) * retryAfter * 1000; console.log(`Rate limited. Retrying after ${delay}ms`); await new Promise(r => setTimeout(r, delay)); attempt++; continue; } return response.json(); } catch (error) { console.error('Request failed:', error); throw error; } } throw new Error('Max retries exceeded'); } ``` ### 3. Batch Requests Combine multiple operations into batch requests to reduce API calls: ```javascript // ❌ Bad: Multiple requests for (let id of productIds) { const product = await fetch(`/api/shop/products/${id}`); } // ✅ Good: Batch request const products = await fetch('/api/shop/products?ids=1,2,3,4,5'); ``` ### 4. Cache Responses Cache API responses to reduce unnecessary requests: ```javascript const cache = new Map(); const CACHE_TTL = 5 * 60 * 1000; // 5 minutes async function cachedFetch(url) { const now = Date.now(); const cached = cache.get(url); if (cached && now - cached.time < CACHE_TTL) { return cached.data; } const data = await fetch(url).then(r => r.json()); cache.set(url, { data, time: now }); return data; } ``` ### 5. Queue Requests Spread requests over time to avoid burst traffic: ```javascript class RequestQueue { constructor(rateLimit = 10) { this.queue = []; this.processing = false; this.requestsPerSecond = rateLimit; } async add(request) { return new Promise((resolve, reject) => { this.queue.push({ request, resolve, reject }); this.process(); }); } async process() { if (this.processing || this.queue.length === 0) return; this.processing = true; while (this.queue.length > 0) { const { request, resolve, reject } = this.queue.shift(); try { const result = await request(); resolve(result); } catch (error) { reject(error); } // Delay between requests await new Promise(r => setTimeout(r, 1000 / this.requestsPerSecond)); } this.processing = false; } } // Usage const queue = new RequestQueue(10); // 10 requests per second queue.add(() => fetch('/api/shop/products/1')); queue.add(() => fetch('/api/shop/products/2')); queue.add(() => fetch('/api/shop/products/3')); ``` ### 6. Handle Rate Limit Gracefully Provide user feedback when rate limited: ```javascript async function makeRequestWithFallback(url, options) { try { const response = await fetch(url, options); if (response.status === 429) { const data = await response.json(); const waitTime = data.retry_after; // Show user message console.error(`API temporarily unavailable. Please try again in ${waitTime} seconds`); // Retry after waiting await new Promise(r => setTimeout(r, waitTime * 1000)); return makeRequestWithFallback(url, options); } return response.json(); } catch (error) { console.error('Request failed:', error); throw error; } } ``` ## Monitoring Rate Limit Usage ### Track Usage Over Time ```javascript class RateLimitMonitor { constructor() { this.stats = { totalRequests: 0, rateLimitedRequests: 0, averageRemaining: 0 }; } trackResponse(response) { const remaining = parseInt(response.headers.get('X-RateLimit-Remaining')); const limit = parseInt(response.headers.get('X-RateLimit-Limit')); this.stats.totalRequests++; if (response.status === 429) { this.stats.rateLimitedRequests++; } this.stats.averageRemaining = (this.stats.averageRemaining * (this.stats.totalRequests - 1) + remaining) / this.stats.totalRequests; return { limitPercentage: Math.round((remaining / limit) * 100), status: remaining < limit * 0.2 ? 'warning' : 'ok' }; } getReport() { const rateLimitPercentage = Math.round((this.stats.rateLimitedRequests / this.stats.totalRequests) * 100); return { totalRequests: this.stats.totalRequests, rateLimitedCount: this.stats.rateLimitedRequests, rateLimitPercentage: rateLimitPercentage, averageRemaining: Math.round(this.stats.averageRemaining) }; } } ``` ## API Key Management ### View Rate Limit Status Check your API key's rate limit configuration: ```bash # Check rate limit for a specific key php artisan bagisto-api:key:manage status --key="Mobile App" # Output: # Key: Mobile App # Status: Active # Rate Limit: 500 requests/minute # Total Requests (last 24h): 12,450 # Average RPM: 345 ``` ### Adjust Rate Limit Update an API key's rate limit: ```bash # Increase rate limit for high-traffic app php artisan bagisto-api:generate-key --name="High Traffic App" --rate-limit=2000 ``` ### List All Keys with Rate Limits ```bash # Summary of all keys and their limits php artisan bagisto-api:key:manage summary # Output: # Total Keys: 5 # # Key Name | Status | Rate Limit | Requests (24h) # ------------------- | -------- | ---------- | --------------- # Mobile App | Active | 500 | 12,450 # Website | Active | 200 | 5,230 # Partner API | Active | 1000 | 8,945 # Development | Inactive | 100 | 0 # Testing | Active | 50 | 145 ``` ## Examples ### cURL ```bash # Check rate limit headers curl -X GET 'https://your-domain.com/api/shop/products' \ -H 'X-STOREFRONT-KEY: pk_storefront_xxxxx' \ -i # Include headers in output ``` Response: ``` HTTP/1.1 200 OK X-RateLimit-Limit: 100 X-RateLimit-Remaining: 95 X-RateLimit-Reset: 1767967200 ``` ### Python ```python import requests import time def make_request_with_rate_limit(url, headers): response = requests.get(url, headers=headers) limit = int(response.headers.get('X-RateLimit-Limit', 100)) remaining = int(response.headers.get('X-RateLimit-Remaining', 100)) reset = int(response.headers.get('X-RateLimit-Reset', 0)) print(f"Rate Limit: {remaining}/{limit}") if response.status_code == 429: retry_after = int(response.headers.get('Retry-After', 60)) print(f"Rate limited. Waiting {retry_after} seconds...") time.sleep(retry_after) return make_request_with_rate_limit(url, headers) return response.json() ``` ### Node.js ```javascript const axios = require('axios'); async function makeRequest(url, apiKey) { try { const response = await axios.get(url, { headers: { 'X-STOREFRONT-KEY': apiKey, }, }); const limit = response.headers['x-ratelimit-limit']; const remaining = response.headers['x-ratelimit-remaining']; const reset = response.headers['x-ratelimit-reset']; console.log(`Rate Limit: ${remaining}/${limit}`); console.log(`Resets at: ${new Date(reset * 1000)}`); return response.data; } catch (error) { if (error.response?.status === 429) { const retryAfter = error.response.headers['retry-after']; console.error(`Rate limited. Retry after ${retryAfter} seconds`); await new Promise(r => setTimeout(r, retryAfter * 1000)); return makeRequest(url, apiKey); } throw error; } } ``` ## Troubleshooting ### Frequently Hitting Rate Limits? 1. **Check your request volume**: Review your API usage patterns 2. **Implement caching**: Cache responses to reduce API calls 3. **Request higher limit**: Contact support for rate limit increase 4. **Batch requests**: Combine multiple operations into single calls 5. **Optimize queries**: Use pagination to fetch data efficiently ### Rate Limit Not Resetting - Verify your server time is synchronized (use NTP) - Check if you're sending requests with different API keys - Verify the `X-RateLimit-Reset` timestamp (Unix format) ### Getting 429 Errors Immediately - Check if your API key has a very low rate limit - Verify you're sending the correct `X-STOREFRONT-KEY` header - Wait until the reset time before retrying ## What's Next? - 🔐 [Authentication Guide](./authentication) - Learn about API authentication - 🔗 [REST API Guide](./rest-api/introduction.html) - Explore REST API endpoints - 🔑 [API Key Management](./storefront-api-key-management-guide) - Manage your API keys - 💡 [Best Practices](./rest-api/best-practices.html) - Performance and security tips --- # Recipes URL: /api/recipes --- outline: false --- # Recipes End-to-end walkthroughs that chain real Bagisto API calls into a working flow. Each recipe lists the steps in order and links the endpoint page backing every step — so you (or an AI agent) can implement the flow in any framework. | Recipe | What it builds | |--------|----------------| | [Build a Storefront](/api/recipes/build-a-storefront) | Catalog browsing, cart, and checkout on the Shop API | | [Build an Admin Dashboard](/api/recipes/build-an-admin-dashboard) | Order listing, detail, and actions on the Admin API | | [Admin Create-Order Flow](/api/recipes/admin-create-order-flow) | Placing an order for a customer from the admin side | ::: tip Building with an AI agent? See [Build with AI](/api/build-with-ai) for the `llms.txt` index, the agent skills, and the optional docs MCP server. ::: --- # Recipe: Admin Create-Order Flow URL: /api/recipes/admin-create-order-flow --- outline: false --- # Recipe: Admin Create-Order Flow Place an order for a customer from the admin side — the API equivalent of the admin panel's **Create Order** screen. Every request carries the admin Integration token: ``` Authorization: Bearer | ``` The flow builds a **draft cart**, fills it, then places it. Do the steps in order — the sequence is enforced (you can't set a shipping method before addresses, etc.). ## The 6 steps (REST) | # | Step | Call | |---|------|------| | 1 | Create a draft cart for the customer | `POST /api/admin/customers/{customerId}/draft-carts` → returns `cartId` | | 2 | Add items | `POST /api/admin/carts/{cartId}/items` | | 3 | Save billing + shipping addresses | `POST /api/admin/carts/{cartId}/addresses` | | 4 | List then set a shipping method | `GET` then `POST /api/admin/carts/{cartId}/shipping-methods` | | 5 | List then set a payment method | `GET` then `POST /api/admin/carts/{cartId}/payment-methods` | | 6 | Place the order | `POST /api/admin/orders/place/{cartId}` → returns `orderId` | Open the Sales → Orders pages for the exact request bodies. Add-item bodies are **product-type-specific** (configurable / grouped / bundle / downloadable need their option selections). Booking products are not supported in admin Create-Order (matches the admin panel). ## GraphQL variant — select result fields, never `id` The draft-cart and cart-write operations are **actions**, not fetchable resources, so they have **no selectable `id`**. Select the result fields instead, or the query returns an internal-server error. Step 1 — create the draft cart (select `cartId`, not `id`): ```graphql mutation CreateDraftCart($input: createAdminDraftCartInput!) { createAdminDraftCart(input: $input) { adminDraftCart { cartId customerId success message } } } ``` ```json { "input": { "customerId": 1 } } ``` Step 6 — place the order (select `orderId`, not `id`): ```graphql mutation PlaceOrder($input: createAdminPlaceOrderInput!) { createAdminPlaceOrder(input: $input) { adminPlaceOrder { orderId incrementId grandTotal success message } } } ``` ```json { "input": { "cartId": 42 } } ``` The intermediate cart mutations (add item, save addresses, set shipping/payment method) likewise return the updated cart payload — select the cart's contents and the `success` / `message` fields, not `id`. ## Why `id` is not selectable here Fetchable resources — a customer, a product, a placed order — have a `GET /…/{id}` route, so GraphQL can resolve their `id`. Action operations (create-draft-cart, cart writes, place-order) have no such route, so there is no `id` to return. Read the action's result fields (`cartId`, `orderId`, `status`, `success`, `message`) instead. ## Status codes to handle `200/201` success · `401` unauthenticated · `403` forbidden · `404` cart/customer not found · `409` wrong step order (e.g. shipping before addresses) · `422` validation (e.g. below minimum order amount, unsupported payment method). --- # Recipe: Build a Storefront URL: /api/recipes/build-a-storefront --- outline: false --- # Recipe: Build a Storefront A storefront on the Shop API, from browsing to a placed order. Every request carries the `X-STOREFRONT-KEY` header; customer-scoped steps also carry the customer's `Authorization: Bearer `. Open each linked page for the exact request body and response shape. ## 1. Authenticate the storefront Every Shop API request sends your storefront key: ``` X-STOREFRONT-KEY: ``` For customer-scoped actions (their cart, account, orders), first log the customer in and keep the returned token: - [Customer Login](/api/rest-api/shop/customers/customer-login) — `POST /api/shop/customers/login` → `data.token`. Send it as `Authorization: Bearer ` on later calls. - New customers: [Customer Registration](/api/rest-api/shop/customers/customer-registration). ## 2. Browse the catalog 1. [Get Categories](/api/rest-api/shop/categories/get-categories) — build the navigation. 2. **List / search products** — paginated with `?page=`, `?per_page=`, `?query=`, `?sort=`, and category / price / attribute filters. 3. **Product detail** — fetch a single product for its price, images, and type-specific data (variants, bundle options, downloadable links, grouped items). ## 3. Cart 1. [Create / Get Cart](/api/rest-api/shop/cart/create-cart) — start or load the cart. 2. [Add to Cart](/api/rest-api/shop/cart/add-to-cart) — `POST /api/shop/cart/items`, body `{ productId, quantity, ... }`. **The extra fields depend on the product type** (configurable products need their selected options; grouped / bundle / downloadable need their selections) — open the page for the exact shape per type. 3. [Update Cart Item](/api/rest-api/shop/cart/update-cart-item) · [Remove Cart Item](/api/rest-api/shop/cart/remove-cart-item). 4. [Apply Coupon](/api/rest-api/shop/cart/apply-coupon) · [Remove Coupon](/api/rest-api/shop/cart/remove-coupon). ## 4. Checkout (in order — the sequence is enforced) 1. [Set Billing Address](/api/rest-api/shop/checkout/set-billing-address) 2. [Set Shipping Address](/api/rest-api/shop/checkout/set-shipping-address) 3. [Get Shipping Methods](/api/rest-api/shop/checkout/get-shipping-methods) → [Set Shipping Method](/api/rest-api/shop/checkout/set-shipping-method) 4. [Get Payment Methods](/api/rest-api/shop/checkout/get-payment-methods) → [Set Payment Method](/api/rest-api/shop/checkout/set-payment-method) 5. [Place Order](/api/rest-api/shop/checkout/place-order) ## 5. Post-order - [Get Customer Orders](/api/rest-api/shop/customer-orders/get-customer-orders) and the single order for the confirmation screen. - [Get Customer Invoices](/api/rest-api/shop/customer-invoices/get-customer-invoices) for downloadable invoices. ## GraphQL variant Every step above has a GraphQL equivalent at `POST /api/graphql` (see the Shop GraphQL section). When mutating the cart or placing the order, **select the result fields** the mutation returns (cart contents, order id, `success`, `message`) — the cart/checkout mutations are actions, so don't select an `id` on them. ## Status codes to handle `200/201` success · `401` unauthenticated (missing key/token) · `403` forbidden · `400` bad input · `404` not found · `422` validation (e.g. quantity exceeds stock). --- # Recipe: Build an Admin Dashboard URL: /api/recipes/build-an-admin-dashboard --- outline: false --- # Recipe: Build an Admin Dashboard An admin orders dashboard on the Admin API. Every request carries a pre-issued admin Integration token: ``` Authorization: Bearer | ``` ## 1. List orders [List Orders](/api/rest-api/admin/sales/orders) — `GET /api/admin/orders`. Returns the `{ data, meta }` envelope: ```json { "data": [ /* order rows */ ], "meta": { "currentPage": 1, "perPage": 10, "lastPage": 14, "total": 137 } } ``` Drive the table with `?page=` and `?per_page=` (default 10, cap 50) plus the listing filters (status, channel, customer, email, grand-total range, date range). Read the page-count headers (`X-Total-Count`, `X-Page`, `X-Per-Page`, `X-Total-Pages`) for pagination UI. ## 2. Order detail [Order Detail](/api/rest-api/admin/sales/orders) — `GET /api/admin/orders/{id}`. Returns the full order: customer, addresses, type-aware items, invoices, shipments, refunds, payment, and totals — everything the order-view screen needs, no follow-up calls. ## 3. Run an action From the order view, call the action endpoints (each has its own eligibility rules — open the page): - [Cancel Order](/api/rest-api/admin/sales/orders/cancel) — `POST /api/admin/orders/{id}/cancel` - [Create Invoice](/api/rest-api/admin/sales/orders/create-invoice) — `POST /api/admin/orders/{id}/invoices` - [Create Shipment](/api/rest-api/admin/sales/orders/create-shipment) — `POST /api/admin/orders/{id}/shipments` - [Create Refund](/api/rest-api/admin/sales/orders/create-refund) — `POST /api/admin/orders/{id}/refunds` - [Add Order Comment](/api/rest-api/admin/sales/orders/add-comment) — `POST /api/admin/orders/{id}/comments` ## 4. Other admin menus The same list → detail → action pattern (and the `{ data, meta }` envelope) applies across every admin menu — Catalog, Customers, Marketing, CMS, Settings. Browse the Admin API section or [`/llms.txt`](/llms.txt) for the full set. ## GraphQL variant The same data is available at `POST /api/admin/graphql` (admin GraphQL is a separate endpoint and takes the admin token). Order actions are mutations — **select the result fields** they return (`orderId`, `incrementId`, `status`, `success`, `message`), not `id`. ## Status codes to handle `200/201` success · `401` unauthenticated · `403` forbidden (permission) · `400` bad input · `404` not found · `422` validation / ineligible action. --- # REST API URL: /api/rest-api # REST API The Bagisto REST API provides a comprehensive RESTful interface to access all core Bagisto features. Built with Laravel Sanctum authentication, it offers secure and efficient endpoints for building mobile apps, third-party integrations, and custom interfaces. ## 🚀 Quick Start ### Live Demo Explore our interactive API documentation and test endpoints in real-time: - 🔧 [**Admin API Demo**](https://api-demo.bagisto.com/api/admin/) - Manage products, orders, customers, and more - 🛍️ [**Shop API Demo**](https://api-demo.bagisto.com/api/shop/) - Customer-facing shopping functionality ::: tip Try It Now Both demos include interactive testing tools where you can send real requests and see responses immediately. ::: ## 📦 Installation ### Step 1: Install the Package Install the REST API package via Composer: ```bash composer require bagisto/rest-api ``` ### Step 2: Environment Configuration Add the following configuration to your `.env` file: ```properties # Replace with your actual domain SANCTUM_STATEFUL_DOMAINS=http://localhost/public ``` ::: warning Domain Configuration Make sure to replace `http://localhost/public` with your actual domain URL. For production, use your live domain (e.g., `https://yourdomain.com`). ::: ### Step 3: Run Installation Command Configure the L5-Swagger documentation: ```bash php artisan bagisto-rest-api:install ``` This command will: - Publish API configuration files - Set up Swagger documentation - Configure authentication routes ## 📖 Documentation Access Once installed, access the interactive API documentation: ### Admin API Documentation ``` https://api-demo.bagisto.com/api/admin/documentation ``` ### Shop API Documentation ``` https://api-demo.bagisto.com/api/shop/documentation ``` ::: info Interactive Testing Both documentation interfaces include built-in testing tools. You can authenticate and test API endpoints directly from the browser. ::: ## 🔐 Authentication The REST API uses Laravel Sanctum for secure token-based authentication: ### Getting an Access Token 1. **Admin Authentication**: Use admin credentials to get admin-level access 2. **Customer Authentication**: Use customer credentials for shop-level access ### Using Tokens Include the token in your requests: ```bash curl -H "Authorization: Bearer YOUR_TOKEN_HERE" \ -H "Accept: application/json" \ https://api-demo.bagisto.com/api/v1/admin/get ``` ## 🎯 Common Use Cases ### Mobile App Development Build native iOS/Android apps with full e-commerce functionality: ```javascript // Example: Fetch products for mobile app fetch('/api/v1/products', { headers: { 'Authorization': 'Bearer ' + token, 'Accept': 'application/json' } }) .then(response => response.json()) .then(products => { // Display products in your mobile app }); ``` ### Third-party Integration Connect external systems with your Bagisto store: ```php // Example: Sync product from external system $response = Http::withToken($token)->post("/api/v1/admin/catalog/products/{$productId}", [ 'name' => 'Product Name', 'sku' => 'PROD-001', 'price' => 99.99 ]); ``` ## 🔗 Next Steps - 📚 Explore the [interactive documentation](https://api-demo.bagisto.com/api) ::: tip Need GraphQL? For modern frontend development with flexible queries, consider our [GraphQL API](./graphql-api) instead. ::: --- # Admin API URL: /api/rest-api/admin-coming-soon # Admin API ## Coming Soon The comprehensive Admin API documentation is coming soon. This section will include detailed guides for: - **Products**: Create, read, update, and delete products with full attribute management - **Categories**: Manage product categories and hierarchies - **Customers**: Customer management and administration - **Orders**: Order management, fulfillment, and tracking - **Attributes**: Attribute creation and configuration - **Inventory**: Stock management and warehouse operations - **Promotions**: Create and manage discounts and promotional campaigns - **Reports**: Access business intelligence and analytics reports - **Mutations**: Advanced operations for admin-level modifications Stay tuned for comprehensive documentation and examples! --- # Admin Authentication URL: /api/rest-api/admin/authentication --- outline: false apiType: rest examples: - id: admin-authenticated-request title: Authenticated Request description: Every Admin REST call carries the admin Bearer token. This example reads the authenticated admin's own profile to confirm the token works. query: | curl -X GET "https://your-domain.com/api/admin/get" \ -H "Accept: application/json" \ -H "Authorization: Bearer |" variables: | {} response: | [ { "id": "4", "name": "Admin User", "email": "admin@example.com", "image": null, "status": "1", "roleId": 1, "roleName": "Administrator", "success": true, "message": null } ] commonErrors: - error: Unauthenticated cause: Missing, malformed, expired, or revoked Bearer token solution: Send a valid Bearer token from an active Integration token on the Authorization header - error: Forbidden cause: The token is valid but the owning admin lacks permission for the action solution: Use a token whose admin has the required role permission --- # Admin Authentication The Bagisto Admin REST API authenticates every request with a pre-issued **Integration token**. There is no login call — you generate a token once in the admin panel and send it on every request. ## How to authenticate 1. In the admin panel, open the **Integration** menu (`Admin → Integration`) and generate a token. 2. Copy the token the moment it is shown — it is displayed **once**. 3. Send it on every Admin API request as a Bearer token: ``` Authorization: Bearer | ``` That single header is all that is required. The `/api/admin/*` routes do **not** use the storefront key — only the Bearer token above. ## About the token - The token belongs to a specific admin user and carries that admin's permissions. A request can never do more than the owning admin is allowed to do. - The plaintext format is `|`. Send it verbatim. - A token can be locked down with scoped **permissions**, an **IP allowlist**, an **expiry date**, and **rate limits** — see [Token security](#token-security) below. - Revoke or regenerate a token at any time from the same **Integration** menu. A revoked token stops working immediately. ## Token security Each Integration token can be locked down at generation time in the **Integration** menu. Four independent controls scope what a token can do: ### Permissions (ACL) A token is tied to one admin and **inherits that admin's role permissions** — it can never do more than its owner. When generating the token you choose a permission mode: - **All** — every action the owner's role allows. - **Custom** — a specific subset of permissions you select, frozen onto the token. - **Same as web** — always mirrors the owner's current role, so the token automatically follows any later changes to that role. A request for an action the token isn't permitted to perform returns **403 Forbidden**. ### IP allowlist Optionally restrict a token to specific client IPs. Individual **IPv4** and **IPv6** addresses and **CIDR ranges** are all supported. Leave the allowlist empty to allow any IP. A request from an address that isn't on the list is rejected as **401 Unauthenticated**. (`127.0.0.1` is always allowed, for local development.) ### Expiry A token can have an **expiry date** (default: one year after generation) or be set to **never expire**. After the expiry date the token stops working (**401**). ### Rate limits Each token is throttled by two independent buckets: | Bucket | Default | |---|---| | Per minute | 60 requests | | Per day | 10,000 requests | Exceeding either limit returns **429 Too Many Requests**. **Unlimited rate limit** — when generating or editing the token, choose the **Unlimited** option for the per-minute and/or per-day limit. That removes the cap for that bucket; set **both** to Unlimited for a fully unthrottled token. ## Errors | Condition | HTTP | Body | |---|---|---| | Missing / malformed / expired / revoked token, or client IP not on the token's allowlist | `401` | `{ "message": "Unauthenticated.", "error": "unauthenticated" }` | | Token valid but lacks permission for the action | `403` | Forbidden | | Per-minute or per-day rate limit exceeded | `429` | Too Many Requests | ## Examples Use the interactive example on the right to see an authenticated request in cURL, Node.js, React, and PHP. --- # Attributes URL: /api/rest-api/admin/catalog/attributes --- outline: false apiType: rest --- # Attributes Attributes are the fields a product can carry — `name`, `price`, `color`, `material`, and so on. The Attributes menu lists, creates, edits, and deletes them, and manages the selectable options of dropdown-style attributes. It mirrors the admin **Catalog → Attributes** screen. ## Attribute types The `type` (fixed at creation) decides how the field is captured and stored: | Type | Notes | |------|-------| | `text` / `textarea` | Free text / multi-line text. | | `price` / `boolean` | A decimal price / a yes-no flag. | | `date` / `datetime` | A date / a date-and-time. | | `image` / `file` | An uploaded image / file. | | `select` / `multiselect` / `checkbox` | A pick-list — these have **options** (see below). | ## Options (for select / multiselect / checkbox) Only `select`, `multiselect`, and `checkbox` attributes have options (e.g. a `color` attribute's Red / Green / Blue). Options are managed through their own endpoints under the attribute, and each option carries its own per-locale translations. A `select` attribute can also drive **swatches** (`swatchType` = dropdown / color / image) used on the storefront. ## Configurable, filterable, system - **`isConfigurable`** — whether the attribute can be used as a variant-defining attribute for configurable products (e.g. colour × size). Only `select`-type attributes qualify. - **`isFilterable`** — whether it appears in storefront layered navigation. - **`isRequired` / `isUnique`** — validation on the product form. - **`valuePerLocale` / `valuePerChannel`** — whether the value can differ per locale / per channel. - **System attributes** (`isUserDefined = false`) are the built-in fields (sku, name, price, …). Their `code` and `type` are immutable and they cannot be deleted. ## Endpoints in this menu | Action | Endpoint | |--------|----------| | [List attributes](/api/rest-api/admin/catalog/attributes/attributes-listing) | `GET /api/admin/catalog/attributes` | | [Attribute detail](/api/rest-api/admin/catalog/attributes/attributes-detail) | `GET /api/admin/catalog/attributes/{id}` | | [Create attribute](/api/rest-api/admin/catalog/attributes/attributes-create) | `POST /api/admin/catalog/attributes` | | [Update attribute](/api/rest-api/admin/catalog/attributes/attributes-update) | `PUT /api/admin/catalog/attributes/{id}` | | [Delete attribute](/api/rest-api/admin/catalog/attributes/attributes-delete) | `DELETE /api/admin/catalog/attributes/{id}` | | [Mass delete](/api/rest-api/admin/catalog/attributes/attributes-mass-delete) | `POST /api/admin/catalog/attributes/mass-delete` | | [Attribute options (CRUD)](/api/rest-api/admin/catalog/attributes/attribute-options) | `POST` / `PUT` / `DELETE /api/admin/catalog/attributes/{id}/options[/{optionId}]` | The single-attribute endpoint embeds the full `translations` and `options` (with their translations) inline; the listing leaves those two heavy blocks out (fetch them by id). All Attributes endpoints require an admin Bearer token — see [Authentication](/api/rest-api/admin/authentication). Reads require `catalog.attributes.view`; writes require the matching `catalog.attributes.create` / `.edit` / `.delete` permission. --- # Catalog Attribute Options — Create / Update / Delete URL: /api/rest-api/admin/catalog/attributes/attribute-options --- outline: false apiType: rest examples: - id: admin-catalog-attribute-option-create title: Create Attribute Option description: Adds a new option to a `select`, `multiselect`, or `checkbox` attribute. Returns the full updated attribute detail. query: | curl -X POST "https://your-domain.com/api/admin/catalog/attributes/12/options" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "admin_name": "Wool", "sort_order": 2, "translations": { "en": { "label": "Wool" }, "fr": { "label": "Laine" } } }' variables: | attributeId=12 response: | { "id": 12, "code": "material", "type": "select", "options": [ { "id": 45, "adminName": "Wool", "sortOrder": 2 } ] } commonErrors: - error: Unsupported attribute type (422) cause: The attribute's `type` is not `select`, `multiselect`, or `checkbox` solution: Options can only be attached to option-bearing types - error: Validation (422) cause: '`admin_name` missing' solution: Provide `admin_name` - error: Not Found (404) cause: Unknown attribute id solution: Verify the attribute id - error: Unauthorized (401) cause: Missing or invalid admin Bearer token solution: Send a valid admin Bearer token (Integration token) in the Authorization header. See the Authentication page. - id: admin-catalog-attribute-option-update title: Update Attribute Option description: Partially update an existing option. Only supplied fields are changed; translations merge per locale. query: | curl -X PUT "https://your-domain.com/api/admin/catalog/attributes/12/options/45" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "admin_name": "Merino Wool", "sort_order": 1, "translations": { "en": { "label": "Merino Wool" }, "fr": { "label": "Laine Mérinos" } } }' variables: | attributeId=12 optionId=45 response: | { "id": 12, "code": "material", "type": "select" } - id: admin-catalog-attribute-option-delete title: Delete Attribute Option description: Removes an option. Refused (HTTP 409) when one or more products still reference the option. query: | curl -X DELETE "https://your-domain.com/api/admin/catalog/attributes/12/options/45" \ -H "Authorization: Bearer " variables: | attributeId=12 optionId=45 response: | { "message": "Attribute option deleted successfully." } commonErrors: - error: Option in use (409) cause: One or more products still reference this option in their attribute values solution: Reassign the products to a different option first, then retry - error: Not Found (404) cause: Unknown attribute or option id solution: Verify the ids --- # Catalog Attribute Options — Create / Update / Delete CRUD for individual options on `select`, `multiselect`, and `checkbox` attributes. Options for other attribute types do not exist (the attribute payload returns `options: null`). ## Endpoints | Endpoint | Method | Purpose | |----------|--------|---------| | `/api/admin/catalog/attributes/{attributeId}/options` | POST | Add a new option | | `/api/admin/catalog/attributes/{attributeId}/options/{optionId}` | PUT | Partially update an option | | `/api/admin/catalog/attributes/{attributeId}/options/{optionId}` | DELETE | Remove an option | Both `{attributeId}` and `{optionId}` are constrained to digits (`\d+`). ## Request body — Create / Update | Field | Type | Required | Notes | |-------|------|----------|-------| | `admin_name` | string | yes (create) / no (update) | Internal admin label. | | `sort_order` | integer | no | Display order. | | `swatch_value` | string\|null | no | Hex color for `color` swatches, image path for `image` swatches, display text for `text` swatches. | | `translations` | object | no | Map of locale → `{ label }`. Merges per-locale on update. | ## Response - **Create** — `201 Created` with the full attribute detail (same shape as `GET /api/admin/catalog/attributes/{id}`). - **Update** — `200 OK` with the full attribute detail. - **Delete** — `200 OK` with `{ "message": "..." }`. ## Errors | HTTP | Cause | |------|-------| | `401 Unauthorized` | Missing or invalid Bearer token | | `404 Not Found` | Unknown `attributeId` or `optionId` | | `409 Conflict` | (delete only) Option is referenced by product attribute values | | `422 Unprocessable Entity` | (create) Attribute type does not support options, or `admin_name` missing | ## Notes - The delete-409 message names the dependency count: `This option is used by N product(s) and cannot be deleted.` - For bulk attribute changes, supply the full `options` array on the [Update Attribute](/api/rest-api/admin/catalog/attributes/attributes-update) endpoint — that replaces the whole option set in one call. --- # Catalog Attribute — Create URL: /api/rest-api/admin/catalog/attributes/attributes-create --- outline: false apiType: rest examples: - id: admin-catalog-attribute-create title: Create Attribute description: Creates an attribute with optional translations and options (for select/multiselect/checkbox types). The `code` must be unique, pass the Code rule (letters/digits/underscore), and not be a reserved word. query: | curl -X POST "https://your-domain.com/api/admin/catalog/attributes" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "code": "material", "admin_name": "Material", "type": "select", "swatch_type": "text", "is_required": false, "is_unique": false, "is_filterable": true, "is_configurable": false, "is_visible_on_front": true, "is_comparable": false, "value_per_locale": false, "value_per_channel": false, "enable_wysiwyg": false, "position": 10, "translations": { "en": { "name": "Material" }, "fr": { "name": "Matière" } }, "options": [ { "admin_name": "Cotton", "sort_order": 1, "translations": { "en": { "label": "Cotton" }, "fr": { "label": "Coton" } } } ] }' variables: | { "code": "material", "admin_name": "Material", "type": "select" } response: | { "id": 50, "code": "material", "type": "select", "adminName": "Material", "isRequired": 0, "isUnique": 0, "valuePerLocale": 0, "valuePerChannel": 0, "isFilterable": 1, "isConfigurable": 0, "isVisibleOnFront": 1, "isUserDefined": 1, "swatchType": "text", "position": 10, "locale": "en", "createdAt": "2026-05-22T10:00:00+00:00", "updatedAt": "2026-05-22T10:00:00+00:00", "validation": null, "defaultValue": null, "translations": [ { "locale": "en", "name": "Material" }, { "locale": "fr", "name": "Matière" } ], "options": [ { "id": 101, "adminName": "Cotton", "sortOrder": 1, "swatchValue": null, "swatchValueUrl": null, "translations": [ { "locale": "en", "label": "Cotton" }, { "locale": "fr", "label": "Coton" } ] } ] } commonErrors: - error: Validation (422) cause: '`code` missing, malformed, duplicate, or a reserved word (`type`, `attribute_family_id`)' solution: Send a unique snake_case code that matches `^[a-z][a-z0-9_]*$` - error: Unauthorized (401) cause: Missing or invalid admin Bearer token solution: Send a valid admin Bearer token (Integration token) in the Authorization header. See the Authentication page. --- # Catalog Attribute — Create Creates a new product attribute. Mirrors **Catalog → Attributes → Create** in the Bagisto admin panel. The same event hooks fire (`catalog.attribute.create.before` / `catalog.attribute.create.after`), so any core listener (search reindex, cache flush, etc.) is triggered. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/catalog/attributes` | POST | ## Request body | Field | Type | Required | Notes | |-------|------|----------|-------| | `code` | string | yes | Snake_case identifier. Must be unique and not a reserved word. | | `admin_name` | string | yes | Internal admin label. | | `type` | string | yes | One of `text`, `textarea`, `price`, `boolean`, `select`, `multiselect`, `checkbox`, `date`, `datetime`, `image`, `file`. | | `swatch_type` | string\|null | no | `color`, `image`, or `text`. Only relevant for `select`/`multiselect`. | | `is_required` / `is_unique` / `is_filterable` / `is_configurable` / `is_visible_on_front` / `is_comparable` / `value_per_locale` / `value_per_channel` / `enable_wysiwyg` | boolean | no | Standard attribute flags. | | `validation` | string\|null | no | Validation rule (`numeric`, `email`, `decimal`, `url`). | | `default_value` | string\|null | no | Default value for the attribute. | | `position` | integer | no | Display order. | | `translations` | object | no | Map of locale → `{ name }`. | | `options` | array | no | Initial options (select/multiselect/checkbox only). Each entry: `{ admin_name, sort_order?, swatch_value?, translations? }`. | ## Response `201 Created` returning the full attribute detail — identical shape to [`GET /api/admin/catalog/attributes/{id}`](/api/rest-api/admin/catalog/attributes/attributes-detail). ## Errors | HTTP | Cause | |------|-------| | `401 Unauthorized` | Missing or invalid Bearer token | | `422 Unprocessable Entity` | Validation failure (missing/duplicate `code`, invalid `type`, malformed body) | ## Notes - The `code` field is **immutable** after creation — see the [Update endpoint](/api/rest-api/admin/catalog/attributes/attributes-update). - The repository returns a contract interface — the response is re-fetched as a full Eloquent model so the payload always carries `translations` and `options`. - For attribute types that do not support options (`text`, `textarea`, `boolean`, etc.), the `options` array in the response is `null`. --- # Catalog Attribute — Delete URL: /api/rest-api/admin/catalog/attributes/attributes-delete --- outline: false apiType: rest examples: - id: admin-catalog-attribute-delete title: Delete Attribute description: Deletes a user-defined attribute. Returns HTTP 403 for system attributes, HTTP 409 if the attribute is referenced by any attribute family. query: | curl -X DELETE "https://your-domain.com/api/admin/catalog/attributes/50" \ -H "Authorization: Bearer " variables: | id=50 response: | { "message": "Attribute deleted successfully." } commonErrors: - error: System attribute (403) cause: The attribute has `is_user_defined = 0` and cannot be deleted solution: System attributes are immutable — pick a different attribute - error: In use by attribute family (409) cause: Removing the attribute would orphan one or more attribute families solution: Remove the attribute from each family first, then retry - error: Not Found (404) cause: Unknown attribute id solution: Verify the id via the listing endpoint - error: Unauthorized (401) cause: Missing or invalid admin Bearer token solution: Send a valid admin Bearer token (Integration token) in the Authorization header. See the Authentication page. --- # Catalog Attribute — Delete Deletes a user-defined attribute. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/catalog/attributes/{id}` | DELETE | `{id}` must be a positive integer (`requirements: ['id' => '\\d+']`). ## Response `200 OK` with a confirmation message: ```json { "message": "Attribute deleted successfully." } ``` ## Errors | HTTP | Cause | |------|-------| | `401 Unauthorized` | Missing or invalid Bearer token | | `403 Forbidden` | System attribute (`is_user_defined = 0`) | | `404 Not Found` | The attribute does not exist | | `409 Conflict` | The attribute is part of one or more attribute families | ## Notes - For bulk deletion, use the [Mass Delete](/api/rest-api/admin/catalog/attributes/attributes-mass-delete) endpoint. - The 409 conflict response includes a message naming the dependency: `Attribute is part of one or more attribute families. Remove it from those families first.` --- # Catalog Attribute — Detail URL: /api/rest-api/admin/catalog/attributes/attributes-detail --- outline: false apiType: rest examples: - id: admin-catalog-attribute-detail title: Attribute Detail (with translations and options) description: Single attribute record including all locale translations and — for select/multiselect/checkbox types — all options, each with their own locale translations. query: | curl -X GET "https://your-domain.com/api/admin/catalog/attributes/12" \ -H "Authorization: Bearer " \ -H "Accept: application/json" variables: | id=12 response: | { "id": 12, "code": "color", "type": "select", "adminName": "Color", "isRequired": 0, "isUnique": 0, "valuePerLocale": 0, "valuePerChannel": 0, "isFilterable": 1, "isConfigurable": 1, "isVisibleOnFront": 1, "isUserDefined": 1, "swatchType": "color", "position": 5, "locale": "en", "createdAt": "2026-01-12T08:15:00+00:00", "updatedAt": "2026-04-30T14:20:09+00:00", "validation": null, "defaultValue": null, "isComparable": 0, "enableWysiwyg": 0, "regex": null, "translations": [ { "locale": "en", "name": "Color" }, { "locale": "fr", "name": "Couleur" } ], "options": [ { "id": 33, "adminName": "Red", "sortOrder": 1, "swatchValue": "#FF0000", "swatchValueUrl": null, "translations": [ { "locale": "en", "label": "Red" }, { "locale": "fr", "label": "Rouge" } ] }, { "id": 34, "adminName": "Blue", "sortOrder": 2, "swatchValue": "#0000FF", "swatchValueUrl": null, "translations": [ { "locale": "en", "label": "Blue" }, { "locale": "fr", "label": "Bleu" } ] } ] } commonErrors: - error: Not Found (404) cause: The attribute ID does not exist in the database solution: 'Verify the ID with the listing endpoint `GET /api/admin/catalog/attributes`' - error: Unauthorized (401) cause: Missing, invalid, expired, or revoked admin Bearer token solution: Send a valid admin Bearer token (Integration token) in the Authorization header. See the Authentication page. --- # Catalog Attribute — Detail Returns a single attribute record by ID, including the **full translations array** (every locale present in the database) and — for `select`, `multiselect`, and `checkbox` attribute types — all **options**, each with their own locale translations. This is the read endpoint to call when an admin needs the complete metadata for an attribute — e.g. when opening the edit form in the Catalog → Attributes UI. ## Endpoint | Endpoint | Method | Authentication | |----------|--------|----------------| | `/api/admin/catalog/attributes/{id}` | GET | Admin Bearer token | `{id}` must be a positive integer. Non-numeric values are rejected by a route requirement (`\d+`) — this prevents the `{id}` segment from matching any other path under `/catalog/attributes/`. ## Authentication Every request requires: ``` Authorization: Bearer ``` Obtain the Bearer token via [Authentication](/api/rest-api/admin/authentication). ## Path Parameter | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `id` | integer | Yes | The numeric attribute ID | ## Response Shape The response is a single JSON object (not wrapped in `{ data }`) with the following fields: | Field | Type | Description | |-------|------|-------------| | `id` | integer | Attribute ID | | `code` | string | Attribute code (e.g. `color`, `size`) | | `type` | string | Attribute type (e.g. `select`, `text`, `boolean`) | | `adminName` | string | Internal admin-facing label | | `isRequired` | integer | `1` = required on product forms, `0` = optional | | `isUnique` | integer | `1` = value must be unique across products | | `valuePerLocale` | integer | `1` = separate value per store locale | | `valuePerChannel` | integer | `1` = separate value per channel | | `isFilterable` | integer | `1` = attribute appears in layered navigation filters | | `isConfigurable` | integer | `1` = used as a configurable variant axis | | `isVisibleOnFront` | integer | `1` = shown on the product page storefront | | `isUserDefined` | integer | `1` = created by an admin user (not a system attribute) | | `swatchType` | string\|null | Swatch rendering mode (`color`, `image`, `text`); `null` for non-swatch types | | `position` | integer | Display order position | | `locale` | string\|null | App locale used for the top-level scalar fields | | `createdAt` | string\|null | ISO 8601 creation timestamp | | `updatedAt` | string\|null | ISO 8601 last-update timestamp | | `validation` | string\|null | Validation rule string (e.g. `numeric`, `email`, `regex`); `null` if none | | `defaultValue` | string\|null | Default value for the attribute; `null` if not configured | | `isComparable` | integer | `1` if the attribute is shown in the storefront product-compare table, else `0` | | `enableWysiwyg` | integer | `1` if a rich-text (WYSIWYG) editor is used for a `textarea` attribute, else `0` | | `regex` | string\|null | Custom regular-expression pattern, used when `validation` is `regex`; `null` otherwise | | `translations` | array | All locale translations (see below) | | `options` | array\|null | Attribute options for `select`, `multiselect`, `checkbox` types; `null` for all other types | ### `translations[]` item shape Each entry in the `translations` array corresponds to one locale row in `attribute_translations`: | Field | Type | Description | |-------|------|-------------| | `locale` | string | Locale code (e.g. `en`, `fr`) | | `name` | string\|null | Locale-specific attribute display name | ### `options[]` item shape Each entry represents one attribute option (row in `attribute_options`): | Field | Type | Description | |-------|------|-------------| | `id` | integer | Option ID | | `adminName` | string | Internal admin label for the option | | `sortOrder` | integer | Display sort order | | `swatchValue` | string\|null | Swatch value — hex color code for `color` swatches, storage path for `image` swatches, display text for `text` swatches; `null` if not a swatch type | | `swatchValueUrl` | string\|null | Full URL to the swatch image for `image` swatches; `null` for other swatch types or no image | | `translations` | array | Locale translations for this option (see below) | ### `options[].translations[]` item shape Each entry corresponds to one row in `attribute_option_translations`: | Field | Type | Description | |-------|------|-------------| | `locale` | string | Locale code (e.g. `en`, `fr`) | | `label` | string\|null | Locale-specific display label for the option | ## Example Request ```bash curl -X GET "https://your-domain.com/api/admin/catalog/attributes/12" \ -H "Authorization: Bearer " \ -H "Accept: application/json" ``` ## Example Response ```json { "id": 12, "code": "color", "type": "select", "adminName": "Color", "isRequired": 0, "isUnique": 0, "valuePerLocale": 0, "valuePerChannel": 0, "isFilterable": 1, "isConfigurable": 1, "isVisibleOnFront": 1, "isUserDefined": 1, "swatchType": "color", "position": 5, "locale": "en", "createdAt": "2026-01-12T08:15:00+00:00", "updatedAt": "2026-04-30T14:20:09+00:00", "validation": null, "defaultValue": null, "isComparable": 0, "enableWysiwyg": 0, "regex": null, "translations": [ { "locale": "en", "name": "Color" }, { "locale": "fr", "name": "Couleur" } ], "options": [ { "id": 33, "adminName": "Red", "sortOrder": 1, "swatchValue": "#FF0000", "swatchValueUrl": null, "translations": [ { "locale": "en", "label": "Red" }, { "locale": "fr", "label": "Rouge" } ] }, { "id": 34, "adminName": "Blue", "sortOrder": 2, "swatchValue": "#0000FF", "swatchValueUrl": null, "translations": [ { "locale": "en", "label": "Blue" }, { "locale": "fr", "label": "Bleu" } ] } ] } ``` ## Errors | HTTP Status | Cause | |-------------|-------| | `401 Unauthorized` | Missing, expired, or revoked admin Bearer token | | `401 Unauthorized` | Missing or invalid admin Bearer token | | `404 Not Found` | The specified `{id}` does not exist in the database | ## Notes - **`translations` contains every locale in the DB**, not just the current app locale. If the store has translations for `en`, `fr`, and `de`, all three entries are returned. Fields with no content for a locale are `null`. - **`options` is `null` for non-option types.** Only `select`, `multiselect`, and `checkbox` attributes have options. For all other types (`text`, `textarea`, `price`, `boolean`, `datetime`, `date`, `image`, `file`), `options` is `null`. - **`translations` and `options` are plain arrays**, not IRIs. Nested objects (options and their translations) are serialized inline in the response — there are no follow-up requests needed. - **The `{id}` route parameter must be a digit.** The route carries a `requirements: ['id' => '\d+']` constraint — non-numeric path segments are rejected with `404` before reaching the provider. - **`swatchValueUrl`** is only non-null for options of attributes with `swatch_type = 'image'`. For `color` and `text` swatches, `swatchValue` carries the display value directly and `swatchValueUrl` is always `null`. - **`validation`** holds the validation rule string stored in `attributes.validation` (e.g. `numeric`, `email`, `decimal`, `url`). `null` means no validation rule is configured for the attribute. - **Top-level `locale`** reflects the app's current default locale. It is a convenience indicator — use `translations` to get display names in other locales. --- # Catalog Attributes — Datagrid Listing URL: /api/rest-api/admin/catalog/attributes/attributes-listing --- outline: false apiType: rest examples: - id: admin-catalog-attributes-list title: List Catalog Attributes (Datagrid) description: Paginated, filterable, sortable attribute list mirroring the Bagisto admin Catalog → Attributes datagrid. Returns the standard `{ data, meta }` envelope. query: | curl -X GET "https://your-domain.com/api/admin/catalog/attributes?per_page=10&page=1&type=select&sort=id&order=desc" \ -H "Authorization: Bearer " \ -H "Accept: application/json" variables: | per_page=10&page=1&type=select&sort=id&order=desc response: | { "data": [ { "id": 1, "code": "sku", "type": "text", "adminName": "SKU", "isRequired": 1, "isUnique": 1, "valuePerLocale": 0, "valuePerChannel": 0, "isFilterable": 0, "isConfigurable": 0, "isVisibleOnFront": 0, "isUserDefined": 0, "swatchType": null, "position": 1, "locale": "en", "createdAt": "2024-01-01T00:00:00+00:00", "updatedAt": "2024-01-01T00:00:00+00:00", "translations": null, "options": null, "validation": null, "defaultValue": null } ], "meta": { "currentPage": 1, "perPage": 10, "lastPage": 5, "total": 47, "from": 1, "to": 10 } } commonErrors: - error: Unauthorized (401) cause: Missing, invalid, expired, or revoked admin Bearer token solution: Send a valid admin Bearer token (Integration token) in the Authorization header. See the Authentication page. --- # Catalog Attributes — Datagrid Listing Paginated, filterable, and sortable attribute list that mirrors the Bagisto admin **Catalog → Attributes** datagrid 1:1. This is the authoritative attribute-management listing for the admin API — same columns, same filters, and the same sort options used by the datagrid. ::: tip How this menu works For attribute types, options, and the configurable/filterable flags, see the [Attributes overview](/api/rest-api/admin/catalog/attributes/). ::: ## Endpoint | Endpoint | Method | Authentication | |----------|--------|----------------| | `/api/admin/catalog/attributes` | GET | Admin Bearer token | ## Authentication Every request requires: ``` Authorization: Bearer ``` Obtain the Bearer token via [Authentication](/api/rest-api/admin/authentication). ## Query Parameters | Parameter | Type | Description | Example | |-----------|------|-------------|---------| | `page` | integer | Page number (1-based, default `1`) | `1` | | `per_page` | integer | Items per page (default `10`, max `50`) | `10` | | `id` | string | Filter by attribute ID — single integer or comma-separated list (e.g. `"1"` or `"1,2"`) | `1` | | `code` | string | Partial attribute code match (SQL `LIKE %value%`) | `color` | | `type` | string | Exact attribute type filter | `select` | | `admin_name` | string | Partial admin name match (SQL `LIKE %value%`) | `Color` | | `is_required` | integer | Filter by is_required: `0` = no, `1` = yes | `1` | | `is_unique` | integer | Filter by is_unique: `0` = no, `1` = yes | `0` | | `is_filterable` | integer | Filter by is_filterable: `0` = no, `1` = yes | `1` | | `is_configurable` | integer | Filter by is_configurable: `0` = no, `1` = yes | `0` | | `is_visible_on_front` | integer | Filter by is_visible_on_front: `0` = no, `1` = yes | `1` | | `is_user_defined` | integer | Filter by is_user_defined: `0` = no, `1` = yes | `1` | | `value_per_locale` | integer | Filter by value_per_locale: `0` = no, `1` = yes | `0` | | `value_per_channel` | integer | Filter by value_per_channel: `0` = no, `1` = yes | `0` | | `locale` | string | Locale code for translation resolution (default: app locale) | `en` | | `sort` | string | Column to sort by (see Sorting section below) | `id` | | `order` | string | Sort direction: `asc` or `desc` (default `desc`) | `desc` | **Valid `type` values:** `text`, `textarea`, `price`, `boolean`, `select`, `multiselect`, `datetime`, `date`, `image`, `file`, `checkbox` ## Response Shape Responses use the standard admin `{ data, meta }` envelope. ### `meta` object | Field | Type | Description | |-------|------|-------------| | `currentPage` | integer | Current page number (1-based) | | `perPage` | integer | Number of items on this page | | `lastPage` | integer | Total number of pages | | `total` | integer | Total matching attributes | | `from` | integer | 1-based index of the first item on this page | | `to` | integer | 1-based index of the last item on this page | ### Row fields (`data[]`) | Field | Type | Description | |-------|------|-------------| | `id` | integer | Attribute ID | | `code` | string | Attribute code (e.g. `color`, `size`) | | `type` | string | Attribute type (e.g. `select`, `text`, `boolean`) | | `adminName` | string | Internal admin-facing label | | `isRequired` | integer | `1` = required on product forms, `0` = optional | | `isUnique` | integer | `1` = value must be unique across products | | `valuePerLocale` | integer | `1` = separate value per store locale | | `valuePerChannel` | integer | `1` = separate value per channel | | `isFilterable` | integer | `1` = attribute appears in layered navigation filters | | `isConfigurable` | integer | `1` = used as a configurable variant axis | | `isVisibleOnFront` | integer | `1` = shown on the product page storefront | | `isUserDefined` | integer | `1` = created by an admin user (not a system attribute) | | `swatchType` | string\|null | Swatch rendering mode (`color`, `image`, `text`); `null` for non-swatch types | | `position` | integer | Display order position | | `locale` | string\|null | Locale code used for name resolution | | `createdAt` | string\|null | ISO 8601 creation timestamp | | `updatedAt` | string\|null | ISO 8601 last-update timestamp | | `translations` | null | Always `null` in list responses — full translations are available via the detail endpoint `GET /api/admin/catalog/attributes/{id}` | | `options` | null | Always `null` in list responses — option data is available via the detail endpoint | | `validation` | null | Always `null` in list responses — available via the detail endpoint | | `defaultValue` | null | Always `null` in list responses — available via the detail endpoint | ## Example Request ```bash curl -X GET "https://your-domain.com/api/admin/catalog/attributes?per_page=10&page=1&type=select&sort=id&order=desc" \ -H "Authorization: Bearer " \ -H "Accept: application/json" ``` ## Example Response ```json { "data": [ { "id": 1, "code": "sku", "type": "text", "adminName": "SKU", "isRequired": 1, "isUnique": 1, "valuePerLocale": 0, "valuePerChannel": 0, "isFilterable": 0, "isConfigurable": 0, "isVisibleOnFront": 0, "isUserDefined": 0, "swatchType": null, "position": 1, "locale": "en", "createdAt": "2024-01-01T00:00:00+00:00", "updatedAt": "2024-01-01T00:00:00+00:00", "translations": null, "options": null, "validation": null, "defaultValue": null } ], "meta": { "currentPage": 1, "perPage": 10, "lastPage": 5, "total": 47, "from": 1, "to": 10 } } ``` ## Sorting | Parameter form | Example | |----------------|---------| | Separate `sort` + `order` params | `?sort=id&order=desc` | **Sortable columns:** | `sort` value | Sorts by | |---|---| | `id` | Attribute ID (default) | | `code` | Attribute code | | `admin_name` | Admin label | | `type` | Attribute type | | `position` | Display order position | ## Pagination - Default page size: **10** items - Maximum page size: **50** items - Use `?page=N` for page navigation and `?per_page=N` to control page size ## Errors | HTTP Status | Cause | |-------------|-------| | `401 Unauthorized` | Missing, expired, or revoked admin Bearer token | | `401 Unauthorized` | Missing or invalid admin Bearer token | **Unknown filter parameters** are silently ignored — no error is returned. ## Notes - **`translations`, `options`, `validation`, and `defaultValue` are always `null` in list rows.** These detail-only fields are only populated by `GET /api/admin/catalog/attributes/{id}`, which loads all locale translations and — for `select`, `multiselect`, and `checkbox` types — all options with their own translations. - **`adminName`** is the value from the `attributes.admin_name` column. It is not locale-aware — it is the internal admin label set when the attribute was created. - **`locale` query parameter** controls which `attribute_translations` row is used to resolve any locale-specific display name in the provider. If no translation exists for the requested locale, the `adminName` column value is used as a fallback. - **No automatic filter applied.** This endpoint returns all attributes (system and user-defined). Pass `?is_user_defined=1` to restrict to admin-created attributes, or `?is_user_defined=0` for system attributes only. - Envelope-wrapped: `{ data: [...], meta: { currentPage, perPage, lastPage, total, from, to } }`. - `per_page` caps at **50**; values ≤ 0 fall back to the default of **10**. --- # Catalog Attribute — Mass Delete URL: /api/rest-api/admin/catalog/attributes/attributes-mass-delete --- outline: false apiType: rest examples: - id: admin-catalog-attribute-mass-delete title: Mass Delete Attributes description: Deletes a batch of user-defined attributes. If any ID in the batch belongs to a system attribute, the entire batch is rejected (HTTP 422). Non-existent IDs are silently skipped. query: | curl -X POST "https://your-domain.com/api/admin/catalog/attributes/mass-delete" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "indices": [24, 31] }' variables: | { "indices": [24, 31] } response: | { "deleted": [24, 31], "message": "Attributes deleted successfully." } commonErrors: - error: System attribute in batch (422) cause: At least one id in `indices` is a system attribute (`is_user_defined = 0`) solution: Remove system-attribute ids from the batch before retrying — the operation is all-or-nothing - error: Unauthorized (401) cause: Missing or invalid admin Bearer token solution: Send a valid admin Bearer token (Integration token) in the Authorization header. See the Authentication page. --- # Catalog Attribute — Mass Delete Deletes multiple user-defined attributes in a single request. The whole batch is pre-validated before any row is touched — if any id is a system attribute, no row is deleted and the entire batch fails with `422`. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/catalog/attributes/mass-delete` | POST | ## Request body | Field | Type | Required | Notes | |-------|------|----------|-------| | `indices` | integer[] | yes | Attribute ids to delete. | ## Response `200 OK`: ```json { "deleted": [24, 31], "message": "Attributes deleted successfully." } ``` ## Errors | HTTP | Cause | |------|-------| | `401 Unauthorized` | Missing or invalid Bearer token | | `422 Unprocessable Entity` | One or more ids in the batch are system attributes — whole batch refused | ## Notes - **All-or-nothing semantics.** A single bad id rejects the entire batch — no partial deletes. - **Unknown ids are silently skipped.** Passing `[24, 9999]` where `9999` does not exist deletes id `24` and reports `"deleted": [24]`. - For single-attribute deletion, use [`DELETE /api/admin/catalog/attributes/{id}`](/api/rest-api/admin/catalog/attributes/attributes-delete). --- # Catalog Attribute — Update URL: /api/rest-api/admin/catalog/attributes/attributes-update --- outline: false apiType: rest examples: - id: admin-catalog-attribute-update title: Update Attribute description: Update an attribute. The `code` field cannot be changed. Changing `type` is refused when product attribute values exist. If `options` is supplied, the full set is replaced — existing options keyed by `id` are updated, omitted ids are deleted, and entries without an `id` are inserted. query: | curl -X PUT "https://your-domain.com/api/admin/catalog/attributes/50" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "code": "material", "admin_name": "Material (updated)", "type": "select", "is_filterable": true, "translations": { "en": { "name": "Material (updated)" }, "fr": { "name": "Matière (mis à jour)" } } }' variables: | id=50 response: | { "id": 50, "code": "material", "adminName": "Material (updated)", "type": "select" } commonErrors: - error: Code change refused (422) cause: A different `code` was supplied in the payload solution: Send the same `code` the attribute was created with — `code` is immutable - error: Type immutable (422) cause: '`type` change attempted while product attribute values exist' solution: Delete dependent product attribute values first, or do not change `type` - error: Not Found (404) cause: The attribute id does not exist solution: Verify the id via the listing endpoint - error: Unauthorized (401) cause: Missing or invalid admin Bearer token solution: Send a valid admin Bearer token (Integration token) in the Authorization header. See the Authentication page. --- # Catalog Attribute — Update Updates an existing attribute. Mirrors **Catalog → Attributes → Edit** in the Bagisto admin panel. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/catalog/attributes/{id}` | PUT | `{id}` must be a positive integer (`requirements: ['id' => '\\d+']`). ## Request body Same fields as [Create](/api/rest-api/admin/catalog/attributes/attributes-create), with these rules: - **`code` is immutable.** Sending a different code returns `422`. - **`type` cannot be changed** if any product attribute value references this attribute. Returns `422`. - **`options` is a full-set replacement.** Entries with an `id` are updated, entries without `id` are inserted, and any existing option ids omitted from the payload are deleted. - System attributes (`is_user_defined = 0`) silently ignore immutable fields by convention — only mutable fields (e.g. `admin_name`, translations) are applied. - `translations` merges per-locale — only the supplied locales are updated; others are untouched. ## Response `200 OK` returning the full attribute detail — identical shape to [`GET /api/admin/catalog/attributes/{id}`](/api/rest-api/admin/catalog/attributes/attributes-detail). ## Errors | HTTP | Cause | |------|-------| | `401 Unauthorized` | Missing or invalid Bearer token | | `404 Not Found` | The attribute does not exist | | `422 Unprocessable Entity` | Code change attempted, type change refused, or other validation failure | --- # Categories URL: /api/rest-api/admin/catalog/categories --- outline: false apiType: rest --- # Categories Categories are the storefront's browsing tree — the sections customers navigate and products are assigned to. The Categories menu lists them (flat or as a nested tree), shows a single category, and creates / edits / deletes / moves them. It mirrors the admin **Catalog → Categories** screen. ## Flat list vs. tree Two read shapes for the same data: - **List** (`GET /api/admin/catalog/categories`) — a flat, paginated, filterable datagrid of categories. Each row carries the category's own fields plus its `parentId`. - **Tree** (`GET /api/admin/catalog/categories/tree`) — the full nested hierarchy, each node carrying its `children` recursively. Use this to render the category tree in one call. ## Hierarchy, status, display - **`parentId`** places a category under its parent. The root category (`id = 1`) and any channel's root category are structural and **cannot be deleted**. - **Moving** a category is just an update with a new `parentId` (and `position`) — there is no separate "move" endpoint. - **`status`** (1 enabled / 0 disabled) and **`position`** (sort order among siblings). - **`displayMode`** — what the category page shows: `products_and_description`, `products_only`, or `description_only`. - **`translations`** hold the per-locale `slug`, `name`, `description`, and meta fields. **`filterableAttributeIds`** are the attributes used for this category's layered-navigation filters. The single-category endpoint embeds the full `translations` and `filterableAttributeIds` inline; the listing leaves those two out (they're detail-only). ## Endpoints in this menu | Action | Endpoint | |--------|----------| | [List categories](/api/rest-api/admin/catalog/categories/categories-listing) | `GET /api/admin/catalog/categories` | | [Category tree](/api/rest-api/admin/catalog/categories/categories-tree) | `GET /api/admin/catalog/categories/tree` | | [Category detail](/api/rest-api/admin/catalog/categories/categories-detail) | `GET /api/admin/catalog/categories/{id}` | | [Create category](/api/rest-api/admin/catalog/categories/categories-create) | `POST /api/admin/catalog/categories` | | [Update / move category](/api/rest-api/admin/catalog/categories/categories-update) | `PUT /api/admin/catalog/categories/{id}` | | [Delete category](/api/rest-api/admin/catalog/categories/categories-delete) | `DELETE /api/admin/catalog/categories/{id}` | | [Mass delete](/api/rest-api/admin/catalog/categories/categories-mass-delete) | `POST /api/admin/catalog/categories/mass-delete` | | [Mass update status](/api/rest-api/admin/catalog/categories/categories-mass-update-status) | `POST /api/admin/catalog/categories/mass-update-status` | All Categories endpoints require an admin Bearer token — see [Authentication](/api/rest-api/admin/authentication). Reads require `catalog.categories.view`; writes require the matching `catalog.categories.create` / `.edit` / `.delete` permission. --- # Category — Create URL: /api/rest-api/admin/catalog/categories/categories-create --- outline: false apiType: rest examples: - id: admin-catalog-category-create title: Create Category description: Mirrors Bagisto admin Catalog → Categories → Create. Validates slug (unique), name, position, attributes. `description` is required when `display_mode` is `description_only` or `products_and_description`. File-upload for `logo_path` / `banner_path` is NOT supported in v1. query: | curl -X POST "https://your-domain.com/api/admin/catalog/categories" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "slug": "apparel", "name": "Apparel", "description": "Men''s and women''s apparel", "position": 1, "attributes": [11, 23], "parent_id": 1, "display_mode": "products_and_description", "status": 1, "locale": "en", "meta_title": "Apparel" }' variables: | { "slug": "apparel", "name": "Apparel", "position": 1, "attributes": [11, 23] } response: | { "id": 7, "position": 1, "status": 1, "parentId": 1, "displayMode": "products_and_description", "name": "Apparel", "slug": "apparel", "description": "Men's and women's apparel", "locale": "en" } commonErrors: - error: Validation (422) cause: '`slug` missing, duplicate, `position` missing, `attributes` empty, or `description` missing when `display_mode` requires it' solution: Send a unique slug and the required core fields - error: Unauthorized (401) cause: Missing or invalid admin Bearer token solution: Send a valid admin Bearer token (Integration token) in the Authorization header. See the Authentication page. --- # Category — Create Creates a new category. Mirrors **Catalog → Categories → Create** in the Bagisto admin panel. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/catalog/categories` | POST | ## Request body | Field | Type | Required | Notes | |-------|------|----------|-------| | `slug` | string | yes | URL-friendly identifier — must be unique. | | `name` | string | yes | Display name. | | `description` | string | conditional | Required when `display_mode` is `description_only` or `products_and_description`. | | `position` | integer | yes | Display order. | | `attributes` | integer[] | yes | List of filterable attribute ids. | | `parent_id` | integer\|null | no | Parent category id (defaults to the root). | | `display_mode` | string | no | One of `products_and_description`, `products_only`, `description_only`. | | `status` | integer | no | `0` = disabled, `1` = enabled. | | `locale` | string | no | Locale code (e.g. `en`). | | `meta_title` / `meta_description` / `meta_keywords` | string | no | SEO fields. | ## Response `201 Created`. Same shape as [`GET /api/admin/catalog/categories/{id}`](/api/rest-api/admin/catalog/categories/categories-detail). ## Errors | HTTP | Cause | |------|-------| | `401 Unauthorized` | Missing or invalid Bearer token | | `422 Unprocessable Entity` | Validation failure | ## Notes - **File upload not supported in v1.** `logo_path` and `banner_path` cannot be set via this endpoint — they remain `null`. Use the admin panel to upload images for now. --- # Category — Delete URL: /api/rest-api/admin/catalog/categories/categories-delete --- outline: false apiType: rest examples: - id: admin-catalog-category-delete title: Delete Category description: Refused with HTTP 400 if the category is the root (id=1) or referenced as `channels.root_category_id`. query: | curl -X DELETE "https://your-domain.com/api/admin/catalog/categories/7" \ -H "Authorization: Bearer " variables: | id=7 response: | { "message": "Category deleted successfully." } commonErrors: - error: Root or channel-root (400) cause: The category is the root (id=1) or is referenced by a channel as its `root_category_id` solution: Reassign the channel root before deleting, or pick a different category - error: Not Found (404) cause: Unknown category id solution: Verify the id with the listing endpoint --- # Category — Delete Deletes a category. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/catalog/categories/{id}` | DELETE | `{id}` must be a positive integer. ## Response `200 OK`: ```json { "message": "Category deleted successfully." } ``` ## Errors | HTTP | Cause | |------|-------| | `400 Bad Request` | Root category (`id=1`) or a category used as a channel's `root_category_id` | | `401 Unauthorized` | Missing or invalid Bearer token | | `404 Not Found` | The category does not exist | For bulk deletion, use the [Mass Delete](/api/rest-api/admin/catalog/categories/categories-mass-delete) endpoint. --- # Catalog Category — Detail URL: /api/rest-api/admin/catalog/categories/categories-detail --- outline: false apiType: rest examples: - id: admin-catalog-category-detail title: Category Detail (with all translations) description: Single category record including the full translations array (all locales present in the DB) and the list of filterable attribute IDs. query: | curl -X GET "https://your-domain.com/api/admin/catalog/categories/7" \ -H "Authorization: Bearer " \ -H "Accept: application/json" variables: | id=7 response: | { "id": 7, "position": 1, "status": 1, "parentId": 1, "displayMode": "products_and_description", "logoUrl": "https://example.com/storage/category/7/logo.webp", "bannerUrl": null, "name": "Apparel", "slug": "apparel", "description": "Men's and women's apparel", "locale": "en", "createdAt": "2026-01-12T08:15:00+00:00", "updatedAt": "2026-04-30T14:20:09+00:00", "translations": [ { "locale": "en", "name": "Apparel", "slug": "apparel", "description": "Men's and women's apparel", "metaTitle": null, "metaDescription": null, "metaKeywords": null }, { "locale": "fr", "name": "Vêtements", "slug": "vetements", "description": null, "metaTitle": null, "metaDescription": null, "metaKeywords": null } ], "filterableAttributeIds": [11, 23] } commonErrors: - error: Not Found (404) cause: The category ID does not exist in the database solution: 'Verify the ID with the listing endpoint `GET /api/admin/catalog/categories`' - error: Unauthorized (401) cause: Missing, invalid, expired, or revoked admin Bearer token solution: Send a valid admin Bearer token (Integration token) in the Authorization header. See the Authentication page. --- # Catalog Category — Detail Returns a single category record by ID, including the **full translations array** (every locale present in the database) and the list of **filterable attribute IDs** configured for the category. This is the read endpoint to call when an admin needs the complete metadata for a category — e.g. when opening the edit form in the Catalog → Categories UI. ## Endpoint | Endpoint | Method | Authentication | |----------|--------|----------------| | `/api/admin/catalog/categories/{id}` | GET | Admin Bearer token | `{id}` must be a positive integer. Non-numeric values are rejected by a route requirement (`\d+`) — this prevents the `{id}` segment from matching the `/tree` path of the tree endpoint. ## Authentication Every request requires: ``` Authorization: Bearer ``` Obtain the Bearer token via [Authentication](/api/rest-api/admin/authentication). ## Path Parameter | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `id` | integer | Yes | The numeric category ID | ## Response Shape The response is a single JSON object (not wrapped in `{ data }`) with the following fields: | Field | Type | Description | |-------|------|-------------| | `id` | integer | Category ID | | `position` | integer | Display order position | | `status` | integer | `1` = enabled, `0` = disabled | | `parentId` | integer\|null | Parent category ID; `null` for root nodes | | `displayMode` | string\|null | Category display mode (e.g. `products_and_description`) | | `logoUrl` | string\|null | Storage URL for the category logo; `null` if not set | | `bannerUrl` | string\|null | Storage URL for the category banner; `null` if not set | | `name` | string\|null | Category name in the current app locale | | `slug` | string\|null | URL slug in the current app locale | | `description` | string\|null | Category description in the current app locale | | `locale` | string\|null | App locale used for the top-level `name`/`slug`/`description` fields | | `createdAt` | string\|null | ISO 8601 creation timestamp | | `updatedAt` | string\|null | ISO 8601 last-update timestamp | | `translations` | array | All locale translations (see below) | | `filterableAttributeIds` | array\|null | Integer IDs of attributes configured as filterable for this category | ### `translations[]` item shape Each entry in the `translations` array corresponds to one locale row in `category_translations`: | Field | Type | Description | |-------|------|-------------| | `locale` | string | Locale code (e.g. `en`, `fr`) | | `name` | string\|null | Category name in this locale | | `slug` | string\|null | URL slug in this locale | | `description` | string\|null | Description in this locale | | `metaTitle` | string\|null | SEO meta title in this locale | | `metaDescription` | string\|null | SEO meta description in this locale | | `metaKeywords` | string\|null | SEO meta keywords in this locale | ## Example Request ```bash curl -X GET "https://your-domain.com/api/admin/catalog/categories/7" \ -H "Authorization: Bearer " \ -H "Accept: application/json" ``` ## Example Response ```json { "id": 7, "position": 1, "status": 1, "parentId": 1, "displayMode": "products_and_description", "logoUrl": "https://example.com/storage/category/7/logo.webp", "bannerUrl": null, "name": "Apparel", "slug": "apparel", "description": "Men's and women's apparel", "locale": "en", "createdAt": "2026-01-12T08:15:00+00:00", "updatedAt": "2026-04-30T14:20:09+00:00", "translations": [ { "locale": "en", "name": "Apparel", "slug": "apparel", "description": "Men's and women's apparel", "metaTitle": null, "metaDescription": null, "metaKeywords": null }, { "locale": "fr", "name": "Vêtements", "slug": "vetements", "description": null, "metaTitle": null, "metaDescription": null, "metaKeywords": null } ], "filterableAttributeIds": [11, 23] } ``` ## Errors | HTTP Status | Cause | |-------------|-------| | `401 Unauthorized` | Missing, expired, or revoked admin Bearer token | | `401 Unauthorized` | Missing or invalid admin Bearer token | | `404 Not Found` | The specified `{id}` does not exist in the database | ## Notes - **`translations` contains every locale in the DB**, not just the current app locale. If the store has 3 locale rows for this category (`en`, `fr`, `de`), all three are returned. Fields with no content for a locale are `null`. - **`filterableAttributeIds`** is an array of integer attribute IDs. An empty array `[]` means no filterable attributes have been configured for the category. This differs from `null`, which would mean the field was not resolved — but in practice the detail provider always returns an array. - **The `{id}` route parameter must be a digit.** Requesting `/api/admin/catalog/categories/tree` will NOT match this endpoint because the `requirements: ['id' => '\d+']` constraint rejects the non-numeric string `tree`. The tree endpoint has its own route at `GET /api/admin/catalog/categories/tree`. - **Top-level `name`, `slug`, `description`, and `locale` fields** reflect the app's current default locale. They are a convenience shortcut — the same values are present in the `translations` array entry for that locale. --- # Catalog Categories — Datagrid Listing URL: /api/rest-api/admin/catalog/categories/categories-listing --- outline: false apiType: rest examples: - id: admin-catalog-categories-list title: List Catalog Categories (Datagrid) description: Paginated, filterable, sortable category list mirroring the Bagisto admin Catalog → Categories datagrid. Returns the standard `{ data, meta }` envelope. query: | curl -X GET "https://your-domain.com/api/admin/catalog/categories?per_page=10&page=1&status=1&sort=name-asc" \ -H "Authorization: Bearer " \ -H "Accept: application/json" variables: | per_page=10&page=1&status=1&sort=name-asc response: | { "data": [ { "id": 7, "position": 1, "status": 1, "parentId": 1, "displayMode": "products_and_description", "logoUrl": null, "bannerUrl": null, "name": "Apparel", "slug": "apparel", "description": "Men's and women's apparel", "locale": "en", "createdAt": "2026-01-12T08:15:00+00:00", "updatedAt": "2026-04-30T14:20:09+00:00", "translations": null, "filterableAttributeIds": null } ], "meta": { "currentPage": 1, "perPage": 10, "lastPage": 5, "total": 47, "from": 1, "to": 10 } } commonErrors: - error: Unauthorized (401) cause: Missing, invalid, expired, or revoked admin Bearer token solution: Send a valid admin Bearer token (Integration token) in the Authorization header. See the Authentication page. --- # Catalog Categories — Datagrid Listing Paginated, filterable, and sortable category list that mirrors the Bagisto admin **Catalog → Categories** datagrid 1:1. This is the authoritative category-management listing for the admin API — same columns, same filters, and the same sort options used by the datagrid. ::: tip How this menu works For the flat-list vs. tree shapes, hierarchy/move semantics, and display modes, see the [Categories overview](/api/rest-api/admin/catalog/categories/). ::: ::: tip Distinct from the tree endpoint `GET /api/admin/catalog/categories` (this endpoint) returns a **flat, paginated list** of categories — ideal for datagrid/table views. `GET /api/admin/catalog/categories/tree` returns the full **nested hierarchy**, ideal for tree-picker UIs. ::: ## Endpoint | Endpoint | Method | Authentication | |----------|--------|----------------| | `/api/admin/catalog/categories` | GET | Admin Bearer token | ## Authentication Every request requires: ``` Authorization: Bearer ``` Obtain the Bearer token via [Authentication](/api/rest-api/admin/authentication). ## Query Parameters | Parameter | Type | Description | Example | |-----------|------|-------------|---------| | `page` | integer | Page number (1-based, default `1`) | `1` | | `per_page` | integer | Items per page (default `10`, max `50`) | `10` | | `category_id` | string | Filter by category ID — single integer or comma-separated list (e.g. `"12"` or `"12,18"`) | `7` | | `name` | string | Partial category name match (SQL `LIKE %value%`) | `Apparel` | | `position` | integer | Exact position filter | `1` | | `status` | integer | Filter by status: `0` = disabled, `1` = enabled | `1` | | `parent_id` | integer | Filter by parent category ID | `1` | | `locale` | string | Locale code for translation resolution (default: app locale) | `en` | | `sort` | string | Column to sort by (see Sorting section below) | `id` | | `order` | string | Sort direction: `asc` or `desc` (default `desc`) | `desc` | ## Response Shape Responses use the standard admin `{ data, meta }` envelope. ### `meta` object | Field | Type | Description | |-------|------|-------------| | `currentPage` | integer | Current page number (1-based) | | `perPage` | integer | Number of items on this page | | `lastPage` | integer | Total number of pages | | `total` | integer | Total matching categories | | `from` | integer | 1-based index of the first item on this page | | `to` | integer | 1-based index of the last item on this page | ### Row fields (`data[]`) | Field | Type | Description | |-------|------|-------------| | `id` | integer | Category ID | | `position` | integer | Display order position | | `status` | integer | `1` = enabled, `0` = disabled | | `parentId` | integer\|null | ID of the parent category; `null` for root nodes | | `displayMode` | string\|null | How the category page is displayed (e.g. `products_and_description`) | | `logoUrl` | string\|null | Storage URL for the category logo; `null` if not set | | `bannerUrl` | string\|null | Storage URL for the category banner; `null` if not set | | `name` | string\|null | Category name resolved via `locale` | | `slug` | string\|null | URL slug (e.g. `apparel`) | | `description` | string\|null | Category description in the resolved locale | | `locale` | string\|null | Locale code used for resolution | | `createdAt` | string\|null | ISO 8601 creation timestamp | | `updatedAt` | string\|null | ISO 8601 last-update timestamp | | `translations` | null | Always `null` in list responses — full translations are available via the detail endpoint `GET /api/admin/catalog/categories/{id}` | | `filterableAttributeIds` | null | Always `null` in list responses — available via the detail endpoint | ## Example Request ```bash curl -X GET "https://your-domain.com/api/admin/catalog/categories?per_page=10&page=1&status=1&sort=name-asc" \ -H "Authorization: Bearer " \ -H "Accept: application/json" ``` ## Example Response ```json { "data": [ { "id": 7, "position": 1, "status": 1, "parentId": 1, "displayMode": "products_and_description", "logoUrl": null, "bannerUrl": null, "name": "Apparel", "slug": "apparel", "description": "Men's and women's apparel", "locale": "en", "createdAt": "2026-01-12T08:15:00+00:00", "updatedAt": "2026-04-30T14:20:09+00:00", "translations": null, "filterableAttributeIds": null } ], "meta": { "currentPage": 1, "perPage": 10, "lastPage": 5, "total": 47, "from": 1, "to": 10 } } ``` ## Sorting Two forms are accepted — choose whichever suits your client: | Form | Example | |------|---------| | Compound `sort` param | `?sort=name-asc` | | Separate `sort` + `order` params | `?sort=name&order=asc` | When both `order` and a compound `sort` value are present, the explicit `order` param takes precedence. **Sortable columns:** | `sort` value | Sorts by | |---|---| | `id` | Category ID (default) | | `name` | Category name | | `position` | Display order position | | `status` | Enabled/disabled status | ## Pagination - Default page size: **10** items - Maximum page size: **50** items - Use `?page=N` for page navigation and `?per_page=N` to control page size ## Errors | HTTP Status | Cause | |-------------|-------| | `401 Unauthorized` | Missing, expired, or revoked admin Bearer token | | `401 Unauthorized` | Missing or invalid admin Bearer token | **Unknown filter parameters** are silently ignored — no error is returned. Invalid `status` values outside `0` or `1` are also silently dropped (the filter is not applied). ## Notes - **`translations` and `filterableAttributeIds` are always `null` in list rows.** These heavy fields are only populated by the detail endpoint `GET /api/admin/catalog/categories/{id}`, which resolves all locale translations in one call. - **`name`, `slug`, and `description`** are resolved from the `category_translations` table for the requested locale. If no translation exists for a category in that locale, these fields are `null`. - **`parent_id` filter** returns only direct children of the specified parent. For the full subtree rooted at a node, use the tree endpoint with `?rootId=`. - **No automatic status filter.** This endpoint returns all statuses by default — admin operators need to see disabled and draft categories. Pass `?status=1` to restrict to enabled categories. - Envelope-wrapped: `{ data: [...], meta: { currentPage, perPage, lastPage, total, from, to } }`. - `per_page` caps at **50**; values ≤ 0 fall back to the default of **10**. --- # Category — Mass Delete URL: /api/rest-api/admin/catalog/categories/categories-mass-delete --- outline: false apiType: rest examples: - id: admin-catalog-category-mass-delete title: Mass Delete Categories description: Deletes a batch of categories. If any ID in the batch is non-deletable (root or a channel root), the entire batch is rejected (HTTP 400). Non-existent IDs are silently skipped. query: | curl -X POST "https://your-domain.com/api/admin/catalog/categories/mass-delete" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "indices": [12, 18] }' variables: | { "indices": [12, 18] } response: | { "deleted": [12, 18], "message": "Categories deleted successfully." } commonErrors: - error: Root or channel-root in batch (400) cause: At least one ID in `indices` is a root category or referenced as a channel's `root_category_id` solution: Remove non-deletable ids before retrying — the operation is all-or-nothing --- # Category — Mass Delete Deletes a batch of categories in a single request. Pre-validates the entire batch before touching any row. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/catalog/categories/mass-delete` | POST | ## Request body | Field | Type | Required | Notes | |-------|------|----------|-------| | `indices` | integer[] | yes | Category ids to delete. | ## Response `200 OK`: ```json { "deleted": [12, 18], "message": "Categories deleted successfully." } ``` ## Errors | HTTP | Cause | |------|-------| | `400 Bad Request` | At least one id is a root or channel root — whole batch refused | | `401 Unauthorized` | Missing or invalid Bearer token | ## Notes - **All-or-nothing semantics.** A single non-deletable id rejects the entire batch. - **Unknown ids are silently skipped** — they do not appear in `deleted`. --- # Category — Mass Update Status URL: /api/rest-api/admin/catalog/categories/categories-mass-update-status --- outline: false apiType: rest examples: - id: admin-catalog-category-mass-update-status title: Mass Update Category Status description: Sets the status of a batch of categories to the given value (0 or 1). query: | curl -X POST "https://your-domain.com/api/admin/catalog/categories/mass-update-status" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "indices": [12, 18], "value": 1 }' variables: | { "indices": [12, 18], "value": 1 } response: | { "updated": [12, 18], "message": "Categories status updated successfully." } commonErrors: - error: Unauthorized (401) cause: Missing or invalid admin Bearer token solution: Send a valid admin Bearer token (Integration token) in the Authorization header. See the Authentication page. --- # Category — Mass Update Status Bulk-flips the status (enabled/disabled) of a batch of categories. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/catalog/categories/mass-update-status` | POST | ## Request body | Field | Type | Required | Notes | |-------|------|----------|-------| | `indices` | integer[] | yes | Category ids to update. | | `value` | integer | yes | `0` to disable, `1` to enable. | ## Response `200 OK`: ```json { "updated": [12, 18], "message": "Categories status updated successfully." } ``` ## Errors | HTTP | Cause | |------|-------| | `401 Unauthorized` | Missing or invalid Bearer token | --- # Catalog Categories — Tree (Nested) URL: /api/rest-api/admin/catalog/categories/categories-tree --- outline: false apiType: rest examples: - id: admin-catalog-categories-tree title: Category Tree (Nested) description: Full nested category tree. Returns a JSON array of root nodes, each carrying its full subtree under `children`. Supports optional locale, status, and rootId filters. query: | curl -X GET "https://your-domain.com/api/admin/catalog/categories/tree?locale=en&status=1" \ -H "Authorization: Bearer " \ -H "Accept: application/json" variables: | locale=en&status=1 response: | { "data": [ { "id": 1, "name": "Root Category", "slug": "root", "status": 1, "position": 0, "parentId": null, "displayMode": null, "children": [ { "id": 2, "name": "Apparel", "slug": "apparel", "status": 1, "position": 1, "parentId": 1, "displayMode": null, "children": [] }, { "id": 5, "name": "Electronics", "slug": "electronics", "status": 1, "position": 2, "parentId": 1, "displayMode": null, "children": [] } ] } ], "meta": { "currentPage": 1, "perPage": 50, "lastPage": 1, "total": 1, "from": 1, "to": 1 } } commonErrors: - error: Unauthorized (401) cause: Missing, invalid, expired, or revoked admin Bearer token solution: Send a valid admin Bearer token (Integration token) in the Authorization header. See the Authentication page. --- # Catalog Categories — Tree (Nested) Returns the full nested category hierarchy for the admin **Catalog → Categories** tree view. Each node carries the same scalar fields as the flat listing plus a `children` array containing its full subtree. Leaf nodes have `children: []`. ::: tip Distinct from the flat listing `GET /api/admin/catalog/categories/tree` (this endpoint) returns the **nested hierarchy** — ideal for tree-picker UIs and category navigation menus. `GET /api/admin/catalog/categories` returns a **flat, paginated list** — ideal for datagrid/table views with filtering and sorting. ::: ## Endpoint | Endpoint | Method | Authentication | |----------|--------|----------------| | `/api/admin/catalog/categories/tree` | GET | Admin Bearer token | ## Authentication Every request requires: ``` Authorization: Bearer ``` Obtain the Bearer token via [Authentication](/api/rest-api/admin/authentication). ## Query Parameters | Parameter | Type | Description | Example | |-----------|------|-------------|---------| | `locale` | string | Locale code for name/slug resolution (default: app locale) | `en` | | `status` | integer | Filter by status: `0` = disabled, `1` = enabled. Ancestor nodes are preserved even when they are disabled, so children of the matching status remain reachable. | `1` | | `rootId` | integer | Limit the tree to descendants of this category ID (inclusive). Returns an empty array if the ID is unknown. | `1` | ## Response Shape The response uses the standard admin `{ data, meta }` envelope. The `data` array contains root-level category nodes. Each node has the same scalar fields and a `children` array with its subtree. ### Node fields | Field | Type | Description | |-------|------|-------------| | `id` | integer | Category ID | | `name` | string\|null | Category name resolved via `locale` | | `slug` | string\|null | URL slug | | `status` | integer | `1` = enabled, `0` = disabled | | `position` | integer | Display order position | | `parentId` | integer\|null | Parent category ID; `null` for root nodes | | `displayMode` | string\|null | Category display mode (e.g. `products_and_description`) | | `children` | array | Nested child nodes (recursive); `[]` for leaf nodes | ### `meta` object The `meta` object counts **root nodes**, not individual categories. It uses `perPage: 50` by default and `total: N` where N is the number of top-level nodes after filtering. ## Example Request ```bash curl -X GET "https://your-domain.com/api/admin/catalog/categories/tree?locale=en&status=1" \ -H "Authorization: Bearer " \ -H "Accept: application/json" ``` ## Example Response ```json { "data": [ { "id": 1, "name": "Root Category", "slug": "root", "status": 1, "position": 0, "parentId": null, "displayMode": null, "children": [ { "id": 2, "name": "Apparel", "slug": "apparel", "status": 1, "position": 1, "parentId": 1, "displayMode": null, "children": [] }, { "id": 5, "name": "Electronics", "slug": "electronics", "status": 1, "position": 2, "parentId": 1, "displayMode": null, "children": [] } ] } ], "meta": { "currentPage": 1, "perPage": 50, "lastPage": 1, "total": 1, "from": 1, "to": 1 } } ``` ## Filtering with `rootId` Pass `?rootId=` to return only the subtree rooted at that category ID (the node itself plus all descendants): ```bash curl -X GET "https://your-domain.com/api/admin/catalog/categories/tree?rootId=2&locale=en" \ -H "Authorization: Bearer " ``` If the `rootId` does not exist in the database, the response is `{ "data": [], "meta": { ... "total": 0 } }`. ## Errors | HTTP Status | Cause | |-------------|-------| | `401 Unauthorized` | Missing, expired, or revoked admin Bearer token | | `401 Unauthorized` | Missing or invalid admin Bearer token | ## Notes - **`children` is a plain JSON array**, not an IRI reference. You can traverse it directly without follow-up requests. - **`status` filtering preserves ancestor nodes.** When `?status=1` is applied, a disabled parent node still appears if it has at least one enabled descendant, so the tree remains navigable. Leaf nodes that do not match the status are pruned. - **No pagination across the tree levels.** The tree endpoint is not paginated per level — the full subtree of every matched root node is returned in one response. For very large catalogs use `?rootId=` to scope the response. - **URL conflict prevention.** The route `/api/admin/catalog/categories/tree` is registered with `requirements: ['id' => '\d+']` on the detail route so that the string `tree` does not match the `{id}` path segment of `GET /api/admin/catalog/categories/{id}`. - **Slim node shape.** Tree nodes carry 7 scalar fields (id, name, slug, status, position, parentId, displayMode) plus `children`. Full translations and filterable attribute IDs are not included — fetch the detail endpoint for a single category when those are needed. --- # Category — Update (and Move) URL: /api/rest-api/admin/catalog/categories/categories-update --- outline: false apiType: rest examples: - id: admin-catalog-category-update title: Update (or Move) Category description: "Mirrors Bagisto admin Catalog → Categories → Edit. Validation is LOCALE-NESTED — `.slug`, `.name`, `.description` (when display_mode requires it) are required. Moving a category is done by sending `parent_id` + `position` on this same endpoint — there is NO separate /move endpoint." query: | curl -X PUT "https://your-domain.com/api/admin/catalog/categories/7" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "locale": "en", "position": 2, "attributes": [11, 23], "parent_id": 1, "status": 1, "en": { "slug": "apparel", "name": "Apparel", "description": "Men''s and women''s apparel" } }' variables: | id=7 response: | { "id": 7, "name": "Apparel", "slug": "apparel", "position": 2 } commonErrors: - error: Validation (422) cause: Locale-nested `.slug` / `.name` missing, or `.description` missing when `display_mode` requires it solution: 'Wrap translatable fields under the locale key (e.g. `"en": { "slug": ... }`)' - error: Not Found (404) cause: Unknown category id solution: Verify the id with the listing endpoint --- # Category — Update (and Move) Updates an existing category. Mirrors **Catalog → Categories → Edit** in the Bagisto admin panel. ::: warning No separate move endpoint **Move semantics are part of the standard update payload.** To re-parent a category or change its sort position, `PUT` the category with the new `parent_id` and `position`. This mirrors the Bagisto admin panel which has no dedicated "move" action either — `parent_id` + `position` are ordinary editable fields on the category form. ::: ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/catalog/categories/{id}` | PUT | `{id}` must be a positive integer. ## Request body | Field | Type | Required | Notes | |-------|------|----------|-------| | `locale` | string | no | Locale being edited (defaults to the app locale). | | `position` | integer | yes | Display order. | | `attributes` | integer[] | yes | List of filterable attribute ids. | | `parent_id` | integer\|null | no | New parent — supply to re-parent (move) the category. | | `status` | integer | no | `0` / `1`. | | `` | object | yes | **Locale-nested** translatable fields — see below. | ### Locale-nested translatable fields Translatable fields go inside a key matching the locale code (e.g. `"en"`): | Field | Notes | |-------|-------| | `slug` | Required. | | `name` | Required. | | `description` | Required when `display_mode` is `description_only` or `products_and_description`. | | `meta_title` / `meta_description` / `meta_keywords` | Optional SEO fields. | ## Response `200 OK`. Same shape as [`GET /api/admin/catalog/categories/{id}`](/api/rest-api/admin/catalog/categories/categories-detail). ## Errors | HTTP | Cause | |------|-------| | `401 Unauthorized` | Missing or invalid Bearer token | | `404 Not Found` | The category does not exist | | `422 Unprocessable Entity` | Locale-nested validation failure | --- # Attribute Families URL: /api/rest-api/admin/catalog/families --- outline: false apiType: rest --- # Attribute Families An attribute family is the **template of fields a product carries**. Every product belongs to exactly one family, and the family decides which attributes (and in what layout) appear on that product's edit form. The Attribute Families menu lists, creates, edits, and deletes them. It mirrors the admin **Catalog → Attribute Families** screen. ## How a family is structured A family is a set of **attribute groups**, and each group holds a list of **attributes**: - **Groups** (e.g. `General`, `Description`, `Meta Description`, `Price`, `Inventories`, `Images`) organise the edit form into sections. Each group has a `column` (1 or 2) controlling which side of the form it renders on, and a `position` (order). - **Attributes** within a group are references to entries from the [Attributes](/api/rest-api/admin/catalog/attributes/) menu, each with its own `position` and `isRequired` flag. The single-family endpoint returns the full `attributeGroups` array (each group with its nested `attributes`) inline; the listing is slim (id, code, name only) — fetch the structure by id. ## Editing the structure Create and update accept the nested `attribute_groups` (with `custom_attributes`). On update, groups are matched by id; a group keyed `group_*` is created, and an existing group id omitted from the payload is removed (and so are omitted attributes within a kept group). This mirrors the admin form's add/remove behaviour. ## Delete guards A family **cannot be deleted** if it is the last remaining family, or if any product is still assigned to it — both return an error. Reassign or remove those products first. ## Endpoints in this menu | Action | Endpoint | |--------|----------| | [List families](/api/rest-api/admin/catalog/families/families-listing) | `GET /api/admin/catalog/families` | | [Family detail](/api/rest-api/admin/catalog/families/families-detail) | `GET /api/admin/catalog/families/{id}` | | [Create family](/api/rest-api/admin/catalog/families/families-create) | `POST /api/admin/catalog/families` | | [Update family](/api/rest-api/admin/catalog/families/families-update) | `PUT /api/admin/catalog/families/{id}` | | [Delete family](/api/rest-api/admin/catalog/families/families-delete) | `DELETE /api/admin/catalog/families/{id}` | All Attribute Families endpoints require an admin Bearer token — see [Authentication](/api/rest-api/admin/authentication). Reads require `catalog.families.view`; writes require the matching `catalog.families.create` / `.edit` / `.delete` permission. --- # Attribute Family — Create URL: /api/rest-api/admin/catalog/families/families-create --- outline: false apiType: rest examples: - id: admin-catalog-family-create title: Create Attribute Family description: Creates an attribute family with optional nested attribute groups and per-group `custom_attributes`. `code` must be unique and pass the Code rule. query: | curl -X POST "https://your-domain.com/api/admin/catalog/families" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "code": "electronics", "name": "Electronics", "attribute_groups": [ { "code": "general", "name": "General", "column": 1, "position": 1, "custom_attributes": [ { "id": 1 }, { "id": 2 } ] } ] }' variables: | { "code": "electronics", "name": "Electronics" } response: | { "id": 4, "code": "electronics", "name": "Electronics", "attributeGroups": [ { "id": 11, "code": "general", "name": "General", "column": 1, "position": 1, "attributes": [] } ] } commonErrors: - error: Validation (422) cause: '`code` missing, malformed, or duplicate' solution: Send a unique snake_case code that matches the Code rule - error: Unauthorized (401) cause: Missing or invalid admin Bearer token solution: Send a valid admin Bearer token (Integration token) in the Authorization header. See the Authentication page. --- # Attribute Family — Create Creates a new attribute family. Mirrors **Catalog → Attribute Families → Create** in the Bagisto admin panel. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/catalog/families` | POST | ## Request body | Field | Type | Required | Notes | |-------|------|----------|-------| | `code` | string | yes | Snake_case identifier — must be unique and pass the Code rule. | | `name` | string | yes | Display name. | | `attribute_groups` | array | no | Initial groups (see shape below). | ### `attribute_groups[]` shape | Field | Type | Notes | |-------|------|-------| | `code` | string | Group code (snake_case). | | `name` | string | Group display name. | | `column` | integer | Column placement (1 or 2). | | `position` | integer | Sort position within the column. | | `custom_attributes` | array | `[{ id: }, ...]` — attributes to attach. | ## Response `201 Created`. Same shape as [`GET /api/admin/catalog/families/{id}`](/api/rest-api/admin/catalog/families/families-detail). ## Errors | HTTP | Cause | |------|-------| | `401 Unauthorized` | Missing or invalid Bearer token | | `422 Unprocessable Entity` | Validation failure (missing/duplicate `code`, malformed body) | --- # Attribute Family — Delete URL: /api/rest-api/admin/catalog/families/families-delete --- outline: false apiType: rest examples: - id: admin-catalog-family-delete title: Delete Attribute Family description: Refuses with HTTP 400 if the family is the last one in the store, or if any product is still using it. query: | curl -X DELETE "https://your-domain.com/api/admin/catalog/families/4" \ -H "Authorization: Bearer " variables: | id=4 response: | { "message": "Attribute family deleted successfully." } commonErrors: - error: Last family (400) cause: There must be at least one attribute family in the store solution: Create another family before deleting this one - error: Family in use (400) cause: One or more products reference this family solution: Reassign or delete those products first - error: Not Found (404) cause: Unknown family id solution: Verify the id with the listing endpoint --- # Attribute Family — Delete Deletes an attribute family. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/catalog/families/{id}` | DELETE | `{id}` must be a positive integer. ## Response `200 OK`: ```json { "message": "Attribute family deleted successfully." } ``` ## Errors | HTTP | Cause | |------|-------| | `400 Bad Request` | The family is the last one in the store (`At least one attribute family is required.`) | | `400 Bad Request` | Products reference this family | | `401 Unauthorized` | Missing or invalid Bearer token | | `404 Not Found` | The family does not exist | --- # Catalog Attribute Family — Detail URL: /api/rest-api/admin/catalog/families/families-detail --- outline: false apiType: rest examples: - id: admin-catalog-family-detail title: Attribute Family Detail (with attribute groups and attributes) description: Single attribute family record including all attribute groups and — within each group — all associated attributes with their pivot position. Use this to populate the edit form in Catalog → Attribute Families. query: | curl -X GET "https://your-domain.com/api/admin/catalog/families/1" \ -H "Authorization: Bearer " \ -H "Accept: application/json" variables: | id=1 response: | { "id": 1, "code": "default", "name": "Default", "attributeGroups": [ { "id": 1, "code": "general", "name": "General", "column": 1, "position": 1, "attributes": [ { "id": 1, "code": "sku", "type": "text", "isRequired": 1, "column": 1, "position": 1 }, { "id": 2, "code": "name", "type": "text", "isRequired": 1, "column": 1, "position": 2 } ] } ] } commonErrors: - error: Not Found (404) cause: The attribute family ID does not exist in the database solution: 'Verify the ID with the listing endpoint `GET /api/admin/catalog/families`' - error: Unauthorized (401) cause: Missing, invalid, expired, or revoked admin Bearer token solution: Send a valid admin Bearer token (Integration token) in the Authorization header. See the Authentication page. --- # Catalog Attribute Family — Detail Returns a single attribute family record by ID, including all **attribute groups** and — within each group — all **attributes** associated via the `attribute_group_mappings` pivot (with their pivot `position` and `column`). This is the read endpoint to call when an admin needs the complete structure of an attribute family — e.g. when opening the edit form in the **Catalog → Attribute Families** UI. ## Endpoint | Endpoint | Method | Authentication | |----------|--------|----------------| | `/api/admin/catalog/families/{id}` | GET | Admin Bearer token | `{id}` must be a positive integer. Non-numeric values are rejected by a route requirement (`\d+`) — this prevents the `{id}` segment from matching any other path under `/catalog/families/`. ## Authentication Every request requires: ``` Authorization: Bearer ``` Obtain the Bearer token via [Authentication](/api/rest-api/admin/authentication). ## Path Parameter | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `id` | integer | Yes | The numeric attribute family ID | ## Response Shape The response is a single JSON object (not wrapped in `{ data }`) with the following fields: | Field | Type | Description | |-------|------|-------------| | `id` | integer | Attribute family ID | | `code` | string | Family code (e.g. `default`, `apparel`) | | `name` | string | Family display name (e.g. `Default`, `Apparel`) | | `attributeGroups` | array | All attribute groups belonging to this family (see below) | ### `attributeGroups[]` item shape Each entry in the `attributeGroups` array corresponds to one row in `attribute_groups`: | Field | Type | Description | |-------|------|-------------| | `id` | integer | Attribute group ID | | `code` | string | Group code (e.g. `general`, `price`) | | `name` | string | Group display name (e.g. `General`, `Price`) | | `column` | integer | Layout column position for the group (typically `1` or `2`) | | `position` | integer | Display order position of the group within the family | | `attributes` | array | Attributes mapped to this group (see below) | ### `attributeGroups[].attributes[]` item shape Each entry represents one attribute mapped to the group via `attribute_group_mappings`: | Field | Type | Description | |-------|------|-------------| | `id` | integer | Attribute ID | | `code` | string | Attribute code (e.g. `sku`, `name`, `color`) | | `type` | string | Attribute type (e.g. `text`, `select`, `boolean`) | | `isRequired` | integer | `1` = required on product forms, `0` = optional | | `column` | integer | Layout column position of this attribute within the group | | `position` | integer | Display order position of this attribute within the group | ::: tip Plain arrays — no follow-up calls needed `attributeGroups` and the nested `attributes` arrays are serialized as plain inline JSON objects — there are no IRI strings or sub-resource links. The full structure is embedded in a single response. ::: ## Example Request ```bash curl -X GET "https://your-domain.com/api/admin/catalog/families/1" \ -H "Authorization: Bearer " \ -H "Accept: application/json" ``` ## Example Response ```json { "id": 1, "code": "default", "name": "Default", "attributeGroups": [ { "id": 1, "code": "general", "name": "General", "column": 1, "position": 1, "attributes": [ { "id": 1, "code": "sku", "type": "text", "isRequired": 1, "column": 1, "position": 1 }, { "id": 2, "code": "name", "type": "text", "isRequired": 1, "column": 1, "position": 2 } ] } ] } ``` ## Errors | HTTP Status | Cause | |-------------|-------| | `401 Unauthorized` | Missing, expired, or revoked admin Bearer token | | `401 Unauthorized` | Missing or invalid admin Bearer token | | `404 Not Found` | The specified `{id}` does not exist in the database | ## Notes - **`attributeGroups` is a plain JSON array**, not a sub-resource IRI. Groups and their nested attributes are embedded directly in the response — no follow-up requests are needed. - **`attributeGroups` is `null` in listing rows.** The `GET /api/admin/catalog/families` listing returns only `id`, `code`, and `name`. The full nested payload is only available from this detail endpoint. - **No timestamps.** The `attribute_families` table has `$timestamps = false` — there are no `createdAt` or `updatedAt` fields on the family itself. The `attribute_groups` table similarly carries no timestamps. - **`column` and `position` fields** come from the `attribute_group_mappings` pivot and control where each attribute is rendered in the product-creation form layout. `column` is typically `1` or `2` (left or right panel); `position` controls vertical order. - **The `{id}` route parameter must be a digit.** The route carries a `requirements: ['id' => '\d+']` constraint — non-numeric path segments are rejected with `404` before reaching the provider. - **Attribute detail fields are slim.** Only the fields needed for family-structure display are returned per attribute (`id`, `code`, `type`, `isRequired`, `column`, `position`). For the full attribute payload (translations, options, validation), use `GET /api/admin/catalog/attributes/{id}`. --- # Catalog Attribute Families — Datagrid Listing URL: /api/rest-api/admin/catalog/families/families-listing --- outline: false apiType: rest examples: - id: admin-catalog-families-list title: List Attribute Families (Datagrid) description: Paginated, filterable, sortable attribute family list mirroring the Bagisto admin Catalog → Attribute Families datagrid. Returns the standard `{ data, meta }` envelope. Only 3 columns are returned — id, code, name — because the underlying table carries no timestamps. query: | curl -X GET "https://your-domain.com/api/admin/catalog/families?per_page=10&page=1&sort=id&order=desc" \ -H "Authorization: Bearer " \ -H "Accept: application/json" variables: | per_page=10&page=1&sort=id&order=desc response: | { "data": [ { "id": 1, "code": "default", "name": "Default" }, { "id": 3, "code": "apparel", "name": "Apparel" } ], "meta": { "currentPage": 1, "perPage": 10, "lastPage": 1, "total": 2, "from": 1, "to": 2 } } commonErrors: - error: Unauthorized (401) cause: Missing, invalid, expired, or revoked admin Bearer token solution: Send a valid admin Bearer token (Integration token) in the Authorization header. See the Authentication page. --- # Catalog Attribute Families — Datagrid Listing Paginated, filterable, and sortable attribute family list that mirrors the Bagisto admin **Catalog → Attribute Families** datagrid 1:1. This is the authoritative family-management listing for the admin API — same 3 columns and the same sort options used by the datagrid. ::: tip How this menu works For how a family's attribute groups + attributes are structured and the delete guards, see the [Attribute Families overview](/api/rest-api/admin/catalog/families/). ::: ## Endpoint | Endpoint | Method | Authentication | |----------|--------|----------------| | `/api/admin/catalog/families` | GET | Admin Bearer token | ## Authentication Every request requires: ``` Authorization: Bearer ``` Obtain the Bearer token via [Authentication](/api/rest-api/admin/authentication). ## Query Parameters | Parameter | Type | Description | Example | |-----------|------|-------------|---------| | `page` | integer | Page number (1-based, default `1`) | `1` | | `per_page` | integer | Items per page (default `10`, max `50`) | `10` | | `id` | string | Filter by family ID — single integer or comma-separated list (e.g. `"1"` or `"1,2"`) | `1` | | `code` | string | Partial family code match (SQL `LIKE %value%`) | `default` | | `name` | string | Partial family name match (SQL `LIKE %value%`) | `Default` | | `sort` | string | Column to sort by (see Sorting section below) | `id` | | `order` | string | Sort direction: `asc` or `desc` (default `desc`) | `desc` | ## Response Shape Responses use the standard admin `{ data, meta }` envelope. ### `meta` object | Field | Type | Description | |-------|------|-------------| | `currentPage` | integer | Current page number (1-based) | | `perPage` | integer | Number of items on this page | | `lastPage` | integer | Total number of pages | | `total` | integer | Total matching families | | `from` | integer | 1-based index of the first item on this page | | `to` | integer | 1-based index of the last item on this page | ### Row fields (`data[]`) | Field | Type | Description | |-------|------|-------------| | `id` | integer | Attribute family ID | | `code` | string | Family code (e.g. `default`, `apparel`) | | `name` | string | Family display name (e.g. `Default`, `Apparel`) | ::: tip Slim listing by design The listing returns only 3 columns. The `attribute_families` table carries no timestamps (`$timestamps = false` on the Eloquent model), so `createdAt` and `updatedAt` do not exist. Attribute groups and their associated attributes are only available via the detail endpoint `GET /api/admin/catalog/families/{id}`. ::: ## Example Request ```bash curl -X GET "https://your-domain.com/api/admin/catalog/families?per_page=10&page=1&sort=id&order=desc" \ -H "Authorization: Bearer " \ -H "Accept: application/json" ``` ## Example Response ```json { "data": [ { "id": 1, "code": "default", "name": "Default" }, { "id": 3, "code": "apparel", "name": "Apparel" } ], "meta": { "currentPage": 1, "perPage": 10, "lastPage": 1, "total": 2, "from": 1, "to": 2 } } ``` ## Sorting | Parameter form | Example | |----------------|---------| | Separate `sort` + `order` params | `?sort=id&order=desc` | **Sortable columns:** | `sort` value | Sorts by | |---|---| | `id` | Family ID (default) | | `code` | Family code | | `name` | Family display name | ## Pagination - Default page size: **10** items - Maximum page size: **50** items - Use `?page=N` for page navigation and `?per_page=N` to control page size ## Errors | HTTP Status | Cause | |-------------|-------| | `401 Unauthorized` | Missing, expired, or revoked admin Bearer token | | `401 Unauthorized` | Missing or invalid admin Bearer token | **Unknown filter parameters** are silently ignored — no error is returned. ## Notes - **Only 3 columns are returned.** The listing mirrors the admin datagrid exactly: `id`, `code`, `name`. No timestamps exist on the `attribute_families` table. No attribute-group or attribute data is included — use `GET /api/admin/catalog/families/{id}` for the full nested payload. - **No automatic filter applied.** All families (system and user-defined) are returned by default. There is no `is_user_defined` filter on families — all families are returned. - Envelope-wrapped: `{ data: [...], meta: { currentPage, perPage, lastPage, total, from, to } }`. - `per_page` caps at **50**; values ≤ 0 fall back to the default of **10**. --- # Attribute Family — Update URL: /api/rest-api/admin/catalog/families/families-update --- outline: false apiType: rest examples: - id: admin-catalog-family-update title: Update Attribute Family description: Update a family. Inside `attribute_groups`, items keyed by numeric id update existing groups; items keyed by `group_*` create new groups; omitted existing ids are deleted. query: | curl -X PUT "https://your-domain.com/api/admin/catalog/families/4" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "code": "electronics", "name": "Electronics (updated)", "attribute_groups": { "11": { "code": "general", "name": "General", "column": 1, "position": 1, "custom_attributes": [ { "id": 1, "position": 1 } ] }, "group_new_1": { "code": "pricing", "name": "Pricing", "column": 2, "position": 2, "custom_attributes": [ { "id": 11, "position": 1 } ] } } }' variables: | id=4 response: | { "id": 4, "code": "electronics", "name": "Electronics (updated)" } commonErrors: - error: Validation (422) cause: '`code` duplicate or other body validation issue' solution: Verify that the supplied code is unique and well-formed - error: Not Found (404) cause: Unknown family id solution: Verify the id with the listing endpoint --- # Attribute Family — Update Updates an existing attribute family. Mirrors **Catalog → Attribute Families → Edit** in the Bagisto admin panel — same partial-update semantics for nested attribute groups. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/catalog/families/{id}` | PUT | `{id}` must be a positive integer. ## Request body Top-level fields are `code` (required), `name` (required), and `attribute_groups` (object, optional). ### `attribute_groups` semantics The `attribute_groups` field is an **object** keyed by either a numeric existing group id or a `group_*` placeholder for a new group: | Key shape | Effect | |-----------|--------| | `"11"` (numeric id) | Updates the existing group with id 11 | | `"group_new_1"` | Creates a new group | | existing id **omitted** from the payload | Deletes that group | Each value is a group object: | Field | Type | Notes | |-------|------|-------| | `code` / `name` / `column` / `position` | scalars | Same fields as on Create | | `custom_attributes` | array | `[{ id, position }, ...]` — full replacement set for the group | ## Response `200 OK`. Same shape as [`GET /api/admin/catalog/families/{id}`](/api/rest-api/admin/catalog/families/families-detail). ## Errors | HTTP | Cause | |------|-------| | `401 Unauthorized` | Missing or invalid Bearer token | | `404 Not Found` | The family does not exist | | `422 Unprocessable Entity` | `code` duplicate or other body validation issue | --- # Products URL: /api/rest-api/admin/catalog/products --- outline: false apiType: rest examples: - id: admin-catalog-products-list title: List Products (datagrid) description: The full Catalog → Products datagrid. Paginated { data, meta } envelope. Heavy relation blocks and a few EAV-derived fields are null on the listing (detail-only). query: | curl -X GET "https://your-domain.com/api/admin/catalog/products?per_page=2&sort=id&order=asc" \ -H "Authorization: Bearer " \ -H "Accept: application/json" response: | { "data": [ { "id": 1, "sku": "COASTALBREEZEMENSHOODIE", "name": "Coastal Breeze Men's Blue Zipper Hoodie", "type": "simple", "status": 1, "price": "100.0000", "formattedPrice": "$100.00", "specialPrice": null, "formattedSpecialPrice": null, "specialPriceFrom": null, "specialPriceTo": null, "quantity": 10000, "baseImageUrl": "http://localhost:8000/storage/product/1/zKcWZTLDjcawJmaNg8g1cpARqwVONgEKEflabstT.webp", "imagesCount": 1, "categoryId": 22, "categoryName": "Fashion", "channel": "default", "locale": "en", "attributeFamilyId": 1, "attributeFamilyName": "Default", "urlKey": "coastal-breeze-mens-blue-zipper-hoodie", "visibleIndividually": true, "shortDescription": "Stay warm and stylish with the Coastal Breeze Men's Blue Zipper Hoodie...", "description": "The Coastal Breeze Men's Blue Zipper Hoodie is your reliable companion...", "metaTitle": "Coastal Breeze Men's Blue Zipper Hoodie", "metaDescription": "Stay warm and stylish with the Coastal Breeze Men's Blue Zipper Hoodie...", "metaKeywords": "", "weight": 1, "featured": true, "new": true, "createdAt": "2024-04-16 17:32:38", "updatedAt": "2026-04-07 15:20:30", "taxCategoryId": null, "manageStock": null, "inStock": null, "translations": null, "images": null, "categories": null, "inventories": null, "customerGroupPrices": null, "superAttributes": null, "variants": null, "bundleOptions": null, "linkedProducts": null, "downloadableLinks": null, "downloadableSamples": null }, { "id": 22, "sku": "bagistoNGRY3424234KJCKJK", "name": "Acme Drawstring Bag", "type": "bundle", "status": 1, "price": "3000.0000", "formattedPrice": "$3,000.00", "specialPrice": "2700.0000", "formattedSpecialPrice": "$2,700.00", "specialPriceFrom": null, "specialPriceTo": null, "quantity": 0, "baseImageUrl": null, "imagesCount": 0, "categoryId": null, "categoryName": null, "channel": "default", "locale": "en", "attributeFamilyId": 1, "attributeFamilyName": "Default", "urlKey": "acme-drawstring-bag", "visibleIndividually": true, "featured": false, "new": false, "createdAt": "2024-04-16 17:32:38", "updatedAt": "2026-04-07 15:20:30" } ], "meta": { "currentPage": 1, "perPage": 2, "lastPage": 151, "total": 302, "from": 1, "to": 2 } } - id: admin-catalog-products-list-filtered title: List Products (AND-combined filters) description: Filters are AND-combined — more filters narrow the result. Here only active simple products of attribute family 1 within a price band are returned. query: | curl -X GET "https://your-domain.com/api/admin/catalog/products?status=1&type=simple&attribute_family=1&price_from=50&price_to=200&sort=price&order=asc&per_page=10" \ -H "Authorization: Bearer " \ -H "Accept: application/json" response: | { "data": [ { "id": 1, "sku": "COASTALBREEZEMENSHOODIE", "name": "Coastal Breeze Men's Blue Zipper Hoodie", "type": "simple", "status": 1, "price": "100.0000", "formattedPrice": "$100.00", "specialPrice": null, "formattedSpecialPrice": null, "quantity": 10000, "attributeFamilyId": 1, "attributeFamilyName": "Default" } ], "meta": { "currentPage": 1, "perPage": 10, "lastPage": 1, "total": 1, "from": 1, "to": 1 } } --- # Products The Products menu is the catalog's product-management surface — list, search, create, edit, copy, and delete products, plus manage each product's images, per-source inventory, and customer-group prices. It mirrors the admin **Catalog → Products** screen. ## Product types Every product has a `type`, fixed at creation. There are seven: | Type | Notes | |------|-------| | `simple` | A standalone product with its own price and stock. | | `virtual` | Like simple but non-shippable (no weight/dimensions) — services, memberships. | | `downloadable` | Sells downloadable **links** (paid files) and **samples** (free previews); non-stockable. | | `grouped` | A storefront grouping of other simple products (**linked products**); has no own price. | | `bundle` | A configurable kit built from **bundle options**; its price is calculated from the chosen items. | | `configurable` | A parent with **variants** generated from variant-defining attributes (`super_attributes`, e.g. colour × size). Each variant is its own SKU with its own price/stock. | | `booking` | A bookable product (default / appointment / event / rental / table sub-types) with time slots; non-stockable. | **Composite types own no price or stock of their own.** For `configurable`, `bundle`, `grouped`, and `booking`, the price and inventory live on the children — the variants, bundle items, linked products, or slots. Their top-level `price` / `quantity` are derived or empty. ## Creating a product is two steps Creation is deliberately minimal: a `POST` creates the shell from just `sku` + `attribute_family_id` + `type` (plus `super_attributes` for `configurable`). Everything else — name, description, price, images, categories, channels, inventory — is filled in afterwards via the **update** endpoint. This mirrors the admin's create-then-edit wizard. ## `status` vs `visibleIndividually` Two independent flags control storefront presence: - **`status`** — `1` enabled / `0` disabled. A disabled product is fully hidden from the storefront. - **`visibleIndividually`** — whether the product appears in category/search listings. Variant products and grouped-component products are usually set to `0` (reachable only through their parent), while still being `status = 1`. Both must effectively be on for a product to be browsable on its own. (`status` is stored on the flattened product table, resolved per channel + locale.) ## Per-product sub-resources The product edit screen's tabs map to their own endpoint groups, each scoped to one product: - **Images** — upload, reorder, and delete a product's images. - **Inventories** — read and bulk-update the per-inventory-source stock quantities. - **Customer-group prices** — tiered prices that apply to specific customer groups. These are not returned in full on the listing (they're detail-only); the single-product endpoint embeds them inline. ## The product listing `GET /api/admin/catalog/products` is the datagrid — a paginated `{ data, meta }` envelope. ### Listing filters (AND-combined) All filters are **combined with AND** — every filter you add **narrows** the result set further. Pass them as query parameters: | Filter | Type | Description | |--------|------|-------------| | `channel` | string | Channel code used to resolve per-channel values. | | `name` | string | Partial product-name match. | | `sku` | string | Partial SKU match. | | `attribute_family` | integer | Attribute-family ID. | | `price_from` / `price_to` | number | Price band (inclusive). `price=50,200` is shorthand for `price_from=50&price_to=200`. | | `product_id` | string | A single ID, or a comma-separated list (e.g. `1,22,2705`). | | `status` | integer | `0` (disabled) or `1` (active). | | `type` | string | One of the seven product types. | | `locale` | string | Locale code used to resolve translated values. | Plus pagination and sort: | Parameter | Type | Description | |-----------|------|-------------| | `page` | integer | 1-based page number (default `1`). | | `per_page` | integer | Page size (default `10`, max `50`). | | `sort` | string | `product_id` (default), `sku`, `name`, `type`, `status`, `price`, `quantity`, `attribute_family`, `channel`. | | `order` | string | `asc` or `desc` (default `desc`). | ### Listing columns Every row carries these scalar columns. **Heavy fields are `null` on the listing** (fetch them from the [detail endpoint](/api/rest-api/admin/catalog/products/products-detail)): | Field | Type | Notes | |-------|------|-------| | `id` | integer | Product ID. | | `sku` | string | SKU. | | `name` | string\|null | Resolved for the active locale/channel. `null` for draft products with no name yet. | | `type` | string | Product type. | | `status` | integer | `1` active / `0` disabled. | | `price` | string\|null | Base price (composite types carry no own price → `null`). | | `formattedPrice` | string\|null | Locale-formatted base price. | | `specialPrice` | string\|null | Discounted price, when a special price is set. | | `formattedSpecialPrice` | string\|null | Locale-formatted special price. | | `specialPriceFrom` | string\|null | Start date of the special-price window (`null` = always on). | | `specialPriceTo` | string\|null | End date of the special-price window (`null` = no end). | | `quantity` | integer | Total stock across inventory sources. | | `baseImageUrl` | string\|null | Medium-cache base image URL. | | `imagesCount` | integer | Number of images. | | `categoryId` | integer\|null | Primary category ID. | | `categoryName` | string\|null | Primary category name. | | `channel` | string | Resolved channel code. | | `locale` | string | Resolved locale code. | | `attributeFamilyId` | integer | Attribute-family ID. | | `attributeFamilyName` | string | Attribute-family name. | | `urlKey` | string\|null | Storefront URL slug. | | `visibleIndividually` | boolean | Appears in category/search listings. | | `shortDescription` / `description` | string\|null | Resolved content. | | `metaTitle` / `metaDescription` / `metaKeywords` | string\|null | SEO fields. | | `weight` | number\|null | Product weight. | | `featured` | boolean | Featured flag. | | `new` | boolean | "New" flag. | | `createdAt` / `updatedAt` | string | Timestamps. | | `taxCategoryId` | integer\|null | **Detail-only** — `null` on the listing. | | `manageStock` | boolean\|null | **Detail-only** — `null` on the listing. | | `inStock` | boolean\|null | **Detail-only** — `null` on the listing. | | `translations`, `images`, `categories`, `inventories`, `customerGroupPrices`, `superAttributes`, `variants`, `bundleOptions`, `linkedProducts`, `downloadableLinks`, `downloadableSamples` | array\|null | Relation blocks — **all `null` on the listing**; populated only on the [detail endpoint](/api/rest-api/admin/catalog/products/products-detail). | ## Actions | Action | What it does | |--------|--------------| | [Copy](/api/rest-api/admin/catalog/products/copy) | Duplicates an existing product into a new **draft** product (a fresh SKU is generated). | | [Mass delete](/api/rest-api/admin/catalog/products/mass-delete) | Deletes several products at once — body `{ "indices": [1, 22] }`. Missing IDs are skipped. | | [Mass update status](/api/rest-api/admin/catalog/products/mass-update-status) | Bulk enable/disable — body `{ "indices": [1, 22], "value": 0 }` (`0` = disable, `1` = active). | | [Export (CSV)](/api/rest-api/admin/catalog/products/export) | Downloads the listing as a CSV file, honouring the current filters; exports **every matching row**, not just the page. REST only. | ## Endpoints in this menu | Action | Endpoint | |--------|----------| | [List products](/api/rest-api/admin/catalog/products) | `GET /api/admin/catalog/products` | | [Product detail](/api/rest-api/admin/catalog/products/products-detail) | `GET /api/admin/catalog/products/{id}` | | [Create product](/api/rest-api/admin/catalog/products/create) | `POST /api/admin/catalog/products` | | [Update product](/api/rest-api/admin/catalog/products/update) | `PUT /api/admin/catalog/products/{id}` | | [Delete product](/api/rest-api/admin/catalog/products/delete) | `DELETE /api/admin/catalog/products/{id}` | | [Copy product](/api/rest-api/admin/catalog/products/copy) | `POST /api/admin/catalog/products/{id}/copy` | | [Mass delete](/api/rest-api/admin/catalog/products/mass-delete) | `POST /api/admin/catalog/products/mass-delete` | | [Mass update status](/api/rest-api/admin/catalog/products/mass-update-status) | `POST /api/admin/catalog/products/mass-update-status` | | [Export products (CSV)](/api/rest-api/admin/catalog/products/export) | `GET /api/admin/catalog/products/export` | | [Upload images](/api/rest-api/admin/catalog/products/images-upload) | `POST /api/admin/catalog/products/{id}/images` | | [Reorder images](/api/rest-api/admin/catalog/products/images-reorder) | `PUT /api/admin/catalog/products/{id}/images/reorder` | | [Delete image](/api/rest-api/admin/catalog/products/images-delete) | `DELETE /api/admin/catalog/products/{id}/images/{imageId}` | | [List inventories](/api/rest-api/admin/catalog/products/inventories-list) | `GET /api/admin/catalog/products/{id}/inventories` | | [Update inventories](/api/rest-api/admin/catalog/products/inventories-update) | `PUT /api/admin/catalog/products/{id}/inventories` | | [List customer-group prices](/api/rest-api/admin/catalog/products/customer-group-prices-list) | `GET /api/admin/catalog/products/{id}/customer-group-prices` | | [Add customer-group price](/api/rest-api/admin/catalog/products/customer-group-prices-create) | `POST /api/admin/catalog/products/{id}/customer-group-prices` | | [Update customer-group price](/api/rest-api/admin/catalog/products/customer-group-prices-update) | `PUT /api/admin/catalog/products/{id}/customer-group-prices/{priceId}` | | [Delete customer-group price](/api/rest-api/admin/catalog/products/customer-group-prices-delete) | `DELETE /api/admin/catalog/products/{id}/customer-group-prices/{priceId}` | The canonical product listing is [List products](/api/rest-api/admin/catalog/products) (`GET /api/admin/catalog/products`) above. There is also a separate slim [Add-Product Search](/api/rest-api/admin/catalog/products/list) (`GET /api/admin/products`) used only by the Create-Order "Add Product" modal — not the product listing. All Products endpoints require an admin Bearer token — see [Authentication](/api/rest-api/admin/authentication). Reads require `catalog.products.view`; writes require the matching `catalog.products.create` / `.edit` / `.delete` permission. --- # List Products URL: /api/rest-api/admin/catalog/products --- outline: false apiType: rest examples: - id: admin-catalog-products-list title: List Products (Datagrid) description: The canonical admin product listing — paginated, filterable, and sortable, mirroring the Bagisto admin Catalog → Products datagrid. Returns the full product row in the standard `{ data, meta }` envelope. query: | curl -X GET "https://your-domain.com/api/admin/catalog/products?per_page=10&page=1&type=simple&status=1" \ -H "Authorization: Bearer " \ -H "Accept: application/json" variables: | per_page=10&page=1&type=simple&status=1 response: | { "data": [ { "id": 22, "sku": "bagistoNGRY3424234KJCKJK", "name": "Acme Drawstring Bag", "type": "simple", "status": 1, "price": "3000.0000", "formattedPrice": "$3,000.00", "specialPrice": "2700.0000", "formattedSpecialPrice": "$2,700.00", "specialPriceFrom": null, "specialPriceTo": null, "quantity": 98, "baseImageUrl": "http://localhost:8000/storage/product/22/1qfyoglc5BP46kofrxYrkJ2MXRxu9lAVG3BDFlTZ.webp", "imagesCount": 1, "categoryId": null, "categoryName": null, "channel": "default", "locale": "en", "attributeFamilyId": 1, "attributeFamilyName": "Default", "urlKey": "acme-drawstring-bag", "visibleIndividually": true, "shortDescription": "Many desktop publishing packages and web page editors now use", "metaTitle": "", "metaDescription": "", "metaKeywords": "", "weight": 32, "featured": true, "new": true, "createdAt": "2024-04-19 11:56:43", "updatedAt": "2026-04-23 16:36:14" } ], "meta": { "currentPage": 1, "perPage": 10, "lastPage": 27, "total": 265, "from": 1, "to": 10 } } commonErrors: - error: Unauthorized (401) cause: Missing, invalid, expired, or revoked admin Bearer token solution: Send a valid admin Bearer token (Integration token) in the Authorization header. See the Authentication page. --- # List Products The **canonical admin product listing** — a paginated, filterable, and sortable product list that mirrors the Bagisto admin **Catalog → Products** datagrid 1:1. Same columns, same filters, and the same sort options used by the admin screen. This is the listing you want for product-management screens. ::: tip How this menu works For the product types, the two-step create flow, status vs. visibleIndividually, and the per-product sub-resources, see the [Products overview](/api/rest-api/admin/catalog/products/). ::: ::: tip Not the Create-Order search `GET /api/admin/catalog/products` (this endpoint) is the full product listing. A separate slim search — [`GET /api/admin/products`](/api/rest-api/admin/catalog/products/list) — powers the admin Create-Order "Add Product" modal only. Use this page for the actual product listing. ::: ## Endpoint | Endpoint | Method | Authentication | |----------|--------|----------------| | `/api/admin/catalog/products` | GET | Admin Bearer token | ## Authentication Every request requires: ``` Authorization: Bearer ``` Obtain the Bearer token via [Authentication](/api/rest-api/admin/authentication). ## Query Parameters | Parameter | Type | Description | Example | |-----------|------|-------------|---------| | `page` | integer | Page number (1-based, default `1`) | `1` | | `per_page` | integer | Items per page (default `10`, max `50`) | `10` | | `product_id` | string | Filter by product ID — single integer or comma-separated list | `142` or `1,2,3` | | `sku` | string | Partial SKU match (SQL `LIKE %value%`) | `SP-001` | | `name` | string | Partial product name match (SQL `LIKE %value%`) | `Classic Watch` | | `type` | string | Filter by product type | `simple` | | `status` | integer | Filter by status: `0` = disabled, `1` = enabled | `1` | | `attribute_family` | integer | Filter by attribute family ID | `1` | | `channel` | string | Channel code for locale/price resolution (default: current channel) | `default` | | `locale` | string | Locale code for name/category resolution (default: app locale) | `en` | | `price_from` | number | Minimum price filter (inclusive) | `10.00` | | `price_to` | number | Maximum price filter (inclusive) | `500.00` | | `price` | string | Price range shorthand — `"min,max"`. Overridden by `price_from`/`price_to` when both are present | `10,500` | | `sort` | string | Column to sort by (see Sorting section below) | `product_id` | | `order` | string | Sort direction: `asc` or `desc` (default `desc`) | `desc` | ### Valid `type` values `simple`, `configurable`, `bundle`, `grouped`, `downloadable`, `virtual`, `booking` ## Response Shape Responses use the standard admin `{ data, meta }` envelope. ### `meta` object | Field | Type | Description | |-------|------|-------------| | `currentPage` | integer | Current page number (1-based) | | `perPage` | integer | Number of items on this page | | `lastPage` | integer | Total number of pages | | `total` | integer | Total matching products | | `from` | integer | 1-based index of the first item on this page | | `to` | integer | 1-based index of the last item on this page | ### Row fields (`data[]`) | Field | Type | Description | |-------|------|-------------| | `id` | integer | Product ID | | `sku` | string\|null | Product SKU | | `name` | string\|null | Product name (resolved via `locale` and `channel`) | | `type` | string\|null | Product type (e.g. `simple`, `configurable`) | | `status` | integer\|null | `1` = enabled, `0` = disabled | | `price` | string\|null | Raw price value (decimal string, e.g. `"3000.0000"`) | | `formattedPrice` | string\|null | Locale-formatted price (e.g. `"$3,000.00"`) | | `specialPrice` | string\|null | Raw special (sale) price as a decimal string; `null` if none | | `formattedSpecialPrice` | string\|null | Locale-formatted special price; `null` if none | | `specialPriceFrom` | string\|null | Start of the special-price window; `null` unless a dated window is set | | `specialPriceTo` | string\|null | End of the special-price window; `null` unless a dated window is set | | `quantity` | integer | Sum of inventory qty across all inventory sources | | `baseImageUrl` | string\|null | Storage URL of the product's first image; `null` if no images | | `imagesCount` | integer | Total number of images attached to the product | | `categoryId` | integer\|null | ID of the first category this product belongs to; `null` if uncategorized | | `categoryName` | string\|null | Translated name of that category (resolved via `locale`); `null` if uncategorized | | `channel` | string\|null | Channel code used for resolution | | `locale` | string\|null | Locale code used for resolution | | `attributeFamilyId` | integer\|null | Attribute family ID | | `attributeFamilyName` | string\|null | Attribute family name | | `urlKey` | string\|null | URL slug (e.g. `acme-drawstring-bag`) | | `visibleIndividually` | boolean\|null | Whether the product appears in category/search listings | | `shortDescription` | string\|null | Short description | | `metaTitle` | string\|null | SEO meta title (empty string when unset) | | `metaDescription` | string\|null | SEO meta description (empty string when unset) | | `metaKeywords` | string\|null | SEO meta keywords (empty string when unset) | | `weight` | number\|null | Product weight | | `featured` | boolean | Whether the product is flagged as featured | | `new` | boolean | Whether the product is flagged as "new" | | `createdAt` | string | Creation timestamp | | `updatedAt` | string | Last-update timestamp | Notes on the listing values: - `price`, `specialPrice` are **decimal strings**, not numbers. - `specialPriceFrom` / `specialPriceTo` are `null` unless a **dated** special-price window is configured. - `quantity` is the **summed** inventory across all sources. The following fields are **detail-only** and always come back `null` on the listing — fetch them from the single-product endpoint `GET /api/admin/catalog/products/{id}`: - `taxCategoryId`, `manageStock`, `inStock` - the relation blocks: `translations`, `images`, `categories`, `inventories`, `customerGroupPrices`, `superAttributes`, `variants`, `bundleOptions`, `linkedProducts`, `downloadableLinks`, `downloadableSamples`, `videos`, `channels`, `relatedProducts`, `upSells`, `crossSells`. ## Example Request ```bash curl -X GET "https://your-domain.com/api/admin/catalog/products?per_page=10&page=1&type=simple&status=1" \ -H "Authorization: Bearer " \ -H "Accept: application/json" ``` ## Example Response ```json { "data": [ { "id": 22, "sku": "bagistoNGRY3424234KJCKJK", "name": "Acme Drawstring Bag", "type": "simple", "status": 1, "price": "3000.0000", "formattedPrice": "$3,000.00", "specialPrice": "2700.0000", "formattedSpecialPrice": "$2,700.00", "specialPriceFrom": null, "specialPriceTo": null, "quantity": 98, "baseImageUrl": "http://localhost:8000/storage/product/22/1qfyoglc5BP46kofrxYrkJ2MXRxu9lAVG3BDFlTZ.webp", "imagesCount": 1, "categoryId": null, "categoryName": null, "channel": "default", "locale": "en", "attributeFamilyId": 1, "attributeFamilyName": "Default", "urlKey": "acme-drawstring-bag", "visibleIndividually": true, "shortDescription": "Many desktop publishing packages and web page editors now use", "metaTitle": "", "metaDescription": "", "metaKeywords": "", "weight": 32, "featured": true, "new": true, "createdAt": "2024-04-19 11:56:43", "updatedAt": "2026-04-23 16:36:14" } ], "meta": { "currentPage": 1, "perPage": 10, "lastPage": 27, "total": 265, "from": 1, "to": 10 } } ``` ## Sorting Two forms are accepted — choose whichever suits your client: | Form | Example | |------|---------| | Compound `sort` param | `?sort=name-asc` | | Separate `sort` + `order` params | `?sort=name&order=asc` | When both `order` and a compound `sort` value are present, the explicit `order` param takes precedence. **Sortable columns:** | `sort` value | Sorts by | |---|---| | `product_id` | Product ID (default) | | `name` | Product name | | `sku` | SKU | | `price` | Price | | `quantity` | Inventory quantity (SUM across sources) | | `status` | Enabled/disabled status | | `type` | Product type | | `attribute_family` | Attribute family ID | | `channel` | Channel code | ## Pagination - Default page size: **10** items - Maximum page size: **50** items - Use `?page=N` for page navigation and `?per_page=N` to control page size ## Errors | HTTP Status | Cause | |-------------|-------| | `401 Unauthorized` | Missing, expired, or revoked admin Bearer token | | `401 Unauthorized` | Missing or invalid admin Bearer token | **Unknown filter parameters** (e.g. a misspelled `?tpye=simple`) are silently ignored — no error is returned. Invalid `status` or `type` values outside their documented ranges are also silently dropped (the filter is not applied). ## Notes - **Elasticsearch is not yet supported.** Even when the Bagisto admin panel is configured to use Elasticsearch for catalog search, this endpoint always uses the database query path. - **No automatic status filter.** Unlike `GET /api/shop/products` which only returns `status = 1` products, this endpoint returns all statuses by default. Admin operators need to see disabled and draft products. Pass `?status=1` to restrict to enabled products. - **Multi-category products** — only the first associated category's `categoryId` and `categoryName` are included in the row (matching the datagrid display). - **Products with no inventory** return `quantity: 0`. - **Products with no images** return `baseImageUrl: null` and `imagesCount: 0`. --- # Catalog Product — Copy URL: /api/rest-api/admin/catalog/products/copy --- outline: false apiType: rest examples: - id: admin-catalog-product-copy title: Copy a Catalog Product description: Duplicates an existing product across all attribute_values, images, inventories, categories and customer_group_prices. Refuses configurable variants. Mirrors Bagisto monolith `ProductController::copy`. query: | curl -X POST "https://your-domain.com/api/admin/catalog/products/12/copy" \ -H "Authorization: Bearer " variables: | {} response: | { "id": 43, "sourceId": 12, "sku": "SKU-001-copy-1", "type": "simple", "name": "Test SKU-001 (Copy)", "success": true, "message": "Product copied successfully." } commonErrors: - error: Not Found (404) cause: Source product not found solution: Verify the `{sourceId}` exists in `products` - error: Unprocessable Entity (422) cause: Source product is a configurable variant (parent_id is set) solution: Copy the parent configurable, not the variant - error: Forbidden (403) cause: Admin role lacks `catalog.products.create` solution: Grant the permission to the admin role --- # Catalog Product — Copy Duplicates an existing catalog product. Fires `catalog.product.create.before` / `catalog.product.create.after` so listeners (search index, cache flush) trigger on the copy. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/catalog/products/{sourceId}/copy` | POST | ## Path parameters | Field | Type | Required | Notes | |-------|------|----------|-------| | `sourceId` | integer | yes | ID of the product to duplicate. | ## Request body Empty. Send an empty JSON object `{}` if your client requires a body. ## Response `200 OK` | Field | Type | Notes | |-------|------|-------| | `id` | integer | ID of the newly created copy. | | `sourceId` | integer | ID of the source product. | | `sku` | string | Auto-suffixed SKU (Bagisto appends `-copy-N`). | | `type` | string | Product type — matches the source. | | `name` | string\|null | Display name of the copy, when available. | | `success` | bool | Always `true` on success. | | `message` | string | Translated confirmation. | ## Errors | HTTP | Cause | |------|-------| | `401 Unauthorized` | Missing or invalid admin Bearer token. | | `403 Forbidden` | Admin role lacks `catalog.products.create`. | | `404 Not Found` | Source product not found. | | `422 Unprocessable Entity` | Source is a configurable variant (`parent_id != null`). | | `500 Internal Server Error` | Underlying copy threw an exception. | ## Notes - The new SKU suffix is generated by Bagisto core — clients cannot pre-specify it. - Variant rows are NOT copy targets — only the parent configurable product is. --- # Catalog Product — Create (step 1) URL: /api/rest-api/admin/catalog/products/create --- outline: false apiType: rest examples: - id: admin-catalog-product-create-simple title: Create — Simple description: Step-1 create for a simple product. Only sku, attribute_family_id and type are submitted; everything else (name, price, inventory, images) is added later via the Update endpoint. query: | curl -X POST "https://your-domain.com/api/admin/catalog/products" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "sku": "sp-001", "attribute_family_id": 1, "type": "simple" }' variables: | { "sku": "sp-001", "attribute_family_id": 1, "type": "simple" } response: | { "id": 43, "sku": "sp-001", "type": "simple", "attributeFamilyId": 1, "attributeFamilyName": "Default", "name": null, "status": null, "price": null } - id: admin-catalog-product-create-virtual title: Create — Virtual description: Step-1 create for a virtual (non-shippable) product. Same minimal body as a simple product. query: | curl -X POST "https://your-domain.com/api/admin/catalog/products" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "sku": "vr-001", "attribute_family_id": 1, "type": "virtual" }' variables: | { "sku": "vr-001", "attribute_family_id": 1, "type": "virtual" } response: | { "id": 44, "sku": "vr-001", "type": "virtual", "attributeFamilyId": 1, "attributeFamilyName": "Default", "name": null, "status": null, "price": null } - id: admin-catalog-product-create-downloadable title: Create — Downloadable description: Step-1 create for a downloadable product. The download links and samples are configured later via the Update endpoint. query: | curl -X POST "https://your-domain.com/api/admin/catalog/products" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "sku": "dl-001", "attribute_family_id": 1, "type": "downloadable" }' variables: | { "sku": "dl-001", "attribute_family_id": 1, "type": "downloadable" } response: | { "id": 45, "sku": "dl-001", "type": "downloadable", "attributeFamilyId": 1, "attributeFamilyName": "Default", "name": null, "status": null, "price": null } - id: admin-catalog-product-create-grouped title: Create — Grouped description: Step-1 create for a grouped product. The associated/linked products are added later via the Update endpoint. query: | curl -X POST "https://your-domain.com/api/admin/catalog/products" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "sku": "gr-001", "attribute_family_id": 1, "type": "grouped" }' variables: | { "sku": "gr-001", "attribute_family_id": 1, "type": "grouped" } response: | { "id": 46, "sku": "gr-001", "type": "grouped", "attributeFamilyId": 1, "attributeFamilyName": "Default", "name": null, "status": null, "price": null } - id: admin-catalog-product-create-bundle title: Create — Bundle description: Step-1 create for a bundle product. The bundle option groups are configured later via the Update endpoint. query: | curl -X POST "https://your-domain.com/api/admin/catalog/products" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "sku": "bn-001", "attribute_family_id": 1, "type": "bundle" }' variables: | { "sku": "bn-001", "attribute_family_id": 1, "type": "bundle" } response: | { "id": 47, "sku": "bn-001", "type": "bundle", "attributeFamilyId": 1, "attributeFamilyName": "Default", "name": null, "status": null, "price": null } - id: admin-catalog-product-create-configurable title: Create — Configurable description: Step-1 create for a configurable product. super_attributes is REQUIRED — a map of attribute code (or id) to a list of option ids. The store generates the cartesian product of variants from these options. query: | curl -X POST "https://your-domain.com/api/admin/catalog/products" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "sku": "cf-001", "attribute_family_id": 1, "type": "configurable", "super_attributes": { "color": [1, 2], "size": [6, 7] } }' variables: | { "sku": "cf-001", "attribute_family_id": 1, "type": "configurable", "super_attributes": { "color": [1, 2], "size": [6, 7] } } response: | { "id": 48, "sku": "cf-001", "type": "configurable", "attributeFamilyId": 1, "attributeFamilyName": "Default", "name": null, "status": null, "price": null, "variants": [ { "id": 49, "sku": "cf-001-variant-1" }, { "id": 50, "sku": "cf-001-variant-2" }, { "id": 51, "sku": "cf-001-variant-3" }, { "id": 52, "sku": "cf-001-variant-4" } ] } - id: admin-catalog-product-create-booking title: Create — Booking description: Step-1 create for a booking product. The booking sub-type (default / appointment / event / rental / table) and its slots/tickets are configured later via the Update endpoint. query: | curl -X POST "https://your-domain.com/api/admin/catalog/products" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "sku": "bk-001", "attribute_family_id": 1, "type": "booking" }' variables: | { "sku": "bk-001", "attribute_family_id": 1, "type": "booking" } response: | { "id": 53, "sku": "bk-001", "type": "booking", "attributeFamilyId": 1, "attributeFamilyName": "Default", "name": null, "status": null, "price": null } commonErrors: - error: Validation (422) cause: Missing sku/family, unsupported type, duplicate SKU, invalid slug, or unknown family solution: Send a unique SKU and a valid attribute_family_id - error: Validation (422) cause: Type is `configurable` but `super_attributes` is missing or empty solution: Send a non-empty map of attribute code (or id) to option ids --- # Catalog Product — Create (step 1) Creates a new catalog product — mirrors the Bagisto admin Create-Product wizard step 1. Only the bare-minimum fields are accepted at this step; everything else (name, description, price, inventories, images, variants, booking slots, etc.) is added through the [Update endpoint](/api/rest-api/admin/catalog/products/update). ::: tip Single-step configurable create This endpoint accepts `super_attributes` in the same request as the create. The store then generates the full cartesian product of variants from the option ids you pass. ::: ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/catalog/products` | POST | ## Request body | Field | Type | Required | Notes | |-------|------|----------|-------| | `sku` | string | yes | Unique product SKU. Slug-validated. | | `attribute_family_id` | integer | yes | Existing attribute family ID. | | `type` | string | yes | One of `simple`, `virtual`, `downloadable`, `grouped`, `bundle`, `configurable`, `booking`. | | `super_attributes` | object | conditional | **Required when `type=configurable`**. Map of attribute code (or id) → non-empty list of option ids. e.g. `{ "color": [1, 2], "size": [6, 7] }`. | For every type except `configurable`, the body is just `sku` + `attribute_family_id` + `type`. Configurable additionally requires `super_attributes`. ### Booking products `type=booking` creates the parent booking product. The 5 sub-types (`default` / `appointment` / `event` / `rental` / `table`) and their slots or tickets are configured during the [Update](/api/rest-api/admin/catalog/products/update) call. ## Response `201 Created` returning the full product detail payload — most fields will be `null` because only `sku`, `type`, and `attribute_family_id` are populated at this point. For `configurable`, the generated `variants` are included so you can reference each variant id when you fill in per-variant pricing via Update. ## Errors | HTTP | Cause | |------|-------| | `401 Unauthorized` | Missing or invalid admin Bearer token. | | `403 Forbidden` | Admin role lacks `catalog.products.create`. | | `422 Unprocessable Entity` | Validation failed — missing sku/family, unsupported type, duplicate SKU, invalid slug, unknown family, missing/invalid `super_attributes` for configurable. | ## Notes - The next call is typically `PUT /api/admin/catalog/products/{id}` to populate the rest of the fields. - See [Update](/api/rest-api/admin/catalog/products/update) for the per-type structure payloads (variants, bundle options, links, booking slots/tickets). --- # Product Customer-Group Prices — Create URL: /api/rest-api/admin/catalog/products/customer-group-prices-create --- outline: false apiType: rest examples: - id: admin-catalog-product-cgp-create title: Add a Customer-Group Price description: "Creates a new tier-price row. `customer_group_id: null` makes the price apply to every customer group. The combination `(qty, customer_group_id)` must be unique per product." query: | curl -X POST "https://your-domain.com/api/admin/catalog/products/1/customer-group-prices" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "qty": 10, "value_type": "discount", "value": 15.0, "customer_group_id": 2 }' variables: | { "qty": 10, "value_type": "discount", "value": 15.0, "customer_group_id": 2 } response: | { "id": 12, "qty": 10, "valueType": "discount", "value": 15.0, "customerGroupId": 2, "productId": 1 } commonErrors: - error: Validation (422) cause: Duplicate qty/customer-group combo, unknown group, or qty < 1 solution: Pick a unique `(qty, customer_group_id)` combo for the product - error: Not Found (404) cause: Product not found solution: Verify `{productId}` exists --- # Product Customer-Group Prices — Create Adds a new tier-price row to a product. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/catalog/products/{productId}/customer-group-prices` | POST | ## Request body | Field | Type | Required | Notes | |-------|------|----------|-------| | `qty` | integer | yes | Minimum qty at which the tier applies. ≥ 1. | | `value_type` | string | yes | `fixed` (absolute price) or `discount` (percent off). | | `value` | number | yes | Price (fixed) or percent (discount). | | `customer_group_id` | integer\|null | no | `null` = applies to every group. | The combination `(qty, customer_group_id)` must be unique per product. ## Response `201 Created` — the new row. ## Errors | HTTP | Cause | |------|-------| | `404 Not Found` | Product not found. | | `422 Unprocessable Entity` | Duplicate `(qty, customer_group_id)`, unknown group, or `qty < 1`. | --- # Product Customer-Group Prices — Delete URL: /api/rest-api/admin/catalog/products/customer-group-prices-delete --- outline: false apiType: rest examples: - id: admin-catalog-product-cgp-delete title: Delete a Customer-Group Price description: Deletes a single customer-group price row. query: | curl -X DELETE "https://your-domain.com/api/admin/catalog/products/1/customer-group-prices/12" \ -H "Authorization: Bearer " variables: | {} response: | { "message": "Customer-group price deleted successfully." } --- # Product Customer-Group Prices — Delete Deletes a tier-price row. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/catalog/products/{productId}/customer-group-prices/{id}` | DELETE | ## Response `200 OK` | Field | Type | Notes | |-------|------|-------| | `message` | string | Confirmation. | ## Errors | HTTP | Cause | |------|-------| | `404 Not Found` | Row not found or does not belong to product. | --- # Product Customer-Group Prices — List URL: /api/rest-api/admin/catalog/products/customer-group-prices-list --- outline: false apiType: rest examples: - id: admin-catalog-product-cgp-list title: List Customer-Group (Tier) Prices description: Lists every customer-group price row attached to the product, in the standard admin `{ data, meta }` envelope. query: | curl "https://your-domain.com/api/admin/catalog/products/1/customer-group-prices" \ -H "Authorization: Bearer " variables: | {} response: | { "data": [ { "id": 12, "qty": 1, "valueType": "fixed", "value": 19.99, "customerGroupId": 2, "customerGroupName": "Wholesale", "productId": 1 } ], "meta": { "currentPage": 1, "perPage": 1, "lastPage": 1, "total": 1, "from": 1, "to": 1 } } --- # Product Customer-Group Prices — List Lists every tier-price row attached to a product. ::: tip Fresh sub-resource under `src/Admin/` This is the **fresh** sub-resource that replaces the legacy admin operations on the storefront `ProductCustomerGroupPrice` resource. The legacy operations are scheduled for removal in Phase 6 — see the package CLAUDE.md "Legacy admin endpoints" section. ::: ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/catalog/products/{productId}/customer-group-prices` | GET | ## Response `200 OK` — `{ data, meta }` envelope, one row per `product_customer_group_prices`. | Field | Type | Notes | |-------|------|-------| | `data[].id` | integer | Row ID. | | `data[].qty` | integer | Minimum quantity at which the tier applies. | | `data[].valueType` | string | `fixed` or `discount`. | | `data[].value` | float | Fixed price or percent discount. | | `data[].customerGroupId` | integer\|null | `null` = applies to every group. | | `data[].customerGroupName` | string | Group label. | | `data[].productId` | integer | Parent product. | --- # Product Customer-Group Prices — Update URL: /api/rest-api/admin/catalog/products/customer-group-prices-update --- outline: false apiType: rest examples: - id: admin-catalog-product-cgp-update title: Update a Customer-Group Price description: Partially updates the given tier-price row. The new `(qty, customer_group_id)` combination must remain unique for the product. query: | curl -X PUT "https://your-domain.com/api/admin/catalog/products/1/customer-group-prices/12" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "qty": 5, "value_type": "fixed", "value": 17.5, "customer_group_id": null }' variables: | { "qty": 5, "value_type": "fixed", "value": 17.5, "customer_group_id": null } response: | { "id": 12, "qty": 5, "valueType": "fixed", "value": 17.5, "customerGroupId": null, "productId": 1 } --- # Product Customer-Group Prices — Update Updates a tier-price row. Partial: only send the fields you want to change. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/catalog/products/{productId}/customer-group-prices/{id}` | PUT | ## Request body Same field set as Create — `qty`, `value_type`, `value`, `customer_group_id`. ## Errors | HTTP | Cause | |------|-------| | `404 Not Found` | Product or row not found, or row does not belong to product. | | `422 Unprocessable Entity` | Validation failed (uniqueness, unknown group, etc.). | --- # Catalog Product — Delete URL: /api/rest-api/admin/catalog/products/delete --- outline: false apiType: rest examples: - id: admin-catalog-product-delete title: Delete a Catalog Product description: Deletes a catalog product. For configurable products, all variants cascade. No "refuse if in non-completed order" guard — mirrors Bagisto admin behaviour. query: | curl -X DELETE "https://your-domain.com/api/admin/catalog/products/42" \ -H "Authorization: Bearer " variables: | {} response: | commonErrors: - error: Not Found (404) cause: Product not found solution: Verify the `{id}` exists - error: Forbidden (403) cause: Admin role lacks `catalog.products.delete` solution: Grant the permission to the admin role --- # Catalog Product — Delete Deletes a catalog product. Fires `catalog.product.delete.before` / `catalog.product.delete.after`. ::: warning No "in-order" delete guard (parity with monolith) Bagisto admin does **not** refuse to delete a product that appears in non-completed orders. This endpoint matches that behaviour — order items preserve their snapshot data after the product row is gone. If you need referential integrity, add the guard at the application layer. ::: ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/catalog/products/{id}` | DELETE | ## Path parameters | Field | Type | Required | Notes | |-------|------|----------|-------| | `id` | integer | yes | Product ID. | ## Response `204 No Content` on success. ## Errors | HTTP | Cause | |------|-------| | `401 Unauthorized` | Missing or invalid admin Bearer token. | | `403 Forbidden` | Admin role lacks `catalog.products.delete`. | | `404 Not Found` | Product not found. | ## Notes - For configurable products, all variants cascade automatically. --- # Export Products URL: /api/rest-api/admin/catalog/products/export --- outline: false apiType: rest examples: - id: admin-products-export title: Export Products (CSV) description: Download the products datagrid as a CSV file — the same data the admin Catalog → Products "Export" button produces. Honours the same filters as the listing and exports EVERY matching row (not just the current page). query: | curl -X GET "https://your-domain.com/api/admin/catalog/products/export?format=csv" \ -H "Authorization: Bearer " \ -H "Accept: text/csv" \ --output products.csv response: | # Binary response: a text/csv attachment is written to products.csv # (Content-Type: text/csv; charset=UTF-8 # Content-Disposition: attachment; filename="products.csv"). Sample contents: ID,Name,SKU,"Attribute Family",Price,Quantity,Status,Category,Type 1,"Coastal Breeze Men's Blue Zipper Hoodie",COASTALBREEZEMENSHOODIE,Default,$100.00,10000,Active,Fashion,simple 22,"Acme Drawstring Bag",bagistoNGRY3424234KJCKJK,Default,"$3,000.00",0,Active,,bundle 2705,,temporary-sku-8816cd,Default,,1,Disabled,,simple - id: admin-products-export-filtered title: Export Products (filtered) description: The export honours every listing filter — pass them as query params to export exactly the rows you are viewing. Here only active products of one attribute family within a price band are exported. query: | curl -X GET "https://your-domain.com/api/admin/catalog/products/export?format=csv&status=1&attribute_family=1&price_from=50&price_to=200" \ -H "Authorization: Bearer " \ -H "Accept: text/csv" \ --output products.csv response: | # Only rows matching status=1 AND attribute_family=1 AND price in [50,200] # are written. Filters are AND-combined (see below). ID,Name,SKU,"Attribute Family",Price,Quantity,Status,Category,Type 1,"Coastal Breeze Men's Blue Zipper Hoodie",COASTALBREEZEMENSHOODIE,Default,$100.00,10000,Active,Fashion,simple - id: admin-products-export-bad-format title: Unsupported format (422) description: Only format=csv is supported. Any other value returns 422. query: | curl -i -X GET "https://your-domain.com/api/admin/catalog/products/export?format=xlsx" \ -H "Authorization: Bearer " \ -H "Accept: text/csv" response: | HTTP/1.1 422 Unprocessable Content Content-Type: application/json { "message": "Only the csv export format is supported." } --- # Export Products Downloads the products datagrid as a **CSV file** — the same data the admin **Catalog → Products** "Export" button produces. The response is a binary `text/csv` attachment, not JSON. Unlike the [listing](/api/rest-api/admin/catalog/products), the export is **not paginated** — it streams **every row that matches the current filters**, so you can export a whole filtered catalog in one call. ::: tip REST only There is no GraphQL counterpart — binary file streams aren't expressible over GraphQL. Use this REST endpoint for the export. ::: ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/catalog/products/export` | GET | ## Request headers | Header | Value | |--------|-------| | `Authorization` | `Bearer ` | | `Accept` | `text/csv` — **required**. The endpoint only produces `text/csv`; sending `Accept: application/json` returns `406 Not Acceptable`. | ## Query parameters | Parameter | Type | Description | |-----------|------|-------------| | `format` | string | Export format — **only `csv` is supported** (the default). Any other value returns `422`. | The export also accepts the **same filters as the [listing](/api/rest-api/admin/catalog/products)**, AND-combined (more filters = narrower result): | Filter | Type | Description | |--------|------|-------------| | `channel` | string | Channel code for value resolution. | | `name` | string | Partial product-name match. | | `sku` | string | Partial SKU match. | | `attribute_family` | integer | Attribute-family ID. | | `price_from` / `price_to` | number | Price band (inclusive). `price=50,200` is shorthand for both. | | `product_id` | string | A single ID or a comma-separated list (e.g. `1,22,2705`). | | `status` | integer | `0` (disabled) or `1` (active). | | `type` | string | `simple`, `virtual`, `downloadable`, `grouped`, `bundle`, `configurable`, `booking`. | ## Columns The CSV carries the nine datagrid columns, in order: | Header | Value | |--------|-------| | `ID` | Product ID. | | `Name` | Product name for the resolved locale (empty for draft products with no name yet). | | `SKU` | Product SKU. | | `Attribute Family` | The product's attribute-family name. | | `Price` | The base price, formatted (e.g. `$100.00`). Empty for composite types (`configurable` / `bundle` / `grouped` / `booking`) that carry no own price. | | `Quantity` | Total stock across inventory sources. | | `Status` | `Active` or `Disabled`. | | `Category` | The primary category name (empty when uncategorised). | | `Type` | The product type. | ## Permission `catalog.products` --- # Product Images — Delete URL: /api/rest-api/admin/catalog/products/images-delete --- outline: false apiType: rest examples: - id: admin-catalog-product-image-delete title: Delete a Product Image description: Deletes the DB row and removes the file from public storage. query: | curl -X DELETE "https://your-domain.com/api/admin/catalog/products/12/images/47" \ -H "Authorization: Bearer " variables: | {} response: | { "success": true, "message": "Product image deleted successfully." } commonErrors: - error: Not Found (404) cause: Image or its parent product not found solution: Verify both `{productId}` and `{id}` exist and `{id}` belongs to `{productId}` --- # Product Images — Delete Removes a single product image — both the DB row and the file on storage. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/catalog/products/{productId}/images/{id}` | DELETE | ## Path parameters | Field | Type | Required | Notes | |-------|------|----------|-------| | `productId` | integer | yes | Parent product ID. | | `id` | integer | yes | Image ID — must belong to `{productId}`. | ## Response `200 OK` | Field | Type | Notes | |-------|------|-------| | `success` | bool | | | `message` | string | | ## Errors | HTTP | Cause | |------|-------| | `401 Unauthorized` | Missing or invalid admin Bearer token. | | `403 Forbidden` | Admin role lacks `catalog.products.edit`. | | `404 Not Found` | Image or its parent product not found. | --- # Product Images — Reorder URL: /api/rest-api/admin/catalog/products/images-reorder --- outline: false apiType: rest examples: - id: admin-catalog-product-image-reorder title: Reorder Product Images description: Updates the position of one or more existing images for a product. Each image ID must belong to the product or the request is rejected. query: | curl -X PUT "https://your-domain.com/api/admin/catalog/products/12/images/reorder" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "order": [ { "id": 47, "position": 2 }, { "id": 48, "position": 1 } ] }' variables: | { "order": [ { "id": 47, "position": 2 }, { "id": 48, "position": 1 } ] } response: | { "success": true, "message": "Product images reordered successfully.", "images": [ { "id": 48, "productId": 12, "path": "product/12/xyz.webp", "position": 1, "url": "/storage/product/12/xyz.webp" }, { "id": 47, "productId": 12, "path": "product/12/abc.webp", "position": 2, "url": "/storage/product/12/abc.webp" } ] } commonErrors: - error: Validation (422) cause: An image ID does not belong to the product, or the order payload is malformed solution: Send only image IDs that belong to `{productId}` --- # Product Images — Reorder Reorders the existing images of a product. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/catalog/products/{productId}/images/reorder` | PUT | ## Request body | Field | Type | Required | Notes | |-------|------|----------|-------| | `order` | array of `{ id, position }` | yes | Each `id` must belong to `{productId}`. | ## Response `200 OK` — returns the full updated list of images, ordered by position. | Field | Type | Notes | |-------|------|-------| | `success` | bool | | | `message` | string | | | `images` | array | One row per image — `id`, `productId`, `path`, `position`, `url`. | ## Errors | HTTP | Cause | |------|-------| | `401 Unauthorized` | Missing or invalid admin Bearer token. | | `403 Forbidden` | Admin role lacks `catalog.products.edit`. | | `422 Unprocessable Entity` | Image ID doesn't belong to the product, or `order` is malformed. | --- # Product Images — Upload URL: /api/rest-api/admin/catalog/products/images-upload --- outline: false apiType: rest examples: - id: admin-catalog-product-image-upload title: Upload a Product Image description: Multipart upload of a single product image. Allowed mime types — bmp, jpeg, jpg, png, webp. query: | curl -X POST "https://your-domain.com/api/admin/catalog/products/12/images" \ -H "Authorization: Bearer " \ -F "image=@/path/to/photo.webp" \ -F "position=1" variables: | multipart/form-data: image: position: 1 response: | { "id": 47, "productId": 12, "path": "product/12/abc123.webp", "position": 1, "url": "/storage/product/12/abc123.webp" } commonErrors: - error: Validation (422) cause: Missing file, invalid mime type, or file too large solution: Send a valid image under the configured size limit - error: Not Found (404) cause: Parent product not found solution: Verify `{productId}` exists --- # Product Images — Upload Uploads a new image for the given product. ::: warning REST only — GraphQL upload not supported Binary file parts are **not transportable over JSON GraphQL.** The GraphQL `createAdminCatalogProductImage` mutation exists as a placeholder only and will not accept a file. Use this REST endpoint for image upload. ::: ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/catalog/products/{productId}/images` | POST | ## Content type `multipart/form-data` — required. ## Form fields | Field | Type | Required | Notes | |-------|------|----------|-------| | `image` | file | yes | One of bmp, jpeg, jpg, png, webp. | | `position` | integer | no | Sort position; appended to the end if omitted. | ## Response `201 Created` | Field | Type | Notes | |-------|------|-------| | `id` | integer | New `product_images.id`. | | `productId` | integer | Parent product ID (echoed). | | `path` | string | Storage-relative file path. | | `position` | integer | Sort position. | | `url` | string | Public URL. | ## Errors | HTTP | Cause | |------|-------| | `401 Unauthorized` | Missing or invalid admin Bearer token. | | `403 Forbidden` | Admin role lacks `catalog.products.edit`. | | `404 Not Found` | Product not found. | | `422 Unprocessable Entity` | Missing file, invalid mime, or oversized payload. | --- # Product Inventories — List URL: /api/rest-api/admin/catalog/products/inventories-list --- outline: false apiType: rest examples: - id: admin-catalog-product-inventories-list title: List Per-Source Inventory Rows description: Returns one row per inventory_source that has a product_inventories entry for the product. The envelope `meta.totalQty` is the sum across all sources. query: | curl "https://your-domain.com/api/admin/catalog/products/12/inventories" \ -H "Authorization: Bearer " variables: | {} response: | { "data": [ { "id": 14, "sourceId": 1, "sourceCode": "default", "sourceName": "Default", "qty": 25 } ], "meta": { "currentPage": 1, "perPage": 1, "lastPage": 1, "total": 1, "from": 1, "to": 1, "totalQty": 25 } } --- # Product Inventories — List Returns the per-source inventory breakdown for a product. The standard admin `{ data, meta }` envelope is used, with one extra `meta.totalQty` field — the sum across all sources. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/catalog/products/{productId}/inventories` | GET | ## Response `200 OK` | Field | Type | Notes | |-------|------|-------| | `data[].id` | integer | `product_inventories` row id. | | `data[].sourceId` | integer | `inventory_source_id`. | | `data[].sourceCode` | string | e.g. `default`. | | `data[].sourceName` | string | Display name. | | `data[].qty` | integer | Quantity on hand at this source. | | `meta.totalQty` | integer | Sum across all sources. | ## Errors | HTTP | Cause | |------|-------| | `401 Unauthorized` | Missing or invalid admin Bearer token. | | `404 Not Found` | Product not found. | --- # Product Inventories — Bulk Update URL: /api/rest-api/admin/catalog/products/inventories-update --- outline: false apiType: rest examples: - id: admin-catalog-product-inventories-update title: Bulk-Update Per-Source Inventory description: Mirrors Bagisto monolith `ProductController::updateInventories`. Sources passed with qty=0 are kept but zeroed-out; sources NOT in the payload are left untouched. query: | curl -X PUT "https://your-domain.com/api/admin/catalog/products/12/inventories" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "inventories": { "1": 25, "2": 0 } }' variables: | { "inventories": { "1": 25, "2": 0 } } response: | { "data": [ { "id": 14, "sourceId": 1, "sourceCode": "default", "sourceName": "Default", "qty": 25 } ], "meta": { "currentPage": 1, "perPage": 1, "lastPage": 1, "total": 1, "from": 1, "to": 1, "totalQty": 25 } } commonErrors: - error: Validation (422) cause: Missing `inventories`, unknown inventory_source_id, or negative quantity solution: Send a map of existing source IDs to non-negative integers --- # Product Inventories — Bulk Update Bulk-upsert inventory quantities for a product across one or more sources. Fires `catalog.product.update.before` / `catalog.product.update.after`. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/catalog/products/{productId}/inventories` | PUT | ## Request body | Field | Type | Required | Notes | |-------|------|----------|-------| | `inventories` | object | yes | Map of `inventory_source_id` → quantity (integer ≥ 0). | ### Upsert semantics - A source passed with `qty > 0` is upserted. - A source passed with `qty = 0` is kept but zeroed-out (the row is NOT deleted unless the underlying repository decides to). - Sources NOT included in the request are left untouched. ## Response `200 OK` — same shape as the [list endpoint](/api/rest-api/admin/catalog/products/inventories-list), with totals refreshed. ## Errors | HTTP | Cause | |------|-------| | `401 Unauthorized` | Missing or invalid admin Bearer token. | | `403 Forbidden` | Admin role lacks `catalog.products.edit`. | | `404 Not Found` | Product not found. | | `422 Unprocessable Entity` | Missing `inventories`, unknown source id, or negative qty. | --- # Add-Product Search (Create-Order) URL: /api/rest-api/admin/catalog/products/list --- outline: false apiType: rest examples: - id: admin-products-list title: Add-Product Search (Create-Order) description: The slim product search behind the admin Create-Order "Add Product" modal. NOT the product listing — for the full product list with all columns and filters use the List Products datagrid. Returns ALL statuses by default (admin must see disabled / draft products too). Booking products ARE listed here so admin can find them; they're blocked at admin cart add-to-cart time. query: | curl -X GET "https://your-domain.com/api/admin/products?sku=SP-001&type=simple&per_page=30&page=1" \ -H "Authorization: Bearer " \ -H "Accept: application/json" variables: | sku=SP-001&type=simple&per_page=30&page=1 response: | { "data": [ { "id": 2512, "sku": "SP-001", "type": "simple", "name": "Arctic Cozy Knit Unisex Beanie", "status": 1, "price": 14, "formattedPrice": "$14.00", "baseImageUrl": "http://localhost:8000/cache/medium/product/2512/Muc0qeWks34MTZaxf38s6DBmfqMqrCxku81Uo8EB.webp", "isSaleable": true } ], "meta": { "currentPage": 1, "perPage": 30, "lastPage": 1, "total": 1, "from": 1, "to": 1 } } commonErrors: - error: Unauthorized (401) cause: Missing or invalid admin Bearer token solution: Send a valid admin Bearer token (Integration token) in the Authorization header. See the Authentication page. --- # Add-Product Search (Create-Order) The slim product **search** that powers the admin **Create Order** screen's "Add Product" modal. It returns a compact row per product so an operator can quickly find and pick a product to add to a draft order. ::: warning This is not the product listing For the full admin product listing — every column plus all the Channel / Name / SKU / Attribute Family / Price / ID / Status / Type filters — use [List Products](/api/rest-api/admin/catalog/products) (`GET /api/admin/catalog/products`). This page documents only the Create-Order search tool. ::: Results come back in the `{ data, meta }` envelope used by every admin collection. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/products` | GET | ## How it differs from `/api/shop/products` | | Shop | Admin | |---|---|---| | Default status filter | Only `status = 1` and `visible_individually = 1` | **No filter — all statuses returned** | | Booking products | Hidden by shop visibility rules | **Listed** (blocked only when added to admin draft cart) | | Row payload | Full storefront fields (variants, prices, special prices, etc.) | Slim picker shape (9 fields) | | Pagination response | Header-based (`X-Total-Count`, `X-Page`, ...) | Body envelope (`{ data, meta }`) | | Authentication | Storefront key + optional customer Sanctum | Admin Sanctum token | ## Query Parameters | Parameter | Type | Description | |-----------|------|-------------| | `page` | integer | Page number (default `1`) | | `per_page` | integer | Items per page (default `30`, cap `50`) | | `query` | string | Free-text — matches SKU OR product name (partial) | | `sku` | string | Exact SKU | | `type` | string | `simple`, `configurable`, `bundle`, `downloadable`, `grouped`, `virtual`, `booking` | | `status` | integer | `0` (disabled) or `1` (enabled) — omit to get both | | `categoryId` | integer | Filter by category ID | | `channel` | string | Channel code for value resolution | | `locale` | string | Locale code for value resolution | | `sort` | string | `id`, `sku`, `created_at`, `updated_at` | | `order` | string | `asc` or `desc` (default `desc`) | ## Row Shape | Field | Type | Notes | |-------|------|-------| | `id` | integer | Product ID | | `sku` | string | SKU | | `type` | string | Product type | | `name` | string\|null | Resolved via locale/channel if provided | | `status` | integer\|null | `1` enabled / `0` disabled | | `price` | number\|null | Minimal price for the resolved customer group | | `formattedPrice` | string\|null | Locale-formatted price | | `baseImageUrl` | string\|null | Medium-cache image URL, falls back to original | | `isSaleable` | boolean | Type-instance saleability check | ## Booking Products Booking products **are returned** by this endpoint so admin can find and review them. They are **blocked** at admin draft-cart add-to-cart time (`POST /api/admin/carts/{id}/items`) with HTTP `400` and the message "Booking products cannot be added to an admin draft order." This matches the Bagisto monolith — the admin Create-Order UI does not ship a booking partial either. --- # Catalog Products — Mass Delete URL: /api/rest-api/admin/catalog/products/mass-delete --- outline: false apiType: rest examples: - id: admin-catalog-product-mass-delete title: Mass Delete Catalog Products description: Deletes a batch of catalog products in one call. Non-existent IDs are silently skipped. Mirrors Bagisto monolith `ProductController::massDestroy`. query: | curl -X POST "https://your-domain.com/api/admin/catalog/products/mass-delete" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "indices": [12, 18] }' variables: | { "indices": [12, 18] } response: | { "deleted": [12, 18], "message": "Products deleted successfully." } commonErrors: - error: Bad Request (400) cause: Empty or malformed `indices` array solution: Send a non-empty array of integers - error: Forbidden (403) cause: Admin role lacks `catalog.products.delete` solution: Grant the permission to the admin role - error: Server Error (500) cause: Underlying delete threw an exception (mirrors monolith behaviour) solution: Investigate the failing ID — usually a FK constraint violation --- # Catalog Products — Mass Delete Deletes a batch of catalog products in a single call. Mirrors **Catalog → Products → Mass Delete** in the Bagisto admin panel. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/catalog/products/mass-delete` | POST | ## Request body | Field | Type | Required | Notes | |-------|------|----------|-------| | `indices` | int[] | yes | Non-empty array of product IDs. Non-existent IDs are silently skipped. | ## Response `200 OK` | Field | Type | Notes | |-------|------|-------| | `deleted` | int[] | IDs that were processed (echoes the request — non-existent IDs are still listed) | | `message` | string | Translated confirmation | ## Errors | HTTP | Cause | |------|-------| | `400 Bad Request` | Empty / malformed `indices` | | `401 Unauthorized` | Missing or invalid admin Bearer token | | `403 Forbidden` | Admin role lacks `catalog.products.delete` | | `500 Internal Server Error` | Underlying delete threw — matches monolith best-effort behaviour | ## Notes - Mirrors the monolith **best-effort** semantics — the call does not short-circuit on a missing or invalid ID. - For configurable products, variants cascade automatically through the core repository. --- # Catalog Products — Mass Update Status URL: /api/rest-api/admin/catalog/products/mass-update-status --- outline: false apiType: rest examples: - id: admin-catalog-product-mass-update-status title: Mass Update Catalog Product Status description: Bulk-flips the `status` flag of a batch of products to either 0 (disabled) or 1 (enabled). Mirrors Bagisto monolith `ProductController::massUpdate` — best-effort, fires `catalog.product.update.before` / `.after` for each ID. query: | curl -X POST "https://your-domain.com/api/admin/catalog/products/mass-update-status" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "indices": [12, 18], "value": 1 }' variables: | { "indices": [12, 18], "value": 1 } response: | { "updated": [12, 18], "message": "Products status updated successfully." } commonErrors: - error: Bad Request (400) cause: Empty `indices`, or `value` not 0/1 solution: Send a non-empty integer array and an integer 0 or 1 - error: Forbidden (403) cause: Admin role lacks `catalog.products.edit` solution: Grant the permission to the admin role --- # Catalog Products — Mass Update Status Bulk-flips status across a batch of catalog products. Each ID fires the same core event hooks the single-product update does (`catalog.product.update.before` / `.after`), so search reindex, cache flush etc. still trigger. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/catalog/products/mass-update-status` | POST | ## Request body | Field | Type | Required | Notes | |-------|------|----------|-------| | `indices` | int[] | yes | Non-empty array of product IDs. | | `value` | integer | yes | `0` (disabled) or `1` (enabled). | ## Response `200 OK` | Field | Type | Notes | |-------|------|-------| | `updated` | int[] | IDs the call attempted to update (best-effort). | | `message` | string | Translated confirmation. | ## Errors | HTTP | Cause | |------|-------| | `400 Bad Request` | Empty / malformed indices or `value` not in `[0,1]`. | | `401 Unauthorized` | Missing or invalid admin Bearer token. | | `403 Forbidden` | Admin role lacks `catalog.products.edit`. | --- # Catalog Product — Detail URL: /api/rest-api/admin/catalog/products/products-detail --- outline: false apiType: rest examples: - id: admin-catalog-product-detail title: Catalog Product Detail (type-aware, all fields inlined) description: Single catalog product record with all detail-level fields populated. Type-specific blocks (superAttributes/variants for configurable, bundleOptions for bundle, linkedProducts for grouped, downloadableLinks/downloadableSamples for downloadable) are populated only for the matching product type; all others are null. No IRI strings — every nested array is an inline JSON object. query: | curl -X GET "https://your-domain.com/api/admin/catalog/products/42" \ -H "Authorization: Bearer " \ -H "Accept: application/json" variables: | id=42 response: | { "id": 42, "sku": "SP-001", "name": "Classic Watch", "type": "simple", "status": 1, "price": "99.9900", "formattedPrice": "$99.99", "quantity": 42, "baseImageUrl": "http://localhost:8000/storage/product/42/image.webp", "imagesCount": 3, "categoryId": 5, "categoryName": "Accessories", "channel": "default", "locale": "en", "attributeFamilyId": 1, "attributeFamilyName": "Default", "urlKey": "classic-watch", "visibleIndividually": true, "shortDescription": "A premium timepiece.", "description": "Full HTML description.", "metaTitle": null, "metaDescription": null, "metaKeywords": null, "weight": 0.5, "taxCategoryId": null, "manageStock": true, "inStock": true, "featured": false, "new": true, "createdAt": "2026-01-12T08:15:00+00:00", "updatedAt": "2026-04-30T14:20:09+00:00", "translations": [ { "locale": "en", "name": "Classic Watch", "description": "Full HTML description.", "shortDescription": "A premium timepiece.", "urlKey": "classic-watch", "metaTitle": null, "metaDescription": null, "metaKeywords": null } ], "images": [ { "id": 1, "path": "product/42/img1.webp", "url": "http://localhost/storage/product/42/img1.webp", "sortOrder": 0 } ], "categories": [ { "id": 5, "name": "Accessories", "slug": "accessories" } ], "inventories": [ { "sourceId": 1, "sourceCode": "default", "qty": 42 } ], "customerGroupPrices": [], "superAttributes": null, "variants": null, "bundleOptions": null, "linkedProducts": null, "downloadableLinks": null, "downloadableSamples": null, "channels": [ { "id": 1, "code": "default", "name": "Default Channel", "assigned": true }, { "id": 2, "code": "mobile", "name": "Mobile Channel", "assigned": false } ], "attributes": [ { "id": 1, "code": "sku", "adminName": "SKU", "type": "text", "isRequired": true, "valuePerChannel": false, "valuePerLocale": false, "groupCode": "general", "groupName": "General", "value": "SP-001", "options": null }, { "id": 23, "code": "color", "adminName": "Color", "type": "select", "isRequired": false, "valuePerChannel": false, "valuePerLocale": false, "groupCode": "general", "groupName": "General", "value": null, "options": [ { "id": 1, "adminName": "Red", "swatchValue": "#ff0000", "sortOrder": 1 } ] }, { "id": 25, "code": "meta_title", "adminName": "Meta Title", "type": "textarea", "isRequired": false, "valuePerChannel": true, "valuePerLocale": true, "groupCode": "meta_description", "groupName": "Meta Description", "value": null, "options": null } ] } commonErrors: - error: Not Found (404) cause: The product ID does not exist in the database solution: 'Verify the ID with the listing endpoint `GET /api/admin/catalog/products`' - error: Unauthorized (401) cause: Missing, invalid, expired, or revoked admin Bearer token solution: Send a valid admin Bearer token (Integration token) in the Authorization header. See the Authentication page. --- # Catalog Product — Detail Returns a single catalog product by ID with **all detail-level fields populated**, including translations, images, categories, inventories, customer group prices, and type-specific blocks. This is the read endpoint for the admin **Catalog → Products** edit form. ## Endpoint | Endpoint | Method | Authentication | |----------|--------|----------------| | `/api/admin/catalog/products/{id}` | GET | Admin Bearer token | `{id}` must be a positive integer. The route carries a `requirements: ['id' => '\d+']` constraint — non-numeric path segments are rejected with `404` before reaching the provider. This prevents the `{id}` segment from accidentally matching sibling routes under `/catalog/products/`. ## Authentication Every request requires: ``` Authorization: Bearer ``` Obtain the Bearer token via [Authentication](/api/rest-api/admin/authentication). ## Path Parameter | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `id` | integer | Yes | The numeric product ID | ## Response Shape The response is a **single JSON object** (not wrapped in a `{ data }` envelope) with the following top-level fields: ### Core fields (always present) | Field | Type | Description | |-------|------|-------------| | `id` | integer | Product ID | | `sku` | string | Product SKU | | `name` | string | Localised product name | | `type` | string | Product type (`simple`, `configurable`, `bundle`, `grouped`, `downloadable`, `virtual`, `booking`) | | `status` | integer | `1` = enabled, `0` = disabled | | `price` | string | Raw decimal price string (e.g. `"99.9900"`) | | `formattedPrice` | string | Currency-formatted price (e.g. `"$99.99"`) | | `quantity` | integer | Total quantity across all inventory sources | | `baseImageUrl` | string\|null | URL of the base/primary image | | `imagesCount` | integer | Total number of product images | | `categoryId` | integer\|null | Primary category ID | | `categoryName` | string\|null | Primary category display name | | `channel` | string | Channel code used for value resolution | | `locale` | string | Locale code used for value resolution | | `attributeFamilyId` | integer | Attribute family ID | | `attributeFamilyName` | string | Attribute family display name | | `urlKey` | string | URL slug (e.g. `classic-watch`) | | `visibleIndividually` | boolean | Whether the product appears in listings | | `shortDescription` | string\|null | Short description (may contain HTML) | | `description` | string\|null | Full description (may contain HTML) | | `metaTitle` | string\|null | SEO meta title | | `metaDescription` | string\|null | SEO meta description | | `metaKeywords` | string\|null | SEO meta keywords | | `weight` | float\|null | Product weight | | `taxCategoryId` | integer\|null | Tax category ID | | `manageStock` | boolean\|null | Whether inventory is managed | | `inStock` | boolean | Whether the product is currently in stock | | `featured` | boolean | Whether the product is featured | | `new` | boolean | Whether the product is marked as new | | `createdAt` | string | ISO 8601 creation timestamp | | `updatedAt` | string | ISO 8601 last-updated timestamp | ### `translations[]` array Each element is one per-locale translation row: | Field | Type | Description | |-------|------|-------------| | `locale` | string | Locale code (e.g. `en`, `fr`) | | `name` | string\|null | Translated product name | | `description` | string\|null | Translated full description | | `shortDescription` | string\|null | Translated short description | | `urlKey` | string\|null | Translated URL slug | | `metaTitle` | string\|null | Translated SEO meta title | | `metaDescription` | string\|null | Translated SEO meta description | | `metaKeywords` | string\|null | Translated SEO meta keywords | ### `images[]` array Each element is one product image: | Field | Type | Description | |-------|------|-------------| | `id` | integer | Image ID | | `path` | string | Storage path relative to the disk root | | `url` | string | Full public URL | | `sortOrder` | integer | Display order position | ### `categories[]` array Each element is a category the product belongs to: | Field | Type | Description | |-------|------|-------------| | `id` | integer | Category ID | | `name` | string | Category display name | | `slug` | string | Category URL slug | ### `inventories[]` array Each element is one inventory source row: | Field | Type | Description | |-------|------|-------------| | `sourceId` | integer | Inventory source ID | | `sourceCode` | string | Inventory source code (e.g. `default`) | | `qty` | integer | Quantity at this source | ### `customerGroupPrices[]` array Each element is a customer-group price override. Empty array (`[]`) when none are configured. ### Type-specific blocks These four fields are **`null` unless the product type matches**: | Field | Present for type | Description | |-------|-----------------|-------------| | `superAttributes` | `configurable` | Array of configurable attributes (id, code, label, options) | | `variants` | `configurable` | Array of variant child products with their attribute values | | `bundleOptions` | `bundle` | Array of bundle option groups with their selectable products | | `linkedProducts` | `grouped` | Array of linked associated products | | `downloadableLinks` | `downloadable` | Array of download link rows (title, type, url/file, price, downloads) | | `downloadableSamples` | `downloadable` | Array of sample download rows | ### `channels[]` array **Every** channel in the store, each flagged with whether this product is assigned to it — mirrors the Channels checkbox box on the edit screen (all options shown, the product's ones ticked). The singular `channel` field above is the channel code the data was resolved for. | Field | Type | Description | |-------|------|-------------| | `id` | integer | Channel ID | | `code` | string | Channel code | | `name` | string | Channel display name | | `assigned` | boolean | `true` if this product is assigned to the channel | ### `attributes[]` array The product's **attribute-family field set** — the same fields the admin edit screen renders, in the same order, driven by the product's attribute family. This includes family-specific fields (e.g. `color`, `size`, `brand`, `product_number`) that aren't top-level columns. Fields with no value are still present, with `value: null`. | Field | Type | Description | |-------|------|-------------| | `id` | integer | Attribute ID | | `code` | string | Attribute code (e.g. `sku`, `color`, `meta_title`) | | `adminName` | string | Field label as shown in the admin | | `type` | string | Input type (`text`, `textarea`, `price`, `boolean`, `select`, `multiselect`, `checkbox`, `date`, `datetime`, `image`, `file`) | | `isRequired` | boolean | Whether the field is required | | `valuePerChannel` | boolean | Whether the value can differ per channel | | `valuePerLocale` | boolean | Whether the value can differ per locale | | `groupCode` | string | Code of the field group it belongs to | | `groupName` | string | Display name of the field group | | `value` | mixed\|null | The product's resolved value for the requested channel/locale (`null` when unset). For `select` it's the chosen option ID; for `multiselect`/`checkbox` a comma-separated list of option IDs | | `options` | array\|null | For `select`/`multiselect`/`checkbox`: the selectable options (`id`, `adminName`, `swatchValue`, `sortOrder`). `null` for other types | ::: tip Plain arrays — no follow-up calls needed All nested fields (`translations`, `images`, `categories`, `inventories`, `customerGroupPrices`, `channels`, `attributes`, and all type-specific blocks) are serialized as **plain inline JSON arrays** — there are no IRI strings or sub-resource links. The full product structure is returned in a single response. ::: ::: info Reconstructing the edit screen `attributes` + `channels` together give you everything the admin edit form shows: `channels` renders the channel checkboxes (with the assigned ones ticked) and `attributes` renders every General / Description / Meta / Settings / Price field for the product's family. The top-level convenience fields (`sku`, `status`, `urlKey`, …) are also present inside `attributes` — they're the same values surfaced twice. ::: ::: info Listing vs detail The `GET /api/admin/catalog/products` listing endpoint returns a slim `{ data, meta }` envelope with ~18 fields per row (suitable for datagrid rendering). This detail endpoint returns all fields, including the heavy blocks that are intentionally omitted from the listing for performance. ::: ## Example Request ```bash curl -X GET "https://your-domain.com/api/admin/catalog/products/42" \ -H "Authorization: Bearer " \ -H "Accept: application/json" ``` ## Example Response (simple product) ```json { "id": 42, "sku": "SP-001", "name": "Classic Watch", "type": "simple", "status": 1, "price": "99.9900", "formattedPrice": "$99.99", "quantity": 42, "baseImageUrl": "http://localhost:8000/storage/product/42/image.webp", "imagesCount": 3, "categoryId": 5, "categoryName": "Accessories", "channel": "default", "locale": "en", "attributeFamilyId": 1, "attributeFamilyName": "Default", "urlKey": "classic-watch", "visibleIndividually": true, "shortDescription": "A premium timepiece.", "description": "Full HTML description.", "metaTitle": null, "metaDescription": null, "metaKeywords": null, "weight": 0.5, "taxCategoryId": null, "manageStock": true, "inStock": true, "featured": false, "new": true, "createdAt": "2026-01-12T08:15:00+00:00", "updatedAt": "2026-04-30T14:20:09+00:00", "translations": [ { "locale": "en", "name": "Classic Watch", "description": "Full HTML description.", "shortDescription": "A premium timepiece.", "urlKey": "classic-watch", "metaTitle": null, "metaDescription": null, "metaKeywords": null } ], "images": [ { "id": 1, "path": "product/42/img1.webp", "url": "http://localhost/storage/product/42/img1.webp", "sortOrder": 0 } ], "categories": [ { "id": 5, "name": "Accessories", "slug": "accessories" } ], "inventories": [ { "sourceId": 1, "sourceCode": "default", "qty": 42 } ], "customerGroupPrices": [], "superAttributes": null, "variants": null, "bundleOptions": null, "linkedProducts": null, "downloadableLinks": null, "downloadableSamples": null, "channels": [ { "id": 1, "code": "default", "name": "Default Channel", "assigned": true }, { "id": 2, "code": "mobile", "name": "Mobile Channel", "assigned": false } ], "attributes": [ { "id": 1, "code": "sku", "adminName": "SKU", "type": "text", "isRequired": true, "valuePerChannel": false, "valuePerLocale": false, "groupCode": "general", "groupName": "General", "value": "SP-001", "options": null }, { "id": 23, "code": "color", "adminName": "Color", "type": "select", "isRequired": false, "valuePerChannel": false, "valuePerLocale": false, "groupCode": "general", "groupName": "General", "value": null, "options": [ { "id": 1, "adminName": "Red", "swatchValue": "#ff0000", "sortOrder": 1 } ] }, { "id": 25, "code": "meta_title", "adminName": "Meta Title", "type": "textarea", "isRequired": false, "valuePerChannel": true, "valuePerLocale": true, "groupCode": "meta_description", "groupName": "Meta Description", "value": null, "options": null } ] } ``` ## Errors | HTTP Status | Cause | |-------------|-------| | `401 Unauthorized` | Missing, expired, or revoked admin Bearer token | | `401 Unauthorized` | Missing or invalid admin Bearer token | | `404 Not Found` | The specified `{id}` does not exist in the database | ## Notes - **Type-aware payload.** The six type-specific blocks (`superAttributes`, `variants`, `bundleOptions`, `linkedProducts`, `downloadableLinks`, `downloadableSamples`) are always present in the response, but are `null` for non-matching types. A simple, virtual, or booking product always has all six as `null`. Switch on `type` to know which block to read. - **All nested objects are plain inline JSON.** There are no IRI strings or sub-resource links in the detail response — the full structure (including type-specific blocks) is embedded in a single HTTP call. - **`null` fields are always included.** Null-valued fields appear explicitly in the JSON rather than being silently dropped. - **Route disambiguation via `\d+` requirement.** The `{id}` path segment is constrained to digits only. Non-numeric segments are rejected with `404` before reaching the provider — this prevents the segment from accidentally matching sibling routes such as `/catalog/products/list`. - **Listing vs detail are the same resource.** The `GET /api/admin/catalog/products` listing returns slim datagrid rows; this detail endpoint returns the full payload including all type-specific blocks. - **Booking products are accessible via this endpoint.** Even though booking products cannot be added to an admin draft cart (blocked at cart add-item time with HTTP 400), their detail record is fully readable here. --- # Catalog Product — Update URL: /api/rest-api/admin/catalog/products/update --- outline: false apiType: rest examples: - id: admin-catalog-product-update-attributes title: Attributes (any type) description: Partial update — send only the attribute codes you change. Every attribute on the product's family is editable by its code (name, url_key, price, status, color, brand, ...). Omitted fields keep their current value. categories/channels replace the current set when sent. query: | curl -X PUT "https://your-domain.com/api/admin/catalog/products/42" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "name": "Arctic Beanie", "url_key": "arctic-beanie", "short_description": "Warm.", "description": "Full text.", "meta_title": "Arctic Beanie", "price": "24.99", "weight": "0.3", "status": 1, "new": 1, "color": 1, "categories": [43, 44] }' variables: | { "name": "Arctic Beanie", "url_key": "arctic-beanie", "price": "24.99", "status": 1, "color": 1, "categories": [43, 44] } response: | { "id": 42, "sku": "sp-001", "type": "simple", "name": "Arctic Beanie", "urlKey": "arctic-beanie", "shortDescription": "Warm.", "description": "Full text.", "metaTitle": "Arctic Beanie", "price": "24.9900", "weight": 0.3, "status": 1, "new": true, "categories": [ { "id": 43, "name": "Hats", "slug": "hats" }, { "id": 44, "name": "Winter", "slug": "winter" } ], "_warnings": [] } - id: admin-catalog-product-update-downloadable title: Downloadable — links & samples description: Replace the download links and samples for a downloadable product. Send the full set — downloadable_links/downloadable_samples replace the current structure. query: | curl -X PUT "https://your-domain.com/api/admin/catalog/products/45" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "downloadable_links": { "link_1": { "en": { "title": "Chapter 1 PDF" }, "price": "5.00", "downloads": "3", "sort_order": "1", "type": "url", "url": "https://example.com/ch1.pdf", "sample_type": "url", "sample_url": "https://example.com/sample.pdf" } }, "downloadable_samples": { "sample_1": { "title": "Preview", "sort_order": "1", "type": "url", "url": "https://example.com/preview.pdf" } } }' variables: | { "downloadable_links": { "link_1": { "en": { "title": "Chapter 1 PDF" }, "price": "5.00", "type": "url", "url": "https://example.com/ch1.pdf" } } } response: | { "id": 45, "sku": "dl-001", "type": "downloadable", "name": "E-Book", "downloadableLinks": [ { "id": 7, "title": "Chapter 1 PDF", "type": "url", "url": "https://example.com/ch1.pdf", "price": "5.0000", "downloads": 3, "sortOrder": 1 } ], "downloadableSamples": [ { "id": 4, "title": "Preview", "type": "url", "url": "https://example.com/preview.pdf", "sortOrder": 1 } ], "_warnings": [] } - id: admin-catalog-product-update-grouped title: Grouped — linked products description: Replace the associated products of a grouped product. Each link references an existing product id with a default quantity and sort order. query: | curl -X PUT "https://your-domain.com/api/admin/catalog/products/46" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "links": { "link_1": { "associated_product_id": 142, "qty": "2", "sort_order": "1" }, "link_2": { "associated_product_id": 143, "qty": "1", "sort_order": "2" } } }' variables: | { "links": { "link_1": { "associated_product_id": 142, "qty": "2", "sort_order": "1" }, "link_2": { "associated_product_id": 143, "qty": "1", "sort_order": "2" } } } response: | { "id": 46, "sku": "gr-001", "type": "grouped", "name": "Starter Kit", "linkedProducts": [ { "id": 142, "sku": "SP-142", "name": "Cable", "qty": 2, "sortOrder": 1 }, { "id": 143, "sku": "SP-143", "name": "Adapter", "qty": 1, "sortOrder": 2 } ], "_warnings": [] } - id: admin-catalog-product-update-bundle title: Bundle — options description: Replace the bundle option groups. Each option has a label, a type (radio/checkbox/select/multiselect), and a set of selectable products with default flags. query: | curl -X PUT "https://your-domain.com/api/admin/catalog/products/47" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "bundle_options": { "option_1": { "en": { "label": "Choose your accessory" }, "type": "radio", "is_required": "1", "sort_order": "1", "products": { "p1": { "product_id": 142, "qty": "1", "is_default": "1", "sort_order": "1" }, "p2": { "product_id": 143, "qty": "1", "is_default": "0", "sort_order": "2" } } } } }' variables: | { "bundle_options": { "option_1": { "en": { "label": "Choose your accessory" }, "type": "radio", "is_required": "1", "sort_order": "1", "products": { "p1": { "product_id": 142, "qty": "1", "is_default": "1", "sort_order": "1" }, "p2": { "product_id": 143, "qty": "1", "is_default": "0", "sort_order": "2" } } } } } response: | { "id": 47, "sku": "bn-001", "type": "bundle", "name": "Accessory Bundle", "bundleOptions": [ { "id": 11, "label": "Choose your accessory", "type": "radio", "isRequired": true, "sortOrder": 1, "products": [ { "id": 21, "productId": 142, "qty": 1, "isDefault": true, "sortOrder": 1 }, { "id": 22, "productId": 143, "qty": 1, "isDefault": false, "sortOrder": 2 } ] } ], "_warnings": [] } - id: admin-catalog-product-update-configurable title: Configurable — variants description: Update per-variant fields. variants is keyed by the variant product id (from the create response or detail variants[].id). Replace-semantics — send every variant you want to keep. query: | curl -X PUT "https://your-domain.com/api/admin/catalog/products/48" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "variants": { "2711": { "sku": "BEANIE-RED-S", "name": "Red / Small", "price": "29.99", "weight": "0.3", "status": "1" } } }' variables: | { "variants": { "2711": { "sku": "BEANIE-RED-S", "name": "Red / Small", "price": "29.99", "weight": "0.3", "status": "1" } } } response: | { "id": 48, "sku": "cf-001", "type": "configurable", "name": "Beanie", "variants": [ { "id": 2711, "sku": "BEANIE-RED-S", "name": "Red / Small", "price": "29.9900", "weight": 0.3, "status": 1 } ], "_warnings": [] } - id: admin-catalog-product-update-booking-default title: Booking — default description: Configure a default booking product — recurring weekly slots with a duration, break time and quantity. query: | curl -X PUT "https://your-domain.com/api/admin/catalog/products/53" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "booking": { "type": "default", "qty": "1", "location": "Studio A", "available_every_week": "1", "booking_type": "many", "duration": "60", "break_time": "10", "slots": [ { "from": "09:00", "to": "17:00" } ] } }' variables: | { "booking": { "type": "default", "qty": "1", "location": "Studio A", "available_every_week": "1", "booking_type": "many", "duration": "60", "break_time": "10", "slots": [ { "from": "09:00", "to": "17:00" } ] } } response: | { "id": 53, "sku": "bk-001", "type": "booking", "name": "Studio Session", "bookingProduct": { "type": "default", "qty": 1, "location": "Studio A", "availableEveryWeek": true, "bookingType": "many", "duration": 60, "breakTime": 10, "slots": [ { "from": "09:00", "to": "17:00" } ] }, "_warnings": [] } - id: admin-catalog-product-update-booking-appointment title: Booking — appointment description: Configure an appointment booking product — per-day slot windows with a fixed appointment duration. query: | curl -X PUT "https://your-domain.com/api/admin/catalog/products/53" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "booking": { "type": "appointment", "qty": "1", "location": "Main Clinic", "available_every_week": "1", "duration": "30", "break_time": "10", "same_slot_all_days": "1", "slots": [ { "from": "09:00", "to": "12:00" }, { "from": "14:00", "to": "17:00" } ] } }' variables: | { "booking": { "type": "appointment", "qty": "1", "location": "Main Clinic", "available_every_week": "1", "duration": "30", "break_time": "10", "same_slot_all_days": "1", "slots": [ { "from": "09:00", "to": "12:00" }, { "from": "14:00", "to": "17:00" } ] } } response: | { "id": 53, "sku": "bk-001", "type": "booking", "name": "Consultation", "bookingProduct": { "type": "appointment", "qty": 1, "location": "Main Clinic", "availableEveryWeek": true, "duration": 30, "breakTime": 10, "sameSlotAllDays": true, "slots": [ { "from": "09:00", "to": "12:00" }, { "from": "14:00", "to": "17:00" } ] }, "_warnings": [] } - id: admin-catalog-product-update-booking-event title: Booking — event description: Configure an event booking product — a fixed date/time window with one or more named tickets. query: | curl -X PUT "https://your-domain.com/api/admin/catalog/products/53" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "booking": { "type": "event", "location": "Grand Arena", "available_from": "2026-08-01 09:00:00", "available_to": "2026-08-01 22:00:00", "tickets": { "ticket_1": { "en": { "name": "VIP", "description": "Front row" }, "price": "120.00", "qty": "50", "special_price": "99.00" } } } }' variables: | { "booking": { "type": "event", "location": "Grand Arena", "available_from": "2026-08-01 09:00:00", "available_to": "2026-08-01 22:00:00", "tickets": { "ticket_1": { "en": { "name": "VIP", "description": "Front row" }, "price": "120.00", "qty": "50", "special_price": "99.00" } } } } response: | { "id": 53, "sku": "bk-001", "type": "booking", "name": "Summer Concert", "bookingProduct": { "type": "event", "location": "Grand Arena", "availableFrom": "2026-08-01 09:00:00", "availableTo": "2026-08-01 22:00:00", "tickets": [ { "id": 9, "name": "VIP", "description": "Front row", "price": "120.0000", "qty": 50, "specialPrice": "99.0000" } ] }, "_warnings": [] } - id: admin-catalog-product-update-booking-rental title: Booking — rental description: Configure a rental booking product — daily and/or hourly pricing over recurring weekly slots. query: | curl -X PUT "https://your-domain.com/api/admin/catalog/products/53" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "booking": { "type": "rental", "qty": "1", "location": "Bike Shop", "available_every_week": "1", "renting_type": "daily_hourly", "daily_price": "40.00", "hourly_price": "10.00", "same_slot_all_days": "1", "slots": [ { "from": "09:00", "to": "18:00" } ] } }' variables: | { "booking": { "type": "rental", "qty": "1", "location": "Bike Shop", "available_every_week": "1", "renting_type": "daily_hourly", "daily_price": "40.00", "hourly_price": "10.00", "same_slot_all_days": "1", "slots": [ { "from": "09:00", "to": "18:00" } ] } } response: | { "id": 53, "sku": "bk-001", "type": "booking", "name": "Bike Rental", "bookingProduct": { "type": "rental", "qty": 1, "location": "Bike Shop", "availableEveryWeek": true, "rentingType": "daily_hourly", "dailyPrice": "40.0000", "hourlyPrice": "10.0000", "sameSlotAllDays": true, "slots": [ { "from": "09:00", "to": "18:00" } ] }, "_warnings": [] } - id: admin-catalog-product-update-booking-table title: Booking — table description: Configure a table booking product — per-guest pricing, guest limit and recurring weekly slots. query: | curl -X PUT "https://your-domain.com/api/admin/catalog/products/53" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "booking": { "type": "table", "qty": "10", "location": "Downtown Bistro", "available_every_week": "1", "price_type": "guest", "guest_limit": "4", "duration": "60", "break_time": "15", "prevent_scheduling_before": "2", "same_slot_all_days": "1", "slots": [ { "from": "12:00", "to": "22:00" } ] } }' variables: | { "booking": { "type": "table", "qty": "10", "location": "Downtown Bistro", "available_every_week": "1", "price_type": "guest", "guest_limit": "4", "duration": "60", "break_time": "15", "prevent_scheduling_before": "2", "same_slot_all_days": "1", "slots": [ { "from": "12:00", "to": "22:00" } ] } } response: | { "id": 53, "sku": "bk-001", "type": "booking", "name": "Dinner Table", "bookingProduct": { "type": "table", "qty": 10, "location": "Downtown Bistro", "availableEveryWeek": true, "priceType": "guest", "guestLimit": 4, "duration": 60, "breakTime": 15, "preventSchedulingBefore": 2, "sameSlotAllDays": true, "slots": [ { "from": "12:00", "to": "22:00" } ] }, "_warnings": [] } - id: admin-catalog-product-update-locale title: Change locale (?locale=) description: Translatable fields write to the locale named in the URL query string. Add ?locale=fr&channel=default to target the French translation — English is untouched. One locale per request; repeat with a different ?locale= to edit another. query: | curl -X PUT "https://your-domain.com/api/admin/catalog/products/42?locale=fr&channel=default" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "name": "Bonnet Arctique", "description": "Texte complet." }' variables: | { "name": "Bonnet Arctique", "description": "Texte complet." } response: | { "id": 42, "sku": "sp-001", "type": "simple", "name": "Bonnet Arctique", "description": "Texte complet.", "locale": "fr", "translations": [ { "locale": "en", "name": "Arctic Beanie", "description": "Full text." }, { "locale": "fr", "name": "Bonnet Arctique", "description": "Texte complet." } ], "_warnings": [] } commonErrors: - error: Validation (422) cause: SKU/url_key duplicate, invalid boolean field, special_price ≥ price, invalid date range solution: Send a unique SKU/url_key and valid field combinations - error: Not Found (404) cause: Product not found solution: Verify the `{id}` exists --- # Catalog Product — Update Updates a catalog product (any of the 7 types). This is a **partial patch** — send only the fields you want to change. Omitted fields keep their current value. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/catalog/products/{id}` | PUT | ## Path parameters | Field | Type | Required | Notes | |-------|------|----------|-------| | `id` | integer | yes | Product ID. | ## Query parameters | Param | Default | Notes | |-------|---------|-------| | `locale` | store default | The locale that translatable fields (`name`, `description`, `short_description`, meta fields, …) are written to. One locale per request — repeat the call with a different `?locale=` to edit another translation. | | `channel` | store default | The channel used for channel-scoped attribute values. | ## Editing attributes by code Every attribute on the product's family is editable **by its code**, sent at the **top level** of the JSON body (snake_case). This includes the common fields and any family-specific ones: | Field code | Notes | |------------|-------| | `name`, `url_key`, `short_description`, `description` | Translatable — written to the requested `?locale=`. | | `meta_title`, `meta_keywords`, `meta_description` | SEO fields (translatable). | | `price`, `weight` | Decimal as string. | | `status`, `new`, `featured`, `visible_individually`, `guest_checkout` | Boolean flags as `0` / `1`. | | `tax_category_id` | Existing tax category id. | | `color`, `size`, `brand`, `product_number`, … | Any family-specific attribute, by its code. | Translatable fields write to the requested locale only — pass `?locale=fr&channel=default` to target the French translation; the other locales are untouched. ## Replace-on-send relations These fields **replace** the product's current set when present, and are **preserved** when omitted: | Field | Notes | |-------|-------| | `categories` | `int[]` — the product's category assignment. | | `channels` | `int[]` — the product's channel assignment. | | `up_sells`, `cross_sells`, `related_products` | `int[]` — product relation lists. | ## Type-structure fields The type-specific structure keys **replace** that structure when sent — send the full set. See the `examples:` dropdown for the verified payload of each: | Type | Field | Notes | |------|-------|-------| | downloadable | `downloadable_links`, `downloadable_samples` | Keyed map of link/sample rows. | | grouped | `links` | Keyed map of `{ associated_product_id, qty, sort_order }`. | | bundle | `bundle_options` | Keyed map of option groups (`type` ∈ `radio` / `checkbox` / `select` / `multiselect`) each with a `products` map. | | configurable | `variants` | Keyed by variant product id (from the create response or detail `variants[].id`). Replace-semantics — send every variant to keep. | | booking | `booking` | Object with `type` (`default` / `appointment` / `event` / `rental` / `table`) plus sub-type fields (slots / tickets / pricing). | ## Sub-resources are not updated here `images`, `videos`, `inventories`, and `customer_group_prices` are **not** handled by this endpoint — they have dedicated endpoints. If sent, they are ignored and noted in the `_warnings` array on the response: - Images → [`POST /api/admin/catalog/products/{id}/images`](/api/rest-api/admin/catalog/products/images-upload) - Inventories → [`PUT /api/admin/catalog/products/{productId}/inventories`](/api/rest-api/admin/catalog/products/inventories-update) - Customer-group prices → [`POST …/customer-group-prices`](/api/rest-api/admin/catalog/products/customer-group-prices-create) ## Response `200 OK` returning the full product detail payload — same shape as [`GET /api/admin/catalog/products/{id}`](/api/rest-api/admin/catalog/products/products-detail). `_warnings` is an array of human-readable strings; it is empty when nothing was dropped, and non-empty (naming each dropped sub-resource field and the endpoint it should be sent to instead) when sub-resource fields were stripped. ## Errors | HTTP | Cause | |------|-------| | `401 Unauthorized` | Missing or invalid admin Bearer token. | | `403 Forbidden` | Admin role lacks `catalog.products.edit`. | | `404 Not Found` | Product not found. | | `422 Unprocessable Entity` | Validation failure (duplicate SKU / url_key, invalid boolean, special_price ≥ price, invalid date range). | --- # CMS Pages URL: /api/rest-api/admin/cms/pages --- outline: false apiType: rest --- # CMS Pages CMS Pages are **static storefront content pages** — About Us, Privacy Policy, custom landing pages, and the like. Each page is served on the storefront at its `url_key` (e.g. `/page/about-us`). This menu mirrors the admin **CMS → Pages** screen: list, create, edit, delete, and export those pages. ## Multi-locale, multi-channel A page is content that spans languages and storefronts: - **Multi-locale** — a page holds **one content set per language** (its `translations`). Each locale has its own `page_title`, `html_content`, `url_key`, and SEO fields. The listing/detail show the values for the resolved `locale`; the full per-locale set comes back under `translations` on the detail endpoint. - **Multi-channel** — a page is **assigned to one or more channels** (storefronts). The detail endpoint returns the assigned `channels` as `{ id, code, name }` objects; the listing returns the channel codes as a `channels` string array. ## `previewUrl` — the "View" action Every row carries a `previewUrl` — the storefront URL where the page actually renders (built from its `url_key`). This is the API equivalent of the admin **View** action: open it in a browser to preview the live page. ## `htmlContent` is detail-only `htmlContent` is the **full page HTML body**. It is **`null` on the listing** (to keep rows light) and is returned only by the [detail endpoint](/api/rest-api/admin/cms/pages-detail). The listing carries every other cheap column (titles, url_key, SEO, layout, channels, timestamps). ## Create vs Update payload shapes The two write endpoints take **different shapes** (this mirrors the admin form): - **Create** sends **top-level** fields (`url_key`, `page_title`, `html_content`, `meta_*`, `channels`). Those values are **broadcast to every configured locale** at creation. - **Update** sends a **locale-nested** body (`{ "en": { url_key, page_title, ... }, "channels": [...] }`) so you edit one locale at a time. ## Endpoints in this menu | Action | Endpoint | |--------|----------| | [List pages](/api/rest-api/admin/cms/pages-list) | `GET /api/admin/cms/pages` | | [Page detail](/api/rest-api/admin/cms/pages-detail) | `GET /api/admin/cms/pages/{id}` | | [Create page](/api/rest-api/admin/cms/pages-create) | `POST /api/admin/cms/pages` | | [Update page](/api/rest-api/admin/cms/pages-update) | `PUT /api/admin/cms/pages/{id}` | | [Delete page](/api/rest-api/admin/cms/pages-delete) | `DELETE /api/admin/cms/pages/{id}` | | [Mass delete pages](/api/rest-api/admin/cms/pages-mass-delete) | `POST /api/admin/cms/pages/mass-delete` | | [Export pages (CSV)](/api/rest-api/admin/cms/pages/export) | `GET /api/admin/cms/pages/export` | All CMS Pages endpoints require an admin Bearer token — see [Authentication](/api/rest-api/admin/authentication). Writes require the matching `cms.create` / `cms.edit` / `cms.delete` permission. --- # CMS Page — Create URL: /api/rest-api/admin/cms/pages-create --- outline: false apiType: rest examples: - id: admin-cms-pages-create title: Create a CMS Page description: Mirrors Bagisto admin CMS → Pages → Create. Top-level translated fields (page_title, html_content, etc.) are broadcast to every locale by the PageRepository. query: | curl -X POST "https://your-domain.com/api/admin/cms/pages" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "url_key": "about-us", "page_title": "About Us", "html_content": "

About Us

Welcome.

", "channels": [1], "meta_title": "About Us", "meta_keywords": "about,us,company", "meta_description": "Learn more about our company." }' variables: | { "url_key": "about-us", "page_title": "About Us", "html_content": "

About Us

", "channels": [1] } response: | { "id": 7, "urlKey": "about-us", "pageTitle": "About Us", "htmlContent": "

About Us

Welcome.

", "metaTitle": "About Us", "metaKeywords": "about,us,company", "metaDescription": "Learn more about our company.", "locale": "en", "translations": [ { "locale": "en", "url_key": "about-us", "page_title": "About Us", "html_content": "

About Us

Welcome.

", "meta_title": "About Us", "meta_keywords": "about,us,company", "meta_description": "Learn more about our company." } ], "channels": [ { "id": 1, "code": "default", "name": "Default" } ] } commonErrors: - error: Validation (422) cause: Missing required field, duplicate url_key, or empty channels array solution: Send url_key + page_title + html_content + non-empty channels --- # CMS Page — Create Creates a new CMS page. ::: warning Top-level fields vs. update locale-nested **Create** takes the translated fields (`page_title`, `html_content`, `meta_*`, `url_key`) at the **top level** — they are broadcast to every locale by the core `PageRepository`. The [Update endpoint](/api/rest-api/admin/cms/pages-update), in contrast, requires a **locale-nested** payload (`{ "en": { "page_title": "...", ... } }`). ::: ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/cms/pages` | POST | ## Request body | Field | Type | Required | Notes | |-------|------|----------|-------| | `url_key` | string | yes | Must be unique on `cms_page_translations` and pass slug regex. | | `page_title` | string | yes | | | `html_content` | string | yes | | | `channels` | int[] | yes | Non-empty array of existing channel IDs. | | `meta_title` | string | no | | | `meta_keywords` | string | no | | | `meta_description` | string | no | | ## Response `201 Created` returning the same shape as [`GET /api/admin/cms/pages/{id}`](/api/rest-api/admin/cms/pages-detail). ## Errors | HTTP | Cause | |------|-------| | `422 Unprocessable Entity` | Validation failure (missing required field, duplicate url_key, empty channels). | --- # CMS Page — Delete URL: /api/rest-api/admin/cms/pages-delete --- outline: false apiType: rest examples: - id: admin-cms-pages-delete title: Delete a CMS Page description: Deletes a CMS page. query: | curl -X DELETE "https://your-domain.com/api/admin/cms/pages/7" \ -H "Authorization: Bearer " variables: | {} response: | --- # CMS Page — Delete Deletes a single CMS page. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/cms/pages/{id}` | DELETE | ## Response `204 No Content` on success. ## Errors | HTTP | Cause | |------|-------| | `404 Not Found` | Page not found. | --- # CMS Page — Detail URL: /api/rest-api/admin/cms/pages-detail --- outline: false apiType: rest examples: - id: admin-cms-pages-detail title: CMS Page Detail description: Returns a single CMS page with the full html_content body, all per-locale translations, and assigned channels inlined. query: | curl -X GET "https://your-domain.com/api/admin/cms/pages/1" \ -H "Authorization: Bearer " \ -H "Accept: application/json" variables: | {} response: | { "id": 1, "urlKey": "about-us", "pageTitle": "About Us", "htmlContent": "
We are dedicated to providing high-quality products and services to our customers...
", "metaTitle": "about us", "metaKeywords": "aboutus", "metaDescription": "", "layout": null, "previewUrl": "https://your-domain.com/page/about-us", "locale": "en", "channel": "default", "createdAt": "2024-04-16T21:44:17+05:30", "updatedAt": "2024-04-16T21:44:17+05:30", "translations": [ { "locale": "ar", "url_key": "about-us", "page_title": "معلومات عنا", "html_content": "
معلومات عنا...
", "meta_title": "معلومات عنا", "meta_keywords": "معلومات عنا", "meta_description": "معلومات عنا" }, { "locale": "en", "url_key": "about-us", "page_title": "About Us", "html_content": "
We are dedicated to providing high-quality products...
", "meta_title": "about us", "meta_keywords": "aboutus", "meta_description": "" } ], "channels": [ { "id": 1, "code": "default", "name": "Default" } ] } --- # CMS Page — Detail Single CMS page with the full `htmlContent` body, every locale's `translations`, and the assigned `channels` inlined. ::: tip For what CMS Pages are and how multi-locale / multi-channel works, see the [CMS Pages overview](/api/rest-api/admin/cms/pages/). ::: ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/cms/pages/{id}` | GET | ## Response `200 OK` | Field | Type | Notes | |-------|------|-------| | `id` | integer | Page ID. | | `urlKey` | string | URL slug for the active locale. | | `pageTitle` | string | Title for the active locale. | | `htmlContent` | string | The full page HTML body (the field that's `null` on the listing). | | `metaTitle` / `metaKeywords` / `metaDescription` | string\|null | SEO fields for the active locale. | | `layout` | string\|null | Page layout identifier. | | `previewUrl` | string | Live storefront URL for the page (the "View" action). | | `locale` | string | Resolved locale code. | | `channel` | string | Resolved channel code. | | `translations` | array | Per-locale rows — `{ locale, url_key, page_title, html_content, meta_title, meta_keywords, meta_description }`. | | `channels` | array | `{ id, code, name }` of every assigned channel. | | `createdAt`, `updatedAt` | string | ISO 8601. | ## Errors | HTTP | Cause | |------|-------| | `404 Not Found` | Page not found. | --- # CMS Pages — List URL: /api/rest-api/admin/cms/pages-list --- outline: false apiType: rest examples: - id: admin-cms-pages-list title: List CMS Pages description: Paginated, filterable, sortable CMS pages list. Mirrors the admin CMS → Pages datagrid. htmlContent is detail-only (null here). query: | curl -X GET "https://your-domain.com/api/admin/cms/pages?page=1&per_page=2&sort=id&order=desc" \ -H "Authorization: Bearer " \ -H "Accept: application/json" variables: | page=1 per_page=2 sort=id order=desc response: | { "data": [ { "id": 1, "urlKey": "about-us", "pageTitle": "About Us", "htmlContent": null, "metaTitle": "about us", "metaKeywords": "aboutus", "metaDescription": "", "layout": null, "previewUrl": "https://your-domain.com/page/about-us", "locale": "en", "channel": "default", "channels": ["default"], "createdAt": "2024-04-16T21:44:17+05:30", "updatedAt": "2024-04-16T21:44:17+05:30" }, { "id": 11, "urlKey": "privacy-policy", "pageTitle": "Privacy Policy", "htmlContent": null, "metaTitle": null, "metaKeywords": null, "metaDescription": null, "layout": null, "previewUrl": "https://your-domain.com/page/privacy-policy", "locale": "en", "channel": "default", "channels": ["default"], "createdAt": "2024-04-16T21:44:17+05:30", "updatedAt": "2024-04-16T21:44:17+05:30" } ], "meta": { "currentPage": 1, "perPage": 2, "lastPage": 7, "total": 13, "from": 1, "to": 2 } } - id: admin-cms-pages-list-filtered title: List CMS Pages (filtered) description: Filter by channel, locale, and a partial title match. query: | curl -X GET "https://your-domain.com/api/admin/cms/pages?channel=1&locale=en&page_title=policy&sort=id&order=asc" \ -H "Authorization: Bearer " \ -H "Accept: application/json" variables: | channel=1 locale=en page_title=policy response: | { "data": [ { "id": 11, "urlKey": "privacy-policy", "pageTitle": "Privacy Policy", "htmlContent": null, "metaTitle": null, "metaKeywords": null, "metaDescription": null, "layout": null, "previewUrl": "https://your-domain.com/page/privacy-policy", "locale": "en", "channel": "default", "channels": ["default"], "createdAt": "2024-04-16T21:44:17+05:30", "updatedAt": "2024-04-16T21:44:17+05:30" } ], "meta": { "currentPage": 1, "perPage": 10, "lastPage": 1, "total": 1, "from": 1, "to": 1 } } --- # CMS Pages — List Paginated CMS-pages list (datagrid parity), returned in the `{ data, meta }` envelope. ::: tip For what CMS Pages are, how multi-locale / multi-channel works, and the `previewUrl` / `htmlContent` semantics, see the [CMS Pages overview](/api/rest-api/admin/cms/pages/). ::: ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/cms/pages` | GET | ## Query parameters | Param | Type | Notes | |-------|------|-------| | `page` | integer | 1-based page number (default `1`). | | `per_page` | integer | Default `10`, max `50`. | | `id` | integer | Filter by page ID. | | `page_title` | string | Partial title match. | | `url_key` | string | Partial url_key match. | | `channel` | integer | Filter by channel ID. | | `locale` | string | Locale code used for translation resolution. | | `sort` | string | One of `id`, `page_title`, `url_key`, `created_at`. | | `order` | string | `asc` or `desc`. | ## Response `200 OK` — `{ data, meta }` envelope. Each row carries every cheap column: | Field | Type | Notes | |-------|------|-------| | `id` | integer | Page ID. | | `urlKey` | string | Storefront URL slug. | | `pageTitle` | string | Title resolved for the active locale. | | `htmlContent` | null | **Detail-only** — always `null` on the listing; fetch the body from the [detail endpoint](/api/rest-api/admin/cms/pages-detail). | | `metaTitle` | string\|null | SEO title. | | `metaKeywords` | string\|null | SEO keywords. | | `metaDescription` | string\|null | SEO description. | | `layout` | string\|null | Page layout identifier. | | `previewUrl` | string | Live storefront URL for the page (the "View" action). | | `locale` | string | Resolved locale code. | | `channel` | string | Resolved channel code. | | `channels` | string[] | Codes of all channels the page is assigned to. | | `createdAt` | string | ISO 8601. | | `updatedAt` | string | ISO 8601. | --- # CMS Pages — Mass Delete URL: /api/rest-api/admin/cms/pages-mass-delete --- outline: false apiType: rest examples: - id: admin-cms-pages-mass-delete title: Mass Delete CMS Pages description: Deletes a batch of CMS pages. Non-existent IDs are silently skipped (mirrors monolith). query: | curl -X POST "https://your-domain.com/api/admin/cms/pages/mass-delete" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "indices": [12, 18] }' variables: | { "indices": [12, 18] } response: | { "deleted": [12, 18], "message": "CMS pages deleted successfully." } commonErrors: - error: Validation (422) cause: Empty or missing indices solution: Send a non-empty integer array --- # CMS Pages — Mass Delete Bulk-deletes CMS pages. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/cms/pages/mass-delete` | POST | ## Request body | Field | Type | Required | Notes | |-------|------|----------|-------| | `indices` | int[] | yes | Non-empty array of CMS page IDs. | ## Response `200 OK` | Field | Type | Notes | |-------|------|-------| | `deleted` | int[] | IDs the call attempted to delete. | | `message` | string | Translated confirmation. | ## Errors | HTTP | Cause | |------|-------| | `422 Unprocessable Entity` | Empty / missing indices. | --- # CMS Page — Update URL: /api/rest-api/admin/cms/pages-update --- outline: false apiType: rest examples: - id: admin-cms-pages-update title: Update a CMS Page (locale-nested) description: Mirrors Bagisto admin CMS → Pages → Edit. Validation is LOCALE-NESTED — `.url_key`, `.page_title`, `.html_content` are required. Top-level `channels` and `locale` are also required. query: | curl -X PUT "https://your-domain.com/api/admin/cms/pages/7" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "locale": "en", "channels": [1], "en": { "url_key": "about-us", "page_title": "About Us (Updated)", "html_content": "

About Us

Welcome back.

", "meta_title": "About Us", "meta_keywords": "about,us,company", "meta_description": "Updated description." } }' variables: | { "locale": "en", "channels": [1], "en": { "url_key": "about-us", "page_title": "About Us (Updated)", "html_content": "

About Us

" } } response: | commonErrors: - error: Validation (422) cause: Missing nested fields, duplicate url_key, or empty channels solution: Send locale-nested url_key/page_title/html_content plus top-level locale + channels - error: Not Found (404) cause: Page not found solution: Verify `{id}` exists --- # CMS Page — Update Updates a CMS page using a locale-nested payload. ::: warning Locale-nested payload required Unlike [Create](/api/rest-api/admin/cms/pages-create) (top-level fields broadcast to all locales), **Update** validates and writes per-locale via nested blocks: ```json { "locale": "en", "channels": [1], "en": { "url_key": "about-us", "page_title": "About Us", "html_content": "

About Us

" } } ``` The top-level `locale` field names which locale block is being updated. `url_key` uniqueness excludes the current page (no false-positive collisions against itself). ::: ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/cms/pages/{id}` | PUT | ## Request body | Field | Type | Required | Notes | |-------|------|----------|-------| | `locale` | string | yes | Names which locale block is being updated. | | `channels` | int[] | yes | Non-empty array of existing channel IDs. | | `` | object | yes | Per-locale block — `url_key`, `page_title`, `html_content` (required), plus optional `meta_*`. | ## Response `200 OK` — same shape as [`GET /api/admin/cms/pages/{id}`](/api/rest-api/admin/cms/pages-detail). ## Errors | HTTP | Cause | |------|-------| | `404 Not Found` | Page not found. | | `422 Unprocessable Entity` | Missing nested fields, duplicate url_key, or empty channels. | --- # Export CMS Pages URL: /api/rest-api/admin/cms/pages/export --- outline: false apiType: rest examples: - id: admin-cms-pages-export title: Export CMS Pages (CSV) description: Download the CMS pages datagrid as a CSV file — the same data the admin CMS → Pages "Export" button produces. Honours the same filters as the listing and exports every matching row (not just the current page). query: | curl -X GET "https://your-domain.com/api/admin/cms/pages/export?format=csv" \ -H "Authorization: Bearer " \ -H "Accept: text/csv" \ --output cms-pages.csv response: | # Binary response: a text/csv attachment is written to cms-pages.csv # (Content-Type: text/csv; charset=UTF-8 # Content-Disposition: attachment; filename="cms-pages.csv"). Sample contents: ID,"Page Title","URL Key",Channel,Locale 11,"Privacy Policy",privacy-policy,default,en 13,"What's new",whats-new1,default,en 1,"About Us",about-us,default,en - id: admin-cms-pages-export-filtered title: Export CMS Pages (filtered) description: The export honours every listing filter. Here only pages on the default channel in the en locale whose title contains "policy" are exported. query: | curl -X GET "https://your-domain.com/api/admin/cms/pages/export?format=csv&channel=1&locale=en&page_title=policy" \ -H "Authorization: Bearer " \ -H "Accept: text/csv" \ --output cms-pages.csv response: | ID,"Page Title","URL Key",Channel,Locale 11,"Privacy Policy",privacy-policy,default,en - id: admin-cms-pages-export-bad-format title: Unsupported format (422) description: Only format=csv is supported. Any other value returns 422. query: | curl -i -X GET "https://your-domain.com/api/admin/cms/pages/export?format=xlsx" \ -H "Authorization: Bearer " \ -H "Accept: text/csv" response: | HTTP/1.1 422 Unprocessable Content Content-Type: application/json { "message": "Only the csv export format is supported." } --- # Export CMS Pages Downloads the CMS pages datagrid as a **CSV file** — the same data the admin **CMS → Pages** "Export" button produces. The response is a binary `text/csv` attachment, not JSON. Unlike the [listing](/api/rest-api/admin/cms/pages-list), the export is **not paginated** — it streams **every page that matches the current filters**. ::: tip REST only There is no GraphQL counterpart — binary file streams aren't expressible over GraphQL. Use this REST endpoint for the export. ::: ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/cms/pages/export` | GET | ## Request headers | Header | Value | |--------|-------| | `Authorization` | `Bearer ` | | `Accept` | `text/csv` — **required**. The endpoint only produces `text/csv`; sending `Accept: application/json` returns `406 Not Acceptable`. | ## Query parameters | Parameter | Type | Description | |-----------|------|-------------| | `format` | string | Export format — **only `csv` is supported** (the default). Any other value returns `422`. | The export also accepts the **same filters as the [listing](/api/rest-api/admin/cms/pages-list)**: | Filter | Type | Description | |--------|------|-------------| | `id` | integer | Filter by page ID. | | `page_title` | string | Partial title match. | | `url_key` | string | Partial url_key match. | | `channel` | integer | Channel ID. | | `locale` | string | Locale code used for translation resolution. | ## Columns The CSV carries the five datagrid columns, in order: | Header | Value | |--------|-------| | `ID` | Page ID. | | `Page Title` | Page title for the resolved locale. | | `URL Key` | The storefront URL slug. | | `Channel` | Channel code. | | `Locale` | Resolved locale code. | ## Permission `cms` --- # Configuration (Admin) URL: /api/rest-api/admin/configuration --- outline: false --- # Configuration (Admin) The admin **Configuration** screen edits Bagisto's store-wide settings — order settings, currencies, email, SEO, inventory, and everything every installed module adds. Internally these are one flat key/value store; the form layout you see in the admin (Section → Group → Field group → Field) is a **schema** that each module registers at runtime. Because that schema has hundreds of fields and grows with every plugin, the API does **not** expose one endpoint per screen. Three generic endpoints cover the entire Configuration area — current and future: | Endpoint | Method | Purpose | |----------|--------|---------| | [`/api/admin/configuration/menu`](./menu) | GET | **Discover** the schema — which fields exist, their type, default, scoping, validation, and options | | [`/api/admin/configuration`](./values) | GET | **Read** the current effective values for a slug | | [`/api/admin/configuration`](./update) | POST | **Write** new values for a slug | ## How the three work together A client edits a section in four steps: 1. **Discover** — `GET /configuration/menu?slug=` to learn the fields under a section: each field's dotted `code`, `type`, whether it is `channelBased` / `localeBased`, its `validation`, and any `options`. 2. **Read** — `GET /configuration?slug=` to load the current values into your form. 3. **Write** — `POST /configuration` with the changed `code → value` map. 4. **Refresh** — the POST returns the re-resolved values, so you can update your form state without another read. Or skip steps 1–2 and call `GET /configuration/menu?slug=...&include_values=true` to get the schema and the current values in a single round trip. ## Core concepts - **Slug** — a dotted path (`section.group`, e.g. `sales.order_settings`) that scopes a request to one subtree of the schema. Required by Values and Update; optional on Menu (omit it to get the whole tree). - **Code** — the fully-qualified field path (`sales.order_settings.reorder.admin`). This is the unit you read and write. - **Scoping** — `channelBased` / `localeBased` (reported by Menu) decide whether the `channel` / `locale` parameters matter for a field. A field with both `false` is global; passing `channel` / `locale` for it is harmless but ignored. - **Values are strings** — the store column is text, so booleans, numbers, and JSON all come back as strings (`"1"`, `"0"`, `"49.99"`). - **Defaults** — a field with no saved value falls back to the schema `default` reported by Menu. ## Rules to know - **Stay in scope** — every key you write must start with the request's `slug.`. You cannot accidentally overwrite a field in another section. - **Validation is server-side** — it is taken from each field's schema `validation` (discovered via Menu), never trusted from the client. - **File / image fields** are set via `multipart/form-data` on the Update endpoint only — JSON / GraphQL cannot carry binaries. - **Custom fields** — fields reported as `type: "custom"` are blade-rendered in the admin and are **read-only** through the API. - **Password fields** are masked in the UI but stored as plaintext (a Bagisto core behaviour, not an API limitation). All Configuration endpoints require an admin Bearer token — see [Authentication](/api/rest-api/admin/authentication). --- # Configuration Menu URL: /api/rest-api/admin/configuration/menu --- outline: false apiType: rest examples: - id: menu-schema title: Menu — schema only description: Discover the fields under a slug (no values embedded). query: | curl -X GET "https://your-domain.com/api/admin/configuration/menu?slug=sales.order_settings&include_values=false&channel=default&locale=en" \ -H "Authorization: Bearer " response: | [ { "slug": "sales.order_settings", "tree": [ { "key": "sales.order_settings", "name": "Order Settings", "info": "Set order numbers, minimum orders and back orders.", "icon": "settings/order.svg", "sort": 4, "children": [ { "key": "sales.order_settings.reorder", "name": "Allow Reorder", "info": "Enable or disable the reordering feature for admin users.", "icon": null, "sort": 2, "fields": [ { "name": "admin", "code": "sales.order_settings.reorder.admin", "title": "Admin Reorder", "type": "boolean", "customView": null, "default": true, "channelBased": false, "localeBased": false, "validation": null, "options": null, "depends": null, "info": "Enable or disable the reordering feature for admin users." }, { "name": "shop", "code": "sales.order_settings.reorder.shop", "title": "Shop Reorder", "type": "boolean", "customView": null, "default": true, "channelBased": false, "localeBased": false, "validation": null, "options": null, "depends": null, "info": "Enable or disable the reordering feature for shop users." } ] } ] } ] } ] - id: menu-with-values title: Menu — schema + values description: include_values=true embeds each field's current value, resolved for channel / locale. query: | curl -X GET "https://your-domain.com/api/admin/configuration/menu?slug=sales.order_settings.reorder&include_values=true&channel=default&locale=en" \ -H "Authorization: Bearer " response: | [ { "slug": "sales.order_settings.reorder", "tree": [ { "key": "sales.order_settings.reorder", "name": "Allow Reorder", "info": "Enable or disable the reordering feature for admin users.", "icon": null, "sort": 2, "fields": [ { "name": "admin", "code": "sales.order_settings.reorder.admin", "title": "Admin Reorder", "type": "boolean", "customView": null, "default": true, "channelBased": false, "localeBased": false, "validation": null, "options": null, "depends": null, "info": "Enable or disable the reordering feature for admin users.", "value": "1" }, { "name": "shop", "code": "sales.order_settings.reorder.shop", "title": "Shop Reorder", "type": "boolean", "customView": null, "default": true, "channelBased": false, "localeBased": false, "validation": null, "options": null, "depends": null, "info": "Enable or disable the reordering feature for shop users.", "value": "0" } ] } ] } ] --- # Configuration Menu | Endpoint | Method | |----------|--------| | `/api/admin/configuration/menu` | GET | The **discovery** endpoint — it returns the configuration schema tree (Section → Group → Field group → Field). Call it first to learn which fields a section has, each field's dotted `code` (the key you read and write), its `type`, `default`, scoping flags, `validation`, and `options`. See the [Configuration overview](./) for how Menu, Values, and Update fit together. The response is a one-element array; the object inside carries `slug` (the requested scope) and `tree` (the schema). ## Query parameters | Param | Type | Notes | |-------|------|-------| | `slug` | string | Optional. Scopes the response to one node, e.g. `sales.order_settings`. Omit to return the whole tree. | | `include_values` | boolean | When `true`, embeds each field's current `value` (resolved with `channel` / `locale`). | | `channel` | string | Channel code used when resolving values. Defaults to the default channel. | | `locale` | string | Locale code used when resolving values. Defaults to the app locale. | ## Field shape Each leaf field carries: | Key | Meaning | |-----|---------| | `name` | Short field name within its group. | | `code` | Fully-qualified dotted path (e.g. `sales.order_settings.reorder.admin`). Use this to read / write. | | `title` | Human-readable label (already translated). | | `type` | `text`, `textarea`, `boolean`, `select`, `multiselect`, `password`, `image`, `file`, or `custom`. | | `default` | Default used when no value has been saved. | | `channelBased` / `localeBased` | Whether the field is scoped per channel / per locale. | | `validation` | Laravel validation string applied on Update (server-enforced). | | `options` | For `select` / `multiselect` — array of `{ title, value }`. | | `depends`, `info` | Optional UI hints. | | `customView` | Set for `type: "custom"` (blade-rendered) fields — read-only via the API. | | `value` | Only present when `include_values=true` — the field's current value (a string, or `null` if unset). | ## Response codes | Code | Meaning | |------|---------| | 200 | Tree returned. | | 401 | Unauthenticated. | | 404 | Slug not registered. | --- # Configuration Update URL: /api/rest-api/admin/configuration/update --- outline: false apiType: rest examples: - id: update-json title: Update — JSON (scalars) description: Bulk-upsert a code → value map under a slug. Returns the re-resolved values. query: | curl -X POST "https://your-domain.com/api/admin/configuration" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "slug": "sales.order_settings", "channel": "default", "locale": "en", "values": { "sales.order_settings.reorder.admin": "1", "sales.order_settings.reorder.shop": "0" } }' response: | { "success": true, "message": "Configuration updated successfully.", "slug": "sales.order_settings", "channel": "default", "locale": "en", "values": { "sales.order_settings.reorder.admin": "1", "sales.order_settings.reorder.shop": "0" } } - id: update-multipart title: Update — multipart (file upload) description: Image / file fields must be sent as multipart/form-data. Mixed scalars + files are allowed in one request. query: | curl -X POST "https://your-domain.com/api/admin/configuration" \ -H "Authorization: Bearer " \ -F "slug=general.design.admin_logo" \ -F "channel=default" \ -F "locale=en" \ -F "values[general.design.admin_logo.logo_image]=@/path/to/logo.png" response: | { "success": true, "message": "Configuration updated successfully.", "slug": "general.design.admin_logo", "channel": "default", "locale": "en", "values": { "general.design.admin_logo.logo_image": "configuration/abc123.png" } } --- # Configuration Update | Endpoint | Method | |----------|--------| | `/api/admin/configuration` | POST | Bulk-upserts every entry in `values` under the given `slug`. On success the response returns the freshly-resolved values for that slug (the same shape as the [Values](./values) endpoint), so the client can refresh its form state without a follow-up read. See the [Configuration overview](./) for the full flow. ## Request body Accepts `application/json` (for scalar fields) or `multipart/form-data` (when uploading `image` / `file` fields). Mixed payloads — some scalars and some files — are supported in a single multipart request. | Field | Type | Notes | |-------|------|-------| | `slug` | string | **Required.** The slug whose subtree is being written. | | `channel` | string | Channel code. Defaults to the default channel. | | `locale` | string | Locale code. Defaults to the app locale. | | `values` | object | Map of `code → value`. **Every key must start with `slug.`.** | For multipart, file parts use `values[]` as the field name — for example `values[general.design.admin_logo.logo_image]`. Uploaded files are stored and the resulting storage path is written as the field's value. ## Validation Each field's validation rules come from the schema (discovered via [Menu](./menu)), resolved and enforced on the server — they are never trusted from the client. Call Menu first to know which rules apply to a given code. ## Permission Requires the admin role to carry `configuration` (or the finer-grained `configuration.edit`), or `permission_type = "all"`. ## Response codes | Code | Meaning | |------|---------| | 200 | Updated. Body returns the freshly-resolved values. | | 401 | Unauthenticated. | | 403 | Missing the `configuration` permission. | | 404 | Slug not registered. | | 422 | Validation failed, out-of-scope key, missing `slug` / `values`, or an attempt to write a custom (blade) field. | ::: warning Stay in scope Every key in `values` must start with the supplied `slug.` prefix. A request with `slug: "sales.order_settings"` cannot write `catalog.inventory.stock_threshold` even if the fully-qualified key is supplied — the server rejects it with the offending key before any write happens. ::: ::: warning File / image fields are multipart-only JSON (and GraphQL) cannot carry binaries. To set an `image` / `file` field, send `multipart/form-data` with the file at `values[]`. ::: ::: warning Custom fields are read-only Fields whose schema declares `type: "custom"` (blade-rendered in the admin) cannot be written via the API. ::: ::: warning Password fields are stored plaintext `type: "password"` fields are UI-masking only; the value is stored as plaintext. This is a Bagisto core behaviour, not an API limitation. ::: --- # Configuration Values URL: /api/rest-api/admin/configuration/values --- outline: false apiType: rest examples: - id: values-section title: Values — a whole section description: Effective values for every field under sales.order_settings. query: | curl -X GET "https://your-domain.com/api/admin/configuration?slug=sales.order_settings&channel=default&locale=en" \ -H "Authorization: Bearer " response: | [ { "slug": "sales.order_settings", "channel": "default", "locale": "en", "values": { "sales.order_settings.order_number.order_number_prefix": null, "sales.order_settings.order_number.order_number_length": null, "sales.order_settings.order_number.order_number_suffix": null, "sales.order_settings.order_number.order_number_generator": null, "sales.order_settings.minimum_order.enable": null, "sales.order_settings.minimum_order.minimum_order_amount": null, "sales.order_settings.minimum_order.include_discount_amount": null, "sales.order_settings.minimum_order.include_tax_to_amount": null, "sales.order_settings.minimum_order.description": null, "sales.order_settings.reorder.admin": "1", "sales.order_settings.reorder.shop": "0" } } ] - id: values-group title: Values — a single group description: Narrow the slug to one group to read just those fields. query: | curl -X GET "https://your-domain.com/api/admin/configuration?slug=sales.order_settings.reorder&channel=default&locale=en" \ -H "Authorization: Bearer " response: | [ { "slug": "sales.order_settings.reorder", "channel": "default", "locale": "en", "values": { "sales.order_settings.reorder.admin": "1", "sales.order_settings.reorder.shop": "0" } } ] --- # Configuration Values | Endpoint | Method | |----------|--------| | `/api/admin/configuration?slug=<…>` | GET | Returns the flat `code → value` map of effective values for every field under the given `slug`. A field with no saved value falls back to the schema `default` reported by the [Menu](./menu) endpoint. See the [Configuration overview](./) for the full read → write flow. The response is a one-element array; the object inside carries `slug`, `channel`, `locale`, and the `values` map. ## Query parameters | Param | Type | Notes | |-------|------|-------| | `slug` | string | **Required.** The `section.group` (or deeper) scope to read, e.g. `sales.order_settings`. | | `channel` | string | Channel code for resolution. Defaults to the requested channel. | | `locale` | string | Locale code for resolution. Defaults to the requested locale. | ## Response shape `values` is a string → string map — the underlying store column is text, so booleans, numbers, and JSON all come back as strings (`"1"`, `"0"`, `"49.99"`). `image` / `file` fields return the storage path written by Update. ## Response codes | Code | Meaning | |------|---------| | 200 | Values returned. | | 401 | Unauthenticated. | | 404 | Slug not registered. | | 422 | `slug` query parameter missing. | ::: warning slug is required The `slug` parameter is mandatory — it prevents accidentally dumping the entire configuration store in one call. ::: ::: tip Scope is per-field Whether `channel` / `locale` change the result depends on each field's `channelBased` / `localeBased` flags (see [Menu](./menu)). A global field returns the same value regardless of the channel / locale you pass. ::: --- # Customer Active Cart Items URL: /api/rest-api/admin/customers/active-cart-items --- outline: false apiType: rest examples: - id: admin-customer-active-cart title: Get Customer's Active Cart Items description: Items in the customer's OWN active storefront cart (carts.is_active = 1) — the right-sidebar "Cart Items" panel on the Create-Order screen. query: | curl -X GET "https://your-domain.com/api/admin/customers/19/cart-items" \ -H "Authorization: Bearer " variables: | {} response: | { "data": [ { "id": 1701, "productId": 2358, "sku": "test65", "type": "simple", "name": "Classic Watch Hand", "quantity": 1, "price": 4000, "formattedPrice": "$4,000.00", "total": 4000, "formattedTotal": "$4,000.00", "additional": { "quantity": 1 } } ], "meta": { "currentPage": 1, "perPage": 1, "lastPage": 1, "total": 1, "from": 1, "to": 1 } } --- # Customer Active Cart Items Items the customer has in their **own** active storefront cart (`carts.is_active = 1`) — distinct from the admin draft cart being built. The Create-Order screen's right-sidebar shows these so the admin can pull items into the draft. | Endpoint | Method | |----------|--------| | `/api/admin/customers/{customerId}/cart-items` | GET | Returns only **top-level** items (`cart_items.parent_id IS NULL`). Empty `data` array when the customer has no active cart. Requires an admin Bearer token. --- # Customer Addresses URL: /api/rest-api/admin/customers/addresses --- outline: false apiType: rest examples: - id: admin-customer-addresses title: Get Customer Addresses description: All saved addresses for a customer — used by the Create-Order screen's billing / shipping picker. query: | curl -X GET "https://your-domain.com/api/admin/customers/122/addresses" \ -H "Authorization: Bearer " variables: | {} response: | { "data": [ { "id": 2638, "addressType": "customer", "firstName": "John", "lastName": "Doe", "companyName": "Webkul Softwares", "address": "Grand Trunk road, Sector-62", "city": "Noida", "state": "UP", "country": "IN", "postcode": "201556", "email": "john@example.com", "phone": "78787887", "vatId": null, "defaultAddress": false } ], "meta": { "currentPage": 1, "perPage": 1, "lastPage": 1, "total": 1, "from": 1, "to": 1 } } --- # Customer Addresses All saved addresses for a customer — read-only sub-resource used by the **Create-Order** screen's billing/shipping picker. | Endpoint | Method | |----------|--------| | `/api/admin/customers/{customerId}/addresses` | GET | Returns the standard `{ data, meta }` envelope. Not paginated by the UI but wrapped for consistency. 404 if the customer doesn't exist; 401 without an admin Bearer token. --- # Create Customer Address URL: /api/rest-api/admin/customers/addresses/create --- outline: false apiType: rest examples: - id: admin-customer-address-create title: Create Customer Address description: Create a new address under a customer. Setting `default_address=true` unsets the previous default for that customer. query: | curl -X POST "https://your-domain.com/api/admin/customers/14/addresses" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "first_name": "Jane", "last_name": "Doe", "address": "742 Evergreen Terrace", "city": "Springfield", "state": "IL", "country": "US", "postcode": "62704", "phone": "+15551234567", "default_address": true }' response: | { "id": 27, "customerId": 14, "address": "742 Evergreen Terrace", "city": "Springfield", "country": "US", "postcode": "62704", "defaultAddress": true } --- # Create Customer Address | Endpoint | Method | |----------|--------| | `/api/admin/customers/{customerId}/addresses` | POST | ## Request Body Required: `first_name`, `last_name`, `address`, `city`, `country`, `postcode`, `phone`. Optional: `company_name`, `vat_id`, `state`, `address2` (legacy lines joined into `address` with `PHP_EOL`), `default_address` (boolean). ::: tip `address` column convention Bagisto's `addresses` table renamed `address1 → address` in 2024 and dropped `address2`. Pass `address` as a single string. Legacy multi-line arrays are joined with `PHP_EOL`. ::: Permission: `customers.addresses.create`. --- # Delete Customer Address URL: /api/rest-api/admin/customers/addresses/delete --- outline: false apiType: rest examples: - id: admin-customer-address-delete title: Delete Customer Address query: | curl -X DELETE "https://your-domain.com/api/admin/customers/14/addresses/27" \ -H "Authorization: Bearer " response: | { "message": "Address deleted." } --- # Delete Customer Address | Endpoint | Method | |----------|--------| | `/api/admin/customers/{customerId}/addresses/{id}` | DELETE | Same ownership guard as Update. Permission: `customers.addresses.delete`. --- # Customer Address Detail URL: /api/rest-api/admin/customers/addresses/detail --- outline: false apiType: rest examples: - id: admin-customer-address-detail title: Customer Address Detail query: | curl -X GET "https://your-domain.com/api/admin/customers/14/addresses/27" \ -H "Authorization: Bearer " response: | { "id": 27, "customerId": 14, "firstName": "Jane", "lastName": "Doe", "address": "742 Evergreen Terrace", "city": "Springfield", "state": "IL", "country": "US", "postcode": "62704", "phone": "+15551234567", "defaultAddress": true } --- # Customer Address Detail | Endpoint | Method | |----------|--------| | `/api/admin/customers/{customerId}/addresses/{id}` | GET | --- # Update Customer Address URL: /api/rest-api/admin/customers/addresses/update --- outline: false apiType: rest examples: - id: admin-customer-address-update title: Update Customer Address query: | curl -X PUT "https://your-domain.com/api/admin/customers/14/addresses/27" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "city": "Chicago", "postcode": "60601" }' response: | { "id": 27, "customerId": 14, "city": "Chicago", "postcode": "60601" } --- # Update Customer Address | Endpoint | Method | |----------|--------| | `/api/admin/customers/{customerId}/addresses/{id}` | PUT | ::: warning Ownership guard If the path `customerId` doesn't match the address's `customer_id`, the request returns 403. This prevents cross-customer edits via fabricated URLs. ::: Partial update. Permission: `customers.addresses.edit`. --- # Create Draft Cart URL: /api/rest-api/admin/customers/create-draft-cart --- outline: false apiType: rest examples: - id: admin-customer-create-draft-cart title: Create Draft Cart description: Bootstrap an empty admin draft cart (`is_active = false`) for the given customer. Returns the `cartId` the admin uses for the rest of the Create-Order flow. query: | curl -X POST "https://your-domain.com/api/admin/customers/7/draft-carts" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" variables: | {} response: | { "cartId": 412, "customerId": 7, "success": true, "message": "Draft cart created." } commonErrors: - error: Not Found (404) cause: customerId in the URL does not match any customer solution: Verify the customer exists before calling this endpoint - error: Unprocessable Entity (422) cause: The underlying Cart::createCart facade refused (e.g. inactive channel) solution: Confirm a default channel + valid customer; review the error message - error: Unauthorized (401) cause: Missing or invalid admin Bearer token solution: Send a valid admin Bearer token (Integration token) in the Authorization header. See the Authentication page. --- # Create Draft Cart Starts a fresh Create-Order session by spawning an empty draft cart (`is_active = false`) bound to the given customer. This is the customer-nested counterpart to the `POST /api/admin/orders/{id}/reorder` action (which builds the cart from an existing order). Both flows produce the same kind of draft cart and end up at the cart-keyed write endpoints (`POST /api/admin/carts/{id}/items`, addresses, shipping, payment). ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/customers/{customerId}/draft-carts` | POST | The customer is taken from the URL — the request body is empty. ## Why a separate endpoint and not just one cart-create route The customer-nested URL keeps fresh Create-Order distinct from Reorder, which is important because the two have completely different inputs and side-effects. Reorder consumes an existing order id; this endpoint only needs the customer. --- # Delete GDPR Request URL: /api/rest-api/admin/customers/gdpr/delete --- outline: false apiType: rest examples: - id: admin-customer-gdpr-delete title: Delete GDPR Request query: | curl -X DELETE "https://your-domain.com/api/admin/customers/gdpr-requests/1" \ -H "Authorization: Bearer " response: | { "message": "GDPR request deleted." } --- # Delete GDPR Request | Endpoint | Method | |----------|--------| | `/api/admin/customers/gdpr-requests/{id}` | DELETE | Hard delete. Permission: `customers.gdpr_requests.delete`. --- # GDPR Request Detail URL: /api/rest-api/admin/customers/gdpr/detail --- outline: false apiType: rest examples: - id: admin-customer-gdpr-detail title: GDPR Request Detail query: | curl -X GET "https://your-domain.com/api/admin/customers/gdpr-requests/1" \ -H "Authorization: Bearer " response: | { "id": 1, "customerId": 14, "customerName": "Jane Doe", "email": "jane@example.com", "type": "delete", "status": "pending", "message": "Please remove my account", "revokedAt": null, "createdAt": "2026-05-25 08:00:00" } --- # GDPR Request Detail | Endpoint | Method | |----------|--------| | `/api/admin/customers/gdpr-requests/{id}` | GET | The `gdpr_data_request` table is singular — there is **no** `revocation_message` column. Revocation reason rides on the free-form `message` field. --- # Download GDPR Data Export URL: /api/rest-api/admin/customers/gdpr/download-data --- outline: false apiType: rest examples: - id: admin-customer-gdpr-download-data title: Download GDPR Data Export description: Ad-hoc data dump (not bound to a GDPR request). Returns every table referencing the customer's id. query: | curl -X POST "https://your-domain.com/api/admin/customers/14/gdpr-download-data" \ -H "Authorization: Bearer " response: | { "customerId": 14, "customerEmail": "jane@example.com", "generatedAt": "2026-05-25 10:00:00", "data": { "customer": { "id": 14, "first_name": "Jane", "last_name": "Doe", "email": "jane@example.com" }, "addresses": [ /* ... */ ], "orders": [ /* with items, addresses, payment */ ], "reviews": [ /* ... */ ], "wishlist": [ /* ... */ ], "notes": [ /* ... */ ] } } --- # Download GDPR Data Export | Endpoint | Method | |----------|--------| | `/api/admin/customers/{customerId}/gdpr-download-data` | POST | ::: tip Wider than the storefront GDPR PDF The storefront `pdfView` only exports orders + addresses. The admin API extends this to addresses + orders + reviews + wishlist + notes — full GDPR-style export. `password` and `remember_token` are stripped from the `customer` block. ::: Each sub-query is wrapped in try/catch so a missing optional table (e.g. wishlist on a non-storefront install) returns `[]` rather than 500ing. Permission: `customers.gdpr_requests.view`. --- # List GDPR Requests URL: /api/rest-api/admin/customers/gdpr/list --- outline: false apiType: rest examples: - id: admin-customer-gdpr-list title: List GDPR Requests query: | curl -X GET "https://your-domain.com/api/admin/customers/gdpr-requests?status=pending&per_page=10" \ -H "Authorization: Bearer " response: | { "data": [ { "id": 1, "customerId": 14, "customerName": "Jane Doe", "email": "jane@example.com", "type": "delete", "status": "pending", "message": "Please remove my account", "revokedAt": null, "createdAt": "2026-05-25 08:00:00", "updatedAt": "2026-05-25 08:00:00" } ], "meta": { "currentPage": 1, "perPage": 10, "lastPage": 1, "total": 1, "from": 1, "to": 1 } } --- # List GDPR Requests | Endpoint | Method | |----------|--------| | `/api/admin/customers/gdpr-requests` | GET | ## Query Parameters | Parameter | Type | Description | |-----------|------|-------------| | `page`, `per_page` | integer | Pagination. | | `status` | string | `pending`, `processing`, `declined`, `approved`, `revoked`. | | `type` | string | `update`, `delete`. | | `customer_id` | integer | Filter by customer id. | | `email` | string | Partial email. | | `customer_name` | string | Partial customer name. | | `created_at_from` / `_to` | date | Range. | | `sort` | string | `id` (default desc), `status`, `type`, `created_at`. | | `order` | string | `asc`, `desc`. | --- # Process GDPR Request URL: /api/rest-api/admin/customers/gdpr/process --- outline: false apiType: rest examples: - id: admin-customer-gdpr-process title: Process (Approve + Execute) GDPR Request description: Approves a pending request AND, for `type=delete`, cascades the customer delete via `CustomerRepository::delete`. query: | curl -X POST "https://your-domain.com/api/admin/customers/gdpr-requests/1/process" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "message": "Approved on customer request" }' response: | { "requestId": 1, "customerId": 14, "type": "delete", "status": "approved", "customerDeleted": true, "processedAt": "2026-05-25 10:30:00", "message": "GDPR request approved and processed." } --- # Process GDPR Request | Endpoint | Method | |----------|--------| | `/api/admin/customers/gdpr-requests/{id}/process` | POST | ::: warning Destructive action For `type=delete` requests this cascades the customer deletion (fires `customer.delete.before/after` so the GDPR module's listeners run). For `type=update` requests it only marks the request approved — admin then applies the requested edits through the regular Customer update endpoint. ::: ::: tip Idempotency by rejection Already-approved or revoked requests are refused with 422. ::: Permission: `customers.gdpr_requests.edit`. --- # Update GDPR Request URL: /api/rest-api/admin/customers/gdpr/update --- outline: false apiType: rest examples: - id: admin-customer-gdpr-update title: Update GDPR Request description: Pure metadata write — status + message. Use the /process endpoint for the destructive cascade. query: | curl -X PUT "https://your-domain.com/api/admin/customers/gdpr-requests/1" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "status": "processing" }' response: | { "id": 1, "status": "processing" } --- # Update GDPR Request | Endpoint | Method | |----------|--------| | `/api/admin/customers/gdpr-requests/{id}` | PUT | ::: tip Pure metadata write — no side effects For `pending → processing` / `pending → declined` use this endpoint. For the destructive cascade (delete customer when `type=delete`), use [Process GDPR Request](./process). ::: Allowed `status` values: `pending`, `processing`, `declined`, `approved`, `revoked`. Invalid → 422. Fires `customer.gdpr-request.update.before/after` + `customer.account.gdpr-request.update.after` so the StatusUpdateNotification email listener still fires. Permission: `customers.gdpr_requests.edit`. --- # Create Customer Group URL: /api/rest-api/admin/customers/groups/create --- outline: false apiType: rest examples: - id: admin-customer-group-create title: Create Customer Group query: | curl -X POST "https://your-domain.com/api/admin/customers/groups" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "code": "vip", "name": "VIP" }' response: | { "id": 5, "code": "vip", "name": "VIP", "isUserDefined": 1 } --- # Create Customer Group | Endpoint | Method | |----------|--------| | `/api/admin/customers/groups` | POST | ## Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `code` | string | yes | Unique. Validated by `Webkul\Core\Rules\Code` (regex `^[a-zA-Z]+[a-zA-Z0-9_]+$`). | | `name` | string | yes | | ::: tip System groups New groups are always created with `is_user_defined=1`. The API cannot create system groups. ::: Fires `customer.customer_group.create.before/after`. Permission: `customers.groups.create`. --- # Delete Customer Group URL: /api/rest-api/admin/customers/groups/delete --- outline: false apiType: rest examples: - id: admin-customer-group-delete title: Delete Customer Group query: | curl -X DELETE "https://your-domain.com/api/admin/customers/groups/4" \ -H "Authorization: Bearer " response: | { "message": "Customer group deleted." } --- # Delete Customer Group | Endpoint | Method | |----------|--------| | `/api/admin/customers/groups/{id}` | DELETE | ::: warning Two delete guards (HTTP 400) - **System group** — refuses if `is_user_defined=0`. - **In use** — refuses if `customers().count() > 0`. ::: Fires `customer.customer_group.delete.before/after`. Permission: `customers.groups.delete`. --- # Customer Group Detail URL: /api/rest-api/admin/customers/groups/detail --- outline: false apiType: rest examples: - id: admin-customer-group-detail title: Customer Group Detail query: | curl -X GET "https://your-domain.com/api/admin/customers/groups/4" \ -H "Authorization: Bearer " response: | { "id": 4, "code": "wholesale", "name": "Wholesale", "isUserDefined": 1, "customersCount": 23, "createdAt": "2026-05-20 12:00:00" } --- # Customer Group Detail | Endpoint | Method | |----------|--------| | `/api/admin/customers/groups/{id}` | GET | --- # List Customer Groups URL: /api/rest-api/admin/customers/groups/list --- outline: false apiType: rest examples: - id: admin-customer-groups-list title: List Customer Groups query: | curl -X GET "https://your-domain.com/api/admin/customers/groups?per_page=10" \ -H "Authorization: Bearer " response: | { "data": [ { "id": 1, "code": "general", "name": "General", "isUserDefined": 0, "customersCount": null, "createdAt": "2025-01-01 00:00:00", "updatedAt": "2025-01-01 00:00:00" }, { "id": 4, "code": "wholesale", "name": "Wholesale", "isUserDefined": 1, "customersCount": null, "createdAt": "2026-05-20 12:00:00", "updatedAt": "2026-05-20 12:00:00" } ], "meta": { "currentPage": 1, "perPage": 10, "lastPage": 1, "total": 2, "from": 1, "to": 2 } } --- # List Customer Groups | Endpoint | Method | |----------|--------| | `/api/admin/customers/groups` | GET | ## Query Parameters | Parameter | Type | Description | |-----------|------|-------------| | `page`, `per_page` | integer | Pagination (default `10`, cap `50`). | | `code` | string | Partial code match. | | `name` | string | Partial name match. | | `is_user_defined` | integer | `0` (system) or `1` (user-defined). | | `sort` | string | `id` (default desc), `code`, `name`. | | `order` | string | `asc`, `desc`. | `customersCount` is detail-only — null on listing rows. --- # Mass Delete Customer Groups URL: /api/rest-api/admin/customers/groups/mass-delete --- outline: false apiType: rest examples: - id: admin-customer-group-mass-delete title: Mass Delete Customer Groups query: | curl -X POST "https://your-domain.com/api/admin/customers/groups/mass-delete" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "indices": [4, 5, 1] }' response: | { "deleted": [5], "skipped": [ { "id": 1, "reason": "System group cannot be deleted" }, { "id": 4, "reason": "Group has customers attached" } ], "message": "Customer groups processed." } --- # Mass Delete Customer Groups | Endpoint | Method | |----------|--------| | `/api/admin/customers/groups/mass-delete` | POST | System-group + in-use guards skip per-id with a reason. Empty `indices` → 422. Permission: `customers.groups.delete`. --- # Update Customer Group URL: /api/rest-api/admin/customers/groups/update --- outline: false apiType: rest examples: - id: admin-customer-group-update title: Update Customer Group query: | curl -X PUT "https://your-domain.com/api/admin/customers/groups/4" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "name": "Wholesale Tier A" }' response: | { "id": 4, "code": "wholesale", "name": "Wholesale Tier A" } --- # Update Customer Group | Endpoint | Method | |----------|--------| | `/api/admin/customers/groups/{id}` | PUT | Partial. `code` uniqueness excludes self. ::: warning System group restrictions For system groups (`is_user_defined=0`), attempting to change `code` or `is_user_defined` returns 422. Only `name` is editable. ::: Permission: `customers.groups.edit`. --- # Impersonate Customer URL: /api/rest-api/admin/customers/impersonate/create --- outline: false apiType: rest examples: - id: admin-customer-impersonate title: Issue Impersonation Token description: Returns a short-lived Sanctum customer token the admin can use to act as the customer. query: | curl -X POST "https://your-domain.com/api/admin/customers/14/impersonate" \ -H "Authorization: Bearer " response: | { "token": "23|aLongRandomSanctumToken", "customerId": 14, "customerEmail": "jane@example.com", "customerName": "Jane Doe", "impersonatedByAdminId": 1, "expiresAt": "2026-05-25 11:00:00" } --- # Impersonate Customer | Endpoint | Method | |----------|--------| | `/api/admin/customers/{customerId}/impersonate` | POST | ::: warning Token expires in 1 hour - Token `name = "admin-impersonate:{adminId}"` — searchable for audit. - Abilities = `['*', 'impersonated-by-admin:{adminId}']`. - `expires_at = now() + 1 hour` — non-negotiable; aligns with short-lived assumed-identity windows. - The plaintext token is returned **once** in the response body. There is no way to retrieve it again — store it immediately. - Issuance is audit-logged via `admin.customer.impersonate` with `{admin_id, customer_id, token_id, expires_at}`. ::: Use the returned token as a regular customer Sanctum Bearer against `/api/shop/*` endpoints. Permission: `customers.customers.edit`. --- # Create Customer URL: /api/rest-api/admin/customers/main/create --- outline: false apiType: rest examples: - id: admin-customer-create title: Create Customer description: Creates a new customer. When `send_password` is true (default) a random password is generated and emailed via `Webkul\Admin\Mail\Customer\NewCustomerNotification`. query: | curl -X POST "https://your-domain.com/api/admin/customers" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "first_name": "Jane", "last_name": "Doe", "email": "jane@example.com", "customer_group_id": 2, "status": 1, "send_password": true }' response: | { "id": 14, "firstName": "Jane", "lastName": "Doe", "email": "jane@example.com", "customerGroupId": 2, "status": 1 } --- # Create Customer ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/customers` | POST | ## Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `first_name` | string | yes | | | `last_name` | string | yes | | | `email` | string | yes | Unique. | | `phone` | string | no | | | `gender` | enum | no | `Male`, `Female`, `Other` | | `date_of_birth` | date | no | | | `customer_group_id` | integer | yes | | | `channel_id` | integer | no | | | `status` | integer | no | `0` or `1` (default `1`). | | `subscribed_to_news_letter` | boolean | no | | | `send_password` | boolean | no | Default `true`. When false, explicit `password` is required. | | `password` | string | conditional | Required when `send_password=false`; min 6 chars. | ::: tip Events Fires `customer.registration.before/after` + `customer.create.before/after`. ::: ## Permission `customers.customers.create` --- # Delete Customer URL: /api/rest-api/admin/customers/main/delete --- outline: false apiType: rest examples: - id: admin-customer-delete title: Delete Customer description: Refuses with HTTP 400 if the customer has any pending/processing orders. query: | curl -X DELETE "https://your-domain.com/api/admin/customers/14" \ -H "Authorization: Bearer " response: | { "message": "Customer deleted." } --- # Delete Customer | Endpoint | Method | |----------|--------| | `/api/admin/customers/{id}` | DELETE | ::: warning Active orders guard `CustomerRepository::haveActiveOrders($customer)` returns true when any order has `status IN ('pending','processing')`. Single delete throws HTTP 400 in that case — mirrors the monolith. ::: Permission: `customers.customers.delete`. --- # Customer Detail URL: /api/rest-api/admin/customers/main/detail --- outline: false apiType: rest examples: - id: admin-customer-detail title: Customer Detail description: Eager-loads `group`, surfaces detail-only counters (`totalAddresses`, `totalOrders`, `totalAmountSpent`). query: | curl -X GET "https://your-domain.com/api/admin/customers/14" \ -H "Authorization: Bearer " response: | { "id": 14, "firstName": "Jane", "lastName": "Doe", "name": "Jane Doe", "email": "jane@example.com", "phone": "+15551234567", "gender": "Female", "dateOfBirth": "1990-01-01", "customerGroupId": 2, "customerGroupName": "Wholesale", "channelId": 1, "status": 1, "subscribedToNewsLetter": false, "isVerified": 1, "isSuspended": 0, "totalAddresses": 2, "totalOrders": 5, "totalAmountSpent": 489.50, "createdAt": "2026-05-20 12:00:00", "updatedAt": "2026-05-20 12:00:00" } --- # Customer Detail ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/customers/{id}` | GET | `totalAmountSpent` sums `orders.base_grand_total_invoiced` for this customer. --- # List Customers (Datagrid) URL: /api/rest-api/admin/customers/main/list --- outline: false apiType: rest examples: - id: admin-customers-list title: List Customers (Datagrid) description: DataGrid-parity listing. Returns slim rows; detail-only fields (`totalAddresses`, `totalOrders`, `totalAmountSpent`) are null on listing. query: | curl -X GET "https://your-domain.com/api/admin/customers?per_page=10&customer_group_id=2" \ -H "Authorization: Bearer " response: | { "data": [ { "id": 14, "firstName": "Jane", "lastName": "Doe", "name": "Jane Doe", "email": "jane@example.com", "phone": "+15551234567", "gender": "Female", "dateOfBirth": "1990-01-01", "customerGroupId": 2, "customerGroupName": "Wholesale", "channelId": 1, "status": 1, "subscribedToNewsLetter": false, "isVerified": 1, "isSuspended": 0, "totalAddresses": null, "totalOrders": null, "totalAmountSpent": null, "createdAt": "2026-05-20 12:00:00", "updatedAt": "2026-05-20 12:00:00" } ], "meta": { "currentPage": 1, "perPage": 10, "lastPage": 1, "total": 1, "from": 1, "to": 1 } } --- # List Customers (Datagrid) Mirrors the admin **Customers → Customers** datagrid. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/customers` | GET | ## Query Parameters | Parameter | Type | Description | |-----------|------|-------------| | `page`, `per_page` | integer | Pagination (default `10`, cap `50`). | | `name` | string | Partial first/last name. | | `email` | string | Partial email. | | `phone` | string | Partial phone. | | `customer_group_id` | integer | Filter by group ID. | | `status` | integer | `0` or `1`. | | `channel_id` | integer | Filter by channel ID. | | `date_of_birth_from` / `_to` | date | DOB range. | | `created_at_from` / `_to` | date | Created range. | | `sort` | string | `id` (default desc), `email`, `first_name`. | | `order` | string | `asc`, `desc`. | --- # Mass Delete Customers URL: /api/rest-api/admin/customers/main/mass-delete --- outline: false apiType: rest examples: - id: admin-customer-mass-delete title: Mass Delete Customers description: Customers with active orders are skipped with a reason instead of aborting the batch. query: | curl -X POST "https://your-domain.com/api/admin/customers/mass-delete" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "indices": [12, 13, 14] }' response: | { "deleted": [12, 14], "skipped": [{ "id": 13, "reason": "Customer has active orders" }], "message": "Customers processed." } --- # Mass Delete Customers | Endpoint | Method | |----------|--------| | `/api/admin/customers/mass-delete` | POST | Body: `{ "indices": int[] }`. Per-id active-orders guard skips with a reason rather than aborting. Permission: `customers.customers.delete`. --- # Mass Update Customer Status URL: /api/rest-api/admin/customers/main/mass-update-status --- outline: false apiType: rest examples: - id: admin-customer-mass-update-status title: Mass Update Customer Status description: Sets `status` on every supplied customer. `value` must be `0` or `1`. Fires `customer.update.before/after` per row. query: | curl -X POST "https://your-domain.com/api/admin/customers/mass-update-status" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "indices": [12, 13, 14], "value": 0 }' response: | { "updated": [12, 13, 14], "value": 0, "message": "Status updated." } --- # Mass Update Customer Status | Endpoint | Method | |----------|--------| | `/api/admin/customers/mass-update-status` | POST | Body: `{ "indices": int[], "value": 0|1 }`. Invalid `value` → 400. Permission: `customers.customers.edit`. --- # Update Customer URL: /api/rest-api/admin/customers/main/update --- outline: false apiType: rest examples: - id: admin-customer-update title: Update Customer description: Partial update. Email uniqueness excludes self. query: | curl -X PUT "https://your-domain.com/api/admin/customers/14" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "first_name": "Janet", "status": 0 }' response: | { "id": 14, "firstName": "Janet", "status": 0 } --- # Update Customer | Endpoint | Method | |----------|--------| | `/api/admin/customers/{id}` | PUT | Same fields as Create — all optional (partial update). `password` is hashed if supplied. Email uniqueness excludes self. Fires `customer.update.before/after`. Permission: `customers.customers.edit`. --- # Add Customer Note URL: /api/rest-api/admin/customers/notes/create --- outline: false apiType: rest examples: - id: admin-customer-note-create title: Add Note to Customer description: Append-only — every POST inserts a new row into `customer_notes` (the legacy `customers.notes` text column was dropped in 2023). query: | curl -X POST "https://your-domain.com/api/admin/customers/14/notes" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "note": "Followed up about return RMA-1023", "customer_notified": false }' response: | { "id": 5, "customerId": 14, "note": "Followed up about return RMA-1023", "customerNotified": false, "createdAt": "2026-05-25 10:00:00" } --- # Add Customer Note | Endpoint | Method | |----------|--------| | `/api/admin/customers/{customerId}/notes` | POST | ## Request Body | Field | Type | Required | Description | |-------|------|----------|-------------| | `note` | string | yes | Non-empty. Empty → 422. | | `customer_notified` | boolean | no | When true, fires the customer notification email listener. | ::: tip Append-only Notes are append-only. There is no update/delete endpoint — every note is a separate row for audit. ::: Permission: `customers.customers.edit`. --- # Customer Recent Order Items URL: /api/rest-api/admin/customers/recent-order-items --- outline: false apiType: rest examples: - id: admin-customer-recent-items title: Get Customer's Recent Order Items description: Up to 5 most-recent distinct items the customer has ordered. Right-sidebar panel on the Create-Order screen. query: | curl -X GET "https://your-domain.com/api/admin/customers/19/recent-order-items" \ -H "Authorization: Bearer " variables: | {} response: | { "data": [ { "id": 2694, "productId": 2358, "sku": "test65", "type": "simple", "name": "Classic Watch Hand", "price": 4000, "formattedPrice": "$4,000.00", "productImage": "http://localhost:8000/storage/product/2358/example.webp", "additional": { "quantity": 1 } } ], "meta": { "currentPage": 1, "perPage": 1, "lastPage": 1, "total": 1, "from": 1, "to": 1 } } --- # Customer Recent Order Items Up to **5 most-recent distinct products** the customer has ordered — the right-sidebar "Recent Order Items" panel on the Create-Order screen. | Endpoint | Method | |----------|--------| | `/api/admin/customers/{customerId}/recent-order-items` | GET | Mirrors the monolith: distinct `product_id` from `order_items` joined to `orders`, `parent_id IS NULL`, ordered by `orders.created_at DESC`, limited to 5. Each row carries the product `type` so the client can render type-specific UI. Requires an admin Bearer token. --- # Delete Review URL: /api/rest-api/admin/customers/reviews/delete --- outline: false apiType: rest examples: - id: admin-customer-review-delete title: Delete Review query: | curl -X DELETE "https://your-domain.com/api/admin/customers/reviews/9" \ -H "Authorization: Bearer " response: | { "message": "Review deleted." } --- # Delete Review | Endpoint | Method | |----------|--------| | `/api/admin/customers/reviews/{id}` | DELETE | Fires `customer.review.delete.before/after`. Permission: `customers.reviews.delete`. --- # Customer Review Detail URL: /api/rest-api/admin/customers/reviews/detail --- outline: false apiType: rest examples: - id: admin-customer-review-detail title: Customer Review Detail description: Eager-loads product + customer + images. Each image has id/path/url. query: | curl -X GET "https://your-domain.com/api/admin/customers/reviews/9" \ -H "Authorization: Bearer " response: | { "id": 9, "title": "Great product!", "comment": "Loved it.", "rating": 5, "status": "pending", "productId": 142, "productSku": "SP-001", "productName": "Classic Watch", "customerId": 14, "customerName": "Jane Doe", "customerEmail": "jane@example.com", "images": [{ "id": 3, "path": "reviews/9/photo.jpg", "url": "https://your-domain.com/storage/reviews/9/photo.jpg" }], "createdAt": "2026-05-25 09:00:00" } --- # Customer Review Detail | Endpoint | Method | |----------|--------| | `/api/admin/customers/reviews/{id}` | GET | --- # List Customer Reviews URL: /api/rest-api/admin/customers/reviews/list --- outline: false apiType: rest examples: - id: admin-customer-reviews-list title: List Customer Reviews (Moderation) description: Reviews are written from the storefront. The admin endpoints are moderation-only — list / detail / status-update / delete. query: | curl -X GET "https://your-domain.com/api/admin/customers/reviews?status=pending&per_page=10" \ -H "Authorization: Bearer " response: | { "data": [ { "id": 9, "title": "Great product!", "comment": "Loved it.", "rating": 5, "status": "pending", "productId": 142, "productSku": "SP-001", "productName": "Classic Watch", "customerId": 14, "customerName": "Jane Doe", "customerEmail": "jane@example.com", "images": null, "createdAt": "2026-05-25 09:00:00" } ], "meta": { "currentPage": 1, "perPage": 10, "lastPage": 1, "total": 1, "from": 1, "to": 1 } } --- # List Customer Reviews | Endpoint | Method | |----------|--------| | `/api/admin/customers/reviews` | GET | ## Query Parameters | Parameter | Type | Description | |-----------|------|-------------| | `page`, `per_page` | integer | Pagination. | | `status` | string | `pending`, `approved`, `disapproved`. | | `rating` | integer | Exact rating (1–5). | | `product_id` | integer | Filter by product. | | `customer_id` | integer | Filter by customer (nullable — guest reviews allowed). | | `created_at_from` / `_to` | datetime | Range. | | `sort` | string | `id` (default desc), `rating`, `created_at`. | | `order` | string | `asc`, `desc`. | `images` is detail-only — null on listing. --- # Mass Delete Reviews URL: /api/rest-api/admin/customers/reviews/mass-delete --- outline: false apiType: rest examples: - id: admin-customer-review-mass-delete title: Mass Delete Reviews query: | curl -X POST "https://your-domain.com/api/admin/customers/reviews/mass-delete" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "indices": [9, 10, 11] }' response: | { "deleted": [9, 10, 11], "skipped": [], "message": "Reviews deleted." } --- # Mass Delete Reviews | Endpoint | Method | |----------|--------| | `/api/admin/customers/reviews/mass-delete` | POST | Empty `indices` → 422. Permission: `customers.reviews.delete`. --- # Mass Update Review Status URL: /api/rest-api/admin/customers/reviews/mass-update-status --- outline: false apiType: rest examples: - id: admin-customer-review-mass-update-status title: Mass Update Review Status query: | curl -X POST "https://your-domain.com/api/admin/customers/reviews/mass-update-status" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "indices": [9, 10], "value": "approved" }' response: | { "updated": [9, 10], "value": "approved", "message": "Statuses updated." } --- # Mass Update Review Status | Endpoint | Method | |----------|--------| | `/api/admin/customers/reviews/mass-update-status` | POST | Body: `{ "indices": int[], "value": "pending"|"approved"|"disapproved" }`. `value` is a **string** (unlike Customers where it is `0|1`). Invalid `value` → 422. Permission: `customers.reviews.edit`. --- # Update Review Status URL: /api/rest-api/admin/customers/reviews/update --- outline: false apiType: rest examples: - id: admin-customer-review-update title: Update Review Status description: Status-only update. Other fields are silently ignored. query: | curl -X PUT "https://your-domain.com/api/admin/customers/reviews/9" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "status": "approved" }' response: | { "id": 9, "status": "approved" } --- # Update Review Status | Endpoint | Method | |----------|--------| | `/api/admin/customers/reviews/{id}` | PUT | ::: tip Status-only Only `status` is editable (`pending` / `approved` / `disapproved`). Title / comment / rating / images are owned by the storefront customer. Fires `customer.review.update.before/after` so the core "review approved" email listener still fires. ::: Invalid `status` → 422. Permission: `customers.reviews.edit`. --- # Customer Wishlist Items URL: /api/rest-api/admin/customers/wishlist-items --- outline: false apiType: rest examples: - id: admin-customer-wishlist title: Get Customer's Wishlist description: The customer's wishlist — the right-sidebar "Wishlist Items" panel on the Create-Order screen. query: | curl -X GET "https://your-domain.com/api/admin/customers/19/wishlist-items" \ -H "Authorization: Bearer " variables: | {} response: | { "data": [ { "id": 88, "productId": 2358, "sku": "test65", "name": "Classic Watch Hand", "price": 4000, "formattedPrice": "$4,000.00", "productImage": "http://localhost:8000/storage/product/2358/example.webp", "additional": null } ], "meta": { "currentPage": 1, "perPage": 1, "lastPage": 1, "total": 1, "from": 1, "to": 1 } } --- # Customer Wishlist Items The customer's full wishlist. Right-sidebar panel on the Create-Order screen. | Endpoint | Method | |----------|--------| | `/api/admin/customers/{customerId}/wishlist-items` | GET | Returns the standard `{ data, meta }` envelope. Each row includes the product thumbnail (`productImage`) for the badge. Requires an admin Bearer token. --- # Dashboard Statistics URL: /api/rest-api/admin/dashboard/stats --- outline: false apiType: rest examples: - id: rest-over-all title: Over-all description: Headline KPIs for the top "Overall Details" cards — customers, orders, sales, average order value and unpaid-invoice total, each compared against the previous period. query: | curl -X GET "https://your-domain.com/api/admin/dashboard/stats?type=over-all&start=2026-05-03&end=2026-06-02&channel=default" \ -H "Accept: application/json" \ -H "Authorization: Bearer |" response: | [ { "type": "over-all", "dateRange": "03 May - 02 Jun", "statistics": { "total_customers": { "previous": 3, "current": 0, "progress": -100 }, "total_orders": { "previous": 3, "current": 22, "progress": 633.33 }, "total_sales": { "previous": 24247, "current": 90401, "formatted_total": "$90,401.00", "progress": 272.83 }, "avg_sales": { "previous": 8082.33, "current": 4109.14, "formatted_total": "$4,109.14", "progress": -49.16 }, "total_unpaid_invoices": { "total": 959719.38, "formatted_total": "$959,719.38" } } } ] - id: rest-today title: Today description: Today-only sales/orders/customers progress plus the list of orders placed today. Each order's `items` field is pre-rendered admin HTML, not structured data. query: | curl -X GET "https://your-domain.com/api/admin/dashboard/stats?type=today&start=2026-05-03&end=2026-06-02" \ -H "Accept: application/json" \ -H "Authorization: Bearer |" response: | [ { "type": "today", "dateRange": "03 May - 02 Jun", "statistics": { "total_sales": { "previous": 0, "current": 1000, "formatted_total": "$1,000.00", "progress": 100 }, "total_orders": { "previous": 0, "current": 2, "progress": 100 }, "total_customers": { "previous": 0, "current": 0, "progress": 0 }, "orders": [ { "id": 638, "increment_id": 638, "status": "processing", "status_label": "Processing", "payment_method": "Money Transfer", "base_grand_total": "465.0000", "formatted_base_grand_total": "$465.00", "channel_name": "bagisto store", "customer_email": "demo@gmail.com", "customer_name": " ", "items": "
...pre-rendered admin HTML...
", "billing_address": "test, India", "created_at": "02 Jun 2026, 12:44:33" } ] } } ] - id: rest-stock-threshold-products title: Stock Threshold Products description: Up to 5 products at or under their inventory threshold. Here `statistics` is a flat array of product rows, not an object. query: | curl -X GET "https://your-domain.com/api/admin/dashboard/stats?type=stock-threshold-products&start=2026-05-03&end=2026-06-02" \ -H "Accept: application/json" \ -H "Authorization: Bearer |" response: | [ { "type": "stock-threshold-products", "dateRange": "03 May - 02 Jun", "statistics": [ { "id": 95, "sku": "Puma-White-variant-2-6", "name": "Variant 2 6", "price": "0.0000", "formatted_price": "$0.00", "total_qty": "0", "image": null }, { "id": 97, "sku": "Puma-White-variant-4-6", "name": "Variant 4 6", "price": "0.0000", "formatted_price": "$0.00", "total_qty": "0", "image": null } ] } ] - id: rest-total-sales title: Total Sales (chart) description: Powers the "Store Stats" sales chart — order/sales progress plus an `over_time` series with one bucket per day across the range. query: | curl -X GET "https://your-domain.com/api/admin/dashboard/stats?type=total-sales&start=2026-05-03&end=2026-06-02" \ -H "Accept: application/json" \ -H "Authorization: Bearer |" response: | [ { "type": "total-sales", "dateRange": "03 May - 02 Jun", "statistics": { "total_orders": { "previous": 3, "current": 22, "progress": 633.33 }, "total_sales": { "previous": 24247, "current": 90401, "formatted_total": "$90,401.00", "progress": 272.83 }, "over_time": [ { "label": "11 May", "total": 0, "count": 0 }, { "label": "12 May", "total": "909.0000", "count": 1 }, { "label": "25 May", "total": "18094.0000", "count": 3 } ] } } ] - id: rest-total-visitors title: Total Visitors (chart) description: Powers the "Visitors" chart — total vs. unique visitor progress plus an `over_time` series (one bucket per day). Requires visitor/analytics data to be populated. query: | curl -X GET "https://your-domain.com/api/admin/dashboard/stats?type=total-visitors&start=2026-05-03&end=2026-06-02" \ -H "Accept: application/json" \ -H "Authorization: Bearer |" response: | [ { "type": "total-visitors", "dateRange": "03 May - 02 Jun", "statistics": { "total": { "previous": 0, "current": 0, "progress": 0 }, "unique": { "previous": 0, "current": 0, "progress": 0 }, "over_time": [ { "label": "31 May", "total": 0 }, { "label": "01 Jun", "total": 0 }, { "label": "02 Jun", "total": 0 } ] } } ] - id: rest-top-selling-products title: Top Selling Products description: Up to 5 best-selling products by revenue. `statistics` is a flat array; each row carries a nested `images` array. query: | curl -X GET "https://your-domain.com/api/admin/dashboard/stats?type=top-selling-products&start=2026-05-03&end=2026-06-02" \ -H "Accept: application/json" \ -H "Authorization: Bearer |" response: | [ { "type": "top-selling-products", "dateRange": "03 May - 02 Jun", "statistics": [ { "id": 2359, "name": "Horizon Arc 49\" OLED Curved Gaming Monitor", "price": "4000.0000", "formatted_price": "$3,899.00", "revenue": "38990.0000", "formatted_revenue": "$38,990.00", "images": [ { "id": 786, "type": "images", "path": "product/2359/Whw0RJrR1dLPn5HHkyk7G7hiUpY6aH8BFYOE7rlc.webp", "product_id": 2359, "position": 1, "url": "https://your-domain.com/storage/product/2359/Whw0RJrR1dLPn5HHkyk7G7hiUpY6aH8BFYOE7rlc.webp" } ] } ] } ] - id: rest-top-customers title: Top Customers description: Up to 5 customers with the most sales in the range. `statistics` is a flat array. `id` may be null for guest checkouts. query: | curl -X GET "https://your-domain.com/api/admin/dashboard/stats?type=top-customers&start=2026-05-03&end=2026-06-02" \ -H "Accept: application/json" \ -H "Authorization: Bearer |" response: | [ { "type": "top-customers", "dateRange": "03 May - 02 Jun", "statistics": [ { "id": 129, "email": "demo@gmail.com", "full_name": "webkul bagisto", "total": "35776.0000", "orders": 6, "formatted_total": "$35,776.00", "datetime": null }, { "id": null, "email": "kesh@king.com", "full_name": "Kesh King", "total": "26504.0000", "orders": 10, "formatted_total": "$26,504.00", "datetime": null } ] } ] --- # Dashboard Statistics Returns the aggregate statistics that power the Bagisto admin **Dashboard** screen — sales, orders, customers, visitors, stock alerts, top products and top customers. | | | |---|---| | **Endpoint** | `GET /api/admin/dashboard/stats` | | **Returns** | A JSON **array** with a single element: `[ { type, dateRange, statistics } ]` | All admin endpoints require an admin Bearer token — see [Authentication](/api/rest-api/admin/authentication). ## Understanding `type` — the dashboard is **seven** separate calls This is the most important thing to understand about this API. The Bagisto admin Dashboard you see in the panel is **not one response**. The page is assembled from **seven independent requests**, one per section, and each is selected with the `?type=` query parameter. These seven groups are exactly the sections of the admin Dashboard screen — no more, no less. So a single call returns **one section** of the dashboard. To render the full screen, call the endpoint once per `type` (or only for the sections you need). The `statistics` payload **changes shape per `type`** — sometimes an object, sometimes a flat array — so always branch on `type` when consuming it. ### Which `type` maps to which part of the dashboard | Dashboard section (admin panel) | `type` | `statistics` is | |---|---|---| | **Overall Details** cards (Total Sales / Orders / Customers / Average Order Sale / Total Unpaid Invoices) | `over-all` | object | | **Today's Details** + today's order list | `today` | object | | **Stock Threshold** product list | `stock-threshold-products` | array | | **Store Stats** sales chart | `total-sales` | object (with `over_time` series) | | **Visitors** chart | `total-visitors` | object (with `over_time` series) | | **Top Selling Products** list | `top-selling-products` | array | | **Customer With Most Sales** list | `top-customers` | array | `over-all` is the default — if you omit `?type=`, you get the "Overall Details" cards. ## Query parameters | Param | Type | Required | Description | |---|---|---|---| | `type` | enum | No | One of the seven values above. Defaults to `over-all`. An unknown value returns **400** (`invalid-type`). | | `start` | date (YYYY-MM-DD) | No | Lower bound of the reporting window. Defaults to **30 days ago**. | | `end` | date (YYYY-MM-DD) | No | Upper bound. Defaults to **today**. | | `channel` | string | No | Channel **code** to scope the figures to a single channel. Defaults to all channels. | `start` / `end` drive both the figures and the `previous` baseline used for each `progress` percentage — the previous period is the same-length window immediately before `start`. ## Response envelope The endpoint always returns a **single-element array**: `[ { type, dateRange, statistics } ]`. - `type` — echoes back the requested group. - `dateRange` — a human-readable label for the window (e.g. `"03 May - 02 Jun"`). *(This is fully populated over REST; it is `null` over the GraphQL transport.)* - `statistics` — an object or array whose shape depends on `type` (documented below). ## Response shapes by `type` Figures with a `previous` / `current` / `progress` shape are period comparisons: `current` is the chosen window, `previous` is the preceding window of equal length, and `progress` is the percentage change (can be negative). ### `over-all` | Key | Shape | Meaning | |---|---|---| | `total_customers` | `{ previous, current, progress }` | New customers registered. | | `total_orders` | `{ previous, current, progress }` | Orders placed. | | `total_sales` | `{ previous, current, formatted_total, progress }` | Gross sales; `formatted_total` is the current value in base currency. | | `avg_sales` | `{ previous, current, formatted_total, progress }` | Average order value. | | `total_unpaid_invoices` | `{ total, formatted_total }` | Outstanding invoice amount (no period comparison). | ### `today` | Key | Shape | Meaning | |---|---|---| | `total_sales` | `{ previous, current, formatted_total, progress }` | Today's sales vs. yesterday. | | `total_orders` | `{ previous, current, progress }` | Today's orders. | | `total_customers` | `{ previous, current, progress }` | Today's new customers. | | `orders` | `array` | Orders placed today. Each row: `id`, `increment_id`, `status`, `status_label`, `payment_method`, `base_grand_total`, `formatted_base_grand_total`, `channel_name`, `customer_email`, `customer_name`, `items`, `billing_address`, `created_at`. | ::: tip `orders[].items` is admin HTML The `items` field is a **pre-rendered admin-panel Blade snippet** (an HTML string of product thumbnails), carried over verbatim from core. It is not structured data — a headless client should ignore it and fetch line items from the Orders API (`/api/admin/orders/{id}`) when product detail is needed. ::: ### `stock-threshold-products` `statistics` is an **array** (up to 5 rows). Each row: `id`, `sku`, `name`, `price`, `formatted_price`, `total_qty`, `image` (base-image URL or `null`). ### `total-sales` | Key | Shape | Meaning | |---|---|---| | `total_orders` | `{ previous, current, progress }` | Orders in the window. | | `total_sales` | `{ previous, current, formatted_total, progress }` | Sales in the window. | | `over_time` | `array` of `{ label, total, count }` | One bucket **per day** across `start`→`end` for the chart line. `total` is sales, `count` is order count. | ### `total-visitors` | Key | Shape | Meaning | |---|---|---| | `total` | `{ previous, current, progress }` | All visits. | | `unique` | `{ previous, current, progress }` | Unique visitors. | | `over_time` | `array` of `{ label, total }` | One bucket per day for the chart. | Visitor figures depend on the Bagisto visitor/analytics tables being populated; on a fresh store they are `0`. ### `top-selling-products` `statistics` is an **array** (up to 5 rows). Each row: `id`, `name`, `price`, `formatted_price`, `revenue`, `formatted_revenue`, and `images` (array of `{ id, type, path, product_id, position, url }`). ### `top-customers` `statistics` is an **array** (up to 5 rows). Each row: `id` (may be `null` for guest checkouts), `email`, `full_name` (may be `null`), `total`, `orders`, `formatted_total`, `datetime`. ## Errors | Condition | HTTP | Body | |---|---|---| | Missing / invalid Bearer token | `401` | `{ "message": "Unauthenticated.", "error": "unauthenticated" }` | | Unknown `type` value | `400` | `{ ... "Invalid dashboard stat type." }` | ## See also - [Dashboard Statistics (GraphQL)](/api/graphql-api/admin/dashboard/stats) — same data over the `statsAdminDashboard` query. - [Reporting](/api/rest-api/admin/reporting/) — deeper, dedicated sales / customer / product report endpoints. --- # Create Marketing Campaign URL: /api/rest-api/admin/marketing/communications/campaigns-create --- outline: false apiType: rest examples: - id: admin-marketing-campaign-create title: Create Marketing Campaign query: | curl -X POST "https://your-domain.com/api/admin/marketing/campaigns" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "name": "July Newsletter", "subject": "Big July deals inside!", "marketing_template_id": 1, "marketing_event_id": 1, "channel_id": 1, "customer_group_id": 1, "status": 1 }' response: | { "id": 1, "name": "July Newsletter", "subject": "Big July deals inside!", "status": 1 } --- # Create Marketing Campaign | Endpoint | Method | |----------|--------| | `/api/admin/marketing/campaigns` | POST | ## Request Body | Field | Type | Required | Notes | |-------|------|----------|-------| | `name` | string | yes | | | `subject` | string | yes | | | `marketing_template_id` | int | yes | | | `marketing_event_id` | int | yes | | | `channel_id` | int | yes | | | `customer_group_id` | int | yes | | | `status` | int | no | 0/1. | Permission: `marketing.communications.campaigns.create`. --- # Delete Marketing Campaign URL: /api/rest-api/admin/marketing/communications/campaigns-delete --- outline: false apiType: rest examples: - id: admin-marketing-campaign-delete title: Delete Marketing Campaign query: | curl -X DELETE "https://your-domain.com/api/admin/marketing/campaigns/1" \ -H "Authorization: Bearer " response: | { "message": "Campaign deleted." } --- # Delete Marketing Campaign | Endpoint | Method | |----------|--------| | `/api/admin/marketing/campaigns/{id}` | DELETE | Permission: `marketing.communications.campaigns.delete`. --- # Marketing Campaign Detail URL: /api/rest-api/admin/marketing/communications/campaigns-detail --- outline: false apiType: rest examples: - id: admin-marketing-campaign-detail title: Marketing Campaign Detail query: | curl -X GET "https://your-domain.com/api/admin/marketing/campaigns/1" \ -H "Authorization: Bearer " response: | { "id": 1, "name": "July Newsletter", "subject": "Big July deals inside!", "status": 1, "marketingTemplateId": 1, "marketingEventId": 1, "channelId": 1, "customerGroupId": 1, "marketingTemplateName": "Welcome Email", "marketingEventName": "Holiday Sale Kickoff", "channelName": "Default", "customerGroupCode": "general" } --- # Marketing Campaign Detail | Endpoint | Method | |----------|--------| | `/api/admin/marketing/campaigns/{id}` | GET | Detail includes embedded `marketingTemplateName`, `marketingEventName`, `channelName`, and `customerGroupCode` (all null on listing rows). --- # List Marketing Campaigns URL: /api/rest-api/admin/marketing/communications/campaigns-list --- outline: false apiType: rest examples: - id: admin-marketing-campaigns-list title: List Marketing Campaigns query: | curl -X GET "https://your-domain.com/api/admin/marketing/campaigns" \ -H "Authorization: Bearer " response: | { "data": [{ "id": 1, "name": "July Newsletter", "subject": "Big July deals inside!", "status": 1, "marketingTemplateId": 1, "marketingEventId": 1, "channelId": 1, "customerGroupId": 1 }], "meta": { "currentPage": 1, "perPage": 10, "lastPage": 1, "total": 1, "from": 1, "to": 1 } } --- # List Marketing Campaigns | Endpoint | Method | |----------|--------| | `/api/admin/marketing/campaigns` | GET | ## Query Parameters `page`, `per_page` (default 10, cap 50), `name`, `status`, `marketing_template_id`, `marketing_event_id`, `channel_id`, `customer_group_id`, `sort` (`id`, `name`), `order`. --- # Send Marketing Campaign URL: /api/rest-api/admin/marketing/communications/campaigns-send --- outline: false apiType: rest examples: - id: admin-marketing-campaign-send title: Send Marketing Campaign query: | curl -X POST "https://your-domain.com/api/admin/marketing/campaigns/12/send" \ -H "Authorization: Bearer " response: | { "id": 12, "campaignId": 12, "queued": 5, "message": "Campaign queued for 5 recipient(s)." } --- # Send Marketing Campaign | Endpoint | Method | |----------|--------| | `/api/admin/marketing/campaigns/{id}/send` | POST | Queues the campaign email for every subscriber in its customer group (or guest subscribers when the group code is `guest`). ::: warning Active campaigns only Send is only allowed on campaigns with `status = 1`. Inactive campaigns return HTTP 422. ::: ::: tip Manual triggers ignore the event date gate Unlike the scheduled `Campaign::process` helper, manual sends ignore the date-based event gate — useful for admin test sends. ::: Permission: `marketing.communications.campaigns.edit`. --- # Update Marketing Campaign URL: /api/rest-api/admin/marketing/communications/campaigns-update --- outline: false apiType: rest examples: - id: admin-marketing-campaign-update title: Update Marketing Campaign query: | curl -X PUT "https://your-domain.com/api/admin/marketing/campaigns/1" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "subject": "Updated subject", "status": 0 }' response: | { "id": 1, "subject": "Updated subject", "status": 0 } --- # Update Marketing Campaign | Endpoint | Method | |----------|--------| | `/api/admin/marketing/campaigns/{id}` | PUT | Permission: `marketing.communications.campaigns.edit`. --- # Create Marketing Event URL: /api/rest-api/admin/marketing/communications/events-create --- outline: false apiType: rest examples: - id: admin-marketing-event-create title: Create Marketing Event query: | curl -X POST "https://your-domain.com/api/admin/marketing/events" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "name": "Holiday Sale Kickoff", "description": "Email blast to all subscribers.", "date": "2026-12-20" }' response: | { "id": 1, "name": "Holiday Sale Kickoff", "description": "Email blast to all subscribers.", "date": "2026-12-20" } --- # Create Marketing Event | Endpoint | Method | |----------|--------| | `/api/admin/marketing/events` | POST | ## Request Body | Field | Type | Required | Notes | |-------|------|----------|-------| | `name` | string | yes | | | `description` | string | yes | | | `date` | string | yes | YYYY-MM-DD. | Permission: `marketing.communications.events.create`. --- # Delete Marketing Event URL: /api/rest-api/admin/marketing/communications/events-delete --- outline: false apiType: rest examples: - id: admin-marketing-event-delete title: Delete Marketing Event query: | curl -X DELETE "https://your-domain.com/api/admin/marketing/events/1" \ -H "Authorization: Bearer " response: | { "message": "Marketing event deleted." } --- # Delete Marketing Event | Endpoint | Method | |----------|--------| | `/api/admin/marketing/events/{id}` | DELETE | Permission: `marketing.communications.events.delete`. --- # Marketing Event Detail URL: /api/rest-api/admin/marketing/communications/events-detail --- outline: false apiType: rest examples: - id: admin-marketing-event-detail title: Marketing Event Detail query: | curl -X GET "https://your-domain.com/api/admin/marketing/events/1" \ -H "Authorization: Bearer " response: | { "id": 1, "name": "Holiday Sale Kickoff", "description": "Email blast to all subscribers.", "date": "2026-12-20" } --- # Marketing Event Detail | Endpoint | Method | |----------|--------| | `/api/admin/marketing/events/{id}` | GET | --- # List Marketing Events URL: /api/rest-api/admin/marketing/communications/events-list --- outline: false apiType: rest examples: - id: admin-marketing-events-list title: List Marketing Events query: | curl -X GET "https://your-domain.com/api/admin/marketing/events" \ -H "Authorization: Bearer " response: | { "data": [{ "id": 1, "name": "Holiday Sale Kickoff", "description": "Email blast to all subscribers.", "date": "2026-12-20" }], "meta": { "currentPage": 1, "perPage": 10, "lastPage": 1, "total": 1, "from": 1, "to": 1 } } --- # List Marketing Events | Endpoint | Method | |----------|--------| | `/api/admin/marketing/events` | GET | ## Query Parameters `page`, `per_page` (default 10, cap 50), `name` (partial), `date_from`, `date_to`, `sort` (`id`, `name`, `date`), `order`. --- # Update Marketing Event URL: /api/rest-api/admin/marketing/communications/events-update --- outline: false apiType: rest examples: - id: admin-marketing-event-update title: Update Marketing Event query: | curl -X PUT "https://your-domain.com/api/admin/marketing/events/1" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "date": "2026-12-22" }' response: | { "id": 1, "name": "Holiday Sale Kickoff", "date": "2026-12-22" } --- # Update Marketing Event | Endpoint | Method | |----------|--------| | `/api/admin/marketing/events/{id}` | PUT | Permission: `marketing.communications.events.edit`. --- # Delete Subscription URL: /api/rest-api/admin/marketing/communications/subscribers-delete --- outline: false apiType: rest examples: - id: admin-marketing-subscriber-delete title: Delete Subscription query: | curl -X DELETE "https://your-domain.com/api/admin/marketing/subscribers/1" \ -H "Authorization: Bearer " response: | { "message": "Subscription deleted." } --- # Delete Subscription | Endpoint | Method | |----------|--------| | `/api/admin/marketing/subscribers/{id}` | DELETE | --- # Newsletter Subscriber Detail URL: /api/rest-api/admin/marketing/communications/subscribers-detail --- outline: false apiType: rest examples: - id: admin-marketing-subscriber-detail title: Newsletter Subscriber Detail query: | curl -X GET "https://your-domain.com/api/admin/marketing/subscribers/1" \ -H "Authorization: Bearer " response: | { "id": 1, "email": "subscriber@example.com", "channelId": 1, "channelName": "Default", "customerId": 12, "customerName": "Jane Doe", "isSubscribed": true } --- # Newsletter Subscriber Detail | Endpoint | Method | |----------|--------| | `/api/admin/marketing/subscribers/{id}` | GET | --- # List Newsletter Subscribers URL: /api/rest-api/admin/marketing/communications/subscribers-list --- outline: false apiType: rest examples: - id: admin-marketing-subscribers-list title: List Newsletter Subscribers query: | curl -X GET "https://your-domain.com/api/admin/marketing/subscribers" \ -H "Authorization: Bearer " response: | { "data": [{ "id": 1, "email": "subscriber@example.com", "channelId": 1, "channelName": "Default", "customerId": 12, "customerName": "Jane Doe", "isSubscribed": true }], "meta": { "currentPage": 1, "perPage": 10, "lastPage": 1, "total": 1, "from": 1, "to": 1 } } --- # List Newsletter Subscribers | Endpoint | Method | |----------|--------| | `/api/admin/marketing/subscribers` | GET | ## Query Parameters `page`, `per_page` (default 10, cap 50), `email` (partial), `channel_id`, `is_subscribed` (0/1), `sort` (`id`, `email`), `order`. ::: warning Storefront-originated Newsletter subscriptions are created via storefront; admin only moderates. There is no `POST` endpoint. ::: --- # Toggle Newsletter Subscription URL: /api/rest-api/admin/marketing/communications/subscribers-toggle --- outline: false apiType: rest examples: - id: admin-marketing-subscriber-toggle title: Toggle Subscription query: | curl -X PUT "https://your-domain.com/api/admin/marketing/subscribers/1" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "is_subscribed": false }' response: | { "id": 1, "isSubscribed": false } --- # Toggle Newsletter Subscription | Endpoint | Method | |----------|--------| | `/api/admin/marketing/subscribers/{id}` | PUT | Sets `is_subscribed` for the subscriber row and mirrors the flag onto the linked customer (if any). ## Request Body | Field | Type | Required | Notes | |-------|------|----------|-------| | `is_subscribed` | boolean | yes | | --- # Create Email Template URL: /api/rest-api/admin/marketing/communications/templates-create --- outline: false apiType: rest examples: - id: admin-marketing-template-create title: Create Email Template query: | curl -X POST "https://your-domain.com/api/admin/marketing/templates" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "name": "Welcome Email", "status": "active", "content": "

Welcome to our store!

" }' response: | { "id": 1, "name": "Welcome Email", "status": "active", "content": "

Welcome to our store!

" } --- # Create Email Template | Endpoint | Method | |----------|--------| | `/api/admin/marketing/templates` | POST | ## Request Body | Field | Type | Required | Notes | |-------|------|----------|-------| | `name` | string | yes | | | `status` | enum | yes | `active`, `inactive`, `draft`. | | `content` | string | yes | HTML body. | Permission: `marketing.communications.email_templates.create`. --- # Delete Email Template URL: /api/rest-api/admin/marketing/communications/templates-delete --- outline: false apiType: rest examples: - id: admin-marketing-template-delete title: Delete Email Template query: | curl -X DELETE "https://your-domain.com/api/admin/marketing/templates/1" \ -H "Authorization: Bearer " response: | { "message": "Email template deleted." } --- # Delete Email Template | Endpoint | Method | |----------|--------| | `/api/admin/marketing/templates/{id}` | DELETE | Permission: `marketing.communications.email_templates.delete`. --- # Email Template Detail URL: /api/rest-api/admin/marketing/communications/templates-detail --- outline: false apiType: rest examples: - id: admin-marketing-template-detail title: Email Template Detail query: | curl -X GET "https://your-domain.com/api/admin/marketing/templates/1" \ -H "Authorization: Bearer " response: | { "id": 1, "name": "Welcome Email", "status": "active", "content": "

Welcome to our store!

" } --- # Email Template Detail | Endpoint | Method | |----------|--------| | `/api/admin/marketing/templates/{id}` | GET | --- # List Email Templates URL: /api/rest-api/admin/marketing/communications/templates-list --- outline: false apiType: rest examples: - id: admin-marketing-templates-list title: List Email Templates query: | curl -X GET "https://your-domain.com/api/admin/marketing/templates?per_page=10" \ -H "Authorization: Bearer " response: | { "data": [{ "id": 1, "name": "Welcome Email", "status": "active", "createdAt": "2026-01-01 00:00:00" }], "meta": { "currentPage": 1, "perPage": 10, "lastPage": 1, "total": 1, "from": 1, "to": 1 } } --- # List Email Templates | Endpoint | Method | |----------|--------| | `/api/admin/marketing/templates` | GET | ## Query Parameters `page`, `per_page` (default 10, cap 50), `name` (partial), `status` (`active`/`inactive`/`draft`), `sort` (`id`, `name`), `order`. --- # Update Email Template URL: /api/rest-api/admin/marketing/communications/templates-update --- outline: false apiType: rest examples: - id: admin-marketing-template-update title: Update Email Template query: | curl -X PUT "https://your-domain.com/api/admin/marketing/templates/1" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "name": "Welcome Email v2", "status": "active", "content": "

Welcome aboard!

" }' response: | { "id": 1, "name": "Welcome Email v2", "status": "active", "content": "

Welcome aboard!

" } --- # Update Email Template | Endpoint | Method | |----------|--------| | `/api/admin/marketing/templates/{id}` | PUT | Permission: `marketing.communications.email_templates.edit`. --- # Create Cart Rule Coupon URL: /api/rest-api/admin/marketing/promotions/cart-rule-coupons-create --- outline: false apiType: rest examples: - id: admin-marketing-cart-rule-coupon-create title: Create Cart Rule Coupon query: | curl -X POST "https://your-domain.com/api/admin/marketing/cart-rules/1/coupons" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "code": "WELCOME10", "usage_limit": 100, "usage_per_customer": 1, "expired_at": "2027-12-31" }' response: | { "id": 12, "cartRuleId": 1, "code": "WELCOME10", "usageLimit": 100, "usagePerCustomer": 1, "expiredAt": "2027-12-31" } --- # Create Cart Rule Coupon | Endpoint | Method | |----------|--------| | `/api/admin/marketing/cart-rules/{cartRuleId}/coupons` | POST | ## Request Body | Field | Type | Required | Notes | |-------|------|----------|-------| | `code` | string | yes | Unique. | | `usage_limit` | int | no | Nullable. | | `usage_per_customer` | int | no | Nullable. | | `expired_at` | date | no | Nullable. | Permission: `marketing.promotions.cart_rules.create`. --- # Delete Cart Rule Coupon URL: /api/rest-api/admin/marketing/promotions/cart-rule-coupons-delete --- outline: false apiType: rest examples: - id: admin-marketing-cart-rule-coupon-delete title: Delete Cart Rule Coupon query: | curl -X DELETE "https://your-domain.com/api/admin/marketing/cart-rules/1/coupons/12" \ -H "Authorization: Bearer " response: | { "message": "Coupon deleted." } --- # Delete Cart Rule Coupon | Endpoint | Method | |----------|--------| | `/api/admin/marketing/cart-rules/{cartRuleId}/coupons/{id}` | DELETE | Coupons not belonging to `{cartRuleId}` → 404. Permission: `marketing.promotions.cart_rules.delete`. --- # Bulk-Generate Cart Rule Coupons URL: /api/rest-api/admin/marketing/promotions/cart-rule-coupons-generate --- outline: false apiType: rest examples: - id: admin-marketing-cart-rule-coupon-generate title: Bulk-Generate Coupons query: | curl -X POST "https://your-domain.com/api/admin/marketing/cart-rules/1/coupons/generate" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "length": 10, "format": "alphanumeric", "prefix": "SAVE-", "suffix": "-2026", "coupon_qty": 5 }' response: | { "id": 1, "cartRuleId": 1, "generated": 5, "coupons": [{ "id": 21, "code": "SAVE-A1B2C3-2026" }], "success": true, "message": "Generated 5 coupons." } --- # Bulk-Generate Cart Rule Coupons | Endpoint | Method | |----------|--------| | `/api/admin/marketing/cart-rules/{cartRuleId}/coupons/generate` | POST | Generates `coupon_qty` random codes of the given format and length, optionally with prefix/suffix. Inherits `usage_limit` / `usage_per_customer` / `expired_at` from the parent cart rule. ## Request Body | Field | Type | Required | Notes | |-------|------|----------|-------| | `length` | int 4-30 | yes | Also accepted as `code_length`. | | `format` | enum | yes | `alphabetic`, `alphanumeric`, `numeric` (core's spelling `alphabetical` also accepted). Also accepted as `code_format`. | | `prefix` | string | no | Also `code_prefix`. | | `suffix` | string | no | Also `code_suffix`. | | `coupon_qty` | int 1-100 | yes | | ::: tip Bulk-generate accepts both shapes The endpoint accepts both the spec's friendlier keys (`length`, `format`, `prefix`, `suffix`) and the core's keys (`code_length`, `code_format`, `code_prefix`, `code_suffix`). ::: Permission: `marketing.promotions.cart_rules.create`. --- # List Cart Rule Coupons URL: /api/rest-api/admin/marketing/promotions/cart-rule-coupons-list --- outline: false apiType: rest examples: - id: admin-marketing-cart-rule-coupons-list title: List Cart Rule Coupons query: | curl -X GET "https://your-domain.com/api/admin/marketing/cart-rules/1/coupons" \ -H "Authorization: Bearer " response: | { "data": [{ "id": 12, "cartRuleId": 1, "code": "WELCOME10", "usageLimit": 100, "usagePerCustomer": 1, "timesUsed": 0, "expiredAt": "2027-12-31" }], "meta": { "currentPage": 1, "perPage": 10, "lastPage": 1, "total": 1, "from": 1, "to": 1 } } --- # List Cart Rule Coupons | Endpoint | Method | |----------|--------| | `/api/admin/marketing/cart-rules/{cartRuleId}/coupons` | GET | Lists coupons belonging to the parent cart rule. Cross-rule access → 404. Permission: `marketing.promotions.cart_rules.view` (falls back to `.create`). --- # Mass Delete Cart Rule Coupons URL: /api/rest-api/admin/marketing/promotions/cart-rule-coupons-mass-delete --- outline: false apiType: rest examples: - id: admin-marketing-cart-rule-coupon-mass-delete title: Mass Delete Cart Rule Coupons query: | curl -X POST "https://your-domain.com/api/admin/marketing/cart-rules/1/coupons/mass-delete" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "indices": [12, 13, 14] }' response: | { "cartRuleId": 1, "deleted": 3, "skipped": [], "success": true, "message": "Deleted 3 coupons." } --- # Mass Delete Cart Rule Coupons | Endpoint | Method | |----------|--------| | `/api/admin/marketing/cart-rules/{cartRuleId}/coupons/mass-delete` | POST | IDs that don't belong to `{cartRuleId}` are silently skipped (cross-rule isolation). Permission: `marketing.promotions.cart_rules.delete`. --- # Copy Cart Rule URL: /api/rest-api/admin/marketing/promotions/cart-rules-copy --- outline: false apiType: rest examples: - id: admin-marketing-cart-rule-copy title: Copy Cart Rule query: | curl -X POST "https://your-domain.com/api/admin/marketing/cart-rules/17/copy" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{}' response: | { "id": 42, "name": "Copy of 10% off summer", "status": 0, "couponType": 1, "actionType": "by_percent", "discountAmount": 10, "channels": [1], "customerGroups": [1, 2, 3], "conditions": [] } --- # Copy Cart Rule | Endpoint | Method | |----------|--------| | `/api/admin/marketing/cart-rules/{id}/copy` | POST | Duplicates an existing cart rule into a brand-new rule. This is the API equivalent of the **Copy** action on the cart rules listing. Send an empty body (`{}`) with `Content-Type: application/json`. The response is the full detail of the **newly created** rule. ## What gets copied | Field | Behaviour | |-------|-----------| | `name` | Prefixed with `Copy of ` (e.g. `Copy of 10% off summer`). | | `status` | Forced to `0` (the copy starts inactive). | | `channels` | Copied from the source rule. | | `customerGroups` | Copied from the source rule. | | `actionType`, `discountAmount`, `conditions`, other settings | Copied from the source rule. | | Coupons | **Not** copied. The new rule has no coupons. | A `404` is returned if the source rule id is unknown. Permission: `marketing.promotions.cart_rules.create`. --- # Create Cart Rule URL: /api/rest-api/admin/marketing/promotions/cart-rules-create --- outline: false apiType: rest examples: - id: admin-marketing-cart-rule-create title: Create Cart Rule query: | curl -X POST "https://your-domain.com/api/admin/marketing/cart-rules" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "name": "10% off summer", "channels": [1], "customer_groups": [1, 2, 3], "coupon_type": 1, "action_type": "by_percent", "discount_amount": 10, "status": 1 }' response: | { "id": 1, "name": "10% off summer", "actionType": "by_percent", "discountAmount": 10 } --- # Create Cart Rule | Endpoint | Method | |----------|--------| | `/api/admin/marketing/cart-rules` | POST | ## Request Body | Field | Type | Required | Notes | |-------|------|----------|-------| | `name` | string | yes | | | `description` | string | no | | | `channels` | int[] | yes | | | `customer_groups` | int[] | yes | | | `starts_from` | datetime | no | | | `ends_till` | datetime | no | | | `status` | int | no | 0/1. | | `coupon_type` | int | yes | 1 = no coupon, 2 = specific coupon. | | `action_type` | enum | yes | `by_percent`, `by_fixed`, `cart_fixed`, `buy_x_get_y`. | | `discount_amount` | number | yes | | | `condition_type` | int | no | 0/1. | | `conditions` | array | no | | Permission: `marketing.promotions.cart_rules.create`. --- # Delete Cart Rule URL: /api/rest-api/admin/marketing/promotions/cart-rules-delete --- outline: false apiType: rest examples: - id: admin-marketing-cart-rule-delete title: Delete Cart Rule query: | curl -X DELETE "https://your-domain.com/api/admin/marketing/cart-rules/1" \ -H "Authorization: Bearer " response: | { "message": "Cart rule deleted." } --- # Delete Cart Rule | Endpoint | Method | |----------|--------| | `/api/admin/marketing/cart-rules/{id}` | DELETE | Permission: `marketing.promotions.cart_rules.delete`. --- # Cart Rule Detail URL: /api/rest-api/admin/marketing/promotions/cart-rules-detail --- outline: false apiType: rest examples: - id: admin-marketing-cart-rule-detail title: Cart Rule Detail query: | curl -X GET "https://your-domain.com/api/admin/marketing/cart-rules/1" \ -H "Authorization: Bearer " response: | { "id": 1, "name": "10% off summer", "status": 1, "couponType": 1, "actionType": "by_percent", "discountAmount": 10, "channels": [1], "customerGroups": [1, 2, 3], "conditions": [] } --- # Cart Rule Detail | Endpoint | Method | |----------|--------| | `/api/admin/marketing/cart-rules/{id}` | GET | --- # List Cart Rules URL: /api/rest-api/admin/marketing/promotions/cart-rules-list --- outline: false apiType: rest examples: - id: admin-marketing-cart-rules-list title: List Cart Rules query: | curl -X GET "https://your-domain.com/api/admin/marketing/cart-rules?per_page=10" \ -H "Authorization: Bearer " response: | { "data": [{ "id": 1, "name": "10% off summer", "status": 1, "couponType": 1, "couponCode": "SUMMER10", "actionType": "by_percent", "discountAmount": 10 }], "meta": { "currentPage": 1, "perPage": 10, "lastPage": 1, "total": 1, "from": 1, "to": 1 } } --- # List Cart Rules | Endpoint | Method | |----------|--------| | `/api/admin/marketing/cart-rules` | GET | ## Query Parameters | Parameter | Notes | |-----------|-------| | `page` | Page number. | | `per_page` | Page size (default 10, cap 50). | | `id` | Single id or comma-separated list (e.g. `1,4,9`). | | `name` | Partial match. | | `status` | `0`/`1`. | | `coupon_type` | `1`/`2`. | | `coupon_code` | Partial match on the rule's primary coupon code. | | `sort_order` | Priority, exact match. | | `starts_from_from` | Start-date range lower bound (ISO 8601). | | `starts_from_to` | Start-date range upper bound (ISO 8601). | | `ends_till_from` | End-date range lower bound (ISO 8601). | | `ends_till_to` | End-date range upper bound (ISO 8601). | | `sort` | `id`, `name`, `sort_order`. | | `order` | `asc`/`desc`. | Each row carries `couponCode` — the rule's primary coupon code (`null` when the rule has no coupon). --- # Mass Delete Cart Rules URL: /api/rest-api/admin/marketing/promotions/cart-rules-mass-delete --- outline: false apiType: rest examples: - id: admin-marketing-cart-rule-mass-delete title: Mass Delete Cart Rules query: | curl -X POST "https://your-domain.com/api/admin/marketing/cart-rules/mass-delete" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "indices": [3, 5] }' response: | { "deleted": [3, 5], "message": "Cart rules deleted." } --- # Mass Delete Cart Rules | Endpoint | Method | |----------|--------| | `/api/admin/marketing/cart-rules/mass-delete` | POST | Non-existent IDs silently skipped. Permission: `marketing.promotions.cart_rules.delete`. --- # Update Cart Rule URL: /api/rest-api/admin/marketing/promotions/cart-rules-update --- outline: false apiType: rest examples: - id: admin-marketing-cart-rule-update title: Update Cart Rule query: | curl -X PUT "https://your-domain.com/api/admin/marketing/cart-rules/1" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "name": "15% off summer", "discount_amount": 15 }' response: | { "id": 1, "name": "15% off summer", "discountAmount": 15 } --- # Update Cart Rule | Endpoint | Method | |----------|--------| | `/api/admin/marketing/cart-rules/{id}` | PUT | Permission: `marketing.promotions.cart_rules.edit`. --- # Create Catalog Rule URL: /api/rest-api/admin/marketing/promotions/catalog-rules-create --- outline: false apiType: rest examples: - id: admin-marketing-catalog-rule-create title: Create Catalog Rule query: | curl -X POST "https://your-domain.com/api/admin/marketing/catalog-rules" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "name": "Summer 10% off", "description": "Sitewide 10% off summer collection", "starts_from": "2026-06-01", "ends_till": "2026-08-31", "status": 1, "sort_order": 0, "condition_type": 1, "conditions": [], "end_other_rules": 0, "action_type": "by_percent", "discount_amount": 10, "channels": [1], "customer_groups": [1, 2] }' response: | { "id": 1, "name": "Summer 10% off", "actionType": "by_percent", "discountAmount": 10, "channels": [1], "customerGroups": [1, 2] } --- # Create Catalog Rule | Endpoint | Method | |----------|--------| | `/api/admin/marketing/catalog-rules` | POST | ## Request Body | Field | Type | Required | Notes | |-------|------|----------|-------| | `name` | string | yes | | | `description` | string | no | | | `starts_from` | string | no | YYYY-MM-DD. | | `ends_till` | string | no | | | `status` | int | no | 0/1. | | `sort_order` | int | no | | | `condition_type` | int | no | | | `conditions` | array | no | | | `end_other_rules` | int | no | | | `action_type` | string | yes | `by_percent`, `by_fixed`, etc. | | `discount_amount` | number | yes | | | `channels` | int[] | yes | | | `customer_groups` | int[] | yes | | Permission: `marketing.promotions.catalog_rules.create`. --- # Delete Catalog Rule URL: /api/rest-api/admin/marketing/promotions/catalog-rules-delete --- outline: false apiType: rest examples: - id: admin-marketing-catalog-rule-delete title: Delete Catalog Rule query: | curl -X DELETE "https://your-domain.com/api/admin/marketing/catalog-rules/1" \ -H "Authorization: Bearer " response: | { "message": "Catalog rule deleted." } --- # Delete Catalog Rule | Endpoint | Method | |----------|--------| | `/api/admin/marketing/catalog-rules/{id}` | DELETE | Permission: `marketing.promotions.catalog_rules.delete`. --- # Catalog Rule Detail URL: /api/rest-api/admin/marketing/promotions/catalog-rules-detail --- outline: false apiType: rest examples: - id: admin-marketing-catalog-rule-detail title: Catalog Rule Detail query: | curl -X GET "https://your-domain.com/api/admin/marketing/catalog-rules/1" \ -H "Authorization: Bearer " response: | { "id": 1, "name": "Summer 10% off", "description": "Sitewide 10% off summer collection", "startsFrom": "2026-06-01", "endsTill": "2026-08-31", "status": 1, "sortOrder": 0, "conditionType": 1, "conditions": [], "endOtherRules": 0, "actionType": "by_percent", "discountAmount": 10, "channels": [1], "customerGroups": [1, 2] } --- # Catalog Rule Detail | Endpoint | Method | |----------|--------| | `/api/admin/marketing/catalog-rules/{id}` | GET | --- # List Catalog Rules URL: /api/rest-api/admin/marketing/promotions/catalog-rules-list --- outline: false apiType: rest examples: - id: admin-marketing-catalog-rules-list title: List Catalog Rules query: | curl -X GET "https://your-domain.com/api/admin/marketing/catalog-rules?per_page=10" \ -H "Authorization: Bearer " response: | { "data": [{ "id": 1, "name": "Summer 10% off", "status": 1, "actionType": "by_percent", "discountAmount": 10, "startsFrom": "2026-06-01", "endsTill": "2026-08-31" }], "meta": { "currentPage": 1, "perPage": 10, "lastPage": 1, "total": 1, "from": 1, "to": 1 } } --- # List Catalog Rules | Endpoint | Method | |----------|--------| | `/api/admin/marketing/catalog-rules` | GET | ## Query Parameters | Parameter | Notes | |-----------|-------| | `page` | Page number. | | `per_page` | Page size (default 10, cap 50). | | `id` | Single id or comma-separated list (e.g. `1,4,9`). | | `name` | Partial match. | | `status` | `0`/`1`. | | `sort_order` | Priority, exact match. | | `starts_from_from` | Start-date range lower bound (ISO 8601). | | `starts_from_to` | Start-date range upper bound (ISO 8601). | | `ends_till_from` | End-date range lower bound (ISO 8601). | | `ends_till_to` | End-date range upper bound (ISO 8601). | | `sort` | `id`, `name`, `sort_order`. | | `order` | `asc`/`desc`. | --- # Mass Delete Catalog Rules URL: /api/rest-api/admin/marketing/promotions/catalog-rules-mass-delete --- outline: false apiType: rest examples: - id: admin-marketing-catalog-rule-mass-delete title: Mass Delete Catalog Rules query: | curl -X POST "https://your-domain.com/api/admin/marketing/catalog-rules/mass-delete" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "indices": [12, 18] }' response: | { "deleted": [12, 18], "message": "Catalog rules deleted." } --- # Mass Delete Catalog Rules | Endpoint | Method | |----------|--------| | `/api/admin/marketing/catalog-rules/mass-delete` | POST | Non-existent IDs are silently skipped. Empty `indices` → 422. Permission: `marketing.promotions.catalog_rules.delete`. --- # Update Catalog Rule URL: /api/rest-api/admin/marketing/promotions/catalog-rules-update --- outline: false apiType: rest examples: - id: admin-marketing-catalog-rule-update title: Update Catalog Rule query: | curl -X PUT "https://your-domain.com/api/admin/marketing/catalog-rules/1" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "name": "Summer 15% off", "discount_amount": 15, "channels": [1], "customer_groups": [1] }' response: | { "id": 1, "name": "Summer 15% off", "discountAmount": 15 } --- # Update Catalog Rule | Endpoint | Method | |----------|--------| | `/api/admin/marketing/catalog-rules/{id}` | PUT | Re-syncs `channels` + `customer_groups` pivots to the supplied lists. Permission: `marketing.promotions.catalog_rules.edit`. --- # Create Search Synonym URL: /api/rest-api/admin/marketing/search-seo/search-synonyms-create --- outline: false apiType: rest examples: - id: admin-marketing-search-synonym-create title: Create Search Synonym query: | curl -X POST "https://your-domain.com/api/admin/marketing/search-synonyms" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "name": "shirt-group", "terms": "shirt,tshirt,tee" }' response: | { "id": 1, "name": "shirt-group", "terms": "shirt,tshirt,tee" } --- # Create Search Synonym | Endpoint | Method | |----------|--------| | `/api/admin/marketing/search-synonyms` | POST | ## Request Body | Field | Type | Required | Notes | |-------|------|----------|-------| | `name` | string | yes | | | `terms` | string | yes | Comma-separated synonyms. | Permission: `marketing.search_seo.search_synonyms.create`. --- # Delete Search Synonym URL: /api/rest-api/admin/marketing/search-seo/search-synonyms-delete --- outline: false apiType: rest examples: - id: admin-marketing-search-synonym-delete title: Delete Search Synonym query: | curl -X DELETE "https://your-domain.com/api/admin/marketing/search-synonyms/1" \ -H "Authorization: Bearer " response: | { "message": "Search synonym deleted." } --- # Delete Search Synonym | Endpoint | Method | |----------|--------| | `/api/admin/marketing/search-synonyms/{id}` | DELETE | Permission: `marketing.search_seo.search_synonyms.delete`. --- # Search Synonym Detail URL: /api/rest-api/admin/marketing/search-seo/search-synonyms-detail --- outline: false apiType: rest examples: - id: admin-marketing-search-synonym-detail title: Search Synonym Detail query: | curl -X GET "https://your-domain.com/api/admin/marketing/search-synonyms/1" \ -H "Authorization: Bearer " response: | { "id": 1, "name": "shirt-group", "terms": "shirt,tshirt,tee" } --- # Search Synonym Detail | Endpoint | Method | |----------|--------| | `/api/admin/marketing/search-synonyms/{id}` | GET | --- # List Search Synonyms URL: /api/rest-api/admin/marketing/search-seo/search-synonyms-list --- outline: false apiType: rest examples: - id: admin-marketing-search-synonyms-list title: List Search Synonyms query: | curl -X GET "https://your-domain.com/api/admin/marketing/search-synonyms" \ -H "Authorization: Bearer " response: | { "data": [{ "id": 1, "name": "shirt-group", "terms": "shirt,tshirt,tee" }], "meta": { "currentPage": 1, "perPage": 10, "lastPage": 1, "total": 1, "from": 1, "to": 1 } } --- # List Search Synonyms | Endpoint | Method | |----------|--------| | `/api/admin/marketing/search-synonyms` | GET | ## Query Parameters `page`, `per_page` (default 10, cap 50), `name` (partial), `terms` (partial), `sort` (`id`, `name`), `order`. --- # Mass Delete Search Synonyms URL: /api/rest-api/admin/marketing/search-seo/search-synonyms-mass-delete --- outline: false apiType: rest examples: - id: admin-marketing-search-synonym-mass-delete title: Mass Delete Search Synonyms query: | curl -X POST "https://your-domain.com/api/admin/marketing/search-synonyms/mass-delete" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "indices": [12, 18] }' response: | { "deleted": [12, 18], "message": "Search synonyms deleted." } --- # Mass Delete Search Synonyms | Endpoint | Method | |----------|--------| | `/api/admin/marketing/search-synonyms/mass-delete` | POST | Non-existent IDs silently skipped. --- # Update Search Synonym URL: /api/rest-api/admin/marketing/search-seo/search-synonyms-update --- outline: false apiType: rest examples: - id: admin-marketing-search-synonym-update title: Update Search Synonym query: | curl -X PUT "https://your-domain.com/api/admin/marketing/search-synonyms/1" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "terms": "shirt,tshirt,tee,polo" }' response: | { "id": 1, "name": "shirt-group", "terms": "shirt,tshirt,tee,polo" } --- # Update Search Synonym | Endpoint | Method | |----------|--------| | `/api/admin/marketing/search-synonyms/{id}` | PUT | Permission: `marketing.search_seo.search_synonyms.edit`. --- # Delete Search Term URL: /api/rest-api/admin/marketing/search-seo/search-terms-delete --- outline: false apiType: rest examples: - id: admin-marketing-search-term-delete title: Delete Search Term query: | curl -X DELETE "https://your-domain.com/api/admin/marketing/search-terms/1" \ -H "Authorization: Bearer " response: | { "message": "Search term deleted." } --- # Delete Search Term | Endpoint | Method | |----------|--------| | `/api/admin/marketing/search-terms/{id}` | DELETE | --- # Search Term Detail URL: /api/rest-api/admin/marketing/search-seo/search-terms-detail --- outline: false apiType: rest examples: - id: admin-marketing-search-term-detail title: Search Term Detail query: | curl -X GET "https://your-domain.com/api/admin/marketing/search-terms/1" \ -H "Authorization: Bearer " response: | { "id": 1, "term": "red shirt", "results": 23, "uses": 142, "redirectUrl": null, "channelId": 1, "channelName": "Default", "locale": "en" } --- # Search Term Detail | Endpoint | Method | |----------|--------| | `/api/admin/marketing/search-terms/{id}` | GET | --- # List Search Terms URL: /api/rest-api/admin/marketing/search-seo/search-terms-list --- outline: false apiType: rest examples: - id: admin-marketing-search-terms-list title: List Search Terms query: | curl -X GET "https://your-domain.com/api/admin/marketing/search-terms?sort=uses&order=desc" \ -H "Authorization: Bearer " response: | { "data": [{ "id": 1, "term": "red shirt", "results": 23, "uses": 142, "channelId": 1, "channelName": "Default", "locale": "en" }], "meta": { "currentPage": 1, "perPage": 10, "lastPage": 1, "total": 1, "from": 1, "to": 1 } } --- # List Search Terms | Endpoint | Method | |----------|--------| | `/api/admin/marketing/search-terms` | GET | ## Query Parameters `page`, `per_page` (default 10, cap 50), `term` (partial), `channel_id`, `locale`, `sort` (`id`, `term`, `uses`, `results`), `order`. ::: warning Auto-recorded Search terms are recorded automatically by storefront searches; there is no `POST` create endpoint. Admin only edits/deletes. ::: --- # Mass Delete Search Terms URL: /api/rest-api/admin/marketing/search-seo/search-terms-mass-delete --- outline: false apiType: rest examples: - id: admin-marketing-search-term-mass-delete title: Mass Delete Search Terms query: | curl -X POST "https://your-domain.com/api/admin/marketing/search-terms/mass-delete" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "indices": [12, 18] }' response: | { "deleted": [12, 18], "message": "Search terms deleted." } --- # Mass Delete Search Terms | Endpoint | Method | |----------|--------| | `/api/admin/marketing/search-terms/mass-delete` | POST | Non-existent IDs silently skipped. --- # Update Search Term URL: /api/rest-api/admin/marketing/search-seo/search-terms-update --- outline: false apiType: rest examples: - id: admin-marketing-search-term-update title: Update Search Term query: | curl -X PUT "https://your-domain.com/api/admin/marketing/search-terms/1" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "term": "red shirt", "redirect_url": "https://example.com/shirts" }' response: | { "id": 1, "term": "red shirt", "redirectUrl": "https://example.com/shirts" } --- # Update Search Term | Endpoint | Method | |----------|--------| | `/api/admin/marketing/search-terms/{id}` | PUT | Admin can edit the term text and optional redirect URL. Counts (`uses` / `results`) are not editable. ## Request Body | Field | Type | Required | Notes | |-------|------|----------|-------| | `term` | string | yes | | | `redirect_url` | string | no | Nullable. | --- # Create Sitemap URL: /api/rest-api/admin/marketing/search-seo/sitemaps-create --- outline: false apiType: rest examples: - id: admin-marketing-sitemap-create title: Create Sitemap query: | curl -X POST "https://your-domain.com/api/admin/marketing/sitemaps" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "file_name": "sitemap.xml", "path": "/" }' response: | { "id": 4, "fileName": "sitemap.xml", "path": "/" } --- # Create Sitemap | Endpoint | Method | |----------|--------| | `/api/admin/marketing/sitemaps` | POST | ## Request Body | Field | Type | Required | Notes | |-------|------|----------|-------| | `file_name` | string | yes | | | `path` | string | yes | | ::: warning Sitemap is not auto-generated on save Creating the row does NOT build the XML files. Call `POST /api/admin/marketing/sitemaps/{id}/generate` to (re)build the XML. ::: Permission: `marketing.search_seo.sitemaps.create`. --- # Delete Sitemap URL: /api/rest-api/admin/marketing/search-seo/sitemaps-delete --- outline: false apiType: rest examples: - id: admin-marketing-sitemap-delete title: Delete Sitemap query: | curl -X DELETE "https://your-domain.com/api/admin/marketing/sitemaps/4" \ -H "Authorization: Bearer " response: | { "message": "Sitemap deleted." } --- # Delete Sitemap | Endpoint | Method | |----------|--------| | `/api/admin/marketing/sitemaps/{id}` | DELETE | Removes the DB row and the generated XML files. Permission: `marketing.search_seo.sitemaps.delete`. --- # Sitemap Detail URL: /api/rest-api/admin/marketing/search-seo/sitemaps-detail --- outline: false apiType: rest examples: - id: admin-marketing-sitemap-detail title: Sitemap Detail query: | curl -X GET "https://your-domain.com/api/admin/marketing/sitemaps/4" \ -H "Authorization: Bearer " response: | { "id": 4, "fileName": "sitemap.xml", "path": "/", "generatedAt": "2026-05-23T11:02:55+00:00", "indexFile": "/sitemap.xml", "generatedSitemaps": ["/sitemap-4-1.xml"] } --- # Sitemap Detail | Endpoint | Method | |----------|--------| | `/api/admin/marketing/sitemaps/{id}` | GET | Detail includes `generatedAt`, `indexFile`, and `generatedSitemaps` (the latter two are detail-only — null in listing rows). --- # Regenerate Sitemap URL: /api/rest-api/admin/marketing/search-seo/sitemaps-generate --- outline: false apiType: rest examples: - id: admin-marketing-sitemap-generate title: Regenerate Sitemap query: | curl -X POST "https://your-domain.com/api/admin/marketing/sitemaps/4/generate" \ -H "Authorization: Bearer " response: | { "id": 4, "sitemapId": 4, "indexFile": "/sitemap.xml", "generatedSitemaps": ["/sitemap-4-1.xml"], "generatedAt": "2026-05-23T11:02:55+00:00", "message": "Sitemap regenerated successfully." } --- # Regenerate Sitemap | Endpoint | Method | |----------|--------| | `/api/admin/marketing/sitemaps/{id}/generate` | POST | Walks every public Category / Product / Page and (re)writes the XML files under the public disk. Updates the sitemap row's `generated_at`, index file, and child sitemap list. ::: tip Synchronous generation The endpoint runs `Webkul\Sitemap\Jobs\ProcessSitemap` via `dispatchSync` — the response carries the generated file paths once the job finishes (not queued in the background). ::: Permission: `marketing.search_seo.sitemaps.edit`. --- # List Sitemaps URL: /api/rest-api/admin/marketing/search-seo/sitemaps-list --- outline: false apiType: rest examples: - id: admin-marketing-sitemaps-list title: List Sitemaps query: | curl -X GET "https://your-domain.com/api/admin/marketing/sitemaps" \ -H "Authorization: Bearer " response: | { "data": [{ "id": 4, "fileName": "sitemap.xml", "path": "/", "generatedAt": "2026-05-23T11:02:55+00:00" }], "meta": { "currentPage": 1, "perPage": 10, "lastPage": 1, "total": 1, "from": 1, "to": 1 } } --- # List Sitemaps | Endpoint | Method | |----------|--------| | `/api/admin/marketing/sitemaps` | GET | ## Query Parameters `page`, `per_page` (default 10, cap 50), `file_name` (partial), `sort` (`id`, `file_name`), `order`. --- # Update Sitemap URL: /api/rest-api/admin/marketing/search-seo/sitemaps-update --- outline: false apiType: rest examples: - id: admin-marketing-sitemap-update title: Update Sitemap query: | curl -X PUT "https://your-domain.com/api/admin/marketing/sitemaps/4" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "file_name": "sitemap-v2.xml" }' response: | { "id": 4, "fileName": "sitemap-v2.xml", "path": "/" } --- # Update Sitemap | Endpoint | Method | |----------|--------| | `/api/admin/marketing/sitemaps/{id}` | PUT | ::: warning No auto-regeneration Updating `file_name` / `path` does NOT regenerate the XML. Call `POST /sitemaps/{id}/generate` to refresh. ::: Permission: `marketing.search_seo.sitemaps.edit`. --- # Create URL Rewrite URL: /api/rest-api/admin/marketing/search-seo/url-rewrites-create --- outline: false apiType: rest examples: - id: admin-marketing-url-rewrite-create title: Create URL Rewrite query: | curl -X POST "https://your-domain.com/api/admin/marketing/url-rewrites" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "entity_type": "product", "request_path": "old-path", "target_path": "new-path", "redirect_type": "301", "locale": "en" }' response: | { "id": 1, "entityType": "product", "requestPath": "old-path", "targetPath": "new-path", "redirectType": "301", "locale": "en" } --- # Create URL Rewrite | Endpoint | Method | |----------|--------| | `/api/admin/marketing/url-rewrites` | POST | ## Request Body | Field | Type | Required | Notes | |-------|------|----------|-------| | `entity_type` | enum | yes | `product`, `category`, `cms_page`. | | `request_path` | string | yes | | | `target_path` | string | yes | | | `redirect_type` | enum | yes | `301`, `302`. | | `locale` | string | yes | Locale code. | Permission: `marketing.search_seo.url_rewrites.create`. --- # Delete URL Rewrite URL: /api/rest-api/admin/marketing/search-seo/url-rewrites-delete --- outline: false apiType: rest examples: - id: admin-marketing-url-rewrite-delete title: Delete URL Rewrite query: | curl -X DELETE "https://your-domain.com/api/admin/marketing/url-rewrites/1" \ -H "Authorization: Bearer " response: | { "message": "URL rewrite deleted." } --- # Delete URL Rewrite | Endpoint | Method | |----------|--------| | `/api/admin/marketing/url-rewrites/{id}` | DELETE | Permission: `marketing.search_seo.url_rewrites.delete`. --- # URL Rewrite Detail URL: /api/rest-api/admin/marketing/search-seo/url-rewrites-detail --- outline: false apiType: rest examples: - id: admin-marketing-url-rewrite-detail title: URL Rewrite Detail query: | curl -X GET "https://your-domain.com/api/admin/marketing/url-rewrites/1" \ -H "Authorization: Bearer " response: | { "id": 1, "entityType": "product", "requestPath": "old-path", "targetPath": "new-path", "redirectType": "301", "locale": "en" } --- # URL Rewrite Detail | Endpoint | Method | |----------|--------| | `/api/admin/marketing/url-rewrites/{id}` | GET | --- # List URL Rewrites URL: /api/rest-api/admin/marketing/search-seo/url-rewrites-list --- outline: false apiType: rest examples: - id: admin-marketing-url-rewrites-list title: List URL Rewrites query: | curl -X GET "https://your-domain.com/api/admin/marketing/url-rewrites" \ -H "Authorization: Bearer " response: | { "data": [{ "id": 1, "entityType": "product", "requestPath": "old-path", "targetPath": "new-path", "redirectType": "301", "locale": "en" }], "meta": { "currentPage": 1, "perPage": 10, "lastPage": 1, "total": 1, "from": 1, "to": 1 } } --- # List URL Rewrites | Endpoint | Method | |----------|--------| | `/api/admin/marketing/url-rewrites` | GET | ## Query Parameters `page`, `per_page` (default 10, cap 50), `entity_type` (`product`/`category`/`cms_page`), `request_path` (partial), `redirect_type` (`301`/`302`), `locale`, `sort` (`id`, `entity_type`, `locale`, `redirect_type`), `order`. --- # Mass Delete URL Rewrites URL: /api/rest-api/admin/marketing/search-seo/url-rewrites-mass-delete --- outline: false apiType: rest examples: - id: admin-marketing-url-rewrite-mass-delete title: Mass Delete URL Rewrites query: | curl -X POST "https://your-domain.com/api/admin/marketing/url-rewrites/mass-delete" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "indices": [12, 18] }' response: | { "deleted": [12, 18], "message": "URL rewrites deleted." } --- # Mass Delete URL Rewrites | Endpoint | Method | |----------|--------| | `/api/admin/marketing/url-rewrites/mass-delete` | POST | Non-existent IDs silently skipped. Permission: `marketing.search_seo.url_rewrites.delete`. --- # Update URL Rewrite URL: /api/rest-api/admin/marketing/search-seo/url-rewrites-update --- outline: false apiType: rest examples: - id: admin-marketing-url-rewrite-update title: Update URL Rewrite query: | curl -X PUT "https://your-domain.com/api/admin/marketing/url-rewrites/1" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "target_path": "newer-path", "redirect_type": "302" }' response: | { "id": 1, "targetPath": "newer-path", "redirectType": "302" } --- # Update URL Rewrite | Endpoint | Method | |----------|--------| | `/api/admin/marketing/url-rewrites/{id}` | PUT | Permission: `marketing.search_seo.url_rewrites.edit`. --- # Get Admin Profile URL: /api/rest-api/admin/profile/get-profile --- outline: false apiType: rest examples: - id: admin-get-profile title: Get Admin Profile description: Return the authenticated admin's profile. Requires the Bearer token in the Authorization header. query: | curl -X GET "https://your-domain.com/api/admin/get" \ -H "Authorization: Bearer " variables: | {} response: | [ { "id": "1", "name": "Example Admin", "email": "admin@example.com", "image": null, "status": "1", "roleId": 1, "roleName": "Administrator", "success": true, "message": null } ] commonErrors: - error: Unauthorized (401) cause: Missing, invalid, or revoked Bearer token solution: Send a valid admin Bearer token (Integration token) in the Authorization header. See the Authentication page. --- # Get Admin Profile Read the profile of the currently authenticated admin. ## Endpoint | Endpoint | Method | Purpose | |----------|--------|---------| | `/api/admin/get` | GET | Return the authenticated admin's profile | ## Details - Requires a valid admin Bearer token. - The response is a JSON array containing a single profile object — `id`, `name`, `email`, `image`, `status`, and role (`roleId` / `roleName`). - An unauthenticated request returns HTTP `401`. ## Examples Use the interactive example on the right. --- # Reporting — Customers URL: /api/rest-api/admin/reporting/customers --- outline: false apiType: rest examples: - id: admin-reporting-customers title: Reporting — Customers query: | curl -X GET "https://your-domain.com/api/admin/reporting/customers?type=total-customers" \ -H "Authorization: Bearer " response: | [ { "entity": "customers", "type": "total-customers", "dateRange": { "previous": "10 Apr 2026 - 10 May 2026", "current": "10 May 2026 - 09 Jun 2026" }, "statistics": { "customers": { "previous": 1, "current": 9, "progress": 800 }, "over_time": { "previous": [ { "label": "23 May", "total": 1 } ], "current": [ { "label": "26 May", "total": 9 } ] } } } ] - id: admin-reporting-customers-filtered title: Reporting — Customers (Filtered by date + channel) query: | curl -X GET "https://your-domain.com/api/admin/reporting/customers?type=total-customers&start=2026-05-10&end=2026-06-09&channel=default" \ -H "Authorization: Bearer " response: | [ { "entity": "customers", "type": "total-customers", "dateRange": { "previous": "10 Apr 2026 - 10 May 2026", "current": "10 May 2026 - 09 Jun 2026" }, "statistics": { "customers": { "previous": 1, "current": 9, "progress": 800 }, "over_time": { "previous": [ { "label": "23 May", "total": 1 } ], "current": [ { "label": "26 May", "total": 9 } ] } } } ] - id: admin-reporting-customers-view title: Reporting — Customers (View Details) query: | curl -X GET "https://your-domain.com/api/admin/reporting/customers/view?type=customers-with-most-sales" \ -H "Authorization: Bearer " response: | [ { "entity": "customers", "type": "customers-with-most-sales", "dateRange": { "previous": "25 Mar - 24 Apr", "current": "25 Apr - 25 May" }, "statistics": { "columns": [ { "key": "name", "label": "Customer" }, { "key": "email", "label": "Email" }, { "key": "total", "label": "Total Sales" } ], "records": [ { "name": "Jane Cooper", "email": "jane@example.com", "total": "$4,820.00" }, { "name": "Devon Lane", "email": "devon@example.com", "total": "$3,150.50" }, { "name": "Arlene McCoy", "email": "arlene@example.com", "total": "$2,940.25" } ] } } ] - id: admin-reporting-customers-export title: Reporting — Customers (Export CSV) query: | curl -X GET "https://your-domain.com/api/admin/reporting/customers/export?type=customers-with-most-sales&format=csv" \ -H "Authorization: Bearer " \ -H "Accept: text/csv" \ -o customers-report.csv response: | Customer,Email,Total Sales Jane Cooper,jane@example.com,"$4,820.00" Devon Lane,devon@example.com,"$3,150.50" --- # Reporting — Customers | Endpoint | Method | |----------|--------| | `/api/admin/reporting/customers` | GET | | `/api/admin/reporting/customers/view` | GET | | `/api/admin/reporting/customers/export` | GET | ## Query Parameters | Param | Type | Notes | |-------|------|-------| | `type` | enum | `total-customers` (default), `customers-traffic`, `customers-with-most-sales`, `customers-with-most-orders`, `customers-with-most-reviews`, `top-customer-groups`. | | `start` | date | Start date. | | `end` | date | End date. | | `channel` | string | Channel code. | ::: warning Helper-method output `statistics` shape depends on the chosen `type`. ::: ## View Details `GET /api/admin/reporting/customers/view` returns the same statistics as the summary `stats` endpoint, but in a detailed table form — the full list that sits behind a panel's **View Details** link. The `statistics` object carries: - `columns` — an ordered list of `{ key, label }` describing each table column. - `records` — the row data, each keyed by the column `key` values. This is the expanded, row-by-row view; the summary `stats` endpoint returns the rolled-up headline figures instead. ## Export (CSV) `GET /api/admin/reporting/customers/export` streams the same detailed table as a `text/csv` attachment (the **Export** button). The header row is built from the column labels, followed by one line per record. Send `Accept: text/csv` and save the response to a file. Only `?format=csv` is accepted — any other `format` value returns HTTP 422. Both **View Details** and **Export** require only authentication; reporting has no permission gate. --- # Reporting — Overview URL: /api/rest-api/admin/reporting/overview --- outline: false apiType: rest examples: - id: admin-reporting-overview title: Reporting Overview query: | curl -X GET "https://your-domain.com/api/admin/reporting/stats?type=total-sales&start=2026-04-01&end=2026-04-30" \ -H "Authorization: Bearer " response: | [ { "entity": "overview", "type": "total-sales", "dateRange": { "previous": "10 Apr 2026 - 10 May 2026", "current": "10 May 2026 - 09 Jun 2026" }, "statistics": { "sales": { "previous": 27243.5, "current": 9697.53, "formatted_total": "$9,697.53", "progress": -64.32 }, "over_time": { "previous": [ { "label": "10 Apr", "total": 4200, "count": 6 } ], "current": [ { "label": "10 May", "total": 8500, "count": 12 } ] } } } ] - id: admin-reporting-overview-filtered title: Reporting Overview (Filtered by date + channel) query: | curl -X GET "https://your-domain.com/api/admin/reporting/stats?type=total-sales&start=2026-05-10&end=2026-06-09&channel=default" \ -H "Authorization: Bearer " response: | [ { "entity": "overview", "type": "total-sales", "dateRange": { "previous": "10 Apr 2026 - 10 May 2026", "current": "10 May 2026 - 09 Jun 2026" }, "statistics": { "sales": { "previous": 27243.5, "current": 9697.53, "formatted_total": "$9,697.53", "progress": -64.32 }, "over_time": { "previous": [ { "label": "10 Apr", "total": 4200, "count": 6 } ], "current": [ { "label": "10 May", "total": 8500, "count": 12 } ] } } } ] --- # Reporting — Overview | Endpoint | Method | |----------|--------| | `/api/admin/reporting/stats` | GET | The Overview endpoint returns a single headline figure across the whole store for the chosen `type`. It is an API convenience aggregation — there is **no** matching "Overview" screen in the admin panel (the admin Reporting menu goes straight to Sales / Customers / Products). Use it to fetch one top-line number without having to call the per-section endpoints. The `type` argument picks which headline you get back: - `total-sales` (default) — total revenue for the period. - `total-orders` — number of orders placed. - `total-customers` — number of new customers. - `top-selling-products-by-revenue` — the best-selling product by revenue. The `statistics` object carries a previous-vs-current comparison plus a `progress` percentage (how the current window compares to the previous one), and for time-based types an `over_time` series of per-day data points for charting. `dateRange` reports the two comparison windows — `current` is the window you asked for, `previous` is the equal-length window immediately before it. Unlike the Sales / Customers / Products pages, the Overview endpoint has **no View Details and no Export** — it is a top-line summary only. ## Query Parameters | Param | Type | Notes | |-------|------|-------| | `type` | enum | `total-sales` (default), `total-orders`, `total-customers`, `top-selling-products-by-revenue`. | | `start` | date | Start of the reporting window (`YYYY-MM-DD`). | | `end` | date | End of the reporting window (`YYYY-MM-DD`). | | `channel` | string | Channel code (e.g. `default`) — scopes the figures to one storefront channel. | Reporting requires only authentication; there is no permission gate. --- # Reporting — Products URL: /api/rest-api/admin/reporting/products --- outline: false apiType: rest examples: - id: admin-reporting-products title: Reporting — Products query: | curl -X GET "https://your-domain.com/api/admin/reporting/products?type=total-sold-quantities" \ -H "Authorization: Bearer " response: | [ { "entity": "products", "type": "total-sold-quantities", "dateRange": { "previous": "10 Apr 2026 - 10 May 2026", "current": "10 May 2026 - 09 Jun 2026" }, "statistics": { "quantities": { "previous": 82, "current": 13, "progress": -84.15 }, "over_time": { "previous": [ { "label": "10 Apr", "total": 21 } ], "current": [ { "label": "10 May", "total": 5 } ] } } } ] - id: admin-reporting-products-filtered title: Reporting — Products (Filtered by date + channel) query: | curl -X GET "https://your-domain.com/api/admin/reporting/products?type=total-sold-quantities&start=2026-05-10&end=2026-06-09&channel=default" \ -H "Authorization: Bearer " response: | [ { "entity": "products", "type": "total-sold-quantities", "dateRange": { "previous": "10 Apr 2026 - 10 May 2026", "current": "10 May 2026 - 09 Jun 2026" }, "statistics": { "quantities": { "previous": 82, "current": 13, "progress": -84.15 }, "over_time": { "previous": [ { "label": "10 Apr", "total": 21 } ], "current": [ { "label": "10 May", "total": 5 } ] } } } ] - id: admin-reporting-products-view title: Reporting — Products (View Details) query: | curl -X GET "https://your-domain.com/api/admin/reporting/products/view?type=top-selling-products-by-revenue" \ -H "Authorization: Bearer " response: | [ { "entity": "products", "type": "top-selling-products-by-revenue", "dateRange": { "previous": "25 Mar - 24 Apr", "current": "25 Apr - 25 May" }, "statistics": { "columns": [ { "key": "name", "label": "Product" }, { "key": "sku", "label": "SKU" }, { "key": "revenue", "label": "Revenue" } ], "records": [ { "name": "Wireless Headphones", "sku": "WH-100", "revenue": "$6,420.00" }, { "name": "Cotton T-Shirt", "sku": "CT-220", "revenue": "$3,980.50" }, { "name": "Leather Wallet", "sku": "LW-045", "revenue": "$2,610.75" } ] } } ] - id: admin-reporting-products-export title: Reporting — Products (Export CSV) query: | curl -X GET "https://your-domain.com/api/admin/reporting/products/export?type=top-selling-products-by-revenue&format=csv" \ -H "Authorization: Bearer " \ -H "Accept: text/csv" \ -o products-report.csv response: | Product,SKU,Revenue Wireless Headphones,WH-100,"$6,420.00" Cotton T-Shirt,CT-220,"$3,980.50" --- # Reporting — Products | Endpoint | Method | |----------|--------| | `/api/admin/reporting/products` | GET | | `/api/admin/reporting/products/view` | GET | | `/api/admin/reporting/products/export` | GET | ## Query Parameters | Param | Type | Notes | |-------|------|-------| | `type` | enum | `total-sold-quantities` (default), `total-products-added-to-wishlist`, `top-selling-products-by-revenue`, `top-selling-products-by-quantity`, `products-with-most-reviews`, `products-with-most-visits`, `last-search-terms`, `top-search-terms`. | | `start` | date | Start date. | | `end` | date | End date. | | `channel` | string | Channel code. | ::: warning Helper-method output `statistics` shape depends on the chosen `type`. ::: ## View Details `GET /api/admin/reporting/products/view` returns the same statistics as the summary `stats` endpoint, but in a detailed table form — the full list that sits behind a panel's **View Details** link. The `statistics` object carries: - `columns` — an ordered list of `{ key, label }` describing each table column. - `records` — the row data, each keyed by the column `key` values. This is the expanded, row-by-row view; the summary `stats` endpoint returns the rolled-up headline figures instead. ## Export (CSV) `GET /api/admin/reporting/products/export` streams the same detailed table as a `text/csv` attachment (the **Export** button). The header row is built from the column labels, followed by one line per record. Send `Accept: text/csv` and save the response to a file. Only `?format=csv` is accepted — any other `format` value returns HTTP 422. Both **View Details** and **Export** require only authentication; reporting has no permission gate. --- # Reporting — Sales URL: /api/rest-api/admin/reporting/sales --- outline: false apiType: rest examples: - id: admin-reporting-sales title: Reporting — Sales query: | curl -X GET "https://your-domain.com/api/admin/reporting/sales?type=total-sales" \ -H "Authorization: Bearer " response: | [ { "entity": "sales", "type": "total-sales", "dateRange": { "previous": "10 Apr 2026 - 10 May 2026", "current": "10 May 2026 - 09 Jun 2026" }, "statistics": { "sales": { "previous": 27243.5, "current": 9697.53, "formatted_total": "$9,697.53", "progress": -64.32 }, "over_time": { "previous": [ { "label": "10 Apr", "total": 4200, "count": 6 } ], "current": [ { "label": "10 May", "total": 8500, "count": 12 } ] } } } ] - id: admin-reporting-sales-filtered title: Reporting — Sales (Filtered by date + channel) query: | curl -X GET "https://your-domain.com/api/admin/reporting/sales?type=total-sales&start=2026-05-10&end=2026-06-09&channel=default" \ -H "Authorization: Bearer " response: | [ { "entity": "sales", "type": "total-sales", "dateRange": { "previous": "10 Apr 2026 - 10 May 2026", "current": "10 May 2026 - 09 Jun 2026" }, "statistics": { "sales": { "previous": 27243.5, "current": 9697.53, "formatted_total": "$9,697.53", "progress": -64.32 }, "over_time": { "previous": [ { "label": "10 Apr", "total": 4200, "count": 6 } ], "current": [ { "label": "10 May", "total": 8500, "count": 12 } ] } } } ] - id: admin-reporting-sales-view title: Reporting — Sales (View Details) query: | curl -X GET "https://your-domain.com/api/admin/reporting/sales/view?type=total-sales" \ -H "Authorization: Bearer " response: | [ { "entity": "sales", "type": "total-sales", "dateRange": { "previous": "25 Mar - 24 Apr", "current": "25 Apr - 25 May" }, "statistics": { "columns": [ { "key": "date", "label": "Date" }, { "key": "total", "label": "Total" } ], "records": [ { "date": "2026-04-25", "total": "$1,240.00" }, { "date": "2026-04-26", "total": "$980.50" }, { "date": "2026-04-27", "total": "$1,510.75" } ] } } ] - id: admin-reporting-sales-export title: Reporting — Sales (Export CSV) query: | curl -X GET "https://your-domain.com/api/admin/reporting/sales/export?type=total-sales&format=csv" \ -H "Authorization: Bearer " \ -H "Accept: text/csv" \ -o sales-report.csv response: | Date,Total 2026-04-25,"$1,240.00" 2026-04-26,"$980.50" --- # Reporting — Sales | Endpoint | Method | |----------|--------| | `/api/admin/reporting/sales` | GET | | `/api/admin/reporting/sales/view` | GET | | `/api/admin/reporting/sales/export` | GET | ## Query Parameters | Param | Type | Notes | |-------|------|-------| | `type` | enum | `total-sales` (default), `average-sales`, `total-orders`, `purchase-funnel`, `abandoned-carts`, `refunds`, `tax-collected`, `shipping-collected`, `top-payment-methods`. | | `start` | date | Start date. | | `end` | date | End date. | | `channel` | string | Channel code. | ::: warning Helper-method output `statistics` payload is the raw helper aggregate keyed to the chosen `type`. ::: ## View Details `GET /api/admin/reporting/sales/view` returns the same statistics as the summary `stats` endpoint, but in a detailed table form — the full list that sits behind a panel's **View Details** link. The `statistics` object carries: - `columns` — an ordered list of `{ key, label }` describing each table column. - `records` — the row data, each keyed by the column `key` values. This is the expanded, row-by-row view; the summary `stats` endpoint returns the rolled-up headline figures instead. ## Export (CSV) `GET /api/admin/reporting/sales/export` streams the same detailed table as a `text/csv` attachment (the **Export** button). The header row is built from the column labels, followed by one line per record. Send `Accept: text/csv` and save the response to a file. Only `?format=csv` is accepted — any other `format` value returns HTTP 422. Both **View Details** and **Export** require only authentication; reporting has no permission gate. --- # Sales URL: /api/rest-api/admin/sales --- outline: false apiType: rest --- # Sales The Sales section covers everything about an order after (and during) checkout — browsing and managing orders, building an order from the admin side, and the post-order documents an order produces. ## Menus | Menu | What it's for | |------|----------------| | [Orders](/api/rest-api/admin/sales/orders/) | Browse orders and run every per-order action — view, reorder, place, cancel, comment, and generate invoices / shipments / refunds. Admins can also **build an order for a customer** from here. | | [Invoices](/api/rest-api/admin/sales/invoices/) | Store-wide list of generated invoices, plus print, send-duplicate, and bulk status update. | | [Shipments](/api/rest-api/admin/sales/shipments/) | Store-wide list of shipments created against orders. | | [Refunds](/api/rest-api/admin/sales/refunds/) | Store-wide list of refunds issued against orders. | | [Transactions](/api/rest-api/admin/sales/transactions/list) | Payment transactions recorded against orders and invoices. | | [Bookings](/api/rest-api/admin/sales/bookings/) | Booking lines produced by orders that contain a booking product. | Invoices, shipments, refunds, transactions, and bookings are **documents an order produces** — a row appears in those menus only once the corresponding document exists for an order. All Sales endpoints require an admin Bearer token — see [Authentication](/api/rest-api/admin/authentication). --- # Bookings URL: /api/rest-api/admin/sales/bookings --- outline: false apiType: rest --- # Bookings The Bookings menu is a read-only list of booking lines produced by orders that contain a booking product. It mirrors the admin **Sales → Bookings** screen. This menu is **read-only** — list and detail only; booking rows are created automatically when an order is placed and cannot be created or edited through the API. ## When a row appears here A row appears whenever an order is placed that contains a booking product — each booked line shows here. A booking row carries the booked quantity, the from/to time window, and a summary of the order and order item it belongs to. ## Booking sub-types The `bookingType` field tells you what kind of booking the line is — it comes from the booked product's configuration: | `bookingType` | Meaning | |---------------|---------| | `default` | A simple bookable product with a fixed availability window. | | `appointment` | A time-slot appointment (e.g. a service booking). | | `event` | A ticketed event. The booking also carries a `bookingProductEventTicketId` identifying which ticket type was booked. | | `rental` | A rental booked for a date/time range (hourly or daily). | | `table` | A table reservation (e.g. restaurant), for a party at a time. | ## The booking window The booked time window is returned twice: `from` / `to` as raw **unix timestamps** (integers, for programmatic use) and `fromFormatted` / `toFormatted` as readable strings. Some sub-types are not strictly time-windowed, so all four can be `null` for those rows. ## What the booking detail embeds In the admin, viewing a booking jumps to the underlying order view. The booking detail mirrors that by embedding the order's **billing and shipping address**, its **payment & shipping info** (payment method/title, shipping method/title), and its **invoices, shipments and refunds** — matching what the admin Booking view shows when it opens the order. Addresses can be `null` when the order has none, and the invoices/shipments/refunds arrays are empty when there are none. ## Endpoints in this menu | Action | Endpoint | |--------|----------| | [List bookings](/api/rest-api/admin/sales/bookings/list) | `GET /api/admin/bookings` | | [Get a single booking](/api/rest-api/admin/sales/bookings/detail) | `GET /api/admin/bookings/{id}` | All Bookings endpoints require the `sales.bookings.view` permission and an admin Bearer token — see [Authentication](/api/rest-api/admin/authentication). --- # Booking Detail URL: /api/rest-api/admin/sales/bookings/detail --- outline: false apiType: rest examples: - id: admin-booking-detail title: Booking Detail description: A single booking with its booking sub-type, time window, and the linked order / order-item summaries. query: | curl -X GET "https://your-domain.com/api/admin/bookings/1" \ -H "Authorization: Bearer " \ -H "Accept: application/json" response: | { "id": 1, "orderId": 8, "orderIncrementId": "00000000008", "orderItemId": 42, "productId": 99, "productSku": "BK-EVENT-01", "productName": "Concert Ticket", "bookingType": "event", "qty": 2, "from": 1716220800, "to": 1716224400, "fromFormatted": "20 May, 2026 12:00PM", "toFormatted": "20 May, 2026 13:00PM", "bookingProductEventTicketId": 5, "order": { "id": 8, "incrementId": "00000000008", "status": "processing", "customerName": "John Doe", "customerEmail": "john.doe@example.com", "grandTotal": 240, "orderCurrencyCode": "USD" }, "orderItem": { "id": 42, "sku": "BK-EVENT-01", "name": "Concert Ticket", "qtyOrdered": 2 }, "paymentMethod": "moneytransfer", "paymentTitle": "Money Transfer", "shippingMethod": null, "shippingTitle": null, "billingAddress": { "id": 4939, "addressType": "order_billing", "firstName": "John", "lastName": "Doe", "companyName": "Acme Inc.", "address": "123 Main St\nApt 4B", "city": "New York", "state": "NY", "country": "US", "postcode": "10001", "email": "john@example.com", "phone": "1234567890" }, "shippingAddress": null, "invoices": [ { "id": 559, "incrementId": "559", "state": "paid", "baseGrandTotal": 204, "formattedBaseGrandTotal": "$204.00", "createdAt": "2026-05-19 13:11:39" } ], "shipments": [], "refunds": [], "createdAt": "2026-05-20 10:00:00" } --- # Booking Detail Returns a single booking with its booking sub-type, the booked time window, the linked order / order-item summaries, and the underlying order's billing/shipping address, payment & shipping info, and its invoices / shipments / refunds — everything the admin Booking view shows when it opens the order, with no follow-up calls required. Requires the `sales.bookings.view` permission. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/bookings/{id}` | GET | ## Response fields | Field | Type | Description | |-------|------|-------------| | `id` | Integer | Booking id. | | `orderId` / `orderIncrementId` | Integer / String | Parent order id and human-facing number. | | `orderItemId` | Integer | The order line this booking belongs to. | | `productId` / `productSku` / `productName` | — | The booked product. | | `bookingType` | String | Booking sub-type: `default`, `appointment`, `event`, `rental`, `table`. | | `qty` | Integer | Booked quantity. | | `from` / `to` | Integer | Booked time window as **unix timestamps** (may be `null` for non-time-based types). | | `fromFormatted` / `toFormatted` | String | The same window as readable strings. | | `bookingProductEventTicketId` | Integer | Linked event-ticket id (set when `bookingType` is `event`). | | `order` | Object | Slim order summary — `id`, `incrementId`, `status`, `customerName`, `customerEmail`, `grandTotal`, `orderCurrencyCode`. | | `orderItem` | Object | Slim order-item summary — `id`, `sku`, `name`, `qtyOrdered`. | | `paymentMethod` / `paymentTitle` | String | The order's payment method code and its display title. | | `shippingMethod` / `shippingTitle` | String | The order's shipping method code and its display title — `null` when the order had no shipping method. | | `billingAddress` | Object | The order's billing address, or `null` when the order has none — see below. | | `shippingAddress` | Object | The order's shipping address, or `null` when the order has none — see below. | | `invoices` | Array | Slim summaries of the order's invoices — empty when none. See below. | | `shipments` | Array | Slim summaries of the order's shipments — empty when none. See below. | | `refunds` | Array | Slim summaries of the order's refunds — empty when none. See below. | | `createdAt` | String | When the order was created. | ### Address objects (`billingAddress`, `shippingAddress`) The order's billing and shipping addresses, mirroring the admin order view's address panel. Each is an object (or `null` when the order has none) carrying `id`, `addressType`, `firstName`, `lastName`, `companyName`, `address`, `city`, `state`, `country`, `postcode`, `email`, and `phone`. ### Invoices (`invoices`) Slim summaries of the order's invoices — an empty array when the order has none. | Field | Type | Description | |-------|------|-------------| | `id` | Integer | Invoice id. | | `incrementId` | String | Human-facing invoice number. | | `state` | String | Invoice state — e.g. `paid`, `pending`. | | `baseGrandTotal` | Number | Invoice grand total in the store's base currency. | | `formattedBaseGrandTotal` | String | The same total pre-formatted for display. | | `createdAt` | String | When the invoice was created. | ### Shipments (`shipments`) Slim summaries of the order's shipments — an empty array when the order has none. | Field | Type | Description | |-------|------|-------------| | `id` | Integer | Shipment id. | | `totalQty` | Number | Total quantity shipped. | | `carrierTitle` | String | Shipping carrier title — may be `null`. | | `trackNumber` | String | Carrier tracking number — may be `null`. | | `createdAt` | String | When the shipment was created. | ### Refunds (`refunds`) Slim summaries of the order's refunds — an empty array when the order has none. | Field | Type | Description | |-------|------|-------------| | `id` | Integer | Refund id. | | `state` | String | Refund state. | | `baseGrandTotal` | Number | Refund grand total in the store's base currency. | | `formattedBaseGrandTotal` | String | The same total pre-formatted for display. | | `createdAt` | String | When the refund was created. | ## Permission `sales.bookings.view` --- # Export Bookings URL: /api/rest-api/admin/sales/bookings/export --- outline: false apiType: rest examples: - id: admin-bookings-export title: Export Bookings (CSV) description: Download the bookings datagrid as a CSV file — the same data the admin Bookings "Export" button produces. Honours the same filters as the listing. query: | curl -X GET "https://your-domain.com/api/admin/bookings/export?format=csv" \ -H "Authorization: Bearer " \ -H "Accept: text/csv" \ --output bookings.csv response: | # Binary response: a text/csv attachment is written to bookings.csv # (Content-Disposition: attachment; filename="bookings.csv"). Sample contents: ID,"Order ID",Qty,From,To,"Booking Date" 17,2391,1,"20 May, 2026 13:00PM","20 May, 2026 14:00PM","2026-05-19 13:11:39" --- # Export Bookings Downloads the bookings datagrid as a **CSV file** — the same data the admin **Sales → Bookings** "Export" button produces. The response is a binary `text/csv` attachment, not JSON. Requires the `sales.bookings.view` permission. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/bookings/export` | GET | ## Columns The CSV carries the six datagrid columns, in order: | Header | Value | |--------|-------| | `ID` | Booking id. | | `Order ID` | The parent order's number. | | `Qty` | Quantity booked. | | `From` | The slot's start, formatted (empty for non-time-based booking types). | | `To` | The slot's end, formatted (empty for non-time-based booking types). | | `Booking Date` | When the booking was created. | ## Query parameters `format` selects the export format — **only `csv` is supported** (the default); any other value returns `422`. The export honours the **same filters as the [listing](/api/rest-api/admin/sales/bookings/list)**, so you export exactly the rows you're viewing: `id`, `order_id`, `qty`, `product_id`, `from_from` / `from_to`, `to_from` / `to_to`, `created_at_from` / `_to`. (Pagination does not apply — the export returns every matching row.) ## Permission `sales.bookings.view` --- # List Bookings URL: /api/rest-api/admin/sales/bookings/list --- outline: false apiType: rest examples: - id: admin-bookings-list title: List Bookings (Datagrid) description: One row per booking line. Every booking column plus the linked order / order-item summary is populated on each row. query: | curl -X GET "https://your-domain.com/api/admin/bookings?per_page=10" \ -H "Authorization: Bearer " \ -H "Accept: application/json" response: | { "data": [ { "id": 1, "orderId": 8, "orderIncrementId": "00000000008", "orderItemId": 42, "productId": 99, "productSku": "BK-EVENT-01", "productName": "Concert Ticket", "bookingType": "event", "qty": 2, "from": 1716220800, "to": 1716224400, "fromFormatted": "20 May, 2026 12:00PM", "toFormatted": "20 May, 2026 13:00PM", "bookingProductEventTicketId": 5, "order": { "id": 8, "incrementId": "00000000008", "status": "processing", "customerName": "John Doe", "customerEmail": "john.doe@example.com", "grandTotal": 240, "orderCurrencyCode": "USD" }, "orderItem": { "id": 42, "sku": "BK-EVENT-01", "name": "Concert Ticket", "qtyOrdered": 2 }, "createdAt": "2026-05-20 10:00:00" } ], "meta": { "currentPage": 1, "perPage": 10, "lastPage": 1, "total": 1, "from": 1, "to": 1 } } --- # List Bookings Mirrors the admin **Sales → Bookings** datagrid. Every booking **column** plus the linked `order` and `orderItem` summaries are populated on each row — the field set is identical to [Booking Detail](/api/rest-api/admin/sales/bookings/detail). ::: tip How this menu works For what a booking row is, the booking sub-types, and the time-window fields, see the [Bookings overview](/api/rest-api/admin/sales/bookings/). ::: ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/bookings` | GET | ## Query Parameters | Parameter | Type | Description | |-----------|------|-------------| | `page`, `per_page` | integer | Pagination (default `10`, cap `50`). | | `id` | string | Filter by booking id (int or comma-list). | | `order_id` | string | Partial order increment number. | | `qty` | integer | Exact quantity. | | `product_id` | integer | Filter by product id. | | `from_from`, `from_to` | date | Slot-start range. | | `to_from`, `to_to` | date | Slot-end range. | | `created_at_from` / `_to` | date | Order created range. | | `sort` | string | `id`, `order_id`, `qty`, `from`, `to`, `created_at`. | | `order` | string | `asc`, `desc`. | `from` and `to` are emitted as both **raw unix timestamps** (`from`, `to`) and pre-formatted strings (`fromFormatted`, `toFormatted`). For non-time-based booking types they may be `null`. ## Permission `sales.bookings.view` --- # Add Item to Cart URL: /api/rest-api/admin/sales/carts/add-item --- outline: false apiType: rest examples: - id: admin-cart-add-item title: Add Item to Cart description: Add a product (any type) to the admin draft cart. Body keys mirror the storefront add-to-cart payload so configurable, bundle, grouped, and downloadable products work without code changes. query: | curl -X POST "https://your-domain.com/api/admin/carts/314/items" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "productId": 142, "quantity": 1 }' variables: | { "productId": 142, "quantity": 1 } response: | { "id": 314, "itemsCount": 2, "itemsQty": 2, "grandTotal": 200, "items": [ /* ... full updated cart payload ... */ ], "success": true, "message": "Item added to cart." } commonErrors: - error: Bad Request (400) cause: productId missing solution: Send productId in the request body - error: Bad Request (400) cause: Booking product — admin draft orders do not support booking products solution: Booking purchases must be made through the customer storefront. Sample response: `{"type":"about:blank","title":"An error occurred","status":400,"detail":"Booking products cannot be added to an admin draft order. Booking purchases must be made through the customer storefront."}` - error: Not Found (404) cause: productId does not exist solution: Verify the product ID - error: Forbidden (403) cause: Cart is an active storefront cart solution: Use a draft cart (is_active = 0) --- # Add Item to Cart Adds a product to the draft cart. Each product type needs its own selection fields in addition to `productId` and `quantity`. The fields below are typed and work **identically over REST and GraphQL**: | Product type | Fields (besides `productId`, `quantity`) | |--------------|-------------------------------------------| | Simple / Virtual | — | | Configurable | `selectedConfigurableOption` — the chosen variant's product id | | Downloadable | `links` — array of downloadable-link ids | | Grouped | `groupedQuantities` — array of `{ productId, quantity }` | | Bundle | `bundleOptions` — array of `{ optionId, productIds, quantity }` | | Booking | not supported in admin Create-Order (returns `400`) | Example bundle body: ```json { "productId": 142, "quantity": 1, "bundleOptions": [ { "optionId": 5, "productIds": [10], "quantity": 1 }, { "optionId": 6, "productIds": [12], "quantity": 1 } ] } ``` ::: tip REST also accepts the raw storefront keys Because REST forwards the whole body to the cart, it additionally accepts the storefront snake_case shape — `selected_configurable_option`, `bundle_options` (map of `optionId => [productIds]`), `bundle_option_qty`, `qty` (map of `productId => quantity`), `links`. The typed fields above are recommended because they also work over GraphQL. ::: ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/carts/{id}/items` | POST | --- # Apply a Coupon URL: /api/rest-api/admin/sales/carts/apply-coupon --- outline: false apiType: rest examples: - id: admin-cart-apply-coupon title: Apply a Coupon description: Apply a cart-rule coupon code to the draft cart and recollect totals. Returns 404 for an unknown / inactive code, 422 if the same coupon is already applied. query: | curl -X POST "https://your-domain.com/api/admin/carts/314/coupon" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "code": "WELCOME10" }' variables: | { "code": "WELCOME10" } response: | { "id": 314, "couponCode": "WELCOME10", "discountAmount": 10, "grandTotal": 90, "success": true, "message": "Coupon applied." } commonErrors: - error: Bad Request (400) cause: code missing solution: Send a code value - error: Not Found (404) cause: Coupon does not exist or its rule is inactive solution: Verify the code and that the cart rule is enabled - error: Unprocessable Entity (422) cause: The same coupon is already applied to this cart solution: Remove first (DELETE /api/admin/carts/{id}/coupon) if you want to re-apply --- # Apply a Coupon Apply a cart-rule coupon to the draft cart. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/carts/{id}/coupon` | POST | --- # Get Cart URL: /api/rest-api/admin/sales/carts/get-cart --- outline: false apiType: rest examples: - id: admin-cart-get title: Get Cart description: Read an admin draft cart (items, totals, addresses, selected shipping/payment). Only `is_active = 0` (draft) carts are accessible; storefront carts return 403. query: | curl -X GET "https://your-domain.com/api/admin/carts/314" \ -H "Authorization: Bearer " variables: | {} response: | { "id": 314, "customerId": 19, "isGuest": false, "isActive": false, "itemsCount": 1, "itemsQty": 1, "subTotal": 100, "formattedSubTotal": "$100.00", "grandTotal": 100, "formattedGrandTotal": "$100.00", "shippingAmount": 0, "taxTotal": 0, "discountAmount": 0, "couponCode": null, "shippingMethod": null, "paymentMethod": null, "haveStockableItems": true, "items": [ { "id": 6711, "cartId": 314, "productId": 1, "sku": "COASTALBREEZEMENSHOODIE", "type": "simple", "name": "Coastal Breeze Men's Blue Zipper Hoodie", "quantity": 1, "price": 100, "total": 100, "additional": { "quantity": 1, "product_id": 1 }, "child": null, "children": [] } ], "billingAddress": null, "shippingAddress": null, "success": null, "message": null } commonErrors: - error: Not Found (404) cause: Unknown cart ID solution: Confirm the cart ID returned by Reorder - error: Forbidden (403) cause: Cart is an active storefront cart (is_active = 1) solution: This endpoint only mutates draft carts. Storefront carts are owned by the customer's session - error: Unauthorized (401) cause: Missing or invalid admin Bearer token solution: Send a valid admin Bearer token (Integration token) in the Authorization header. See the Authentication page. --- # Get Cart Returns the full draft-cart payload — line items (with type-specific `additional` data), totals, billing & shipping addresses, and the currently-selected shipping/payment method. The same payload shape is returned by every cart write operation, so the client never needs a refetch. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/carts/{id}` | GET | `{id}` is the draft cart id (typically the value returned by the Reorder action or a future Create-Cart endpoint). Requires an admin Bearer token. --- # List Payment Methods URL: /api/rest-api/admin/sales/carts/list-payment-methods --- outline: false apiType: rest examples: - id: admin-cart-list-payment-methods title: List Payment Methods description: List the payment methods supported for the draft cart. Requires a shipping method to be selected first. query: | curl -X GET "https://your-domain.com/api/admin/carts/314/payment-methods" \ -H "Authorization: Bearer " variables: | {} response: | { "data": [ { "method": "cashondelivery", "methodTitle": "Cash On Delivery", "description": "", "sort": 1, "image": null }, { "method": "moneytransfer", "methodTitle": "Money Transfer", "description": "", "sort": 2, "image": null } ], "meta": { "currentPage": 1, "perPage": 2, "lastPage": 1, "total": 2, "from": 1, "to": 2 } } commonErrors: - error: Conflict (409) cause: Shipping method not yet selected, or addresses missing solution: Save addresses then call `POST /api/admin/carts/{id}/shipping-methods` first - error: Not Found (404) cause: Unknown cart ID solution: Confirm the cart ID - error: Forbidden (403) cause: Cart is an active storefront cart solution: Only draft carts can be modified - error: Unauthorized (401) cause: Missing or invalid admin Bearer token solution: Send a valid admin Bearer token (Integration token) in the Authorization header. See the Authentication page. --- # List Payment Methods Returns the payment methods valid for the current draft cart. Only `cashondelivery` and `moneytransfer` are usable from `POST /api/admin/orders/place/{cartId}` (matches the admin Create-Order screen). ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/carts/{cartId}/payment-methods` | GET | ## Sequence A shipping method must already be selected on the cart, otherwise the response is HTTP 409 with *Shipping method must be selected before payment method.* --- # List Shipping Methods URL: /api/rest-api/admin/sales/carts/list-shipping-methods --- outline: false apiType: rest examples: - id: admin-cart-list-shipping-methods title: List Shipping Methods description: Returns the available shipping rates for a draft cart, flattened across carriers. Requires both addresses to already be saved on the cart. query: | curl -X GET "https://your-domain.com/api/admin/carts/314/shipping-methods" \ -H "Authorization: Bearer " variables: | {} response: | { "data": [ { "method": "flatrate_flatrate", "carrierCode": "flatrate", "carrierTitle": "Flat Rate", "methodTitle": "Fixed", "price": 10, "formattedPrice": "$10.00", "baseTotal": 10, "formattedBaseTotal": "$10.00" }, { "method": "free_free", "carrierCode": "free", "carrierTitle": "Free Shipping", "methodTitle": "Free Shipping", "price": 0, "formattedPrice": "$0.00", "baseTotal": 0, "formattedBaseTotal": "$0.00" } ], "meta": { "currentPage": 1, "perPage": 2, "lastPage": 1, "total": 2, "from": 1, "to": 2 } } commonErrors: - error: Conflict (409) cause: Cart is empty, or billing/shipping address not yet saved solution: Add at least one item and call `POST /api/admin/carts/{id}/addresses` first - error: Not Found (404) cause: Unknown cart ID solution: Confirm the cart ID returned by Create-Cart or Reorder - error: Forbidden (403) cause: Cart is an active storefront cart (is_active = 1) solution: This endpoint only works for draft carts - error: Unauthorized (401) cause: Missing or invalid admin Bearer token solution: Send a valid admin Bearer token (Integration token) in the Authorization header. See the Authentication page. --- # List Shipping Methods Returns the available shipping rates for the draft cart. The grouped result is flattened into one row per rate so the client can render a flat selector. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/carts/{cartId}/shipping-methods` | GET | ## Sequence The provider enforces the Create-Order sequence — both billing AND shipping addresses must already be saved on the cart, otherwise the response is HTTP 409 with `Addresses must be saved before selecting a shipping method.` --- # Remove Applied Coupon URL: /api/rest-api/admin/sales/carts/remove-coupon --- outline: false apiType: rest examples: - id: admin-cart-remove-coupon title: Remove Applied Coupon description: Remove the currently-applied coupon (if any) from the draft cart and recollect totals. Idempotent. query: | curl -X DELETE "https://your-domain.com/api/admin/carts/314/coupon" \ -H "Authorization: Bearer " variables: | {} response: | { "id": 314, "couponCode": null, "discountAmount": 0, "grandTotal": 100, "success": true, "message": "Coupon removed." } commonErrors: - error: Not Found (404) cause: Unknown cart ID solution: Verify the cart ID --- # Remove Applied Coupon Remove the currently-applied coupon from the draft cart. Safe to call when no coupon is applied — the underlying facade is a no-op in that case. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/carts/{id}/coupon` | DELETE | --- # Remove a Cart Item URL: /api/rest-api/admin/sales/carts/remove-item --- outline: false apiType: rest examples: - id: admin-cart-remove-item title: Remove a Cart Item description: Remove a single line item from the draft cart. Body carries the cartItemId. query: | curl -X DELETE "https://your-domain.com/api/admin/carts/314/items" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "cartItemId": 6711 }' variables: | { "cartItemId": 6711 } response: | { "id": 314, "itemsCount": 0, "items": [], "success": true, "message": "Item removed from cart." } commonErrors: - error: Bad Request (400) cause: cartItemId missing solution: Send `{ cartItemId }` in the request body --- # Remove a Cart Item Removes a single line item from the draft cart. The endpoint returns the **full updated cart** (HTTP 200), not 204 — the client can reuse it without a follow-up GET. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/carts/{id}/items` | DELETE | --- # Save Cart Addresses URL: /api/rest-api/admin/sales/carts/save-address --- outline: false apiType: rest examples: - id: admin-cart-save-address title: Save Cart Addresses description: Set the billing (and shipping unless billing.useForShipping is true) addresses on the draft cart and recollect totals. query: | curl -X POST "https://your-domain.com/api/admin/carts/314/addresses" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "billing": { "firstName": "Jane", "lastName": "Doe", "email": "jane@example.com", "address": ["12 Main St"], "city": "Berlin", "country": "DE", "state": "BE", "postcode": "10115", "phone": "+4930123456", "useForShipping": true } }' variables: | { "billing": { "firstName": "Jane", "lastName": "Doe", "email": "jane@example.com", "address": ["12 Main St"], "city": "Berlin", "country": "DE", "state": "BE", "postcode": "10115", "phone": "+4930123456", "useForShipping": true } } response: | { "id": 314, "billingAddress": { "firstName": "Jane", "lastName": "Doe", "address": "12 Main St", "city": "Berlin", "country": "DE", "state": "BE", "postcode": "10115" }, "shippingAddress": { /* same as billing when useForShipping = true */ }, "success": true, "message": "Address saved." } commonErrors: - error: Bad Request (400) cause: billing object missing or empty solution: Send at least a billing address - error: Unprocessable Entity (422) cause: a required address field is missing solution: Send every required field (see below) for billing — and for shipping when useForShipping is false --- # Save Cart Addresses Saves the billing (and optionally a separate shipping) address on the draft cart. CamelCase keys (`firstName`, `useForShipping`) are accepted and normalised to snake_case before being saved. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/carts/{id}/addresses` | POST | If `billing.useForShipping` is `true`, the `shipping` block is optional and the billing address is reused for shipping. ## Required fields Each address (billing always; shipping too when `useForShipping` is `false`) must include every one of these fields, otherwise the request is rejected with `422`: `firstName`, `lastName`, `email`, `address` (a non-empty array of street lines), `city`, `country`, `state`, `postcode`, `phone`. `companyName` is optional. --- # Set Payment Method URL: /api/rest-api/admin/sales/carts/set-payment-method --- outline: false apiType: rest examples: - id: admin-cart-set-payment-method title: Set Payment Method description: Save the selected payment method on the draft cart and recollect totals. query: | curl -X POST "https://your-domain.com/api/admin/carts/314/payment-methods" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "method": "cashondelivery" }' variables: | { "method": "cashondelivery" } response: | { "id": 314, "paymentMethod": "cashondelivery", "paymentMethodTitle": "Cash On Delivery", "grandTotal": 110, "success": true, "message": "Payment method saved." } commonErrors: - error: Conflict (409) cause: Shipping method not yet selected solution: 'Call `POST /api/admin/carts/{id}/shipping-methods` first' - error: Bad Request (400) cause: method field missing solution: 'Send `{ "method": "cashondelivery" }`' - error: Forbidden (403) cause: Cart is an active storefront cart solution: 'Only draft carts can be modified' - error: Unauthorized (401) cause: Missing or invalid admin Bearer token solution: Send a valid admin Bearer token (Integration token) in the Authorization header. See the Authentication page. --- # Set Payment Method Saves the chosen payment method on the cart and recollects totals. The response is the full updated cart. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/carts/{cartId}/payment-methods` | POST | ## Sequence Items + addresses + shipping method must be present. Otherwise the response is HTTP 409 with a precise message identifying the missing step. ## Note on supported methods For `POST /api/admin/orders/place/{cartId}` to succeed, the saved method must be one of `cashondelivery` or `moneytransfer` (matches the admin Create-Order screen). This endpoint will *save* any supported method, but place-order returns HTTP 422 for any other choice. --- # Set Shipping Method URL: /api/rest-api/admin/sales/carts/set-shipping-method --- outline: false apiType: rest examples: - id: admin-cart-set-shipping-method title: Set Shipping Method description: Save the selected shipping method on the draft cart and recollect totals. query: | curl -X POST "https://your-domain.com/api/admin/carts/314/shipping-methods" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "shippingMethod": "flatrate_flatrate" }' variables: | { "shippingMethod": "flatrate_flatrate" } response: | { "id": 314, "shippingMethod": "flatrate_flatrate", "shippingAmount": 10, "grandTotal": 110, "success": true, "message": "Shipping method saved." } commonErrors: - error: Conflict (409) cause: Addresses not yet saved on the cart solution: 'Call `POST /api/admin/carts/{id}/addresses` first' - error: Bad Request (400) cause: shippingMethod field missing solution: 'Send `{ "shippingMethod": "" }`' - error: Forbidden (403) cause: Cart is an active storefront cart solution: 'Only draft carts (is_active = 0) can be modified' - error: Unauthorized (401) cause: Missing or invalid admin Bearer token solution: Send a valid admin Bearer token (Integration token) in the Authorization header. See the Authentication page. --- # Set Shipping Method Saves the chosen shipping method on the cart and recollects totals. The response is the full updated cart so the UI can render new totals without another fetch. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/carts/{cartId}/shipping-methods` | POST | ## Sequence The processor enforces: items, billing AND shipping addresses must already be present on the cart. Otherwise the call returns HTTP 409 with a precise message — *Addresses must be saved before selecting a shipping method.* --- # Update Cart Item Quantities URL: /api/rest-api/admin/sales/carts/update-items --- outline: false apiType: rest examples: - id: admin-cart-update-items title: Update Cart Item Quantities description: Bulk-update line-item quantities. `qty` is a map of cart_item_id → new quantity. query: | curl -X PUT "https://your-domain.com/api/admin/carts/314/items" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "qty": { "6711": 3 } }' variables: | { "qty": { "6711": 3 } } response: | { "id": 314, "itemsQty": 3, "grandTotal": 300, "items": [ /* updated rows */ ], "success": true, "message": "Cart items updated." } commonErrors: - error: Bad Request (400) cause: qty missing or not an object solution: 'Send qty as `{ "": }`' --- # Update Cart Item Quantities Bulk-update line-item quantities on a draft cart. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/carts/{id}/items` | PUT | Body: `{ "qty": { "": , ... } }`. Quantities of `0` remove the line. --- # Invoices URL: /api/rest-api/admin/sales/invoices --- outline: false apiType: rest --- # Invoices The Invoices menu is the store-wide list of every invoice that has been generated across all orders, plus the actions you can take on a single invoice. It mirrors the admin **Sales → Invoices** screen. ## When a row appears here An invoice row exists only after an invoice has been **generated** for an order — created manually (the Create Invoice action) or auto-generated if your store enables auto-invoicing. Placing or paying for an order does not by itself create an invoice; until one is generated, the order has no row in this menu. ## `state` — what each value means In practice the grid shows just two badges — **Paid** and **Pending**. The "Overdue by N days" you usually see is a *derived countdown* shown under a **Pending** invoice (see [the countdown](#payment-due-date-the-red-countdown) below), **not** a stored state. | `state` | Badge | Meaning | |---------|-------|---------| | `paid` | green **Paid** | The invoiced amount has been **captured** — a payment transaction was recorded against it (cash-on-delivery, the "record transaction" option at create-invoice time, or a paid gateway). | | `pending` | yellow **Pending** | The invoice exists but the money has **not been captured yet**. This is the everyday non-paid state. | | `pending_payment` | yellow **Pending** | A programmatic variant of `pending` set by some payment flows; displayed identically. | | `overdue` | red **Overdue** | A status you can set **manually** via [Mass Update Status](/api/rest-api/admin/sales/invoices/mass-update-status). Rarely used day-to-day — the "Overdue by N days" the grid normally shows is the derived countdown under a **Pending** invoice, whose stored `state` is still `pending`. | There is **no `refunded` invoice state** — refunds are tracked separately under [Refunds](/api/rest-api/admin/sales/refunds/), not on an invoice's `state`. **Why an order can still read "pending" after you invoice it.** Generating an invoice records *what is owed*, not that it has been *paid*. The invoice (and the order's payment state) flips to `paid` only when a payment transaction is recorded against it. So an invoiced order whose payment hasn't been captured — e.g. an offline method, or an invoice generated without recording a transaction — stays `pending` until the money is taken. Expected behaviour, not a defect. ## Payment due date & the red countdown For a `pending` invoice the admin grid shows a small red line under the status: a countdown to the **payment due date**. That due date is the invoice's creation date plus your store's configured *payment term* — the "due duration" in days, set under **Sales → Invoice Settings → Payment Terms**. While the due date is in the future the line reads "N day(s) left"; once it passes it turns red and reads "**Overdue by N day(s)**". Either way the invoice's stored `state` is still `pending` — the countdown is a display only, never a separate state. The API returns the raw `state` and `createdAt`, so a client can reproduce the same line: `dueDate = createdAt + dueDuration(days)`, then compare to today. ## What each action does | Action | What it does | |--------|--------------| | **Print Invoice (PDF)** | Downloads a print-ready PDF of the invoice — a binary `application/pdf` attachment, ready to save or print. | | **Send Duplicate Invoice** | Re-sends the invoice email to the customer. You can override the recipient with a custom email; otherwise it goes to the order's customer email. Re-delivers a copy without regenerating the invoice. | | **Mass Update Status** | Bulk-sets the status flag (`pending` / `paid` / `overdue`) on the selected invoices. It is a **manual flag only** — it does **not** capture or reverse a payment, and it does not create a transaction. Use it to correct or annotate state, not to take money. | | **Export (CSV)** | Downloads the invoices grid — with your current filters applied — as a CSV file. | Invoice **creation** is not in this menu — it runs against an order; see [Create Invoice](/api/rest-api/admin/sales/orders/create-invoice). ## Endpoints in this menu | Action | Endpoint | |--------|----------| | [List invoices](/api/rest-api/admin/sales/invoices/list) | `GET /api/admin/invoices` | | [Get a single invoice](/api/rest-api/admin/sales/orders/get-invoice) | `GET /api/admin/invoices/{id}` | | [Print invoice (PDF)](/api/rest-api/admin/sales/orders/print-invoice) | `GET /api/admin/invoices/{id}/print` | | [Send duplicate invoice email](/api/rest-api/admin/sales/orders/send-duplicate-invoice) | `POST /api/admin/invoices/{id}/send-duplicate` | | [Mass update status](/api/rest-api/admin/sales/invoices/mass-update-status) | `POST /api/admin/invoices/mass-update-status` | | [Export invoices (CSV)](/api/rest-api/admin/sales/invoices/export) | `GET /api/admin/invoices/export` | All Invoice endpoints require the `sales.invoices.view` permission and an admin Bearer token — see [Authentication](/api/rest-api/admin/authentication). --- # Export Invoices URL: /api/rest-api/admin/sales/invoices/export --- outline: false apiType: rest examples: - id: admin-invoices-export title: Export Invoices (CSV) description: Download the invoices datagrid as a CSV file — the same data the admin Invoices "Export" button produces. Honours the same filters as the listing. query: | curl -X GET "https://your-domain.com/api/admin/invoices/export?format=csv" \ -H "Authorization: Bearer " \ -H "Accept: text/csv" \ --output invoices.csv response: | # Binary response: a text/csv attachment is written to invoices.csv # (Content-Disposition: attachment; filename="invoices.csv"). Sample contents: ID,"Order ID",Status,"Grand Total","Invoice Date" 587,2413,paid,$554.00,"2026-06-04 18:34:43" --- # Export Invoices Downloads the invoices datagrid as a **CSV file** — the same data the admin **Sales → Invoices** "Export" button produces. The response is a binary `text/csv` attachment, not JSON. Requires the `sales.invoices.view` permission. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/invoices/export` | GET | ## Columns The CSV carries the five datagrid columns, in order: | Header | Value | |--------|-------| | `ID` | Invoice number. | | `Order ID` | The parent order's number. | | `Status` | The invoice state (e.g. `paid`, `pending`). | | `Grand Total` | The invoice's total, in the store's base currency, formatted (e.g. `$554.00`). | | `Invoice Date` | When the invoice was created. | ## Query parameters `format` selects the export format — **only `csv` is supported** (the default); any other value returns `422`. The export honours the **same filters as the [listing](/api/rest-api/admin/sales/invoices/list)**, so you export exactly the rows you're viewing: `id`, `order_id`, `state`, `base_grand_total_from` / `_to`, `created_at_from` / `_to`. (Pagination does not apply — the export returns every matching row.) ## Permission `sales.invoices.view` --- # List Invoices URL: /api/rest-api/admin/sales/invoices/list --- outline: false apiType: rest examples: - id: admin-invoices-list title: List Invoices (Datagrid) description: Paginated invoices listing. Every invoice column is populated on each row. Returns a `{ data, meta }` envelope. query: | curl -X GET "https://your-domain.com/api/admin/invoices?per_page=10&state=paid" \ -H "Authorization: Bearer " \ -H "Accept: application/json" response: | { "data": [ { "id": 560, "incrementId": "560", "orderId": 2392, "orderIncrementId": "2392", "state": "paid", "emailSent": true, "totalQty": 1, "orderCurrencyCode": "USD", "baseCurrencyCode": "USD", "channelCurrencyCode": "USD", "subTotal": 4000, "formattedSubTotal": "$4,000.00", "baseSubTotal": 4000, "formattedBaseSubTotal": "$4,000.00", "subTotalInclTax": 4000, "formattedSubTotalInclTax": "$4,000.00", "baseSubTotalInclTax": 4000, "formattedBaseSubTotalInclTax": "$4,000.00", "grandTotal": 4000, "formattedGrandTotal": "$4,000.00", "baseGrandTotal": 4000, "formattedBaseGrandTotal": "$4,000.00", "taxAmount": 0, "formattedTaxAmount": "$0.00", "baseTaxAmount": 0, "formattedBaseTaxAmount": "$0.00", "discountAmount": 0, "formattedDiscountAmount": "$0.00", "baseDiscountAmount": 0, "formattedBaseDiscountAmount": "$0.00", "shippingAmount": 0, "formattedShippingAmount": "$0.00", "baseShippingAmount": 0, "formattedBaseShippingAmount": "$0.00", "shippingAmountInclTax": 0, "formattedShippingAmountInclTax": "$0.00", "baseShippingAmountInclTax": 0, "formattedBaseShippingAmountInclTax": "$0.00", "shippingTaxAmount": 0, "formattedShippingTaxAmount": "$0.00", "baseShippingTaxAmount": 0, "formattedBaseShippingTaxAmount": "$0.00", "transactionId": null, "reminders": 0, "nextReminderAt": null, "createdAt": "2026-05-19 13:13:30", "updatedAt": "2026-05-29 13:30:32", "orderStatus": "processing", "orderStatusLabel": "Processing", "orderDate": "2026-05-19 13:13:29", "channelName": "bagisto store", "customerName": "John Doe", "customerEmail": "john.doe@example.com", "billingAddress": { "id": 268, "addressType": "order_billing", "firstName": "John", "lastName": "Doe", "companyName": "Acme Trades", "address": "21 Market Street", "city": "Los Angeles", "state": "CA", "country": "US", "postcode": "90001", "email": "john.doe@example.com", "phone": "5551234567" }, "shippingAddress": { "id": 267, "addressType": "order_shipping", "firstName": "John", "lastName": "Doe", "companyName": "Acme Trades", "address": "21 Market Street", "city": "Los Angeles", "state": "CA", "country": "US", "postcode": "90001", "email": "john.doe@example.com", "phone": "5551234567" }, "items": [] } ], "meta": { "currentPage": 1, "perPage": 10, "lastPage": 57, "total": 562, "from": 1, "to": 10 } } --- # List Invoices Listing of every invoice across all orders, matching the admin Invoices grid. Every invoice **field** plus the `billingAddress` / `shippingAddress` objects are populated on each row — only the line `items` are left empty on the listing. Requires the `sales.invoices.view` permission. ::: tip How this menu works For the invoice `state` semantics, why a paid order can read "pending", the red payment-due countdown, and the print / send-duplicate / mass-status actions, see the [Invoices overview](/api/rest-api/admin/sales/invoices/). ::: ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/invoices` | GET | ## Query Parameters | Parameter | Type | Description | |-----------|------|-------------| | `page` | integer | Page number (1-based). | | `per_page` | integer | Items per page (default `10`, cap `50`). | | `id` | string | Filter by invoice id (integer or comma-separated list). | | `order_id` | string | Partial match on the parent order number. | | `state` | string | `pending`, `pending_payment`, `paid`, `overdue`. | | `base_grand_total_from` | number | Min grand total. | | `base_grand_total_to` | number | Max grand total. | | `created_at_from` | date | Created after (ISO date). | | `created_at_to` | date | Created before (ISO date). | | `sort` | string | `id` (default desc), `increment_id`, `order_id`, `base_grand_total`, `state`, `created_at`. | | `order` | string | `asc` or `desc`. | ## Row shape Each row carries the full invoice column set, order/customer context, and the billing & shipping addresses. The field reference is identical to [Get Invoice](/api/rest-api/admin/sales/orders/get-invoice) — the only difference is that the line `items` are empty (`[]`) on the listing. | Group | Fields | |-------|--------| | Identity | `id`, `incrementId`, `orderId`, `orderIncrementId`, `state`, `emailSent`, `totalQty` | | Currency codes | `orderCurrencyCode`, `baseCurrencyCode`, `channelCurrencyCode` | | Totals | `subTotal*`, `grandTotal*`, `taxAmount*`, `discountAmount*`, `shippingAmount*` — each in order + base currency, with `formatted*` and incl-tax variants | | Status & timestamps | `transactionId`, `reminders`, `nextReminderAt`, `createdAt`, `updatedAt` | | Order & customer | `orderStatus`, `orderStatusLabel`, `orderDate`, `channelName`, `customerName`, `customerEmail` | | Addresses | `billingAddress`, `shippingAddress` (objects) | | Line items (empty on listing) | `items` (`[]`) | ::: warning A `null` here means the DB is genuinely empty Listing rows return the actual stored value for every column. A `null` (e.g. `transactionId`, `customerName`, `baseCurrencyCode`) means that row has no value stored for it — not that the listing is withholding data. Only the line `items` are deliberately omitted on the listing. ::: ::: info For invoice **detail** + **PDF** + **create**, see the per-order endpoints under [Orders](/api/rest-api/admin/sales/orders/get-invoice). ::: --- # Mass Update Invoice Status URL: /api/rest-api/admin/sales/invoices/mass-update-status --- outline: false apiType: rest examples: - id: admin-invoices-mass-update-status title: Mass Update Invoice Status description: Bulk-set the status of several invoices at once. A manual status override — it does not capture or reverse a payment. query: | curl -X POST "https://your-domain.com/api/admin/invoices/mass-update-status" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d '{ "indices": [560, 561], "value": "paid" }' response: | { "updated": [560, 561], "message": "Invoice status updated successfully." } commonErrors: - error: Unprocessable Entity (422) cause: Empty indices, or value not one of pending/paid/overdue solution: Send a non-empty indices array and a value of pending, paid, or overdue. - error: Unauthorized (401) cause: Missing or invalid admin Bearer token solution: Send a valid admin Bearer token in the Authorization header. See the Authentication page. - error: Forbidden (403) cause: Admin role lacks the invoices permission solution: Use an admin whose role grants sales.invoices.view. --- # Mass Update Invoice Status Bulk-sets the status of a batch of invoices in one call — the API behind the admin Invoices datagrid's "Update Status" bulk action. Requires the `sales.invoices.view` permission. ::: warning This is a manual status override Setting an invoice to `paid` here **does not capture a payment**, and setting it to `pending` / `overdue` does **not** reverse one. It only changes the stored status flag. Use it to correct or annotate invoice states — not as a payment operation. ::: ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/invoices/mass-update-status` | POST | ## Request body | Field | Type | Description | |-------|------|-------------| | `indices` | integer[] | Ids of the invoices to update. Must be non-empty. | | `value` | string | New status — one of `pending`, `paid`, `overdue`. | ## Response | Field | Type | Description | |-------|------|-------------| | `updated` | integer[] | Ids that were updated. | | `message` | string | Success message. | Ids in `indices` that don't exist are silently skipped — `updated` lists only the ids that were actually changed. --- # Orders URL: /api/rest-api/admin/sales/orders --- outline: false apiType: rest --- # Orders The Orders menu is the heart of Sales: browse every order in the store and run every per-order action. From here you can view a single order in full, drive the admin **Create Order** flow (reorder an existing order, or place a prepared draft cart), run order lifecycle actions (cancel an order, add and list its comments), and generate the documents an order produces — invoices, shipments, and refunds. It mirrors the admin **Sales → Orders** screen. Invoices, shipments, and refunds are **generated from an order** here, but each also has its own store-wide menu listing every such document across all orders: see the [Invoices](/api/rest-api/admin/sales/invoices/), [Shipments](/api/rest-api/admin/sales/shipments/), and [Refunds](/api/rest-api/admin/sales/refunds/) overviews. ## Creating an order for a customer (admin Create Order) An admin can place an order **on behalf of a customer** — the same "Create Order" flow as the admin panel. It works through a **draft cart**: create a draft cart for the customer, add products to it, save the billing/shipping addresses, choose a shipping method and a payment method, then place the order. The draft cart is an internal building block of order creation, not a separate menu. | Step | Endpoint | |------|----------| | [Start a draft cart for a customer](/api/rest-api/admin/customers/create-draft-cart) | `POST /api/admin/customers/{customerId}/draft-carts` | | [Get the draft cart](/api/rest-api/admin/sales/carts/get-cart) | `GET /api/admin/carts/{id}` | | [Add an item](/api/rest-api/admin/sales/carts/add-item) · [update](/api/rest-api/admin/sales/carts/update-items) · [remove](/api/rest-api/admin/sales/carts/remove-item) | `.../carts/{id}/items` | | [Save addresses](/api/rest-api/admin/sales/carts/save-address) | `POST /api/admin/carts/{id}/addresses` | | [List](/api/rest-api/admin/sales/carts/list-shipping-methods) / [set](/api/rest-api/admin/sales/carts/set-shipping-method) shipping method | `.../carts/{id}/shipping-methods` | | [List](/api/rest-api/admin/sales/carts/list-payment-methods) / [set](/api/rest-api/admin/sales/carts/set-payment-method) payment method | `.../carts/{id}/payment-methods` | | [Place the order](/api/rest-api/admin/sales/orders/place-order) | `POST /api/admin/orders/place/{cartId}` | (There's also [apply](/api/rest-api/admin/sales/carts/apply-coupon) / [remove coupon](/api/rest-api/admin/sales/carts/remove-coupon) on the draft cart.) [Reorder](/api/rest-api/admin/sales/orders/reorder) is a shortcut that seeds a fresh draft cart from an existing order's items. ::: tip Only saleable products can be added [Add Item](/api/rest-api/admin/sales/carts/add-item) accepts only products that are in stock and enabled. Adding an out-of-stock or disabled product returns a clear error and **leaves the draft cart intact** so you can add a different product — the cart is never lost. Booking products can't be added to an admin order (no admin Create-Order surface for them). ::: ## The order lifecycle — which action, in what order Once an order exists (placed through Create Order above, [Reorder](/api/rest-api/admin/sales/orders/reorder), or the storefront), it moves through a lifecycle. Each action has prerequisites — this is the order they run in and what gates each one. **1. Invoice — record payment.** [Create Invoice](/api/rest-api/admin/sales/orders/create-invoice) records that payment was collected for some or all of the order's items. An order generally can't be refunded until it has been invoiced (you refund money that was billed). You can invoice part of an order now and the rest later. Not available for orders paid via `paypal_standard` (those are captured by the gateway, not the admin). **2. Ship — fulfil.** [Create Shipment](/api/rest-api/admin/sales/orders/create-shipment) marks items as dispatched and records the carrier and tracking number. It needs items still awaiting shipment and enough stock at the chosen inventory source. Partial shipments are allowed. **3. Refund — return money.** [Create Refund](/api/rest-api/admin/sales/orders/create-refund) returns money for invoiced items and/or an arbitrary adjustment. Call [Refund Preview](/api/rest-api/admin/sales/orders/refund-preview) first to see the computed totals without writing anything. Requires something left to refund (an un-refunded invoiced amount or a returnable quantity). **Cancel — abandon early.** [Cancel Order](/api/rest-api/admin/sales/orders/cancel) is only possible while there is still something to cancel (nothing has been fully invoiced or shipped). A closed or fraud-flagged order can't be cancelled. **Comments — any time.** [Add Comment](/api/rest-api/admin/sales/orders/add-comment) / [List Comments](/api/rest-api/admin/sales/orders/list-comments) work at any stage; set `customerNotified` to email the customer the note. Every action refuses with a clear error when its prerequisite isn't met — nothing left to invoice/ship/refund, the order is already closed or flagged, insufficient stock, or a payment method that can't be invoiced. A typical fulfilled order runs **Create → Invoice → Ship** (then an optional **Refund**); an abandoned one runs **Create → Cancel**. ## Endpoints in this menu | Action | Endpoint | |--------|----------| | [List Orders](/api/rest-api/admin/sales/orders/list-orders) | `GET /api/admin/orders` | | [Order Detail](/api/rest-api/admin/sales/orders/order-detail) | `GET /api/admin/orders/{id}` | | [Reorder](/api/rest-api/admin/sales/orders/reorder) | `POST /api/admin/orders/{id}/reorder` | | [Place Order](/api/rest-api/admin/sales/orders/place-order) | `POST /api/admin/orders/place/{cartId}` | | [Cancel Order](/api/rest-api/admin/sales/orders/cancel) | `POST /api/admin/orders/{id}/cancel` | | [Add Comment](/api/rest-api/admin/sales/orders/add-comment) | `POST /api/admin/orders/{id}/comments` | | [List Comments](/api/rest-api/admin/sales/orders/list-comments) | `GET /api/admin/orders/{id}/comments` | | [Create Invoice](/api/rest-api/admin/sales/orders/create-invoice) | `POST /api/admin/orders/{id}/invoices` | | [Create Shipment](/api/rest-api/admin/sales/orders/create-shipment) | `POST /api/admin/orders/{id}/shipments` | | [Create Refund](/api/rest-api/admin/sales/orders/create-refund) | `POST /api/admin/orders/{id}/refunds` | | [Refund Preview](/api/rest-api/admin/sales/orders/refund-preview) | `POST /api/admin/orders/{id}/refunds/preview` | All Orders endpoints require an admin Bearer token — see [Authentication](/api/rest-api/admin/authentication). --- # Add Order Comment URL: /api/rest-api/admin/sales/orders/add-comment --- outline: false apiType: rest examples: - id: admin-add-order-comment title: Add Order Comment description: Add a comment to an order. When `customerNotified` is true, the customer is sent a notification email with the comment. query: | curl -X POST "https://your-domain.com/api/admin/orders/2392/comments" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "comment": "Customer called to confirm shipping address.", "customerNotified": true }' variables: | { "comment": "Customer called to confirm shipping address.", "customerNotified": true } response: | { "id": 17, "orderId": 2392, "comment": "Customer called to confirm shipping address.", "customerNotified": true, "createdAt": "2026-05-21 10:14:31", "updatedAt": "2026-05-21 10:14:31" } commonErrors: - error: Empty comment (422) cause: '`comment` field missing or blank' solution: Send a non-empty comment string - error: Not Found (404) cause: Unknown order ID solution: Verify the order ID - error: Unauthorized (401) cause: Missing or invalid admin Bearer token solution: Send a valid admin Bearer token (Integration token) in the Authorization header. See the Authentication page. --- # Add Order Comment Adds a comment to an order. When `customerNotified=true`, the customer is sent a notification email with the comment. **No permission gate** — any authenticated admin can add a comment. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/orders/{orderId}/comments` | POST | ## Request body | Field | Type | Required | Notes | |-------|------|----------|-------| | `comment` | string | yes | Free-form comment body. | | `customerNotified` | boolean | no | When `true`, the customer is emailed the comment. Defaults to `false`. | ## Errors | HTTP | Lang key | Message | |------|----------|---------| | 422 | `bagistoapi::app.admin.order.actions.comment.empty` | Comment is required. | ### Sample 422 response ```json { "type": "/errors/422", "title": "Bad Request", "status": 422, "detail": "Comment is required." } ``` --- # Cancel Order URL: /api/rest-api/admin/sales/orders/cancel --- outline: false apiType: rest examples: - id: admin-cancel-order title: Cancel Order description: Cancel every cancellable item on an order. Returns the updated `OrderDetail` so the client can refresh the order-view screen without a follow-up GET. query: | curl -X POST "https://your-domain.com/api/admin/orders/2392/cancel" \ -H "Authorization: Bearer " variables: | {} response: | { "id": 2392, "incrementId": "1000002392", "status": "canceled", "grandTotal": 149.99, "items": [ { "id": 42, "sku": "WS-12-S", "qtyOrdered": 1, "qtyCanceled": 1 } ] } commonErrors: - error: Closed (422) cause: Order is already closed solution: Closed orders cannot be canceled — pick a different action - error: Fraud (422) cause: Order is flagged as fraud solution: Resolve the fraud flag before retrying - error: Nothing to cancel (422) cause: Every item has already been invoiced / shipped / canceled solution: No further cancellation is possible on this order - error: No permission (422) cause: Admin's role lacks `sales.orders.cancel` solution: Grant the role the `sales.orders.cancel` permission - error: Not Found (404) cause: Unknown order ID solution: Verify the order ID - error: Unauthorized (401) cause: Missing or invalid admin Bearer token solution: Send a valid admin Bearer token (Integration token) in the Authorization header. See the Authentication page. --- # Cancel Order Cancels every cancellable item on an order. This is the same action as the **Cancel** button on the admin order-view screen, with the same eligibility gates. The response is the full updated order detail, so the client can refresh the screen without a follow-up GET. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/orders/{id}/cancel` | POST | The request body is empty — the order is identified by the URL. ## Errors The endpoint enforces the same 4 eligibility checks the admin panel uses. Each failure returns **HTTP 422** with the matching message: | Condition | Lang key | Message | |-----------|----------|---------| | Order is `closed` | `bagistoapi::app.admin.order.actions.cancel.closed` | Closed orders cannot be canceled. | | Order is `fraud` | `bagistoapi::app.admin.order.actions.cancel.fraud` | Fraud orders cannot be canceled. | | No item has `qty_to_cancel > 0` | `bagistoapi::app.admin.order.actions.cancel.nothing-to-cancel` | There is nothing to cancel on this order. | | Admin role lacks `sales.orders.cancel` | `bagistoapi::app.admin.order.actions.cancel.no-permission` | You do not have permission to cancel orders. | ### Sample 422 response ```json { "type": "/errors/422", "title": "Bad Request", "status": 422, "detail": "There is nothing to cancel on this order." } ``` --- # Create Invoice URL: /api/rest-api/admin/sales/orders/create-invoice --- outline: false apiType: rest examples: - id: admin-create-invoice title: Create Invoice description: Create an invoice for one or more order items. Quantity is validated against `qty_to_invoice`, and a per-SKU error is returned when the requested quantity exceeds what remains. query: | curl -X POST "https://your-domain.com/api/admin/orders/2392/invoices" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "items": [ { "orderItemId": 42, "quantity": 3 }, { "orderItemId": 43, "quantity": 1 } ] }' variables: | { "items": [ { "orderItemId": 42, "quantity": 3 }, { "orderItemId": 43, "quantity": 1 } ] } response: | { "id": 88, "incrementId": "100000088", "orderId": 2392, "state": "paid", "emailSent": false, "totalQty": 4, "orderCurrencyCode": "USD", "subTotal": 119.96, "formattedSubTotal": "$119.96", "grandTotal": 129.96, "formattedGrandTotal": "$129.96", "taxAmount": 10.0, "formattedTaxAmount": "$10.00", "discountAmount": 0.0, "formattedDiscountAmount": "$0.00", "shippingAmount": 0.0, "formattedShippingAmount": "$0.00", "transactionId": null, "createdAt": "2026-05-21 10:32:01", "updatedAt": "2026-05-21 10:32:01", "items": [ { "id": 901, "orderItemId": 42, "sku": "WS-12-S", "name": "Argus All-Weather Tank-S", "qty": 3, "price": 29.99, "formattedPrice": "$29.99", "total": 89.97, "formattedTotal": "$89.97" } ] } commonErrors: - error: Closed (422) cause: Order is already closed solution: Closed orders cannot be invoiced - error: Fraud (422) cause: Order is flagged as fraud solution: Resolve the fraud flag before invoicing - error: PayPal Standard (422) cause: Order was paid through PayPal Standard solution: Invoices cannot be created via admin for PayPal Standard orders - error: Nothing to invoice (422) cause: Every item has already been fully invoiced solution: No further invoice can be created - error: No permission (422) cause: Admin role lacks `sales.invoices.create` solution: Grant the role the `sales.invoices.create` permission - error: Items required (422) cause: '`items` array missing, empty, or every quantity is zero' solution: Provide at least one `{ orderItemId, quantity > 0 }` entry - error: Quantity exceeds (422) cause: Requested quantity for an SKU is greater than `qty_to_invoice` solution: Lower the quantity to at most `qty_to_invoice` - error: Not Found (404) cause: Unknown order ID solution: Verify the order ID - error: Unauthorized (401) cause: Missing or invalid admin Bearer token solution: Send a valid admin Bearer token (Integration token) in the Authorization header. See the Authentication page. --- # Create Invoice Creates an invoice for one or more order items. The same eligibility checks as the admin Invoice screen apply (the order must not be closed, marked fraud, or paid through PayPal Standard). Each item's requested quantity is validated against its still-invoiceable quantity, `qty_to_invoice` (rejected per-SKU with a message carrying the requested vs available quantity) before the invoice is created. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/orders/{orderId}/invoices` | POST | ## Request body | Field | Type | Required | Notes | |-------|------|----------|-------| | `items` | array of `{ orderItemId, quantity }` | yes | At least one entry with `quantity > 0`. | ## Errors | HTTP | Lang key | Message | |------|----------|---------| | 422 | `bagistoapi::app.admin.order.actions.invoice.closed` | Closed orders cannot be invoiced. | | 422 | `bagistoapi::app.admin.order.actions.invoice.fraud` | Fraud orders cannot be invoiced. | | 422 | `bagistoapi::app.admin.order.actions.invoice.paypal-standard-blocked` | Invoices cannot be created for orders paid through PayPal Standard. | | 422 | `bagistoapi::app.admin.order.actions.invoice.nothing-to-invoice` | There is nothing to invoice on this order. | | 422 | `bagistoapi::app.admin.order.actions.invoice.no-permission` | You do not have permission to create invoices. | | 422 | `bagistoapi::app.admin.order.actions.invoice.items-required` | At least one item with a positive quantity is required. | | 422 | `bagistoapi::app.admin.order.actions.invoice.qty-exceeds` | Requested quantity (`:requested`) exceeds available quantity (`:available`) for SKU `:sku`. | | 422 | `bagistoapi::app.admin.order.actions.invoice.failed` | Could not create the invoice. | ### Sample 422 response ```json { "type": "/errors/422", "title": "Bad Request", "status": 422, "detail": "Requested quantity (5) exceeds available quantity (3) for SKU WS-12-S." } ``` --- # Create Refund URL: /api/rest-api/admin/sales/orders/create-refund --- outline: false apiType: rest examples: - id: admin-create-refund title: Create Refund description: Refund one or more order items, with optional shipping refund and adjustment fee/refund. Quantity is validated against `qty_to_refund`, and the computed total is checked against the maximum refundable amount before saving. query: | curl -X POST "https://your-domain.com/api/admin/orders/2392/refunds" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "items": [ { "orderItemId": 42, "quantity": 1 } ], "shipping": 0, "adjustmentRefund": 0, "adjustmentFee": 0 }' variables: | { "items": [ { "orderItemId": 42, "quantity": 1 } ], "shipping": 0, "adjustmentRefund": 0, "adjustmentFee": 0 } response: | { "id": 22, "orderId": 2392, "state": "refunded", "totalQty": 1, "orderCurrencyCode": "USD", "subTotal": 29.99, "formattedSubTotal": "$29.99", "grandTotal": 32.99, "formattedGrandTotal": "$32.99", "shippingAmount": 0.0, "formattedShippingAmount": "$0.00", "adjustmentRefund": 0.0, "formattedAdjustmentRefund": "$0.00", "adjustmentFee": 0.0, "formattedAdjustmentFee": "$0.00", "taxAmount": 3.0, "formattedTaxAmount": "$3.00", "discountAmount": 0.0, "formattedDiscountAmount": "$0.00", "createdAt": "2026-05-21 11:48:10", "updatedAt": "2026-05-21 11:48:10", "items": [ { "id": 701, "orderItemId": 42, "sku": "WS-12-S", "name": "Argus All-Weather Tank-S", "qty": 1, "total": 29.99, "formattedTotal": "$29.99" } ] } commonErrors: - error: Closed (422) cause: Order is already closed solution: Closed orders cannot be refunded - error: Fraud (422) cause: Order is flagged as fraud solution: Resolve the fraud flag before refunding - error: Nothing to refund (422) cause: No item has `qty_to_refund > 0` and no outstanding balance remains solution: Nothing further can be refunded on this order - error: No permission (422) cause: Admin role lacks `sales.refunds.create` solution: Grant the role the `sales.refunds.create` permission - error: Quantity exceeds (422) cause: Requested quantity for an SKU is greater than `qty_to_refund` solution: Lower the quantity to at most `qty_to_refund` - error: Amount zero (422) cause: Computed refund total resolves to zero solution: Adjust quantity, shipping, or adjustment-refund values - error: Amount exceeds maximum (422) cause: Computed refund total exceeds the order's remaining refundable amount solution: Lower quantities or adjustment-refund value - error: Not Found (404) cause: Unknown order ID solution: Verify the order ID - error: Unauthorized (401) cause: Missing or invalid admin Bearer token solution: Send a valid admin Bearer token (Integration token) in the Authorization header. See the Authentication page. --- # Create Refund Refunds one or more order items, with an optional shipping refund and an adjustment fee/refund. The same eligibility checks as the admin Refund screen apply (the order must not be closed or marked fraud, and each item's requested quantity must be ≤ its still-refundable quantity, `qty_to_refund`). The refund totals are computed from the items + shipping + adjustments, and the total cannot exceed the order's remaining refundable balance. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/orders/{orderId}/refunds` | POST | ## Request body | Field | Type | Required | Notes | |-------|------|----------|-------| | `items` | array of `{ orderItemId, quantity }` | yes | Items to refund. | | `shipping` | float | no (default `0`) | Original shipping to refund (base currency). | | `adjustmentRefund` | float | no (default `0`) | Positive adjustment added to the refund total. | | `adjustmentFee` | float | no (default `0`) | Fee subtracted from the refund total. | ## Errors | HTTP | Lang key | Message | |------|----------|---------| | 422 | `bagistoapi::app.admin.order.actions.refund.closed` | Closed orders cannot be refunded. | | 422 | `bagistoapi::app.admin.order.actions.refund.fraud` | Fraud orders cannot be refunded. | | 422 | `bagistoapi::app.admin.order.actions.refund.nothing-to-refund` | There is nothing left to refund on this order. | | 422 | `bagistoapi::app.admin.order.actions.refund.no-permission` | You do not have permission to create refunds. | | 422 | `bagistoapi::app.admin.order.actions.refund.qty-exceeds` | Requested quantity (`:requested`) exceeds available quantity (`:available`) for SKU `:sku`. | | 422 | `bagistoapi::app.admin.order.actions.refund.amount-zero` | The computed refund amount is zero. Adjust quantity, shipping or adjustment values. | | 422 | `bagistoapi::app.admin.order.actions.refund.amount-exceeds-max` | The refund amount (`:amount`) exceeds the maximum refundable amount (`:max`). | | 422 | `bagistoapi::app.admin.order.actions.refund.failed` | Could not create the refund. | > Tip: use **Refund Preview** (`POST /api/admin/orders/{orderId}/refunds/preview`) > to validate the computed totals without saving. --- # Create Shipment URL: /api/rest-api/admin/sales/orders/create-shipment --- outline: false apiType: rest examples: - id: admin-create-shipment title: Create Shipment description: Ship one or more order items from a chosen inventory source. Quantity is validated against `qty_to_ship` and against the available inventory at the chosen source. query: | curl -X POST "https://your-domain.com/api/admin/orders/2392/shipments" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "source": 1, "items": [ { "orderItemId": 42, "inventorySourceId": 1, "quantity": 3 } ], "carrierTitle": "UPS", "trackNumber": "1Z999AA1" }' variables: | { "source": 1, "items": [ { "orderItemId": 42, "inventorySourceId": 1, "quantity": 3 } ], "carrierTitle": "UPS", "trackNumber": "1Z999AA1" } response: | { "id": 55, "orderId": 2392, "status": "1", "totalQty": 3, "totalWeight": 1.2, "carrierCode": null, "carrierTitle": "UPS", "trackNumber": "1Z999AA1", "emailSent": false, "inventorySourceId": 1, "inventorySourceName": "Default", "createdAt": "2026-05-21 11:02:18", "updatedAt": "2026-05-21 11:02:18", "items": [ { "id": 401, "orderItemId": 42, "sku": "WS-12-S", "name": "Argus All-Weather Tank-S", "qty": 3 } ] } commonErrors: - error: Closed (422) cause: Order is already closed solution: Closed orders cannot be shipped - error: Fraud (422) cause: Order is flagged as fraud solution: Resolve the fraud flag before shipping - error: Nothing to ship (422) cause: No item has `qty_to_ship > 0` and is stockable solution: All shippable items are already shipped - error: No permission (422) cause: Admin role lacks `sales.shipments.create` solution: Grant the role the `sales.shipments.create` permission - error: Source required (422) cause: '`source` field missing or non-positive' solution: Send the inventory source id all items ship from - error: Items required (422) cause: '`items` array missing, empty, or every quantity is zero' solution: Provide at least one `{ orderItemId, inventorySourceId, quantity > 0 }` entry - error: Quantity exceeds (422) cause: Requested quantity for an SKU is greater than `qty_to_ship` solution: Lower the quantity to at most `qty_to_ship` - error: Inventory insufficient (422) cause: The selected source does not stock enough of an SKU solution: Pick a different source or split the shipment - error: Not Found (404) cause: Unknown order ID solution: Verify the order ID - error: Unauthorized (401) cause: Missing or invalid admin Bearer token solution: Send a valid admin Bearer token (Integration token) in the Authorization header. See the Authentication page. --- # Create Shipment Ships one or more order items from a chosen inventory source. The same eligibility checks as the admin Shipment screen apply (the order must not be closed or marked fraud). Each item's requested quantity is validated against its still-shippable quantity, `qty_to_ship`, AND against the inventory available at the chosen source before the shipment is created. For **composite products** (bundle, configurable, grouped), the requested quantity is split across the line's component items, and each component's own shippable quantity and stock at the chosen source are validated — so a shipment can't be created for more component stock than is physically available. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/orders/{orderId}/shipments` | POST | ## Request body | Field | Type | Required | Notes | |-------|------|----------|-------| | `source` | integer | yes | Inventory source id all items ship from. | | `items` | array of `{ orderItemId, inventorySourceId, quantity }` | yes | At least one entry with `quantity > 0`. | | `carrierTitle` | string | no | Free-form carrier label (e.g. `UPS`). | | `trackNumber` | string | no | Tracking number. | ## Errors | HTTP | Lang key | Message | |------|----------|---------| | 422 | `bagistoapi::app.admin.order.actions.shipment.closed` | Closed orders cannot be shipped. | | 422 | `bagistoapi::app.admin.order.actions.shipment.fraud` | Fraud orders cannot be shipped. | | 422 | `bagistoapi::app.admin.order.actions.shipment.nothing-to-ship` | There is nothing to ship on this order. | | 422 | `bagistoapi::app.admin.order.actions.shipment.no-permission` | You do not have permission to ship orders. | | 422 | `bagistoapi::app.admin.order.actions.shipment.source-required` | Inventory source is required. | | 422 | `bagistoapi::app.admin.order.actions.shipment.items-required` | At least one item with a positive quantity is required. | | 422 | `bagistoapi::app.admin.order.actions.shipment.qty-exceeds` | Requested quantity (`:requested`) exceeds available quantity (`:available`) for SKU `:sku`. | | 422 | `bagistoapi::app.admin.order.actions.shipment.inventory-insufficient` | Inventory at the selected source is insufficient for SKU `:sku`. | | 422 | `bagistoapi::app.admin.order.actions.shipment.failed` | Could not create the shipment. | ### Sample 422 response ```json { "type": "/errors/422", "title": "Bad Request", "status": 422, "detail": "Inventory at the selected source is insufficient for SKU WS-12-S." } ``` --- # Export Orders URL: /api/rest-api/admin/sales/orders/export --- outline: false apiType: rest examples: - id: admin-orders-export title: Export Orders (CSV) description: Download the orders datagrid as a CSV file — the same data the admin Orders "Export" button produces. Honours the same filters as the listing. query: | curl -X GET "https://your-domain.com/api/admin/orders/export?format=csv" \ -H "Authorization: Bearer " \ -H "Accept: text/csv" \ --output orders.csv response: | # Binary response: a text/csv attachment is written to orders.csv # (Content-Disposition: attachment; filename="orders.csv"). Sample contents: ID,Status,"Grand Total","Payment Method",Channel,Customer,Email,"Order Date" 2413,processing,$554.00,"Money Transfer","Default Channel","John Doe",john@example.com,"2026-06-04 18:34:43" --- # Export Orders Downloads the orders datagrid as a **CSV file** — the same data the admin **Sales → Orders** "Export" button produces. The response is a binary `text/csv` attachment, not JSON. Requires the `sales.orders.view` permission. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/orders/export` | GET | ## Columns The CSV carries the eight datagrid columns, in order: | Header | Value | |--------|-------| | `ID` | Order number. | | `Status` | The order status (e.g. `processing`, `completed`). | | `Grand Total` | The order's total, in the store's base currency, formatted (e.g. `$554.00`). | | `Payment Method` | The payment method used (e.g. `Money Transfer`). | | `Channel` | The channel the order was placed on. | | `Customer` | The customer's name. | | `Email` | The customer's email. | | `Order Date` | When the order was placed. | ## Query parameters `format` selects the export format — **only `csv` is supported** (the default); any other value returns `422`. The export honours the **same filters as the [listing](/api/rest-api/admin/sales/orders/list-orders)**, so you export exactly the rows you're viewing: `order_id`, `status`, `grand_total` (plus the `grand_total_from` / `_to` range), `channel`, `customer`, `email`, plus the date presets (`today`, `yesterday`, `this_week`, `this_month`, `last_month`, `last_three_months`, `last_six_months`, `this_year`) and custom `date_from` / `date_to`. (Pagination does not apply — the export returns every matching row.) ## Permission `sales.orders.view` --- # Get Invoice URL: /api/rest-api/admin/sales/orders/get-invoice --- outline: false apiType: rest examples: - id: admin-get-invoice title: Get Invoice description: Fetch a single invoice with the full totals breakdown, order/customer context, billing & shipping addresses, and embedded line items. query: | curl -X GET "https://your-domain.com/api/admin/invoices/1" \ -H "Authorization: Bearer " \ -H "Accept: application/json" variables: | {} response: | { "id": 1, "incrementId": "1", "orderId": 58, "orderIncrementId": "58", "state": "paid", "emailSent": true, "totalQty": 2, "orderCurrencyCode": "USD", "baseCurrencyCode": "USD", "channelCurrencyCode": "USD", "subTotal": 8000, "formattedSubTotal": "$8,000.00", "baseSubTotal": 8000, "formattedBaseSubTotal": "$8,000.00", "subTotalInclTax": 8000, "formattedSubTotalInclTax": "$8,000.00", "baseSubTotalInclTax": 8000, "formattedBaseSubTotalInclTax": "$8,000.00", "grandTotal": 8000, "formattedGrandTotal": "$8,000.00", "baseGrandTotal": 8000, "formattedBaseGrandTotal": "$8,000.00", "taxAmount": 0, "formattedTaxAmount": "$0.00", "baseTaxAmount": 0, "formattedBaseTaxAmount": "$0.00", "discountAmount": 0, "formattedDiscountAmount": "$0.00", "baseDiscountAmount": 0, "formattedBaseDiscountAmount": "$0.00", "shippingAmount": 0, "formattedShippingAmount": "$0.00", "baseShippingAmount": 0, "formattedBaseShippingAmount": "$0.00", "shippingAmountInclTax": 0, "formattedShippingAmountInclTax": "$0.00", "baseShippingAmountInclTax": 0, "formattedBaseShippingAmountInclTax": "$0.00", "shippingTaxAmount": 0, "formattedShippingTaxAmount": "$0.00", "baseShippingTaxAmount": 0, "formattedBaseShippingTaxAmount": "$0.00", "transactionId": null, "reminders": 0, "nextReminderAt": null, "createdAt": "2024-07-01 06:41:14", "updatedAt": "2026-05-29 13:30:32", "orderStatus": "processing", "orderStatusLabel": "Processing", "orderDate": "2024-07-01 06:41:14", "channelName": "bagisto store", "customerName": "John Doe", "customerEmail": "john.doe@example.com", "billingAddress": { "id": 268, "addressType": "order_billing", "firstName": "John", "lastName": "Doe", "companyName": "Acme Trades", "address": "21 Market Street", "city": "Los Angeles", "state": "CA", "country": "US", "postcode": "90001", "email": "john.doe@example.com", "phone": "5551234567" }, "shippingAddress": { "id": 267, "addressType": "order_shipping", "firstName": "John", "lastName": "Doe", "companyName": "Acme Trades", "address": "21 Market Street", "city": "Los Angeles", "state": "CA", "country": "US", "postcode": "90001", "email": "john.doe@example.com", "phone": "5551234567" }, "items": [ { "id": 1, "orderItemId": 70, "sku": "Head13", "name": "Bagisto Cowboy Hat", "qty": 2, "price": 4000, "formattedPrice": "$4,000.00", "basePrice": 4000, "basePriceInclTax": 4000, "total": 8000, "formattedTotal": "$8,000.00", "baseTotal": 8000, "baseTotalInclTax": 8000, "taxAmount": 0, "formattedTaxAmount": "$0.00", "discountAmount": 0, "formattedDiscountAmount": "$0.00", "productId": 122, "productType": "simple", "baseImageUrl": "https://example.com/storage/product/122/cowboy-hat.webp", "additional": { "locale": "en", "quantity": 2, "product_id": "122" } } ] } commonErrors: - error: Not Found (404) cause: Unknown invoice ID solution: Verify the invoice ID - error: Unauthorized (401) cause: Missing or invalid admin Bearer token solution: Send a valid admin Bearer token (Integration token) in the Authorization header. See the Authentication page. --- # Get Invoice Returns a single invoice with the full totals breakdown, the order/customer context, the billing & shipping addresses, and the invoiced line items — no follow-up calls required. Requires the `sales.invoices.view` permission. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/invoices/{id}` | GET | ## Response fields | Field | Type | Description | |-------|------|-------------| | `id` | Integer | Numeric invoice id. | | `incrementId` | String | Human-facing invoice number. | | `orderId` | Integer | Id of the parent order. | | `orderIncrementId` | String | Human-facing number of the parent order. | | `state` | String | Invoice state — `pending`, `pending_payment`, `paid`, `overdue`. | | `emailSent` | Boolean | Whether the invoice email was sent to the customer. | | `totalQty` | Integer | Total quantity invoiced. | ### Currency codes | Field | Type | Description | |-------|------|-------------| | `orderCurrencyCode` | String | Currency the order/invoice was placed in. | | `baseCurrencyCode` | String | The store's base currency. | | `channelCurrencyCode` | String | The sales channel's currency. | ### Totals Each money total is provided in the **order** currency and the store's **base** currency, with a `formatted*` string carrying the currency symbol. Sub-total and shipping additionally expose an **incl-tax** variant. | Field | Type | Description | |-------|------|-------------| | `subTotal` / `formattedSubTotal` | Number / String | Line-items subtotal (order currency). | | `baseSubTotal` / `formattedBaseSubTotal` | Number / String | Subtotal (base currency). | | `subTotalInclTax` / `formattedSubTotalInclTax` | Number / String | Subtotal incl. tax (order currency). | | `baseSubTotalInclTax` / `formattedBaseSubTotalInclTax` | Number / String | Subtotal incl. tax (base currency). | | `grandTotal` / `formattedGrandTotal` | Number / String | Invoice total (order currency). | | `baseGrandTotal` / `formattedBaseGrandTotal` | Number / String | Invoice total (base currency). | | `taxAmount` / `formattedTaxAmount` | Number / String | Tax total (order currency). | | `baseTaxAmount` / `formattedBaseTaxAmount` | Number / String | Tax total (base currency). | | `discountAmount` / `formattedDiscountAmount` | Number / String | Discount total (order currency). | | `baseDiscountAmount` / `formattedBaseDiscountAmount` | Number / String | Discount total (base currency). | | `shippingAmount` / `formattedShippingAmount` | Number / String | Shipping total (order currency). | | `baseShippingAmount` / `formattedBaseShippingAmount` | Number / String | Shipping total (base currency). | | `shippingAmountInclTax` / `formattedShippingAmountInclTax` | Number / String | Shipping incl. tax (order currency). | | `baseShippingAmountInclTax` / `formattedBaseShippingAmountInclTax` | Number / String | Shipping incl. tax (base currency). | | `shippingTaxAmount` / `formattedShippingTaxAmount` | Number / String | Tax on shipping (order currency). | | `baseShippingTaxAmount` / `formattedBaseShippingTaxAmount` | Number / String | Tax on shipping (base currency). | ### Status & timestamps | Field | Type | Description | |-------|------|-------------| | `transactionId` | String | Payment transaction reference (null until captured). | | `reminders` | Integer | Number of payment reminders sent (for pending invoices). | | `nextReminderAt` | String | When the next payment reminder is scheduled (null if none). | | `createdAt` | String | When the invoice was created. | | `updatedAt` | String | When the invoice was last updated. | ### Order & customer context Resolved from the parent order so the invoice can be rendered without a second call. | Field | Type | Description | |-------|------|-------------| | `orderStatus` | String | Parent order status code. | | `orderStatusLabel` | String | Human-readable order status. | | `orderDate` | String | When the parent order was placed. | | `channelName` | String | Sales channel the order belongs to. | | `customerName` | String | Customer's full name. | | `customerEmail` | String | Customer's email. | ### Addresses (`billingAddress`, `shippingAddress`) Objects with: `id`, `addressType` (`order_billing` / `order_shipping`), `firstName`, `lastName`, `companyName` (nullable), `address`, `city`, `state`, `country`, `postcode`, `email`, `phone`. ### Line items (`items`) Array of objects, each with: | Field | Type | Description | |-------|------|-------------| | `id` | Integer | Invoice-item id. | | `orderItemId` | Integer | Id of the order item this line was invoiced from. | | `sku` | String | Product SKU. | | `name` | String | Product name as ordered. | | `qty` | Integer | Quantity invoiced for this line. | | `price` / `formattedPrice` | Number / String | Unit price (order currency). | | `basePrice` | Number | Unit price (base currency). | | `basePriceInclTax` | Number | Unit price incl. tax (base currency). | | `total` / `formattedTotal` | Number / String | Line total (order currency). | | `baseTotal` | Number | Line total (base currency). | | `baseTotalInclTax` | Number | Line total incl. tax (base currency). | | `taxAmount` / `formattedTaxAmount` | Number / String | Tax for this line. | | `discountAmount` / `formattedDiscountAmount` | Number / String | Discount for this line. | | `productId` | Integer | Id of the product. | | `productType` | String | Product type — `simple`, `configurable`, `bundle`, etc. | | `baseImageUrl` | String | URL of the product's base image (null if none). | | `additional` | Object | Extra item data (selected options, configurable attributes, etc.). | --- # Get Refund URL: /api/rest-api/admin/sales/orders/get-refund --- outline: false apiType: rest examples: - id: admin-get-refund title: Get Refund description: Fetch a single refund with the full totals breakdown (incl. adjustments), order/customer context, payment info, billing & shipping addresses, and embedded line items. query: | curl -X GET "https://your-domain.com/api/admin/refunds/1" \ -H "Authorization: Bearer " \ -H "Accept: application/json" variables: | {} response: | { "id": 1, "orderId": 105, "orderIncrementId": "105", "state": "refunded", "emailSent": true, "totalQty": 3, "orderCurrencyCode": "USD", "baseCurrencyCode": "USD", "channelCurrencyCode": "USD", "subTotal": 4203, "formattedSubTotal": "$4,203.00", "baseSubTotal": 4203, "formattedBaseSubTotal": "$4,203.00", "subTotalInclTax": 4203, "formattedSubTotalInclTax": "$4,203.00", "baseSubTotalInclTax": 4203, "formattedBaseSubTotalInclTax": "$4,203.00", "grandTotal": 4233, "formattedGrandTotal": "$4,233.00", "baseGrandTotal": 4233, "formattedBaseGrandTotal": "$4,233.00", "taxAmount": 0, "formattedTaxAmount": "$0.00", "baseTaxAmount": 0, "formattedBaseTaxAmount": "$0.00", "discountAmount": 0, "formattedDiscountAmount": "$0.00", "baseDiscountAmount": 0, "formattedBaseDiscountAmount": "$0.00", "shippingAmount": 30, "formattedShippingAmount": "$30.00", "baseShippingAmount": 30, "formattedBaseShippingAmount": "$30.00", "shippingAmountInclTax": 30, "formattedShippingAmountInclTax": "$30.00", "baseShippingAmountInclTax": 30, "formattedBaseShippingAmountInclTax": "$30.00", "shippingTaxAmount": 0, "formattedShippingTaxAmount": "$0.00", "baseShippingTaxAmount": 0, "formattedBaseShippingTaxAmount": "$0.00", "adjustmentRefund": 0, "formattedAdjustmentRefund": "$0.00", "baseAdjustmentRefund": 0, "formattedBaseAdjustmentRefund": "$0.00", "adjustmentFee": 0, "formattedAdjustmentFee": "$0.00", "baseAdjustmentFee": 0, "formattedBaseAdjustmentFee": "$0.00", "createdAt": "2026-05-20 14:00:00", "updatedAt": "2026-05-20 14:00:02", "billedTo": "John Doe", "orderStatus": "closed", "orderStatusLabel": "Closed", "orderDate": "2026-05-19 16:47:17", "channelName": "bagisto store", "customerName": "John Doe", "customerEmail": "john.doe@example.com", "paymentMethod": "cashondelivery", "paymentTitle": "Cash On Delivery", "shippingMethod": "flatrate_flatrate", "shippingTitle": "Flat Rate - Flat Rate", "billingAddress": { "id": 493, "addressType": "order_billing", "firstName": "John", "lastName": "Doe", "companyName": "Acme Trades", "address": "21 Market Street", "city": "Los Angeles", "state": "CA", "country": "US", "postcode": "90001", "email": "john.doe@example.com", "phone": "5551234567" }, "shippingAddress": { "id": 492, "addressType": "order_shipping", "firstName": "John", "lastName": "Doe", "companyName": "Acme Trades", "address": "21 Market Street", "city": "Los Angeles", "state": "CA", "country": "US", "postcode": "90001", "email": "john.doe@example.com", "phone": "5551234567" }, "items": [ { "id": 1, "orderItemId": 119, "sku": "Nike-Shoes", "name": "Nike Shoes", "qty": 1, "price": 123, "formattedPrice": "$123.00", "basePrice": 123, "basePriceInclTax": 123, "total": 123, "formattedTotal": "$123.00", "baseTotal": 123, "baseTotalInclTax": 123, "taxAmount": 0, "formattedTaxAmount": "$0.00", "discountAmount": 0, "formattedDiscountAmount": "$0.00", "productId": 114, "productType": "simple", "baseImageUrl": "https://example.com/storage/product/114/nike-shoes.webp", "additional": { "locale": "en", "quantity": 1, "product_id": "114" } } ] } commonErrors: - error: Not Found (404) cause: Unknown refund ID solution: Verify the refund ID - error: Unauthorized (401) cause: Missing or invalid admin Bearer token solution: Send a valid admin Bearer token in the Authorization header. See the Authentication page. --- # Get Refund Returns a single refund with the full totals breakdown (including the adjustment refund/fee), the order/customer context, payment info, the billing & shipping addresses, and the refunded line items — no follow-up calls required. Requires the `sales.refunds.view` permission. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/refunds/{id}` | GET | ## Response fields | Field | Type | Description | |-------|------|-------------| | `id` | Integer | Refund id. | | `orderId` | Integer | Parent order id. | | `orderIncrementId` | String | Human-facing order number. | | `state` | String | Refund state (e.g. `refunded`). | | `emailSent` | Boolean | Whether the refund email was sent. | | `totalQty` | Integer | Total quantity refunded. | ### Currency codes `orderCurrencyCode`, `baseCurrencyCode`, `channelCurrencyCode`. ### Totals Each money total is provided in the **order** currency and the store's **base** currency with a `formatted*` string; sub-total and shipping also expose **incl-tax** variants. | Field | Type | Description | |-------|------|-------------| | `subTotal` / `formattedSubTotal` (+ `base*`, `*InclTax`, `base*InclTax`) | Number / String | Refunded line-items subtotal. | | `grandTotal` / `formattedGrandTotal` (+ `base*`) | Number / String | Refund grand total. | | `taxAmount` (+ `base*`, `formatted*`) | Number / String | Tax refunded. | | `discountAmount` (+ `base*`, `formatted*`) | Number / String | Discount refunded. | | `shippingAmount` (+ `base*`, `*InclTax`, `base*InclTax`, `formatted*`) | Number / String | Shipping refunded. | | `shippingTaxAmount` (+ `base*`, `formatted*`) | Number / String | Tax on refunded shipping. | | `adjustmentRefund` (+ `base*`, `formatted*`) | Number / String | Extra amount refunded beyond line items. | | `adjustmentFee` (+ `base*`, `formatted*`) | Number / String | Amount withheld from the refund. | ### Status, timestamps & order/customer context `createdAt`, `updatedAt`, `billedTo`, `orderStatus`, `orderStatusLabel`, `orderDate`, `channelName`, `customerName`, `customerEmail`. ### Payment info | Field | Type | Description | |-------|------|-------------| | `paymentMethod` | String | Payment method code of the order. | | `paymentTitle` | String | Human-readable payment method title. | | `shippingMethod` | String | Shipping method code. | | `shippingTitle` | String | Human-readable shipping method title. | ### Addresses (`billingAddress`, `shippingAddress`) Objects with: `id`, `addressType`, `firstName`, `lastName`, `companyName` (nullable), `address`, `city`, `state`, `country`, `postcode`, `email`, `phone`. ### Line items (`items`) Array of objects, each with: `id`, `orderItemId`, `sku`, `name`, `qty`, `price`/`formattedPrice`, `basePrice`, `basePriceInclTax`, `total`/`formattedTotal`, `baseTotal`, `baseTotalInclTax`, `taxAmount`/`formattedTaxAmount`, `discountAmount`/`formattedDiscountAmount`, `productId`, `productType`, `baseImageUrl`, `additional`. --- # Get Shipment URL: /api/rest-api/admin/sales/orders/get-shipment --- outline: false apiType: rest examples: - id: admin-get-shipment title: Get Shipment description: A single shipment — every column, the order/customer context, both addresses, and the shipped line items. query: | curl -X GET "https://your-domain.com/api/admin/shipments/7" \ -H "Authorization: Bearer " \ -H "Accept: application/json" response: | { "id": 7, "orderId": 8, "orderIncrementId": "00000000008", "shippedTo": "John Doe", "orderDate": "2026-05-20 10:00:00", "orderStatus": "processing", "orderStatusLabel": "Processing", "channelName": "Default", "customerName": "John Doe", "customerEmail": "john.doe@example.com", "paymentMethod": "cashondelivery", "paymentTitle": "Cash On Delivery", "orderCurrencyCode": "USD", "shippingMethod": "free_free", "shippingTitle": "Free Shipping - Free Shipping", "baseShippingAmount": 0, "formattedBaseShippingAmount": "$0.00", "status": null, "totalQty": 2, "totalWeight": null, "carrierCode": null, "carrierTitle": "UPS", "trackNumber": "1Z999AA1", "emailSent": false, "inventorySourceId": 1, "inventorySourceName": "Default", "billingAddress": { "id": 16, "addressType": "order_billing", "firstName": "John", "lastName": "Doe", "city": "Los Angeles", "country": "US", "postcode": "90001", "email": "john.doe@example.com", "phone": "5551234567" }, "shippingAddress": { "id": 15, "addressType": "order_shipping", "firstName": "John", "lastName": "Doe", "city": "Los Angeles", "country": "US", "postcode": "90001", "email": "john.doe@example.com", "phone": "5551234567" }, "createdAt": "2026-05-20 12:00:00", "updatedAt": "2026-05-20 12:00:00", "items": [ { "id": 11, "orderItemId": 42, "sku": "TSHIRT-RED-M", "name": "Red T-Shirt", "qty": 2 } ] } --- # Get Shipment Returns a single shipment by id — every column, the order/customer context, both the billing and shipping addresses, and the shipped line `items` so clients can render without a follow-up fetch. Requires the `sales.shipments.view` permission. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/shipments/{id}` | GET | ## Response fields | Field | Type | Description | |-------|------|-------------| | `id` | Integer | Shipment row id. | | `orderId` / `orderIncrementId` | Integer / String | Parent order id and human-facing number. | | `shippedTo` | String | The name on the order's shipping address. | | `orderDate` | String | When the order was placed. | | `orderStatus` / `orderStatusLabel` | String | Parent order status code and its display label. | | `channelName` | String | Sales channel the order belongs to. | | `customerName` / `customerEmail` | String | Customer who placed the order. | | `paymentMethod` / `paymentTitle` | String | The order's payment method code and its display title — see Payment and Shipping below. | | `orderCurrencyCode` | String | Currency the order was placed in (e.g. `USD`). | | `shippingMethod` / `shippingTitle` | String | The order's shipping method code and its display title — may be `null`. | | `baseShippingAmount` | Number | Shipping price in the store's base currency — may be `null`. | | `formattedBaseShippingAmount` | String | The same shipping price pre-formatted for display — may be `null`. | | `status` | String | Shipment status — often `null`. | | `totalQty` | Number | Total quantity shipped across all line items. | | `totalWeight` | Number | Combined weight of the shipment — may be `null`. | | `carrierCode` / `carrierTitle` | String | Shipping carrier code and its title — either may be `null`. | | `trackNumber` | String | Carrier tracking number — may be `null`. | | `emailSent` | Boolean | Whether the shipment notification email was sent. | | `inventorySourceId` / `inventorySourceName` | Integer / String | The warehouse/source the items shipped from. | | `billingAddress` | Object | The order's billing address — see below. | | `shippingAddress` | Object | The order's shipping address — see below. | | `createdAt` / `updatedAt` | String | Timestamps. | | `items` | Array | The shipped line items — see below. | ### Payment and Shipping Mirrors the "Payment and Shipping" panel on the admin Shipment view — the order's payment and shipping details carried alongside the shipment. | Field | Type | Description | |-------|------|-------------| | `paymentMethod` | String | The order's payment method code (e.g. `cashondelivery`). | | `paymentTitle` | String | The payment method's display title (e.g. `Cash On Delivery`). | | `orderCurrencyCode` | String | Currency the order was placed in (e.g. `USD`). | | `shippingMethod` | String | The order's shipping method code (e.g. `free_free`) — `null` when the order had no shipping method (e.g. virtual/free). | | `shippingTitle` | String | The shipping method's display title — `null` when there was no shipping method. | | `baseShippingAmount` | Number | Shipping price in the store's base currency — `null` when there was no shipping method. | | `formattedBaseShippingAmount` | String | The same shipping price pre-formatted for display — `null` when there was no shipping method. | ### Address objects (`billingAddress`, `shippingAddress`) Each address carries `id`, `addressType`, `firstName`, `lastName`, `city`, `country`, `postcode`, `email`, and `phone`. ### Shipped items (`items`) | Field | Type | Description | |-------|------|-------------| | `id` | Integer | Shipment-item row id. | | `orderItemId` | Integer | The order line this shipped item maps to. | | `sku` | String | Product SKU. | | `name` | String | Product name. | | `qty` | Number | Quantity shipped for this line. | ## Permission `sales.shipments.view` --- # List Order Comments URL: /api/rest-api/admin/sales/orders/list-comments --- outline: false apiType: rest examples: - id: admin-list-order-comments title: List Order Comments description: Cursor-friendly list of an order's comments, newest first. Wrapped in the standard `{ data, meta }` admin envelope. query: | curl -X GET "https://your-domain.com/api/admin/orders/2392/comments?per_page=10&page=1" \ -H "Authorization: Bearer " variables: | {} response: | { "data": [ { "id": 17, "orderId": 2392, "comment": "Customer called to confirm shipping address.", "customerNotified": true, "createdAt": "2026-05-21 10:14:31", "updatedAt": "2026-05-21 10:14:31" }, { "id": 16, "orderId": 2392, "comment": "Picking started.", "customerNotified": false, "createdAt": "2026-05-20 17:02:01", "updatedAt": "2026-05-20 17:02:01" } ], "meta": { "currentPage": 1, "perPage": 10, "lastPage": 1, "total": 2, "from": 1, "to": 2 } } commonErrors: - error: Not Found (404) cause: Unknown order ID solution: Verify the order ID - error: Unauthorized (401) cause: Missing or invalid admin Bearer token solution: Send a valid admin Bearer token (Integration token) in the Authorization header. See the Authentication page. --- # List Order Comments Returns all comments on an order, newest first, in the standard admin `{ data, meta }` envelope. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/orders/{orderId}/comments` | GET | ## Query parameters | Parameter | Default | Max | Notes | |-----------|---------|-----|-------| | `per_page` | 10 | 50 | Items per page. | | `page` | 1 | — | 1-indexed page number. | --- # List Orders URL: /api/rest-api/admin/sales/orders/list-orders --- outline: false apiType: rest examples: - id: admin-list-orders title: List Orders description: Paginated, filterable list of all orders, returned in the { data, meta } envelope. query: | curl -X GET "https://your-domain.com/api/admin/orders?per_page=10&page=1" \ -H "Authorization: Bearer " variables: | {} response: | { "data": [ { "id": 2392, "incrementId": "2392", "status": "processing", "statusLabel": "Processing", "channelName": "bagisto store", "isGuest": false, "customerEmail": "admin@example.com", "customerName": "Test User", "paymentTitle": "Money Transfer", "totalItemCount": 1, "totalQtyOrdered": 1, "grandTotal": 4000, "formattedGrandTotal": "$4,000.00", "location": "New York, NY, US", "createdAt": "2026-05-19 13:13:29", "items": [ { "id": 2694, "sku": "test65", "name": "Classic Watch Hand", "qtyOrdered": 1, "productImage": "http://localhost:8000/storage/product/2358/example.webp" } ] } ], "meta": { "currentPage": 1, "perPage": 10, "lastPage": 62, "total": 616, "from": 1, "to": 10 } } - id: admin-list-orders-filtered title: Filter Orders description: Filter by status and a date preset. Filters compose; date presets and date_from/date_to are mutually exclusive. query: | curl -X GET "https://your-domain.com/api/admin/orders?status=processing&date_range=this_month" \ -H "Authorization: Bearer " variables: | {} response: | { "data": [ { "id": 2392, "status": "processing", "...": "..." } ], "meta": { "currentPage": 1, "perPage": 10, "lastPage": 1, "total": 1, "from": 1, "to": 1 } } --- # List Orders Lists every order across all customers — the data behind the admin **Sales → Orders** screen. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/orders` | GET | ## Response envelope Admin collections return a `{ data, meta }` body envelope: - `data` — the slim order rows for this page. - `meta` — `currentPage`, `perPage`, `lastPage`, `total`, `from`, `to`. Each row is **slim** — flat order fields plus a light `items` preview (name / sku / qty / thumbnail) for the listing badge. Heavy relations (full items, invoices, shipments) are served by the order **detail** endpoint, not the listing. ## Query parameters | Parameter | Description | |-----------|-------------| | `page`, `per_page` | Pagination (`per_page` default 10, max 50) | | `order_id` | Order increment ID — partial match | | `status` | `pending`, `pending_payment`, `processing`, `completed`, `canceled`, `closed`, `fraud` | | `grand_total` | Exact grand total (matched against the base grand total) | | `grand_total_from`, `grand_total_to` | Grand total range (minimum / maximum) | | `channel` | Channel ID | | `customer` | Customer name — partial match | | `email` | Customer email — partial match | | `date_range` | Preset: `today`, `yesterday`, `this_week`, `this_month`, `last_month`, `last_three_months`, `last_six_months`, `this_year` | | `date_from`, `date_to` | Custom date range (`Y-m-d`) — overrides `date_range` | | `sort`, `order` | Sort field + `asc`/`desc` (default `created_at desc`) | Every `/api/admin/*` request requires an admin Bearer token. --- # Order Detail URL: /api/rest-api/admin/sales/orders/order-detail --- outline: false apiType: rest examples: - id: admin-order-detail title: Get Order Detail description: Full order-view payload — every relation embedded inline. query: | curl -X GET "https://your-domain.com/api/admin/orders/2392" \ -H "Authorization: Bearer " variables: | {} response: | { "id": 2392, "incrementId": "2392", "status": "processing", "statusLabel": "Processing", "channelName": "bagisto store", "customerEmail": "admin@example.com", "paymentTitle": "Money Transfer", "grandTotal": 4000, "formattedGrandTotal": "$4,000.00", "subTotal": 4000, "createdAt": "2026-05-19 13:13:29", "customer": { "id": 19, "email": "admin@example.com", "name": "Test User", "group": { "id": 2, "code": "general", "name": "General" } }, "billingAddress": { "id": 4943, "addressType": "order_billing", "city": "New York", "state": "NY", "country": "US" }, "shippingAddress": { "id": 4942, "addressType": "order_shipping", "city": "New York", "state": "NY", "country": "US" }, "items": [ { "id": 2694, "sku": "test65", "type": "simple", "name": "Classic Watch Hand", "productId": 2358, "qtyOrdered": 1, "price": 4000, "formattedPrice": "$4,000.00", "additional": { "quantity": 1 }, "child": null, "children": [], "downloadableLinks": [] } ], "totalDue": 0, "formattedTotalDue": "$0.00", "paymentMethod": "moneytransfer", "invoices": [], "shipments": [], "refunds": [], "comments": [ { "id": 11, "comment": "Customer called to confirm the shipping address.", "customerNotified": false, "createdAt": "2026-05-19 14:02:10" } ] } commonErrors: - error: Not Found (404) cause: No order exists with the given ID solution: Verify the order ID - error: Unauthorized (401) cause: Missing or invalid admin Bearer token solution: Send a valid admin Bearer token (Integration token) in the Authorization header. See the Authentication page. --- # Order Detail Returns the complete order-view payload for a single order — the data behind the admin **Sales → Orders → View** screen. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/orders/{id}` | GET | ## What's embedded Unlike the listing, the detail **embeds every relation inline** — one request returns the whole screen: - Flat order fields + totals (grand / sub / tax / discount / shipping, invoiced and refunded variants, **total due**, with `formatted*` strings) plus `paymentMethod` / `paymentTitle`. - `customer` (with `group`), `billingAddress`, `shippingAddress` (each address includes `vatId`). - `items` — each with `type` (`simple`, `configurable`, `bundle`, `downloadable`, `grouped`, `virtual`) and type-specific data in `additional`, `child`, `children`, `downloadableLinks`. Switch on `type` to render. - `invoices`, `shipments`, `refunds` — the order's documents. - `comments` — the order's comment thread, **newest first**, each with `comment`, `customerNotified`, `createdAt`. (Add a comment with `POST /api/admin/orders/{id}/comments`.) Eager-loading everything for one order is bounded (~constant query count, measured ~20 ms), so no sub-resource round trips are needed. ## Product-type-aware items The frontend reads `item.type` and renders accordingly: | `type` | Type-specific data | |--------|--------------------| | `simple`, `virtual` | — | | `configurable` | `child` — the chosen variant; `additional.super_attribute` | | `bundle` | `children` — bundle selections; `additional.bundle_options` | | `grouped` | `children` — grouped sub-items | | `downloadable` | `downloadableLinks` — purchased links | Every `/api/admin/*` request requires an admin Bearer token. --- # Place Order URL: /api/rest-api/admin/sales/orders/place-order --- outline: false apiType: rest examples: - id: admin-place-order title: Place Order description: Finalise a fully prepared draft cart into a real order. query: | curl -X POST "https://your-domain.com/api/admin/orders/place/314" \ -H "Authorization: Bearer " variables: | {} response: | { "orderId": 1284, "incrementId": "1000001284", "customerId": 7, "grandTotal": 149.99, "success": true, "message": "Order placed successfully." } commonErrors: - error: Conflict (409) — cart is empty cause: No items added to the cart solution: Add items via `POST /api/admin/carts/{id}/items` - error: Conflict (409) — addresses required cause: Billing and/or shipping address not saved solution: Save addresses via `POST /api/admin/carts/{id}/addresses` - error: Conflict (409) — shipping required cause: No shipping method selected solution: Select a shipping method via `POST /api/admin/carts/{id}/shipping-methods` - error: Conflict (409) — payment required cause: No payment method selected solution: Select a payment method via `POST /api/admin/carts/{id}/payment-methods` - error: Unprocessable Entity (422) — below minimum order amount cause: The cart total is below the store's configured minimum order amount solution: Add more items until the cart meets the minimum, or disable the minimum-order requirement in store settings - error: Unprocessable Entity (422) cause: Payment method is not in ['cashondelivery','moneytransfer'] solution: Select COD or money transfer. Other methods are blocked for admin-placed orders. - error: Forbidden (403) cause: Cart is an active storefront cart solution: Only draft carts can be finalised - error: Not Found (404) cause: Unknown cart ID solution: Confirm the cart ID returned by Create-Cart / Reorder - error: Unauthorized (401) cause: Missing or invalid admin Bearer token solution: Send a valid admin Bearer token (Integration token) in the Authorization header. See the Authentication page. --- # Place Order Finalises a fully prepared draft cart into a real order. This is the same flow as the admin Create-Order screen's place-order step: 1. The cart's totals are recalculated. 2. The cart total must meet the store's configured **minimum order amount** (when that requirement is enabled) — otherwise the order is rejected with `422`. 3. The selected payment method must be one of `cashondelivery` or `moneytransfer` (other gateways are not supported for admin-placed orders). 4. The order is created from the cart, and the draft cart is then removed. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/orders/place/{cartId}` | POST | `{cartId}` is the draft cart id. The request body is empty — all payment, shipping, and address selections must already be saved on the cart. ## Sequence enforcement This endpoint enforces the entire Create-Order sequence explicitly. Each missing step returns a distinct HTTP 409 with its own message, so the client can drive the user back to the right step instead of seeing a generic 500. | Step | Status | Message key | |------|--------|-------------| | Items present | 409 | `bagistoapi::app.admin.cart.place-order.empty-cart` | | Addresses saved | 409 | `bagistoapi::app.admin.cart.place-order.addresses-required` | | Shipping selected | 409 | `bagistoapi::app.admin.cart.place-order.shipping-required` | | Payment selected | 409 | `bagistoapi::app.admin.cart.place-order.payment-required` | | Payment in {cashondelivery, moneytransfer} | 422 | `bagistoapi::app.admin.cart.place-order.payment-method-unsupported` | The supported-payment restriction matches the Bagisto admin UI — other methods (Stripe, PayPal, …) cannot be admin-finalised through core's Create-Order screen either. --- # Print Invoice (PDF) URL: /api/rest-api/admin/sales/orders/print-invoice --- outline: false apiType: rest examples: - id: admin-print-invoice title: Print Invoice (PDF) description: Downloads the invoice as an `application/pdf` binary attachment — the same PDF the admin panel produces. query: | curl -X GET "https://your-domain.com/api/admin/invoices/585/print" \ -H "Authorization: Bearer " \ --output invoice-585.pdf variables: | {} response: | HTTP/1.1 200 OK Content-Type: application/pdf Content-Disposition: attachment; filename="invoice-585.pdf" commonErrors: - error: Not Found (404) cause: Unknown invoice ID solution: Verify the invoice ID - error: PDF generation failed (500) cause: The PDF could not be rendered solution: Retry; the underlying error message is returned in `detail` - error: Unauthorized (401) cause: Missing or invalid admin Bearer token solution: Send a valid admin Bearer token in the Authorization header. See the Authentication page. --- # Print Invoice (PDF) Returns the invoice as an `application/pdf` binary attachment — the same PDF the admin panel produces. Requires the `sales.invoices.view` permission. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/invoices/{id}/print` | GET | ## Response headers ``` Content-Type: application/pdf Content-Disposition: attachment; filename="invoice-{id}.pdf" ``` > The body is a raw PDF — do not JSON-decode it. ## GraphQL There is no GraphQL counterpart for this endpoint — GraphQL transports cannot return a binary PDF stream. Use this REST endpoint when you need the PDF. --- # Refund Preview URL: /api/rest-api/admin/sales/orders/refund-preview --- outline: false apiType: rest examples: - id: admin-refund-preview title: Refund Preview description: Computes refund totals (subtotal, discount, tax, shipping, grandTotal) for a hypothetical refund body without saving anything. Same body as Create Refund. query: | curl -X POST "https://your-domain.com/api/admin/orders/2392/refunds/preview" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "items": [ { "orderItemId": 42, "quantity": 1 } ], "shipping": 0, "adjustmentRefund": 0, "adjustmentFee": 0 }' variables: | { "items": [ { "orderItemId": 42, "quantity": 1 } ], "shipping": 0, "adjustmentRefund": 0, "adjustmentFee": 0 } response: | { "orderId": 2392, "subtotal": 29.99, "formattedSubtotal": "$29.99", "discount": 0.0, "formattedDiscount": "$0.00", "tax": 3.0, "formattedTax": "$3.00", "shipping": 0.0, "formattedShipping": "$0.00", "adjustmentRefund": 0.0, "adjustmentFee": 0.0, "grandTotal": 32.99, "formattedGrandTotal": "$32.99" } commonErrors: - error: Closed / Fraud / No permission / Nothing to refund (422) cause: Same eligibility gates as Create Refund solution: See the Create Refund page for the full list - error: Not Found (404) cause: Unknown order ID solution: Verify the order ID - error: Unauthorized (401) cause: Missing or invalid admin Bearer token solution: Send a valid admin Bearer token (Integration token) in the Authorization header. See the Authentication page. --- # Refund Preview Computes the refund totals (subtotal, discount, tax, shipping, grandTotal, plus pre-formatted variants) for a hypothetical refund body **without saving anything**. Same request body as Create Refund — useful for live-updating the "Total Refund" widget in the admin refund form. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/orders/{orderId}/refunds/preview` | POST | Eligibility gates fire here too — if the order cannot be refunded at all (closed / fraud / no permission / nothing-to-refund), preview rejects with the same 422 messages as Create Refund. --- # Reorder URL: /api/rest-api/admin/sales/orders/reorder --- outline: false apiType: rest examples: - id: admin-reorder title: Reorder description: Build a fresh admin draft cart from a previous order's items. Returns the new cart ID — the admin can then finalise the order in /admin/sales/orders/create. query: | curl -X POST "https://your-domain.com/api/admin/orders/2392/reorder" \ -H "Authorization: Bearer " variables: | {} response: | { "success": true, "message": "Reorder successful. A new draft cart has been created.", "cartId": 314 } commonErrors: - error: Guest order (422) cause: The target order was placed as a guest (`is_guest = 1`) solution: Reorder is only supported for customer orders - error: Items not saleable (422) cause: One or more of the order's products are no longer purchasable (disabled / out of stock / deleted) solution: Restore product availability, or remove the item from the original order before reordering - error: No permission (422) cause: The authenticated admin's role does not include `sales.orders.create` solution: Grant the role the `sales.orders.create` permission (or assign a role that has it) - error: Disabled in settings (422) cause: '`Configure → Sales → Order Settings → Reorder → Admin Reorder` is OFF' solution: Re-enable Admin Reorder in the store settings - error: Not Found (404) cause: Unknown order ID solution: Verify the order ID - error: Unauthorized (401) cause: Missing or invalid admin Bearer token solution: Send a valid admin Bearer token (Integration token) in the Authorization header. See the Authentication page. --- # Reorder Build a fresh admin draft cart from a previous order's items, ready for the admin to finalise on the customer's behalf — the same flow as the **Reorder** button on the admin order-view screen. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/orders/{id}/reorder` | POST | ## What it does This is the same action as the admin Reorder button: 1. A new draft admin cart is created for the order's customer (separate from the customer's own active cart). 2. Each item from the order is re-added to that draft cart. Per-item failures are swallowed (best-effort) — the same behaviour the admin panel has. 3. Returns the new cart ID. The client can then redirect to the admin's order create screen with that cart. ## When it refuses The endpoint enforces the same 3 eligibility checks the admin panel uses. Each failure returns **HTTP 422** with the error message in the `detail` field — different message per failure mode so the client can act on it. | HTTP | Condition | Message | |------|-----------|---------| | 422 | Order was placed as guest (`is_guest = 1`) | `Reorder is not supported for guest orders.` | | 422 | At least one item's product is no longer purchasable | `One or more items in this order are no longer available for purchase.` | | 422 | Admin's role lacks `sales.orders.create` | `You do not have permission to create orders.` | | 422 | `sales.order_settings.reorder.admin` config is off | `Reorder by admin is disabled in store settings.` | ### Sample 422 response ```json { "type": "/errors/422", "title": "Bad Request", "status": 422, "detail": "Reorder is not supported for guest orders." } ``` There is no request body — the order is identified by the URL. --- # Send Duplicate Invoice URL: /api/rest-api/admin/sales/orders/send-duplicate-invoice --- outline: false apiType: rest examples: - id: admin-send-duplicate-invoice title: Send Duplicate Invoice description: Emails a copy of the invoice. The recipient is the `email` you pass, or the order's customer email when omitted. query: | curl -X POST "https://your-domain.com/api/admin/invoices/585/send-duplicate" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d '{ "email": "customer@example.com" }' variables: | { "email": "customer@example.com" } response: | { "id": 585, "email": "customer@example.com", "success": true, "message": "Invoice email sent to customer@example.com." } commonErrors: - error: Unprocessable (422) cause: The recipient email is missing/invalid and the order has no customer email to fall back to solution: Pass a valid `email` in the request body - error: Not Found (404) cause: Unknown invoice ID solution: Verify the invoice ID - error: Unauthorized (401) cause: Missing or invalid admin Bearer token solution: Send a valid admin Bearer token in the Authorization header. See the Authentication page. --- # Send Duplicate Invoice Emails a copy of the invoice. Requires the `sales.invoices.view` permission. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/invoices/{id}/send-duplicate` | POST | ## Request body | Field | Type | Required | Description | |-------|------|----------|-------------| | `email` | String | No | Recipient address. Defaults to the order's customer email when omitted. Must be a valid email when provided. | ## Response fields | Field | Type | Description | |-------|------|-------------| | `id` | Integer | Invoice id. | | `email` | String | The address the invoice was sent to. | | `success` | Boolean | Whether the email was queued. | | `message` | String | Human-readable result message. | ::: tip Recipient Whatever address you pass in `email` is the actual recipient. Leave it out to send to the order's customer. ::: --- # Refunds URL: /api/rest-api/admin/sales/refunds --- outline: false apiType: rest --- # Refunds The Refunds menu is the store-wide, **read-only** list of every refund that has been issued across all orders. It mirrors the admin **Sales → Refunds** screen — browse refunds, open one for detail, and export the list. It does not create refunds (that happens against an order — see below). ## When a row appears here A refund row exists only after a refund has been **issued** for an order. A refund records money returned to the customer for some or all of the order's items, plus an optional adjustment-refund (an extra amount returned beyond the line items) and an optional adjustment-fee (an amount withheld from the refund). Placing, invoicing, or shipping an order does not by itself create a refund; until one is issued, the order has no row in this menu. Issuing a refund is an **order action**, not part of this menu — it runs against a specific order under [Orders → Create Refund](/api/rest-api/admin/sales/orders/create-refund) (with a [Refund Preview](/api/rest-api/admin/sales/orders/refund-preview) to compute totals first). ## Refunded Amount The list's **Refunded Amount** is the refund's grand total in the store's base currency (subtotal + tax + shipping + adjustment-refund − adjustment-fee) — not just the line-items subtotal. In the API payload this is `formattedBaseGrandTotal` (raw: `baseGrandTotal`). ## Endpoints in this menu | Action | Endpoint | |--------|----------| | [List refunds](/api/rest-api/admin/sales/refunds/list) | `GET /api/admin/refunds` | | [Get a single refund](/api/rest-api/admin/sales/orders/get-refund) | `GET /api/admin/refunds/{id}` | | [Export refunds (CSV)](/api/rest-api/admin/sales/refunds/export) | `GET /api/admin/refunds/export` | All Refunds endpoints require the `sales.refunds.view` permission and an admin Bearer token — see [Authentication](/api/rest-api/admin/authentication). --- # Export Refunds URL: /api/rest-api/admin/sales/refunds/export --- outline: false apiType: rest examples: - id: admin-refunds-export title: Export Refunds (CSV) description: Download the refunds datagrid as a CSV file — the same data the admin Refunds "Export" button produces. Honours the same filters as the listing. query: | curl -X GET "https://your-domain.com/api/admin/refunds/export?format=csv" \ -H "Authorization: Bearer " \ -H "Accept: text/csv" \ --output refunds.csv response: | # Binary response: a text/csv attachment is written to refunds.csv # (Content-Disposition: attachment; filename="refunds.csv"). Sample contents: ID,"Order ID","Refunded Amount","Billed To","Refund Date" 1,105,"$4,233.00","John Doe","2025-04-16 22:13:30" --- # Export Refunds Downloads the refunds datagrid as a **CSV file** — the same data the admin **Sales → Refunds** "Export" button produces. The response is a binary `text/csv` attachment, not JSON. Requires the `sales.refunds.view` permission. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/refunds/export` | GET | ## Columns The CSV carries the five datagrid columns, in order: | Header | Value | |--------|-------| | `ID` | Refund id. | | `Order ID` | The parent order's number. | | `Refunded Amount` | The total amount refunded, in the store's base currency, formatted (e.g. `$4,233.00`) — this is the refund's grand total, not just the line-items subtotal. | | `Billed To` | Name on the order's billing address. | | `Refund Date` | When the refund was created. | ## Query parameters `format` selects the export format — **only `csv` is supported** (the default); any other value returns `422`. The export honours the **same filters as the [listing](/api/rest-api/admin/sales/refunds/list)**, so you export exactly the rows you're viewing: `id`, `order_id`, `state`, `base_grand_total_from` / `_to`, `billed_to`, `created_at_from` / `_to`. (Pagination does not apply — the export returns every matching row.) ## Permission `sales.refunds.view` --- # List Refunds URL: /api/rest-api/admin/sales/refunds/list --- outline: false apiType: rest examples: - id: admin-refunds-list title: List Refunds (Datagrid) description: Paginated refunds listing. Every refund column + billing/shipping addresses are populated per row (line items are detail-only). query: | curl -X GET "https://your-domain.com/api/admin/refunds?per_page=10" \ -H "Authorization: Bearer " \ -H "Accept: application/json" response: | { "data": [ { "id": 1, "orderId": 105, "orderIncrementId": "105", "state": "refunded", "emailSent": true, "totalQty": 3, "orderCurrencyCode": "USD", "baseCurrencyCode": "USD", "subTotal": 4203, "formattedSubTotal": "$4,203.00", "baseSubTotal": 4203, "grandTotal": 4233, "baseGrandTotal": 4233, "formattedBaseGrandTotal": "$4,233.00", "taxAmount": 0, "discountAmount": 0, "shippingAmount": 30, "adjustmentRefund": 0, "adjustmentFee": 0, "createdAt": "2026-05-20 14:00:00", "updatedAt": "2026-05-20 14:00:02", "billedTo": "John Doe", "orderStatus": "closed", "orderStatusLabel": "Closed", "channelName": "bagisto store", "customerName": "John Doe", "customerEmail": "john.doe@example.com", "billingAddress": { "id": 493, "addressType": "order_billing", "firstName": "John", "lastName": "Doe", "city": "Los Angeles", "country": "US", "postcode": "90001", "email": "john.doe@example.com", "phone": "5551234567" }, "shippingAddress": { "id": 492, "addressType": "order_shipping", "firstName": "John", "lastName": "Doe", "city": "Los Angeles", "country": "US", "postcode": "90001", "email": "john.doe@example.com", "phone": "5551234567" }, "paymentMethod": null, "items": [] } ], "meta": { "currentPage": 1, "perPage": 10, "lastPage": 1, "total": 1, "from": 1, "to": 1 } } --- # List Refunds Mirrors the admin **Sales → Refunds** datagrid. Every refund **column** plus the billing/shipping addresses are populated per row — only the line `items` (and payment info) are detail-only. Field reference is identical to [Get Refund](/api/rest-api/admin/sales/orders/get-refund). ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/refunds` | GET | ## Query Parameters | Parameter | Type | Description | |-----------|------|-------------| | `page`, `per_page` | integer | Pagination (default `per_page=10`, cap `50`). | | `id` | string | Filter by refund id (int or comma-list). | | `order_id` | string | Partial match on `orders.increment_id`. | | `state` | string | Refund state. | | `base_grand_total_from` / `_to` | number | Refund amount range. | | `billed_to` | string | Partial billing-address full-name match. | | `created_at_from` / `_to` | date | Created range. | | `sort` | string | `id`, `order_id`, `state`, `base_grand_total`, `billed_to`, `created_at`. | | `order` | string | `asc`, `desc`. | ## Permission `sales.refunds.view` ::: info Refund **detail**, **create**, and **preview** live under [Orders](/api/rest-api/admin/sales/orders/get-refund). ::: --- # Shipments URL: /api/rest-api/admin/sales/shipments --- outline: false apiType: rest --- # Shipments The Shipments menu is the store-wide list of every shipment that has been created across all orders. It mirrors the admin **Sales → Shipments** screen. ## When a row appears here A shipment row exists only after a shipment has been **created** for an order (the Create Shipment action). Placing or invoicing an order does not by itself create a shipment; until one is created, the order has no row in this menu. ## What a shipment records A shipment records which items, in what quantity, were dispatched — and from which inventory source. It can optionally carry a shipping carrier (`carrierCode` / `carrierTitle`) and a tracking number (`trackNumber`). An order can be shipped in parts: each partial dispatch creates its own shipment, so a single order may have several shipment rows, each covering a subset of its items from a (possibly different) inventory source. ## Shipped-to address The `shippingAddress` carried on each shipment, and the `shippedTo` name, come from the **order's shipping address** — the destination the items were dispatched to. The order's billing address is also included as `billingAddress` for context. ## Payment and Shipping The shipment detail also surfaces the order's Payment and Shipping info — the payment method and its title, the order currency, and the shipping method, its title and price — matching the admin Shipment view's "Payment and Shipping" panel. The shipping fields can be `null` when the order had no shipping method (e.g. virtual/free). ## Endpoints in this menu | Action | Endpoint | |--------|----------| | [List shipments](/api/rest-api/admin/sales/shipments/list) | `GET /api/admin/shipments` | | [Get a single shipment](/api/rest-api/admin/sales/orders/get-shipment) | `GET /api/admin/shipments/{id}` | | [Create shipment](/api/rest-api/admin/sales/orders/create-shipment) | `POST /api/admin/orders/{id}/shipments` | Shipment **creation** runs against an order — see [Create Shipment](/api/rest-api/admin/sales/orders/create-shipment). All Shipments endpoints require the `sales.shipments.view` permission and an admin Bearer token — see [Authentication](/api/rest-api/admin/authentication). --- # Export Shipments URL: /api/rest-api/admin/sales/shipments/export --- outline: false apiType: rest examples: - id: admin-shipments-export title: Export Shipments (CSV) description: Download the shipments datagrid as a CSV file — the same data the admin Shipments "Export" button produces. Honours the same filters as the listing. query: | curl -X GET "https://your-domain.com/api/admin/shipments/export?format=csv" \ -H "Authorization: Bearer " \ -H "Accept: text/csv" \ --output shipments.csv response: | # Binary response: a text/csv attachment is written to shipments.csv # (Content-Disposition: attachment; filename="shipments.csv"). Sample contents: ID,"Order ID","Total Qty","Inventory Source","Shipped To","Shipment Date" 15,172,1,Default,"John Doe","2026-04-23 22:06:13" --- # Export Shipments Downloads the shipments datagrid as a **CSV file** — the same data the admin **Sales → Shipments** "Export" button produces. The response is a binary `text/csv` attachment, not JSON. Requires the `sales.shipments.view` permission. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/shipments/export` | GET | ## Columns The CSV carries the six datagrid columns, in order: | Header | Value | |--------|-------| | `ID` | Shipment id. | | `Order ID` | The parent order's number. | | `Total Qty` | Total quantity of items in the shipment. | | `Inventory Source` | The source the shipment was dispatched from. | | `Shipped To` | Name on the order's shipping address. | | `Shipment Date` | When the shipment was created. | ## Query parameters `format` selects the export format — **only `csv` is supported** (the default); any other value returns `422`. The export honours the **same filters as the [listing](/api/rest-api/admin/sales/shipments/list)**, so you export exactly the rows you're viewing: `id`, `order_id`, `total_qty`, `inventory_source_name`, `shipped_to`, `order_date_from` / `_to`, `created_at_from` / `_to`. (Pagination does not apply — the export returns every matching row.) ## Permission `sales.shipments.view` --- # List Shipments (Datagrid) URL: /api/rest-api/admin/sales/shipments/list --- outline: false apiType: rest examples: - id: admin-shipments-list title: List Shipments (Datagrid) description: One row per shipment. Every column plus the order/customer context and both addresses is populated on each row — only the shipped line items are detail-only. query: | curl -X GET "https://your-domain.com/api/admin/shipments?per_page=10" \ -H "Authorization: Bearer " \ -H "Accept: application/json" response: | { "data": [ { "id": 7, "orderId": 8, "orderIncrementId": "00000000008", "shippedTo": "John Doe", "orderDate": "2026-05-20 10:00:00", "orderStatus": "processing", "orderStatusLabel": "Processing", "channelName": "Default", "customerName": "John Doe", "customerEmail": "john.doe@example.com", "status": null, "totalQty": 2, "totalWeight": null, "carrierCode": null, "carrierTitle": "UPS", "trackNumber": "1Z999AA1", "emailSent": false, "inventorySourceId": 1, "inventorySourceName": "Default", "billingAddress": { "id": 16, "addressType": "order_billing", "firstName": "John", "lastName": "Doe", "city": "Los Angeles", "country": "US", "postcode": "90001", "email": "john.doe@example.com", "phone": "5551234567" }, "shippingAddress": { "id": 15, "addressType": "order_shipping", "firstName": "John", "lastName": "Doe", "city": "Los Angeles", "country": "US", "postcode": "90001", "email": "john.doe@example.com", "phone": "5551234567" }, "createdAt": "2026-05-20 12:00:00", "updatedAt": "2026-05-20 12:00:00", "items": [] } ], "meta": { "currentPage": 1, "perPage": 10, "lastPage": 1, "total": 1, "from": 1, "to": 1 } } --- # List Shipments (Datagrid) Mirrors the admin **Sales → Shipments** datagrid. Every shipment **column** plus the order/customer context and both the billing and shipping addresses are populated on each row — the field set is identical to [Shipment Detail](/api/rest-api/admin/sales/orders/get-shipment) except for the shipped line `items`, which are returned only by the detail endpoint (`[]` on the listing). ::: tip How this menu works For when a shipment row appears and what each field means, see the [Shipments overview](/api/rest-api/admin/sales/shipments/). ::: ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/shipments` | GET | ## Query Parameters | Parameter | Type | Description | |-----------|------|-------------| | `page`, `per_page` | integer | Pagination (default `per_page=10`, cap `50`). | | `id` | string | Filter by shipment id (int or comma-list). | | `order_id` | string | Partial match on `orders.increment_id`. | | `total_qty` | integer | Exact total quantity. | | `inventory_source_name` | string | Partial source name. | | `shipped_to` | string | Partial shipped-to (address full-name). | | `order_date_from` / `order_date_to` | date | Order created range. | | `created_at_from` / `created_at_to` | date | Shipment created range. | | `sort` | string | `id`, `order_id`, `total_qty`, `inventory_source_name`, `shipped_to`, `order_date`, `created_at`. | | `order` | string | `asc`, `desc`. | ## Permission `sales.shipments.view` --- # Export Transactions URL: /api/rest-api/admin/sales/transactions/export --- outline: false apiType: rest examples: - id: admin-transactions-export title: Export Transactions (CSV) description: Download the transactions datagrid as a CSV file — the same data the admin Transactions "Export" button produces. Honours the same filters as the listing. query: | curl -X GET "https://your-domain.com/api/admin/transactions/export?format=csv" \ -H "Authorization: Bearer " \ -H "Accept: text/csv" \ --output transactions.csv response: | # Binary response: a text/csv attachment is written to transactions.csv # (Content-Disposition: attachment; filename="transactions.csv"). Sample contents: ID,"Transaction ID","Invoice ID","Order ID",Status,Date 6,605a2919e8a49b907e98f3cc8f71397d,585,41,paid,"2026-06-04 17:52:47" --- # Export Transactions Downloads the transactions datagrid as a **CSV file** — the same data the admin **Sales → Transactions** "Export" button produces. The response is a binary `text/csv` attachment, not JSON. Requires the `sales.transactions.view` permission. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/transactions/export` | GET | ## Columns The CSV carries the six datagrid columns, in order: | Header | Value | |--------|-------| | `ID` | Transaction id. | | `Transaction ID` | The payment gateway's transaction id. | | `Invoice ID` | The related invoice's number. | | `Order ID` | The parent order's number. | | `Status` | The transaction status (e.g. `paid`, `pending`). | | `Date` | When the transaction was recorded. | ## Query parameters `format` selects the export format — **only `csv` is supported** (the default); any other value returns `422`. The export honours the **same filters as the [listing](/api/rest-api/admin/sales/transactions/list)**, so you export exactly the rows you're viewing: `id`, `transaction_id`, `invoice_id`, `order_id`, `status`, `created_at_from` / `_to`. (Pagination does not apply — the export returns every matching row.) ## Permission `sales.transactions.view` --- # List Transactions URL: /api/rest-api/admin/sales/transactions/list --- outline: false apiType: rest examples: - id: admin-transactions-list title: List Transactions (Datagrid) description: One row per payment transaction. Every column plus the raw gateway data blob and the linked order summary is populated on each row. query: | curl -X GET "https://your-domain.com/api/admin/transactions?per_page=10" \ -H "Authorization: Bearer " \ -H "Accept: application/json" response: | { "data": [ { "id": 4, "transactionId": "pi_3PqXyz9aBcD", "invoiceId": 12, "orderId": 8, "orderIncrementId": "00000000008", "amount": 99.99, "formattedAmount": "$99.99", "status": "paid", "type": "capture", "paymentMethod": "cashondelivery", "paymentTitle": "Cash On Delivery", "data": { "gateway": "offline", "captured": true }, "createdAt": "2026-05-20 12:35:00", "updatedAt": "2026-05-20 12:35:00", "order": { "id": 8, "incrementId": "00000000008", "status": "processing", "customerName": "John Doe", "customerEmail": "john.doe@example.com", "grandTotal": 99.99, "orderCurrencyCode": "USD" } } ], "meta": { "currentPage": 1, "perPage": 10, "lastPage": 1, "total": 1, "from": 1, "to": 1 } } --- # List Transactions Mirrors the admin **Sales → Transactions** datagrid. Every transaction **column** plus the raw gateway `data` blob and the linked `order` summary are populated on each row. ## Endpoint | Endpoint | Method | |----------|--------| | `/api/admin/transactions` | GET | ## Query Parameters | Parameter | Type | Description | |-----------|------|-------------| | `page`, `per_page` | integer | Pagination (default `10`, cap `50`). | | `id` | string | Filter by transaction row id (int or comma-list). | | `transaction_id` | string | Partial gateway transaction id. | | `invoice_id` | integer | Filter by invoice id. | | `order_id` | string | Partial order increment number. | | `status` | string | `paid`, `pending`, `COMPLETED`. | | `created_at_from` / `_to` | date | Range. | | `sort` | string | `id`, `transaction_id`, `amount`, `invoice_id`, `order_id`, `status`, `created_at`. | | `order` | string | `asc`, `desc`. | ## Response fields | Field | Type | Description | |-------|------|-------------| | `id` | Integer | Transaction row id. | | `transactionId` | String | Gateway transaction id. | | `invoiceId` | Integer | The invoice this transaction paid (if any). | | `orderId` / `orderIncrementId` | Integer / String | Parent order id and human-facing number. | | `amount` / `formattedAmount` | Number / String | Transaction amount, raw and formatted. | | `status` | String | Transaction status — e.g. `paid`, `pending`. | | `type` | String | Transaction type — e.g. `capture`. | | `paymentMethod` / `paymentTitle` | String | Payment method code and its human-readable title. | | `data` | Object | The verbatim gateway response payload (shape varies by gateway; may be `null`). | | `createdAt` / `updatedAt` | String | Timestamps. | | `order` | Object | Slim order summary — `id`, `incrementId`, `status`, `customerName`, `customerEmail`, `grandTotal`, `orderCurrencyCode`. | ## Permission `sales.transactions.view` --- # Create Channel URL: /api/rest-api/admin/settings/channels/create --- outline: false apiType: rest examples: - id: rest title: Create Channel query: | curl -X POST "https://your-domain.com/api/admin/settings/channels" -H "Authorization: Bearer " -H "Content-Type: application/json" -d '{ "code": "us", "name": "US Store", "hostname": "us.example.com", "locales": [1], "currencies": [1], "inventory_sources": [1], "default_locale_id": 1, "base_currency_id": 1, "root_category_id": 1, "description": "Our US storefront", "seo_title": "Best products", "seo_description": "Welcome to our shop" }' response: | { "id": 2, "code": "us", "name": "US Store", "hostname": "us.example.com" } --- # Create Channel ## Validation - `code` — unique, alpha-dash. - `hostname` — unique. - `locales`, `currencies`, `inventory_sources` — non-empty integer arrays. - `default_locale_id` must appear in `locales`; `base_currency_id` must appear in `currencies`. - `root_category_id` must exist. ::: warning Logo / favicon upload deferred Channel `logo` and `favicon` multipart uploads are **not yet supported via the API** — use the admin panel. Other scalar/translation fields work fine. ::: Permission: `settings.channels.create`. --- # Delete Channel URL: /api/rest-api/admin/settings/channels/delete --- outline: false apiType: rest examples: - id: rest title: Delete Channel query: | curl -X DELETE "https://your-domain.com/api/admin/settings/channels/2" -H "Authorization: Bearer " response: | { "message": "Channel deleted." } --- # Delete Channel ::: warning Two guards (HTTP 400) - **Last channel** — refuses if this is the only channel left. - **Default channel** — refuses if its `code` matches the application-wide default (`config('app.channel')`). ::: Permission: `settings.channels.delete`. --- # Channel Detail URL: /api/rest-api/admin/settings/channels/detail --- outline: false apiType: rest examples: - id: rest title: Channel Detail query: | curl -X GET "https://your-domain.com/api/admin/settings/channels/1" -H "Authorization: Bearer " response: | { "id": 1, "code": "default", "name": "Default", "hostname": "store.example.com", "rootCategoryId": 1, "defaultLocaleId": 1, "baseCurrencyId": 1, "locales": [{ "id": 1, "code": "en" }], "currencies": [{ "id": 1, "code": "USD" }], "inventorySources": [{ "id": 1, "code": "default" }], "translations": [{ "locale": "en", "name": "Default" }] } --- # Channel Detail | Endpoint | Method | |----------|--------| | `/api/admin/settings/channels/{id}` | GET | --- # List Channels URL: /api/rest-api/admin/settings/channels/list --- outline: false apiType: rest examples: - id: rest title: List Channels query: | curl -X GET "https://your-domain.com/api/admin/settings/channels" -H "Authorization: Bearer " response: | { "data": [{ "id": 1, "code": "default", "name": "Default Channel", "hostname": "store.example.com", "rootCategoryId": 1, "defaultLocaleId": 1, "baseCurrencyId": 1 }], "meta": { "currentPage": 1, "perPage": 10, "lastPage": 1, "total": 1, "from": 1, "to": 1 } } --- # List Channels | Endpoint | Method | |----------|--------| | `/api/admin/settings/channels` | GET | --- # Update Channel URL: /api/rest-api/admin/settings/channels/update --- outline: false apiType: rest examples: - id: rest title: Update Channel query: | curl -X PUT "https://your-domain.com/api/admin/settings/channels/2" -H "Authorization: Bearer " -H "Content-Type: application/json" -d '{ "translations": { "en": { "name": "United States Store", "description": "Our US storefront", "seo_description": "Welcome" } } }' response: | { "id": 2, "code": "us", "name": "United States Store" } --- # Update Channel Code/hostname uniqueness excludes self. Use `translations` map for locale-nested attributes (name, description, home_page_content, footer_content, seo_*, maintenance_mode_text). Top-level scalar fields broadcast to every configured locale via the repository. Permission: `settings.channels.edit`. --- # Create Currency URL: /api/rest-api/admin/settings/currencies/create --- outline: false apiType: rest examples: - id: admin-settings-currency-create title: Create Currency query: | curl -X POST "https://your-domain.com/api/admin/settings/currencies" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "code": "EUR", "name": "Euro", "symbol": "€" }' response: | { "id": 2, "code": "EUR", "name": "Euro", "symbol": "€" } --- # Create Currency | Endpoint | Method | |----------|--------| | `/api/admin/settings/currencies` | POST | ## Request Body | Field | Type | Required | Notes | |-------|------|----------|-------| | `code` | string | yes | Alpha, exactly 3 chars, unique. Uppercased by the model mutator. | | `name` | string | yes | | | `symbol` | string | no | | | `decimal` | string | no | | | `group_separator` | string | no | | | `decimal_separator` | string | no | | | `currency_position` | enum | no | `left`, `right`, `left_with_space`, `right_with_space`. | Fires `core.currency.create.before/after`. Permission: `settings.currencies.create`. --- # Delete Currency URL: /api/rest-api/admin/settings/currencies/delete --- outline: false apiType: rest examples: - id: admin-settings-currency-delete title: Delete Currency query: | curl -X DELETE "https://your-domain.com/api/admin/settings/currencies/2" \ -H "Authorization: Bearer " response: | { "message": "Currency deleted." } --- # Delete Currency | Endpoint | Method | |----------|--------| | `/api/admin/settings/currencies/{id}` | DELETE | ::: warning Two delete guards (HTTP 400) - **Last currency** — refuses if this is the only currency left. - **Channel base** — refuses if any channel uses this as its `base_currency_id`. ::: Permission: `settings.currencies.delete`. --- # Currency Detail URL: /api/rest-api/admin/settings/currencies/detail --- outline: false apiType: rest examples: - id: admin-settings-currency-detail title: Currency Detail query: | curl -X GET "https://your-domain.com/api/admin/settings/currencies/1" \ -H "Authorization: Bearer " response: | { "id": 1, "code": "USD", "name": "US Dollar", "symbol": "$" } --- # Currency Detail | Endpoint | Method | |----------|--------| | `/api/admin/settings/currencies/{id}` | GET | --- # List Currencies URL: /api/rest-api/admin/settings/currencies/list --- outline: false apiType: rest examples: - id: admin-settings-currencies-list title: List Currencies query: | curl -X GET "https://your-domain.com/api/admin/settings/currencies?per_page=10" \ -H "Authorization: Bearer " response: | { "data": [{ "id": 1, "code": "USD", "name": "US Dollar", "symbol": "$", "decimal": ".", "groupSeparator": ",", "decimalSeparator": ".", "currencyPosition": "left", "createdAt": "2025-01-01 00:00:00" }], "meta": { "currentPage": 1, "perPage": 10, "lastPage": 1, "total": 1, "from": 1, "to": 1 } } --- # List Currencies | Endpoint | Method | |----------|--------| | `/api/admin/settings/currencies` | GET | ## Query Parameters `page`, `per_page` (default 10, cap 50), `code` (partial), `name` (partial), `symbol` (partial), `sort` (`id`, `code`, `name`), `order` (`asc`/`desc`). --- # Mass Delete Currencies URL: /api/rest-api/admin/settings/currencies/mass-delete --- outline: false apiType: rest examples: - id: admin-settings-currency-mass-delete title: Mass Delete Currencies query: | curl -X POST "https://your-domain.com/api/admin/settings/currencies/mass-delete" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "indices": [3, 4] }' response: | { "deleted": [3, 4], "message": "Currencies deleted." } --- # Mass Delete Currencies ::: warning Whole-batch pre-validation Rejects with HTTP 400 if the batch would empty the currencies table OR if any id is a channel `base_currency_id`. Non-existent IDs silently skipped. Empty `indices` → 422. ::: Permission: `settings.currencies.delete`. --- # Update Currency URL: /api/rest-api/admin/settings/currencies/update --- outline: false apiType: rest examples: - id: admin-settings-currency-update title: Update Currency query: | curl -X PUT "https://your-domain.com/api/admin/settings/currencies/2" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "name": "Euro (EU)" }' response: | { "id": 2, "code": "EUR", "name": "Euro (EU)" } --- # Update Currency | Endpoint | Method | |----------|--------| | `/api/admin/settings/currencies/{id}` | PUT | ::: warning `code` is immutable Mirrors the monolith — `code` is silently dropped from the update payload. Only `name`, `symbol`, separators, and `currency_position` are editable. ::: Permission: `settings.currencies.edit`. --- # Index Import URL: /api/rest-api/admin/settings/data-transfer-imports --- outline: false apiType: rest examples: - id: rest title: Index Import query: | curl -X POST "https://your-domain.com/api/admin/settings/data-transfer/imports/12/index" -H "Authorization: Bearer " response: | { "stats": { "processed": 12, "total": 12, "invalid": 0 }, "import": { "id": 12, "type": "products", "action": "append", "state": "indexed", "processedRowsCount": 12, "invalidRowsCount": 0, "errorsCount": 0, "createdAt": "2026-06-08 09:00:00" } } --- # Index Import | Endpoint | Method | |----------|--------| | `/api/admin/settings/data-transfer/imports/{id}/index` | POST | Runs the indexing stage of the import. This is the final stage, following the linking stage (see [Link](./link.md)), and makes the imported records searchable and visible on the storefront. The `{id}` is the import id. ```json { "stats": { "processed": 12, "total": 12, "invalid": 0 }, "import": { /* ... */ } } ``` The `stats` values above are illustrative. Permission: `settings.data_transfer.imports.edit`. --- # Cancel Import URL: /api/rest-api/admin/settings/data-transfer-imports/cancel --- outline: false apiType: rest examples: - id: rest title: Cancel Import query: | curl -X POST "https://your-domain.com/api/admin/settings/data-transfer/imports/3/cancel" -H "Authorization: Bearer " response: | { "id": 3, "state": "cancelled", "message": "Import cancelled successfully." } --- # Cancel Import Sets state to `cancelled`. ::: warning Terminal-state guard Refuses (HTTP 422) when the import is in a terminal state (`completed`, `processed`, `failed`, `cancelled`). ::: Permission: `settings.data_transfer.imports.edit`. --- # Create Import URL: /api/rest-api/admin/settings/data-transfer-imports/create --- outline: false apiType: rest examples: - id: rest title: Create Import query: | curl -X POST "https://your-domain.com/api/admin/settings/data-transfer/imports" \ -H "Authorization: Bearer " \ -F "type=products" \ -F "action=append" \ -F "validation_strategy=stop-on-errors" \ -F "allowed_errors=0" \ -F "field_separator=," \ -F "process_in_queue=false" \ -F "file=@products.csv" response: | { "id": 12, "type": "products", "action": "append", "state": "pending", "validationStrategy": "stop-on-errors", "allowedErrors": 0, "fieldSeparator": ",", "processedRowsCount": 0, "invalidRowsCount": 0, "errorsCount": 0, "filePath": "imports/products.csv", "createdAt": "2026-06-08 09:00:00" } --- # Create Import | Endpoint | Method | |----------|--------| | `/api/admin/settings/data-transfer/imports` | POST | Uploads a source file and creates a new import. The import is created in the `pending` state, ready to be validated and processed. Send the request as `multipart/form-data`. | Field | Required | Description | |-------|----------|-------------| | `type` | yes | The importer to use, e.g. `products`, `customers`, `tax_rates`. | | `action` | yes | `append` (add / update rows) or `delete` (remove rows). | | `validation_strategy` | yes | `stop-on-errors` (abort the run on the first invalid row) or `skip-errors` (skip invalid rows and continue). | | `allowed_errors` | yes | Integer `≥ 0`. Maximum number of errors tolerated before the run stops. | | `field_separator` | yes | The column delimiter used in the file, e.g. `,`. | | `process_in_queue` | no | When `true`, large imports are processed asynchronously. | | `images_directory_path` | no | Folder containing referenced product images. | | `file` | yes | The import file (`csv`, `xls`, `xlsx`, or `xml`). | Returns the created import detail with HTTP `201`. ::: warning File upload is REST only Creating an import requires uploading a file, which cannot be done over GraphQL. Use this REST endpoint to create imports. ::: Permission: `settings.data_transfer.imports.create`. --- # Delete Import URL: /api/rest-api/admin/settings/data-transfer-imports/delete --- outline: false apiType: rest examples: - id: rest title: Delete Import query: | curl -X DELETE "https://your-domain.com/api/admin/settings/data-transfer/imports/3" -H "Authorization: Bearer " response: | { "message": "Import deleted." } --- # Delete Import Removes the DB row and best-effort deletes the underlying upload file from storage. Permission: `settings.data_transfer.imports.delete`. --- # Import Detail URL: /api/rest-api/admin/settings/data-transfer-imports/detail --- outline: false apiType: rest examples: - id: rest title: Import Detail query: | curl -X GET "https://your-domain.com/api/admin/settings/data-transfer/imports/3" -H "Authorization: Bearer " response: | { "id": 3, "code": "products", "action": "append", "state": "completed", "processed": 150, "summary": { "created": 100, "updated": 50, "deleted": 0 }, "file": { "name": "products.csv", "size": 12345 }, "createdAt": "2026-05-25 09:00:00" } --- # Import Detail --- # Download Source File URL: /api/rest-api/admin/settings/data-transfer-imports/download --- outline: false apiType: rest examples: - id: rest title: Download Source File query: | curl -X GET "https://your-domain.com/api/admin/settings/data-transfer/imports/12/download" \ -H "Authorization: Bearer " \ -H "Accept: application/octet-stream" \ -o products.csv response: | (binary file download — the uploaded source file) --- # Download Source File | Endpoint | Method | |----------|--------| | `/api/admin/settings/data-transfer/imports/{id}/download` | GET | Downloads the source file that was uploaded for this import. The response is the raw file as a binary attachment — send `Accept: application/octet-stream` and write the body to disk with `-o`. The `{id}` is the import id. Returns `404` when the import has no associated file. ::: tip REST only Binary file downloads are not available over GraphQL. ::: Permission: `settings.data_transfer.imports.view`. --- # Download Error Report URL: /api/rest-api/admin/settings/data-transfer-imports/download-error-report --- outline: false apiType: rest examples: - id: rest title: Download Error Report query: | curl -X GET "https://your-domain.com/api/admin/settings/data-transfer/imports/12/download-error-report" \ -H "Authorization: Bearer " \ -H "Accept: application/octet-stream" \ -o error-report.csv response: | (binary file download — the import error report) --- # Download Error Report | Endpoint | Method | |----------|--------| | `/api/admin/settings/data-transfer/imports/{id}/download-error-report` | GET | Downloads the error report generated for an import. The report lists the rows that failed validation or processing along with the reason. The response is a binary attachment — send `Accept: application/octet-stream` and write the body to disk with `-o`. The `{id}` is the import id. Returns `404` when no error report exists for the import. ::: tip REST only Binary file downloads are not available over GraphQL. ::: Permission: `settings.data_transfer.imports.view`. --- # Download Sample Template URL: /api/rest-api/admin/settings/data-transfer-imports/download-sample --- outline: false apiType: rest examples: - id: rest title: Download Sample Template query: | curl -X GET "https://your-domain.com/api/admin/settings/data-transfer/imports/sample/products/csv" \ -H "Authorization: Bearer " \ -H "Accept: application/octet-stream" \ -o products-sample.csv response: | (binary file download — a sample import template for the requested type) --- # Download Sample Template | Endpoint | Method | |----------|--------| | `/api/admin/settings/data-transfer/imports/sample/{type}/{format}` | GET | Downloads a ready-made sample template for an importer type, so you can see the expected columns before preparing your own file. | Path part | Description | |-----------|-------------| | `{type}` | The importer type, e.g. `products`, `customers`, `tax_rates`. | | `{format}` | The file format, e.g. `csv`, `xls`, `xlsx`, `xml`. | For example, `/sample/products/csv` returns a CSV product template. The response is a binary attachment — send `Accept: application/octet-stream` and write the body to disk with `-o`. Returns `422` or `404` for an unknown type or format. ::: tip REST only Binary file downloads are not available over GraphQL. ::: Permission: `settings.data_transfer.imports.view`. --- # Link Import URL: /api/rest-api/admin/settings/data-transfer-imports/link --- outline: false apiType: rest examples: - id: rest title: Link Import query: | curl -X POST "https://your-domain.com/api/admin/settings/data-transfer/imports/12/link" -H "Authorization: Bearer " response: | { "stats": { "processed": 12, "total": 12, "invalid": 0 }, "import": { "id": 12, "type": "products", "action": "append", "state": "linked", "processedRowsCount": 12, "invalidRowsCount": 0, "errorsCount": 0, "createdAt": "2026-06-08 09:00:00" } } --- # Link Import | Endpoint | Method | |----------|--------| | `/api/admin/settings/data-transfer/imports/{id}/link` | POST | Runs the post-process linking stage of the import. This follows the processing stage (see [Start](./start.md)) and resolves relationships between the imported records. The `{id}` is the import id. ```json { "stats": { "processed": 12, "total": 12, "invalid": 0 }, "import": { /* ... */ } } ``` The `stats` values above are illustrative. Permission: `settings.data_transfer.imports.edit`. --- # List Data Transfer Imports URL: /api/rest-api/admin/settings/data-transfer-imports/list --- outline: false apiType: rest examples: - id: rest title: List Imports query: | curl -X GET "https://your-domain.com/api/admin/settings/data-transfer/imports?per_page=10" -H "Authorization: Bearer " response: | { "data": [{ "id": 3, "code": "products", "action": "append", "state": "completed", "processed": 150, "summary": { "created": 100, "updated": 50, "deleted": 0 }, "createdAt": "2026-05-25 09:00:00" }], "meta": { "currentPage": 1, "perPage": 10, "lastPage": 1, "total": 1, "from": 1, "to": 1 } } --- # List Data Transfer Imports | Endpoint | Method | |----------|--------| | `/api/admin/settings/data-transfer/imports` | GET | Filters: `code` (entity type), `type` (synonym for code), `action`, `state`, `created_at_from`, `created_at_to`. Sort: `id` (default desc), `state`, `created_at`. --- # Start Import URL: /api/rest-api/admin/settings/data-transfer-imports/start --- outline: false apiType: rest examples: - id: rest title: Start Import query: | curl -X POST "https://your-domain.com/api/admin/settings/data-transfer/imports/12/start" -H "Authorization: Bearer " response: | { "stats": { "processed": 10, "total": 12, "invalid": 0 }, "import": { "id": 12, "type": "products", "action": "append", "state": "processing", "processedRowsCount": 10, "invalidRowsCount": 0, "errorsCount": 0, "createdAt": "2026-06-08 09:00:00" } } --- # Start Import | Endpoint | Method | |----------|--------| | `/api/admin/settings/data-transfer/imports/{id}/start` | POST | Processes the next pending batch of rows. Call this repeatedly until there are no pending batches left — each call advances the import and returns progress in the `stats` object. The `{id}` is the import id. ```json { "stats": { "processed": 10, "total": 12, "invalid": 0 }, "import": { /* ... */ } } ``` The `stats` values above are illustrative. ## Import lifecycle A full import runs through these stages, in order: 1. **Create** the import (uploads the file). 2. **Validate** the file. 3. **Start** — processes one batch per call; repeat until no pending batch remains. 4. **Link** — runs the post-process linking stage. 5. **Index** — runs the indexing stage. ## Errors | Condition | Status | |-----------|--------| | Nothing left to import | `400` | | Import has not been validated / is not valid | `400` | | `process_in_queue` requested but the queue is not configured | `400` | Permission: `settings.data_transfer.imports.edit`. --- # Import Stats URL: /api/rest-api/admin/settings/data-transfer-imports/stats --- outline: false apiType: rest examples: - id: rest title: Import Stats query: | curl -X GET "https://your-domain.com/api/admin/settings/data-transfer/imports/12/stats?state=processed" -H "Authorization: Bearer " response: | { "stats": { "processed": 12, "total": 12, "invalid": 0 }, "import": { "id": 12, "type": "products", "action": "append", "state": "processed", "processedRowsCount": 12, "invalidRowsCount": 0, "errorsCount": 0, "createdAt": "2026-06-08 09:00:00" } } --- # Import Stats | Endpoint | Method | |----------|--------| | `/api/admin/settings/data-transfer/imports/{id}/stats` | GET | Returns the current progress of an import without advancing it. Use the optional `state` query parameter (e.g. `processed`) to scope the counts to a particular processing state. The `{id}` is the import id. ```json { "stats": { "processed": 12, "total": 12, "invalid": 0 }, "import": { /* ... */ } } ``` The `stats` values above are illustrative. Permission: `settings.data_transfer.imports.view`. --- # Update Import URL: /api/rest-api/admin/settings/data-transfer-imports/update --- outline: false apiType: rest examples: - id: rest title: Update Import query: | curl -X PUT "https://your-domain.com/api/admin/settings/data-transfer/imports/12" \ -H "Authorization: Bearer " \ -F "type=products" \ -F "action=append" \ -F "validation_strategy=skip-errors" \ -F "allowed_errors=10" \ -F "field_separator=," \ -F "file=@products-revised.csv" response: | { "id": 12, "type": "products", "action": "append", "state": "pending", "validationStrategy": "skip-errors", "allowedErrors": 10, "fieldSeparator": ",", "processedRowsCount": 0, "invalidRowsCount": 0, "errorsCount": 0, "filePath": "imports/products-revised.csv", "createdAt": "2026-06-08 09:00:00" } --- # Update Import | Endpoint | Method | |----------|--------| | `/api/admin/settings/data-transfer/imports/{id}` | PUT | Re-saves an import's configuration. The import is reset back to `pending` and any earlier processing counts and error report are cleared. The `{id}` is the import id. Send the request as `multipart/form-data`. The `file` field is optional — when a new file is sent it replaces the previous source file; when omitted the existing file is kept. | Field | Required | Description | |-------|----------|-------------| | `type` | yes | The importer to use, e.g. `products`, `customers`, `tax_rates`. | | `action` | yes | `append` or `delete`. | | `validation_strategy` | yes | `stop-on-errors` or `skip-errors`. | | `allowed_errors` | yes | Integer `≥ 0`. | | `field_separator` | yes | The column delimiter, e.g. `,`. | | `process_in_queue` | no | When `true`, large imports are processed asynchronously. | | `images_directory_path` | no | Folder containing referenced product images. | | `file` | no | A replacement import file (`csv`, `xls`, `xlsx`, or `xml`). | Returns the updated import detail. Permission: `settings.data_transfer.imports.edit`. --- # Validate Import URL: /api/rest-api/admin/settings/data-transfer-imports/validate --- outline: false apiType: rest examples: - id: rest title: Validate Import query: | curl -X POST "https://your-domain.com/api/admin/settings/data-transfer/imports/12/validate" -H "Authorization: Bearer " response: | { "is_valid": true, "import": { "id": 12, "type": "products", "action": "append", "state": "validated", "validationStrategy": "stop-on-errors", "allowedErrors": 0, "processedRowsCount": 0, "invalidRowsCount": 0, "errorsCount": 0, "createdAt": "2026-06-08 09:00:00" } } --- # Validate Import | Endpoint | Method | |----------|--------| | `/api/admin/settings/data-transfer/imports/{id}/validate` | POST | Runs validation over the uploaded file without importing any data. This is the second step of the import lifecycle (after the import is created). The `{id}` is the import id. The response carries an `is_valid` flag and the refreshed import object: ```json { "is_valid": true, "import": { /* ... */ } } ``` When `is_valid` is `false`, inspect the import's error counts and download the error report to see which rows failed. Permission: `settings.data_transfer.imports.edit`. --- # Create Exchange Rate URL: /api/rest-api/admin/settings/exchange-rates/create --- outline: false apiType: rest examples: - id: rest title: Create Exchange Rate query: | curl -X POST "https://your-domain.com/api/admin/settings/exchange-rates" -H "Authorization: Bearer " -H "Content-Type: application/json" -d '{ "target_currency": 2, "rate": 0.92 }' response: | { "id": 1, "targetCurrency": 2, "rate": 0.92 } --- # Create Exchange Rate ## Request Body | Field | Type | Required | Notes | |-------|------|----------|-------| | `target_currency` | integer | yes | Currency ID. Must exist. Composite-unique on `currency_exchange_rates.target_currency`. | | `rate` | number | yes | Must be `> 0`. | ::: tip Source currency is implicit There is **no** `source_currency` column. The source is the channel's base currency; only the target/rate pair is stored. ::: ::: warning Auto-sync deferred The admin "Mass Auto-Sync" action (`ExchangeRateController::updateRates`) which calls an external rate-source helper is **intentionally not exposed in v1** — wiring that requires deciding how to surface provider-side errors. Will be revisited if there's integrator demand; for now the admin UI remains the entry point. ::: Fires `core.exchange_rate.create.before/after`. Permission: `settings.exchange_rates.create`. --- # Delete Exchange Rate URL: /api/rest-api/admin/settings/exchange-rates/delete --- outline: false apiType: rest examples: - id: rest title: Delete Exchange Rate query: | curl -X DELETE "https://your-domain.com/api/admin/settings/exchange-rates/1" -H "Authorization: Bearer " response: | { "message": "Exchange rate deleted." } --- # Delete Exchange Rate Returns 200 + message. Permission: `settings.exchange_rates.delete`. --- # Exchange Rate Detail URL: /api/rest-api/admin/settings/exchange-rates/detail --- outline: false apiType: rest examples: - id: rest title: Exchange Rate Detail query: | curl -X GET "https://your-domain.com/api/admin/settings/exchange-rates/1" -H "Authorization: Bearer " response: | { "id": 1, "targetCurrency": 2, "targetCurrencyCode": "EUR", "targetCurrencyName": "Euro", "rate": 0.92 } --- # Exchange Rate Detail | Endpoint | Method | |----------|--------| | `/api/admin/settings/exchange-rates/{id}` | GET | --- # List Exchange Rates URL: /api/rest-api/admin/settings/exchange-rates/list --- outline: false apiType: rest examples: - id: rest title: List Exchange Rates query: | curl -X GET "https://your-domain.com/api/admin/settings/exchange-rates?per_page=10" -H "Authorization: Bearer " response: | { "data": [{ "id": 1, "targetCurrency": 2, "targetCurrencyCode": "EUR", "targetCurrencyName": "Euro", "rate": 0.92 }], "meta": { "currentPage": 1, "perPage": 10, "lastPage": 1, "total": 1, "from": 1, "to": 1 } } --- # List Exchange Rates | Endpoint | Method | |----------|--------| | `/api/admin/settings/exchange-rates` | GET | Joins `currency_exchange_rates × currencies` so each row carries `targetCurrencyCode` + `targetCurrencyName` inline. Filters: `target_currency`, `rate_from`/`rate_to`. Sort: `id`, `target_currency`, `rate`. --- # Mass Delete Exchange Rates URL: /api/rest-api/admin/settings/exchange-rates/mass-delete --- outline: false apiType: rest examples: - id: rest title: Mass Delete Exchange Rates query: | curl -X POST "https://your-domain.com/api/admin/settings/exchange-rates/mass-delete" -H "Authorization: Bearer " -H "Content-Type: application/json" -d '{ "indices": [1, 2] }' response: | { "deleted": [1, 2], "message": "Exchange rates deleted." } --- # Mass Delete Exchange Rates Non-existent IDs silently skipped. Empty `indices` → 422. Permission: `settings.exchange_rates.delete`. --- # Update Exchange Rate URL: /api/rest-api/admin/settings/exchange-rates/update --- outline: false apiType: rest examples: - id: rest title: Update Exchange Rate query: | curl -X PUT "https://your-domain.com/api/admin/settings/exchange-rates/1" -H "Authorization: Bearer " -H "Content-Type: application/json" -d '{ "rate": 0.94 }' response: | { "id": 1, "targetCurrency": 2, "rate": 0.94 } --- # Update Exchange Rate Partial-update friendly. Composite-uniqueness excludes self. Permission: `settings.exchange_rates.edit`. --- # Update Rates (auto-sync) URL: /api/rest-api/admin/settings/exchange-rates/update-rates --- outline: false apiType: rest examples: - id: rest title: Update Rates (auto-sync) query: | curl -X POST "https://your-domain.com/api/admin/settings/exchange-rates/update-rates" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" response: | { "success": true, "updated": 3, "message": "Exchange rates updated successfully." } --- # Update Rates (auto-sync) Refreshes every non-base currency's exchange rate from the store's configured external rate provider — the API equivalent of the **Update Rates** button on the Exchange Rates screen. - Takes no request body. - On success returns `{ success, updated, message }`, where `updated` is the number of exchange-rate rows present after the sync. - If the external provider fails (for example a missing or invalid API key, or a network error), the endpoint returns `422` with the provider's own error message. - The provider is configured server-side; this endpoint does not choose or accept a provider. Permission: `settings.exchange_rates.edit`. --- # Create Inventory Source URL: /api/rest-api/admin/settings/inventory-sources/create --- outline: false apiType: rest examples: - id: rest title: Create Inventory Source query: | curl -X POST "https://your-domain.com/api/admin/settings/inventory-sources" -H "Authorization: Bearer " -H "Content-Type: application/json" -d '{ "code": "warehouse-east", "name": "East Coast Warehouse", "contact_name": "Ops", "contact_email": "east@example.com", "contact_number": "+15551112222", "country": "US", "state": "NY", "city": "Brooklyn", "street": "123 Front St", "postcode": "11201", "priority": 1, "status": 1 }' response: | { "id": 2, "code": "warehouse-east", "name": "East Coast Warehouse" } --- # Create Inventory Source Required: `code` (unique alpha-dash), `name`, `contact_name`, `contact_number`, `country`, `state`, `city`, `street`, `postcode`. Optional: `description`, `contact_email`, `contact_fax`, `latitude`, `longitude`, `priority`, `status` (`0`/`1`). Fires `inventory.inventory_source.create.before/after`. Permission: `settings.inventory_sources.create`. --- # Delete Inventory Source URL: /api/rest-api/admin/settings/inventory-sources/delete --- outline: false apiType: rest examples: - id: rest title: Delete Inventory Source query: | curl -X DELETE "https://your-domain.com/api/admin/settings/inventory-sources/2" -H "Authorization: Bearer " response: | { "message": "Inventory source deleted." } --- # Delete Inventory Source ::: warning Two guards (HTTP 400) - **Last source** — refuses if this is the only inventory source left (parity with monolith). - **FK guard** — refuses if `product_inventories.inventory_source_id` references it. API-specific safeguard. ::: Permission: `settings.inventory_sources.delete`. --- # Inventory Source Detail URL: /api/rest-api/admin/settings/inventory-sources/detail --- outline: false apiType: rest examples: - id: rest title: Inventory Source Detail query: | curl -X GET "https://your-domain.com/api/admin/settings/inventory-sources/1" -H "Authorization: Bearer " response: | { "id": 1, "code": "default", "name": "Default Warehouse", "description": "Primary", "contactName": "Ops", "contactEmail": "ops@example.com", "contactNumber": "+15551112222", "country": "US", "state": "IL", "city": "Springfield", "street": "742 Evergreen", "postcode": "62704", "latitude": 39.78, "longitude": -89.65, "priority": 1, "status": 1 } --- # Inventory Source Detail | Endpoint | Method | |----------|--------| | `/api/admin/settings/inventory-sources/{id}` | GET | --- # List Inventory Sources URL: /api/rest-api/admin/settings/inventory-sources/list --- outline: false apiType: rest examples: - id: rest title: List Inventory Sources query: | curl -X GET "https://your-domain.com/api/admin/settings/inventory-sources?per_page=10" -H "Authorization: Bearer " response: | { "data": [{ "id": 1, "code": "default", "name": "Default Warehouse", "priority": 1, "status": 1, "country": "US", "city": "Springfield" }], "meta": { "currentPage": 1, "perPage": 10, "lastPage": 1, "total": 1, "from": 1, "to": 1 } } --- # List Inventory Sources | Endpoint | Method | |----------|--------| | `/api/admin/settings/inventory-sources` | GET | Filters: `code`, `name` (partial), `status` (0/1), `country`. Sort: `id`, `code`, `name`, `priority`, `status`. --- # Mass Delete Inventory Sources URL: /api/rest-api/admin/settings/inventory-sources/mass-delete --- outline: false apiType: rest examples: - id: rest title: Mass Delete Inventory Sources query: | curl -X POST "https://your-domain.com/api/admin/settings/inventory-sources/mass-delete" -H "Authorization: Bearer " -H "Content-Type: application/json" -d '{ "indices": [3, 4] }' response: | { "deleted": [4], "skipped": [{ "id": 3, "reason": "In use by product_inventories" }], "message": "Inventory sources processed." } --- # Mass Delete Inventory Sources Pre-validates the whole batch: 400 if delete would leave zero sources OR if any id is referenced by `product_inventories`. Empty `indices` → 422. --- # Update Inventory Source URL: /api/rest-api/admin/settings/inventory-sources/update --- outline: false apiType: rest examples: - id: rest title: Update Inventory Source query: | curl -X PUT "https://your-domain.com/api/admin/settings/inventory-sources/2" -H "Authorization: Bearer " -H "Content-Type: application/json" -d '{ "name": "East Coast Warehouse (NY)" }' response: | { "id": 2, "name": "East Coast Warehouse (NY)" } --- # Update Inventory Source Partial. `code` uniqueness excludes self. Permission: `settings.inventory_sources.edit`. --- # Create Locale URL: /api/rest-api/admin/settings/locales/create --- outline: false apiType: rest examples: - id: rest title: Create Locale query: | curl -X POST "https://your-domain.com/api/admin/settings/locales" -H "Authorization: Bearer " -H "Content-Type: application/json" -d '{ "code": "fr", "name": "French", "direction": "ltr" }' response: | { "id": 2, "code": "fr", "name": "French", "direction": "ltr" } --- # Create Locale ## Request Body | Field | Type | Notes | |-------|------|-------| | `code` | string | Required, unique, regex `^[a-z0-9_-]+$`. | | `name` | string | Required. | | `direction` | enum | Required, `ltr` or `rtl`. | | `logo_path` | string | Optional storage path (binary upload deferred — see warning). | ::: warning Image upload deferred (v1) `logo_path` is accepted only as a path string. Multipart binary upload for locale logos is **not yet supported via the API** — use the admin panel for now. Fires `core.locale.create.before/after`. ::: Permission: `settings.locales.create`. --- # Delete Locale URL: /api/rest-api/admin/settings/locales/delete --- outline: false apiType: rest examples: - id: rest title: Delete Locale query: | curl -X DELETE "https://your-domain.com/api/admin/settings/locales/2" -H "Authorization: Bearer " response: | { "message": "Locale deleted." } --- # Delete Locale ::: warning Two guards (HTTP 400) - **Last locale** — refuses if this is the only locale left. - **Channel default** — refuses if any channel references it as `default_locale_id`. (Project-specific safeguard — the monolith silently breaks the channel in this case.) ::: Permission: `settings.locales.delete`. --- # Locale Detail URL: /api/rest-api/admin/settings/locales/detail --- outline: false apiType: rest examples: - id: rest title: Locale Detail query: | curl -X GET "https://your-domain.com/api/admin/settings/locales/1" -H "Authorization: Bearer " response: | { "id": 1, "code": "en", "name": "English", "direction": "ltr", "logoPath": null, "logoUrl": null } --- # Locale Detail | Endpoint | Method | |----------|--------| | `/api/admin/settings/locales/{id}` | GET | --- # List Locales URL: /api/rest-api/admin/settings/locales/list --- outline: false apiType: rest examples: - id: rest title: List Locales query: | curl -X GET "https://your-domain.com/api/admin/settings/locales?per_page=10" -H "Authorization: Bearer " response: | { "data": [{ "id": 1, "code": "en", "name": "English", "direction": "ltr", "logoPath": null, "logoUrl": null }], "meta": { "currentPage": 1, "perPage": 10, "lastPage": 1, "total": 1, "from": 1, "to": 1 } } --- # List Locales | Endpoint | Method | |----------|--------| | `/api/admin/settings/locales` | GET | Filters: `code` (partial), `name` (partial), `direction` (`ltr`/`rtl`). Sort: `id`, `code`, `name`. --- # Mass Delete Locales URL: /api/rest-api/admin/settings/locales/mass-delete --- outline: false apiType: rest examples: - id: rest title: Mass Delete Locales query: | curl -X POST "https://your-domain.com/api/admin/settings/locales/mass-delete" -H "Authorization: Bearer " -H "Content-Type: application/json" -d '{ "indices": [3, 4] }' response: | { "deleted": [4], "skipped": [{ "id": 3, "reason": "Channel default" }], "message": "Locales processed." } --- # Mass Delete Locales Per-id guards (last-locale, channel-default) skip with a reason in `skipped`. Empty `indices` → 422. Permission: `settings.locales.delete`. --- # Update Locale URL: /api/rest-api/admin/settings/locales/update --- outline: false apiType: rest examples: - id: rest title: Update Locale query: | curl -X PUT "https://your-domain.com/api/admin/settings/locales/2" -H "Authorization: Bearer " -H "Content-Type: application/json" -d '{ "name": "Français" }' response: | { "id": 2, "code": "fr", "name": "Français" } --- # Update Locale Partial. `code` uniqueness excludes self. Image upload deferred. Permission: `settings.locales.edit`. --- # Create Role URL: /api/rest-api/admin/settings/roles/create --- outline: false apiType: rest examples: - id: rest title: Create Role query: | curl -X POST "https://your-domain.com/api/admin/settings/roles" -H "Authorization: Bearer " -H "Content-Type: application/json" -d '{ "name": "Catalog Manager", "description": "Can manage catalog only", "permission_type": "custom", "permissions": ["catalog.products.view", "catalog.products.edit"] }' response: | { "id": 3, "name": "Catalog Manager", "permissionType": "custom", "permissions": ["catalog.products.view", "catalog.products.edit"] } --- # Create Role | Field | Type | Required | Notes | |-------|------|----------|-------| | `name` | string | yes | | | `description` | string | yes | | | `permission_type` | enum | yes | `all` or `custom`. | | `permissions` | string[] | conditional | Required when `permission_type=custom`. | Permission: `settings.roles.create`. --- # Delete Role URL: /api/rest-api/admin/settings/roles/delete --- outline: false apiType: rest examples: - id: rest title: Delete Role query: | curl -X DELETE "https://your-domain.com/api/admin/settings/roles/3" -H "Authorization: Bearer " response: | { "message": "Role deleted." } --- # Delete Role ::: warning Two guards (HTTP 400) - **In use** — refuses if any admin (`admins.role_id`) references this role. - **Last role** — refuses if this is the only role remaining. ::: Permission: `settings.roles.delete`. --- # Role Detail URL: /api/rest-api/admin/settings/roles/detail --- outline: false apiType: rest examples: - id: rest title: Role Detail query: | curl -X GET "https://your-domain.com/api/admin/settings/roles/1" -H "Authorization: Bearer " response: | { "id": 1, "name": "Administrator", "description": "Full access", "permissionType": "all", "permissions": null } --- # Role Detail --- # List Roles URL: /api/rest-api/admin/settings/roles/list --- outline: false apiType: rest examples: - id: rest title: List Roles query: | curl -X GET "https://your-domain.com/api/admin/settings/roles" -H "Authorization: Bearer " response: | { "data": [{ "id": 1, "name": "Administrator", "description": "Full access", "permissionType": "all", "permissions": null }], "meta": { "currentPage": 1, "perPage": 10, "lastPage": 1, "total": 1, "from": 1, "to": 1 } } --- # List Roles --- # Update Role URL: /api/rest-api/admin/settings/roles/update --- outline: false apiType: rest examples: - id: rest title: Update Role query: | curl -X PUT "https://your-domain.com/api/admin/settings/roles/3" -H "Authorization: Bearer " -H "Content-Type: application/json" -d '{ "name": "Catalog Manager+", "description": "Updated", "permission_type": "custom", "permissions": ["catalog.products.view", "catalog.products.edit", "catalog.categories.view"] }' response: | { "id": 3, "name": "Catalog Manager+", "permissions": ["catalog.products.view", "catalog.products.edit", "catalog.categories.view"] } --- # Update Role --- # Create Tax Category URL: /api/rest-api/admin/settings/tax-categories/create --- outline: false apiType: rest examples: - id: rest title: Create Tax Category query: | curl -X POST "https://your-domain.com/api/admin/settings/tax-categories" -H "Authorization: Bearer " -H "Content-Type: application/json" -d '{ "code": "us-tax", "name": "US Tax", "description": "Standard US sales tax", "taxrates": [1, 2] }' response: | { "id": 1, "code": "us-tax", "name": "US Tax", "taxRates": [1, 2] } --- # Create Tax Category Required: `code` (unique), `name`, `description`, `taxrates` (array of tax_rate ids). Permission: `settings.taxes.tax_categories.create`. --- # Delete Tax Category URL: /api/rest-api/admin/settings/tax-categories/delete --- outline: false apiType: rest examples: - id: rest title: Delete Tax Category query: | curl -X DELETE "https://your-domain.com/api/admin/settings/tax-categories/1" -H "Authorization: Bearer " response: | { "message": "Tax category deleted." } --- # Delete Tax Category ::: warning Guard Mirrors monolith `TaxCategoryController::destroy` — refuses with HTTP 400 if any tax_rates are still attached to the category. ::: --- # Tax Category Detail URL: /api/rest-api/admin/settings/tax-categories/detail --- outline: false apiType: rest examples: - id: rest title: Tax Category Detail query: | curl -X GET "https://your-domain.com/api/admin/settings/tax-categories/1" -H "Authorization: Bearer " response: | { "id": 1, "code": "us-tax", "name": "US Tax", "description": "Standard US sales tax", "taxRates": [1, 2] } --- # Tax Category Detail --- # List Tax Categories URL: /api/rest-api/admin/settings/tax-categories/list --- outline: false apiType: rest examples: - id: rest title: List Tax Categories query: | curl -X GET "https://your-domain.com/api/admin/settings/tax-categories" -H "Authorization: Bearer " response: | { "data": [{ "id": 1, "code": "us-tax", "name": "US Tax", "description": "Standard US sales tax", "taxRates": [1, 2] }], "meta": { "currentPage": 1, "perPage": 10, "lastPage": 1, "total": 1, "from": 1, "to": 1 } } --- # List Tax Categories --- # Update Tax Category URL: /api/rest-api/admin/settings/tax-categories/update --- outline: false apiType: rest examples: - id: rest title: Update Tax Category query: | curl -X PUT "https://your-domain.com/api/admin/settings/tax-categories/1" -H "Authorization: Bearer " -H "Content-Type: application/json" -d '{ "code": "us-tax", "name": "US Sales Tax", "description": "Updated", "taxrates": [1, 2, 3] }' response: | { "id": 1, "code": "us-tax", "name": "US Sales Tax", "taxRates": [1, 2, 3] } --- # Update Tax Category Code uniqueness excludes self. Re-syncs the `tax_rates` pivot to the supplied list. --- # Create Tax Rate URL: /api/rest-api/admin/settings/tax-rates/create --- outline: false apiType: rest examples: - id: rest title: Create Tax Rate (specific ZIP, is_zip=false) query: | curl -X POST "https://your-domain.com/api/admin/settings/tax-rates" -H "Authorization: Bearer " -H "Content-Type: application/json" -d '{ "identifier": "us-il-7", "tax_rate": 7.25, "country": "US", "state": "IL", "is_zip": false, "zip_code": "62704" }' response: | { "id": 1, "identifier": "us-il-7", "taxRate": 7.25 } - id: rest-zip-range title: Create Tax Rate (ZIP range, is_zip=true) query: | curl -X POST "https://your-domain.com/api/admin/settings/tax-rates" -H "Authorization: Bearer " -H "Content-Type: application/json" -d '{ "identifier": "us-il-zip-range", "tax_rate": 7.25, "country": "US", "state": "IL", "is_zip": true, "zip_from": "60000", "zip_to": "62999" }' response: | { "id": 2, "identifier": "us-il-zip-range" } --- # Create Tax Rate Body fields differ by `is_zip`: - `is_zip=false` — pass `zip_code`. - `is_zip=true` — pass `zip_from` + `zip_to`. `identifier` must be unique. Permission: `settings.taxes.tax_rates.create`. --- # Delete Tax Rate URL: /api/rest-api/admin/settings/tax-rates/delete --- outline: false apiType: rest examples: - id: rest title: Delete Tax Rate query: | curl -X DELETE "https://your-domain.com/api/admin/settings/tax-rates/1" -H "Authorization: Bearer " response: | { "message": "Tax rate deleted." } --- # Delete Tax Rate Tax category pivot cascades automatically. Permission: `settings.taxes.tax_rates.delete`. --- # Tax Rate Detail URL: /api/rest-api/admin/settings/tax-rates/detail --- outline: false apiType: rest examples: - id: rest title: Tax Rate Detail query: | curl -X GET "https://your-domain.com/api/admin/settings/tax-rates/1" -H "Authorization: Bearer " response: | { "id": 1, "identifier": "us-il-7", "taxRate": 7.25, "country": "US", "state": "IL", "isZip": false, "zipCode": "62704" } --- # Tax Rate Detail --- # Export Tax Rates (CSV) URL: /api/rest-api/admin/settings/tax-rates/export --- outline: false apiType: rest examples: - id: rest title: Export Tax Rates (CSV) query: | curl -X GET "https://your-domain.com/api/admin/settings/tax-rates/export?format=csv" \ -H "Authorization: Bearer " \ -H "Accept: text/csv" \ -o tax-rates.csv response: | ID,Identifier,State,Country,"Zip Code","Zip From","Zip To","Tax Rate" 3,us-ca-bay,CA,US,,94000,94999,8.25 1,us-il-7,IL,US,62704,,,7.25 --- # Export Tax Rates (CSV) Downloads the Tax Rates list as a CSV file — the API equivalent of the **Export** button on the Tax Rates screen. - Send `Accept: text/csv`. The response is a `text/csv` attachment named `tax-rates.csv`. - Columns: ID, Identifier, State, Country, Zip Code, Zip From, Zip To, Tax Rate. - Honours the **same filters** as the [list endpoint](./list) — `identifier`, `country`, `state`, `tax_rate_from`, `tax_rate_to`. The export returns every matching row, not just the current page. - `?format=` accepts only `csv`; any other value returns `422`. - This endpoint is REST only (there is no GraphQL equivalent for binary downloads). Permission: `settings.taxes.tax_rates`. --- # List Tax Rates URL: /api/rest-api/admin/settings/tax-rates/list --- outline: false apiType: rest examples: - id: rest title: List Tax Rates query: | curl -X GET "https://your-domain.com/api/admin/settings/tax-rates" -H "Authorization: Bearer " response: | { "data": [{ "id": 1, "identifier": "us-il-7", "taxRate": 7.25, "country": "US", "state": "IL", "isZip": false, "zipCode": "62704" }], "meta": { "currentPage": 1, "perPage": 10, "lastPage": 1, "total": 1, "from": 1, "to": 1 } } --- # List Tax Rates --- # Update Tax Rate URL: /api/rest-api/admin/settings/tax-rates/update --- outline: false apiType: rest examples: - id: rest title: Update Tax Rate query: | curl -X PUT "https://your-domain.com/api/admin/settings/tax-rates/1" -H "Authorization: Bearer " -H "Content-Type: application/json" -d '{ "tax_rate": 7.5 }' response: | { "id": 1, "taxRate": 7.5 } --- # Update Tax Rate Partial. `identifier` uniqueness excludes self. Permission: `settings.taxes.tax_rates.edit`. --- # Create Theme Customization (Step 1) URL: /api/rest-api/admin/settings/themes/create --- outline: false apiType: rest examples: - id: rest title: Create Theme Customization (Step 1) query: | curl -X POST "https://your-domain.com/api/admin/settings/themes" -H "Authorization: Bearer " -H "Content-Type: application/json" -d '{ "name": "Homepage Banner", "type": "image_carousel", "sort_order": 1, "channel_id": 1, "theme_code": "default" }' response: | { "id": 1, "name": "Homepage Banner", "type": "image_carousel" } --- # Create Theme Customization (Step 1) Step-1: name, type, sort_order, channel_id, theme_code. Per-locale `options` are configured via PUT. ::: warning Theme uploads deferred Multipart binary upload for theme images is **not yet supported via the API** — use the admin panel. ::: Permission: `settings.themes.create`. --- # Delete Theme Customization URL: /api/rest-api/admin/settings/themes/delete --- outline: false apiType: rest examples: - id: rest title: Delete Theme Customization query: | curl -X DELETE "https://your-domain.com/api/admin/settings/themes/1" -H "Authorization: Bearer " response: | { "message": "Theme deleted." } --- # Delete Theme Customization Permission: `settings.themes.delete`. --- # Theme Customization Detail URL: /api/rest-api/admin/settings/themes/detail --- outline: false apiType: rest examples: - id: rest title: Theme Customization Detail query: | curl -X GET "https://your-domain.com/api/admin/settings/themes/1" -H "Authorization: Bearer " response: | { "id": 1, "name": "Homepage Banner", "type": "image_carousel", "sortOrder": 1, "channelId": 1, "themeCode": "default", "status": 1, "translations": [{ "locale": "en", "options": {} }] } --- # Theme Customization Detail --- # List Theme Customizations URL: /api/rest-api/admin/settings/themes/list --- outline: false apiType: rest examples: - id: rest title: List Theme Customizations query: | curl -X GET "https://your-domain.com/api/admin/settings/themes" -H "Authorization: Bearer " response: | { "data": [{ "id": 1, "name": "Homepage Banner", "type": "image_carousel", "sortOrder": 1, "channelId": 1, "themeCode": "default", "status": 1 }], "meta": { "currentPage": 1, "perPage": 10, "lastPage": 1, "total": 1, "from": 1, "to": 1 } } --- # List Theme Customizations --- # Mass Delete Theme Customizations URL: /api/rest-api/admin/settings/themes/mass-delete --- outline: false apiType: rest examples: - id: rest title: Mass Delete Theme Customizations query: | curl -X POST "https://your-domain.com/api/admin/settings/themes/mass-delete" -H "Authorization: Bearer " -H "Content-Type: application/json" -d '{ "indices": [1, 2] }' response: | { "deleted": [1, 2], "message": "Themes deleted." } --- # Mass Delete Theme Customizations Empty indices → 422. --- # Mass Update Theme Status URL: /api/rest-api/admin/settings/themes/mass-update-status --- outline: false apiType: rest examples: - id: rest title: Mass Update Theme Status query: | curl -X POST "https://your-domain.com/api/admin/settings/themes/mass-update-status" -H "Authorization: Bearer " -H "Content-Type: application/json" -d '{ "indices": [1, 2], "value": 0 }' response: | { "updated": [1, 2], "value": 0, "message": "Statuses updated." } --- # Mass Update Theme Status Body: `{ indices: int[], value: 0|1 }`. --- # Update Theme Customization URL: /api/rest-api/admin/settings/themes/update --- outline: false apiType: rest examples: - id: rest title: Update Theme Customization query: | curl -X PUT "https://your-domain.com/api/admin/settings/themes/1" -H "Authorization: Bearer " -H "Content-Type: application/json" -d '{ "locale": "en", "options": { "title": "Welcome to our store!" } }' response: | { "id": 1, "name": "Homepage Banner" } --- # Update Theme Customization Pass `locale` + `options` to write per-locale options. Permission: `settings.themes.edit`. --- # Create Admin User URL: /api/rest-api/admin/settings/users/create --- outline: false apiType: rest examples: - id: rest title: Create Admin User query: | curl -X POST "https://your-domain.com/api/admin/settings/users" -H "Authorization: Bearer " -H "Content-Type: application/json" -d '{ "name": "Ops User", "email": "ops@example.com", "password": "secret123", "password_confirmation": "secret123", "role_id": 2, "status": 1 }' response: | { "id": 4, "name": "Ops User", "email": "ops@example.com", "roleId": 2, "status": 1 } --- # Create Admin User Required: `name`, `email` (unique), `password` (+ `password_confirmation`), `role_id`. Optional: `status` (`0`/`1`). ::: warning Image upload deferred Admin user profile-image upload is not yet supported via the API. ::: Permission: `settings.users.create`. --- # Delete Admin User URL: /api/rest-api/admin/settings/users/delete --- outline: false apiType: rest examples: - id: rest title: Delete Admin User query: | curl -X DELETE "https://your-domain.com/api/admin/settings/users/4" -H "Authorization: Bearer " response: | { "message": "Admin deleted." } --- # Delete Admin User ::: warning Two guards (HTTP 400) - **Self-delete** — refuses if the caller is deleting themselves. - **Last admin** — refuses if this is the only admin remaining. ::: Permission: `settings.users.delete`. --- # Delete My Account URL: /api/rest-api/admin/settings/users/delete-self --- outline: false apiType: rest examples: - id: rest title: Delete My Account query: | curl -X POST "https://your-domain.com/api/admin/settings/users/delete-self" \ -H "Authorization: Bearer " \ -H "Content-Type: application/json" \ -d '{ "password": "current-password" }' response: | { "success": true, "message": "Your admin account has been deleted." } --- # Delete My Account Deletes the **authenticated** admin's own account after re-confirming their password — the API equivalent of the "delete my account" action on the admin profile. - Requires the caller's current `password`. A missing or incorrect password returns `422`. - Refuses to delete the **last remaining** admin (`400`). - This is distinct from [Delete User](./delete) (`DELETE /settings/users/{id}`), which deletes **another** admin and always refuses self-deletion. - Deleting the account also invalidates the token that owns it. No additional permission is required beyond authentication — the password confirmation is the gate. --- # Admin User Detail URL: /api/rest-api/admin/settings/users/detail --- outline: false apiType: rest examples: - id: rest title: Admin User Detail query: | curl -X GET "https://your-domain.com/api/admin/settings/users/1" -H "Authorization: Bearer " response: | { "id": 1, "name": "Super Admin", "email": "admin@example.com", "roleId": 1, "status": 1 } --- # Admin User Detail --- # List Admin Users URL: /api/rest-api/admin/settings/users/list --- outline: false apiType: rest examples: - id: rest title: List Admin Users query: | curl -X GET "https://your-domain.com/api/admin/settings/users" -H "Authorization: Bearer " response: | { "data": [{ "id": 1, "name": "Super Admin", "email": "admin@example.com", "roleId": 1, "status": 1 }], "meta": { "currentPage": 1, "perPage": 10, "lastPage": 1, "total": 1, "from": 1, "to": 1 } } --- # List Admin Users | Endpoint | Method | |----------|--------| | `/api/admin/settings/users` | GET | --- # Update Admin User URL: /api/rest-api/admin/settings/users/update --- outline: false apiType: rest examples: - id: rest title: Update Admin User query: | curl -X PUT "https://your-domain.com/api/admin/settings/users/4" -H "Authorization: Bearer " -H "Content-Type: application/json" -d '{ "name": "Ops User (Updated)" }' response: | { "id": 4, "name": "Ops User (Updated)" } --- # Update Admin User Partial. Permission: `settings.users.edit`. --- # Authentication URL: /api/rest-api/authentication # Authentication Complete guide to API authentication methods including customer authentication, admin authentication, and token management using the REST API. ## Customer Authentication ### Customer Registration Register a new customer account. **Endpoint:** ``` POST /api/shop/customers ``` :::tabs == cURL ```bash curl -X POST "https://your-domain.com/api/shop/customers" \ -H "Content-Type: application/json" \ -d '{ "first_name": "John", "last_name": "Doe", "email": "john@example.com", "password": "SecurePassword123!", "password_confirmation": "SecurePassword123!", "phone": "1234567890" }' ``` == Node.js ```javascript const response = await fetch('https://your-domain.com/api/shop/customers', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ first_name: 'John', last_name: 'Doe', email: 'john@example.com', password: 'SecurePassword123!', password_confirmation: 'SecurePassword123!', phone: '1234567890' }) }); const customer = await response.json(); console.log(customer); ``` == Python ```python import requests response = requests.post( 'https://your-domain.com/api/shop/customers', headers={'Content-Type': 'application/json'}, json={ 'first_name': 'John', 'last_name': 'Doe', 'email': 'john@example.com', 'password': 'SecurePassword123!', 'password_confirmation': 'SecurePassword123!', 'phone': '1234567890' } ) customer = response.json() print(customer) ``` ::: **Response (201 Created):** ```json { "@context": "/api/contexts/Customer", "@id": "/api/shop/customers/10", "@type": "Customer", "id": 10, "first_name": "John", "last_name": "Doe", "email": "john@example.com", "phone": "1234567890", "status": 1, "created_at": "2024-01-20T10:30:00Z" } ``` ### Customer Login Authenticate a customer and receive bearer token. **Endpoint:** ``` POST /api/shop/customer/login ``` :::tabs == cURL ```bash curl -X POST "https://your-domain.com/api/shop/customer/login" \ -H "Content-Type: application/json" \ -d '{ "email": "john@example.com", "password": "SecurePassword123!" }' ``` == Node.js ```javascript const response = await fetch('https://your-domain.com/api/shop/customer/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: 'john@example.com', password: 'SecurePassword123!' }) }); const data = await response.json(); localStorage.setItem('authToken', data.access_token); console.log('Access Token:', data.access_token); ``` == Python ```python import requests response = requests.post( 'https://your-domain.com/api/shop/customer/login', headers={'Content-Type': 'application/json'}, json={ 'email': 'john@example.com', 'password': 'SecurePassword123!' } ) data = response.json() print(f"Token: {data['access_token']}") ``` ::: **Response (200 OK):** ```json { "@context": "/api/contexts/Customer", "@type": "Token", "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOjEwLCJlbWFpbCI6ImpvaG5AZXhhbXBsZS5jb20ifQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c", "token_type": "Bearer", "expires_in": 86400, "customer": { "id": 10, "first_name": "John", "last_name": "Doe", "email": "john@example.com" } } ``` ### Verify Authentication Token Verify if authentication token is still valid. **Endpoint:** ``` POST /api/shop/verify-tokens ``` :::tabs == cURL ```bash curl -X POST "https://your-domain.com/api/shop/verify-tokens" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" ``` == Node.js ```javascript const token = localStorage.getItem('authToken'); const response = await fetch('https://your-domain.com/api/shop/verify-tokens', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } }); const result = await response.json(); console.log('Token valid:', result.is_valid); ``` == Python ```python import requests token = 'YOUR_ACCESS_TOKEN' response = requests.post( 'https://your-domain.com/api/shop/verify-tokens', headers={ 'Authorization': f'Bearer {token}', 'Content-Type': 'application/json' } ) result = response.json() print(f"Token valid: {result['is_valid']}") ``` ::: **Response (200 OK):** ```json { "@type": "TokenVerification", "is_valid": true, "customer_id": 10, "expires_at": "2024-01-21T10:30:00Z" } ``` ## Admin Authentication Admin API requests authenticate with a pre-issued **Integration token** — there is no admin login endpoint. Generate a token from the **Integration** menu in the admin panel (a store owner can generate tokens here and share them with the sub-admins who need API access), then send it on every admin request: ``` Authorization: Bearer | ``` See [Admin Authentication](/api/rest-api/admin/authentication) for the full token lifecycle, IP allowlists, and rate limits. ## Cart Token Generation ### Create Cart Token Generate a guest cart token for unauthenticated users. **Endpoint:** ``` POST /api/shop/cart-tokens ``` :::tabs == cURL ```bash curl -X POST "https://your-domain.com/api/shop/cart-tokens" \ -H "Content-Type: application/json" \ -d '{}' ``` == Node.js ```javascript const response = await fetch('https://your-domain.com/api/shop/cart-tokens', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}) }); const cart = await response.json(); localStorage.setItem('cartToken', cart.token); console.log('Cart Token:', cart.token); ``` == Python ```python import requests response = requests.post( 'https://your-domain.com/api/shop/cart-tokens', headers={'Content-Type': 'application/json'}, json={} ) cart = response.json() print(f"Cart Token: {cart['token']}") ``` ::: **Response (201 Created):** ```json { "@context": "/api/contexts/Cart", "@id": "/api/shop/cart-tokens/xyz-token-123", "@type": "Cart", "token": "xyz-token-123", "items": [], "total": 0, "created_at": "2024-01-20T10:30:00Z" } ``` ## Password Management ### Forgot Password Request password reset for customer account. **Endpoint:** ``` POST /api/shop/forgot-passwords ``` :::tabs == cURL ```bash curl -X POST "https://your-domain.com/api/shop/forgot-passwords" \ -H "Content-Type: application/json" \ -d '{ "email": "john@example.com" }' ``` == Node.js ```javascript const response = await fetch('https://your-domain.com/api/shop/forgot-passwords', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: 'john@example.com' }) }); const result = await response.json(); console.log(result.message); ``` == Python ```python import requests response = requests.post( 'https://your-domain.com/api/shop/forgot-passwords', headers={'Content-Type': 'application/json'}, json={'email': 'john@example.com'} ) result = response.json() print(result['message']) ``` ::: **Response (200 OK):** ```json { "@type": "Message", "message": "If that email address is in our database, we will send you an email with password reset instructions." } ``` ### Change Password There is no standalone change-password endpoint. A logged-in customer changes their password through the **Customer Profile Update** endpoint (`PUT /api/shop/customer-profile-updates/{id}`) by sending the current password alongside the new password. ## Token Revocation ### Logout & Revoke Token Invalidate current authentication token. **Endpoint:** ``` POST /api/shop/customer/logout ``` :::tabs == cURL ```bash curl -X POST "https://your-domain.com/api/shop/customer/logout" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" ``` == Node.js ```javascript const token = localStorage.getItem('authToken'); const response = await fetch('https://your-domain.com/api/shop/customer/logout', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } }); localStorage.removeItem('authToken'); console.log('Logged out successfully'); ``` == Python ```python import requests token = 'YOUR_ACCESS_TOKEN' response = requests.post( 'https://your-domain.com/api/shop/customer/logout', headers={ 'Authorization': f'Bearer {token}', 'Content-Type': 'application/json' } ) print('Logged out successfully') ``` ::: **Response (200 OK):** ```json { "@type": "Message", "message": "Successfully logged out" } ``` ### Admin Logout Admin tokens are not logged out over the API. To revoke an admin Integration token, open the **Integration** menu in the admin panel and click **Revoke** (or use the signed one-click link in the lifecycle email). A revoked token stops working immediately. ## Authentication Headers ### Using Bearer Token Include bearer token in Authorization header for authenticated requests. ``` Authorization: Bearer YOUR_ACCESS_TOKEN ``` ### Using Cart Token Include cart token in X-Cart-Token header for guest cart operations. ``` X-Cart-Token: YOUR_CART_TOKEN ``` ### Using API Key (if available) Some endpoints may accept API Key authentication. ``` X-API-Key: YOUR_API_KEY ``` ## Error Handling ### Authentication Errors **401 Unauthorized:** ```json { "@context": "/api/contexts/Error", "@type": "hydra:Error", "hydra:title": "An error occurred", "hydra:description": "Invalid credentials or expired token" } ``` **403 Forbidden:** ```json { "@context": "/api/contexts/Error", "@type": "hydra:Error", "hydra:title": "Access Denied", "hydra:description": "Insufficient permissions for this operation" } ``` ## Best Practices - Store tokens securely (use httpOnly cookies or secure storage) - Implement token refresh mechanisms before expiration - Use HTTPS for all authentication requests - Validate tokens on every request - Revoke tokens on logout - Implement rate limiting on authentication endpoints - Use strong passwords (minimum 8 characters, mixed case, numbers, special characters) ## Related Resources - [Customer Management](/api/rest-api/customers) - [Best Practices](/api/rest-api/best-practices) - [Shop Resources](/api/rest-api/shop-resources) --- # REST API Best Practices URL: /api/rest-api/best-practices # REST API Best Practices This guide provides best practices and recommendations for building robust, scalable, and secure applications using the Bagisto REST API. ## Authentication & Security ### 1. Token Management **Use Bearer Tokens Securely:** ```javascript // ✅ GOOD: Store token securely const token = localStorage.getItem('auth_token'); const headers = { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }; // ❌ BAD: Don't expose tokens in URLs const url = `https://your-domain.com/api/customers?token=${token}`; // ❌ BAD: Don't log sensitive tokens console.log('Token:', token); ``` ### 2. Token Refresh & Expiration Implement token refresh logic before expiration: ```javascript class ApiClient { constructor(baseUrl) { this.baseUrl = baseUrl; this.token = localStorage.getItem('token'); this.expiresAt = parseInt(localStorage.getItem('expires_at')); } async ensureTokenValid() { if (Date.now() >= this.expiresAt - 60000) { // 1 min buffer await this.refreshToken(); } } async refreshToken() { const response = await fetch(`${this.baseUrl}/refresh-token`, { method: 'POST', headers: { 'Authorization': `Bearer ${this.token}` } }); const data = await response.json(); this.token = data.access_token; this.expiresAt = Date.now() + (data.expires_in * 1000); localStorage.setItem('token', this.token); localStorage.setItem('expires_at', this.expiresAt); } async request(endpoint, options = {}) { await this.ensureTokenValid(); return fetch(`${this.baseUrl}${endpoint}`, { ...options, headers: { 'Authorization': `Bearer ${this.token}`, ...options.headers } }); } } ``` ### 3. Cart Token Security Use unique cart tokens for guest users and include them in headers: ```javascript // Create guest cart const cartResponse = await fetch(`${API_URL}/cart_tokens`, { method: 'POST', headers: { 'Content-Type': 'application/json' } }); const { id: cartToken } = await cartResponse.json(); // Store securely (not in localStorage for critical apps) sessionStorage.setItem('cartToken', cartToken); // Use in subsequent requests const addToCart = await fetch(`${API_URL}/shop/add-product-in-cart`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Cart-Token': cartToken }, body: JSON.stringify({ product_id: 1, quantity: 2 }) }); ``` ## Error Handling ### 1. Comprehensive Error Handling ```javascript class ApiError extends Error { constructor(status, data) { super(data?.message || 'API Error'); this.status = status; this.data = data; this.violations = data?.violations || []; } } async function makeRequest(endpoint, options = {}) { try { const response = await fetch(endpoint, options); const data = await response.json(); if (!response.ok) { throw new ApiError(response.status, data); } return data; } catch (error) { if (error instanceof ApiError) { // Handle API errors if (error.status === 401) { // Handle unauthorized - redirect to login window.location.href = '/login'; } else if (error.status === 422) { // Handle validation errors console.error('Validation errors:', error.violations); } else if (error.status >= 500) { // Handle server errors with retry logic console.error('Server error, retry later'); } } else { // Handle network errors console.error('Network error:', error); } throw error; } } ``` ### 2. Validation Error Handling ```javascript // Parse validation errors from API response function parseValidationErrors(violations) { const errors = {}; violations?.forEach(violation => { errors[violation.propertyPath] = violation.message; }); return errors; } // Usage try { await createCustomer(data); } catch (error) { if (error.status === 422) { const errors = parseValidationErrors(error.violations); displayValidationErrors(errors); } } ``` ## Pagination & Data Loading ### 1. Efficient Pagination ```javascript // ✅ GOOD: Use pagination for large datasets async function loadProducts(page = 1, itemsPerPage = 20) { const response = await fetch( `${API_URL}/shop/products?page=${page}&itemsPerPage=${itemsPerPage}` ); const data = await response.json(); return { items: data['hydra:member'], totalItems: data['hydra:totalItems'], hasNextPage: !!data['hydra:view']['hydra:next'] }; } // ❌ AVOID: Fetching all items at once async function loadAllProducts() { const response = await fetch(`${API_URL}/shop/products`); const data = await response.json(); // This loads all products which can be thousands } ``` ### 2. Lazy Loading with Cursor ```javascript class PaginationManager { constructor(apiUrl, endpoint) { this.apiUrl = apiUrl; this.endpoint = endpoint; this.currentPage = 1; this.items = []; this.totalItems = 0; } async loadMore() { const response = await fetch( `${this.apiUrl}${this.endpoint}?page=${this.currentPage}&itemsPerPage=20` ); const data = await response.json(); this.items = [...this.items, ...data['hydra:member']]; this.totalItems = data['hydra:totalItems']; this.currentPage++; return this.items; } hasMore() { return this.items.length < this.totalItems; } } ``` ## Request Optimization ### 1. Batch Operations ```javascript // ✅ GOOD: Batch add multiple items async function addMultipleItemsToCart(cartId, items) { const response = await fetch(`${API_URL}/shop/add-product-in-cart`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Cart-Token': cartId }, body: JSON.stringify({ items }) }); return response.json(); } // ❌ AVOID: Multiple requests for same operation async function addItemsOneByOne(cartId, items) { for (const item of items) { await fetch(`${API_URL}/shop/add-product-in-cart`, { method: 'POST', body: JSON.stringify(item) }); } } ``` ### 2. Request Debouncing ```javascript // Debounce search requests function debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } // Usage: debounce API calls when typing in search const debouncedSearch = debounce(async (query) => { const products = await searchProducts(query); displayResults(products); }, 500); input.addEventListener('input', (e) => { debouncedSearch(e.target.value); }); ``` ### 3. Request Caching ```javascript class ApiCache { constructor(ttl = 5 * 60 * 1000) { // 5 minutes default this.cache = new Map(); this.ttl = ttl; } set(key, value) { this.cache.set(key, { value, timestamp: Date.now() }); } get(key) { const item = this.cache.get(key); if (!item) return null; // Check if expired if (Date.now() - item.timestamp > this.ttl) { this.cache.delete(key); return null; } return item.value; } clear() { this.cache.clear(); } } // Usage const apiCache = new ApiCache(); async function getProduct(id) { const cacheKey = `product_${id}`; // Check cache first const cached = apiCache.get(cacheKey); if (cached) return cached; // Fetch from API const response = await fetch(`${API_URL}/shop/products/${id}`); const product = await response.json(); // Store in cache apiCache.set(cacheKey, product); return product; } ``` ## Performance ### 1. Connection Pooling ```javascript // Reuse HTTP connections for better performance const client = { baseUrl: 'https://your-domain.com/api', headers: { 'Content-Type': 'application/json' }, async request(endpoint, options = {}) { const response = await fetch(`${this.baseUrl}${endpoint}`, { ...options, headers: { ...this.headers, ...options.headers } }); return response.json(); } }; // Use the same client for all requests ``` ### 2. Gzip Compression ```javascript // Accept compressed responses const response = await fetch(url, { headers: { 'Accept-Encoding': 'gzip, deflate', 'Content-Type': 'application/json' } }); ``` ## Data Validation ### 1. Client-Side Validation Before API Call ```javascript const schema = { email: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, password: { minLength: 8, requiresUppercase: true } }; function validateCustomer(data) { const errors = {}; if (!schema.email.test(data.email)) { errors.email = 'Invalid email format'; } if (data.password.length < schema.password.minLength) { errors.password = 'Password must be at least 8 characters'; } return { isValid: Object.keys(errors).length === 0, errors }; } // Validate before sending to API const { isValid, errors } = validateCustomer(formData); if (!isValid) { displayErrors(errors); return; } // Send to API only if valid await createCustomer(formData); ``` ## Rate Limiting Handling ### 1. Respect Rate Limits ```javascript class RateLimitedApiClient { constructor(baseUrl) { this.baseUrl = baseUrl; this.queue = []; this.processing = false; this.requestsPerSecond = 10; } async request(endpoint, options) { return new Promise((resolve, reject) => { this.queue.push({ endpoint, options, resolve, reject }); this.process(); }); } async process() { if (this.processing || this.queue.length === 0) return; this.processing = true; while (this.queue.length > 0) { const { endpoint, options, resolve, reject } = this.queue.shift(); try { const response = await fetch(`${this.baseUrl}${endpoint}`, options); // Check rate limit headers const remaining = response.headers.get('X-RateLimit-Remaining'); if (remaining === '0') { const reset = response.headers.get('X-RateLimit-Reset'); const wait = reset * 1000 - Date.now(); await new Promise(r => setTimeout(r, wait)); } resolve(await response.json()); } catch (error) { reject(error); } // Throttle requests await new Promise(r => setTimeout(r, 1000 / this.requestsPerSecond)); } this.processing = false; } } ``` ## Webhook Integration ### 1. Verify Webhook Signature ```javascript const crypto = require('crypto'); function verifyWebhookSignature(payload, signature, secret) { const hash = crypto .createHmac('sha256', secret) .update(payload) .digest('hex'); return hash === signature; } // Express example app.post('/webhook', (req, res) => { const signature = req.headers['x-bagisto-signature']; const payload = JSON.stringify(req.body); if (!verifyWebhookSignature(payload, signature, process.env.WEBHOOK_SECRET)) { return res.status(401).json({ error: 'Invalid signature' }); } // Process webhook processEvent(req.body); res.json({ success: true }); }); ``` ## Logging & Monitoring ### 1. Structured Logging ```javascript class Logger { log(level, message, context = {}) { console.log(JSON.stringify({ timestamp: new Date().toISOString(), level, message, ...context })); } debug(message, context) { this.log('DEBUG', message, context); } info(message, context) { this.log('INFO', message, context); } error(message, error, context) { this.log('ERROR', message, { error: error.message, stack: error.stack, ...context }); } } const logger = new Logger(); // Usage logger.info('Product fetched', { productId: 1, duration: 150 }); logger.error('Failed to create order', error, { cartId: '...' }); ``` ## Summary of Best Practices | Practice | Benefit | |----------|---------| | Use pagination | Reduce memory usage, improve performance | | Cache API responses | Decrease latency, reduce API calls | | Batch operations | Reduce number of requests | | Handle errors properly | Better user experience, easier debugging | | Validate input client-side | Catch errors before API call | | Use secure token storage | Prevent security vulnerabilities | | Respect rate limits | Avoid service disruptions | | Log structured data | Easier debugging and monitoring | | Use HTTPS | Protect data in transit | | Implement retry logic | Handle transient failures | ## Related Resources - [REST API Introduction](/api/rest-api/introduction) - [Shop Resources](/api/rest-api/shop-resources) - [Cart & Checkout](/api/rest-api/cart-checkout) - [Customer Management](/api/rest-api/customers) - [Product Management](/api/rest-api/products) --- # REST API - Introduction URL: /api/rest-api/introduction # REST API - Introduction Welcome to the Bagisto REST API documentation! This guide will help you build modern, efficient e-commerce applications using our comprehensive REST API platform built with **API Platform** and **Laravel**. ## Endpoints and Requests All Bagisto REST API endpoints follow this pattern: ``` https://{your-domain.com}/api/{type}/{resource} ``` API endpoints are organized by resource type. You'll need to use different endpoints depending on your app's requirements. ### Common Endpoint Patterns | Pattern | Example | Purpose | |---------|---------|---------| | `GET /api/{type}/{resource}` | `GET /api/shop/products` | List resources with pagination | | `GET /api/{type}/{resource}/{id}` | `GET /api/shop/products/1` | Retrieve a specific resource | | `POST /api/{type}/{resource}` | `POST /api/shop/customers` | Create a new resource | | `PATCH /api/{type}/{resource}/{id}` | `PATCH /api/admin/products/1` | Update a resource | | `DELETE /api/{type}/{resource}/{id}` | `DELETE /api/admin/products/5` | Delete a resource | ## What is REST API? REST (Representational State Transfer) is an architectural style for building web services using HTTP. It provides a simple, stateless interface for building client-server applications with standard HTTP methods (GET, POST, PATCH, DELETE). **Key Benefits for Bagisto:** - 🎯 **Standard HTTP Methods** - Familiar REST conventions for all developers - ⚡ **Efficient Data Transfer** - Optimized payloads with selective field inclusion - 📱 **Mobile Optimized** - Perfect for native mobile app integrations - 🔒 **Secure Authentication** - Token-based authentication with Laravel Sanctum - 📚 **Well-Documented** - Comprehensive OpenAPI/Swagger documentation - � **Consistent API Design** - Uniform patterns across all resources ## Architecture Overview Bagisto's REST API is built using the **API Platform** framework with **Laravel**, providing two distinct API layers: ### 🛍️ Shop API (Frontend) The public-facing API for customer-facing operations: - Product browsing and catalog management - Shopping cart management - Customer registration and authentication - Order placement and management - Address and customer profile management - Reviews and ratings - Wishlist and product reviews ### 👨‍💼 Admin API (Backend) The administrative API for management operations: - Product and category management - Customer administration - Order management and fulfillment - Inventory management - System configuration - Reports and analytics ## Quick Start ### 1. Installation See the [Installation Guide](/api/installation).** ### 2. Enable Swagger UI - By default swagger UI is enabled but you can enable / disable using the file config/api-platform.conf ### 3. Access Swagger UI - [Shop API Swagger](https://api-demo.bagisto.com/api/shop) - [Admin API Swagger](https://api-demo.bagisto.com/api/admin) ## API Endpoints Structure Bagisto REST API follows a hierarchical endpoint structure: | Category | Base Path | Purpose | |----------|-----------|---------| | Shop API | `/api/shop/` | Customer-facing operations | | Admin API | `/api/admin/` | Administrative operations | ## REST API HTTP Methods | Method | Purpose | Use Case | |--------|---------|----------| | `GET` | Retrieve resources | Fetch single or collection of resources | | `POST` | Create resource | Create new resource or perform mutations | | `PUT` | Full replace | Replace entire resource | | `DELETE` | Remove resource | Delete a resource | ## Common Headers Every Shop API call must carry a storefront key. Authentication, scoping, and content negotiation are all controlled through HTTP headers — there are no query parameters for any of them. | Header | Required | Purpose | Example | |--------------------|-------------------|--------------------------------------------------------------|--------------------------------------------------| | `Accept` | Yes | Expected response format | `application/json` | | `Content-Type` | On `POST`/`PUT` | Request payload format | `application/json` | | `X-STOREFRONT-KEY` | **Yes** (Shop API) | Identifies the calling storefront — every Shop endpoint requires it | `pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx` | | `Authorization` | Auth-only routes | Sanctum bearer token for endpoints scoped to a logged-in customer (orders, addresses, profile, etc.) | `Bearer 1\|abcdef…` | | `X-Locale` | No | Override request locale (falls back to channel default) | `en`, `fr`, `ar` | | `X-Channel` | No | Override the channel scope (falls back to current channel) | `default` | | `X-Currency` | No | Override response currency (falls back to channel default) | `USD`, `EUR`, `INR` | ### About the storefront key The `X-STOREFRONT-KEY` is generated by the package installer (`php artisan bagisto-api-platform:install`) and lives in your `.env` as `STOREFRONT_PLAYGROUND_KEY`. You can also issue additional keys with: ```bash php artisan bagisto-api:generate-key --name="My Mobile App" ``` Each key is rate-limited and can be revoked or rotated from the admin panel. **A 401 response from any Shop endpoint usually means the key is missing, inactive, or expired** — not a customer-auth problem. ### `X-Locale`, `X-Channel`, `X-Currency` These three headers control the *scope* of the response. They are independent — you can set any combination: - `X-Channel` switches catalog/channel scope (different products, different inventory). - `X-Locale` switches translation locale (product names, category labels, attribute labels). - `X-Currency` switches currency formatting on `formattedPrice`, `formattedSpecialPrice`, etc. and influences cart totals. If a header is omitted, the API falls back to the **default for the current channel** (configured in admin → Channels). When a non-default value is sent, the SetLocaleChannel middleware swaps the active scope for the rest of the request. ## Authorizing on Swagger UI The Swagger playground (`/api/shop`, `/api/admin`) ships with **a single global "Authorize" button at the top of the page**. There is no per-endpoint auth UI — every endpoint reuses the credentials you set there. ### Workflow 1. Click **Authorize** at the top of the page. 2. Fill in the relevant credentials: - `STOREFRONT_KEY (apiKey)` — paste your `pk_storefront_…` key. **Required for every Shop request**, including public catalog endpoints. - `Bearer (http, Bearer)` — paste a Sanctum token *without* the `Bearer ` prefix. Only required for customer-scoped endpoints (`/customer-orders`, `/customer-addresses`, `/customer-profile-*`, etc.). 3. Click **Authorize**, then **Close**. The values are cached in the browser session and automatically attached to every subsequent request you trigger from the page. ### Updating the auth values Switching between accounts (e.g. testing as a different customer) requires a manual update — the playground will not refresh tokens for you: 1. Click **Authorize** again. 2. Click **Logout** next to the credential you want to change (this clears it). 3. Paste the new value and click **Authorize** → **Close**. ### Logging out There's no automatic session expiry inside the UI. To clear credentials: - Open **Authorize** and click **Logout** for each credential you set, or - Refresh the page in a fresh browser tab — credentials are stored only for the active session. ## IRIs & HATEOAS Bagisto's REST API is built on **API Platform**, which follows the [HATEOAS](https://en.wikipedia.org/wiki/HATEOAS) principle: instead of embedding every related resource inline, responses expose **IRIs** (Internationalized Resource Identifiers — server-relative URL strings) that the client can dereference on demand. ### What an IRI looks like An IRI is just a path string in the same API namespace: ``` /api/shop/attribute_translations/23 /api/shop/categories/4 /api/shop/products/42/customer-group-prices ``` You GET it directly to retrieve the resource — no special parser, no envelope: ```bash curl "https://your-domain/api/shop/attribute_translations/23" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" ``` ### Where they appear Three patterns in responses: 1. **Inline objects** — the most common related-data is already expanded: ```json "translation": { "id": 1, "attributeId": 1, "locale": "en", "name": "SKU" } ``` No follow-up call needed. 2. **IRI strings** — for "the rest of the locales / the rest of the relation": ```json "translations": [ "/api/shop/attribute_translations/1", "/api/shop/attribute_translations/86" ] ``` Each entry is a fully-resolvable URL — `GET ` returns that single resource. 3. **Inline arrays of objects** — when the relation is small or always needed inline (e.g. an attribute's options, an order's items): the related objects are returned in full, no IRIs. The same field can be inline on one resource and IRI on another. This is intentional — payload size is balanced against how often the client needs the data. ### Why follow IRIs instead of constructing URLs - **Stability** — the API can change a route prefix or restructure a sub-resource without breaking clients that follow links rather than build them. - **No guessing** — you never have to know how to assemble `/api/shop/foo/{id}/bar`; the server tells you the exact URL. - **Discoverability** — every related resource is reachable from its parent, so a client can crawl the graph. ### Rule of thumb - Need the field that's already inline → use it. - Need data behind an IRI → `GET` the IRI as-is, with the same headers (storefront key, optional locale/channel/currency overrides). - Don't construct routes from IDs unless an endpoint is documented with a fixed URL pattern (`/api/shop/products/{id}`, `/api/shop/categories/{id}`, etc.). ## Pagination Every collection endpoint (`GET /api/shop/products`, `/categories`, `/attributes`, `/customer-orders`, …) returns a JSON **array** — not an envelope. Paging metadata is delivered as **response headers**, so the array can be rendered directly without unwrapping. ### Query parameters | Parameter | Type | Default | Description | |-------------|---------|---------|------------------------------------------------------| | `page` | integer | `1` | Page number (1-based) | | `per_page` | integer | `10` | Items per page. Hard-capped at **50** server-side. | > Use `per_page` (snake_case). The legacy API Platform name `itemsPerPage` is **not** accepted — it's remapped to `per_page` in the package config. ### Response headers | Header | Type | Description | |------------------|---------|------------------------------------------------------| | `X-Total-Count` | integer | Total items across **all** pages | | `X-Page` | integer | Current page (1-based) | | `X-Per-Page` | integer | Items returned on this page | | `X-Total-Pages` | integer | Total number of pages (`ceil(X-Total-Count / X-Per-Page)`) | All four headers are CORS-exposed via `Access-Control-Expose-Headers`, so JS clients can read them with `response.headers.get('X-Total-Count')` from a browser without extra server config. ### Example Request: ``` GET /api/shop/products?page=2&per_page=20 ``` Response headers: ``` HTTP/1.1 200 OK X-Total-Count: 137 X-Page: 2 X-Per-Page: 20 X-Total-Pages: 7 Access-Control-Expose-Headers: X-Total-Count, X-Page, X-Per-Page, X-Total-Pages ``` Response body: a JSON array of 20 product objects (or fewer on the last page). ### Building pager UI ```js const res = await fetch(`/api/shop/products?page=${page}&per_page=20`, { headers }); const items = await res.json(); const total = Number(res.headers.get('X-Total-Count')); const pages = Number(res.headers.get('X-Total-Pages')); // items is an array — render it directly ``` ### Notes - Requesting `?page=N` beyond `X-Total-Pages` returns an empty array `[]` with `200 OK` (not a 404). - Single-resource endpoints (`GET /…/{id}`) don't paginate and don't emit these headers. - Pagination defaults are configurable in `config/api-platform.php` (`pagination_items_per_page`, `pagination_maximum_items_per_page`). ## Working with Carts as a Guest Guest customers (no Sanctum token) cannot rely on session cookies — every request stands alone. To add products to a cart as a guest, the flow is **always** two requests: ### 1. Create a cart token ``` POST /api/shop/cart-tokens X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx Accept: application/json Content-Type: application/json ``` The response carries a server-generated `cartToken` string. Store it client-side (cookie / localStorage / mobile keychain). ### 2. Send that token on every cart-related request Pass the token as `cart_token` in the request body of every subsequent cart endpoint (`add-product-in-cart`, `update-cart-items`, `remove-cart-items`, `apply-coupon`, …) and on cart reads (`POST /api/shop/cart`): ```json { "cart_token": "cT_5b3a1c…", "product_id": 42, "quantity": 1 } ``` The same token also drives checkout — pass it through the address, shipping-method, payment-method and order-place steps. > Logged-in customers (who send `Authorization: Bearer …`) do **not** need a cart token. Their cart is identified by the authenticated user, and any guest cart they previously held is merged into their account on login. > See the [Cart Token endpoint](/api/rest-api/shop/cart/cart-token) and [Add Product to Cart](/api/rest-api/shop/cart/add-product-in-cart) for full request/response shapes. ## What's Next? - 🛍️ [Shop API Resources](/api/rest-api/shop-resources) - Complete shop operations guide - 👨‍💼 [Cart & Checkout](/api/rest-api/cart-checkout) - Shopping cart and order management - 👤 [Customer Management](/api/rest-api/customers) - Customer profiles and addresses - 📦 [Product Management](/api/rest-api/products) - Product operations and details - 🔐 [Authentication Guide](/api/rest-api/authentication) - Detailed auth methods - 💡 [Best Practices](/api/rest-api/best-practices) - Performance and security tips ## Support & Resources - 🌐 [Interactive Swagger UI](https://api-demo.bagisto.com/api) - 📖 [OpenAPI Schema](https://api-demo.bagisto.com/api/docs.json) - 💬 [Community Forum](https://forums.bagisto.com) - 🐛 [Issue Tracker](https://github.com/bagisto/bagisto-api/issues) - 📧 [Contact Support](https://bagisto.com/en/contacts/) --- --- # Product Management URL: /api/rest-api/products # Product Management Complete guide to product operations including CRUD operations, images, videos, reviews, bundles, and variants using the REST API. ## Products ### List All Products Retrieve paginated list of products. **Endpoint:** ``` GET /api/products ``` **Query Parameters:** - `page` - Page number (default: 1) - `limit` - Results per page (default: 15) - `sort` - Sort field (default: id) - `order` - Sort order (asc/desc) - `search` - Search term - `category_id` - Filter by category :::tabs == cURL ```bash curl -X GET "https://your-domain.com/api/products?page=1&limit=10" \ -H "Content-Type: application/json" ``` == Node.js ```javascript const params = new URLSearchParams({ page: 1, limit: 10 }); const response = await fetch(`https://your-domain.com/api/products?${params}`, { method: 'GET', headers: { 'Content-Type': 'application/json' } }); const products = await response.json(); console.log(products); ``` == Python ```python import requests params = { 'page': 1, 'limit': 10 } response = requests.get( 'https://your-domain.com/api/products', headers={'Content-Type': 'application/json'}, params=params ) products = response.json() print(products) ``` ::: **Response (200 OK):** ```json { "@context": "/api/contexts/Product", "@id": "/api/products", "@type": "hydra:Collection", "hydra:member": [ { "@id": "/api/products/1", "@type": "Product", "id": 1, "name": "Premium T-Shirt", "slug": "premium-t-shirt", "description": "High quality cotton t-shirt", "short_description": "Premium quality tee", "sku": "TSHIRT-001", "price": 29.99, "cost": 10.00, "weight": 0.5, "status": 1, "visibility": "visible", "type": "simple", "url_key": "premium-t-shirt", "meta_title": "Premium T-Shirt", "meta_description": "Best quality t-shirt", "created_at": "2024-01-10T10:00:00Z" } ], "hydra:view": { "@id": "/api/products?page=1", "@type": "hydra:PartialCollectionView", "hydra:first": "/api/products?page=1", "hydra:last": "/api/products?page=5", "hydra:next": "/api/products?page=2" }, "hydra:totalItems": 45 } ``` ### Get Product Details Retrieve detailed information for a specific product. **Endpoint:** ``` GET /api/products/{id} ``` :::tabs == cURL ```bash curl -X GET "https://your-domain.com/api/products/1" \ -H "Content-Type: application/json" ``` == Node.js ```javascript const response = await fetch('https://your-domain.com/api/products/1', { method: 'GET', headers: { 'Content-Type': 'application/json' } }); const product = await response.json(); console.log(product); ``` == Python ```python import requests response = requests.get( 'https://your-domain.com/api/products/1', headers={'Content-Type': 'application/json'} ) product = response.json() print(product) ``` ::: **Response (200 OK):** ```json { "@context": "/api/contexts/Product", "@id": "/api/products/1", "@type": "Product", "id": 1, "name": "Premium T-Shirt", "slug": "premium-t-shirt", "description": "Crafted from 100% organic cotton, this premium t-shirt offers comfort and style.", "short_description": "High quality cotton t-shirt", "sku": "TSHIRT-001", "price": 29.99, "original_price": 39.99, "cost": 10.00, "weight": 0.5, "status": 1, "visibility": "visible", "type": "simple", "url_key": "premium-t-shirt", "images": [ { "id": 10, "url": "https://cdn.example.com/products/tshirt-1.jpg", "alt": "Front view", "position": 0 } ], "attributes": [ { "id": 1, "code": "color", "label": "Color", "value": "Blue" }, { "id": 2, "code": "size", "label": "Size", "value": "M" } ], "categories": [ { "id": 3, "name": "Clothing", "slug": "clothing" } ], "created_at": "2024-01-10T10:00:00Z", "updated_at": "2024-01-20T10:00:00Z" } ``` ### Create Product Create a new product in the catalog. **Endpoint:** ``` POST /api/products ``` :::tabs == cURL ```bash curl -X POST "https://your-domain.com/api/products" \ -H "Authorization: Bearer ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "New Product", "slug": "new-product", "type": "simple", "sku": "PROD-001", "description": "Product description", "short_description": "Short desc", "price": 49.99, "cost": 20.00, "weight": 1.0, "status": 1, "visibility": "visible" }' ``` == Node.js ```javascript const token = 'ADMIN_TOKEN'; const response = await fetch('https://your-domain.com/api/products', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'New Product', slug: 'new-product', type: 'simple', sku: 'PROD-001', description: 'Product description', short_description: 'Short desc', price: 49.99, cost: 20.00, weight: 1.0, status: 1, visibility: 'visible' }) }); const product = await response.json(); console.log(product); ``` == Python ```python import requests token = 'ADMIN_TOKEN' response = requests.post( 'https://your-domain.com/api/products', headers={ 'Authorization': f'Bearer {token}', 'Content-Type': 'application/json' }, json={ 'name': 'New Product', 'slug': 'new-product', 'type': 'simple', 'sku': 'PROD-001', 'description': 'Product description', 'short_description': 'Short desc', 'price': 49.99, 'cost': 20.00, 'weight': 1.0, 'status': 1, 'visibility': 'visible' } ) product = response.json() print(product) ``` ::: **Response (201 Created):** ```json { "@context": "/api/contexts/Product", "@id": "/api/products/50", "@type": "Product", "id": 50, "name": "New Product", "slug": "new-product", "sku": "PROD-001", "price": 49.99, "cost": 20.00, "status": 1, "created_at": "2024-01-20T11:00:00Z" } ``` ### Update Product Update an existing product. **Endpoint:** ``` PATCH /api/products/{id} ``` :::tabs == cURL ```bash curl -X PATCH "https://your-domain.com/api/products/1" \ -H "Authorization: Bearer ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Updated Product Name", "price": 34.99 }' ``` == Node.js ```javascript const token = 'ADMIN_TOKEN'; const response = await fetch('https://your-domain.com/api/products/1', { method: 'PATCH', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'Updated Product Name', price: 34.99 }) }); const product = await response.json(); console.log(product); ``` == Python ```python import requests token = 'ADMIN_TOKEN' response = requests.patch( 'https://your-domain.com/api/products/1', headers={ 'Authorization': f'Bearer {token}', 'Content-Type': 'application/json' }, json={ 'name': 'Updated Product Name', 'price': 34.99 } ) product = response.json() print(product) ``` ::: **Response (200 OK):** ```json { "@context": "/api/contexts/Product", "@id": "/api/products/1", "@type": "Product", "id": 1, "name": "Updated Product Name", "price": 34.99, "updated_at": "2024-01-20T11:30:00Z" } ``` ### Delete Product Delete a product from the catalog. **Endpoint:** ``` DELETE /api/products/{id} ``` :::tabs == cURL ```bash curl -X DELETE "https://your-domain.com/api/products/1" \ -H "Authorization: Bearer ADMIN_TOKEN" ``` == Node.js ```javascript const token = 'ADMIN_TOKEN'; const response = await fetch('https://your-domain.com/api/products/1', { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}` } }); console.log('Product deleted'); ``` == Python ```python import requests token = 'ADMIN_TOKEN' response = requests.delete( 'https://your-domain.com/api/products/1', headers={'Authorization': f'Bearer {token}'} ) print('Product deleted') ``` ::: **Response (204 No Content):** ``` [Empty response body] ``` ## Product Images ### Add Product Image Upload and add an image to a product. **Endpoint:** ``` POST /api/product_images ``` :::tabs == cURL ```bash curl -X POST "https://your-domain.com/api/product_images" \ -H "Authorization: Bearer ADMIN_TOKEN" \ -F "product_id=1" \ -F "alt=Front view" \ -F "position=0" \ -F "image=@/path/to/image.jpg" ``` == Node.js ```javascript const fs = require('fs'); const FormData = require('form-data'); const token = 'ADMIN_TOKEN'; const form = new FormData(); form.append('product_id', 1); form.append('alt', 'Front view'); form.append('position', 0); form.append('image', fs.createReadStream('/path/to/image.jpg')); const response = await fetch('https://your-domain.com/api/product_images', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, ...form.getHeaders() }, body: form }); const image = await response.json(); console.log(image); ``` == Python ```python import requests token = 'ADMIN_TOKEN' with open('/path/to/image.jpg', 'rb') as image_file: files = {'image': image_file} data = { 'product_id': 1, 'alt': 'Front view', 'position': 0 } response = requests.post( 'https://your-domain.com/api/product_images', headers={'Authorization': f'Bearer {token}'}, files=files, data=data ) image = response.json() print(image) ``` ::: **Response (201 Created):** ```json { "@context": "/api/contexts/ProductImage", "@id": "/api/product_images/10", "@type": "ProductImage", "id": 10, "product_id": 1, "url": "https://cdn.example.com/products/image-10.jpg", "alt": "Front view", "position": 0, "created_at": "2024-01-20T11:45:00Z" } ``` ### Delete Product Image Remove an image from a product. **Endpoint:** ``` DELETE /api/product_images/{id} ``` :::tabs == cURL ```bash curl -X DELETE "https://your-domain.com/api/product_images/10" \ -H "Authorization: Bearer ADMIN_TOKEN" ``` == Node.js ```javascript const token = 'ADMIN_TOKEN'; const response = await fetch('https://your-domain.com/api/product_images/10', { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}` } }); console.log('Image deleted'); ``` == Python ```python import requests token = 'ADMIN_TOKEN' response = requests.delete( 'https://your-domain.com/api/product_images/10', headers={'Authorization': f'Bearer {token}'} ) print('Image deleted') ``` ::: **Response (204 No Content):** ``` [Empty response body] ``` ## Product Videos ### Add Product Video Attach a video to a product. **Endpoint:** ``` POST /api/product_videos ``` :::tabs == cURL ```bash curl -X POST "https://your-domain.com/api/product_videos" \ -H "Authorization: Bearer ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "product_id": 1, "url": "https://youtube.com/watch?v=dQw4w9WgXcQ", "type": "youtube", "title": "Product Demo" }' ``` == Node.js ```javascript const token = 'ADMIN_TOKEN'; const response = await fetch('https://your-domain.com/api/product_videos', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ product_id: 1, url: 'https://youtube.com/watch?v=dQw4w9WgXcQ', type: 'youtube', title: 'Product Demo' }) }); const video = await response.json(); console.log(video); ``` == Python ```python import requests token = 'ADMIN_TOKEN' response = requests.post( 'https://your-domain.com/api/product_videos', headers={ 'Authorization': f'Bearer {token}', 'Content-Type': 'application/json' }, json={ 'product_id': 1, 'url': 'https://youtube.com/watch?v=dQw4w9WgXcQ', 'type': 'youtube', 'title': 'Product Demo' } ) video = response.json() print(video) ``` ::: **Response (201 Created):** ```json { "@context": "/api/contexts/ProductVideo", "@id": "/api/product_videos/5", "@type": "ProductVideo", "id": 5, "product_id": 1, "url": "https://youtube.com/watch?v=dQw4w9WgXcQ", "type": "youtube", "title": "Product Demo", "created_at": "2024-01-20T11:50:00Z" } ``` ## Product Reviews ### List Product Reviews Retrieve reviews for a product. **Endpoint:** ``` GET /api/product_reviews?product_id={product_id} ``` :::tabs == cURL ```bash curl -X GET "https://your-domain.com/api/product_reviews?product_id=1" \ -H "Content-Type: application/json" ``` == Node.js ```javascript const params = new URLSearchParams({ product_id: 1 }); const response = await fetch(`https://your-domain.com/api/product_reviews?${params}`, { method: 'GET', headers: { 'Content-Type': 'application/json' } }); const reviews = await response.json(); console.log(reviews); ``` == Python ```python import requests params = {'product_id': 1} response = requests.get( 'https://your-domain.com/api/product_reviews', headers={'Content-Type': 'application/json'}, params=params ) reviews = response.json() print(reviews) ``` ::: **Response (200 OK):** ```json { "@context": "/api/contexts/ProductReview", "@id": "/api/product_reviews", "@type": "hydra:Collection", "hydra:member": [ { "@id": "/api/product_reviews/1", "@type": "ProductReview", "id": 1, "product_id": 1, "customer_id": 5, "title": "Excellent Quality!", "comment": "Best t-shirt I've ever purchased", "rating": 5, "status": "approved", "created_at": "2024-01-15T10:00:00Z" } ], "hydra:totalItems": 12 } ``` ### Create Product Review Submit a review for a product. **Endpoint:** ``` POST /api/product_reviews ``` :::tabs == cURL ```bash curl -X POST "https://your-domain.com/api/product_reviews" \ -H "Authorization: Bearer CUSTOMER_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "product_id": 1, "title": "Great Product", "comment": "Very satisfied with this purchase", "rating": 5 }' ``` == Node.js ```javascript const token = 'CUSTOMER_TOKEN'; const response = await fetch('https://your-domain.com/api/product_reviews', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ product_id: 1, title: 'Great Product', comment: 'Very satisfied with this purchase', rating: 5 }) }); const review = await response.json(); console.log(review); ``` == Python ```python import requests token = 'CUSTOMER_TOKEN' response = requests.post( 'https://your-domain.com/api/product_reviews', headers={ 'Authorization': f'Bearer {token}', 'Content-Type': 'application/json' }, json={ 'product_id': 1, 'title': 'Great Product', 'comment': 'Very satisfied with this purchase', 'rating': 5 } ) review = response.json() print(review) ``` ::: **Response (201 Created):** ```json { "@context": "/api/contexts/ProductReview", "@id": "/api/product_reviews/15", "@type": "ProductReview", "id": 15, "product_id": 1, "customer_id": 10, "title": "Great Product", "comment": "Very satisfied with this purchase", "rating": 5, "status": "pending", "created_at": "2024-01-20T12:00:00Z" } ``` ## Product Bundles ### List Product Bundles Get bundle products with their children items. **Endpoint:** ``` GET /api/product_bundles ``` :::tabs == cURL ```bash curl -X GET "https://your-domain.com/api/product_bundles?page=1" \ -H "Content-Type: application/json" ``` == Node.js ```javascript const response = await fetch('https://your-domain.com/api/product_bundles?page=1', { method: 'GET', headers: { 'Content-Type': 'application/json' } }); const bundles = await response.json(); console.log(bundles); ``` == Python ```python import requests response = requests.get( 'https://your-domain.com/api/product_bundles?page=1', headers={'Content-Type': 'application/json'} ) bundles = response.json() print(bundles) ``` ::: **Response (200 OK):** ```json { "@context": "/api/contexts/ProductBundle", "@type": "hydra:Collection", "hydra:member": [ { "@id": "/api/products/20", "@type": "Product", "id": 20, "name": "Premium Bundle", "type": "bundle", "price": 99.99, "bundle_items": [ { "id": 1, "product_id": 1, "quantity": 2 }, { "id": 2, "product_id": 5, "quantity": 1 } ] } ], "hydra:totalItems": 3 } ``` ### Create Bundle Product Create a new bundle product. **Endpoint:** ``` POST /api/products (with type=bundle) ``` :::tabs == cURL ```bash curl -X POST "https://your-domain.com/api/products" \ -H "Authorization: Bearer ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Bundle Package", "type": "bundle", "sku": "BUNDLE-001", "price": 99.99, "status": 1, "bundle_items": [ { "product_id": 1, "quantity": 2 }, { "product_id": 5, "quantity": 1 } ] }' ``` == Node.js ```javascript const token = 'ADMIN_TOKEN'; const response = await fetch('https://your-domain.com/api/products', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'Bundle Package', type: 'bundle', sku: 'BUNDLE-001', price: 99.99, status: 1, bundle_items: [ { product_id: 1, quantity: 2 }, { product_id: 5, quantity: 1 } ] }) }); const bundle = await response.json(); console.log(bundle); ``` == Python ```python import requests token = 'ADMIN_TOKEN' response = requests.post( 'https://your-domain.com/api/products', headers={ 'Authorization': f'Bearer {token}', 'Content-Type': 'application/json' }, json={ 'name': 'Bundle Package', 'type': 'bundle', 'sku': 'BUNDLE-001', 'price': 99.99, 'status': 1, 'bundle_items': [ {'product_id': 1, 'quantity': 2}, {'product_id': 5, 'quantity': 1} ] } ) bundle = response.json() print(bundle) ``` ::: **Response (201 Created):** ```json { "@id": "/api/products/20", "@type": "Product", "id": 20, "name": "Bundle Package", "type": "bundle", "sku": "BUNDLE-001", "price": 99.99, "bundle_items": [ { "id": 1, "product_id": 1, "quantity": 2 } ] } ``` ## Grouped & Configurable Products ### Create Grouped Product Create a grouped product linking related products. **Endpoint:** ``` POST /api/products (with type=grouped) ``` :::tabs == cURL ```bash curl -X POST "https://your-domain.com/api/products" \ -H "Authorization: Bearer ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "Grouped Product Set", "type": "grouped", "sku": "GROUP-001", "status": 1, "grouped_products": [1, 2, 3, 4] }' ``` == Node.js ```javascript const token = 'ADMIN_TOKEN'; const response = await fetch('https://your-domain.com/api/products', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'Grouped Product Set', type: 'grouped', sku: 'GROUP-001', status: 1, grouped_products: [1, 2, 3, 4] }) }); const grouped = await response.json(); console.log(grouped); ``` == Python ```python import requests token = 'ADMIN_TOKEN' response = requests.post( 'https://your-domain.com/api/products', headers={ 'Authorization': f'Bearer {token}', 'Content-Type': 'application/json' }, json={ 'name': 'Grouped Product Set', 'type': 'grouped', 'sku': 'GROUP-001', 'status': 1, 'grouped_products': [1, 2, 3, 4] } ) grouped = response.json() print(grouped) ``` ::: **Response (201 Created):** ```json { "@id": "/api/products/21", "@type": "Product", "id": 21, "name": "Grouped Product Set", "type": "grouped", "sku": "GROUP-001", "grouped_products": [1, 2, 3, 4] } ``` ## Downloadable Products ### Create Downloadable Product Create a product with downloadable files. **Endpoint:** ``` POST /api/products (with type=downloadable) ``` :::tabs == cURL ```bash curl -X POST "https://your-domain.com/api/products" \ -H "Authorization: Bearer ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "name": "E-Book", "type": "downloadable", "sku": "EBOOK-001", "price": 19.99, "status": 1, "downloadable_links": [ { "title": "Main PDF", "url": "https://cdn.example.com/ebook.pdf" } ] }' ``` == Node.js ```javascript const token = 'ADMIN_TOKEN'; const response = await fetch('https://your-domain.com/api/products', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'E-Book', type: 'downloadable', sku: 'EBOOK-001', price: 19.99, status: 1, downloadable_links: [ { title: 'Main PDF', url: 'https://cdn.example.com/ebook.pdf' } ] }) }); const product = await response.json(); console.log(product); ``` == Python ```python import requests token = 'ADMIN_TOKEN' response = requests.post( 'https://your-domain.com/api/products', headers={ 'Authorization': f'Bearer {token}', 'Content-Type': 'application/json' }, json={ 'name': 'E-Book', 'type': 'downloadable', 'sku': 'EBOOK-001', 'price': 19.99, 'status': 1, 'downloadable_links': [ {'title': 'Main PDF', 'url': 'https://cdn.example.com/ebook.pdf'} ] } ) product = response.json() print(product) ``` ::: **Response (201 Created):** ```json { "@id": "/api/products/22", "@type": "Product", "id": 22, "name": "E-Book", "type": "downloadable", "sku": "EBOOK-001", "price": 19.99, "downloadable_links": [ { "id": 1, "title": "Main PDF", "url": "https://cdn.example.com/ebook.pdf" } ] } ``` ## Product Pricing & Inventory ### Update Product Pricing Update product pricing information. **Endpoint:** ``` PATCH /api/products/{id} ``` :::tabs == cURL ```bash curl -X PATCH "https://your-domain.com/api/products/1" \ -H "Authorization: Bearer ADMIN_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "price": 44.99, "cost": 15.00, "special_price": 39.99 }' ``` == Node.js ```javascript const token = 'ADMIN_TOKEN'; const response = await fetch('https://your-domain.com/api/products/1', { method: 'PATCH', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ price: 44.99, cost: 15.00, special_price: 39.99 }) }); const product = await response.json(); console.log(product); ``` == Python ```python import requests token = 'ADMIN_TOKEN' response = requests.patch( 'https://your-domain.com/api/products/1', headers={ 'Authorization': f'Bearer {token}', 'Content-Type': 'application/json' }, json={ 'price': 44.99, 'cost': 15.00, 'special_price': 39.99 } ) product = response.json() print(product) ``` ::: **Response (200 OK):** ```json { "@id": "/api/products/1", "@type": "Product", "id": 1, "price": 44.99, "cost": 15.00, "special_price": 39.99, "updated_at": "2024-01-20T12:15:00Z" } ``` ## Related Resources - [Shop Resources](/api/rest-api/shop-resources) - [Customers](/api/rest-api/customers) - [Best Practices](/api/rest-api/best-practices) --- # Shop API Resources URL: /api/rest-api/shop-resources # Shop API Resources The Shop API provides comprehensive endpoints for customer-facing e-commerce operations. This resource covers product browsing, catalog management, and basic shop information. ## Authentication All Shop API endpoints require the `X-STOREFRONT-KEY` header for authentication. ### Getting Your Storefront Key To use the Shop APIs, you need a Storefront Key. Generate one using the Artisan command: ```bash php artisan bagisto-api:generate-key --name="Web Storefront" ``` This will output a key in the format: `pk_storefront_xxxxx` ### Headers Required All requests must include: ``` X-STOREFRONT-KEY: pk_storefront_xxxxx Content-Type: application/json ``` ### Rate Limiting - **Default limit**: 100 requests per minute per key - **Response headers**: Each response includes rate limit information - `X-RateLimit-Limit`: Total requests allowed - `X-RateLimit-Remaining`: Remaining requests this minute - `X-RateLimit-Reset`: Unix timestamp when limit resets ### Error Responses **401 - Missing Key** ```json { "message": "X-STOREFRONT-KEY header is required", "error": "missing_key" } ``` **403 - Invalid Key** ```json { "message": "Invalid storefront key", "error": "invalid_key" } ``` **429 - Rate Limit Exceeded** ```json { "message": "Rate limit exceeded", "error": "rate_limit_exceeded", "retry_after": 35 } ``` ## Products ### List Products Retrieve a paginated collection of products. **Endpoint:** ``` GET /api/shop/products ``` **Headers:** ``` X-STOREFRONT-KEY: pk_storefront_xxxxx Content-Type: application/json ``` **Parameters:** - `page`: Page number (default: 1) - `itemsPerPage`: Items per page (default: 30, max: 100) - `name`: Filter by product name - `status`: Filter by status (true/false) :::tabs == cURL ```bash curl -X GET "https://your-domain.com/api/shop/products?page=1&itemsPerPage=10" \ -H "Content-Type: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxx" ``` == Node.js ```javascript const response = await fetch('https://your-domain.com/api/shop/products?page=1&itemsPerPage=10', { method: 'GET', headers: { 'Content-Type': 'application/json', 'X-STOREFRONT-KEY': 'pk_storefront_xxxxx' } }); const data = await response.json(); console.log(data); ``` == Python ```python import requests response = requests.get( 'https://your-domain.com/api/shop/products', params={'page': 1, 'itemsPerPage': 10}, headers={ 'Content-Type': 'application/json', 'X-STOREFRONT-KEY': 'pk_storefront_xxxxx' } ) data = response.json() print(data) ``` == PHP ```php request('GET', 'https://your-domain.com/api/shop/products', [ 'query' => [ 'page' => 1, 'itemsPerPage' => 10 ], 'headers' => [ 'Content-Type' => 'application/json', 'X-STOREFRONT-KEY' => 'pk_storefront_xxxxx' ] ]); $data = json_decode($response->getBody(), true); print_r($data); ``` ::: **Response (200 OK):** ```json { "@context": "/api/contexts/Product", "@id": "/api/shop/products", "@type": "hydra:Collection", "hydra:member": [ { "@id": "/api/shop/products/1", "@type": "Product", "id": 1, "sku": "SKU-001", "name": "Premium Wireless Headphones", "price": "199.99", "status": true, "url_key": "premium-wireless-headphones" } ], "hydra:totalItems": 150, "hydra:view": { "hydra:first": "/api/shop/products?page=1", "hydra:next": "/api/shop/products?page=2", "hydra:last": "/api/shop/products?page=8" } } ``` ### Get Product Details Retrieve comprehensive information about a specific product. **Endpoint:** ``` GET /api/shop/products/{id} ``` :::tabs == cURL ```bash curl -X GET "https://your-domain.com/api/shop/products/1" \ -H "Content-Type: application/json" ``` == Node.js ```javascript const productId = 1; const response = await fetch(`https://your-domain.com/api/shop/products/${productId}`, { method: 'GET', headers: { 'Content-Type': 'application/json' } }); const product = await response.json(); console.log(product); ``` == Python ```python import requests product_id = 1 response = requests.get( f'https://your-domain.com/api/shop/products/{product_id}', headers={'Content-Type': 'application/json'} ) product = response.json() print(product) ``` ::: **Response (200 OK):** ```json { "@context": "/api/contexts/Product", "@id": "/api/shop/products/1", "@type": "Product", "id": 1, "sku": "SKU-001", "name": "Premium Wireless Headphones", "description": "High-quality wireless headphones with noise cancellation", "short_description": "Best in class wireless audio", "price": "199.99", "cost": "100.00", "status": true, "type": "simple", "weight": "0.25", "url_key": "premium-wireless-headphones" } ``` ### Create Product Create a new product in the catalog (admin only). **Endpoint:** ``` POST /api/shop/products ``` :::tabs == cURL ```bash curl -X POST "https://your-domain.com/api/shop/products" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_ADMIN_TOKEN" \ -d '{ "sku": "NEW-PRODUCT-001", "name": "New Product", "description": "Product description", "price": "99.99", "cost": "50.00", "status": true }' ``` == Node.js ```javascript const response = await fetch('https://your-domain.com/api/shop/products', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer YOUR_ADMIN_TOKEN' }, body: JSON.stringify({ sku: 'NEW-PRODUCT-001', name: 'New Product', description: 'Product description', price: '99.99', cost: '50.00', status: true }) }); const product = await response.json(); console.log(product); ``` == Python ```python import requests response = requests.post( 'https://your-domain.com/api/shop/products', headers={ 'Content-Type': 'application/json', 'Authorization': 'Bearer YOUR_ADMIN_TOKEN' }, json={ 'sku': 'NEW-PRODUCT-001', 'name': 'New Product', 'description': 'Product description', 'price': '99.99', 'cost': '50.00', 'status': True } ) product = response.json() print(product) ``` ::: **Response (201 Created):** ```json { "@context": "/api/contexts/Product", "@id": "/api/shop/products/100", "@type": "Product", "id": 100, "sku": "NEW-PRODUCT-001", "name": "New Product", "price": "99.99", "status": true } ``` ### Update Product Modify an existing product (admin only). **Endpoint:** ``` PATCH /api/shop/products/{id} ``` :::tabs == cURL ```bash curl -X PATCH "https://your-domain.com/api/shop/products/1" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_ADMIN_TOKEN" \ -d '{ "name": "Updated Product Name", "price": "249.99" }' ``` == Node.js ```javascript const response = await fetch('https://your-domain.com/api/shop/products/1', { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer YOUR_ADMIN_TOKEN' }, body: JSON.stringify({ name: 'Updated Product Name', price: '249.99' }) }); const product = await response.json(); console.log(product); ``` == Python ```python import requests response = requests.patch( 'https://your-domain.com/api/shop/products/1', headers={ 'Content-Type': 'application/json', 'Authorization': 'Bearer YOUR_ADMIN_TOKEN' }, json={ 'name': 'Updated Product Name', 'price': '249.99' } ) product = response.json() print(product) ``` ::: ## Categories ### List Categories Retrieve all product categories. **Endpoint:** ``` GET /api/shop/categories ``` :::tabs == cURL ```bash curl -X GET "https://your-domain.com/api/shop/categories" \ -H "Content-Type: application/json" ``` == Node.js ```javascript const response = await fetch('https://your-domain.com/api/shop/categories', { method: 'GET', headers: { 'Content-Type': 'application/json' } }); const categories = await response.json(); console.log(categories); ``` == Python ```python import requests response = requests.get( 'https://your-domain.com/api/shop/categories', headers={'Content-Type': 'application/json'} ) categories = response.json() print(categories) ``` ::: **Response (200 OK):** ```json { "@context": "/api/contexts/Category", "@id": "/api/shop/categories", "@type": "hydra:Collection", "hydra:member": [ { "@id": "/api/shop/categories/1", "@type": "Category", "id": 1, "name": "Electronics", "status": true, "url_key": "electronics" } ], "hydra:totalItems": 8 } ``` ## Attributes ### List Attributes Retrieve all product attributes. **Endpoint:** ``` GET /api/shop/attributes ``` :::tabs == cURL ```bash curl -X GET "https://your-domain.com/api/shop/attributes" \ -H "Content-Type: application/json" ``` == Node.js ```javascript const response = await fetch('https://your-domain.com/api/shop/attributes', { method: 'GET', headers: { 'Content-Type': 'application/json' } }); const attributes = await response.json(); console.log(attributes); ``` == Python ```python import requests response = requests.get( 'https://your-domain.com/api/shop/attributes', headers={'Content-Type': 'application/json'} ) attributes = response.json() print(attributes) ``` ::: ## Attribute Options ### List Attribute Options Retrieve options for attributes. **Endpoint:** ``` GET /api/shop/attribute-options ``` :::tabs == cURL ```bash curl -X GET "https://your-domain.com/api/shop/attribute-options" \ -H "Content-Type: application/json" ``` == Node.js ```javascript const response = await fetch('https://your-domain.com/api/shop/attribute-options', { method: 'GET' }); const options = await response.json(); console.log(options); ``` ::: ## Channels ### List Channels Retrieve all available channels. **Endpoint:** ``` GET /api/shop/channels ``` :::tabs == cURL ```bash curl -X GET "https://your-domain.com/api/shop/channels" \ -H "Content-Type: application/json" ``` == Node.js ```javascript const response = await fetch('https://your-domain.com/api/shop/channels'); const channels = await response.json(); console.log(channels); ``` ::: ## Countries & States ### List Countries Retrieve all available countries. **Endpoint:** ``` GET /api/shop/countries ``` :::tabs == cURL ```bash curl -X GET "https://your-domain.com/api/shop/countries" \ -H "Content-Type: application/json" ``` == Node.js ```javascript const response = await fetch('https://your-domain.com/api/shop/countries'); const countries = await response.json(); console.log(countries); ``` ::: ### List Country States Retrieve states for a specific country. **Endpoint:** ``` GET /api/shop/countries/{country_id}/states ``` :::tabs == cURL ```bash curl -X GET "https://your-domain.com/api/shop/countries/1/states" \ -H "Content-Type: application/json" ``` == Node.js ```javascript const countryId = 1; const response = await fetch(`https://your-domain.com/api/shop/countries/${countryId}/states`); const states = await response.json(); console.log(states); ``` ::: ## Locales ### List Locales Retrieve all available locales. **Endpoint:** ``` GET /api/shop/locales ``` :::tabs == cURL ```bash curl -X GET "https://your-domain.com/api/shop/locales" \ -H "Content-Type: application/json" ``` == Node.js ```javascript const response = await fetch('https://your-domain.com/api/shop/locales'); const locales = await response.json(); console.log(locales); ``` ::: ## Related Resources - [Cart & Checkout](/api/rest-api/cart-checkout) - [Customer Management](/api/rest-api/customers) - [Product Management](/api/rest-api/products) - [Best Practices](/api/rest-api/best-practices) ## Products The Product resource represents items in your store catalog. ### List Products Retrieve a collection of products with optional filtering and pagination. **Endpoint:** ``` GET /api/shop/products ``` **Query Parameters:** - `page`: Page number (default: 1) - `itemsPerPage`: Items per page (default: 30, max: 100) - `name`: Filter by product name - `status`: Filter by status (true/false) - `order[name]`: Sort by name (asc/desc) - `order[price]`: Sort by price (asc/desc) **Request:** ```bash curl -X GET "https://your-domain.com/api/shop/products?page=1&itemsPerPage=10" \ -H "Content-Type: application/json" ``` **Response (200 OK):** ```json { "@context": "/api/contexts/Product", "@id": "/api/shop/products", "@type": "hydra:Collection", "hydra:member": [ { "@id": "/api/shop/products/1", "@type": "Product", "id": 1, "name": "Premium Wireless Headphones", "description": "High-quality wireless headphones with noise cancellation", "price": "199.99", "status": true, "url_key": "premium-wireless-headphones", "short_description": "Best in class wireless audio" } ], "hydra:totalItems": 150, "hydra:view": { "@id": "/api/shop/products?page=1&itemsPerPage=10", "@type": "hydra:PartialCollectionView", "hydra:first": "/api/shop/products?page=1&itemsPerPage=10", "hydra:next": "/api/shop/products?page=2&itemsPerPage=10", "hydra:last": "/api/shop/products?page=15&itemsPerPage=10" } } ``` ### Get Product Details Retrieve detailed information about a specific product. **Endpoint:** ``` GET /api/shop/products/{id} ``` **Path Parameters:** - `id`: Product ID (required) **Request:** ```bash curl -X GET "https://your-domain.com/api/shop/products/1" \ -H "Content-Type: application/json" ``` **Response (200 OK):** ```json { "@context": "/api/contexts/Product", "@id": "/api/shop/products/1", "@type": "Product", "id": 1, "name": "Premium Wireless Headphones", "description": "High-quality wireless headphones with noise cancellation", "price": "199.99", "status": true, "url_key": "premium-wireless-headphones", "short_description": "Best in class wireless audio", "attribute_family_id": 1, "created_at": "2024-01-15T10:30:00Z", "updated_at": "2024-01-20T15:45:00Z" } ``` ### Create Product Create a new product in the catalog. **Endpoint:** ``` POST /api/shop/products ``` **Request Body:** ```json { "name": "New Smartphone", "description": "Latest model smartphone with advanced features", "short_description": "Premium smartphone", "price": "899.99", "status": true, "url_key": "new-smartphone" } ``` **Request:** ```bash curl -X POST "https://your-domain.com/api/shop/products" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_ADMIN_TOKEN" \ -d '{ "name": "New Smartphone", "description": "Latest model smartphone", "price": "899.99", "status": true, "url_key": "new-smartphone" }' ``` **Response (201 Created):** ```json { "@context": "/api/contexts/Product", "@id": "/api/shop/products/5", "@type": "Product", "id": 5, "name": "New Smartphone", "description": "Latest model smartphone with advanced features", "price": "899.99", "status": true, "url_key": "new-smartphone" } ``` ### Update Product Update an existing product. **Endpoint:** ``` PUT /api/shop/products/{id} PATCH /api/shop/products/{id} ``` **Request:** ```bash curl -X PATCH "https://your-domain.com/api/shop/products/1" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_ADMIN_TOKEN" \ -d '{ "name": "Updated Product Name", "price": "249.99" }' ``` **Response (200 OK):** ```json { "@context": "/api/contexts/Product", "@id": "/api/shop/products/1", "@type": "Product", "id": 1, "name": "Updated Product Name", "price": "249.99", "status": true } ``` ## Categories The Category resource represents product categories for organizing your catalog. ### List Categories Retrieve all product categories. **Endpoint:** ``` GET /api/shop/categories ``` **Request:** ```bash curl -X GET "https://your-domain.com/api/shop/categories" \ -H "Content-Type: application/json" ``` **Response (200 OK):** ```json { "@context": "/api/contexts/Category", "@id": "/api/shop/categories", "@type": "hydra:Collection", "hydra:member": [ { "@id": "/api/shop/categories/1", "@type": "Category", "id": 1, "name": "Electronics", "description": "Electronic devices and accessories", "status": true, "url_key": "electronics" }, { "@id": "/api/shop/categories/2", "@type": "Category", "id": 2, "name": "Clothing", "description": "Fashion and apparel", "status": true, "url_key": "clothing" } ], "hydra:totalItems": 8 } ``` ### Get Category Details Retrieve detailed information about a specific category. **Endpoint:** ``` GET /api/shop/categories/{id} ``` **Request:** ```bash curl -X GET "https://your-domain.com/api/shop/categories/1" \ -H "Content-Type: application/json" ``` **Response (200 OK):** ```json { "@context": "/api/contexts/Category", "@id": "/api/shop/categories/1", "@type": "Category", "id": 1, "name": "Electronics", "description": "Electronic devices and accessories", "status": true, "url_key": "electronics", "parent_id": null, "created_at": "2024-01-01T10:00:00Z" } ``` ## Attributes The Attribute resource represents product attributes for filtering and product characteristics. ### List Attributes Retrieve all product attributes. **Endpoint:** ``` GET /api/shop/attributes ``` **Request:** ```bash curl -X GET "https://your-domain.com/api/shop/attributes?page=1&itemsPerPage=20" \ -H "Content-Type: application/json" ``` **Response (200 OK):** ```json { "@context": "/api/contexts/Attribute", "@id": "/api/shop/attributes", "@type": "hydra:Collection", "hydra:member": [ { "@id": "/api/shop/attributes/1", "@type": "Attribute", "id": 1, "name": "Color", "code": "color", "type": "select", "is_filterable": true, "is_searchable": true }, { "@id": "/api/shop/attributes/2", "@type": "Attribute", "id": 2, "name": "Size", "code": "size", "type": "select", "is_filterable": true } ], "hydra:totalItems": 12 } ``` ### Get Attribute Details Retrieve a specific attribute with its options. **Endpoint:** ``` GET /api/shop/attributes/{id} ``` **Request:** ```bash curl -X GET "https://your-domain.com/api/shop/attributes/1" \ -H "Content-Type: application/json" ``` **Response (200 OK):** ```json { "@context": "/api/contexts/Attribute", "@id": "/api/shop/attributes/1", "@type": "Attribute", "id": 1, "name": "Color", "code": "color", "type": "select", "is_filterable": true, "is_searchable": true, "is_comparable": false, "is_visible_on_front": true } ``` ## Attribute Options The AttributeOption resource represents available values for attributes. ### List Attribute Options Retrieve options for all attributes or a specific attribute. **Endpoint:** ``` GET /api/shop/attribute-options GET /api/shop/attributes/{attribute_id}/options ``` **Request:** ```bash # All attribute options curl -X GET "https://your-domain.com/api/shop/attribute-options" \ -H "Content-Type: application/json" # Options for specific attribute curl -X GET "https://your-domain.com/api/shop/attributes/1/options" \ -H "Content-Type: application/json" ``` **Response (200 OK):** ```json { "@context": "/api/contexts/AttributeOption", "@id": "/api/shop/attribute-options", "@type": "hydra:Collection", "hydra:member": [ { "@id": "/api/shop/attribute-options/1", "@type": "AttributeOption", "id": 1, "attribute_id": 1, "label": "Red", "code": "red", "position": 1 }, { "@id": "/api/shop/attribute-options/2", "@type": "AttributeOption", "id": 2, "attribute_id": 1, "label": "Blue", "code": "blue", "position": 2 } ], "hydra:totalItems": 5 } ``` ## Channels The Channel resource represents different sales channels (e.g., desktop, mobile). ### List Channels Retrieve all available channels. **Endpoint:** ``` GET /api/shop/channels ``` **Request:** ```bash curl -X GET "https://your-domain.com/api/shop/channels" \ -H "Content-Type: application/json" ``` **Response (200 OK):** ```json { "@context": "/api/contexts/Channel", "@id": "/api/shop/channels", "@type": "hydra:Collection", "hydra:member": [ { "@id": "/api/shop/channels/1", "@type": "Channel", "id": 1, "name": "Default Channel", "code": "default", "description": "Default sales channel", "is_active": true, "base_currency_code": "USD" } ], "hydra:totalItems": 1 } ``` ### Get Channel Details Retrieve specific channel configuration. **Endpoint:** ``` GET /api/shop/channels/{id} ``` **Request:** ```bash curl -X GET "https://your-domain.com/api/shop/channels/1" \ -H "Content-Type: application/json" ``` **Response (200 OK):** ```json { "@context": "/api/contexts/Channel", "@id": "/api/shop/channels/1", "@type": "Channel", "id": 1, "name": "Default Channel", "code": "default", "description": "Default sales channel", "is_active": true, "base_currency_code": "USD", "base_url": "https://your-domain.com", "hostname": "your-domain.com" } ``` ## Countries & States The Country resource represents available countries for shipping and billing. ### List Countries Retrieve all available countries. **Endpoint:** ``` GET /api/shop/countries ``` **Request:** ```bash curl -X GET "https://your-domain.com/api/shop/countries" \ -H "Content-Type: application/json" ``` **Response (200 OK):** ```json { "@context": "/api/contexts/Country", "@id": "/api/shop/countries", "@type": "hydra:Collection", "hydra:member": [ { "@id": "/api/shop/countries/1", "@type": "Country", "id": 1, "name": "United States", "code": "US" }, { "@id": "/api/shop/countries/2", "@type": "Country", "id": 2, "name": "Canada", "code": "CA" } ], "hydra:totalItems": 195 } ``` ### Get Country Details Retrieve a specific country. **Endpoint:** ``` GET /api/shop/countries/{id} ``` **Request:** ```bash curl -X GET "https://your-domain.com/api/shop/countries/1" \ -H "Content-Type: application/json" ``` **Response (200 OK):** ```json { "@context": "/api/contexts/Country", "@id": "/api/shop/countries/1", "@type": "Country", "id": 1, "name": "United States", "code": "US" } ``` ### List Country States Retrieve states for a specific country. **Endpoint:** ``` GET /api/shop/country-states GET /api/shop/countries/{country_id}/states ``` **Request:** ```bash # All country states curl -X GET "https://your-domain.com/api/shop/country-states" \ -H "Content-Type: application/json" # States for specific country curl -X GET "https://your-domain.com/api/shop/countries/1/states" \ -H "Content-Type: application/json" ``` **Response (200 OK):** ```json { "@context": "/api/contexts/CountryState", "@id": "/api/shop/country-states", "@type": "hydra:Collection", "hydra:member": [ { "@id": "/api/shop/country-states/1", "@type": "CountryState", "id": 1, "country_id": 1, "name": "California", "code": "CA" }, { "@id": "/api/shop/country-states/2", "@type": "CountryState", "id": 2, "country_id": 1, "name": "Texas", "code": "TX" } ], "hydra:totalItems": 50 } ``` ## Locales The Locale resource represents available languages and locales. ### List Locales Retrieve all available locales. **Endpoint:** ``` GET /api/shop/locales ``` **Request:** ```bash curl -X GET "https://your-domain.com/api/shop/locales" \ -H "Content-Type: application/json" ``` **Response (200 OK):** ```json { "@context": "/api/contexts/Locale", "@id": "/api/shop/locales", "@type": "hydra:Collection", "hydra:member": [ { "@id": "/api/shop/locales/1", "@type": "Locale", "id": 1, "name": "English", "code": "en", "language": "English", "script": "Latn" }, { "@id": "/api/shop/locales/2", "@type": "Locale", "id": 2, "name": "French", "code": "fr", "language": "French", "script": "Latn" } ], "hydra:totalItems": 10 } ``` ## Error Handling ### 404 Not Found ```json { "@context": "/api/contexts/Error", "@type": "hydra:Error", "hydra:title": "An error occurred", "hydra:description": "Not Found", "status": 404 } ``` ### 400 Bad Request ```json { "@context": "/api/contexts/Error", "@type": "hydra:Error", "hydra:title": "An error occurred", "hydra:description": "Invalid request", "status": 400 } ``` ## Related Resources - [Cart & Checkout](/api/rest-api/cart-checkout) - [Customer Management](/api/rest-api/customers) - [Product Management](/api/rest-api/products) - [Best Practices](/api/rest-api/best-practices) --- # Attribute Options URL: /api/rest-api/shop/attributes/get-attribute-options --- outline: false examples: - id: list-attribute-options title: List Attribute Options description: Retrieve a paginated, flat list of every attribute option in the system. request: | curl -X GET "http://localhost/api/shop/attribute-options?per_page=2" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK X-Total-Count: 84 X-Page: 1 X-Per-Page: 2 X-Total-Pages: 42 [ { "id": 1, "adminName": "Red", "sortOrder": 0, "translation": { "id": 1, "attributeOptionId": 1, "locale": "en", "label": "Red" }, "translations": [ { "id": 1, "attributeOptionId": 1, "locale": "en", "label": "Red" }, { "id": 86, "attributeOptionId": 1, "locale": "de", "label": "" }, { "id": 85, "attributeOptionId": 1, "locale": "es", "label": "" }, { "id": 82, "attributeOptionId": 1, "locale": "fr", "label": "" } ] }, { "id": 2, "adminName": "Green", "sortOrder": 0, "translation": { "id": 2, "attributeOptionId": 2, "locale": "en", "label": "Green" }, "translations": [] } ] commonErrors: - error: 401 Unauthorized cause: Missing or invalid `X-STOREFRONT-KEY` solution: Send a valid storefront API key. - id: get-attribute-option title: Get Single Attribute Option description: Retrieve a single attribute option by ID. request: | curl -X GET "http://localhost/api/shop/attribute-options/1" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK { "id": 1, "adminName": "Red", "sortOrder": 0, "translation": { "id": 1, "attributeOptionId": 1, "locale": "en", "label": "Red" }, "translations": [ { "id": 1, "attributeOptionId": 1, "locale": "en", "label": "Red" }, { "id": 86, "attributeOptionId": 1, "locale": "de", "label": "" }, { "id": 85, "attributeOptionId": 1, "locale": "es", "label": "" }, { "id": 82, "attributeOptionId": 1, "locale": "fr", "label": "" }, { "id": 87, "attributeOptionId": 1, "locale": "it", "label": "" }, { "id": 83, "attributeOptionId": 1, "locale": "nl", "label": "" }, { "id": 88, "attributeOptionId": 1, "locale": "ru", "label": "" }, { "id": 84, "attributeOptionId": 1, "locale": "tr", "label": "" } ] } commonErrors: - error: 404 Not Found cause: No attribute option with the given `{id}` exists solution: List options via `GET /api/shop/attribute-options` or fetch the parent attribute via `GET /api/shop/attributes/{id}`. - error: 401 Unauthorized cause: Missing or invalid `X-STOREFRONT-KEY` solution: Send a valid storefront API key. --- # Attribute Options Attribute options are the selectable values for a `select` / `multiselect` attribute (color values, sizes, brands, etc.). > Most clients should prefer reading options inline via [`GET /api/shop/attributes/{id}`](/api/rest-api/shop/attributes/get-attributes) — that returns options scoped to a single attribute. Use these endpoints when you need the full option catalog or want to look up an option by ID without knowing its parent attribute. > > ⚠️ The URL is **flat**, not nested under an attribute: `/attribute-options`, NOT `/attributes/{id}/options`. ## Endpoints | Method | Path | Purpose | |--------|---------------------------------------|--------------------------------------------| | GET | `/api/shop/attribute-options` | Paginated flat list of every option | | GET | `/api/shop/attribute-options/{id}` | Single option by ID | ## Request Headers | Header | Required | Description | |--------------------|----------|------------------------------------------| | `Accept` | Yes | `application/json` | | `X-STOREFRONT-KEY` | Yes | Storefront API key (`pk_storefront_…`) | ## Query Parameters (collection only) | Parameter | Type | Default | Description | |-------------|---------|---------|---------------------------------------------| | `page` | integer | 1 | Page number (1-based) | | `per_page` | integer | 10 | Items per page. Max **50**. | Pagination headers are emitted on the collection. See [Pagination](/api/rest-api/introduction#pagination). ## Attribute Option Object Fields | Field | Type | Description | |----------------|---------|------------------------------------------------------------------------------------------| | `id` | integer | Option primary key — use this value when filtering products by an attribute | | `adminName` | string | Internal admin label | | `sortOrder` | integer | Display order within its parent attribute | | `translation` | object | Current-locale translation: `{ id, attributeOptionId, locale, label }` | | `translations` | array | All locale-specific labels (each `{ id, attributeOptionId, locale, label }`). Empty `[]` if no other locales have stored labels. | > The parent attribute is **not** embedded in this response. To find which attribute an option belongs to, fetch the attribute itself. ## Use Cases - Pre-load all option labels in every storefront locale for offline / SPA caching. - Resolve historical option IDs stored against orders or carts back to readable labels. - Build admin-style "find option by label" search. - Look up the localized label for a specific option in a non-default locale. ## Related Resources - [Attributes](/api/rest-api/shop/attributes/get-attributes) — preferred when you only need options for one attribute - [Attribute Translations](/api/rest-api/shop/attributes/get-attribute-translations) --- # Attribute Translations URL: /api/rest-api/shop/attributes/get-attribute-translations --- outline: false examples: - id: list-attribute-translations title: List Attribute Translations description: Retrieve a paginated, flat list of attribute translations across all locales. request: | curl -X GET "http://localhost/api/shop/attribute_translations?per_page=2" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK X-Total-Count: 47 X-Page: 1 X-Per-Page: 2 X-Total-Pages: 24 [ { "id": 1, "attributeId": 1, "locale": "en", "name": "SKU" }, { "id": 2, "attributeId": 2, "locale": "en", "name": "Name" } ] commonErrors: - error: 401 Unauthorized cause: Missing or invalid `X-STOREFRONT-KEY` solution: Send a valid storefront API key. - id: get-attribute-translation title: Get Single Attribute Translation description: Retrieve a single attribute translation by ID. Typically reached by following an entry from the `translations[]` array of an Attribute response. request: | curl -X GET "http://localhost/api/shop/attribute_translations/1" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK { "id": 1, "attributeId": 1, "locale": "en", "name": "SKU" } commonErrors: - error: 404 Not Found cause: No translation with the given `{id}` exists solution: List translations via `GET /api/shop/attribute_translations` or read the parent attribute's `translations[]` array. - error: 401 Unauthorized cause: Missing or invalid `X-STOREFRONT-KEY` solution: Send a valid storefront API key. --- # Attribute Translations Locale-specific names for attributes. The single endpoint is the destination of the IRI strings emitted in the `translations[]` array of [`GET /api/shop/attributes/{id}`](/api/rest-api/shop/attributes/get-attributes) — for example `/api/shop/attribute_translations/23`. > ⚠️ The URL uses an **underscore**: `attribute_translations`, not `attribute-translations`. Most clients won't call this collection directly — they'll either: - read the inline `translation` object on the parent attribute (current locale only), or - follow a `translations[]` IRI for a single non-default locale. Use these endpoints when you need to bulk-load every translation (e.g. for offline caching) or audit translation completeness. ## Endpoints | Method | Path | Purpose | |--------|-----------------------------------------------|--------------------------------------------------| | GET | `/api/shop/attribute_translations` | Paginated flat list of every translation | | GET | `/api/shop/attribute_translations/{id}` | Single translation by ID | ## Request Headers | Header | Required | Description | |--------------------|----------|------------------------------------------| | `Accept` | Yes | `application/json` | | `X-STOREFRONT-KEY` | Yes | Storefront API key (`pk_storefront_…`) | ## Query Parameters (collection only) | Parameter | Type | Default | Description | |-------------|---------|---------|---------------------------------------------| | `page` | integer | 1 | Page number (1-based) | | `per_page` | integer | 10 | Items per page. Max **50**. | Pagination headers are emitted on the collection. See [Pagination](/api/rest-api/introduction#pagination). ## Translation Object Fields | Field | Type | Description | |---------------|---------|------------------------------------------------------------| | `id` | integer | Translation primary key | | `attributeId` | integer | Owning attribute's ID — fetch via `GET /api/shop/attributes/{attributeId}` | | `locale` | string | Locale code (e.g. `en`, `fr`, `de`, `ar`) | | `name` | string | Localized attribute name shown to customers in that locale | ## Typical Flow ``` GET /api/shop/attributes/23 └─ response.translations = [ "/api/shop/attribute_translations/23", "/api/shop/attribute_translations/156", ... ] GET /api/shop/attribute_translations/156 └─ { "id": 156, "attributeId": 23, "locale": "fr", "name": "Couleur" } ``` You don't need to know the translation ID up front — read the `translations[]` array on the attribute and dereference any entry you need. ## Use Cases - Pre-populate a localized admin tool that lets store managers review every attribute translation in one view. - Build an offline cache of all attribute names per locale for an SPA. - Validate translation completeness — sort/filter by locale and check for empty `name` values. ## Related Resources - [Attributes](/api/rest-api/shop/attributes/get-attributes) — returns inline `translation` for the request locale and `translations[]` IRIs for the rest - [Attribute Options](/api/rest-api/shop/attributes/get-attribute-options) - [Introduction → IRIs & HATEOAS](/api/rest-api/introduction#iris-hateoas) --- # Attributes URL: /api/rest-api/shop/attributes/get-attributes --- outline: false examples: - id: list-attributes title: List Attributes description: Retrieve a paginated list of product attributes. request: | curl -X GET "http://localhost/api/shop/attributes?page=1&per_page=2" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK X-Total-Count: 28 X-Page: 1 X-Per-Page: 2 X-Total-Pages: 14 [ { "id": 1, "code": "sku", "adminName": "SKU", "type": "text", "position": 1, "isRequired": 1, "isUnique": 1, "isFilterable": 0, "isComparable": 0, "isConfigurable": 0, "isUserDefined": 0, "isVisibleOnFront": 0, "valuePerLocale": 0, "valuePerChannel": 0, "enableWysiwyg": 0, "createdAt": "2024-04-16T21:44:17+05:30", "updatedAt": "2024-04-16T21:44:17+05:30", "columnName": "text_value", "validations": "{ required: true }", "options": [], "translation": { "id": 1, "attributeId": 1, "locale": "en", "name": "SKU" }, "translations": [ "/api/shop/attribute_translations/1" ] }, { "id": 23, "code": "color", "adminName": "Color", "type": "select", "swatchType": "dropdown", "position": 26, "isRequired": 0, "isUnique": 0, "isFilterable": 1, "isComparable": 0, "isConfigurable": 1, "isUserDefined": 1, "isVisibleOnFront": 0, "valuePerLocale": 0, "valuePerChannel": 0, "enableWysiwyg": 0, "createdAt": "2024-04-16T21:44:17+05:30", "updatedAt": "2024-04-16T21:44:17+05:30", "columnName": "integer_value", "validations": "{ }", "options": [ { "id": 1, "adminName": "Red", "sortOrder": 0, "translation": { "id": 1, "attributeOptionId": 1, "locale": "en", "label": "Red" } } ], "translation": { "id": 23, "attributeId": 23, "locale": "en", "name": "Color" }, "translations": [ "/api/shop/attribute_translations/23" ] } ] commonErrors: - error: 401 Unauthorized cause: Missing or invalid `X-STOREFRONT-KEY` solution: Send a valid storefront API key. - error: 403 Forbidden cause: Storefront key inactive or rate-limited solution: Activate the key or wait for the rate limit window to reset. - id: get-attribute title: Get Single Attribute description: Retrieve a single attribute by ID with its inline options array. request: | curl -X GET "http://localhost/api/shop/attributes/23" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK { "id": 23, "code": "color", "adminName": "Color", "type": "select", "swatchType": "dropdown", "position": 26, "isRequired": 0, "isUnique": 0, "isFilterable": 1, "isComparable": 0, "isConfigurable": 1, "isUserDefined": 1, "isVisibleOnFront": 0, "valuePerLocale": 0, "valuePerChannel": 0, "enableWysiwyg": 0, "createdAt": "2024-04-16T21:44:17+05:30", "updatedAt": "2026-01-15T22:17:59+05:30", "columnName": "integer_value", "validations": "{ }", "options": [ { "id": 1, "adminName": "Red", "sortOrder": 0, "translation": { "id": 1, "attributeOptionId": 1, "locale": "en", "label": "Red" }, "translations": [ { "id": 1, "attributeOptionId": 1, "locale": "en", "label": "Red" }, { "id": 86, "attributeOptionId": 1, "locale": "de", "label": "" } ] }, { "id": 2, "adminName": "Green", "sortOrder": 0, "translation": { "id": 2, "attributeOptionId": 2, "locale": "en", "label": "Green" }, "translations": [] } ], "translation": { "id": 23, "attributeId": 23, "locale": "en", "name": "Color" }, "translations": [ "/api/shop/attribute_translations/23" ] } commonErrors: - error: 404 Not Found cause: No attribute with the given `{id}` exists solution: List attributes via `GET /api/shop/attributes` to discover valid IDs. - error: 401 Unauthorized cause: Missing or invalid `X-STOREFRONT-KEY` solution: Send a valid storefront API key. --- # Attributes Attributes describe customer-facing or internal properties of a product (SKU, name, color, size, etc.). For `select` / `multiselect` types, every selectable value is inlined under `options[]` — fetch a single attribute and you have its full option set in one request. ## Endpoints | Method | Path | Purpose | |--------|-------------------------------|------------------------------------------| | GET | `/api/shop/attributes` | Paginated list of attributes | | GET | `/api/shop/attributes/{id}` | Single attribute by ID | Use the example switcher above to flip between the list and single calls. ## Request Headers | Header | Required | Description | |--------------------|----------|------------------------------------------| | `Accept` | Yes | `application/json` | | `X-STOREFRONT-KEY` | Yes | Storefront API key (`pk_storefront_…`) | ## Query Parameters (collection only) | Parameter | Type | Default | Description | |-------------|---------|---------|---------------------------------------------| | `page` | integer | 1 | Page number (1-based) | | `per_page` | integer | 10 | Items per page. Max **50**. | Pagination headers are emitted on the collection. See [Pagination](/api/rest-api/introduction#pagination). ## Attribute Object Fields Both endpoints return the same shape — the collection wraps an array of these objects, the single endpoint returns one. | Field | Type | Description | |---------------------|-----------------------|----------------------------------------------------------------------------------------------| | `id` | integer | Attribute primary key | | `code` | string | Unique attribute code (`sku`, `color`, `size`, …) | | `adminName` | string | Internal admin label | | `type` | string | `text`, `textarea`, `price`, `boolean`, `select`, `multiselect`, `datetime`, `date`, `image`, `file`, `checkbox` | | `swatchType` | string \| null | `dropdown`, `text`, `color`, `image` — only for visual selection attributes | | `position` | integer | Display order in admin | | `isRequired` | boolean (0/1) | Whether values are required when saving a product | | `isUnique` | boolean (0/1) | Whether values must be unique across products | | `isFilterable` | boolean (0/1) | Whether the attribute appears in storefront layered filters | | `isComparable` | boolean (0/1) | Whether the attribute appears in product comparison | | `isConfigurable` | boolean (0/1) | Whether the attribute can be used to build configurable variants | | `isUserDefined` | boolean (0/1) | `1` for custom attributes, `0` for system attributes | | `isVisibleOnFront` | boolean (0/1) | Whether the attribute is shown on the product detail page | | `valuePerLocale` | boolean (0/1) | Whether values can differ per locale | | `valuePerChannel` | boolean (0/1) | Whether values can differ per channel | | `enableWysiwyg` | boolean (0/1) | Whether `textarea` type uses a WYSIWYG editor | | `validations` | string | Serialized validation rules (e.g. `"{ required: true }"`) | | `columnName` | string | Storage column on `product_attribute_values` | | `createdAt`, `updatedAt` | string (ISO-8601) | Timestamps | | `options` | array | Inline list of `AttributeOption` objects (empty for non-select types) | | `translation` | object \| null | Translation for the current request locale (`{ id, attributeId, locale, name }`) | | `translations` | array of IRI strings | Links to all locale translations — see [IRIs & HATEOAS](/api/rest-api/introduction#iris-hateoas) | ### Embedded `options[]` Each option carries: | Field | Type | Description | |------------------|---------------------|--------------------------------------------------------------| | `id` | integer | Option primary key | | `adminName` | string | Internal admin label | | `sortOrder` | integer | Display order within the attribute | | `translation` | object | Current-locale translation (`{ id, attributeOptionId, locale, label }`) | | `translations` | array | All locale-specific labels | For non-select types (`text`, `textarea`, `boolean`, `price`, `date`, …), `options` is an empty array `[]`. ## Use Cases - Build the attribute set for the product editor / filter sidebar. - Discover which attributes are filterable (`isFilterable=1`) for a category page. - Resolve `code` → `id` mappings for filter query parameters on `/products`. - Render a configurable product's variant selectors (fetch the attribute once, get every option inline). - Resolve attribute metadata (`type`, `validations`) before building a product-edit form. ## Related Resources - [Attribute Options](/api/rest-api/shop/attributes/get-attribute-options) - [Attribute Translations](/api/rest-api/shop/attributes/get-attribute-translations) - [Get Products](/api/rest-api/shop/products/get-products) — pass `?=` to filter - [Introduction → IRIs & HATEOAS](/api/rest-api/introduction#iris-hateoas) --- # Cart & Checkout URL: /api/rest-api/shop/cart-checkout # Cart & Checkout Complete guide to shopping cart management and checkout operations using the REST API. This section covers cart creation, item management, address handling, and order placement. ## Cart Token Management ### Create Cart Token Create a new guest shopping cart with a unique token. **Endpoint:** ``` POST /api/cart_tokens ``` :::tabs == cURL ```bash curl -X POST "https://your-domain.com/api/cart_tokens" \ -H "Content-Type: application/json" \ -d '{}' ``` == Node.js ```javascript const response = await fetch('https://your-domain.com/api/cart_tokens', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({}) }); const cart = await response.json(); console.log(cart.id); // Use this token for subsequent requests ``` == Python ```python import requests response = requests.post( 'https://your-domain.com/api/cart_tokens', headers={'Content-Type': 'application/json'}, json={} ) cart = response.json() print(cart['id']) # Use this token for subsequent requests ``` == PHP ```php request('POST', 'https://your-domain.com/api/cart_tokens', [ 'headers' => [ 'Content-Type' => 'application/json' ], 'json' => [] ]); $cart = json_decode($response->getBody(), true); echo $cart['id']; // Use this token for subsequent requests ``` ::: **Response (201 Created):** ```json { "@context": "/api/contexts/CartToken", "@id": "/api/cart_tokens/550e8400-e29b-41d4-a716-446655440000", "@type": "CartToken", "id": "550e8400-e29b-41d4-a716-446655440000", "token": "550e8400-e29b-41d4-a716-446655440000", "is_active": true, "created_at": "2024-01-20T10:30:00Z" } ``` ### Get Cart Token Retrieve cart details by token. **Endpoint:** ``` GET /api/cart_tokens/{id} ``` **Request:** ```bash curl -X GET "https://your-domain.com/api/cart_tokens/550e8400-e29b-41d4-a716-446655440000" \ -H "Content-Type: application/json" ``` **Response (200 OK):** ```json { "@context": "/api/contexts/CartToken", "@id": "/api/cart_tokens/550e8400-e29b-41d4-a716-446655440000", "@type": "CartToken", "id": "550e8400-e29b-41d4-a716-446655440000", "token": "550e8400-e29b-41d4-a716-446655440000", "is_active": true, "created_at": "2024-01-20T10:30:00Z" } ``` ### List Cart Tokens Retrieve all cart tokens (customer view shows only their carts). **Endpoint:** ``` GET /api/cart_tokens ``` **Request:** ```bash curl -X GET "https://your-domain.com/api/cart_tokens" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_CUSTOMER_TOKEN" ``` **Response (200 OK):** ```json { "@context": "/api/contexts/CartToken", "@id": "/api/cart_tokens", "@type": "hydra:Collection", "hydra:member": [ { "@id": "/api/cart_tokens/550e8400-e29b-41d4-a716-446655440000", "@type": "CartToken", "id": "550e8400-e29b-41d4-a716-446655440000", "is_active": true, "created_at": "2024-01-20T10:30:00Z" } ], "hydra:totalItems": 1 } ``` ## Add Product to Cart ### Add Single Product Add a product to the shopping cart. **Endpoint:** ``` POST /api/shop/add-product-in-cart ``` :::tabs == cURL ```bash curl -X POST "https://your-domain.com/api/shop/add-product-in-cart" \ -H "Content-Type: application/json" \ -H "X-Cart-Token: 550e8400-e29b-41d4-a716-446655440000" \ -d '{ "cart_id": "550e8400-e29b-41d4-a716-446655440000", "product_id": 5, "quantity": 2, "options": { "size": "large", "color": "red" } }' ``` == Node.js ```javascript const cartToken = '550e8400-e29b-41d4-a716-446655440000'; const response = await fetch('https://your-domain.com/api/shop/add-product-in-cart', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Cart-Token': cartToken }, body: JSON.stringify({ cart_id: cartToken, product_id: 5, quantity: 2, options: { size: 'large', color: 'red' } }) }); const result = await response.json(); console.log(result); ``` == Python ```python import requests cart_token = '550e8400-e29b-41d4-a716-446655440000' response = requests.post( 'https://your-domain.com/api/shop/add-product-in-cart', headers={ 'Content-Type': 'application/json', 'X-Cart-Token': cart_token }, json={ 'cart_id': cart_token, 'product_id': 5, 'quantity': 2, 'options': { 'size': 'large', 'color': 'red' } } ) result = response.json() print(result) ``` == PHP ```php request('POST', 'https://your-domain.com/api/shop/add-product-in-cart', [ 'headers' => [ 'Content-Type' => 'application/json', 'X-Cart-Token' => $cartToken ], 'json' => [ 'cart_id' => $cartToken, 'product_id' => 5, 'quantity' => 2, 'options' => [ 'size' => 'large', 'color' => 'red' ] ] ]); $result = json_decode($response->getBody(), true); print_r($result); ``` ::: **Response (201 Created):** ```json { "@context": "/api/contexts/AddProductInCart", "@id": "/api/shop/add-product-in-cart", "@type": "AddProductInCart", "message": "Product added to cart successfully", "cart": { "@id": "/api/cart_tokens/550e8400-e29b-41d4-a716-446655440000", "id": "550e8400-e29b-41d4-a716-446655440000", "items": [ { "id": 1, "product_id": 5, "quantity": 2, "price": "49.99", "total": "99.98" } ], "subtotal": "99.98", "total": "99.98" } } ``` ### Batch Add Products Add multiple products in a single request. **Request:** ```bash curl -X POST "https://your-domain.com/api/shop/add-product-in-cart" \ -H "Content-Type: application/json" \ -H "X-Cart-Token: 550e8400-e29b-41d4-a716-446655440000" \ -d '{ "cart_id": "550e8400-e29b-41d4-a716-446655440000", "items": [ { "product_id": 5, "quantity": 2, "options": {"size": "large"} }, { "product_id": 7, "quantity": 1, "options": {"color": "blue"} } ] }' ``` ## Update Cart Item ### Update Item Quantity Update the quantity of an item in the cart. **Endpoint:** ``` POST /api/update_cart_items PATCH /api/update_cart_items ``` **Request Body:** ```json { "cart_id": "550e8400-e29b-41d4-a716-446655440000", "item_id": 1, "quantity": 5 } ``` **Request:** ```bash curl -X POST "https://your-domain.com/api/update_cart_items" \ -H "Content-Type: application/json" \ -H "X-Cart-Token: 550e8400-e29b-41d4-a716-446655440000" \ -d '{ "cart_id": "550e8400-e29b-41d4-a716-446655440000", "item_id": 1, "quantity": 5 }' ``` **Response (200 OK):** ```json { "@context": "/api/contexts/UpdateCartItem", "@type": "UpdateCartItem", "message": "Cart item updated successfully", "cart": { "id": "550e8400-e29b-41d4-a716-446655440000", "items": [ { "id": 1, "product_id": 5, "quantity": 5, "price": "49.99", "total": "249.95" } ], "subtotal": "249.95", "total": "249.95" } } ``` ## Remove Cart Item ### Remove Single Item Remove a specific item from the cart. **Endpoint:** ``` POST /api/shop/remove-cart-item DELETE /api/remove_cart_items/{id} ``` **Request Body:** ```json { "cart_id": "550e8400-e29b-41d4-a716-446655440000", "item_id": 1 } ``` **Request:** ```bash curl -X POST "https://your-domain.com/api/shop/remove-cart-item" \ -H "Content-Type: application/json" \ -H "X-Cart-Token: 550e8400-e29b-41d4-a716-446655440000" \ -d '{ "cart_id": "550e8400-e29b-41d4-a716-446655440000", "item_id": 1 }' ``` **Response (200 OK):** ```json { "@context": "/api/contexts/RemoveCartItem", "@type": "RemoveCartItem", "message": "Item removed from cart successfully", "cart": { "id": "550e8400-e29b-41d4-a716-446655440000", "items": [], "subtotal": "0.00", "total": "0.00" } } ``` ### Remove Multiple Items Remove multiple items from cart at once. **Endpoint:** ``` POST /api/remove_cart_items ``` **Request:** ```bash curl -X POST "https://your-domain.com/api/remove_cart_items" \ -H "Content-Type: application/json" \ -H "X-Cart-Token: 550e8400-e29b-41d4-a716-446655440000" \ -d '{ "cart_id": "550e8400-e29b-41d4-a716-446655440000", "item_ids": [1, 2, 3] }' ``` ## Get Cart Details ### Read Cart Retrieve current cart details. **Endpoint:** ``` POST /api/read_carts GET /api/read_carts/{id} ``` **Request:** ```bash curl -X POST "https://your-domain.com/api/read_carts" \ -H "Content-Type: application/json" \ -H "X-Cart-Token: 550e8400-e29b-41d4-a716-446655440000" \ -d '{ "cart_id": "550e8400-e29b-41d4-a716-446655440000" }' ``` **Response (200 OK):** ```json { "@context": "/api/contexts/ReadCart", "@id": "/api/read_carts/550e8400-e29b-41d4-a716-446655440000", "@type": "ReadCart", "id": "550e8400-e29b-41d4-a716-446655440000", "items": [ { "id": 1, "product": { "@id": "/api/shop/products/5", "id": 5, "name": "Premium Wireless Headphones", "price": "199.99" }, "quantity": 2, "price": "199.99", "total": "399.98" } ], "subtotal": "399.98", "tax": "31.99", "shipping": "10.00", "discount": "0.00", "total": "441.97", "is_guest": true } ``` ## Coupon Management ### Apply Coupon Apply a discount coupon code to the cart. **Endpoint:** ``` POST /api/shop/apply-coupon PATCH /api/shop/apply-coupon ``` **Request Body:** ```json { "cart_id": "550e8400-e29b-41d4-a716-446655440000", "coupon_code": "SAVE20" } ``` **Request:** ```bash curl -X POST "https://your-domain.com/api/shop/apply-coupon" \ -H "Content-Type: application/json" \ -H "X-Cart-Token: 550e8400-e29b-41d4-a716-446655440000" \ -d '{ "cart_id": "550e8400-e29b-41d4-a716-446655440000", "coupon_code": "SAVE20" }' ``` **Response (200 OK):** ```json { "@context": "/api/contexts/ApplyCoupon", "@type": "ApplyCoupon", "message": "Coupon applied successfully", "discount": "80.00", "cart": { "id": "550e8400-e29b-41d4-a716-446655440000", "subtotal": "399.98", "discount": "80.00", "tax": "25.59", "total": "345.57" } } ``` ### Remove Coupon Remove an applied coupon code from the cart. **Endpoint:** ``` POST /api/remove_coupons ``` **Request Body:** ```json { "cart_id": "550e8400-e29b-41d4-a716-446655440000" } ``` **Request:** ```bash curl -X POST "https://your-domain.com/api/remove_coupons" \ -H "Content-Type: application/json" \ -H "X-Cart-Token: 550e8400-e29b-41d4-a716-446655440000" \ -d '{ "cart_id": "550e8400-e29b-41d4-a716-446655440000" }' ``` **Response (200 OK):** ```json { "@context": "/api/contexts/RemoveCoupon", "@type": "RemoveCoupon", "message": "Coupon removed successfully", "cart": { "id": "550e8400-e29b-41d4-a716-446655440000", "subtotal": "399.98", "discount": "0.00", "tax": "31.99", "total": "441.97" } } ``` ## Shipping & Tax Estimation ### Estimate Shipping Estimate shipping cost and tax for a given address. **Endpoint:** ``` POST /api/estimate_shippings ``` **Request Body:** ```json { "cart_id": "550e8400-e29b-41d4-a716-446655440000", "address": { "country_id": 1, "state_id": 5, "postcode": "10001" } } ``` **Request:** ```bash curl -X POST "https://your-domain.com/api/estimate_shippings" \ -H "Content-Type: application/json" \ -H "X-Cart-Token: 550e8400-e29b-41d4-a716-446655440000" \ -d '{ "cart_id": "550e8400-e29b-41d4-a716-446655440000", "address": { "country_id": 1, "state_id": 5, "postcode": "10001" } }' ``` **Response (200 OK):** ```json { "@context": "/api/contexts/EstimateShipping", "@type": "EstimateShipping", "shipping_methods": [ { "id": 1, "code": "flatrate", "title": "Flat Rate", "price": "10.00", "description": "Standard Shipping" }, { "id": 2, "code": "freeshipping", "title": "Free Shipping", "price": "0.00", "description": "Free Shipping" } ], "tax": "31.99", "total_with_shipping": "441.97" } ``` ## Checkout Address ### Save Billing Address Save or update billing address during checkout. **Endpoint:** ``` POST /api/checkout_addresses PATCH /api/checkout_addresses/{id} ``` **Request Body:** ```json { "cart_id": "550e8400-e29b-41d4-a716-446655440000", "address_type": "billing", "first_name": "John", "last_name": "Doe", "email": "john@example.com", "phone": "1234567890", "address": "123 Main St", "city": "New York", "state": "NY", "country": "US", "postcode": "10001", "company": "Acme Inc" } ``` **Request:** ```bash curl -X POST "https://your-domain.com/api/checkout_addresses" \ -H "Content-Type: application/json" \ -H "X-Cart-Token: 550e8400-e29b-41d4-a716-446655440000" \ -d '{ "cart_id": "550e8400-e29b-41d4-a716-446655440000", "address_type": "billing", "first_name": "John", "last_name": "Doe", "email": "john@example.com", "phone": "1234567890", "address": "123 Main St", "city": "New York", "state": "NY", "country": "US", "postcode": "10001", "company": "Acme Inc" }' ``` **Response (201 Created):** ```json { "@context": "/api/contexts/CheckoutAddress", "@id": "/api/checkout_addresses/1", "@type": "CheckoutAddress", "id": 1, "cart_id": "550e8400-e29b-41d4-a716-446655440000", "address_type": "billing", "first_name": "John", "last_name": "Doe", "email": "john@example.com", "phone": "1234567890", "address": "123 Main St", "city": "New York", "state": "NY", "country": "US", "postcode": "10001" } ``` ### Save Shipping Address Save shipping address for order delivery. **Endpoint:** ``` POST /api/checkout_addresses ``` **Request:** ```bash curl -X POST "https://your-domain.com/api/checkout_addresses" \ -H "Content-Type: application/json" \ -H "X-Cart-Token: 550e8400-e29b-41d4-a716-446655440000" \ -d '{ "cart_id": "550e8400-e29b-41d4-a716-446655440000", "address_type": "shipping", "first_name": "John", "last_name": "Doe", "address": "456 Oak St", "city": "Boston", "state": "MA", "country": "US", "postcode": "02101" }' ``` ## Checkout Shipping Method ### Set Shipping Method Select a shipping method for the order. **Endpoint:** ``` POST /api/checkout_shipping_methods PATCH /api/checkout_shipping_methods/{id} ``` **Request Body:** ```json { "cart_id": "550e8400-e29b-41d4-a716-446655440000", "shipping_method_code": "flatrate", "shipping_method_title": "Flat Rate" } ``` **Request:** ```bash curl -X POST "https://your-domain.com/api/checkout_shipping_methods" \ -H "Content-Type: application/json" \ -H "X-Cart-Token: 550e8400-e29b-41d4-a716-446655440000" \ -d '{ "cart_id": "550e8400-e29b-41d4-a716-446655440000", "shipping_method_code": "flatrate", "shipping_method_title": "Flat Rate" }' ``` **Response (201 Created):** ```json { "@context": "/api/contexts/CheckoutShippingMethod", "@id": "/api/checkout_shipping_methods/1", "@type": "CheckoutShippingMethod", "id": 1, "cart_id": "550e8400-e29b-41d4-a716-446655440000", "shipping_method_code": "flatrate", "shipping_method_title": "Flat Rate", "shipping_price": "10.00" } ``` ## Create Order ### Place Order Finalize the checkout and create an order. **Endpoint:** ``` POST /api/checkout_orders ``` **Request Body:** ```json { "cart_id": "550e8400-e29b-41d4-a716-446655440000", "payment_method": "paypal", "billing_address_id": 1, "shipping_address_id": 2, "shipping_method": "flatrate" } ``` **Request:** ```bash curl -X POST "https://your-domain.com/api/checkout_orders" \ -H "Content-Type: application/json" \ -H "X-Cart-Token: 550e8400-e29b-41d4-a716-446655440000" \ -d '{ "cart_id": "550e8400-e29b-41d4-a716-446655440000", "payment_method": "paypal", "billing_address_id": 1, "shipping_address_id": 2, "shipping_method": "flatrate" }' ``` **Response (201 Created):** ```json { "@context": "/api/contexts/CheckoutOrder", "@id": "/api/checkout_orders/1", "@type": "CheckoutOrder", "id": 1, "order_number": "100000001", "cart_id": "550e8400-e29b-41d4-a716-446655440000", "status": "pending", "subtotal": "399.98", "tax": "31.99", "shipping": "10.00", "discount": "0.00", "total": "441.97", "payment_method": "paypal", "items": [ { "id": 1, "product_id": 5, "quantity": 2, "price": "199.99", "total": "399.98" } ], "created_at": "2024-01-20T10:30:00Z" } ``` ## Merge Cart ### Merge Guest Cart to Customer Cart After a guest user logs in, merge their guest cart items into their customer account. **Endpoint:** ``` POST /api/merge_carts ``` **Request Body:** ```json { "guest_cart_token": "550e8400-e29b-41d4-a716-446655440000" } ``` **Request:** ```bash curl -X POST "https://your-domain.com/api/merge_carts" \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_CUSTOMER_TOKEN" \ -d '{ "guest_cart_token": "550e8400-e29b-41d4-a716-446655440000" }' ``` **Response (200 OK):** ```json { "@context": "/api/contexts/MergeCart", "@type": "MergeCart", "message": "Carts merged successfully", "merged_items": 3, "customer_cart": { "id": "customer-cart-id", "items_count": 5, "total": "899.99" } } ``` ## Related Resources - [Shop Resources](/api/rest-api/shop-resources) - [Customer Management](/api/rest-api/customers) - [Best Practices](/api/rest-api/best-practices) --- # Add to Cart URL: /api/rest-api/shop/cart/add-to-cart --- outline: false examples: - id: add-simple title: Simple / Virtual description: Add a simple or virtual product. Only productId and quantity are needed. request: | POST /api/shop/add-product-in-cart Content-Type: application/json X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxx Authorization: Bearer { "productId": 1, "quantity": 2 } response: | { "id": 6698, "cartToken": "6698", "customerId": null, "channelId": 1, "itemsCount": 1, "items": [ { "id": 7567, "cartId": 6698, "productId": 1, "name": "Coastal Breeze Men's Blue Zipper Hoodie", "sku": "COASTALBREEZEMENSHOODIE", "quantity": 2, "price": 100, "basePrice": 100, "total": 200, "baseTotal": 200, "type": "simple", "options": null, "formattedPrice": "$100.00", "formattedTotal": "$200.00", "canChangeQty": true } ], "subtotal": 200, "grandTotal": 200, "formattedSubtotal": "$200.00", "formattedGrandTotal": "$200.00", "couponCode": null, "success": true, "message": "Product added to cart successfully." } - id: add-configurable title: Configurable description: Add a configurable product. Pass the chosen variant's product ID in selectedConfigurableOption (this field is required). request: | POST /api/shop/add-product-in-cart Content-Type: application/json X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxx Authorization: Bearer { "productId": 123, "quantity": 1, "selectedConfigurableOption": 124 } response: | { "id": 6699, "itemsCount": 1, "items": [ { "id": 7568, "productId": 123, "name": "Zoe Tank", "quantity": 1, "type": "configurable", "options": [ { "attribute_name": "Size", "option_label": "S" }, { "attribute_name": "Color", "option_label": "Red" } ], "formattedTotal": "$2,040.00" } ], "grandTotal": 2040, "formattedGrandTotal": "$2,040.00", "success": true, "message": "Product added to cart successfully." } - id: add-bundle title: Bundle description: Add a bundle product. bundleOptions maps each bundle option ID to the chosen bundle-option-product IDs; bundleOptionQty maps each option ID to its quantity. request: | POST /api/shop/add-product-in-cart Content-Type: application/json X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxx Authorization: Bearer { "productId": 2517, "quantity": 1, "bundleOptions": { "1": [1], "2": [2], "3": [3], "4": [4] }, "bundleOptionQty": { "1": 1, "2": 1, "3": 1, "4": 1 } } response: | { "id": 6700, "itemsCount": 1, "items": [ { "productId": 2517, "name": "Arctic Frost Winter Accessories", "quantity": 1, "type": "bundle", "formattedTotal": "$69.00" } ], "grandTotal": 69, "formattedGrandTotal": "$69.00", "success": true, "message": "Product added to cart successfully." } - id: add-grouped title: Grouped description: Add a grouped product. groupedQty maps each associated product ID to its quantity (include every associated product). request: | POST /api/shop/add-product-in-cart Content-Type: application/json X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxx Authorization: Bearer { "productId": 2516, "quantity": 1, "groupedQty": { "2512": 1, "2514": 2, "2515": 1 } } response: | { "id": 6701, "itemsCount": 3, "items": [ { "productId": 2512, "type": "simple", "quantity": 1 }, { "productId": 2514, "type": "simple", "quantity": 2 }, { "productId": 2515, "type": "simple", "quantity": 1 } ], "success": true, "message": "Product added to cart successfully." } - id: add-downloadable title: Downloadable description: Add a downloadable product. links is the array of selected download-link IDs. request: | POST /api/shop/add-product-in-cart Content-Type: application/json X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxx Authorization: Bearer { "productId": 2506, "quantity": 1, "links": [2] } response: | { "id": 6702, "itemsCount": 1, "items": [ { "productId": 2506, "name": "Complete Personal Finance Guide", "quantity": 1, "type": "downloadable", "options": [ { "option_id": 0, "option_label": "Full eBook PDF", "attribute_name": "Downloads" } ], "formattedTotal": "$18.00" } ], "grandTotal": 18, "formattedGrandTotal": "$18.00", "success": true, "message": "Product added to cart successfully." } commonErrors: - error: 401 Unauthorized cause: Missing cart/customer token solution: Create a guest cart with POST /api/shop/cart-tokens and send its cartToken as a Bearer token (or log a customer in and send their token). - error: 400 Bad Request cause: An inactive or out-of-stock item was requested (e.g. a grouped/bundle child) solution: Choose products that are active and in stock. - error: 422 Validation Error cause: Quantity below 1, or required options missing for the product type solution: Send a valid quantity and the option fields required by the product type (see below). --- # Add to Cart Add a product to the shopping cart. The request body depends on the **product type** — simple and virtual need only `productId` + `quantity`, while configurable, bundle, grouped, and downloadable products require their option selections. ## Endpoint ``` POST /api/shop/add-product-in-cart ``` ## Authentication This endpoint operates on a specific cart, so every request needs a **cart token** (Bearer), in addition to the storefront key: | Header | Required | Description | |--------|----------|-------------| | `X-STOREFRONT-KEY` | Yes | Your storefront API key | | `Authorization` | Yes | `Bearer ` — a guest **cart token** or a logged-in **customer token** | | `Content-Type` | Yes | `application/json` | Get a guest cart token first with [Create Cart](/api/rest-api/shop/cart/create-cart) (`POST /api/shop/cart-tokens`) — its `cartToken` is the Bearer value. For a logged-in customer, use the token from [Customer Login](/api/rest-api/shop/customers/customer-login). ## Request body by product type All types take `productId` and `quantity`. Add the fields for the product's type: | Field | Type | Applies to | Description | |-------|------|-----------|-------------| | `productId` | integer | all | The product ID being added | | `quantity` | integer | all | Quantity (minimum 1) | | `selectedConfigurableOption` | integer | configurable | **Required.** The chosen **variant** product ID. | | `superAttribute` | object | configurable | `{ "": }`. Accepted but **not used by add-to-cart** — the cart resolves the variant from `selectedConfigurableOption`, so always send that. | | `bundleOptions` | object | bundle | `{ "": [, …] }` — the **bundle-option-product IDs** (from the product detail), not raw product IDs. | | `bundleOptionQty` | object | bundle | `{ "": }` — quantity per bundle option | | `groupedQty` | object | grouped | `{ "": }` — include every associated product | | `links` | array | downloadable | `[, …]` — selected download-link IDs | The option IDs (variant product IDs, bundle-option-product IDs, associated product IDs, link IDs) come from the **product detail** response — fetch the product first to discover them. The product, variant, and bundle-option products must be **active and in stock**, or the item is rejected. > Use the example dropdown (top-right) to see the exact body for each product type. ## Response On success the endpoint returns the **full updated cart** (HTTP 200), not just the added line. Key fields: | Field | Type | Description | |-------|------|-------------| | `id` | integer | Cart ID | | `itemsCount` | integer | Number of line items | | `items` | array | Cart line items (each with `productId`, `name`, `quantity`, `type`, `options`, `formattedTotal`, …) | | `subtotal` / `grandTotal` | decimal | Cart totals (raw) | | `formattedSubtotal` / `formattedGrandTotal` | string | Localised, currency-formatted totals | | `couponCode` | string\|null | Applied coupon, if any | | `success` | boolean | Whether the add succeeded | | `message` | string | Human-readable result | ## Behavior - If the same product (same options) is already in the cart, its quantity is increased. - Stock and saleability are validated; an inactive/out-of-stock item is rejected. - Booking products are added through the booking-specific flow, not this endpoint. ## Related Resources - [Create Cart](/api/rest-api/shop/cart/create-cart) — get a guest cart token first - [Get Cart](/api/rest-api/shop/cart/get-cart) - [Update Cart Item](/api/rest-api/shop/cart/update-cart-item) - [Remove Cart Item](/api/rest-api/shop/cart/remove-cart-item) --- # Apply Coupon URL: /api/rest-api/shop/cart/apply-coupon --- outline: false examples: - id: apply-coupon title: Apply Coupon to Cart description: Apply a discount coupon code to the shopping cart. request: | POST /api/shop/apply-coupon Content-Type: application/json X-STOREFRONT-KEY: pk_storefront_PvlE42nWGsKRVIf8bDlJngTPAdWAZbIy { "couponCode": "SAVE20" } response: | { "data": { "couponCode": "SAVE20", "discountAmount": 240.00, "discountPercentage": 20, "subtotal": 1199.98, "discount": 240.00, "tax": 95.99, "total": 1055.97, "message": "Coupon applied successfully" } } commonErrors: - error: 404 Not Found cause: Coupon code does not exist solution: Verify the coupon code - error: 422 Validation Error cause: Coupon expired or not applicable solution: Check coupon validity and conditions - error: 400 Bad Request cause: Minimum cart value not met solution: Add more items to reach minimum --- # Apply Coupon Apply a discount coupon code to the shopping cart. ## Endpoint ``` POST /api/shop/apply-coupon ``` ## Request Headers | Header | Required | Description | |--------|----------|-------------| | `Content-Type` | Yes | application/json | | `X-STOREFRONT-KEY` | Yes | Your storefront API key | ## Request Body ```json { "couponCode": "SAVE20" } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `couponCode` | string | Yes | Discount coupon code | ## Response Fields (200 OK) | Field | Type | Description | |-------|------|-------------| | `couponCode` | string | Applied coupon code | | `discountAmount` | decimal | Discount amount in currency | | `discountPercentage` | decimal | Discount percentage | | `subtotal` | decimal | Items subtotal | | `discount` | decimal | Total discount amount | | `tax` | decimal | Calculated tax | | `total` | decimal | New cart total after discount | | `message` | string | Success message | ## Validation - Coupon code must exist and be active - Coupon must not be expired - Cart must meet minimum purchase requirement (if any) - Coupon may have usage limits or customer restrictions - Only one coupon per cart (typically) ## Use Cases - Apply promotional discount codes - Enable customer discount redemption - Support seasonal promotions - Apply gift cards or vouchers - Implement loyalty program discounts ## Notes - Discount is calculated in real-time - Tax may be recalculated based on new total - Invalid coupons return error without modifying cart - Coupon can be removed separately ## Related Resources - [Remove Coupon](/api/rest-api/shop/cart/remove-coupon) - [Get Cart](/api/rest-api/shop/cart/get-cart) --- # Create Cart URL: /api/rest-api/shop/cart/create-cart --- outline: false examples: - id: create-cart title: Create Cart description: Create a new shopping cart. request: | POST /api/shop/cart Content-Type: application/json X-STOREFRONT-KEY: pk_storefront_PvlE42nWGsKRVIf8bDlJngTPAdWAZbIy response: | { "data": { "id": 1, "items": [], "subtotal": 0, "tax": 0, "shippingCost": 0, "total": 0, "itemCount": 0, "couponCode": null, "createdAt": "2024-01-20T15:30:00Z" }, "message": "Cart created successfully" } commonErrors: - error: 400 Bad Request cause: Cart already exists solution: Use existing cart or clear it first - error: 401 Unauthorized cause: Invalid X-STOREFRONT-KEY solution: Provide valid storefront API key --- # Create Cart Create a new shopping cart. A new cart is typically created at the start of a shopping session. ## Endpoint ``` POST /api/shop/cart ``` ## Request Headers | Header | Required | Description | |--------|----------|-------------| | `Content-Type` | Yes | application/json | | `X-STOREFRONT-KEY` | Yes | Your storefront API key | ## Request Body Optional. Can be empty `{}` or include initial items: ```json { "items": [ { "productId": 1, "quantity": 2, "attributes": { "color": "Black" } } ] } ``` ## Response Fields (201 Created) | Field | Type | Description | |-------|------|-------------| | `id` | integer | Cart ID | | `items` | array | Items in cart (empty if newly created) | | `subtotal` | decimal | Items subtotal | | `tax` | decimal | Calculated tax | | `shippingCost` | decimal | Shipping cost | | `total` | decimal | Grand total | | `itemCount` | integer | Total items in cart | | `couponCode` | string | Applied coupon code (if any) | | `createdAt` | string | Cart creation timestamp | ## Use Cases - Create cart at start of shopping session - Initialize empty cart for customer - Set up cart for guest checkout - Prepare cart for adding items ## Notes - Cart is typically created once per session - Multiple carts can exist (for different users) - Empty carts can expire after inactivity - Cart ID is needed for subsequent operations ## Related Resources - [Get Cart](/api/rest-api/shop/cart/get-cart) - [Add to Cart](/api/rest-api/shop/cart/add-to-cart) - [Update Cart Item](/api/rest-api/shop/cart/update-cart-item) - [Remove Cart Item](/api/rest-api/shop/cart/remove-cart-item) --- # Get Cart URL: /api/rest-api/shop/cart/get-cart --- outline: false examples: - id: get-cart title: Get Cart description: Retrieve the current shopping cart with items. request: | GET /api/shop/cart Content-Type: application/json X-STOREFRONT-KEY: pk_storefront_PvlE42nWGsKRVIf8bDlJngTPAdWAZbIy response: | { "data": { "id": 1, "items": [ { "id": 1, "productId": 1, "productName": "Smartphone", "quantity": 2, "price": 599.99, "subtotal": 1199.98, "attributes": { "color": "Black" } } ], "subtotal": 1199.98, "tax": 119.99, "shippingCost": 10.00, "total": 1329.97, "itemCount": 2, "couponCode": null } } commonErrors: - error: 404 Not Found cause: Cart not found or expired solution: Create a new cart first - error: 401 Unauthorized cause: Invalid X-STOREFRONT-KEY solution: Provide valid storefront API key --- # Get Cart Retrieve the current shopping cart with all items and totals. ## Endpoint ``` GET /api/shop/cart ``` ## Request Headers | Header | Required | Description | |--------|----------|-------------| | `Content-Type` | Yes | application/json | | `X-STOREFRONT-KEY` | Yes | Your storefront API key | ## Response Fields (200 OK) | Field | Type | Description | |-------|------|-------------| | `id` | integer | Cart ID | | `items` | array | Items in cart | | `subtotal` | decimal | Items subtotal | | `tax` | decimal | Calculated tax amount | | `shippingCost` | decimal | Shipping cost | | `total` | decimal | Grand total | | `itemCount` | integer | Total items in cart | | `couponCode` | string | Applied coupon code (if any) | ## Item Fields | Field | Type | Description | |-------|------|-------------| | `id` | integer | Cart item ID | | `productId` | integer | Product ID | | `productName` | string | Product name | | `quantity` | integer | Item quantity | | `price` | decimal | Unit price | | `subtotal` | decimal | Line item total | | `attributes` | object | Product attributes (color, size, etc.) | ## Use Cases - Display shopping cart summary - Show cart contents to customer - Calculate cart totals - Display cart in sidebar/dropdown - Pre-checkout cart review ## Related Resources - [Create Cart](/api/rest-api/shop/cart/create-cart) - [Add to Cart](/api/rest-api/shop/cart/add-to-cart) - [Update Cart Item](/api/rest-api/shop/cart/update-cart-item) - [Remove Cart Item](/api/rest-api/shop/cart/remove-cart-item) --- # Remove Cart Item URL: /api/rest-api/shop/cart/remove-cart-item --- outline: false examples: - id: remove-cart-item title: Remove Item from Cart description: Remove a product from the shopping cart. request: | DELETE /api/shop/remove-cart-item Content-Type: application/json X-STOREFRONT-KEY: pk_storefront_PvlE42nWGsKRVIf8bDlJngTPAdWAZbIy response: | { "message": "Item removed from cart successfully", "cartTotal": 0 } commonErrors: - error: 404 Not Found cause: Cart item does not exist solution: Verify the cart item ID - error: 400 Bad Request cause: Cannot remove last item solution: Leave item in cart or create new cart --- # Remove Cart Item Remove a product item from the shopping cart. ## Endpoint ``` DELETE /api/shop/remove-cart-item ``` ## Request Headers | Header | Required | Description | |--------|----------|-------------| | `Content-Type` | Yes | application/json | | `X-STOREFRONT-KEY` | Yes | Your storefront API key | ## Path Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `itemId` | integer | Yes | Cart item ID | ## Response (200 OK) ```json { "message": "Item removed from cart successfully", "cartTotal": 2999.95 } ``` | Field | Type | Description | |-------|------|-------------| | `message` | string | Success message | | `cartTotal` | decimal | Updated cart total | ## Alternative Response (204 No Content) No response body returned. ## Use Cases - Remove unwanted items from cart - Delete items from cart view - Clear items before re-ordering - Remove items from review cart ## Related Resources - [Get Cart](/api/rest-api/shop/cart/get-cart) - [Add to Cart](/api/rest-api/shop/cart/add-to-cart) - [Update Cart Item](/api/rest-api/shop/cart/update-cart-item) --- # Remove Coupon URL: /api/rest-api/shop/cart/remove-coupon --- outline: false examples: - id: remove-coupon title: Remove Coupon from Cart description: Remove a discount coupon code from the shopping cart. request: | DELETE /api/shop/remove-coupon Content-Type: application/json X-STOREFRONT-KEY: pk_storefront_PvlE42nWGsKRVIf8bDlJngTPAdWAZbIy response: | { "message": "Coupon removed successfully", "cartTotal": 1199.98, "tax": 119.99, "total": 1319.97 } commonErrors: - error: 404 Not Found cause: No coupon currently applied solution: Apply a coupon first before removing --- # Remove Coupon Remove a discount coupon code from the shopping cart. ## Endpoint ``` DELETE /api/shop/remove-coupon ``` ## Request Headers | Header | Required | Description | |--------|----------|-------------| | `Content-Type` | Yes | application/json | | `X-STOREFRONT-KEY` | Yes | Your storefront API key | ## Response (200 OK) ```json { "message": "Coupon removed successfully", "cartTotal": 1199.98, "tax": 119.99, "total": 1319.97 } ``` | Field | Type | Description | |-------|------|-------------| | `message` | string | Success message | | `cartTotal` | decimal | Updated cart subtotal | | `tax` | decimal | Recalculated tax | | `total` | decimal | New cart total | ## Alternative Response (204 No Content) No response body returned. ## Use Cases - Remove discount coupon from cart - Switch to different coupon - Cancel promotional discount - Clear coupon before re-applying ## Effects - Discount amount is subtracted from cart - Cart total is recalculated - Tax may be recalculated based on new total - Cart items remain unchanged ## Related Resources - [Apply Coupon](/api/rest-api/shop/cart/apply-coupon) - [Get Cart](/api/rest-api/shop/cart/get-cart) --- # Update Cart Item URL: /api/rest-api/shop/cart/update-cart-item --- outline: false examples: - id: update-cart-item title: Update Cart Item description: Update quantity or attributes of an item in cart. request: | PUT /api/shop/update-cart-item Content-Type: application/json X-STOREFRONT-KEY: pk_storefront_PvlE42nWGsKRVIf8bDlJngTPAdWAZbIy { "quantity": 5, "attributes": { "color": "Red", "size": "Medium" } } response: | { "data": { "id": 1, "productId": 1, "productName": "Smartphone", "quantity": 5, "price": 599.99, "subtotal": 2999.95, "attributes": { "color": "Red", "size": "Medium" } }, "message": "Cart item updated successfully", "cartTotal": 2999.95 } commonErrors: - error: 404 Not Found cause: Cart item does not exist solution: Verify the cart item ID - error: 422 Validation Error cause: Quantity exceeds available stock solution: Reduce quantity --- # Update Cart Item Update the quantity or attributes of an item in the shopping cart. ## Endpoint ``` PUT /api/shop/update-cart-item ``` ## Request Headers | Header | Required | Description | |--------|----------|-------------| | `Content-Type` | Yes | application/json | | `X-STOREFRONT-KEY` | Yes | Your storefront API key | ## Path Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `itemId` | integer | Yes | Cart item ID | ## Request Body ```json { "quantity": 5, "attributes": { "color": "Red", "size": "Medium" } } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `quantity` | integer | No | New quantity | | `attributes` | object | No | Updated attributes | ## Response Fields (200 OK) | Field | Type | Description | |-------|------|-------------| | `id` | integer | Cart item ID | | `productId` | integer | Product ID | | `productName` | string | Product name | | `quantity` | integer | Updated quantity | | `price` | decimal | Unit price | | `subtotal` | decimal | Updated line total | | `attributes` | object | Updated attributes | ## Response Metadata | Field | Type | Description | |-------|------|-------------| | `message` | string | Success message | | `cartTotal` | decimal | Updated cart total | ## Validation - Quantity must be at least 1 - Quantity cannot exceed available stock - Attributes must be valid for the product ## Use Cases - Change quantity in cart view - Update product variations (size, color) - Modify options before checkout - Adjust selections on cart page ## Related Resources - [Get Cart](/api/rest-api/shop/cart/get-cart) - [Add to Cart](/api/rest-api/shop/cart/add-to-cart) - [Remove Cart Item](/api/rest-api/shop/cart/remove-cart-item) --- # Categories URL: /api/rest-api/shop/categories/get-categories --- outline: false examples: - id: list-categories title: List Categories description: Retrieve a paginated, flat list of active categories with optional parent filtering. request: | curl -X GET "http://localhost/api/shop/categories?per_page=2&parent_id=1" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK X-Total-Count: 6 X-Page: 1 X-Per-Page: 2 X-Total-Pages: 3 [ { "id": 8, "position": 2, "logoPath": "category/8/Vk59z6w128ExCrY3lwlSYWhVrYenucFhTuick0VD.webp", "status": 1, "displayMode": "products_and_description", "_lft": 14, "_rgt": 15, "createdAt": "2024-04-19T19:06:12+05:30", "updatedAt": "2026-01-03T00:53:45+05:30", "url": "http://localhost/electronics", "logoUrl": "http://localhost/storage/category/8/Vk59z6w128ExCrY3lwlSYWhVrYenucFhTuick0VD.webp", "filterableAttributes": [ { "id": 11, "code": "price", "type": "price", "isFilterable": 1, "options": [] }, { "id": 23, "code": "color", "type": "select", "isFilterable": 1, "options": [ { "id": 1, "adminName": "Red", "sortOrder": 0 } ] } ], "translation": { "id": 60, "categoryId": 8, "name": "Electronics", "slug": "electronics", "urlPath": "electronics", "description": "

Discover a wide range of cutting-edge electronics…

", "metaTitle": "Electronics", "metaDescription": "", "metaKeywords": "electronics, electronics-keyboard", "localeId": 1, "locale": "en" }, "translations": [ "/api/shop/category_translations/60", "/api/shop/category_translations/209" ], "parent": "/api/shop/categories/1", "children": [] } ] commonErrors: - error: 401 Unauthorized cause: Missing or invalid `X-STOREFRONT-KEY` solution: Send a valid storefront API key. - id: get-category title: Get Single Category description: Retrieve a single category by ID with its inline translation, parent IRI, child categories, and filterable attributes. request: | curl -X GET "http://localhost/api/shop/categories/8" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK { "id": 8, "position": 2, "logoPath": "category/8/Vk59z6w128ExCrY3lwlSYWhVrYenucFhTuick0VD.webp", "status": 1, "displayMode": "products_and_description", "_lft": 14, "_rgt": 15, "createdAt": "2024-04-19T19:06:12+05:30", "updatedAt": "2026-01-03T00:53:45+05:30", "url": "http://localhost/electronics", "logoUrl": "http://localhost/storage/category/8/Vk59z6w128ExCrY3lwlSYWhVrYenucFhTuick0VD.webp", "filterableAttributes": [ { "id": 11, "code": "price", "type": "price", "isFilterable": 1, "options": [] }, { "id": 23, "code": "color", "type": "select", "isFilterable": 1, "options": [ { "id": 1, "adminName": "Red", "sortOrder": 0 } ] } ], "translation": { "id": 60, "categoryId": 8, "name": "Electronics", "slug": "electronics", "urlPath": "electronics", "description": "

Discover a wide range of cutting-edge electronics…

", "metaTitle": "Electronics", "metaDescription": "", "metaKeywords": "electronics, electronics-keyboard", "localeId": 1, "locale": "en" }, "translations": [ "/api/shop/category_translations/60", "/api/shop/category_translations/209" ], "parent": "/api/shop/categories/1", "children": [] } commonErrors: - error: 404 Not Found cause: No category with the given `{id}` exists, or the category has `status=0` solution: List active categories via `GET /api/shop/categories` to discover valid IDs. - error: 401 Unauthorized cause: Missing or invalid `X-STOREFRONT-KEY` solution: Send a valid storefront API key. --- # Categories > **Always filtered to `status=1`** — disabled categories are never returned. Disabled IDs respond `404`. > > If you need a hierarchical (nested) tree response, use [Get Category Tree](/api/rest-api/shop/categories/get-category-tree) instead. ## Endpoints | Method | Path | Purpose | |--------|---------------------------------|---------------------------------------------------------------| | GET | `/api/shop/categories` | Flat, paginated list of active categories | | GET | `/api/shop/categories/{id}` | Single category by ID | Use the example switcher above the curl block to flip between the two. ## Request Headers | Header | Required | Description | |--------------------|----------|------------------------------------------| | `Accept` | Yes | `application/json` | | `X-STOREFRONT-KEY` | Yes | Storefront API key (`pk_storefront_…`) | | `X-Locale` | No | Override request locale | | `X-Channel` | No | Override channel scope | ## Query Parameters (collection only) | Parameter | Type | Default | Description | |-------------|---------|---------|--------------------------------------------------------------------------------------| | `page` | integer | 1 | Page number (1-based) | | `per_page` | integer | 15 | Items per page. Max **100** for this endpoint. | | `parent_id` | integer | — | Return **only direct children** of this category ID. Accepts `parentId` as alias. | Pagination headers (`X-Total-Count`, `X-Page`, `X-Per-Page`, `X-Total-Pages`) are emitted on the collection. See [Pagination](/api/rest-api/introduction#pagination). ## Category Object Fields Both endpoints return the same shape — the collection wraps an array of these objects, the single endpoint returns one. | Field | Type | Description | |------------------------|-----------------------|----------------------------------------------------------------------------| | `id` | integer | Category primary key | | `position` | integer | Display order | | `status` | boolean (0/1) | Always `1` on this endpoint | | `displayMode` | string | `products_and_description`, `products`, or `description_only` | | `logoPath` | string \| null | Storage path to the category image | | `logoUrl` | string \| null | Fully-qualified image URL | | `url` | string | Storefront URL for the category page | | `_lft`, `_rgt` | integer | Nested-set tree pointers (internal; safe to ignore on the client) | | `createdAt`, `updatedAt` | string (ISO-8601) | Timestamps | | `translation` | object | Inline translation for the request locale (see below) | | `translations` | array of IRI strings | All locale translations — `GET ` to dereference | | `parent` | string (IRI) \| null | IRI to parent category. `null` for root categories | | `children` | array | Inline child category objects (one level deep). Empty `[]` for leaves | | `filterableAttributes` | array | Inline list of attributes flagged as filterable for this category | ### Inline `translation` fields | Field | Type | Description | |------------------|---------|---------------------------------------------------| | `id` | integer | Translation primary key | | `categoryId` | integer | Owning category ID | | `locale` | string | Locale code (`en`, `fr`, `de`, …) | | `localeId` | integer | Locale primary key | | `name` | string | Localized category name | | `slug` | string | URL slug (e.g. `electronics`) | | `urlPath` | string | Full URL path including any parent slugs | | `description` | string | HTML description shown on the category page | | `metaTitle` | string | SEO `` value | | `metaDescription`| string | SEO meta description | | `metaKeywords` | string | SEO meta keywords | See [IRIs & HATEOAS](/api/rest-api/introduction#iris-hateoas) for how to dereference the IRI fields. ## Use Cases - Render a flat category listing for a sidebar, footer, or admin search. - Get all direct children of a parent (`?parent_id=N`) for a "subcategories" widget without traversing the whole tree. - Read `filterableAttributes` to build a category-page faceted filter UI without an extra round trip. - Walk up to the parent for breadcrumbs by following the `parent` IRI. - Switch locales by following an entry in `translations[]` instead of re-issuing the request with a different `X-Locale` header. ## Related Resources - [Get Category Tree](/api/rest-api/shop/categories/get-category-tree) — hierarchical tree response - [Get Products](/api/rest-api/shop/products/get-products) — pass `?category_id=N` to filter by category - [Introduction → IRIs & HATEOAS](/api/rest-api/introduction#iris-hateoas) --- # Get Category Tree URL: /api/rest-api/shop/categories/get-category-tree --- outline: false examples: - id: get-category-tree title: Get Category Tree description: Retrieve the active category tree as a nested structure, scoped to a parent. request: | curl -X GET "http://localhost/api/shop/category-trees?parentId=1&depth=2" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK [ { "id": 8, "position": 2, "status": 1, "displayMode": "products_and_description", "_lft": 14, "_rgt": 15, "createdAt": "2024-04-19T19:06:12+05:30", "updatedAt": "2026-01-03T00:53:45+05:30", "url": "http://localhost/electronics", "translation": { "id": 60, "categoryId": 8, "name": "Electronics", "slug": "electronics", "urlPath": "electronics", "description": "<p>Discover a wide range of cutting-edge electronics…</p>", "metaTitle": "Electronics", "metaDescription": "", "metaKeywords": "electronics, electronics-keyboard", "locale": "en" }, "children": [] }, { "id": 23, "position": 3, "status": 1, "displayMode": "products_and_description", "_lft": 18, "_rgt": 25, "createdAt": "2025-09-03T18:13:50+05:30", "updatedAt": "2025-09-03T23:56:45+05:30", "url": "http://localhost/furniture", "translation": { "id": 195, "categoryId": 23, "name": "Furniture", "slug": "furniture", "urlPath": "", "description": "<p>Discover our wide range of furniture…</p>", "metaTitle": "", "metaDescription": "", "metaKeywords": "", "locale": "en" }, "children": [ { "id": 21, "position": 5, "status": 1, "displayMode": "products_and_description", "url": "http://localhost/leather-sofa", "translation": { "id": 177, "categoryId": 21, "name": "Leather Sofa", "slug": "leather-sofa", "urlPath": "furniture/leather-sofa", "locale": "en" }, "children": [] } ] } ] commonErrors: - error: 401 Unauthorized cause: Missing or invalid `X-STOREFRONT-KEY` solution: Send a valid storefront API key. - error: 403 Forbidden cause: Storefront key inactive or rate-limited solution: Activate the key or wait for the rate limit window to reset. --- # Get Category Tree Retrieve the **active** category tree as a nested structure. Children are inlined recursively up to the requested depth, so a single request renders the full menu/sidebar without follow-up calls. > The endpoint URL is plural and hyphenated: `/category-trees`. > > Always filtered to active categories (`status=1`). Disabled categories never appear in the response. > > ⚠️ **You must pass `?parentId=N`** — without it the response is an empty array. Pass the root category ID for your channel (commonly `1`) to get the full menu. ## Endpoint ``` GET /api/shop/category-trees ``` ## Request Headers | Header | Required | Description | |--------------------|----------|---------------------------------------------------| | `Accept` | Yes | `application/json` | | `X-STOREFRONT-KEY` | Yes | Storefront API key (`pk_storefront_…`) | | `X-Locale` | No | Override request locale (default: channel locale) | | `X-Channel` | No | Override channel scope | ## Query Parameters | Parameter | Type | Default | Description | |-------------|---------|---------|-----------------------------------------------------------------------------| | `parentId` | integer | — | **Required.** Return descendants of this category. Use the root category ID (e.g. `1`) for the full menu. | | `depth` | integer | 4 | Maximum nesting depth — how many child levels to inline | > Pagination headers (`X-Total-Count`, `X-Page`, `X-Per-Page`, `X-Total-Pages`) are **not** emitted on this endpoint — the entire tree is returned in one response. ## Response `200 OK` — JSON array of root nodes, each with a recursive `children[]` array. ### Tree Node Fields Each node carries a slimmer shape than the flat-list category endpoint — `parent`, `translations[]`, `filterableAttributes`, and image URLs are **not** included. Use [Get Single Category](/api/rest-api/shop/categories/get-category) when you need those. | Field | Type | Description | |----------------|-------------------|------------------------------------------------------------| | `id` | integer | Category primary key | | `position` | integer | Display order | | `status` | boolean (0/1) | Always `1` | | `displayMode` | string | `products_and_description`, `products`, or `description_only` | | `url` | string | Storefront URL | | `_lft`, `_rgt` | integer | Nested-set tree pointers (internal; safe to ignore) | | `createdAt` | string (ISO-8601) | Creation timestamp | | `updatedAt` | string (ISO-8601) | Last update timestamp | | `translation` | object | Inline translation for the request locale | | `children` | array | Recursive child nodes — empty `[]` at the leaf or at `depth` cap | The inline `translation` contains the same fields as the inline translation on [Get Categories](/api/rest-api/shop/categories/get-categories#inline-translation-fields). ## Use Cases - Render a header / sidebar / mega-menu in one request. - Build a sitemap of every navigable category for SEO crawling. - Draw a recursive "browse-by-category" tree in admin tooling. ## Differences vs `/categories` | Concern | `/categories` (flat) | `/category-trees` (nested) | |--------------------------|------------------------------------------------|----------------------------------------| | Shape | Flat array, paginated | Nested tree, no pagination | | `parent_id` / `parentId` | Optional | **Required** (else returns `[]`) | | Depth control | n/a (one level at a time via `?parent_id`) | `?depth=N` | | Embeds | `parent`, `translations[]`, `filterableAttributes`, `logoUrl`, `children` (inline 1 level) | Recursive `children[]`, single `translation` | ## Related Resources - [Get Categories](/api/rest-api/shop/categories/get-categories) — flat collection - [Get Category](/api/rest-api/shop/categories/get-category) — full detail for a single category - [Get Products](/api/rest-api/shop/products/get-products) — `?category_id=N` to scope products --- # Channel Translations URL: /api/rest-api/shop/channels/get-channel-translations --- outline: false examples: - id: list-channel-translations title: List Channel Translations description: Retrieve a paginated, flat list of channel translations across all locales. request: | curl -X GET "http://localhost/api/shop/channel_translations?per_page=2" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK X-Total-Count: 5 X-Page: 1 X-Per-Page: 2 X-Total-Pages: 3 [ { "id": 1, "channelId": 1, "locale": "en", "name": "bagisto store", "description": "", "maintenanceModeText": "Maintenance Mode", "homeSeo": { "meta_title": "Demo store", "meta_keywords": "Demo store meta keyword", "meta_description": "Demo store meta description" }, "createdAt": null, "updatedAt": "2026-04-08T17:23:40+05:30" }, { "id": 2, "channelId": 1, "locale": "fr", "name": "Default", "description": null, "maintenanceModeText": null, "homeSeo": { "meta_title": "Demo store", "meta_keywords": "Demo store meta keyword", "meta_description": "Demo store meta description" }, "createdAt": null, "updatedAt": null } ] commonErrors: - error: 401 Unauthorized cause: Missing or invalid `X-STOREFRONT-KEY` solution: Send a valid storefront API key. - id: get-channel-translation title: Get Single Channel Translation description: Retrieve a single channel translation by ID. Typically reached by following a `translation` / `translations[]` IRI from the parent Channel response. request: | curl -X GET "http://localhost/api/shop/channel_translations/1" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK { "id": 1, "channelId": 1, "locale": "en", "name": "bagisto store", "description": "", "maintenanceModeText": "Maintenance Mode", "homeSeo": { "meta_title": "Demo store", "meta_keywords": "Demo store meta keyword", "meta_description": "Demo store meta description" }, "createdAt": null, "updatedAt": "2026-04-08T17:23:40+05:30" } commonErrors: - error: 404 Not Found cause: No translation with the given `{id}` exists solution: List translations via `GET /api/shop/channel_translations` or read the channel's `translations[]` array. --- # Channel Translations The locale-specific copy for a channel — its display name, tagline, maintenance-mode text, and per-locale home-page SEO. > ⚠️ The URL uses an **underscore**: `channel_translations`, not `channel-translations`. This matches the IRI strings emitted by `/api/shop/channels/{id}` (`translation` and `translations[]`). Most clients reach a single row by following the `translation` / `translations[]` IRIs on a Channel response. The collection endpoint is mostly useful for bulk auditing or pre-caching every locale. ## Endpoints | Method | Path | Purpose | |--------|--------------------------------------------|------------------------------------------| | GET | `/api/shop/channel_translations` | Paginated flat list of every translation | | GET | `/api/shop/channel_translations/{id}` | Single translation by ID | ## Request Headers | Header | Required | Description | |--------------------|----------|------------------------------------------| | `Accept` | Yes | `application/json` | | `X-STOREFRONT-KEY` | Yes | Storefront API key (`pk_storefront_…`) | ## Query Parameters (collection only) | Parameter | Type | Default | Description | |-------------|---------|---------|---------------------------------------------| | `page` | integer | 1 | Page number (1-based) | | `per_page` | integer | 10 | Items per page. Max **50**. | Pagination headers are emitted on the collection. See [Pagination](/api/rest-api/introduction#pagination). ## Translation Object Fields | Field | Type | Description | |-----------------------|-------------------|------------------------------------------------------------| | `id` | integer | Translation primary key | | `channelId` | integer | Owning channel — fetch via `GET /api/shop/channels/{channelId}` | | `locale` | string | Locale code (`en`, `fr`, `de`, `ar`, …) | | `name` | string | Channel display name in this locale | | `description` | string \| null | Channel tagline / description | | `maintenanceModeText` | string \| null | Text shown to customers while maintenance mode is active | | `homeSeo` | object | `{ meta_title, meta_keywords, meta_description }` for the home page in this locale | | `createdAt` | string \| null | Creation timestamp | | `updatedAt` | string \| null | Last update timestamp | ## Typical Flow ``` GET /api/shop/channels/1 └─ response.translation = "/api/shop/channel_translations/1" └─ response.translations = [ "/api/shop/channel_translations/1", // en "/api/shop/channel_translations/2", // fr "/api/shop/channel_translations/3", // de ... ] GET /api/shop/channel_translations/2 └─ { id: 2, channelId: 1, locale: "fr", name: "...", maintenanceModeText: "...", homeSeo: {...} } ``` You don't need to know the translation ID up front — read the parent channel's `translation` / `translations[]` and dereference any entry you need. ## Use Cases - Audit translation completeness across every locale and channel. - Build an admin UI that lets store managers review channel-level localization. - Pre-cache every locale's home-page SEO for a multi-locale SPA. - Render the localized maintenance-mode banner when `Channel.isMaintenanceOn = 1`. ## Related Resources - [Channels](/api/rest-api/shop/channels/get-channels) - [Introduction → IRIs & HATEOAS](/api/rest-api/introduction#iris-hateoas) --- # Channels URL: /api/rest-api/shop/channels/get-channels --- outline: false examples: - id: list-channels title: List Channels description: Retrieve a paginated list of store channels. request: | curl -X GET "http://localhost/api/shop/channels?per_page=10" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK X-Total-Count: 1 X-Page: 1 X-Per-Page: 10 X-Total-Pages: 1 [ { "id": 1, "code": "default", "timezone": null, "theme": "default", "hostname": "https://api-demo.bagisto.com", "logo": null, "favicon": null, "homeSeo": { "meta_title": "Demo store", "meta_keywords": "Demo store meta keyword", "meta_description": "Demo store meta description" }, "isMaintenanceOn": 0, "allowedIps": "192.168.45.51", "createdAt": null, "updatedAt": "2026-04-08T17:23:40+05:30", "logoUrl": null, "faviconUrl": null, "locales": [ "/api/shop/locales/1", "/api/shop/locales/10" ], "currencies": [ "/api/shop/currencies/1", "/api/shop/currencies/3", "/api/shop/currencies/4" ], "defaultLocale": "/api/shop/locales/1", "baseCurrency": "/api/shop/currencies/1", "translation": "/api/shop/channel_translations/1", "translations": [ "/api/shop/channel_translations/1", "/api/shop/channel_translations/5", "/api/shop/channel_translations/2" ] } ] commonErrors: - error: 401 Unauthorized cause: Missing or invalid `X-STOREFRONT-KEY` solution: Send a valid storefront API key. - error: 403 Forbidden cause: Storefront key inactive or rate-limited solution: Activate the key or wait for the rate limit window to reset. - id: get-channel title: Get Single Channel description: Retrieve a single channel by ID. request: | curl -X GET "http://localhost/api/shop/channels/1" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK { "id": 1, "code": "default", "timezone": null, "theme": "default", "hostname": "https://api-demo.bagisto.com", "logo": null, "favicon": null, "homeSeo": { "meta_title": "Demo store", "meta_keywords": "Demo store meta keyword", "meta_description": "Demo store meta description" }, "isMaintenanceOn": 0, "allowedIps": "192.168.45.51", "createdAt": null, "updatedAt": "2026-04-08T17:23:40+05:30", "logoUrl": null, "faviconUrl": null, "locales": [ "/api/shop/locales/1", "/api/shop/locales/10" ], "currencies": [ "/api/shop/currencies/1", "/api/shop/currencies/3", "/api/shop/currencies/4" ], "defaultLocale": "/api/shop/locales/1", "baseCurrency": "/api/shop/currencies/1", "translation": "/api/shop/channel_translations/1", "translations": [ "/api/shop/channel_translations/1", "/api/shop/channel_translations/5", "/api/shop/channel_translations/2", "/api/shop/channel_translations/3", "/api/shop/channel_translations/4" ] } commonErrors: - error: 404 Not Found cause: No channel with the given `{id}` exists solution: List channels via `GET /api/shop/channels` to discover valid IDs. - error: 401 Unauthorized cause: Missing or invalid `X-STOREFRONT-KEY` solution: Send a valid storefront API key. --- # Channels A **channel** represents an individual storefront — its hostname, theme, allowed locales and currencies, default locale, base currency, and SEO defaults. Most stores have one channel; multi-channel installations expose every channel through the same endpoints. ## Endpoints | Method | Path | Purpose | |--------|-------------------------------|----------------------------------| | GET | `/api/shop/channels` | Paginated list of channels | | GET | `/api/shop/channels/{id}` | Single channel by ID | Use the example switcher above the curl block to flip between the two. ## Request Headers | Header | Required | Description | |--------------------|----------|------------------------------------------| | `Accept` | Yes | `application/json` | | `X-STOREFRONT-KEY` | Yes | Storefront API key (`pk_storefront_…`) | ## Query Parameters (collection only) | Parameter | Type | Default | Description | |-------------|---------|---------|---------------------------------------------| | `page` | integer | 1 | Page number (1-based) | | `per_page` | integer | 10 | Items per page. Max **50**. | The collection response carries pagination headers (`X-Total-Count`, `X-Page`, `X-Per-Page`, `X-Total-Pages`). See [Pagination](/api/rest-api/introduction#pagination). ## Channel Object Fields Both endpoints return the same shape — the collection wraps an array of these objects, the single endpoint returns one. | Field | Type | Description | |-------------------|-----------------------|----------------------------------------------------------------------------| | `id` | integer | Channel primary key | | `code` | string | Unique channel code — pass it as the `X-Channel` header to scope a request | | `hostname` | string | Public hostname for this channel | | `theme` | string | Theme assigned to the channel | | `timezone` | string \| null | Timezone code (e.g. `America/New_York`) | | `homeSeo` | object | SEO metadata: `{ meta_title, meta_keywords, meta_description }` | | `isMaintenanceOn` | boolean (0/1) | Maintenance mode flag | | `allowedIps` | string \| null | Comma-separated IP allowlist (used when maintenance mode is on) | | `logo`, `favicon` | string \| null | Storage paths | | `logoUrl`, `faviconUrl` | string \| null | Fully-qualified asset URLs | | `createdAt`, `updatedAt` | string (ISO-8601) \| null | Timestamps | | `locales` | array of IRI strings | Locales enabled for this channel — `GET <iri>` to dereference | | `currencies` | array of IRI strings | Currencies enabled for this channel | | `defaultLocale` | string (IRI) \| null | Default locale for the channel | | `baseCurrency` | string (IRI) \| null | Base currency for prices | | `translation` | string (IRI) \| null | Channel translation for the request locale (single IRI, **not inline**) | | `translations` | array of IRI strings | All locale translations | > Unlike `Attribute` / `Category`, the channel's `translation` is returned as an **IRI**, not an inline object. Follow it with `GET <iri>` to read `name`, `description`, `maintenanceModeText`, etc. See [IRIs & HATEOAS](/api/rest-api/introduction#iris-hateoas). ## Use Cases - Discover the channel `code` to send as `X-Channel` on subsequent requests. - Read `locales` / `currencies` / `defaultLocale` / `baseCurrency` IRIs to render channel-aware switchers. - Read `homeSeo` for the home-page `<title>` and meta tags before any product is loaded. - Check `isMaintenanceOn` before showing the storefront; if `1`, fetch the `translation` IRI for the localized maintenance text. ## Related Resources - [Channel Translations](/api/rest-api/shop/channels/get-channel-translations) - [Introduction → IRIs & HATEOAS](/api/rest-api/introduction#iris-hateoas) --- # Get Checkout Addresses URL: /api/rest-api/shop/checkout/get-addresses --- outline: false examples: - id: get-checkout-addresses title: Get Checkout Addresses description: Retrieve the guest / authenticated customer's saved checkout addresses to select as shipping or billing address during checkout. request: | GET /api/shop/checkout-addresses Content-Type: application/json X-STOREFRONT-KEY: pk_storefront_PvlE42nWGsKRVIf8bDlJngTPAdWAZbIy Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... response: | { "data": { "addresses": [ { "id": 1, "firstName": "John", "lastName": "Doe", "email": "john@example.com", "address": "123 Main St", "city": "New York", "state": "NY", "country": "US", "postcode": "10001", "phone": "1234567890" } ], "defaultShippingId": 1, "defaultBillingId": 1 } } commonErrors: - error: 401 Unauthorized cause: Customer not authenticated solution: Provide valid Bearer token - error: 404 Not Found cause: Customer has no saved addresses solution: Add new address first --- # Get Checkout Addresses Retrieve guest / the authenticated customer's checkout saved addresses so they can select one as their shipping or billing address during checkout. This endpoint returns previously saved addresses — it does not create new ones. ## Endpoint ``` GET /api/shop/checkout-addresses ``` ## Request Headers | Header | Required | Description | |--------|----------|-------------| | `Content-Type` | Yes | application/json | | `X-STOREFRONT-KEY` | Yes | Your storefront API key | | `Authorization` | Yes | Bearer token (customer login required) | ## Response Fields (200 OK) | Field | Type | Description | |-------|------|-------------| | `addresses` | array | List of customer addresses | | `defaultShippingId` | integer | ID of default shipping address | | `defaultBillingId` | integer | ID of default billing address | ## Address Fields | Field | Type | Description | |-------|------|-------------| | `id` | integer | Address ID | | `firstName` | string | First name | | `lastName` | string | Last name | | `email` | string | Email address | | `address` | string | Street address | | `city` | string | City | | `state` | string | State/Province | | `country` | string | Country code | | `postcode` | string | Postal code | | `phone` | string | Phone number | ## Use Cases - Populate shipping address dropdown - Populate billing address dropdown - Show saved addresses in checkout - Allow address selection during checkout - Set default addresses ## Related Resources - [Set Shipping Address](/api/rest-api/shop/checkout/set-shipping-address) - [Set Billing Address](/api/rest-api/shop/checkout/set-billing-address) - [Get Shipping Methods](/api/rest-api/shop/checkout/get-shipping-methods) ::: tip Fetching Customer Addresses To retrieve the full list of a customer's saved addresses (outside of checkout), use the dedicated customer address queries: - **GraphQL:** [Get Customer Addresses](/api/graphql-api/shop/queries/get-customer-addresses) - **REST:** [Get Customer Addresses](/api/rest-api/shop/customers/get-customer-addresses) ::: --- # Get Payment Methods URL: /api/rest-api/shop/checkout/get-payment-methods --- outline: false examples: - id: get-payment-methods title: Get Available Payment Methods description: Retrieve available payment methods for checkout. request: | GET /api/shop/checkout-payment-methods Content-Type: application/json X-STOREFRONT-KEY: pk_storefront_PvlE42nWGsKRVIf8bDlJngTPAdWAZbIy response: | { "data": [ { "id": 1, "code": "card", "name": "Credit/Debit Card", "description": "Pay with credit or debit card", "isActive": true, "instructions": "" }, { "id": 2, "code": "paypal", "name": "PayPal", "description": "Pay securely with PayPal", "isActive": true, "instructions": "You will be redirected to PayPal" }, { "id": 3, "code": "bank_transfer", "name": "Bank Transfer", "description": "Direct bank transfer", "isActive": true, "instructions": "Bank details will be provided after order" } ] } commonErrors: - error: 401 Unauthorized cause: Invalid X-STOREFRONT-KEY solution: Provide valid storefront API key --- # Get Payment Methods Retrieve available payment methods for checkout. ## Endpoint ``` GET /api/shop/checkout-payment-methods ``` ## Request Headers | Header | Required | Description | |--------|----------|-------------| | `Content-Type` | Yes | application/json | | `X-STOREFRONT-KEY` | Yes | Your storefront API key | ## Response Fields (200 OK) | Field | Type | Description | |-------|------|-------------| | `id` | integer | Payment method ID | | `code` | string | Payment method code | | `name` | string | Display name | | `description` | string | Method description | | `isActive` | boolean | Method active status | | `instructions` | string | Payment instructions or notes | | `icon` | string | Method icon URL | | `additionalData` | object | Additional configuration (if any) | ## Use Cases - Display payment options during checkout - Allow customer to select payment method - Show payment instructions - Validate payment method availability - Implement payment gateway integration ## Common Payment Methods - Credit/Debit Card (Stripe, Square, etc.) - PayPal - Bank Transfer - Cash on Delivery - Wallet/Gift Card - Buy Now Pay Later (Klarna, Afterpay) ## Notes - Methods availability depends on store configuration - Some methods may have requirements or restrictions - Instructions help guide customer through payment - Payment processing happens after order placement ## Related Resources - [Set Payment Method](/api/rest-api/shop/checkout/set-payment-method) - [Get Shipping Methods](/api/rest-api/shop/checkout/get-shipping-methods) - [Place Order](/api/rest-api/shop/checkout/place-order) --- # Get Shipping Methods URL: /api/rest-api/shop/checkout/get-shipping-methods --- outline: false examples: - id: get-shipping-methods title: Get Available Shipping Methods description: Retrieve available shipping methods for checkout. request: | GET /api/shop/checkout-shipping-methods Content-Type: application/json X-STOREFRONT-KEY: pk_storefront_PvlE42nWGsKRVIf8bDlJngTPAdWAZbIy { "country": "US", "state": "NY", "postcode": "10001" } response: | { "data": [ { "id": 1, "code": "standard", "name": "Standard Shipping", "description": "Delivery in 5-7 business days", "price": 10.00, "estimatedDays": 7 }, { "id": 2, "code": "express", "name": "Express Shipping", "description": "Delivery in 2-3 business days", "price": 25.00, "estimatedDays": 3 } ] } commonErrors: - error: 422 Validation Error cause: Invalid location data solution: Provide valid country, state, and postcode - error: 400 Bad Request cause: No shipping methods available for location solution: Check location or contact support --- # Get Shipping Methods Retrieve available shipping methods based on address and cart contents. ## Endpoint ``` POST /api/shop/checkout-shipping-methods ``` ## Request Headers | Header | Required | Description | |--------|----------|-------------| | `Content-Type` | Yes | application/json | | `X-STOREFRONT-KEY` | Yes | Your storefront API key | ## Request Body ```json { "country": "US", "state": "NY", "postcode": "10001" } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `country` | string | Yes | Country code (ISO) | | `state` | string | Yes | State/Province code | | `postcode` | string | Yes | Postal code | ## Response Fields (200 OK) | Field | Type | Description | |-------|------|-------------| | `id` | integer | Shipping method ID | | `code` | string | Shipping method code | | `name` | string | Display name | | `description` | string | Method description | | `price` | decimal | Shipping cost | | `estimatedDays` | integer | Estimated delivery days | | `maxDeliveryDate` | string | Expected delivery date | ## Use Cases - Display shipping options during checkout - Calculate shipping cost - Allow customer to select shipping method - Show delivery estimates - Filter methods by location ## Notes - Methods vary by location and product - Prices may be calculated dynamically - Some methods may have weight/dimension limits - International shipping may have restrictions ## Related Resources - [Set Shipping Method](/api/rest-api/shop/checkout/set-shipping-method) - [Get Payment Methods](/api/rest-api/shop/checkout/get-payment-methods) - [Get Checkout Addresses](/api/rest-api/shop/checkout/get-addresses) --- # Place Order URL: /api/rest-api/shop/checkout/place-order --- outline: false examples: - id: place-order title: Place Order description: Create an order from the cart. request: | POST /api/shop/checkout-orders Content-Type: application/json X-STOREFRONT-KEY: pk_storefront_PvlE42nWGsKRVIf8bDlJngTPAdWAZbIy Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... { "shippingMethodCode": "flatrate_flatrate", "paymentMethod": "paypal" } response: | { "data": { "order": { "id": 12345, "incrementId": "#000012345", "status": "pending", "grandTotal": 1329.97, "itemsCount": 2, "createdAt": "2024-01-15T10:30:00Z" }, "redirect": "https://checkout.paypal.com/..." }, "message": "Order placed successfully" } commonErrors: - error: 401 Unauthorized cause: Customer not authenticated solution: Provide valid Bearer token - error: 400 Bad Request cause: Missing required checkout details solution: Ensure shipping address, method, and payment method are set - error: 409 Conflict cause: Cart is empty solution: Add items to cart first - error: 422 Unprocessable Entity cause: Inventory not available solution: Verify product stock --- # Place Order Create an order from the shopping cart. This completes the checkout process. ## Endpoint ``` POST /api/shop/checkout-orders ``` ## Request Headers | Header | Required | Description | |--------|----------|-------------| | `Content-Type` | Yes | application/json | | `X-STOREFRONT-KEY` | Yes | Your storefront API key | | `Authorization` | Yes | Bearer token (customer login required) | ## Request Body ```json { "shippingMethodCode": "flatrate_flatrate", "paymentMethod": "paypal" } ``` ## Request Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `shippingMethodCode` | string | Yes | Code of selected shipping method | | `paymentMethod` | string | Yes | Selected payment method | ## Response Fields (200-201 Created) | Field | Type | Description | |-------|------|-------------| | `order` | object | Created order details | | `redirect` | string | Payment redirect URL (if needed) | | `message` | string | Success message | ## Order Fields | Field | Type | Description | |-------|------|-------------| | `id` | integer | Order ID | | `incrementId` | string | Order increment ID | | `status` | string | Order status | | `grandTotal` | decimal | Total amount | | `itemsCount` | integer | Number of items | | `createdAt` | string | Creation timestamp | | `invoiceUrl` | string | Invoice download URL | ## Order Status Values - `pending` - Awaiting payment confirmation - `processing` - Payment confirmed, preparing shipment - `shipped` - Order shipped - `delivered` - Order delivered - `canceled` - Order canceled - `failed` - Payment failed ## Pre-requisites All of these must be completed before placing order: 1. Cart must have items 2. Shipping address must be set 3. Shipping method must be selected 4. Billing address must be set 5. Payment method must be selected 6. All items in stock ## After Order Placement - Cart is automatically cleared - Customer receives confirmation email - Order status can be tracked - Invoice becomes available - Payment processing may redirect customer ## Validation Rules - Cart cannot be empty - All addresses must be complete - Inventory must be available for all items - Shipping method must match location - Payment method must be valid ## Use Cases - Complete customer checkout - Create order for in-store pickup - Process cash on delivery - Execute payment gateway transaction - Generate order confirmation and invoice ## Related Resources - [Get Cart](/api/rest-api/shop/cart/get-cart) - [Set Shipping Address](/api/rest-api/shop/checkout/set-shipping-address) - [Set Billing Address](/api/rest-api/shop/checkout/set-billing-address) - [Set Shipping Method](/api/rest-api/shop/checkout/set-shipping-method) - [Set Payment Method](/api/rest-api/shop/checkout/set-payment-method) - [Get Customer Orders](/api/rest-api/shop/customers/get-customer-orders) --- # Set Billing Address URL: /api/rest-api/shop/checkout/set-billing-address --- outline: false examples: - id: set-billing-address title: Set Billing Address description: Set the billing address for checkout. request: | POST /api/shop/checkout-addresses Content-Type: application/json X-STOREFRONT-KEY: pk_storefront_PvlE42nWGsKRVIf8bDlJngTPAdWAZbIy Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... { "addressId": 1, "useShippingAddress": false } response: | { "data": { "billingAddress": { "id": 1, "firstName": "John", "lastName": "Doe", "address": "123 Main St", "city": "New York", "state": "NY", "country": "US", "postcode": "10001", "phone": "1234567890" } }, "message": "Billing address set successfully" } commonErrors: - error: 401 Unauthorized cause: Customer not authenticated solution: Provide valid Bearer token - error: 404 Not Found cause: Address does not exist solution: Verify the address ID --- # Set Billing Address Set or update the billing address for the checkout process. ## Endpoint ``` POST /api/shop/checkout-addresses ``` ## Request Headers | Header | Required | Description | |--------|----------|-------------| | `Content-Type` | Yes | application/json | | `X-STOREFRONT-KEY` | Yes | Your storefront API key | | `Authorization` | Yes | Bearer token (customer login required) | ## Request Body **Option 1: Use Saved Address** ```json { "addressId": 1 } ``` **Option 2: Same as Shipping Address** ```json { "useShippingAddress": true } ``` **Option 3: New Address** ```json { "firstName": "Jane", "lastName": "Doe", "email": "jane@example.com", "address": "456 Oak Ave", "city": "Los Angeles", "state": "CA", "country": "US", "postcode": "90001", "phone": "9876543210" } ``` ## Response Fields (200 OK) | Field | Type | Description | |-------|------|-------------| | `billingAddress` | object | Confirmed billing address | | `message` | string | Success message | ## Billing Address Fields | Field | Type | Description | |-------|------|-------------| | `id` | integer | Address ID | | `firstName` | string | First name | | `lastName` | string | Last name | | `email` | string | Email | | `address` | string | Street address | | `city` | string | City | | `state` | string | State/Province | | `country` | string | Country code | | `postcode` | string | Postal code | | `phone` | string | Phone number | ## Use Cases - Set billing destination - Use different address for billing - Validate billing address - Create customer invoice address - Proceed to order placement ## Related Resources - [Set Shipping Address](/api/rest-api/shop/checkout/set-shipping-address) - [Get Checkout Addresses](/api/rest-api/shop/checkout/get-addresses) - [Create Order](/api/rest-api/shop/checkout/place-order) --- # Set Payment Method URL: /api/rest-api/shop/checkout/set-payment-method --- outline: false examples: - id: set-payment-method title: Set Payment Method description: Select a payment method for the order. request: | POST /api/shop/checkout-payment-methods Content-Type: application/json X-STOREFRONT-KEY: pk_storefront_PvlE42nWGsKRVIf8bDlJngTPAdWAZbIy Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... { "payment": { "method": "paypal" } } response: | { "data": { "paymentMethod": { "method": "paypal", "title": "PayPal", "description": "Pay securely with PayPal" } }, "message": "Payment method set successfully" } commonErrors: - error: 401 Unauthorized cause: Customer not authenticated solution: Provide valid Bearer token - error: 422 Unprocessable Entity cause: Invalid payment method solution: Use method from get-payment-methods response --- # Set Payment Method Select a payment method for the order checkout. ## Endpoint ``` POST /api/shop/checkout-payment-methods ``` ## Request Headers | Header | Required | Description | |--------|----------|-------------| | `Content-Type` | Yes | application/json | | `X-STOREFRONT-KEY` | Yes | Your storefront API key | | `Authorization` | Yes | Bearer token (customer login required) | ## Request Body ```json { "payment": { "method": "paypal" } } ``` ## Payment Method Options Common payment methods: - `paypal` - PayPal - `stripe` - Stripe - `cod` - Cash on Delivery - `bank_transfer` - Bank Transfer ## Response Fields (200 OK) | Field | Type | Description | |-------|------|-------------| | `paymentMethod` | object | Selected payment method details | | `message` | string | Success message | ## Payment Method Fields | Field | Type | Description | |-------|------|-------------| | `method` | string | Payment method code | | `title` | string | Display name | | `description` | string | Method description | | `instructions` | string | Payment instructions | ## Use Cases - Select PayPal payment - Choose credit card - Use cash on delivery - Select bank transfer - Finalize payment details ## Validation Rules - Payment method must be available - Must have valid billing address - Some methods restricted by location - Payment may require additional setup ## Related Resources - [Get Payment Methods](/api/rest-api/shop/checkout/get-payment-methods) - [Set Billing Address](/api/rest-api/shop/checkout/set-billing-address) - [Place Order](/api/rest-api/shop/checkout/place-order) --- # Set Shipping Address URL: /api/rest-api/shop/checkout/set-shipping-address --- outline: false examples: - id: set-shipping-address title: Set Shipping Address description: Set the shipping address for checkout. request: | POST /api/shop/checkout-addresses Content-Type: application/json X-STOREFRONT-KEY: pk_storefront_PvlE42nWGsKRVIf8bDlJngTPAdWAZbIy Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... { "addressId": 1 } response: | { "data": { "shippingAddress": { "id": 1, "firstName": "John", "lastName": "Doe", "address": "123 Main St", "city": "New York", "state": "NY", "country": "US", "postcode": "10001", "phone": "1234567890" }, "cartTotal": 1319.97 }, "message": "Shipping address set successfully" } commonErrors: - error: 401 Unauthorized cause: Customer not authenticated solution: Provide valid Bearer token - error: 404 Not Found cause: Address does not exist solution: Verify the address ID --- # Set Shipping Address Set or update the shipping address for the checkout process. ## Endpoint ``` POST /api/shop/checkout-addresses ``` ## Request Headers | Header | Required | Description | |--------|----------|-------------| | `Content-Type` | Yes | application/json | | `X-STOREFRONT-KEY` | Yes | Your storefront API key | | `Authorization` | Yes | Bearer token (customer login required) | ## Request Body Either use saved address ID or provide new address: **Option 1: Use Saved Address** ```json { "addressId": 1 } ``` **Option 2: New Address** ```json { "firstName": "John", "lastName": "Doe", "email": "john@example.com", "address": "123 Main St", "city": "New York", "state": "NY", "country": "US", "postcode": "10001", "phone": "1234567890" } ``` ## Response Fields (200 OK) | Field | Type | Description | |-------|------|-------------| | `shippingAddress` | object | Confirmed shipping address | | `cartTotal` | decimal | Updated cart total | | `message` | string | Success message | ## Shipping Address Fields | Field | Type | Description | |-------|------|-------------| | `id` | integer | Address ID | | `firstName` | string | First name | | `lastName` | string | Last name | | `email` | string | Email | | `address` | string | Street address | | `city` | string | City | | `state` | string | State/Province | | `country` | string | Country code | | `postcode` | string | Postal code | | `phone` | string | Phone number | ## Use Cases - Set shipping destination - Calculate shipping costs - Validate address - Proceed to shipping method selection - Update address during checkout ## Effects - Shipping methods are recalculated - Shipping cost may change - Cart total is updated - Address is locked for shipping ## Related Resources - [Get Checkout Addresses](/api/rest-api/shop/checkout/get-addresses) - [Set Billing Address](/api/rest-api/shop/checkout/set-billing-address) - [Get Shipping Methods](/api/rest-api/shop/checkout/get-shipping-methods) --- # Set Shipping Method URL: /api/rest-api/shop/checkout/set-shipping-method --- outline: false examples: - id: set-shipping-method title: Set Shipping Method description: Select a shipping method for the order. request: | POST /api/shop/checkout-shipping-methods Content-Type: application/json X-STOREFRONT-KEY: pk_storefront_PvlE42nWGsKRVIf8bDlJngTPAdWAZbIy Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... { "shippingMethodCode": "flatrate_flatrate", "shippingMethod": "flat_rate" } response: | { "data": { "shippingMethod": { "code": "flatrate_flatrate", "method": "flat_rate", "title": "Flat Rate", "price": 10.00 }, "cartTotal": 1329.97 }, "message": "Shipping method set successfully" } commonErrors: - error: 401 Unauthorized cause: Customer not authenticated solution: Provide valid Bearer token - error: 422 Unprocessable Entity cause: Invalid shipping method solution: Use method from get-shipping-methods response --- # Set Shipping Method Select a shipping method for the order. ## Endpoint ``` POST /api/shop/checkout-shipping-methods ``` ## Request Headers | Header | Required | Description | |--------|----------|-------------| | `Content-Type` | Yes | application/json | | `X-STOREFRONT-KEY` | Yes | Your storefront API key | | `Authorization` | Yes | Bearer token (customer login required) | ## Request Body ```json { "shippingMethodCode": "flatrate_flatrate", "shippingMethod": "flat_rate" } ``` ## Request Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `shippingMethodCode` | string | Yes | Code of the shipping method | | `shippingMethod` | string | Yes | Shipping method type | ## Response Fields (200 OK) | Field | Type | Description | |-------|------|-------------| | `shippingMethod` | object | Selected shipping method details | | `cartTotal` | decimal | Updated cart total with shipping | | `message` | string | Success message | ## Shipping Method Fields | Field | Type | Description | |-------|------|-------------| | `code` | string | Shipping method code | | `method` | string | Shipping method type | | `title` | string | Display name | | `price` | decimal | Shipping cost | | `carrier` | string | Carrier name | ## Use Cases - Select standard shipping - Choose express shipping - Apply overnight delivery - Calculate final total - Proceed to payment ## Important Notes - Must set shipping address first - Different methods available per location - Cost varies by location and weight - Some methods may have time restrictions ## Related Resources - [Get Shipping Methods](/api/rest-api/shop/checkout/get-shipping-methods) - [Set Shipping Address](/api/rest-api/shop/checkout/set-shipping-address) - [Set Payment Method](/api/rest-api/shop/checkout/set-payment-method) --- # Countries URL: /api/rest-api/shop/countries/get-countries --- outline: false examples: - id: list-countries title: List Countries description: Retrieve a paginated list of countries with their inline translations. request: | curl -X GET "http://localhost/api/shop/countries?per_page=2" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK X-Total-Count: 255 X-Page: 1 X-Per-Page: 2 X-Total-Pages: 128 [ { "id": 1, "code": "AF", "name": "Afghanistan", "states": [], "translations": [ { "id": 1, "countryId": 1, "locale": "ar", "name": "أفغانستان" }, { "id": 256, "countryId": 1, "locale": "es", "name": "Afganistán" }, { "id": 511, "countryId": 1, "locale": "fa", "name": "افغانستان" }, { "id": 766, "countryId": 1, "locale": "pt_BR", "name": "Afeganistão" } ] }, { "id": 2, "code": "AX", "name": "Åland Islands", "states": [], "translations": [ { "id": 2, "countryId": 2, "locale": "ar", "name": "جزر آلاند" } ] } ] commonErrors: - error: 401 Unauthorized cause: Missing or invalid `X-STOREFRONT-KEY` solution: Send a valid storefront API key. - id: get-country title: Get Single Country description: Retrieve a single country by ID. States are inlined when the country has them. request: | curl -X GET "http://localhost/api/shop/countries/40" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK { "id": 40, "code": "CA", "name": "Canada", "states": [ { "id": 66, "countryId": 40, "countryCode": "CA", "code": "AB", "defaultName": "Alberta", "translations": [ "/api/shop/country_state_translations/66", "/api/shop/country_state_translations/634", "/api/shop/country_state_translations/1220", "/api/shop/country_state_translations/1788" ] }, { "id": 67, "countryId": 40, "countryCode": "CA", "code": "BC", "defaultName": "British Columbia", "translations": [ "/api/shop/country_state_translations/67", "/api/shop/country_state_translations/635", "/api/shop/country_state_translations/1221", "/api/shop/country_state_translations/1789" ] } ], "translations": [ { "id": 40, "countryId": 40, "locale": "ar", "name": "كندا" }, { "id": 295, "countryId": 40, "locale": "es", "name": "Canadá" } ] } commonErrors: - error: 404 Not Found cause: No country with the given `{id}` exists solution: List countries via `GET /api/shop/countries` to discover valid IDs. - error: 401 Unauthorized cause: Missing or invalid `X-STOREFRONT-KEY` solution: Send a valid storefront API key. --- # Countries Countries are the catalog of nations available for billing/shipping addresses, taxes, and storefront scoping. Each country may have one or more sub-divisions ("states", "provinces", "territories" — generically called *country states* in this API). ## Endpoints | Method | Path | Purpose | |--------|-------------------------------|------------------------------------------| | GET | `/api/shop/countries` | Paginated list of countries | | GET | `/api/shop/countries/{id}` | Single country by ID | Use the example switcher above to flip between the two. ## Request Headers | Header | Required | Description | |--------------------|----------|------------------------------------------| | `Accept` | Yes | `application/json` | | `X-STOREFRONT-KEY` | Yes | Storefront API key (`pk_storefront_…`) | | `X-Locale` | No | Override request locale | ## Query Parameters (collection only) | Parameter | Type | Default | Description | |-------------|---------|---------|---------------------------------------------| | `page` | integer | 1 | Page number (1-based) | | `per_page` | integer | 10 | Items per page. Max **50**. | Pagination headers (`X-Total-Count`, `X-Page`, `X-Per-Page`, `X-Total-Pages`) are emitted on the collection. See [Pagination](/api/rest-api/introduction#pagination). ## Country Object Fields Both endpoints return the same shape — the collection wraps an array of these objects, the single endpoint returns one. | Field | Type | Description | |----------------|---------|--------------------------------------------------------------------------------------| | `id` | integer | Country primary key | | `code` | string | ISO 3166-1 alpha-2 country code (`AF`, `CA`, `US`, `IN`, …) | | `name` | string | Default English name | | `states` | array | Inline list of [country states](/api/rest-api/shop/countries/get-country-states) for this country. Empty `[]` for countries with no sub-divisions in Bagisto's data | | `translations` | array | All locale translations as **inline objects**: `{ id, countryId, locale, name }` | > ⚠️ Unlike `Attribute` / `Channel` / `Category`, `translations` on Country is returned as **inline objects** (not IRI strings). This avoids an extra round-trip per locale. There is no `translation` field for the request locale — pick the entry that matches your `X-Locale`. ### Inline `states[]` shape Each entry has the same fields as `/country-states/{id}` — see the [Country States](/api/rest-api/shop/countries/get-country-states) page. ## Use Cases - Populate a country dropdown in a checkout / address form. - Look up a country by ISO code (`?code=US`) is **not** supported — fetch the full list and filter client-side, or use the GraphQL query for a single lookup. - Resolve a country's localized display name from `translations[]` based on the customer's locale. - Chain to the country's states for a country/state cascade picker (use the [nested states endpoint](/api/rest-api/shop/countries/get-country-states) to skip refetching the country payload). ## Related Resources - [Country States](/api/rest-api/shop/countries/get-country-states) - [Introduction → IRIs & HATEOAS](/api/rest-api/introduction#iris-hateoas) --- # Country States URL: /api/rest-api/shop/countries/get-country-states --- outline: false examples: - id: list-country-states-nested title: List States for a Country (nested) description: Retrieve every state for a single country. Recommended for country/state cascade pickers. request: | curl -X GET "http://localhost/api/shop/countries/40/states?per_page=2" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK X-Total-Count: 13 X-Page: 1 X-Per-Page: 2 X-Total-Pages: 7 [ { "id": 66, "countryId": 40, "countryCode": "CA", "code": "AB", "defaultName": "Alberta", "translations": [ "/api/shop/country_state_translations/66", "/api/shop/country_state_translations/634", "/api/shop/country_state_translations/1220", "/api/shop/country_state_translations/1788" ] }, { "id": 67, "countryId": 40, "countryCode": "CA", "code": "BC", "defaultName": "British Columbia", "translations": [ "/api/shop/country_state_translations/67", "/api/shop/country_state_translations/635", "/api/shop/country_state_translations/1221", "/api/shop/country_state_translations/1789" ] } ] commonErrors: - error: 404 Not Found cause: No country with the given `{country_id}` exists solution: List countries via `GET /api/shop/countries` to discover valid IDs. - error: 401 Unauthorized cause: Missing or invalid `X-STOREFRONT-KEY` solution: Send a valid storefront API key. - id: get-country-state-nested title: Get Single State (nested) description: Retrieve a single state scoped to a parent country. request: | curl -X GET "http://localhost/api/shop/countries/40/states/66" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK { "id": 66, "countryId": 40, "countryCode": "CA", "code": "AB", "defaultName": "Alberta", "translations": [ "/api/shop/country_state_translations/66", "/api/shop/country_state_translations/634", "/api/shop/country_state_translations/1220", "/api/shop/country_state_translations/1788" ] } commonErrors: - error: 404 Not Found cause: Either the country `{country_id}` or the state `{id}` doesn't exist, or the state belongs to a different country solution: Verify both IDs against `GET /api/shop/countries` and `GET /api/shop/country-states`. - error: 401 Unauthorized cause: Missing or invalid `X-STOREFRONT-KEY` solution: Send a valid storefront API key. - id: list-country-states-flat title: List All States (flat) description: Retrieve every state across every country in one paginated stream. request: | curl -X GET "http://localhost/api/shop/country-states?per_page=2" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK X-Total-Count: 586 X-Page: 1 X-Per-Page: 2 X-Total-Pages: 293 [ { "id": 1, "countryId": 244, "countryCode": "US", "code": "AL", "defaultName": "Alabama", "translations": [ "/api/shop/country_state_translations/1", "/api/shop/country_state_translations/569", "/api/shop/country_state_translations/1155", "/api/shop/country_state_translations/1723" ] }, { "id": 2, "countryId": 244, "countryCode": "US", "code": "AK", "defaultName": "Alaska", "translations": [ "/api/shop/country_state_translations/2" ] } ] commonErrors: - error: 401 Unauthorized cause: Missing or invalid `X-STOREFRONT-KEY` solution: Send a valid storefront API key. - id: get-country-state-flat title: Get Single State (flat) description: Retrieve a single state by its global ID without specifying a parent country. request: | curl -X GET "http://localhost/api/shop/country-states/66" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK { "id": 66, "countryId": 40, "countryCode": "CA", "code": "AB", "defaultName": "Alberta", "translations": [ "/api/shop/country_state_translations/66", "/api/shop/country_state_translations/634", "/api/shop/country_state_translations/1220", "/api/shop/country_state_translations/1788" ] } commonErrors: - error: 404 Not Found cause: No state with the given `{id}` exists solution: List states via `GET /api/shop/country-states` to discover valid IDs. - error: 401 Unauthorized cause: Missing or invalid `X-STOREFRONT-KEY` solution: Send a valid storefront API key. --- # Country States A *country state* is any sub-division of a country — state, province, territory, prefecture, etc. Each state belongs to exactly one country and identifies itself with a short code (`AL`, `CA`, `BC`, `MH`, …). ## Endpoints The same resource is exposed under **two URL shapes** so clients can choose the access pattern that fits their UI: | Method | Path | Purpose | |--------|---------------------------------------------------|--------------------------------------------------------------------------------| | GET | `/api/shop/countries/{country_id}/states` | **Nested** — every state for a single country (recommended for cascade pickers) | | GET | `/api/shop/countries/{country_id}/states/{id}` | **Nested** — single state, scoped to its parent country | | GET | `/api/shop/country-states` | **Flat** — every state across every country in one paginated stream | | GET | `/api/shop/country-states/{id}` | **Flat** — single state by global ID | The response shape is **identical** in all four — the only difference is the URL/scoping. Use the example switcher above the curl block to flip through them. > Both shapes return state IDs from the same global sequence — `id: 66` is "Alberta" whether you reach it via `/countries/40/states/66` or `/country-states/66`. ## When to use which | Scenario | Endpoint | |-------------------------------------------------------------|---------------------------------------------------| | Country/state cascade dropdown in a checkout form | `GET /api/shop/countries/{country_id}/states` | | Resolve a single state ID (e.g. stored on an order address) | `GET /api/shop/country-states/{id}` | | Build an offline cache of every state in the world | `GET /api/shop/country-states` (paginate through) | | Validate that a state belongs to a specific country | `GET /api/shop/countries/{country_id}/states/{id}` (404 if mismatched) | ## Request Headers | Header | Required | Description | |--------------------|----------|------------------------------------------| | `Accept` | Yes | `application/json` | | `X-STOREFRONT-KEY` | Yes | Storefront API key (`pk_storefront_…`) | ## Query Parameters (collections only) | Parameter | Type | Default | Description | |-------------|---------|---------|---------------------------------------------| | `page` | integer | 1 | Page number (1-based) | | `per_page` | integer | 10 | Items per page. Max **50**. | Pagination headers (`X-Total-Count`, `X-Page`, `X-Per-Page`, `X-Total-Pages`) are emitted on both collection variants. See [Pagination](/api/rest-api/introduction#pagination). ## State Object Fields | Field | Type | Description | |----------------|-----------------------|------------------------------------------------------------------------------------------| | `id` | integer | State primary key (globally unique across countries) | | `countryId` | integer | Owning country ID | | `countryCode` | string | ISO country code of the parent (`CA`, `US`, …) | | `code` | string | State/province code within the country (`AB`, `CA`, `MH`, …) | | `defaultName` | string | Default English name | | `translations` | array of IRI strings | One IRI per locale translation. `GET /api/shop/country_state_translations/{id}` to dereference | > Unlike `Country`, where `translations` is inlined, `country state` translations are returned as IRI strings. This keeps the flat list (~586 rows) lean. See [IRIs & HATEOAS](/api/rest-api/introduction#iris-hateoas). ## Use Cases - Render a state/province dropdown that depends on the chosen country (use the **nested** collection). - Validate a `state_id` saved on an order against its `country_id` (use the **nested single** — 404 means mismatched). - Resolve a localized state name for a given customer locale by following one of the `translations[]` IRIs. - Bulk-export every state for a CSV / SPA cache (use the **flat** collection with `?per_page=50`). ## Related Resources - [Countries](/api/rest-api/shop/countries/get-countries) — the parent resource; emits the same state objects inline under `Country.states[]` - [Introduction → IRIs & HATEOAS](/api/rest-api/introduction#iris-hateoas) --- # Get Customer Downloadable Product URL: /api/rest-api/shop/customer-downloadable-products/get-customer-downloadable-product --- outline: false examples: - id: get-customer-downloadable-product title: Get Single Customer Downloadable Product description: Retrieve details of a specific downloadable product purchase by its ID. request: | GET /api/shop/customer-downloadable-products/1 Content-Type: application/json X-STOREFRONT-KEY: pk_storefront_PvlE42nWGsKRVIf8bDlJngTPAdWAZbIy Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... response: | { "id": 1, "productName": "Laravel E-Book", "name": "PDF Download", "url": null, "file": "downloadable/laravel-ebook.pdf", "fileName": "laravel-ebook.pdf", "type": "file", "downloadBought": 5, "downloadUsed": 1, "downloadCanceled": 0, "status": "available", "remainingDownloads": 4, "customerId": 1, "orderId": 101, "orderItemId": 201, "order": { "id": 101, "incrementId": "101", "status": "completed" }, "createdAt": "2025-06-15T10:30:00.000000Z", "updatedAt": "2025-06-15T10:30:00.000000Z" } commonErrors: - error: 401 Unauthorized cause: Missing or invalid Bearer token solution: Login and provide a valid customer authentication token - error: 404 Not Found cause: The downloadable product purchase ID does not exist or belongs to another customer solution: Verify the purchase ID and ensure you are authenticated as the correct customer - error: 403 Forbidden cause: Storefront key is missing or invalid solution: Provide a valid X-STOREFRONT-KEY header --- # Get Customer Downloadable Product Retrieve details of a specific downloadable product purchase by its ID. This is a **read-only** API — customers can view a single purchased downloadable link, check its download status, and see remaining downloads. ## Endpoint ``` GET /api/shop/customer-downloadable-products/{id} ``` ## Path Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `id` | Integer | Yes | The downloadable product purchase ID | ## Request Headers | Header | Value | Required | Description | |--------|-------|----------|-------------| | `Content-Type` | `application/json` | Yes | Request content type | | `X-STOREFRONT-KEY` | `pk_storefront_xxx` | Yes | Storefront API key | | `Authorization` | `Bearer {token}` | Yes | Customer authentication token | ## Response Fields | Field | Type | Description | |-------|------|-------------| | `id` | Integer | Downloadable link purchase ID | | `productName` | String | Name of the purchased product | | `name` | String | Name of the downloadable link | | `url` | String\|null | External download URL (for URL-type links) | | `file` | String\|null | File path (for file-type links) | | `fileName` | String | Display name of the file | | `type` | String | Link type: `file` or `url` | | `downloadBought` | Integer | Total number of allowed downloads | | `downloadUsed` | Integer | Number of times downloaded | | `downloadCanceled` | Integer | Number of canceled downloads | | `status` | String | Purchase status: `available`, `expired`, or `pending` | | `remainingDownloads` | Integer | Computed remaining downloads (`null` if unlimited) | | `customerId` | Integer | ID of the customer who purchased | | `orderId` | Integer | Associated order ID | | `orderItemId` | Integer | Associated order item ID | | `order` | Object | Associated order details | | `createdAt` | DateTime | Purchase creation date | | `updatedAt` | DateTime | Purchase last update date | ## cURL Example ```bash curl -X GET "https://api-demo.bagisto.com/api/shop/customer-downloadable-products/1" \ -H "X-STOREFRONT-KEY: pk_storefront_your_key_here" \ -H "Authorization: Bearer YOUR_CUSTOMER_TOKEN" \ -H "Content-Type: application/json" ``` ## Error Responses ### Item Not Found (404) ```json { "message": "Customer downloadable product with ID \"999\" not found" } ``` ### Accessing Another Customer's Purchase (404) Requesting a purchase that belongs to a different customer returns the same 404 response, preventing enumeration attacks: ```json { "message": "Customer downloadable product with ID \"5\" not found" } ``` ## Notes - **Customer isolation:** A customer can only access their own purchases. Requesting another customer's purchase returns a 404. - **Computed field:** `remainingDownloads` is calculated as `downloadBought - downloadUsed - downloadCanceled`. Returns `null` for unlimited downloads. --- # Get Customer Downloadable Products URL: /api/rest-api/shop/customer-downloadable-products/get-customer-downloadable-products --- outline: false examples: - id: get-customer-downloadable-products title: Get All Customer Downloadable Products description: Retrieve all downloadable product purchases for the authenticated customer. request: | GET /api/shop/customer-downloadable-products Content-Type: application/json X-STOREFRONT-KEY: pk_storefront_PvlE42nWGsKRVIf8bDlJngTPAdWAZbIy Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... response: | [ { "id": 1, "productName": "Laravel E-Book", "name": "PDF Download", "url": null, "file": "downloadable/laravel-ebook.pdf", "fileName": "laravel-ebook.pdf", "type": "file", "downloadBought": 5, "downloadUsed": 1, "downloadCanceled": 0, "status": "available", "remainingDownloads": 4, "customerId": 1, "orderId": 101, "orderItemId": 201, "order": { "id": 101, "incrementId": "101", "status": "completed" }, "createdAt": "2025-06-15T10:30:00.000000Z", "updatedAt": "2025-06-15T10:30:00.000000Z" }, { "id": 2, "productName": "Stock Photo Pack", "name": "High-Res Bundle", "url": "https://cdn.example.com/photo-pack.zip", "file": null, "fileName": "photo-pack.zip", "type": "url", "downloadBought": 3, "downloadUsed": 3, "downloadCanceled": 0, "status": "expired", "remainingDownloads": 0, "customerId": 1, "orderId": 102, "orderItemId": 202, "order": { "id": 102, "incrementId": "102", "status": "completed" }, "createdAt": "2025-06-10T08:00:00.000000Z", "updatedAt": "2025-06-12T14:00:00.000000Z" } ] commonErrors: - error: 401 Unauthorized cause: Missing or invalid Bearer token solution: Login and provide a valid customer authentication token - error: 403 Forbidden cause: Storefront key is missing or invalid solution: Provide a valid X-STOREFRONT-KEY header --- # Get Customer Downloadable Products Retrieve all downloadable product purchases belonging to the authenticated customer. This is a **read-only** API — customers can view their purchased downloadable links, check download status, and see remaining downloads. ## Endpoint ``` GET /api/shop/customer-downloadable-products ``` ## Request Headers | Header | Value | Required | Description | |--------|-------|----------|-------------| | `Content-Type` | `application/json` | Yes | Request content type | | `X-STOREFRONT-KEY` | `pk_storefront_xxx` | Yes | Storefront API key | | `Authorization` | `Bearer {token}` | Yes | Customer authentication token | ## Response Fields | Field | Type | Description | |-------|------|-------------| | `id` | Integer | Downloadable link purchase ID | | `productName` | String | Name of the purchased product | | `name` | String | Name of the downloadable link | | `url` | String\|null | External download URL (for URL-type links) | | `file` | String\|null | File path (for file-type links) | | `fileName` | String | Display name of the file | | `type` | String | Link type: `file` or `url` | | `downloadBought` | Integer | Total number of allowed downloads | | `downloadUsed` | Integer | Number of times downloaded | | `downloadCanceled` | Integer | Number of canceled downloads | | `status` | String | Purchase status: `available`, `expired`, or `pending` | | `remainingDownloads` | Integer | Computed remaining downloads (`null` if unlimited) | | `customerId` | Integer | ID of the customer who purchased | | `orderId` | Integer | Associated order ID | | `orderItemId` | Integer | Associated order item ID | | `order` | Object | Associated order details | | `createdAt` | DateTime | Purchase creation date | | `updatedAt` | DateTime | Purchase last update date | ### Status Values | Status | Description | |--------|-------------| | `available` | Download link is active and can be used | | `pending` | Order has not been invoiced yet; download is not available | | `expired` | All downloads have been used or the link has expired | ## cURL Example ```bash curl -X GET "https://api-demo.bagisto.com/api/shop/customer-downloadable-products" \ -H "X-STOREFRONT-KEY: pk_storefront_your_key_here" \ -H "Authorization: Bearer YOUR_CUSTOMER_TOKEN" \ -H "Content-Type: application/json" ``` ## Empty Collection When the customer has no downloadable product purchases: ```json [] ``` ## Notes - **Read-only API:** Only `GET` operations are available. - **Customer isolation:** Purchases are automatically filtered by the authenticated customer. A customer can never see another customer's purchases. - **Field naming:** REST responses use camelCase field names (e.g., `productName`, `downloadBought`, `remainingDownloads`). - **Computed field:** `remainingDownloads` is calculated as `downloadBought - downloadUsed - downloadCanceled`. Returns `null` for unlimited downloads. --- # Download Customer Invoice PDF URL: /api/rest-api/shop/customer-invoices/download-customer-invoice-pdf --- outline: false examples: - id: download-customer-invoice-pdf title: Download Invoice PDF description: Download an invoice as a PDF file. request: | GET /api/shop/customer-invoices/1/pdf X-STOREFRONT-KEY: pk_storefront_PvlE42nWGsKRVIf8bDlJngTPAdWAZbIy Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... response: | Binary PDF file Content-Type: application/pdf Content-Disposition: attachment; filename="invoice-1.pdf" commonErrors: - error: 404 Not Found cause: Invoice with specified ID does not exist or does not belong to the customer solution: Verify the invoice ID and ensure it belongs to the authenticated customer's orders - error: 401 Unauthorized cause: Missing or invalid Bearer token solution: Login and provide a valid customer authentication token --- # Download Customer Invoice PDF Download an invoice as a PDF file. The response is a binary PDF stream containing the full invoice document. ## Endpoint ``` GET /api/shop/customer-invoices/{id}/pdf ``` ## Request Headers | Header | Required | Description | |--------|----------|-------------| | `X-STOREFRONT-KEY` | Yes | Your storefront API key | | `Authorization` | Yes | Bearer token (customer login required) | ## Path Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `id` | integer | Yes | Customer invoice ID | ## Response (200 OK) The response is a binary PDF file with the following headers: | Header | Value | |--------|-------| | `Content-Type` | `application/pdf` | | `Content-Disposition` | `attachment; filename="invoice-{id}.pdf"` | ### PDF Contents The PDF document includes: - Store information and logo - Billing and shipping addresses - Invoice line items with SKU, quantity, price, and totals - Financial summary (subtotal, tax, shipping, discount, grand total) - Invoice number, order number, and date ### cURL Example ```bash curl -X GET "https://api-demo.bagisto.com/api/shop/customer-invoices/1/pdf" \ -H "X-STOREFRONT-KEY: pk_storefront_your_key_here" \ -H "Authorization: Bearer YOUR_CUSTOMER_TOKEN" \ -o invoice-001.pdf ``` ## Error Responses **Not Found (404):** ```json { "message": "Customer invoice with ID \"999\" not found." } ``` **Unauthenticated (401):** ```json { "message": "Customer is not logged in." } ``` ## Notes - **Separate route:** The `/pdf` endpoint is a separate route (not an API Platform operation). - **Rendering engine:** Uses DomPDF for LTR locales and mPDF for RTL locales, supporting multilingual invoice rendering. - **Customer isolation:** Customers can only download PDFs for invoices from their own orders. - **Binary response:** The response body is a raw PDF binary stream, not JSON. ## Use Cases - Allow customers to download invoice PDFs for their records - Provide printable invoice documents - Support accounting and tax filing workflows ## Related Resources - [Get All Customer Invoices](/api/rest-api/shop/customer-invoices/get-customer-invoices) - [Get Single Customer Invoice](/api/rest-api/shop/customer-invoices/get-customer-invoice) - [Get Customer Orders](/api/rest-api/shop/customer-orders/get-customer-orders) --- # Get Customer Invoice URL: /api/rest-api/shop/customer-invoices/get-customer-invoice --- outline: false examples: - id: get-customer-invoice title: Get Single Customer Invoice description: Retrieve details of a specific customer invoice by ID. request: | GET /api/shop/customer-invoices/1 Content-Type: application/json X-STOREFRONT-KEY: pk_storefront_PvlE42nWGsKRVIf8bDlJngTPAdWAZbIy Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... response: | { "id": 1, "incrementId": "INV-001", "state": "paid", "totalQty": 2, "emailSent": true, "subTotal": 100.00, "baseSubTotal": 100.00, "grandTotal": 110.00, "baseGrandTotal": 110.00, "shippingAmount": 5.00, "baseShippingAmount": 5.00, "taxAmount": 5.00, "baseTaxAmount": 5.00, "discountAmount": 0.00, "baseDiscountAmount": 0.00, "shippingTaxAmount": 0.00, "baseShippingTaxAmount": 0.00, "subTotalInclTax": 105.00, "baseSubTotalInclTax": 105.00, "shippingAmountInclTax": 5.00, "baseShippingAmountInclTax": 5.00, "baseCurrencyCode": "USD", "channelCurrencyCode": "USD", "orderCurrencyCode": "USD", "transactionId": "TXN-12345", "reminders": 0, "nextReminderAt": null, "order": "/api/shop/customer-orders/1", "items": ["/api/shop/invoice-items/1", "/api/shop/invoice-items/2"], "createdAt": "2025-02-10T10:30:00.000000Z", "updatedAt": "2025-02-10T10:30:00.000000Z" } commonErrors: - error: 404 Not Found cause: Invoice with specified ID does not exist or does not belong to the customer solution: Verify the invoice ID and ensure it belongs to the authenticated customer's orders - error: 401 Unauthorized cause: Missing or invalid Bearer token solution: Login and provide a valid customer authentication token - error: 403 Forbidden cause: Storefront key is missing or invalid solution: Provide a valid X-STOREFRONT-KEY header --- # Get Customer Invoice Retrieve detailed information for a specific customer invoice by its ID. Customers can only access invoices from their own orders — requesting another customer's invoice returns a 404, preventing enumeration attacks. ## Endpoint ``` GET /api/shop/customer-invoices/{id} ``` ## Request Headers | Header | Required | Description | |--------|----------|-------------| | `Content-Type` | Yes | application/json | | `X-STOREFRONT-KEY` | Yes | Your storefront API key | | `Authorization` | Yes | Bearer token (customer login required) | ## Path Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `id` | integer | Yes | Customer invoice ID | ## Response Fields (200 OK) | Field | Type | Description | |-------|------|-------------| | `id` | integer | Invoice ID | | `incrementId` | string | Human-readable invoice number (e.g. `INV-001`) | | `state` | string | Invoice state | | `totalQty` | integer | Total quantity of items in the invoice | | `emailSent` | boolean | Whether the invoice email was sent | | `subTotal` | float | Sub total | | `baseSubTotal` | float | Base sub total | | `grandTotal` | float | Grand total | | `baseGrandTotal` | float | Base grand total | | `shippingAmount` | float | Shipping amount | | `baseShippingAmount` | float | Base shipping amount | | `taxAmount` | float | Tax amount | | `baseTaxAmount` | float | Base tax amount | | `discountAmount` | float | Discount amount | | `baseDiscountAmount` | float | Base discount amount | | `shippingTaxAmount` | float | Shipping tax amount | | `baseShippingTaxAmount` | float | Base shipping tax amount | | `subTotalInclTax` | float | Sub total including tax | | `baseSubTotalInclTax` | float | Base sub total including tax | | `shippingAmountInclTax` | float | Shipping amount including tax | | `baseShippingAmountInclTax` | float | Base shipping amount including tax | | `baseCurrencyCode` | string | Base currency code (e.g. `USD`) | | `channelCurrencyCode` | string | Channel currency code | | `orderCurrencyCode` | string | Order currency code | | `transactionId` | string | Payment transaction ID | | `reminders` | integer | Number of reminders sent | | `nextReminderAt` | string | Next reminder scheduled date | | `order` | string | Associated order IRI (e.g. `/api/shop/customer-orders/1`) | | `items` | array | Invoice line item IRIs | | `createdAt` | string | ISO 8601 creation timestamp | | `updatedAt` | string | ISO 8601 last update timestamp | ## Error Responses **Not Found (404):** ```json { "message": "Customer invoice with ID \"999\" not found." } ``` **Unauthenticated (401):** ```json { "message": "Customer is not logged in." } ``` **Accessing Another Customer's Invoice (404):** Requesting an invoice that belongs to a different customer's order returns the same 404 response, preventing enumeration attacks: ```json { "message": "Customer invoice with ID \"5\" not found." } ``` ## Use Cases - Display detailed invoice page in customer account - Show full financial breakdown of an invoice - Track payment state and transaction ID - View tax, shipping, and discount details - Check if invoice email was sent ## Related Resources - [Get All Customer Invoices](/api/rest-api/shop/customer-invoices/get-customer-invoices) - [Download Invoice PDF](/api/rest-api/shop/customer-invoices/download-customer-invoice-pdf) - [Get Customer Orders](/api/rest-api/shop/customer-orders/get-customer-orders) - [Get Customer Profile](/api/rest-api/shop/customers/get-customer-profile) --- # Get Customer Invoices URL: /api/rest-api/shop/customer-invoices/get-customer-invoices --- outline: false examples: - id: get-customer-invoices title: Get All Customer Invoices description: Retrieve all invoices for the authenticated customer's orders. request: | GET /api/shop/customer-invoices Content-Type: application/json X-STOREFRONT-KEY: pk_storefront_PvlE42nWGsKRVIf8bDlJngTPAdWAZbIy Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... response: | [ { "id": 1, "incrementId": "INV-001", "state": "paid", "totalQty": 2, "emailSent": true, "subTotal": 100.00, "baseSubTotal": 100.00, "grandTotal": 110.00, "baseGrandTotal": 110.00, "shippingAmount": 5.00, "baseShippingAmount": 5.00, "taxAmount": 5.00, "baseTaxAmount": 5.00, "discountAmount": 0.00, "baseDiscountAmount": 0.00, "shippingTaxAmount": 0.00, "baseShippingTaxAmount": 0.00, "subTotalInclTax": 105.00, "baseSubTotalInclTax": 105.00, "shippingAmountInclTax": 5.00, "baseShippingAmountInclTax": 5.00, "baseCurrencyCode": "USD", "channelCurrencyCode": "USD", "orderCurrencyCode": "USD", "transactionId": "TXN-12345", "reminders": 0, "nextReminderAt": null, "order": "/api/shop/customer-orders/1", "items": ["/api/shop/invoice-items/1"], "createdAt": "2025-02-10T10:30:00.000000Z", "updatedAt": "2025-02-10T10:30:00.000000Z" }, { "id": 2, "incrementId": "INV-002", "state": "pending", "totalQty": 1, "grandTotal": 55.00, "baseGrandTotal": 55.00, "baseCurrencyCode": "USD", "createdAt": "2025-02-10T14:00:00.000000Z", "updatedAt": "2025-02-10T14:00:00.000000Z" } ] commonErrors: - error: 401 Unauthorized cause: Missing or invalid Bearer token solution: Login and provide a valid customer authentication token - error: 403 Forbidden cause: Storefront key is missing or invalid solution: Provide a valid X-STOREFRONT-KEY header --- # Get Customer Invoices Retrieve all invoices belonging to the authenticated customer's orders. This is a **read-only** API — customers can only view their own invoices. Invoices are automatically scoped to the authenticated customer via the order relationship. ## Endpoint ``` GET /api/shop/customer-invoices ``` ## Request Headers | Header | Required | Description | |--------|----------|-------------| | `Content-Type` | Yes | application/json | | `X-STOREFRONT-KEY` | Yes | Your storefront API key | | `Authorization` | Yes | Bearer token (customer login required) | ## Response Fields (200 OK) The response is a plain JSON array of invoice objects. | Field | Type | Description | |-------|------|-------------| | `id` | integer | Invoice ID | | `incrementId` | string | Human-readable invoice number (e.g. `INV-001`) | | `state` | string | Invoice state | | `totalQty` | integer | Total quantity of items in the invoice | | `emailSent` | boolean | Whether the invoice email was sent | | `subTotal` | float | Sub total | | `baseSubTotal` | float | Base sub total | | `grandTotal` | float | Grand total | | `baseGrandTotal` | float | Base grand total | | `shippingAmount` | float | Shipping amount | | `baseShippingAmount` | float | Base shipping amount | | `taxAmount` | float | Tax amount | | `baseTaxAmount` | float | Base tax amount | | `discountAmount` | float | Discount amount | | `baseDiscountAmount` | float | Base discount amount | | `shippingTaxAmount` | float | Shipping tax amount | | `baseShippingTaxAmount` | float | Base shipping tax amount | | `subTotalInclTax` | float | Sub total including tax | | `baseSubTotalInclTax` | float | Base sub total including tax | | `shippingAmountInclTax` | float | Shipping amount including tax | | `baseShippingAmountInclTax` | float | Base shipping amount including tax | | `baseCurrencyCode` | string | Base currency code (e.g. `USD`) | | `channelCurrencyCode` | string | Channel currency code | | `orderCurrencyCode` | string | Order currency code | | `transactionId` | string | Payment transaction ID | | `reminders` | integer | Number of reminders sent | | `nextReminderAt` | string | Next reminder scheduled date | | `order` | string | Associated order IRI (e.g. `/api/shop/customer-orders/1`) | | `items` | array | Invoice line item IRIs | | `createdAt` | string | ISO 8601 creation timestamp | | `updatedAt` | string | ISO 8601 last update timestamp | ## Invoice State Values | State | Description | |-------|-------------| | `pending` | Invoice created, awaiting action | | `pending_payment` | Payment initiated but not yet confirmed | | `paid` | Payment received and confirmed | | `overdue` | Payment past due date | | `refunded` | Invoice has been refunded | ## Empty Collection When the customer has no invoices, an empty array is returned: ```json [] ``` ## Error Responses **Unauthenticated (401):** ```json { "message": "Customer is not logged in." } ``` ## Use Cases - Display invoice history in customer account dashboard - Show invoice list with state, totals, and dates - Track payment status across orders - View financial records ## Notes - **Read-only API:** Only `GET` operations are available. Invoices cannot be created, updated, or deleted through this API. - **Customer isolation:** Invoices are scoped through the order relationship. A customer can never see another customer's invoices. - **Relationships:** In REST responses, relationships (`order`, `items`) are represented as IRIs (e.g. `/api/shop/customer-orders/1`). - **Response format:** The collection endpoint returns all matching invoices as a flat JSON array. ## Related Resources - [Get Single Customer Invoice](/api/rest-api/shop/customer-invoices/get-customer-invoice) - [Download Invoice PDF](/api/rest-api/shop/customer-invoices/download-customer-invoice-pdf) - [Get Customer Orders](/api/rest-api/shop/customer-orders/get-customer-orders) - [Get Customer Profile](/api/rest-api/shop/customers/get-customer-profile) --- # Get Customer Order URL: /api/rest-api/shop/customer-orders/get-customer-order --- outline: false examples: - id: get-customer-order title: Get Single Customer Order description: Retrieve details of a specific customer order by ID. request: | GET /api/shop/customer-orders/1 Content-Type: application/json X-STOREFRONT-KEY: pk_storefront_PvlE42nWGsKRVIf8bDlJngTPAdWAZbIy Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... response: | { "id": 1, "incrementId": "1", "status": "pending", "channelName": "Default", "isGuest": 0, "customerEmail": "customer@example.com", "customerFirstName": "John", "customerLastName": "Doe", "shippingMethod": "flatrate_flatrate", "shippingTitle": "Flat Rate - Flat Rate", "couponCode": null, "isGift": 0, "totalItemCount": 1, "totalQtyOrdered": 2, "baseCurrencyCode": "USD", "channelCurrencyCode": "USD", "orderCurrencyCode": "USD", "grandTotal": 150.00, "baseGrandTotal": 150.00, "grandTotalInvoiced": 150.00, "baseGrandTotalInvoiced": 150.00, "grandTotalRefunded": 0.00, "baseGrandTotalRefunded": 0.00, "subTotal": 140.00, "baseSubTotal": 140.00, "taxAmount": 0.00, "baseTaxAmount": 0.00, "discountAmount": 0.00, "baseDiscountAmount": 0.00, "shippingAmount": 10.00, "baseShippingAmount": 10.00, "createdAt": "2025-01-15T10:30:00.000000Z", "updatedAt": "2025-01-15T10:30:00.000000Z" } commonErrors: - error: 404 Not Found cause: Order with specified ID does not exist or does not belong to the customer solution: Verify the order ID and ensure it belongs to the authenticated customer - error: 401 Unauthorized cause: Missing or invalid Bearer token solution: Login and provide a valid customer authentication token - error: 403 Forbidden cause: Storefront key is missing or invalid solution: Provide a valid X-STOREFRONT-KEY header --- # Get Customer Order Retrieve detailed information for a specific customer order by its ID. Customers can only access their own orders — requesting another customer's order returns a 404, preventing enumeration attacks. ## Endpoint ``` GET /api/shop/customer-orders/{id} ``` ## Request Headers | Header | Required | Description | |--------|----------|-------------| | `Content-Type` | Yes | application/json | | `X-STOREFRONT-KEY` | Yes | Your storefront API key | | `Authorization` | Yes | Bearer token (customer login required) | ## Path Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `id` | integer | Yes | Customer order ID | ## Response Fields (200 OK) | Field | Type | Description | |-------|------|-------------| | `id` | integer | Order ID | | `incrementId` | string | Human-readable order number | | `status` | string | Order status | | `channelName` | string | Channel the order was placed on | | `isGuest` | integer | Whether the order was placed as guest | | `customerEmail` | string | Customer email | | `customerFirstName` | string | Customer first name | | `customerLastName` | string | Customer last name | | `shippingMethod` | string | Shipping method code | | `shippingTitle` | string | Shipping method display name | | `couponCode` | string | Applied coupon code | | `isGift` | integer | Whether the order is a gift | | `totalItemCount` | integer | Number of distinct items | | `totalQtyOrdered` | integer | Total quantity ordered | | `baseCurrencyCode` | string | Base currency code | | `channelCurrencyCode` | string | Channel currency code | | `orderCurrencyCode` | string | Order currency code | | `grandTotal` | float | Grand total | | `baseGrandTotal` | float | Base grand total | | `grandTotalInvoiced` | float | Grand total invoiced | | `baseGrandTotalInvoiced` | float | Base grand total invoiced | | `grandTotalRefunded` | float | Grand total refunded | | `baseGrandTotalRefunded` | float | Base grand total refunded | | `subTotal` | float | Sub total | | `baseSubTotal` | float | Base sub total | | `taxAmount` | float | Tax amount | | `baseTaxAmount` | float | Base tax amount | | `discountAmount` | float | Discount amount | | `baseDiscountAmount` | float | Base discount amount | | `shippingAmount` | float | Shipping amount | | `baseShippingAmount` | float | Base shipping amount | | `createdAt` | string | ISO 8601 creation timestamp | | `updatedAt` | string | ISO 8601 last update timestamp | ## Error Responses **Not Found (404):** ```json { "message": "Customer order with ID \"999\" not found." } ``` **Unauthenticated (401):** ```json { "message": "Customer is not logged in." } ``` **Accessing Another Customer's Order (404):** Requesting an order that belongs to a different customer returns the same 404 response, preventing enumeration attacks: ```json { "message": "Customer order with ID \"5\" not found." } ``` ## Use Cases - Display detailed order page in customer account - Show order summary with all financial details - Track shipping method and status - View applied coupons and discounts - Display invoiced and refunded amounts ## Notes - **Customer isolation:** A customer can never see another customer's orders. Requesting another customer's order returns a 404. - **Read-only:** Only `GET` operations are available. Orders cannot be modified through this API. - **Channel scoping:** Orders are filtered by the customer's channel for multi-tenant isolation. ## Related Resources - [Get All Customer Orders](/api/rest-api/shop/customer-orders/get-customer-orders) - [Place Order](/api/rest-api/shop/checkout/place-order) - [Get Customer Profile](/api/rest-api/shop/customers/get-customer-profile) --- # Get Customer Orders URL: /api/rest-api/shop/customer-orders/get-customer-orders --- outline: false examples: - id: get-customer-orders title: Get All Customer Orders description: Retrieve all orders for the authenticated customer. request: | GET /api/shop/customer-orders Content-Type: application/json X-STOREFRONT-KEY: pk_storefront_PvlE42nWGsKRVIf8bDlJngTPAdWAZbIy Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... response: | [ { "id": 1, "incrementId": "1", "status": "pending", "channelName": "Default", "isGuest": 0, "customerEmail": "customer@example.com", "customerFirstName": "John", "customerLastName": "Doe", "shippingMethod": "flatrate_flatrate", "shippingTitle": "Flat Rate - Flat Rate", "couponCode": null, "isGift": 0, "totalItemCount": 1, "totalQtyOrdered": 2, "baseCurrencyCode": "USD", "channelCurrencyCode": "USD", "orderCurrencyCode": "USD", "grandTotal": 150.00, "baseGrandTotal": 150.00, "grandTotalInvoiced": 150.00, "grandTotalRefunded": 0.00, "baseGrandTotalRefunded": 0.00, "subTotal": 140.00, "baseSubTotal": 140.00, "taxAmount": 0.00, "baseTaxAmount": 0.00, "discountAmount": 0.00, "baseDiscountAmount": 0.00, "shippingAmount": 10.00, "baseShippingAmount": 10.00, "createdAt": "2025-01-15T10:30:00.000000Z", "updatedAt": "2025-01-15T10:30:00.000000Z" }, { "id": 2, "incrementId": "2", "status": "completed", "channelName": "Default", "isGuest": 0, "customerEmail": "customer@example.com", "customerFirstName": "John", "customerLastName": "Doe", "grandTotal": 250.00, "baseGrandTotal": 250.00, "createdAt": "2025-01-16T14:00:00.000000Z", "updatedAt": "2025-01-16T14:00:00.000000Z" } ] commonErrors: - error: 401 Unauthorized cause: Missing or invalid Bearer token solution: Login and provide a valid customer authentication token - error: 403 Forbidden cause: Storefront key is missing or invalid solution: Provide a valid X-STOREFRONT-KEY header --- # Get Customer Orders Retrieve all orders belonging to the authenticated customer. This is a **read-only** API — customers can only view their own orders. Orders are automatically scoped to the authenticated customer and current channel. ## Endpoint ``` GET /api/shop/customer-orders ``` ## Request Headers | Header | Required | Description | |--------|----------|-------------| | `Content-Type` | Yes | application/json | | `X-STOREFRONT-KEY` | Yes | Your storefront API key | | `Authorization` | Yes | Bearer token (customer login required) | ## Response Fields (200 OK) The response is a plain JSON array of order objects. | Field | Type | Description | |-------|------|-------------| | `id` | integer | Order ID | | `incrementId` | string | Human-readable order number | | `status` | string | Order status | | `channelName` | string | Channel the order was placed on | | `isGuest` | integer | Whether the order was placed as guest | | `customerEmail` | string | Customer email | | `customerFirstName` | string | Customer first name | | `customerLastName` | string | Customer last name | | `shippingMethod` | string | Shipping method code | | `shippingTitle` | string | Shipping method display name | | `couponCode` | string | Applied coupon code | | `isGift` | integer | Whether the order is a gift | | `totalItemCount` | integer | Number of distinct items | | `totalQtyOrdered` | integer | Total quantity ordered | | `baseCurrencyCode` | string | Base currency code | | `channelCurrencyCode` | string | Channel currency code | | `orderCurrencyCode` | string | Order currency code | | `grandTotal` | float | Grand total | | `baseGrandTotal` | float | Base grand total | | `grandTotalInvoiced` | float | Grand total invoiced | | `grandTotalRefunded` | float | Grand total refunded | | `baseGrandTotalRefunded` | float | Base grand total refunded | | `subTotal` | float | Sub total | | `baseSubTotal` | float | Base sub total | | `taxAmount` | float | Tax amount | | `baseTaxAmount` | float | Base tax amount | | `discountAmount` | float | Discount amount | | `baseDiscountAmount` | float | Base discount amount | | `shippingAmount` | float | Shipping amount | | `baseShippingAmount` | float | Base shipping amount | | `createdAt` | string | ISO 8601 creation timestamp | | `updatedAt` | string | ISO 8601 last update timestamp | ## Order Status Values | Status | Description | |--------|-------------| | `pending` | Awaiting payment confirmation | | `processing` | Payment confirmed, order being processed | | `completed` | Order fulfilled and delivered | | `canceled` | Order canceled | | `closed` | Order closed | | `fraud` | Flagged as fraudulent | ## Empty Collection When the customer has no orders, an empty array is returned: ```json [] ``` ## Error Responses **Unauthenticated (401):** ```json { "message": "Customer is not logged in." } ``` ## Use Cases - Display order history in customer account dashboard - Show order list with status, totals, and dates - Track previous purchases - View recent orders ## Notes - **Read-only API:** Only `GET` operations are available. Orders cannot be created, updated, or deleted through this API. - **Customer isolation:** Orders are automatically filtered by the authenticated customer. A customer can never see another customer's orders. - **Channel scoping:** Orders are filtered by the customer's channel for multi-tenant isolation. - **Response format:** The collection endpoint returns all matching orders as a flat JSON array. ## Related Resources - [Get Single Customer Order](/api/rest-api/shop/customer-orders/get-customer-order) - [Place Order](/api/rest-api/shop/checkout/place-order) - [Get Customer Profile](/api/rest-api/shop/customers/get-customer-profile) --- # Get Customer Review URL: /api/rest-api/shop/customer-reviews/get-customer-review --- outline: false examples: - id: get-customer-review title: Get Single Customer Review description: Retrieve a specific product review by ID for the authenticated customer. request: | GET /api/shop/customer-reviews/1 Content-Type: application/json X-STOREFRONT-KEY: pk_storefront_PvlE42nWGsKRVIf8bDlJngTPAdWAZbIy Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... response: | { "data": { "id": 1, "title": "Great product", "comment": "Really enjoyed using this product.", "rating": 5, "status": "approved", "name": "John", "product": { "id": 2 }, "customer": { "id": 1 }, "createdAt": "2026-02-18T10:30:00+00:00", "updatedAt": "2026-02-18T10:30:00+00:00" } } commonErrors: - error: 404 Not Found cause: Review with specified ID does not exist or does not belong to the customer solution: Verify the review ID and ensure it belongs to the authenticated customer - error: 401 Unauthorized cause: Missing or invalid Bearer token solution: Login and provide a valid customer authentication token - error: 403 Forbidden cause: Storefront key is missing or invalid solution: Provide a valid X-STOREFRONT-KEY header --- # Get Customer Review Retrieve detailed information for a specific product review submitted by the authenticated customer. Customers can only access their own reviews. ## Endpoint ``` GET /api/shop/customer-reviews/{id} ``` ## Request Headers | Header | Required | Description | |--------|----------|-------------| | `Content-Type` | Yes | application/json | | `X-STOREFRONT-KEY` | Yes | Your storefront API key | | `Authorization` | Yes | Bearer token (customer login required) | ## Path Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `id` | integer | Yes | Customer review ID | ## Response Fields (200 OK) | Field | Type | Description | |-------|------|-------------| | `id` | integer | Review ID | | `title` | string | Review title | | `comment` | string | Review body text | | `rating` | integer | Star rating (1–5) | | `status` | string | Review status: `pending`, `approved`, or `rejected` | | `name` | string | Reviewer display name | | `product` | object | Associated product (nested resource) | | `customer` | object | Customer who wrote the review (nested resource) | | `createdAt` | string | ISO 8601 creation timestamp | | `updatedAt` | string | ISO 8601 last update timestamp | ## Error Responses | Status | Error | Description | |--------|-------|-------------| | `401` | Unauthenticated | Missing or invalid Bearer token | | `404` | Not Found | `Customer review with ID "999" not found` — review doesn't exist or doesn't belong to the customer | **Error — Not Found (404):** ```json { "message": "Customer review with ID \"999\" not found" } ``` **Error — Unauthenticated (401):** ```json { "message": "Unauthenticated. Please login to perform this action" } ``` ## Use Cases - Display individual review details - Show review with full context in account dashboard - Load specific review for viewing status - Check approval status of a submitted review ## Related Resources - [Get All Customer Reviews](/api/rest-api/shop/customer-reviews/get-customer-reviews) - [Get Product Reviews](/api/rest-api/shop/product-reviews/get-product-reviews) - [Create Product Review](/api/rest-api/shop/product-reviews/create-product-review) - [Get Customer Profile](/api/rest-api/shop/customers/get-customer-profile) --- # Get Customer Reviews URL: /api/rest-api/shop/customer-reviews/get-customer-reviews --- outline: false examples: - id: get-customer-reviews title: Get Customer Reviews description: Retrieve all product reviews submitted by the authenticated customer. request: | GET /api/shop/customer-reviews Content-Type: application/json X-STOREFRONT-KEY: pk_storefront_PvlE42nWGsKRVIf8bDlJngTPAdWAZbIy Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... response: | { "data": [ { "id": 1, "title": "Great product", "comment": "Really enjoyed using this product.", "rating": 5, "status": "approved", "name": "John", "product": { "id": 2 }, "customer": { "id": 1 }, "createdAt": "2026-02-18T10:30:00+00:00", "updatedAt": "2026-02-18T10:30:00+00:00" } ] } commonErrors: - error: 401 Unauthorized cause: Missing or invalid Bearer token solution: Login and provide a valid customer authentication token - error: 403 Forbidden cause: Storefront key is missing or invalid solution: Provide a valid X-STOREFRONT-KEY header - id: get-customer-reviews-filtered-status title: Get Customer Reviews - Filter by Status description: Retrieve customer reviews filtered by approval status. request: | GET /api/shop/customer-reviews?status=approved Content-Type: application/json X-STOREFRONT-KEY: pk_storefront_PvlE42nWGsKRVIf8bDlJngTPAdWAZbIy Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... response: | { "data": [ { "id": 1, "title": "Great product", "comment": "Really enjoyed using this product.", "rating": 5, "status": "approved", "name": "John", "product": { "id": 2 }, "customer": { "id": 1 }, "createdAt": "2026-02-18T10:30:00+00:00", "updatedAt": "2026-02-18T10:30:00+00:00" } ] } commonErrors: - error: 400 Bad Request cause: Invalid status value provided solution: Use one of pending, approved, or rejected - id: get-customer-reviews-filtered-rating title: Get Customer Reviews - Filter by Rating description: Retrieve customer reviews filtered by star rating. request: | GET /api/shop/customer-reviews?rating=5 Content-Type: application/json X-STOREFRONT-KEY: pk_storefront_PvlE42nWGsKRVIf8bDlJngTPAdWAZbIy Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... response: | { "data": [ { "id": 1, "title": "Great product", "comment": "Really enjoyed using this product.", "rating": 5, "status": "approved", "name": "John", "product": { "id": 2 }, "customer": { "id": 1 }, "createdAt": "2026-02-18T10:30:00+00:00", "updatedAt": "2026-02-18T10:30:00+00:00" } ] } commonErrors: - error: 400 Bad Request cause: Invalid rating value provided solution: Use a rating between 1 and 5 - id: get-customer-reviews-combined-filters title: Get Customer Reviews - Combined Filters description: Retrieve customer reviews filtered by both status and rating. request: | GET /api/shop/customer-reviews?status=approved&rating=5 Content-Type: application/json X-STOREFRONT-KEY: pk_storefront_PvlE42nWGsKRVIf8bDlJngTPAdWAZbIy Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... response: | { "data": [ { "id": 1, "title": "Great product", "comment": "Really enjoyed using this product.", "rating": 5, "status": "approved", "name": "John", "product": { "id": 2 }, "customer": { "id": 1 }, "createdAt": "2026-02-18T10:30:00+00:00", "updatedAt": "2026-02-18T10:30:00+00:00" } ] } commonErrors: - error: 401 Unauthorized cause: Missing or invalid Bearer token solution: Login and provide a valid customer authentication token --- # Get Customer Reviews Retrieve a paginated list of product reviews submitted by the authenticated customer. This is a **read-only, customer-scoped** resource — customers can only see their own reviews. ## Endpoint ``` GET /api/shop/customer-reviews ``` ## Request Headers | Header | Required | Description | |--------|----------|-------------| | `Content-Type` | Yes | application/json | | `X-STOREFRONT-KEY` | Yes | Your storefront API key | | `Authorization` | Yes | Bearer token (customer login required) | ## Query Parameters | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `status` | string | - | Filter by review status (`pending`, `approved`, `rejected`) | | `rating` | integer | - | Filter by star rating (`1`–`5`) | ## Response Fields (200 OK) | Field | Type | Description | |-------|------|-------------| | `id` | integer | Review ID | | `title` | string | Review title | | `comment` | string | Review body text | | `rating` | integer | Star rating (1–5) | | `status` | string | Review status: `pending`, `approved`, or `rejected` | | `name` | string | Reviewer display name | | `product` | object | Associated product (nested resource) | | `customer` | object | Customer who wrote the review (nested resource) | | `createdAt` | string | ISO 8601 creation timestamp | | `updatedAt` | string | ISO 8601 last update timestamp | ## Status Values | Status | Description | |--------|-------------| | `pending` | Awaiting approval | | `approved` | Published on storefront | | `rejected` | Not published | ## Filters ```bash # Get approved reviews only GET /api/shop/customer-reviews?status=approved # Get 5-star reviews only GET /api/shop/customer-reviews?rating=5 # Get approved 5-star reviews GET /api/shop/customer-reviews?status=approved&rating=5 # Get pending reviews GET /api/shop/customer-reviews?status=pending # Get rejected reviews GET /api/shop/customer-reviews?status=rejected ``` ## Use Cases - Display customer's review history in account dashboard - Show pending reviews awaiting approval - Filter reviews by rating or status - Build review management UI for customers ## Related Resources - [Get Single Customer Review](/api/rest-api/shop/customer-reviews/get-customer-review) - [Get Product Reviews](/api/rest-api/shop/product-reviews/get-product-reviews) - [Create Product Review](/api/rest-api/shop/product-reviews/create-product-review) - [Get Customer Profile](/api/rest-api/shop/customers/get-customer-profile) --- # Customer Management URL: /api/rest-api/shop/customers # Customer Management Complete guide to customer operations including registration, authentication, profile management, and address management using the REST API. ## Customer Authentication ### Register New Customer Create a new customer account. **Endpoint:** ``` POST /api/customers ``` :::tabs == cURL ```bash curl -X POST "https://your-domain.com/api/customers" \ -H "Content-Type: application/json" \ -d '{ "first_name": "John", "last_name": "Doe", "email": "john@example.com", "password": "SecurePassword123!", "password_confirmation": "SecurePassword123!", "phone": "1234567890", "is_subscribed_to_newsletter": true }' ``` == Node.js ```javascript const response = await fetch('https://your-domain.com/api/customers', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ first_name: 'John', last_name: 'Doe', email: 'john@example.com', password: 'SecurePassword123!', password_confirmation: 'SecurePassword123!', phone: '1234567890', is_subscribed_to_newsletter: true }) }); const customer = await response.json(); console.log(customer); ``` == Python ```python import requests response = requests.post( 'https://your-domain.com/api/customers', headers={'Content-Type': 'application/json'}, json={ 'first_name': 'John', 'last_name': 'Doe', 'email': 'john@example.com', 'password': 'SecurePassword123!', 'password_confirmation': 'SecurePassword123!', 'phone': '1234567890', 'is_subscribed_to_newsletter': True } ) customer = response.json() print(customer) ``` ::: **Response (201 Created):** ```json { "@context": "/api/contexts/Customer", "@id": "/api/customers/10", "@type": "Customer", "id": 10, "first_name": "John", "last_name": "Doe", "email": "john@example.com", "phone": "1234567890", "is_subscribed_to_newsletter": true, "status": 1, "created_at": "2024-01-20T10:30:00Z" } ``` ### Customer Login Authenticate a customer and receive access token. **Endpoint:** ``` POST /api/customers/login ``` :::tabs == cURL ```bash curl -X POST "https://your-domain.com/api/customers/login" \ -H "Content-Type: application/json" \ -d '{ "email": "john@example.com", "password": "SecurePassword123!" }' ``` == Node.js ```javascript const response = await fetch('https://your-domain.com/api/customers/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: 'john@example.com', password: 'SecurePassword123!' }) }); const data = await response.json(); localStorage.setItem('authToken', data.access_token); console.log('Logged in:', data.customer); ``` == Python ```python import requests response = requests.post( 'https://your-domain.com/api/customers/login', headers={'Content-Type': 'application/json'}, json={ 'email': 'john@example.com', 'password': 'SecurePassword123!' } ) data = response.json() print(f"Token: {data['access_token']}") print(f"Logged in as: {data['customer']['email']}") ``` ::: **Response (200 OK):** ```json { "@context": "/api/contexts/Customer", "@type": "Customer", "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", "token_type": "Bearer", "expires_in": 86400, "customer": { "id": 10, "first_name": "John", "last_name": "Doe", "email": "john@example.com", "phone": "1234567890" } } ``` ### Customer Logout Invalidate the current authentication token. **Endpoint:** ``` POST /api/customers/logout ``` :::tabs == cURL ```bash curl -X POST "https://your-domain.com/api/customers/logout" \ -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \ -H "Content-Type: application/json" ``` == Node.js ```javascript const token = localStorage.getItem('authToken'); const response = await fetch('https://your-domain.com/api/customers/logout', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } }); const result = await response.json(); localStorage.removeItem('authToken'); console.log(result.message); ``` == Python ```python import requests token = 'YOUR_ACCESS_TOKEN' response = requests.post( 'https://your-domain.com/api/customers/logout', headers={ 'Authorization': f'Bearer {token}', 'Content-Type': 'application/json' } ) result = response.json() print(result['message']) ``` ::: **Response (200 OK):** ```json { "@type": "Message", "message": "Successfully logged out" } ``` ## Forgot & Reset Password ### Request Password Reset Request a password reset token for a customer. **Endpoint:** ``` POST /api/forgot_passwords ``` :::tabs == cURL ```bash curl -X POST "https://your-domain.com/api/forgot_passwords" \ -H "Content-Type: application/json" \ -d '{ "email": "john@example.com" }' ``` == Node.js ```javascript const response = await fetch('https://your-domain.com/api/forgot_passwords', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: 'john@example.com' }) }); const result = await response.json(); console.log(result.message); ``` == Python ```python import requests response = requests.post( 'https://your-domain.com/api/forgot_passwords', headers={'Content-Type': 'application/json'}, json={'email': 'john@example.com'} ) result = response.json() print(result['message']) ``` ::: **Response (200 OK):** ```json { "@type": "Message", "message": "Password reset email sent successfully" } ``` ### Reset Password Reset customer password with valid token. **Endpoint:** ``` POST /api/reset_passwords ``` :::tabs == cURL ```bash curl -X POST "https://your-domain.com/api/reset_passwords" \ -H "Content-Type: application/json" \ -d '{ "email": "john@example.com", "token": "reset-token-from-email", "password": "NewPassword123!", "password_confirmation": "NewPassword123!" }' ``` == Node.js ```javascript const response = await fetch('https://your-domain.com/api/reset_passwords', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: 'john@example.com', token: 'reset-token-from-email', password: 'NewPassword123!', password_confirmation: 'NewPassword123!' }) }); const result = await response.json(); console.log(result.message); ``` == Python ```python import requests response = requests.post( 'https://your-domain.com/api/reset_passwords', headers={'Content-Type': 'application/json'}, json={ 'email': 'john@example.com', 'token': 'reset-token-from-email', 'password': 'NewPassword123!', 'password_confirmation': 'NewPassword123!' } ) result = response.json() print(result['message']) ``` ::: **Response (200 OK):** ```json { "@type": "Message", "message": "Password has been reset successfully" } ``` ## Customer Profile Management ### Get Customer Profile Retrieve authenticated customer's profile. **Endpoint:** ``` POST /api/customer_profiles ``` :::tabs == cURL ```bash curl -X POST "https://your-domain.com/api/customer_profiles" \ -H "Authorization: Bearer YOUR_CUSTOMER_TOKEN" \ -H "Content-Type: application/json" \ -d '{}' ``` == Node.js ```javascript const token = localStorage.getItem('authToken'); const response = await fetch('https://your-domain.com/api/customer_profiles', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({}) }); const profile = await response.json(); console.log(profile); ``` == Python ```python import requests token = 'YOUR_CUSTOMER_TOKEN' response = requests.post( 'https://your-domain.com/api/customer_profiles', headers={ 'Authorization': f'Bearer {token}', 'Content-Type': 'application/json' }, json={} ) profile = response.json() print(profile) ``` ::: **Response (200 OK):** ```json { "@context": "/api/contexts/CustomerProfile", "@type": "CustomerProfile", "id": 10, "first_name": "John", "last_name": "Doe", "email": "john@example.com", "phone": "1234567890", "date_of_birth": "1990-01-15", "gender": "M", "is_subscribed_to_newsletter": true, "created_at": "2024-01-10T10:00:00Z", "updated_at": "2024-01-20T10:30:00Z" } ``` ### Update Customer Profile Update authenticated customer's profile information. **Endpoint:** ``` POST /api/customer_profile_updates ``` :::tabs == cURL ```bash curl -X POST "https://your-domain.com/api/customer_profile_updates" \ -H "Authorization: Bearer YOUR_CUSTOMER_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "first_name": "Jonathan", "phone": "0987654321" }' ``` == Node.js ```javascript const token = localStorage.getItem('authToken'); const response = await fetch('https://your-domain.com/api/customer_profile_updates', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ first_name: 'Jonathan', phone: '0987654321' }) }); const result = await response.json(); console.log(result.message); ``` == Python ```python import requests token = 'YOUR_CUSTOMER_TOKEN' response = requests.post( 'https://your-domain.com/api/customer_profile_updates', headers={ 'Authorization': f'Bearer {token}', 'Content-Type': 'application/json' }, json={ 'first_name': 'Jonathan', 'phone': '0987654321' } ) result = response.json() print(result['message']) ``` ::: **Response (200 OK):** ```json { "@context": "/api/contexts/CustomerProfileUpdate", "@type": "CustomerProfileUpdate", "message": "Customer profile updated successfully", "customer": { "id": 10, "first_name": "Jonathan", "last_name": "Doe", "email": "john@example.com", "phone": "0987654321" } } ``` ### Delete Customer Profile Delete authenticated customer account. **Endpoint:** ``` POST /api/customer_profile_deletes ``` :::tabs == cURL ```bash curl -X POST "https://your-domain.com/api/customer_profile_deletes" \ -H "Authorization: Bearer YOUR_CUSTOMER_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "password": "SecurePassword123!" }' ``` == Node.js ```javascript const token = localStorage.getItem('authToken'); const response = await fetch('https://your-domain.com/api/customer_profile_deletes', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ password: 'SecurePassword123!' }) }); const result = await response.json(); console.log(result.message); ``` == Python ```python import requests token = 'YOUR_CUSTOMER_TOKEN' response = requests.post( 'https://your-domain.com/api/customer_profile_deletes', headers={ 'Authorization': f'Bearer {token}', 'Content-Type': 'application/json' }, json={'password': 'SecurePassword123!'} ) result = response.json() print(result['message']) ``` ::: **Response (200 OK):** ```json { "@context": "/api/contexts/CustomerProfileDelete", "@type": "CustomerProfileDelete", "message": "Customer account deleted successfully" } ``` ## Customer Addresses ### List Customer Addresses Retrieve all addresses for authenticated customer. **Endpoint:** ``` GET /api/customer-addresses-filter ``` :::tabs == cURL ```bash curl -X GET "https://your-domain.com/api/customer-addresses-filter" \ -H "Authorization: Bearer YOUR_CUSTOMER_TOKEN" \ -H "Content-Type: application/json" ``` == Node.js ```javascript const token = localStorage.getItem('authToken'); const response = await fetch('https://your-domain.com/api/customer-addresses-filter', { method: 'GET', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } }); const addresses = await response.json(); console.log(addresses); ``` == Python ```python import requests token = 'YOUR_CUSTOMER_TOKEN' response = requests.get( 'https://your-domain.com/api/customer-addresses-filter', headers={ 'Authorization': f'Bearer {token}', 'Content-Type': 'application/json' } ) addresses = response.json() print(addresses) ``` ::: **Response (200 OK):** ```json { "@context": "/api/contexts/GetCustomerAddresses", "@id": "/api/customer-addresses-filter", "@type": "hydra:Collection", "hydra:member": [ { "@id": "/api/customer_addresses/15", "@type": "CustomerAddress", "id": 15, "customer_id": 10, "first_name": "John", "last_name": "Doe", "address": "123 Main St", "city": "New York", "state": "NY", "country": "US", "postcode": "10001", "phone": "1234567890", "is_default": true } ], "hydra:totalItems": 2 } ``` ### Create Customer Address Add a new address to customer account. **Endpoint:** ``` POST /api/customer_addresses ``` :::tabs == cURL ```bash curl -X POST "https://your-domain.com/api/customer_addresses" \ -H "Authorization: Bearer YOUR_CUSTOMER_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "first_name": "John", "last_name": "Doe", "address": "123 Main St", "city": "New York", "state": "NY", "country": "US", "postcode": "10001", "phone": "1234567890", "is_default": false }' ``` == Node.js ```javascript const token = localStorage.getItem('authToken'); const response = await fetch('https://your-domain.com/api/customer_addresses', { method: 'POST', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ first_name: 'John', last_name: 'Doe', address: '123 Main St', city: 'New York', state: 'NY', country: 'US', postcode: '10001', phone: '1234567890', is_default: false }) }); const address = await response.json(); console.log(address); ``` == Python ```python import requests token = 'YOUR_CUSTOMER_TOKEN' response = requests.post( 'https://your-domain.com/api/customer_addresses', headers={ 'Authorization': f'Bearer {token}', 'Content-Type': 'application/json' }, json={ 'first_name': 'John', 'last_name': 'Doe', 'address': '123 Main St', 'city': 'New York', 'state': 'NY', 'country': 'US', 'postcode': '10001', 'phone': '1234567890', 'is_default': False } ) address = response.json() print(address) ``` ::: **Response (201 Created):** ```json { "@context": "/api/contexts/CustomerAddress", "@id": "/api/customer_addresses/16", "@type": "CustomerAddress", "id": 16, "customer_id": 10, "first_name": "John", "last_name": "Doe", "address": "123 Main St", "city": "New York", "state": "NY", "country": "US", "postcode": "10001", "phone": "1234567890", "is_default": false, "created_at": "2024-01-20T10:30:00Z" } ``` ### Update Customer Address Modify an existing customer address. **Endpoint:** ``` PATCH /api/customer_addresses/{id} ``` :::tabs == cURL ```bash curl -X PATCH "https://your-domain.com/api/customer_addresses/16" \ -H "Authorization: Bearer YOUR_CUSTOMER_TOKEN" \ -H "Content-Type: application/json" \ -d '{ "city": "Boston", "state": "MA" }' ``` == Node.js ```javascript const token = localStorage.getItem('authToken'); const response = await fetch('https://your-domain.com/api/customer_addresses/16', { method: 'PATCH', headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ city: 'Boston', state: 'MA' }) }); const address = await response.json(); console.log(address); ``` == Python ```python import requests token = 'YOUR_CUSTOMER_TOKEN' response = requests.patch( 'https://your-domain.com/api/customer_addresses/16', headers={ 'Authorization': f'Bearer {token}', 'Content-Type': 'application/json' }, json={ 'city': 'Boston', 'state': 'MA' } ) address = response.json() print(address) ``` ::: **Response (200 OK):** ```json { "@context": "/api/contexts/CustomerAddress", "@id": "/api/customer_addresses/16", "@type": "CustomerAddress", "id": 16, "customer_id": 10, "city": "Boston", "state": "MA", "is_default": false } ``` ### Delete Customer Address Remove an address from customer account. **Endpoint:** ``` DELETE /api/customer_addresses/{id} ``` :::tabs == cURL ```bash curl -X DELETE "https://your-domain.com/api/customer_addresses/16" \ -H "Authorization: Bearer YOUR_CUSTOMER_TOKEN" ``` == Node.js ```javascript const token = localStorage.getItem('authToken'); const response = await fetch('https://your-domain.com/api/customer_addresses/16', { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}` } }); console.log('Address deleted'); ``` == Python ```python import requests token = 'YOUR_CUSTOMER_TOKEN' response = requests.delete( 'https://your-domain.com/api/customer_addresses/16', headers={'Authorization': f'Bearer {token}'} ) print('Address deleted') ``` ::: **Response (204 No Content):** ``` [Empty response body] ``` ## Newsletter Subscription ### Subscribe to Newsletter Subscribe an email to newsletter. **Endpoint:** ``` POST /api/newsletter_subscriptions ``` :::tabs == cURL ```bash curl -X POST "https://your-domain.com/api/newsletter_subscriptions" \ -H "Content-Type: application/json" \ -d '{ "email": "subscriber@example.com" }' ``` == Node.js ```javascript const response = await fetch('https://your-domain.com/api/newsletter_subscriptions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: 'subscriber@example.com' }) }); const subscription = await response.json(); console.log(subscription); ``` == Python ```python import requests response = requests.post( 'https://your-domain.com/api/newsletter_subscriptions', headers={'Content-Type': 'application/json'}, json={'email': 'subscriber@example.com'} ) subscription = response.json() print(subscription) ``` ::: **Response (201 Created):** ```json { "@context": "/api/contexts/NewsletterSubscription", "@id": "/api/newsletter_subscriptions/1", "@type": "NewsletterSubscription", "id": 1, "email": "subscriber@example.com", "is_subscribed": true, "created_at": "2024-01-20T10:30:00Z" } ``` ## Related Resources - [Cart & Checkout](/api/rest-api/cart-checkout) - [Shop Resources](/api/rest-api/shop-resources) - [Best Practices](/api/rest-api/best-practices) --- # Change Password URL: /api/rest-api/shop/customers/change-password --- outline: false examples: - id: change-password title: Change Password description: A logged-in customer changes their password by supplying the current password plus a new password and its confirmation. request: | PUT /api/shop/customer-profile-updates/1192 Content-Type: application/json X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxx Authorization: Bearer 3628|zGSvw5t... { "currentPassword": "OldPass123!", "password": "NewPass456!", "confirmPassword": "NewPass456!" } response: | { "id": "1192", "_id": "1192", "firstName": "Pw", "lastName": "Tester", "email": "pw.change@example.com", "status": "1", "subscribedToNewsLetter": false, "isVerified": "false", "isSuspended": "false", "success": true, "message": "Customer profile updated successfully" } commonErrors: - error: 401 Unauthorized cause: Missing or invalid customer token solution: Log the customer in and send their Bearer token. - error: 400 Bad Request cause: currentPassword is missing or does not match the account's current password solution: Send the customer's correct current password. - error: 422 Unprocessable Entity cause: password and confirmPassword do not match solution: Make the new password and its confirmation identical. --- # Change Password A **logged-in** customer changes their own password by sending their **current** password together with a new password and its confirmation. This is part of the customer profile-update endpoint. ::: tip Forgot the password instead? This endpoint is for a customer who knows their current password. If the password was **forgotten**, use [Forgot Password](/api/rest-api/shop/customers/forgot-password) (`POST /api/shop/forgot-passwords`) — that emails a reset link the customer completes on the web. The storefront API has no token-based reset endpoint. ::: ## Endpoint ``` PUT /api/shop/customer-profile-updates/{id} ``` `{id}` is the authenticated customer's ID. ## Request Headers | Header | Required | Description | |--------|----------|-------------| | `Content-Type` | Yes | application/json | | `X-STOREFRONT-KEY` | Yes | Your storefront API key | | `Authorization` | Yes | `Bearer <token>` — the customer's token from [Customer Login](/api/rest-api/shop/customers/customer-login) | ## Request Body ```json { "currentPassword": "OldPass123!", "password": "NewPass456!", "confirmPassword": "NewPass456!" } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `currentPassword` | string | Yes | The customer's existing password (verified before the change) | | `password` | string | Yes | The new password | | `confirmPassword` | string | Yes | Must match `password` | This is the same endpoint used to update profile fields (name, email, …) — to change the password, send the three password fields above. Sending profile fields without the password fields updates the profile without touching the password. ## Response (200 OK) The endpoint returns the updated customer profile. | Field | Type | Description | |-------|------|-------------| | `id` / `_id` | string | Customer ID | | `firstName` / `lastName` / `email` | string | Profile fields | | `success` | boolean | Whether the update succeeded | | `message` | string | Human-readable result | After the change, the old password stops working and the customer logs in with the new one (existing tokens remain valid until they expire). ## Related Resources - [Customer Login](/api/rest-api/shop/customers/customer-login) - [Update Customer Profile](/api/rest-api/shop/customers/update-customer-profile) - [Forgot Password](/api/rest-api/shop/customers/forgot-password) --- # Create Customer Address URL: /api/rest-api/shop/customers/create-customer-address --- outline: false examples: - id: create-customer-address title: Create Customer Address description: Add a new address to the customer's address book. request: | POST /api/shop/customer-addresses Content-Type: application/json X-STOREFRONT-KEY: pk_storefront_PvlE42nWGsKRVIf8bDlJngTPAdWAZbIy Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... { "firstName": "Jane", "lastName": "Doe", "companyName": "ANC Corporation", "vatId": "GB123456789", "email": "jane@example.com", "phone": "9876543210", "address1": "456 Oak Ave", "address2": "Suite 200", "city": "Los Angeles", "state": "CA", "country": "US", "postcode": "90001", "defaultAddress": false } response: | { "data": { "address": { "id": 2, "firstName": "Jane", "lastName": "Doe", "companyName": "ANC Corporation", "vatId": "GB123456789", "email": "jane@example.com", "phone": "9876543210", "address1": "456 Oak Ave", "address2": "Suite 200", "city": "Los Angeles", "state": "CA", "country": "US", "postcode": "90001", "defaultAddress": false } }, "message": "Address created successfully" } commonErrors: - error: 401 Unauthorized cause: Customer not authenticated solution: Provide valid Bearer token - error: 422 Unprocessable Entity cause: Missing required fields solution: Provide all required fields --- # Create Customer Address Add a new address to the customer's address book. ## Endpoint ``` POST /api/shop/customer-addresses ``` ## Request Headers | Header | Required | Description | |--------|----------|-------------| | `Content-Type` | Yes | application/json | | `X-STOREFRONT-KEY` | Yes | Your storefront API key | | `Authorization` | Yes | Bearer token (customer login required) | ## Request Body ```json { "firstName": "Jane", "lastName": "Doe", "companyName": "ANC Corporation", "vatId": "GB123456789", "email": "jane@example.com", "phone": "9876543210", "address1": "456 Oak Ave", "address2": "Suite 200", "city": "Los Angeles", "state": "CA", "country": "US", "postcode": "90001", "defaultAddress": false } ``` ## Request Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `firstName` | string | Yes | First name | | `lastName` | string | Yes | Last name | | `companyName` | string | No | Company name | | `vatId` | string | No | VAT identification number | | `email` | string | Yes | Email address | | `phone` | string | Yes | Phone number | | `address1` | string | Yes | Street address line 1 | | `address2` | string | No | Street address line 2 | | `city` | string | Yes | City | | `state` | string | Yes | State/Province | | `country` | string | Yes | Country code | | `postcode` | string | Yes | Postal code | | `defaultAddress` | boolean | No | Set as default address | ## Response Fields (201 Created) | Field | Type | Description | |-------|------|-------------| | `address` | object | Created address details | | `message` | string | Success message | ## Validation Rules - All required fields must be provided - Country/State must be valid - Phone must be valid format - Email must be valid format - Maximum 10 addresses per customer ## Use Cases - Add billing address - Add shipping address - Save alternate location - Store office address - Store home address ## Related Resources - [Get Customer Addresses](/api/rest-api/shop/customers/get-customer-addresses) - [Update Customer Address](/api/rest-api/shop/customers/update-customer-address) - [Delete Customer Address](/api/rest-api/shop/customers/delete-customer-address) --- # Customer Login URL: /api/rest-api/shop/customers/customer-login --- outline: false examples: - id: customer-login title: Customer Login description: Authenticate a customer with email and password to get a Bearer token for subsequent requests. request: | POST /api/shop/customer/login Content-Type: application/json X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxx { "email": "john@example.com", "password": "Password123!" } response: | { "id": 1191, "_id": 1191, "apiToken": "aRfn7cVRSN7qUR6W7vGnlgb40XXa1mko4QNoLbiui1dAAKFcFh3yHY1PtG68OfJdksl0aHgbRKOvdxdl", "token": "3627|DfkAK11F8qdqtaFVJPvBxlJyNbCSMNl8TFWhWm4G5c9660e4", "success": true, "message": "You have logged in successfully" } commonErrors: - error: 401 Unauthorized cause: Invalid email or password solution: Verify credentials and try again - error: 400 Bad Request cause: Missing email or password solution: Provide both email and password - error: 403 Forbidden cause: Account is suspended solution: Contact support to reactivate account --- # Customer Login Authenticate a customer with email and password and receive a Bearer token to use on subsequent customer-scoped requests. ## Endpoint ``` POST /api/shop/customer/login ``` ## Request Headers | Header | Required | Description | |--------|----------|-------------| | `Content-Type` | Yes | application/json | | `X-STOREFRONT-KEY` | Yes | Your storefront API key | ## Request Body ```json { "email": "john@example.com", "password": "Password123!" } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `email` | string | Yes | Customer email address | | `password` | string | Yes | Customer password | ## Response Fields (201) The response is flat — the token is at the top level. | Field | Type | Description | |-------|------|-------------| | `id` / `_id` | integer | Customer ID | | `token` | string | Bearer token, format `<id>\|<secret>` — send as `Authorization: Bearer <token>` | | `apiToken` | string | Long-lived customer API token | | `success` | boolean | Whether login succeeded | | `message` | string | Human-readable result | ## Token Usage Send the returned `token` on customer-scoped requests: ```bash Authorization: Bearer 3627|DfkAK11F8qdqtaFVJPvBxlJyNbCSMNl8TFWhWm4G5c9660e4 ``` ## Session Management - Use [Verify Token](/api/rest-api/shop/customers/customer-verify-token) to check validity. - Use [Customer Logout](/api/rest-api/shop/customers/customer-logout) to end the session. ## Related Resources - [Customer Registration](/api/rest-api/shop/customers/customer-registration) - [Verify Customer Token](/api/rest-api/shop/customers/customer-verify-token) - [Customer Logout](/api/rest-api/shop/customers/customer-logout) - [Get Customer Profile](/api/rest-api/shop/customers/get-customer-profile) --- # Customer Logout URL: /api/rest-api/shop/customers/customer-logout --- outline: false examples: - id: customer-logout title: Customer Logout description: End the customer's authenticated session. request: | POST /api/shop/customer/logout Content-Type: application/json X-STOREFRONT-KEY: pk_storefront_PvlE42nWGsKRVIf8bDlJngTPAdWAZbIy Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... response: | { "message": "Successfully logged out" } commonErrors: - error: 401 Unauthorized cause: Customer not authenticated solution: Only authenticated customers can logout - error: 400 Bad Request cause: Invalid logout request solution: Ensure Bearer token is provided --- # Customer Logout End the customer's authenticated session and invalidate their token. ## Endpoint ``` POST /api/shop/customer/logout ``` ## Request Headers | Header | Required | Description | |--------|----------|-------------| | `Content-Type` | Yes | application/json | | `X-STOREFRONT-KEY` | Yes | Your storefront API key | | `Authorization` | Yes | Bearer token (customer login required) | ## Request Body ```json {} ``` No body parameters required. ## Response Fields (200 OK) | Field | Type | Description | |-------|------|-------------| | `message` | string | Success message | ## After Logout - Token is invalidated - Cannot use token for further requests - Customer session is ended - Must login again to access protected endpoints - Cart may be cleared (depends on configuration) ## Use Cases - End customer session - Log out from dashboard - Clear authentication token - Secure session termination - Multi-session logout ## Important Notes ⚠️ **Token is invalidated immediately after logout** - Previously working token will return 401 - Cannot be reversed - Customer must login again - Cart state depends on configuration ## Security - Ensures session termination - Invalidates all tokens for customer - May clear sensitive data - Secure way to end session - Prevents unauthorized access ## Related Resources - [Customer Login](/api/rest-api/shop/customers/customer-login) - [Customer Registration](/api/rest-api/shop/customers/customer-registration) - [Verify Customer Token](/api/rest-api/shop/customers/customer-verify-token) --- # Register Customer URL: /api/rest-api/shop/customers/customer-registration --- outline: false examples: - id: register-customer title: Customer Registration description: Create a new customer account with complete profile information. request: | POST /api/shop/customers Content-Type: application/json X-STOREFRONT-KEY: pk_storefront_PvlE42nWGsKRVIf8bDlJngTPAdWAZbIy { "firstName": "John", "lastName": "Doe", "gender": "Male", "dateOfBirth": "01/15/1990", "phone": "5550123", "status": "1", "isVerified": "1", "isSuspended": "0", "email": "john.doe@example.com", "password": "SecurePass@123", "confirmPassword": "SecurePass@123", "subscribedToNewsLetter": true } response: | { "id": 12, "firstName": "John", "lastName": "Doe", "gender": "Male", "dateOfBirth": "1990-01-15", "email": "john@example.com", "phone": "1234567890", "status": 1, "apiToken": "LXrSaQRvrKfSNz5CtHr2r2hBZQ7HtWGVKvNoORHOcsmo0aYpSi7MVPk5dOV3Kjcqjr57MSSQ2eM2lcrg", "customerGroupId": null, "channelId": null, "subscribedToNewsLetter": true, "isVerified": 0, "isSuspended": 0, "token": "269336856151628b51b1e3107906a2bf", "rememberToken": null, "name": "John Doe" } commonErrors: - error: 400 Bad Request cause: Email already registered solution: Use a different email address - error: 422 Validation Error cause: Password does not meet requirements solution: Use password with 8+ characters, mixed case, numbers, special chars - id: register-customer-newsletter title: Register with Newsletter Subscription description: Create new customer and subscribe to newsletter. request: | POST /api/shop/customers Content-Type: application/json X-STOREFRONT-KEY: pk_storefront_PvlE42nWGsKRVIf8bDlJngTPAdWAZbIy { "firstName": "Jane", "lastName": "Smith", "email": "jane@example.com", "password": "SecurePass456!", "confirmPassword": "SecurePass456!", "phone": "9876543210", "gender": "Female", "dateOfBirth": "1990-05-20", "subscribedToNewsLetter": true } response: | { "id": 13, "firstName": "Jane", "lastName": "Smith", "gender": "Female", "dateOfBirth": "1990-05-20", "email": "jane@example.com", "phone": "9876543210", "status": 1, "apiToken": "aBcDeFgHiJkLmNoPqRsTuVwXyZ1a2b3c4d5e6f7g8h9i0j1k2l3m4n5o6p7q8r9", "customerGroupId": null, "channelId": null, "subscribedToNewsLetter": true, "isVerified": 0, "isSuspended": 0, "token": "36d4d8a94f2c94e5c2a1b0d3f9e8a7c6", "rememberToken": null, "name": "Jane Smith" } commonErrors: - error: 409 Conflict cause: Customer email already exists solution: Use unique email or reset password instead --- # Register Customer Create a new customer account with email, password, and optional profile information. ## Endpoint ``` POST /api/shop/customers ``` ## Request Headers | Header | Required | Description | |--------|----------|-------------| | `Content-Type` | Yes | application/json | | `X-STOREFRONT-KEY` | Yes | Your storefront API key | ## Request Body ```json { "firstName": "John", "lastName": "Doe", "email": "john@example.com", "password": "Password123!", "confirmPassword": "Password123!", "phone": "1234567890", "gender": "Male", "dateOfBirth": "1990-01-15", "subscribedToNewsLetter": true } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `firstName` | string | Yes | Customer first name | | `lastName` | string | Yes | Customer last name | | `email` | string | Yes | Unique email address | | `password` | string | Yes | Password (min 8 chars, mixed case, numbers, special) | | `confirmPassword` | string | Yes | Password confirmation (must match) | | `phone` | string | No | Phone number | | `gender` | string | No | Customer gender (Male/Female) | | `dateOfBirth` | string | No | Date of birth (YYYY-MM-DD) | | `subscribedToNewsLetter` | boolean | No | Newsletter subscription (default: false) | ## Response Fields (201 Created) | Field | Type | Description | |-------|------|-------------| | `id` | integer | Customer ID | | `firstName` | string | Customer first name | | `lastName` | string | Customer last name | | `email` | string | Customer email | | `phone` | string | Customer phone | | `gender` | string | Customer gender | | `dateOfBirth` | string | Customer date of birth | | `apiToken` | string | API authentication token | | `token` | string | Session token | | `status` | integer | Account status (1=active, 0=inactive) | | `subscribedToNewsLetter` | boolean | Newsletter subscription status | | `isVerified` | integer | Email verification status (0=not verified) | | `isSuspended` | integer | Suspension status (0=active) | | `name` | string | Full customer name | | `customerGroupId` | mixed | Customer group ID | | `channelId` | mixed | Channel ID | | `rememberToken` | mixed | Remember token | ## Password Requirements Passwords must contain: - Minimum 8 characters - At least one uppercase letter (A-Z) - At least one lowercase letter (a-z) - At least one number (0-9) - At least one special character (!@#$%^&*) ## Related Resources - [Customer Login](/api/rest-api/customers/mutations/login-customer) - [Update Profile](/api/rest-api/customers/mutations/update-profile) - [Get Profile](/api/rest-api/customers/queries/get-profile) --- # Verify Customer Token URL: /api/rest-api/shop/customers/customer-verify-token --- outline: false examples: - id: customer-verify-token title: Verify Customer Token description: Verify if the customer authentication token is still valid. request: | GET /api/shop/verify-tokens X-STOREFRONT-KEY: pk_storefront_PvlE42nWGsKRVIf8bDlJngTPAdWAZbIy Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... response: | { "data": { "valid": true, "customer": { "id": 1, "firstName": "John", "lastName": "Doe", "email": "john@example.com" } }, "message": "Token is valid" } commonErrors: - error: 401 Unauthorized cause: Token is expired or invalid solution: Login again to get new token - error: 400 Bad Request cause: No token provided solution: Include Authorization header with Bearer token --- # Verify Customer Token Verify if the customer authentication token is still valid and retrieve customer information. ## Endpoint ``` GET /api/shop/verify-tokens ``` ## Request Headers | Header | Required | Description | |--------|----------|-------------| | `X-STOREFRONT-KEY` | Yes | Your storefront API key | | `Authorization` | Yes | Bearer token to verify | ## Response Fields (200 OK - Valid Token) | Field | Type | Description | |-------|------|-------------| | `valid` | boolean | Token validity status | | `customer` | object | Customer information | | `message` | string | Success message | ## Customer Fields (if valid) | Field | Type | Description | |-------|------|-------------| | `id` | integer | Customer ID | | `firstName` | string | First name | | `lastName` | string | Last name | | `email` | string | Email address | ## Token States - **Valid (200)** - Token is active and not expired - **Invalid (401)** - Token is expired or tampered with - **Missing (400)** - No token provided ## Use Cases - Check if user is still logged in - Validate session before API calls - Prevent stale token usage - Auto-logout on token expiry - Refresh session state ## Token Expiry - Tokens expire after a set period (typically 7 days) - Expired tokens return 401 Unauthorized - Use refresh token to get new token (if available) - Token becomes invalid after user logout ## Related Resources - [Customer Login](/api/rest-api/shop/customers/customer-login) - [Customer Logout](/api/rest-api/shop/customers/customer-logout) - [Customer Registration](/api/rest-api/shop/customers/customer-registration) --- # Delete Customer Address URL: /api/rest-api/shop/customers/delete-customer-address --- outline: false examples: - id: delete-customer-address title: Delete Customer Address description: Remove an address from the customer's address book. request: | DELETE /api/shop/customer-addresses/1 X-STOREFRONT-KEY: pk_storefront_PvlE42nWGsKRVIf8bDlJngTPAdWAZbIy Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... response: | { "message": "Address deleted successfully" } commonErrors: - error: 401 Unauthorized cause: Customer not authenticated solution: Provide valid Bearer token - error: 404 Not Found cause: Address does not exist solution: Verify the address ID - error: 403 Forbidden cause: Address belongs to different customer solution: Only delete your own addresses --- # Delete Customer Address Remove an address from the customer's address book. ## Endpoint ``` DELETE /api/shop/customer-addresses/{addressId} ``` ## URL Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `addressId` | integer | Yes | Address ID to delete | ## Request Headers | Header | Required | Description | |--------|----------|-------------| | `X-STOREFRONT-KEY` | Yes | Your storefront API key | | `Authorization` | Yes | Bearer token (customer login required) | ## Response Fields (200 OK) | Field | Type | Description | |-------|------|-------------| | `message` | string | Success message | ## Important Notes ⚠️ **Address deletion effects** - Address is permanently removed - Cannot be recovered - If default address, another is set as default - Does not affect past orders ## Preconditions - Address must belong to authenticated customer - Customer can delete own addresses only - At least one address may be required ## Use Cases - Remove old addresses - Clean up address book - Delete alternate locations - Remove incorrect address ## Related Resources - [Get Customer Addresses](/api/rest-api/shop/customers/get-customer-addresses) - [Create Customer Address](/api/rest-api/shop/customers/create-customer-address) - [Update Customer Address](/api/rest-api/shop/customers/update-customer-address) --- # Delete Customer Profile URL: /api/rest-api/shop/customers/delete-customer-profile --- outline: false examples: - id: delete-customer-profile title: Delete Customer Profile description: Delete the authenticated customer's account. request: | DELETE /api/shop/customer-profile-deletes/{id} X-STOREFRONT-KEY: pk_storefront_PvlE42nWGsKRVIf8bDlJngTPAdWAZbIy Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... response: | { "message": "Account deleted successfully" } commonErrors: - error: 401 Unauthorized cause: Customer not authenticated solution: Provide valid Bearer token - error: 409 Conflict cause: Active orders prevent deletion solution: Cancel/complete orders before deleting account --- # Delete Customer Profile Permanently delete the authenticated customer's account. ## Endpoint ``` DELETE /api/shop/customer-profile-deletes/{id} ``` ## Request Headers | Header | Required | Description | |--------|----------|-------------| | `X-STOREFRONT-KEY` | Yes | Your storefront API key | | `Authorization` | Yes | Bearer token (customer login required) | ## Response Fields (200 OK) | Field | Type | Description | |-------|------|-------------| | `message` | string | Success message | ## Important Notes ⚠️ **This action is irreversible** - Account is permanently deleted - All personal data is removed - Order history is preserved (anonymized) - Wishlist items are deleted - Reviews are retained without customer info - Cannot be undone ## Preconditions - All active orders must be completed/cancelled - No pending refunds - Account must be owned by authenticated customer ## Use Cases - Remove account permanently - Delete personal information - Opt out of email marketing - Account cleanup ## Data Retention After deletion: - Account is inaccessible - Personal information removed - Orders kept for legal/tax purposes (anonymized) - Email cannot be reused - Reviews remain visible (author anonymous) ## Related Resources - [Get Customer Profile](/api/rest-api/shop/customers/get-customer-profile) - [Update Customer Profile](/api/rest-api/shop/customers/update-customer-profile) - [Customer Logout](/api/rest-api/shop/customers/customer-logout) --- # Forgot Password URL: /api/rest-api/shop/customers/forgot-password --- outline: false examples: - id: forgot-password title: Forgot Password description: Request a password reset email. request: | POST /api/shop/forgot-passwords Content-Type: application/json X-STOREFRONT-KEY: pk_storefront_PvlE42nWGsKRVIf8bDlJngTPAdWAZbIy { "email": "john@example.com" } response: | { "message": "Reset password link has been sent to your email" } commonErrors: - error: 404 Not Found cause: Email address not found solution: Verify the email is associated with an account - error: 400 Bad Request cause: Invalid email format solution: Provide valid email address --- # Forgot Password Request a password reset email. A reset link will be sent to the customer's email address. ## Endpoint ``` POST /api/shop/forgot-passwords ``` ## Request Headers | Header | Required | Description | |--------|----------|-------------| | `Content-Type` | Yes | application/json | | `X-STOREFRONT-KEY` | Yes | Your storefront API key | **Note:** No authentication required for this endpoint ## Request Body ```json { "email": "john@example.com" } ``` ## Request Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `email` | string | Yes | Email address associated with account | ## Response Fields (200 OK) | Field | Type | Description | |-------|------|-------------| | `message` | string | Success message | ## Email Contents The reset email will contain: - Reset link with token - Link expiration information - Instructions for password reset - Security information ## Reset Link The email includes a link in format: ``` https://yourstore.com/reset-password?token=xxxxx ``` ## Token Validity - Reset token valid for 24 hours - Can be used only once - Token is invalidated after successful reset - Requesting new reset invalidates previous token ## Use Cases - Customer forgot their password - Locked out of account - Need to reset forgotten password - Regain access to account - Security password change ## Important Notes - No authentication required - Email must exist in system - Token is sent via email - User must click link in email - Token expires after 24 hours ## Security - Token-based reset (not SMS) - Email verification required - One-time use tokens - Prevents unauthorized access - Rate limiting (optional) ## Related Resources - [Change Password](/api/rest-api/shop/customers/change-password) - [Customer Login](/api/rest-api/shop/customers/customer-login) - [Customer Registration](/api/rest-api/shop/customers/customer-registration) --- # Get Customer Addresses URL: /api/rest-api/shop/customers/get-customer-addresses --- outline: false examples: - id: get-customer-addresses title: Get Customer Addresses description: Retrieve all saved addresses for the authenticated customer. request: | GET /api/shop/customer-addresses X-STOREFRONT-KEY: pk_storefront_PvlE42nWGsKRVIf8bDlJngTPAdWAZbIy Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... response: | { "data": { "addresses": [ { "id": 1, "firstName": "John", "lastName": "Doe", "address": "123 Main St", "city": "New York", "state": "NY", "country": "US", "postcode": "10001", "phone": "1234567890", "isDefault": true }, { "id": 2, "firstName": "John", "lastName": "Doe", "address": "456 Oak Ave", "city": "Los Angeles", "state": "CA", "country": "US", "postcode": "90001", "phone": "9876543210", "isDefault": false } ] } } commonErrors: - error: 401 Unauthorized cause: Customer not authenticated solution: Provide valid Bearer token --- # Get Customer Addresses Retrieve all saved addresses for the authenticated customer. ## Endpoint ``` GET /api/shop/customer-addresses ``` ## Request Headers | Header | Required | Description | |--------|----------|-------------| | `X-STOREFRONT-KEY` | Yes | Your storefront API key | | `Authorization` | Yes | Bearer token (customer login required) | ## Response Fields (200 OK) | Field | Type | Description | |-------|------|-------------| | `addresses` | array | List of customer addresses | ## Address Fields | Field | Type | Description | |-------|------|-------------| | `id` | integer | Address ID | | `firstName` | string | First name | | `lastName` | string | Last name | | `address` | string | Street address | | `city` | string | City | | `state` | string | State/Province | | `country` | string | Country code | | `postcode` | string | Postal code | | `phone` | string | Phone number | | `isDefault` | boolean | Is default address | | `addressType` | string | Type (residential/commercial) | ## Use Cases - Display saved addresses in checkout - Allow customer to select address - Populate address dropdown - Show customer's address book - Select shipping address - Select billing address ## Related Resources - [Create Customer Address](/api/rest-api/shop/customers/create-customer-address) - [Update Customer Address](/api/rest-api/shop/customers/update-customer-address) - [Delete Customer Address](/api/rest-api/shop/customers/delete-customer-address) --- # Get Customer Orders URL: /api/rest-api/shop/customers/get-customer-orders --- outline: false examples: - id: get-customer-orders title: Get Customer Orders description: Retrieve all orders for the authenticated customer. request: | GET /api/shop/customer-orders?page=1&limit=10&sort=-createdAt X-STOREFRONT-KEY: pk_storefront_PvlE42nWGsKRVIf8bDlJngTPAdWAZbIy Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... response: | { "data": { "orders": [ { "id": 12345, "incrementId": "#000012345", "status": "processing", "grandTotal": 1329.97, "itemsCount": 2, "createdAt": "2024-01-15T10:30:00Z" } ], "meta": { "total": 5, "count": 1, "perPage": 10, "currentPage": 1, "lastPage": 1 } } } commonErrors: - error: 401 Unauthorized cause: Customer not authenticated solution: Provide valid Bearer token --- # Get Customer Orders Retrieve all orders for the authenticated customer with pagination support. ## Endpoint ``` GET /api/shop/customer-orders ``` ## Request Headers | Header | Required | Description | |--------|----------|-------------| | `X-STOREFRONT-KEY` | Yes | Your storefront API key | | `Authorization` | Yes | Bearer token (customer login required) | ## Query Parameters | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `page` | integer | 1 | Page number | | `limit` | integer | 10 | Items per page (max 100) | | `sort` | string | -createdAt | Sort by field (prefix `-` for descending) | | `status` | string | - | Filter by status | ## Sort Options - `createdAt` / `-createdAt` - By creation date - `updatedAt` / `-updatedAt` - By update date - `grandTotal` / `-grandTotal` - By total amount ## Response Fields (200 OK) | Field | Type | Description | |-------|------|-------------| | `orders` | array | List of customer orders | | `meta` | object | Pagination metadata | ## Order Fields | Field | Type | Description | |-------|------|-------------| | `id` | integer | Order ID | | `incrementId` | string | Order increment ID | | `status` | string | Current status | | `grandTotal` | decimal | Total amount | | `itemsCount` | integer | Number of items | | `createdAt` | string | Creation timestamp | | `updatedAt` | string | Last update timestamp | | `shippingStatus` | string | Shipping status | ## Order Status Values - `pending` - Awaiting payment confirmation - `processing` - Payment confirmed - `shipped` - Order shipped - `delivered` - Order delivered - `canceled` - Order canceled - `failed` - Payment failed ## Meta Pagination Fields | Field | Type | Description | |-------|------|-------------| | `total` | integer | Total number of orders | | `count` | integer | Number of orders in this page | | `perPage` | integer | Items per page | | `currentPage` | integer | Current page number | | `lastPage` | integer | Last page number | ## Use Cases - Display order history - Show order list in account dashboard - Track previous orders - Check order status - View recent purchases ## Filters ```bash # Get pending orders GET /api/shop/customer-orders?status=pending # Get last 5 orders GET /api/shop/customer-orders?limit=5 # Get page 2 (10 items per page) GET /api/shop/customer-orders?page=2&limit=10 # Sort by latest GET /api/shop/customer-orders?sort=-createdAt ``` ## Related Resources - [Place Order](/api/rest-api/shop/checkout/place-order) - [Get Customer Profile](/api/rest-api/shop/customers/get-customer-profile) --- # Get Customer Profile URL: /api/rest-api/shop/customers/get-customer-profile --- outline: false examples: - id: get-customer-profile title: Get Customer Profile description: Retrieve the authenticated customer's profile information. request: | GET /api/shop/customer-profile X-STOREFRONT-KEY: pk_storefront_PvlE42nWGsKRVIf8bDlJngTPAdWAZbIy Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... response: | { "data": { "customer": { "id": 1, "firstName": "John", "lastName": "Doe", "email": "john@example.com", "phone": "1234567890", "gender": "M", "dateOfBirth": "1990-01-15", "status": "active", "createdAt": "2023-01-15T10:30:00Z" } } } commonErrors: - error: 401 Unauthorized cause: Customer not authenticated solution: Provide valid Bearer token - error: 404 Not Found cause: Customer profile not found solution: Verify authentication token validity --- # Get Customer Profile Retrieve the authenticated customer's profile information. ## Endpoint ``` GET /api/shop/customer-profile ``` ## Request Headers | Header | Required | Description | |--------|----------|-------------| | `X-STOREFRONT-KEY` | Yes | Your storefront API key | | `Authorization` | Yes | Bearer token (customer login required) | ## Response Fields (200 OK) | Field | Type | Description | |-------|------|-------------| | `customer` | object | Customer profile data | ## Customer Fields | Field | Type | Description | |-------|------|-------------| | `id` | integer | Customer ID | | `firstName` | string | First name | | `lastName` | string | Last name | | `email` | string | Email address | | `phone` | string | Phone number | | `gender` | string | Gender (M/F/Other) | | `dateOfBirth` | string | Birth date (YYYY-MM-DD) | | `status` | string | Account status | | `createdAt` | string | Account creation date | | `updatedAt` | string | Last update date | ## Use Cases - Display customer information - Show account details in dashboard - Verify customer information - Pre-fill profile forms - Display greeting with customer name ## Related Resources - [Update Customer Profile](/api/rest-api/shop/customers/update-customer-profile) - [Get Customer Addresses](/api/rest-api/shop/customers/get-customer-addresses) - [Get Customer Orders](/api/rest-api/shop/customers/get-customer-orders) --- # Register Customer URL: /api/rest-api/shop/customers/mutations/register-customer --- outline: false examples: - id: register-customer title: Customer Registration description: Create a new customer account. request: | POST /api/customers Content-Type: application/json { "first_name": "John", "last_name": "Doe", "email": "john@example.com", "password": "SecurePassword123!", "password_confirmation": "SecurePassword123!", "phone": "1234567890" } response: | { "@context": "/api/contexts/Customer", "@id": "/api/customers/10", "@type": "Customer", "id": 10, "first_name": "John", "last_name": "Doe", "email": "john@example.com", "phone": "1234567890", "status": 1, "created_at": "2024-01-20T10:30:00Z" } commonErrors: - error: 400 Bad Request cause: Email already registered solution: Use a different email address - error: 422 Validation Error cause: Password does not meet requirements solution: Use password with 8+ characters, mixed case, numbers, special chars - id: register-customer-newsletter title: Register with Newsletter Subscription description: Create new customer and subscribe to newsletter. request: | POST /api/customers Content-Type: application/json { "first_name": "Jane", "last_name": "Smith", "email": "jane@example.com", "password": "SecurePass456!", "password_confirmation": "SecurePass456!", "phone": "9876543210", "is_subscribed_to_newsletter": true } response: | { "@context": "/api/contexts/Customer", "@id": "/api/customers/11", "@type": "Customer", "id": 11, "first_name": "Jane", "last_name": "Smith", "email": "jane@example.com", "phone": "9876543210", "is_subscribed_to_newsletter": true, "status": 1, "created_at": "2024-01-20T10:35:00Z" } commonErrors: - error: 409 Conflict cause: Customer email already exists solution: Use unique email or reset password instead --- # Register Customer Create a new customer account with email, password, and optional profile information. ## Endpoint ``` POST /api/customers ``` ## Request Headers | Header | Required | Description | |--------|----------|-------------| | `Content-Type` | Yes | application/json | ## Request Body ```json { "first_name": "John", "last_name": "Doe", "email": "john@example.com", "password": "SecurePassword123!", "password_confirmation": "SecurePassword123!", "phone": "1234567890", "is_subscribed_to_newsletter": true } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `first_name` | string | Yes | Customer first name | | `last_name` | string | Yes | Customer last name | | `email` | string | Yes | Unique email address | | `password` | string | Yes | Password (min 8 chars, mixed case, numbers, special) | | `password_confirmation` | string | Yes | Password confirmation (must match) | | `phone` | string | No | Phone number | | `is_subscribed_to_newsletter` | boolean | No | Newsletter subscription (default: false) | ## Response Fields | Field | Type | Description | |-------|------|-------------| | `id` | integer | Customer ID | | `first_name` | string | Customer first name | | `last_name` | string | Customer last name | | `email` | string | Customer email | | `phone` | string | Customer phone | | `status` | integer | Account status (1=active, 0=inactive) | | `is_subscribed_to_newsletter` | boolean | Newsletter subscription status | | `created_at` | string | Creation timestamp | ## Usage Examples :::examples-selector ## Password Requirements Passwords must contain: - Minimum 8 characters - At least one uppercase letter (A-Z) - At least one lowercase letter (a-z) - At least one number (0-9) - At least one special character (!@#$%^&*) ## Related Resources - [Customer Login](/api/rest-api/customers/mutations/login-customer) - [Update Profile](/api/rest-api/customers/mutations/update-profile) - [Get Profile](/api/rest-api/customers/queries/get-profile) --- # Update Customer Address URL: /api/rest-api/shop/customers/update-customer-address --- outline: false examples: - id: update-customer-address title: Update Customer Address description: Update an existing customer address. request: | PUT /api/shop/customer-addresses/1 Content-Type: application/json X-STOREFRONT-KEY: pk_storefront_PvlE42nWGsKRVIf8bDlJngTPAdWAZbIy Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... { "firstName": "Jane", "lastName": "Doe", "companyName": "Updated Corp.", "vatId": "DE987654321", "email": "jane@example.com", "phone": "9876543210", "address1": "789 Pine Rd", "address2": "Suite 300", "city": "Los Angeles", "state": "CA", "country": "US", "postcode": "90002" } response: | { "data": { "address": { "id": 1, "firstName": "Jane", "lastName": "Doe", "companyName": "Updated Corp.", "vatId": "DE987654321", "email": "jane@example.com", "phone": "9876543210", "address1": "789 Pine Rd", "address2": "Suite 300", "city": "Los Angeles", "state": "CA", "country": "US", "postcode": "90002" } }, "message": "Address updated successfully" } commonErrors: - error: 401 Unauthorized cause: Customer not authenticated solution: Provide valid Bearer token - error: 404 Not Found cause: Address does not exist solution: Verify the address ID - error: 403 Forbidden cause: Address belongs to different customer solution: Only update your own addresses --- # Update Customer Address Update an existing address in the customer's address book. ## Endpoint ``` PUT /api/shop/customer-addresses/{addressId} ``` ## URL Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `addressId` | integer | Yes | Address ID to update | ## Request Headers | Header | Required | Description | |--------|----------|-------------| | `Content-Type` | Yes | application/json | | `X-STOREFRONT-KEY` | Yes | Your storefront API key | | `Authorization` | Yes | Bearer token (customer login required) | ## Request Body ```json { "firstName": "Jane", "lastName": "Doe", "companyName": "Updated Corp.", "vatId": "DE987654321", "email": "jane@example.com", "phone": "9876543210", "address1": "789 Pine Rd", "address2": "Suite 300", "city": "Los Angeles", "state": "CA", "country": "US", "postcode": "90002" } ``` ## Request Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `firstName` | string | No | First name | | `lastName` | string | No | Last name | | `companyName` | string | No | Company name | | `vatId` | string | No | VAT identification number | | `email` | string | No | Email address | | `phone` | string | No | Phone number | | `address1` | string | No | Street address line 1 | | `address2` | string | No | Street address line 2 | | `city` | string | No | City | | `state` | string | No | State/Province | | `country` | string | No | Country code | | `postcode` | string | No | Postal code | | `defaultAddress` | boolean | No | Set as default address | ## Response Fields (200 OK) | Field | Type | Description | |-------|------|-------------| | `address` | object | Updated address details | | `message` | string | Success message | ## Validation Rules - Address must belong to customer - Country/State must be valid if provided - Cannot update other customer's addresses ## Use Cases - Correct address information - Update phone number - Change city/state - Modify street address - Update postal code - Update company/VAT details ## Related Resources - [Get Customer Addresses](/api/rest-api/shop/customers/get-customer-addresses) - [Create Customer Address](/api/rest-api/shop/customers/create-customer-address) - [Delete Customer Address](/api/rest-api/shop/customers/delete-customer-address) --- # Update Customer Profile URL: /api/rest-api/shop/customers/update-customer-profile --- outline: false examples: - id: update-customer-profile title: Update Customer Profile description: Update the authenticated customer's profile information. request: | PUT /api/shop/customer-profile-updates/{id} Content-Type: application/json X-STOREFRONT-KEY: pk_storefront_PvlE42nWGsKRVIf8bDlJngTPAdWAZbIy Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... { "firstName": "John", "lastName": "Doe", "email": "john.doe@example.com", "phone": "1234567890", "gender": "M", "dateOfBirth": "1990-01-15" } response: | { "data": { "customer": { "id": 1, "firstName": "John", "lastName": "Doe", "email": "john.doe@example.com", "phone": "1234567890", "gender": "M", "dateOfBirth": "1990-01-15", "updatedAt": "2024-01-15T10:30:00Z" } }, "message": "Profile updated successfully" } commonErrors: - error: 401 Unauthorized cause: Customer not authenticated solution: Provide valid Bearer token - error: 422 Unprocessable Entity cause: Email already exists solution: Use unique email address - error: 400 Bad Request cause: Invalid data format solution: Verify all fields match required format --- # Update Customer Profile Update the authenticated customer's profile information. ## Endpoint ``` PUT /api/shop/customer-profile-updates/{id} ``` ## Request Headers | Header | Required | Description | |--------|----------|-------------| | `Content-Type` | Yes | application/json | | `X-STOREFRONT-KEY` | Yes | Your storefront API key | | `Authorization` | Yes | Bearer token (customer login required) | ## Request Body ```json { "firstName": "John", "lastName": "Doe", "email": "john.doe@example.com", "phone": "1234567890", "gender": "M", "dateOfBirth": "1990-01-15" } ``` ## Request Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `firstName` | string | Yes | First name (max 255 chars) | | `lastName` | string | Yes | Last name (max 255 chars) | | `email` | string | Yes | Valid email address | | `phone` | string | No | Phone number | | `gender` | string | No | Gender (M/F/Other) | | `dateOfBirth` | string | No | Birth date (YYYY-MM-DD) | ## Response Fields (200 OK) | Field | Type | Description | |-------|------|-------------| | `customer` | object | Updated customer profile | | `message` | string | Success message | ## Validation Rules - First name: required, max 255 characters - Last name: required, max 255 characters - Email: must be unique, valid format - Phone: optional, valid format - Date of birth: optional, valid date format ## Use Cases - Update account information - Change email address - Modify personal details - Update phone number - Add/change birth date ## Important Notes - Changing email requires verification - Some fields may be locked by admin - Updates reflected immediately - Original email may be required for verification ## Related Resources - [Get Customer Profile](/api/rest-api/shop/customers/get-customer-profile) - [Delete Customer Profile](/api/rest-api/shop/customers/delete-customer-profile) --- # Get All Locales URL: /api/rest-api/shop/locales/get-locales --- outline: false examples: - id: get-locales title: Get All Locales description: Retrieve all available language locales in the store. request: | GET /api/shop/locales?page=1 Accept: application/json X-STOREFRONT-KEY: pk_storefront_PvlE42nWGsKRVIf8bDlJngTPAdWAZbIy response: | [ { "id": 1, "code": "en", "name": "English", "direction": "ltr", "logoPath": "locales/JuC6oD40TWtf6R2S08kQ95cecRqUKd3UctVivnSt.png", "logoUrl": "https://api-demo.bagisto.com/storage/locales/JuC6oD40TWtf6R2S08kQ95cecRqUKd3UctVivnSt.png" } ] commonErrors: - error: 401 Unauthorized cause: Invalid or missing X-STOREFRONT-KEY solution: Provide valid storefront API key in X-STOREFRONT-KEY header - error: 400 Bad Request cause: Invalid page parameter solution: Use valid page numbers starting from 1 --- # Get All Locales Retrieve all available language locales supported by your store. ## Endpoint ``` GET /api/shop/locales ``` ## Query Parameters | Parameter | Type | Required | Description | Example | |-----------|------|----------|-------------|---------| | `page` | integer | No | Pagination page number | `1` | | `per_page` | integer | No | Items per page (max: 100) | `10` | ## Request Headers | Header | Type | Required | Description | Example | |--------|------|----------|-------------|---------| | `X-STOREFRONT-KEY` | string | **Yes** | Storefront API key for authentication | `pk_storefront_PvlE42nWGsKRVIf8bDlJngTPAdWAZbIy` | | `Accept` | string | No | Response format | `application/json` | ## Response Fields (200 OK) | Field | Type | Description | |-------|------|-------------| | `id` | integer | Locale ID | | `code` | string | Language code (ISO 639-1, e.g., en, fr, ar) | | `name` | string | English locale name | | `direction` | string | Text direction (ltr=left-to-right, rtl=right-to-left) | | `logoPath` | string | Logo path in storage | | `logoUrl` | string | Full URL to locale logo | ## cURL Example ```bash curl -X GET 'https://api-demo.bagisto.com/api/shop/locales?page=1' \ -H 'accept: application/json' \ -H 'X-STOREFRONT-KEY: pk_storefront_PvlE42nWGsKRVIf8bDlJngTPAdWAZbIy' ``` ## JavaScript/Fetch Example ```javascript fetch('https://your-domain.com/api/shop/locales?page=1', { method: 'GET', headers: { 'Accept': 'application/json', 'X-STOREFRONT-KEY': 'pk_storefront_PvlE42nWGsKRVIf8bDlJngTPAdWAZbIy' } }) .then(response => response.json()) .then(data => console.log(data)) .catch(error => console.error('Error:', error)); ``` ## Response Headers | Header | Description | |--------|-------------| | `Content-Type` | `application/json; charset=utf-8` | | `X-RateLimit-Limit` | Maximum requests allowed per minute (e.g., `100`) | | `X-RateLimit-Remaining` | Remaining requests in current window (e.g., `95`) | | `X-RateLimit-Reset` | Unix timestamp when limit resets | | `X-Built-With` | `Bagisto` | ## Error Responses ### 401 Unauthorized ```json { "message": "Unauthorized", "errors": { "X-STOREFRONT-KEY": [ "The X-STOREFRONT-KEY header is missing or invalid" ] } } ``` ### 400 Bad Request ```json { "message": "Invalid parameters", "errors": { "page": [ "The page parameter must be a valid integer" ] } } ``` ## Language Codes Common ISO 639-1 codes: - `en` - English - `fr` - French - `de` - German - `es` - Spanish - `it` - Italian - `pt` - Portuguese - `ar` - Arabic - `zh` - Chinese - `ja` - Japanese ## Text Direction - `ltr` - Left-to-right (Latin, Cyrillic scripts) - `rtl` - Right-to-left (Arabic, Hebrew) ## Use Cases - Build language/locale selector dropdowns - Support multi-language storefront - Display locale-specific content - Set user language preferences - Load locale-specific product information - Render RTL layouts for Arabic/Hebrew ## Related Resources - [Get Single Locale](/api/rest-api/shop/locales/get-single-locale) --- # Get Single Locale URL: /api/rest-api/shop/locales/get-single-locale --- outline: false examples: - id: get-single-locale title: Get Single Locale description: Retrieve detailed information for a specific language locale. request: | GET /api/shop/locales/1 Accept: application/json X-STOREFRONT-KEY: pk_storefront_PvlE42nWGsKRVIf8bDlJngTPAdWAZbIy response: | { "id": 1, "code": "en", "name": "English", "direction": "ltr", "logoPath": "locales/JuC6oD40TWtf6R2S08kQ95cecRqUKd3UctVivnSt.png", "logoUrl": "https://api-demo.bagisto.com/storage/locales/JuC6oD40TWtf6R2S08kQ95cecRqUKd3UctVivnSt.png" } commonErrors: - error: 404 Not Found cause: Locale with specified ID does not exist solution: Verify the locale ID and try again - error: 401 Unauthorized cause: Invalid or missing X-STOREFRONT-KEY solution: Provide valid storefront API key in X-STOREFRONT-KEY header --- # Get Single Locale Retrieve detailed information for a specific language locale. ## Endpoint ``` GET /api/shop/locales/{id} ``` ## Path Parameters | Parameter | Type | Required | Description | Example | |-----------|------|----------|-------------|---------| | `id` | string/integer | **Yes** | Locale identifier | `1` | ## Request Headers | Header | Type | Required | Description | Example | |--------|------|----------|-------------|---------| | `X-STOREFRONT-KEY` | string | **Yes** | Storefront API key for authentication | `pk_storefront_PvlE42nWGsKRVIf8bDlJngTPAdWAZbIy` | | `Accept` | string | No | Response format | `application/json` | ## Response Fields (200 OK) | Field | Type | Description | |-------|------|-------------| | `id` | integer | Locale ID | | `code` | string | Language code (ISO 639-1, e.g., en, fr, ar) | | `name` | string | English locale name | | `direction` | string | Text direction (ltr=left-to-right, rtl=right-to-left) | | `logoPath` | string | Logo path in storage | | `logoUrl` | string | Full URL to locale logo | ## cURL Example ```bash curl -X GET 'https://api-demo.bagisto.com/api/shop/locales/1' \ -H 'accept: application/json' \ -H 'X-STOREFRONT-KEY: pk_storefront_PvlE42nWGsKRVIf8bDlJngTPAdWAZbIy' ``` ## JavaScript/Fetch Example ```javascript fetch('https://your-domain.com/api/shop/locales/1', { method: 'GET', headers: { 'Accept': 'application/json', 'X-STOREFRONT-KEY': 'pk_storefront_PvlE42nWGsKRVIf8bDlJngTPAdWAZbIy' } }) .then(response => response.json()) .then(data => console.log(data)) .catch(error => console.error('Error:', error)); ``` ## Response Headers | Header | Description | |--------|-------------| | `Content-Type` | `application/json; charset=utf-8` | | `X-RateLimit-Limit` | Maximum requests allowed per minute (e.g., `100`) | | `X-RateLimit-Remaining` | Remaining requests in current window (e.g., `94`) | | `X-RateLimit-Reset` | Unix timestamp when limit resets | | `X-Built-With` | `Bagisto` | | `Cache-Control` | `no-cache,private` | ## Error Responses ### 404 Not Found ```json { "message": "Locale not found", "errors": { "id": [ "The specified locale does not exist" ] } } ``` ### 401 Unauthorized ```json { "message": "Unauthorized", "errors": { "X-STOREFRONT-KEY": [ "The X-STOREFRONT-KEY header is missing or invalid" ] } } ``` ## Text Direction - `ltr` - Left-to-right (Latin, Cyrillic scripts) - `rtl` - Right-to-left (Arabic, Hebrew) ## Use Cases - Get locale details for content rendering - Validate locale codes - Display locale information - Set up locale-specific formatters - Configure RTL layout - Fetch native language name ## Common Locale IDs - `1` - English (en) - `2` - French (fr) - `3` - Arabic (ar) - Check [Get All Locales](/api/rest-api/shop/locales/get-locales) for complete list ## Related Resources - [Get All Locales](/api/rest-api/shop/locales/get-locales) --- # Get Locales URL: /api/rest-api/shop/locales/list --- outline: false examples: - id: get-locales-basic title: Get Locales - Basic description: Retrieve all store locales with basic information. query: | query getLocales { locales { edges { node { id _id code name direction } } pageInfo { hasNextPage endCursor } } } variables: | {} response: | { "data": { "locales": { "edges": [ { "node": { "id": "/api/shop/locales/1", "_id": 1, "code": "en", "name": "English", "direction": "ltr" } }, { "node": { "id": "/api/shop/locales/2", "_id": 2, "code": "ar", "name": "Arabic", "direction": "rtl" } }, { "node": { "id": "/api/shop/locales/3", "_id": 3, "code": "fr", "name": "French", "direction": "ltr" } } ], "pageInfo": { "hasNextPage": false, "endCursor": "Mw==" } } } } commonErrors: - error: UNAUTHORIZED cause: Invalid or missing authentication token solution: Provide valid authentication credentials - error: NO_LOCALES cause: No locales configured in the system solution: Create locales in the admin panel - id: get-locales-complete title: Get Locales - Complete Details description: Retrieve all locales with complete information including logo paths and timestamps. query: | query getLocales { locales { edges { cursor node { id _id code name direction logoPath createdAt updatedAt logoUrl } } pageInfo { endCursor startCursor hasNextPage hasPreviousPage } totalCount } } variables: | {} response: | { "data": { "locales": { "edges": [ { "cursor": "MQ==", "node": { "id": "/api/shop/locales/1", "_id": 1, "code": "en", "name": "English", "direction": "ltr", "logoPath": "/locales/en-logo.png", "createdAt": "2024-01-15T10:30:00Z", "updatedAt": "2024-01-20T14:22:00Z", "logoUrl": "https://example.com/storage/locales/en-logo.png" } }, { "cursor": "Mg==", "node": { "id": "/api/shop/locales/2", "_id": 2, "code": "ar", "name": "Arabic", "direction": "rtl", "logoPath": "/locales/ar-logo.png", "createdAt": "2024-01-15T10:35:00Z", "updatedAt": "2024-01-20T14:25:00Z", "logoUrl": "https://example.com/storage/locales/ar-logo.png" } }, { "cursor": "Mw==", "node": { "id": "/api/shop/locales/3", "_id": 3, "code": "fr", "name": "French", "direction": "ltr", "logoPath": "/locales/fr-logo.png", "createdAt": "2024-01-15T10:40:00Z", "updatedAt": "2024-01-20T14:30:00Z", "logoUrl": "https://example.com/storage/locales/fr-logo.png" } } ], "pageInfo": { "endCursor": "Mw==", "startCursor": "MQ==", "hasNextPage": false, "hasPreviousPage": false }, "totalCount": 3 } } } commonErrors: - error: INVALID_PAGINATION cause: Invalid pagination parameters provided solution: Ensure first/last are positive integers and cursors are valid - error: INVALID_CURSOR cause: Pagination cursor is invalid or expired solution: Use cursor values from the previous response - id: get-locales-with-pagination title: Get Locales with Pagination description: Retrieve locales with cursor-based pagination for handling large datasets. query: | query getLocalesWithPagination($first: Int, $after: String) { locales(first: $first, after: $after) { edges { cursor node { id _id code name direction logoUrl } } pageInfo { endCursor startCursor hasNextPage hasPreviousPage } totalCount } } variables: | { "first": 10, "after": null } response: | { "data": { "locales": { "edges": [ { "cursor": "MQ==", "node": { "id": "/api/shop/locales/1", "_id": 1, "code": "en", "name": "English", "direction": "ltr", "logoUrl": "https://example.com/storage/locales/en-logo.png" } }, { "cursor": "Mg==", "node": { "id": "/api/shop/locales/2", "_id": 2, "code": "ar", "name": "Arabic", "direction": "rtl", "logoUrl": "https://example.com/storage/locales/ar-logo.png" } } ], "pageInfo": { "endCursor": "Mg==", "startCursor": "MQ==", "hasNextPage": false, "hasPreviousPage": false }, "totalCount": 2 } } } commonErrors: - error: INVALID_FIRST_VALUE cause: The first argument exceeds maximum allowed value solution: Use first value between 1 and 100 - error: INVALID_CURSOR cause: The provided cursor is invalid solution: Use a valid cursor from a previous response --- # Get Locales ## About The `getLocales` query retrieves locale information from your store with support for pagination and detailed field access. This query is essential for: - Displaying available language and locale options - Building multi-language selector interfaces - Determining text direction (LTR/RTL) for UI layout - Retrieving locale-specific logos and branding - Managing store language configurations - Building locale management interfaces The query supports cursor-based pagination and allows you to fetch all locales with full relationship access. ## Arguments | Argument | Type | Required | Description | |----------|------|----------|-------------| | `first` | `Int` | ❌ No | Number of locales to retrieve from the start (forward pagination). Max: 100. | | `after` | `String` | ❌ No | Cursor to start after for forward pagination. | | `last` | `Int` | ❌ No | Number of locales to retrieve from the end (backward pagination). Max: 100. | | `before` | `String` | ❌ No | Cursor to start before for backward pagination. | ## Possible Returns | Field | Type | Description | |-------|------|-------------| | `edges` | `[LocaleEdge!]!` | Array of locale edges containing locales and cursors. | | `edges.node` | `Locale!` | The actual locale object with id, code, name, direction, and other fields. | | `edges.cursor` | `String!` | Pagination cursor for this locale. Use with `after` or `before` arguments. | | `pageInfo` | `PageInfo!` | Pagination metadata object. | | `pageInfo.hasNextPage` | `Boolean!` | Whether more locales exist after the current page. | | `pageInfo.hasPreviousPage` | `Boolean!` | Whether locales exist before the current page. | | `pageInfo.startCursor` | `String` | Cursor of the first locale on the current page. | | `pageInfo.endCursor` | `String` | Cursor of the last locale on the current page. | | `totalCount` | `Int!` | Total number of locales available. | ## Locale Fields | Field | Type | Description | |-------|------|-------------| | `id` | `String!` | Unique identifier in format `/api/shop/locales/{id}` | | `_id` | `Int!` | Numeric identifier for the locale | | `code` | `String!` | Unique locale code (e.g., "en", "ar", "fr", "de") | | `name` | `String!` | Display name of the locale (e.g., "English", "Arabic") | | `direction` | `String!` | Text direction: "ltr" (left-to-right) or "rtl" (right-to-left) | | `logoPath` | `String` | File path to the locale logo | | `logoUrl` | `String` | Full URL to the locale logo image | | `createdAt` | `String!` | Creation timestamp (ISO 8601 format) | | `updatedAt` | `String!` | Last update timestamp (ISO 8601 format) | ## Common Use Cases ### Display All Available Locales ```graphql query GetAllLocales { locales { edges { node { id code name direction } } } } ``` ### Build Language Selector with Logos ```graphql query GetLocalesForSelector { locales { edges { node { code name logoUrl direction } } } } ``` ### Get Locale with Complete Information ```graphql query GetLocalesWithDetails { locales { edges { node { id _id code name direction logoPath logoUrl createdAt updatedAt } } pageInfo { hasNextPage endCursor } totalCount } } ``` ### Get Locale Count and Pagination ```graphql query GetLocalesWithPagination($first: Int!) { locales(first: $first) { edges { cursor node { id code name direction } } pageInfo { hasNextPage hasPreviousPage startCursor endCursor } totalCount } } ``` ### Build Right-to-Left (RTL) Language List ```graphql query GetRTLLocales { locales { edges { node { code name direction logoUrl } } } } ``` ## Error Handling ### Missing Locales Configuration ```json { "data": { "locales": { "edges": [], "pageInfo": { "hasNextPage": false, "hasPreviousPage": false, "startCursor": null, "endCursor": null }, "totalCount": 0 } } } ``` ### Invalid Pagination Parameters ```json { "errors": [ { "message": "Argument \"first\" must be between 1 and 100" } ] } ``` ### Invalid Cursor ```json { "errors": [ { "message": "Invalid cursor provided" } ] } ``` ## Best Practices 1. **Cache Locales** - Locales change infrequently; implement client-side caching 2. **Use Direction Field** - Always check the `direction` field for proper UI layout 3. **Request Only Needed Fields** - Reduce payload by selecting specific fields 4. **Display Logo URLs** - Use `logoUrl` for locale-specific branding in selectors 5. **Handle RTL/LTR** - Use the direction field to apply appropriate CSS classes 6. **Paginate When Needed** - For systems with many locales, use pagination 7. **Use Variables** - Use GraphQL variables for dynamic locale queries ## Related Resources - [Pagination Guide](/api/graphql-api/pagination) - Cursor pagination documentation - [Shop API Overview](/api/graphql-api/shop-api) - Overview of Shop API resources - [Authentication Guide](/api/graphql-api/authentication) - Authentication and authorization - [Channels API](/api/graphql-api/shop/queries/get-channels) - Store channel information --- # Get Single Locale URL: /api/rest-api/shop/locales/single --- outline: false examples: - id: get-locale-basic title: Get Single Locale - English description: Retrieve a single locale by ID with basic information. curl: | curl -X 'GET' \ 'https://api-demo.bagisto.com/api/shop/locales/1' \ -H 'accept: application/json' headers: | Content-Type: application/json Accept: application/json response: | { "id": 1, "code": "en", "name": "English", "direction": "ltr", "logoPath": "locales/en.png", "logoUrl": "https://api-demo.bagisto.com/storage/locales/en.png" } commonErrors: - error: 404 Not Found cause: Locale ID does not exist solution: Provide a valid locale ID that exists in the system - id: get-locale-complete title: Get Single Locale - With Timestamps description: Retrieve a single locale with all fields including timestamps. curl: | curl -X 'GET' \ 'https://api-demo.bagisto.com/api/shop/locales/2' \ -H 'accept: application/json' headers: | Content-Type: application/json Accept: application/json response: | { "id": 2, "code": "ar", "name": "Arabic", "direction": "rtl", "logoPath": "locales/ar.png", "logoUrl": "https://api-demo.bagisto.com/storage/locales/ar.png", "createdAt": "2023-11-20T18:15:58+05:30", "updatedAt": "2023-11-20T18:15:58+05:30" } commonErrors: - error: 404 Not Found cause: The provided locale ID does not exist solution: Use a valid locale ID from the list locales endpoint - id: get-locale-rtl title: Get RTL Locale Details description: Retrieve a right-to-left locale with full details for UI configuration. curl: | curl -X 'GET' \ 'https://api-demo.bagisto.com/api/shop/locales/2' \ -H 'accept: application/json' headers: | Content-Type: application/json Accept: application/json response: | { "id": 2, "code": "ar", "name": "Arabic", "direction": "rtl", "logoPath": "locales/ar.png", "logoUrl": "https://api-demo.bagisto.com/storage/locales/ar.png", "createdAt": "2023-11-20T18:15:58+05:30", "updatedAt": "2023-11-20T18:15:58+05:30" } commonErrors: - error: 404 Not Found cause: The provided locale ID does not exist solution: Verify the locale ID is correct - id: get-locale-node-js title: Get Locale - Node.js description: Retrieve a locale using Node.js fetch API. curl: | curl -X 'GET' \ 'https://api-demo.bagisto.com/api/shop/locales/1' \ -H 'accept: application/json' headers: | Content-Type: application/json Accept: application/json response: | { "id": 1, "code": "en", "name": "English", "direction": "ltr", "logoPath": "locales/en.png", "logoUrl": "https://api-demo.bagisto.com/storage/locales/en.png" } commonErrors: - error: 404 Not Found cause: Locale not found solution: Verify locale ID exists --- # Get Single Locale ## About Retrieves a single locale resource by ID with complete locale information. This endpoint is essential for: - Fetching specific locale details for UI configuration - Checking text direction (LTR/RTL) for layout adjustments - Retrieving locale-specific branding logos and URLs - Validating locale existence before operations - Building locale detail pages and selectors - Configuring locale-specific settings in the storefront The endpoint returns a single locale object with all properties including timestamps and asset URLs. ## Endpoint ``` GET /api/shop/locales/{id} ``` ## Path Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `id` | string | ✅ Yes | The unique identifier of the locale | ## Query Parameters | Parameter | Type | Description | |-----------|------|-------------| | `Accept` | string | Response format (application/json) | ## Response Fields | Field | Type | Description | |-------|------|-------------| | `id` | integer | Unique identifier for the locale | | `code` | string | Unique locale code (e.g., "en", "ar", "fr", "de") | | `name` | string | Display name of the locale (e.g., "English", "Arabic") | | `direction` | string | Text direction: "ltr" (left-to-right) or "rtl" (right-to-left) | | `logoPath` | string | File path to the locale logo (e.g., "locales/en.png") | | `logoUrl` | string | Full URL to the locale logo asset | | `createdAt` | string | Creation timestamp (ISO 8601 format) | | `updatedAt` | string | Last update timestamp (ISO 8601 format) | ## Common Use Cases ### Get Locale Details by ID ```bash GET /api/shop/locales/1 ``` Response: ```json { "id": 1, "code": "en", "name": "English", "direction": "ltr", "logoPath": "locales/en.png", "logoUrl": "https://api-demo.bagisto.com/storage/locales/en.png" } ``` ### Get Locale with Logo URL for Display ```bash GET /api/shop/locales/1 ``` Use the `logoUrl` field to display the locale flag or logo: ```json { "id": 1, "code": "en", "name": "English", "logoUrl": "https://api-demo.bagisto.com/storage/locales/en.png" } ``` ### Check If Locale is RTL ```bash GET /api/shop/locales/2 ``` Response with `direction` field to determine RTL layout: ```json { "id": 2, "code": "ar", "name": "Arabic", "direction": "rtl", "logoUrl": "https://api-demo.bagisto.com/storage/locales/ar.png" } ``` ### Get Complete Locale Information ```bash GET /api/shop/locales/1 ``` Full response including timestamps: ```json { "id": 1, "code": "en", "name": "English", "direction": "ltr", "logoPath": "locales/en.png", "logoUrl": "https://api-demo.bagisto.com/storage/locales/en.png", "createdAt": "2023-11-20T18:15:58+05:30", "updatedAt": "2023-11-20T18:15:58+05:30" } ``` ## Error Handling ### Locale Not Found ``` Status Code: 404 ``` ```json { "message": "Locale not found" } ``` ### Invalid Locale ID ``` Status Code: 400 ``` ```json { "message": "Invalid locale identifier" } ``` ## Best Practices 1. **Always Provide ID** - The ID parameter is required for this endpoint 2. **Handle 404 Errors** - Handle the case when locale is not found 3. **Use Direction Field** - Always check the `direction` field for proper UI layout 4. **Cache Results** - Locales change infrequently; implement caching strategy 5. **Use Logo URLs** - Display locale logos/flags for better UX using the `logoUrl` field 6. **Validate Before Using** - Verify locale exists before using in operations 7. **Handle RTL Properly** - Apply appropriate CSS classes based on direction value 8. **Request Only Needed Data** - Filter response fields if API supports sparse fieldsets ## Related Resources - [List Locales](/api/rest-api/shop/queries/get-locales) - Retrieve all locales with pagination - [Pagination Guide](/api/rest-api/introduction#pagination) - Pagination and filtering documentation - [Shop Resources](/api/rest-api/introduction#shop-resources) - Overview of Shop API resources - [Authentication Guide](/api/rest-api/authentication) - Authentication and authorization --- # Add Product to Cart URL: /api/rest-api/shop/mutations/add-to-cart --- outline: false examples: - id: add-simple-product-to-cart title: Add Simple Product to Cart description: Add a simple product with quantity to the shopping cart. request: | POST /api/cart_items X-Cart-Token: xyz-token-123 Content-Type: application/json { "product_id": 1, "quantity": 2 } response: | { "@context": "/api/contexts/CartItem", "@id": "/api/cart_items/1", "@type": "CartItem", "id": 1, "cart_token": "xyz-token-123", "product_id": 1, "product_name": "Premium T-Shirt", "sku": "TSHIRT-001", "quantity": 2, "price": 29.99, "subtotal": 59.98, "created_at": "2024-01-20T10:30:00Z" } commonErrors: - error: 404 Not Found cause: Product does not exist solution: Verify product_id is valid - error: 400 Out of Stock cause: Requested quantity exceeds stock solution: Reduce quantity or select different product - error: 400 Invalid Quantity cause: Quantity is 0 or negative solution: Provide quantity greater than 0 - id: add-configurable-product-to-cart title: Add Configurable Product to Cart description: Add a configurable product with selected variants to cart. request: | POST /api/cart_items X-Cart-Token: xyz-token-123 Content-Type: application/json { "product_id": 5, "quantity": 1, "variants": { "color": "Blue", "size": "M" } } response: | { "@context": "/api/contexts/CartItem", "@id": "/api/cart_items/2", "@type": "CartItem", "id": 2, "cart_token": "xyz-token-123", "product_id": 5, "product_name": "Configurable T-Shirt", "sku": "TSHIRT-BLU-M", "quantity": 1, "price": 34.99, "subtotal": 34.99, "variants": { "color": "Blue", "size": "M" }, "created_at": "2024-01-20T10:35:00Z" } commonErrors: - error: 400 Invalid Variant cause: Selected variant combination not available solution: Choose valid attribute values - error: 404 Not Found cause: Cart token does not exist solution: Create a new cart first --- # Add Product to Cart Add a product to the shopping cart with support for simple and configurable products with variants. ## Endpoint ``` POST /api/cart_items ``` ## Request Headers | Header | Required | Description | |--------|----------|-------------| | `X-Cart-Token` | Yes | Cart token from cart creation | | `Content-Type` | Yes | application/json | | `Authorization` | No | Bearer token (only for customer carts) | ## Request Body ```json { "product_id": 1, "quantity": 2, "variants": { "color": "Blue", "size": "M" } } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `product_id` | integer | Yes | Product ID to add | | `quantity` | integer | Yes | Quantity (minimum 1) | | `variants` | object | No | Variant selections for configurable products | ## Response Fields | Field | Type | Description | |-------|------|-------------| | `id` | integer | Cart item ID | | `cart_token` | string | Associated cart token | | `product_id` | integer | Product ID | | `product_name` | string | Product name | | `quantity` | integer | Item quantity | | `price` | number | Unit price | | `subtotal` | number | Total for this item (price × quantity) | | `variants` | object | Selected variants (if configurable) | | `created_at` | string | Creation timestamp | ## Usage Examples :::examples-selector ## Related Resources - [Create Cart](/api/rest-api/shop/mutations/create-cart) - [Update Cart Item](/api/rest-api/shop/mutations/update-cart-item) - [Remove Cart Item](/api/rest-api/shop/mutations/remove-cart-item) - [Get Cart Details](/api/rest-api/shop/queries/get-cart) --- # Create Cart URL: /api/rest-api/shop/mutations/create-cart --- outline: false examples: - id: create-cart-token title: Create Cart Token (Guest) description: Generate a new shopping cart token for guest users. request: | POST /api/carts Content-Type: application/json {} response: | { "@context": "/api/contexts/Cart", "@id": "/api/carts/xyz-token-123", "@type": "Cart", "token": "xyz-token-123", "items": [], "total": 0, "item_count": 0, "created_at": "2024-01-20T10:30:00Z" } commonErrors: - error: 400 Bad Request cause: Invalid request format solution: Ensure request body is valid JSON - error: 500 Internal Server Error cause: Server error creating cart solution: Retry the request or contact support - id: create-customer-cart title: Create Cart for Authenticated Customer description: Generate a shopping cart token for authenticated customer. request: | POST /api/carts Authorization: Bearer YOUR_CUSTOMER_TOKEN Content-Type: application/json {} response: | { "@context": "/api/contexts/Cart", "@id": "/api/carts/cust-token-456", "@type": "Cart", "token": "cust-token-456", "customer_id": 10, "items": [], "total": 0, "item_count": 0, "created_at": "2024-01-20T10:30:00Z" } commonErrors: - error: 401 Unauthorized cause: Invalid or expired authentication token solution: Provide a valid customer access token - error: 403 Forbidden cause: Customer account is disabled solution: Contact support to reactivate account --- # Create Cart Create a new shopping cart session for customers or guests. A cart token is required for adding products and managing the shopping experience. ## Endpoint ``` POST /api/carts ``` ## Request Headers | Header | Required | Description | |--------|----------|-------------| | `Authorization` | No | Bearer token for authenticated customers (optional) | | `Content-Type` | Yes | application/json | ## Request Body Empty JSON object: ```json {} ``` ## Response Fields | Field | Type | Description | |-------|------|-------------| | `token` | string | Unique cart token for session | | `customer_id` | integer | Customer ID (only if authenticated) | | `items` | array | Array of cart items (empty on creation) | | `total` | number | Cart total amount | | `item_count` | integer | Number of items in cart | | `created_at` | string | Creation timestamp | ## Usage Examples :::examples-selector ## Notes - Cart tokens are valid for 30 days of inactivity - Include the cart token in `X-Cart-Token` header for subsequent cart operations - Guest and customer carts are created with different token prefixes ## Related Resources - [Add Product to Cart](/api/rest-api/shop/mutations/add-to-cart) - [Get Cart Details](/api/rest-api/shop/queries/get-cart) - [Remove Cart Item](/api/rest-api/shop/mutations/remove-cart-item) --- # Create Product Review URL: /api/rest-api/shop/product-reviews/create-product-review --- outline: false examples: - id: create-product-review title: Create Product Review description: Create a new product review or rating. request: | POST /api/shop/products/1/reviews Content-Type: application/json X-STOREFRONT-KEY: pk_storefront_PvlE42nWGsKRVIf8bDlJngTPAdWAZbIy Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... { "title": "Excellent product!", "comment": "Very satisfied with this purchase. Great quality and fast delivery.", "rating": 5, "authorName": "John Doe", "authorEmail": "john@example.com" } response: | { "data": { "id": 1, "productId": 1, "title": "Excellent product!", "comment": "Very satisfied with this purchase. Great quality and fast delivery.", "rating": 5, "authorName": "John Doe", "authorEmail": "john@example.com", "status": "pending", "createdAt": "2024-01-20T15:30:00Z" }, "message": "Review submitted successfully and is pending approval" } commonErrors: - error: 401 Unauthorized cause: Customer not authenticated solution: Provide valid Bearer token - error: 422 Validation Error cause: Rating not between 1-5 or title is empty solution: Ensure rating is 1-5 and provide valid title - error: 404 Not Found cause: Product does not exist solution: Verify the product ID --- # Create Product Review Submit a new review and rating for a product. Reviews are typically pending approval before being displayed. ## Endpoint ``` POST /api/shop/products/{productId}/reviews ``` ## Request Headers | Header | Required | Description | |--------|----------|-------------| | `Content-Type` | Yes | application/json | | `X-STOREFRONT-KEY` | Yes | Your storefront API key | | `Authorization` | Yes | Bearer token (customer login required) | ## Path Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `productId` | integer | Yes | Product ID being reviewed | ## Request Body ```json { "title": "string", "comment": "string", "rating": "integer (1-5)", "authorName": "string", "authorEmail": "string" } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `title` | string | Yes | Review title (max 255 characters) | | `comment` | string | Yes | Review content/comment (min 10 characters) | | `rating` | integer | Yes | Rating (1=Poor, 5=Excellent) | | `authorName` | string | No | Reviewer name (defaults to customer name) | | `authorEmail` | string | No | Reviewer email (defaults to customer email) | ## Response Fields (201 Created) | Field | Type | Description | |-------|------|-------------| | `id` | integer | Review ID | | `productId` | integer | Product ID | | `title` | string | Review title | | `comment` | string | Review content | | `rating` | integer | Star rating | | `authorName` | string | Reviewer name | | `authorEmail` | string | Reviewer email | | `status` | string | Review status (pending, approved, rejected) | | `createdAt` | string | Creation timestamp | ## Use Cases - Allow customers to submit product reviews - Collect product feedback and ratings - Build social proof on product pages - Moderate reviews before publishing - Track customer satisfaction ## Rules - Customer must be authenticated - One review per customer per product recommended - Reviews require approval before display - Minimum comment length: 10 characters - Rating must be 1-5 stars ## Related Resources - [Get Product Reviews](/api/rest-api/shop/product-reviews/get-product-reviews) - [Update Product Review](/api/rest-api/shop/product-reviews/update-product-review) - [Delete Product Review](/api/rest-api/shop/product-reviews/delete-product-review) - [Get Product](/api/rest-api/shop/products/get-product) --- # Delete Product Review URL: /api/rest-api/shop/product-reviews/delete-product-review --- outline: false examples: - id: delete-product-review title: Delete Product Review description: Delete an existing product review. request: | DELETE /api/shop/reviews/1 Content-Type: application/json X-STOREFRONT-KEY: pk_storefront_PvlE42nWGsKRVIf8bDlJngTPAdWAZbIy Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... response: | { "message": "Review deleted successfully" } commonErrors: - error: 401 Unauthorized cause: Not authenticated or not review author solution: Ensure you own the review and provide valid token - error: 403 Forbidden cause: User is not the review author solution: Only review author can delete their review - error: 404 Not Found cause: Review does not exist solution: Verify the review ID --- # Delete Product Review Delete an existing product review. Only the review author can delete their own review. ## Endpoint ``` DELETE /api/shop/reviews/{reviewId} ``` ## Request Headers | Header | Required | Description | |--------|----------|-------------| | `Content-Type` | Yes | application/json | | `X-STOREFRONT-KEY` | Yes | Your storefront API key | | `Authorization` | Yes | Bearer token (review author required) | ## Path Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `productId` | integer | Yes | Product ID | | `reviewId` | integer | Yes | Review ID to delete | ## Response (204 No Content) ``` No response body ``` ## Alternative Response (200 OK) ```json { "message": "Review deleted successfully" } ``` ## Use Cases - Allow customers to remove their reviews - Delete inappropriate or accidental reviews - Clean up old reviews - Remove reviews if product opinion changes ## Permissions - Only review author can delete their review - Admin users can delete any review - Deletion is permanent ## Related Resources - [Get Product Review](/api/rest-api/shop/product-reviews/get-product-review) - [Create Product Review](/api/rest-api/shop/product-reviews/create-product-review) - [Update Product Review](/api/rest-api/shop/product-reviews/update-product-review) --- # Get Product Review URL: /api/rest-api/shop/product-reviews/get-product-review --- outline: false examples: - id: get-product-review title: Get Single Product Review description: Retrieve detailed information for a specific product review. request: | GET /api/shop/reviews/1 Content-Type: application/json X-STOREFRONT-KEY: pk_storefront_PvlE42nWGsKRVIf8bDlJngTPAdWAZbIy response: | { "data": { "id": 1, "productId": 1, "title": "Excellent product!", "comment": "Very satisfied with this purchase. Great quality and fast delivery.", "rating": 5, "authorName": "John Doe", "authorEmail": "john@example.com", "status": "approved", "helpful": 24, "unhelpful": 2, "createdAt": "2024-01-15T10:30:00Z", "updatedAt": "2024-01-16T14:20:00Z" } } commonErrors: - error: 404 Not Found cause: Review with specified ID does not exist solution: Verify the review ID - error: 401 Unauthorized cause: Invalid X-STOREFRONT-KEY solution: Provide valid storefront API key --- # Get Product Review Retrieve detailed information for a specific product review. ## Endpoint ``` GET /api/shop/reviews/{reviewId} ``` ## Request Headers | Header | Required | Description | |--------|----------|-------------| | `Content-Type` | Yes | application/json | | `X-STOREFRONT-KEY` | Yes | Your storefront API key | ## Path Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `productId` | integer | Yes | Product ID | | `reviewId` | integer | Yes | Review ID | ## Response Fields (200 OK) | Field | Type | Description | |-------|------|-------------| | `id` | integer | Review ID | | `productId` | integer | Product ID reviewed | | `title` | string | Review title | | `comment` | string | Full review text | | `rating` | integer | Rating (1-5 stars) | | `authorName` | string | Reviewer name | | `authorEmail` | string | Reviewer email | | `customerId` | integer | Customer ID (if customer review) | | `status` | string | Review status (approved, pending, rejected) | | `helpful` | integer | Number of helpful votes | | `unhelpful` | integer | Number of unhelpful votes | | `createdAt` | string | Creation date | | `updatedAt` | string | Last update date | ## Use Cases - Display individual review details - Show review with full context - Build review reply/discussion features - Load specific review for editing/flagging - Create featured review sections ## Related Resources - [Get Product Reviews](/api/rest-api/shop/product-reviews/get-product-reviews) - [Create Product Review](/api/rest-api/shop/product-reviews/create-product-review) - [Update Product Review](/api/rest-api/shop/product-reviews/update-product-review) --- # Get Product Reviews URL: /api/rest-api/shop/product-reviews/get-product-reviews --- outline: false examples: - id: get-product-reviews title: Get Product Reviews description: Retrieve paginated list of reviews for a product. request: | GET /api/shop/products/1/reviews Content-Type: application/json X-STOREFRONT-KEY: pk_storefront_PvlE42nWGsKRVIf8bDlJngTPAdWAZbIy response: | { "data": [ { "id": 1, "productId": 1, "title": "Excellent product!", "comment": "Very satisfied with this purchase", "rating": 5, "authorName": "John Doe", "authorEmail": "john@example.com", "status": "approved", "createdAt": "2024-01-15T10:30:00Z" }, { "id": 2, "productId": 1, "title": "Good but expensive", "comment": "Quality is great but price is high", "rating": 4, "authorName": "Jane Smith", "authorEmail": "jane@example.com", "status": "approved", "createdAt": "2024-01-14T15:20:00Z" } ], "pagination": { "total": 2, "perPage": 10, "currentPage": 1 } } commonErrors: - error: 404 Not Found cause: Product with specified ID does not exist solution: Verify the product ID - error: 401 Unauthorized cause: Invalid X-STOREFRONT-KEY solution: Provide valid storefront API key --- # Get Product Reviews Retrieve a paginated list of reviews and ratings for a specific product. ## Endpoint ``` GET /api/shop/products/{productId}/reviews ``` ## Request Headers | Header | Required | Description | |--------|----------|-------------| | `Content-Type` | Yes | application/json | | `X-STOREFRONT-KEY` | Yes | Your storefront API key | ## Path Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `productId` | integer | Yes | Product ID | ## Query Parameters | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `page` | integer | 1 | Page number | | `limit` | integer | 10 | Reviews per page | | `sort` | string | latest | Sort by (latest, oldest, helpful, rating) | | `rating` | integer | - | Filter by rating (1-5) | | `status` | string | approved | Filter by status (approved, pending, rejected) | ## Response Fields (200 OK) | Field | Type | Description | |-------|------|-------------| | `id` | integer | Review ID | | `productId` | integer | Product ID reviewed | | `title` | string | Review title | | `comment` | string | Review comment/content | | `rating` | integer | Rating (1-5 stars) | | `authorName` | string | Reviewer name | | `authorEmail` | string | Reviewer email | | `status` | string | Review status (approved, pending, rejected) | | `helpful` | integer | Number of helpful votes | | `unhelpful` | integer | Number of unhelpful votes | | `createdAt` | string | Review creation date | | `updatedAt` | string | Last update date | ## Pagination | Field | Type | Description | |-------|------|-------------| | `total` | integer | Total reviews for product | | `perPage` | integer | Reviews per page | | `currentPage` | integer | Current page | ## Use Cases - Display product reviews on detail pages - Show review ratings and statistics - Filter reviews by rating - Load recent reviews - Build review management interfaces ## Related Resources - [Get Product Review](/api/rest-api/shop/product-reviews/get-product-review) - [Create Product Review](/api/rest-api/shop/product-reviews/create-product-review) - [Get Product](/api/rest-api/shop/products/get-product) --- # Update Product Review URL: /api/rest-api/shop/product-reviews/update-product-review --- outline: false examples: - id: update-product-review title: Update Product Review description: Update an existing product review. request: | PUT /api/shop/reviews/1 Content-Type: application/json X-STOREFRONT-KEY: pk_storefront_PvlE42nWGsKRVIf8bDlJngTPAdWAZbIy Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... { "title": "Great product!", "comment": "Updated review - even better than expected", "rating": 5 } response: | { "data": { "id": 1, "productId": 1, "title": "Great product!", "comment": "Updated review - even better than expected", "rating": 5, "authorName": "John Doe", "authorEmail": "john@example.com", "status": "pending", "updatedAt": "2024-01-21T10:15:00Z" }, "message": "Review updated successfully" } commonErrors: - error: 401 Unauthorized cause: Not authenticated or not review author solution: Ensure you own the review and provide valid token - error: 403 Forbidden cause: User is not the review author solution: Only review author can update their review - error: 404 Not Found cause: Review does not exist solution: Verify the review ID --- # Update Product Review Update an existing product review. Only the review author can update their own review. ## Endpoint ``` PUT /api/shop/reviews/{reviewId} ``` ## Request Headers | Header | Required | Description | |--------|----------|-------------| | `Content-Type` | Yes | application/json | | `X-STOREFRONT-KEY` | Yes | Your storefront API key | | `Authorization` | Yes | Bearer token (review author required) | ## Path Parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `productId` | integer | Yes | Product ID | | `reviewId` | integer | Yes | Review ID to update | ## Request Body ```json { "title": "string", "comment": "string", "rating": "integer (1-5)" } ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `title` | string | No | Updated review title | | `comment` | string | No | Updated review content | | `rating` | integer | No | Updated rating (1-5) | ## Response Fields (200 OK) | Field | Type | Description | |-------|------|-------------| | `id` | integer | Review ID | | `productId` | integer | Product ID | | `title` | string | Review title | | `comment` | string | Review content | | `rating` | integer | Star rating | | `authorName` | string | Reviewer name | | `authorEmail` | string | Reviewer email | | `status` | string | Review status | | `updatedAt` | string | Last update timestamp | ## Use Cases - Allow customers to edit their reviews - Update reviews based on new experience - Correct mistakes or typos in reviews - Change ratings if opinion changes ## Permissions - Only review author can update their review - Admin users can update any review - Updated reviews may require re-approval ## Related Resources - [Get Product Review](/api/rest-api/shop/product-reviews/get-product-review) - [Create Product Review](/api/rest-api/shop/product-reviews/create-product-review) - [Delete Product Review](/api/rest-api/shop/product-reviews/delete-product-review) --- # Booking Slots URL: /api/rest-api/shop/products/get-booking-slots --- outline: false examples: - id: get-default-slots title: Default / Appointment / Table / Event slots description: | For non-rental booking types, each entry is a flat slot with `from`, `to`, `timestamp`, `qty`. The example below is for a `default`-type booking product on a specific date. request: | curl -X GET "http://localhost/api/shop/booking-slots?id=1&date=2026-05-04" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK [ { "slotId": "1777876200-1777897800", "from": "12:00 PM", "to": "06:00 PM", "timestamp": "1777876200-1777897800" } ] commonErrors: - error: 400 Bad Request cause: Missing or invalid `id` / `date` solution: Pass `?id=<booking_product_id>&date=YYYY-MM-DD` (both required). - error: 404 Not Found cause: No booking product with the given `id` solution: Discover IDs via `GET /api/shop/products?type=booking` then `GET /api/shop/products/{id}` (the `bookingProducts[].id` is the value to send here). - error: 401 Unauthorized cause: Missing or invalid `X-STOREFRONT-KEY` solution: Send a valid storefront API key. - id: get-rental-slots title: Rental hourly slots description: | For rental hourly products, the response is a list of **time-range groups**. Each group has a `time` label and a nested `slots[]` array of individual hourly sub-slots. request: | curl -X GET "http://localhost/api/shop/booking-slots?id=4&date=2026-05-04" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK [ { "slotId": "1", "time": "10:00 AM - 12:00 PM", "slots": [ { "from": "10:00 AM", "to": "11:00 AM", "timestamp": "1777867200-1777870800", "qty": "5" }, { "from": "11:00 AM", "to": "12:00 PM", "timestamp": "1777870800-1777874400", "qty": "5" } ] }, { "slotId": "2", "time": "02:00 PM - 04:00 PM", "slots": [ { "from": "02:00 PM", "to": "03:00 PM", "timestamp": "1777881600-1777885200", "qty": "5" } ] } ] --- # Booking Slots Returns the **runtime availability** of slots for a booking product on a specific date. The Bagisto booking system supports five booking types (`default`, `appointment`, `rental`, `event`, `table`); the slot-config block lives on the parent product (see `bookingProducts[]` in [Single Product](/api/rest-api/shop/products/get-product)). This endpoint converts that config + a target date into the actual bookable time slots. ## Endpoint ``` GET /api/shop/booking-slots ``` ## Request Headers | Header | Required | Description | |--------------------|----------|------------------------------------------| | `Accept` | Yes | `application/json` | | `X-STOREFRONT-KEY` | Yes | Storefront API key (`pk_storefront_…`) | | `X-Locale` | No | Override request locale | | `X-Channel` | No | Override channel scope | ## Query Parameters | Parameter | Type | Required | Description | |-----------|---------|----------|----------------------------------------------------------------------------------------------------------------------| | `id` | integer | Yes | **Booking product ID** — i.e. `bookingProducts[].id` from a parent product, **not** the parent product's `id`. | | `date` | string | Yes | Target date in `YYYY-MM-DD` format. Times in the response are computed against the channel's timezone for that date. | > Pagination headers are **not** emitted — the response is the full list of slots for the date in one shot. ## Response shape — by booking type The internal structure depends on the booking product's `type` field. The API auto-routes to the right shape based on the parent booking product, so you don't pick which response — you get the right one for the product. ### Flat slots (`default` · `appointment` · `event` · `table`) ```json [ { "slotId": "...", "from": "12:00 PM", "to": "06:00 PM", "timestamp": "1777876200-1777897800", "qty": "5" } ] ``` | Field | Type | Description | |--------------|-----------------|----------------------------------------------------------------------------------------| | `slotId` | string | Stable identifier — pass it back to the cart endpoint when adding the slot. | | `from` | string | Slot start time (`hh:mm AM/PM`) | | `to` | string | Slot end time | | `timestamp` | string | Unix `from-to` range — server-computed, useful for logging and analytics | | `qty` | string \| null | Remaining capacity. `null` for types that don't track per-slot inventory | ### Grouped slots (`rental` hourly) ```json [ { "slotId": "1", "time": "10:00 AM - 12:00 PM", "slots": [ { "from": "10:00 AM", "to": "11:00 AM", "timestamp": "1777867200-1777870800", "qty": "5" } ] } ] ``` | Field | Type | Description | |--------------|-----------------|----------------------------------------------------------------------------------------| | `slotId` | string | Sequential index of the time-range group | | `time` | string | Group label (e.g. `"10:00 AM - 12:00 PM"`) | | `slots` | array | Nested hourly sub-slots — same `{ from, to, timestamp, qty }` shape as flat slots | ## Typical Flow ``` GET /api/shop/products/110 └─ response.type = "booking" └─ response.bookingProducts = [{ id: 1, type: "default", … }] GET /api/shop/booking-slots?id=1&date=2026-05-04 └─ [{ slotId: "...", from: "12:00 PM", to: "06:00 PM", … }] ``` The `id` you pass to `/booking-slots` is the **booking product** ID (the entry from the parent's `bookingProducts[]` array), **not** the parent product ID. ## Empty results A `200 OK` with an empty array `[]` means "the product is bookable in principle but has no slots on this date". Common causes: - The booking product's weekly schedule excludes that day of the week. - All slots for the date are sold out. - The date is before/after the booking product's allowed window. ## Use Cases - Render a date-picker on a booking-product detail page that shows available slots when a date is selected. - Validate a chosen `slotId` before adding it to the cart (re-fetch with the same date to confirm it's still in the response). - Power an admin "view today's bookings" tool — combine `?date=` with each active booking product. ## Related Resources - [Single Product](/api/rest-api/shop/products/get-product) — embeds the `bookingProducts[]` slot config that this endpoint dereferences - [Products](/api/rest-api/shop/products/get-products) — discover booking products via `?type=booking` - [Search Products](/api/rest-api/shop/products/search-product) --- # Single Product URL: /api/rest-api/shop/products/get-product --- outline: false examples: - id: get-simple-product title: Get a Simple Product description: PDP-ready document for a `simple` product. All relations are inlined — no follow-up requests required. `isInWishlist` / `isInCompare` reflect the signed-in customer (`1` = in the list, `0` = not; send the customer Bearer token); this guest request returns both as `0`. request: | curl -X GET "http://localhost/api/shop/products/2" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK { "id": 2, "sku": "PUREWHTSNEAK2023", "type": "simple", "name": "PureStride Men's Classic White Sneakers", "urlKey": "purestride-mens-classic-white-sneakers", "status": true, "description": "Introducing PureStride Men's Classic White Sneakers…", "shortDescription": "Step into timeless style…", "price": 189, "specialPrice": null, "new": true, "featured": true, "minimumPrice": 189, "maximumPrice": 189, "formattedPrice": "$189.00", "formattedSpecialPrice": null, "formattedMinimumPrice": "$189.00", "formattedMaximumPrice": "$189.00", "baseImageUrl": "http://localhost/storage/product/2/XmdfIafCjuYEhHiBkHvzmOuDT0mpGHDTi9QhnUoY.webp", "createdAt": "2024-04-16T23:04:43+05:30", "updatedAt": "2024-04-16T23:04:43+05:30", "isSaleable": true, "isInWishlist": 0, "isInCompare": 0, "color": null, "size": null, "brand": null, "categories": [], "channels": [ { "id": 1, "code": "default", "hostname": "https://example.com", "currencyCode": "USD", "localeCode": "en" } ], "attributeFamily": { "id": 1, "code": "default", "name": "Default" }, "images": [ { "id": 969, "type": "images", "path": "product/2/XmdfIafCjuYEhHiBkHvzmOuDT0mpGHDTi9QhnUoY.webp", "productId": 2, "position": 1, "publicPath": "http://localhost/storage/product/2/XmdfIafCjuYEhHiBkHvzmOuDT0mpGHDTi9QhnUoY.webp" } ], "videos": [], "superAttributes": [], "variants": [], "bookingProducts": [], "bundleOptions": [], "groupedProducts": [], "downloadableLinks": [], "downloadableSamples": [], "customizableOptions": [], "relatedProducts": [], "upSells": [], "crossSells": [] } commonErrors: - error: 404 Not Found cause: No product with the given `{id}` exists or the product has `status=false` solution: Discover valid IDs via `GET /api/shop/products`. - error: 401 Unauthorized cause: Missing or invalid `X-STOREFRONT-KEY` solution: Send a valid storefront API key. - id: get-configurable-product title: Get a Configurable Product description: For configurable products, `superAttributes[]` and `variants[]` are inlined alongside the parent fields. request: | curl -X GET "http://localhost/api/shop/products/23" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK { "id": 23, "sku": "NGJGAJSDGJ123123GJGJ", "type": "configurable", "name": "Luggage bags", "minimumPrice": 0, "maximumPrice": 0, "superAttributes": [ { "id": 23, "code": "color", "type": "select", "adminName": "Color", "options": [ { "id": 1, "adminName": "Red", "translation": { "label": "Red" } }, { "id": 2, "adminName": "Green", "translation": { "label": "Green" } } ] } ], "variants": [ { "id": 24, "sku": "...", "type": "simple", "name": "Luggage bags-Red", "price": 0, "...": "..." }, { "id": 25, "sku": "...", "type": "simple", "name": "Luggage bags-Green", "price": 0, "...": "..." } ], "...": "..." } commonErrors: - error: 404 Not Found cause: No product with the given `{id}` exists solution: List products via `GET /api/shop/products`. - id: get-booking-product title: Get a Booking Product description: For `booking` products, `bookingProducts[]` carries the slot-config block. Use [Booking Slots](/api/rest-api/shop/products/get-booking-slots) for runtime availability on a specific date. request: | curl -X GET "http://localhost/api/shop/products/110" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK { "id": 110, "sku": "BOOK-DEFAULT-1", "type": "booking", "name": "Yoga Class", "...": "...", "bookingProducts": [ { "id": 1, "type": "default", "qty": 10, "availableEveryWeek": 1, "slots": { "duration": "60", "break_time": "10", "same_slot_all_days": 1, "slots": [{ "from": "09:00 AM", "to": "06:00 PM" }] } } ] } commonErrors: - error: 404 Not Found cause: No product with the given `{id}` exists solution: List products via `GET /api/shop/products`. --- # Single Product Returns the **full PDP-ready document** for a product — categories, channels, attribute family, images, videos, super-attributes (configurable parents), variants, bundle options, booking config, grouped members, downloadable links / samples, customizable options, related/up-sell/cross-sell products — all embedded inline. **No follow-up requests are needed** to render a complete product detail page. ## Endpoint ``` GET /api/shop/products/{id} ``` ## Request Headers | Header | Required | Description | |--------------------|----------|------------------------------------------| | `Accept` | Yes | `application/json` | | `X-STOREFRONT-KEY` | Yes | Storefront API key (`pk_storefront_…`) | | `X-Locale` | No | Override request locale | | `X-Channel` | No | Override channel scope | | `X-Currency` | No | Override currency in `formattedPrice` etc. | ## Path Parameters | Parameter | Type | Required | Description | |-----------|---------|----------|-----------------------------------------| | `id` | integer | Yes | Product primary key | ## Response `200 OK` — single product object. The card-level fields (id, sku, type, name, price, …) are present, plus the embedded relations below. ### Card-level fields Same shape as items in [Products](/api/rest-api/shop/products/get-products#card-level-fields) — see that page for the field table. This includes the per-customer `isInWishlist` and `isInCompare` booleans. > **Wishlist & compare flags:** `isInWishlist` (active channel) and `isInCompare` tell you whether the signed-in customer already has this product in their wishlist / compare list (`1` = yes, `0` = no), so you can highlight the wishlist / compare icon on the product page without a separate lookup. Include the customer Bearer token alongside the storefront key — for guests both are `0`. REST returns `1` / `0` integers (over GraphQL the same flags come back as the strings `"1"` / `"0"`). ### Always-present extras (over the list) | Field | Type | Description | |-------------------|-----------------|--------------------------------------------------------------------------| | `description` | string | Full HTML description | | `createdAt` | string (ISO) | Creation timestamp | | `updatedAt` | string (ISO) | Last update timestamp | | `isSaleable` | boolean | Whether the product passes saleability checks (stock, status, …) | | `attributeFamily` | object | `{ id, code, name }` | ### Filterable attribute summary The default-family product output includes top-level shortcuts for the common filterable attributes: | Field | Type | Description | |----------|-----------------|------------------------------------------| | `color` | object \| null | `{ id, code, label }` for the assigned color option | | `size` | object \| null | Same shape, for `size` | | `brand` | object \| null | Same shape, for `brand` | > The exact set depends on the active attribute family — these three are present for the default family. ### Embedded relations (always present, may be empty `[]`) | Field | Type | Notes | |-------------------------|--------|-------------------------------------------------------------------------------------------------------------| | `categories` | array | Categories the product belongs to | | `channels` | array | Channels exposing this product (`{ id, code, hostname, currencyCode, localeCode }`) | | `images` | array | Image objects (`{ id, type, path, productId, position, publicPath }`) | | `videos` | array | Video objects | | `superAttributes` | array | **Configurable type only** — list of attributes used to build variants, with their options inlined | | `variants` | array | **Configurable type only** — child variant products, each carrying card-level fields | | `bookingProducts` | array | **Booking type only** — slot config with `type` (`default`/`appointment`/`rental`/`event`/`table`) and a type-specific `slots` block | | `bundleOptions` | array | **Bundle type only** — option groups with their member products inlined | | `groupedProducts` | array | **Grouped type only** — associated products | | `downloadableLinks` | array | **Downloadable type only** — purchasable links | | `downloadableSamples` | array | **Downloadable type only** — preview samples | | `customizableOptions` | array | Per-product custom inputs (text fields, file uploads, dropdowns added by the merchant) | | `relatedProducts` | array | "Customers also bought" cards | | `upSells` | array | "Upgrade to" cards | | `crossSells` | array | "Pairs well with" cards (shown in the cart/checkout) | > Empty `[]` for product types that don't apply — a `simple` product has no `variants`, a `booking` product has no `bundleOptions`, etc. Clients should treat these as "render only if non-empty". ## What this endpoint deliberately omits - **`attributeValues`** — the raw EAV table is not returned. Use the typed fields (`name`, `description`, `price`, `color`, …) instead. - **Reviews** — fetched separately via the [Product Reviews](/api/rest-api/shop/product-reviews/get-product-reviews) endpoint, paginated. ## Sub-resource endpoints Each embedded relation is also available as its own URL — useful when you only need one slice without re-fetching the entire PDP. These are tagged **`Product`** or **`Product Types`** in Swagger UI: | URL | Returns | |------------------------------------------------------|-----------------------------------------------| | `GET /api/shop/products/{productId}/variants` | Variants for a configurable product | | `GET /api/shop/products/{productId}/booking-products`| Booking config row(s) for a booking product | | `GET /api/shop/booking-products/{id}` | Single booking config (with type-specific `slots`) | | `GET /api/shop/booking-slots?id={bp}&date=YYYY-MM-DD`| Runtime slot availability — see [Booking Slots](/api/rest-api/shop/products/get-booking-slots) | ## Use Cases - Render a full product detail page in **one** network round trip. - Configurable variant picker: read `superAttributes[]` for the dimensions, `variants[]` for the inventory of combinations. - Bundle builder: walk `bundleOptions[]` for the selectable groups; each group has its member products inlined. - Determine product type at render time via `type` (`simple`, `configurable`, `bundle`, …) and `bookingType` (`default`, `appointment`, `rental`, `event`, `table` for booking products). ## Related Resources - [Products](/api/rest-api/shop/products/get-products) — paginated card-level list - [Search Products](/api/rest-api/shop/products/search-product) — full filter / sort / search reference - [Booking Slots](/api/rest-api/shop/products/get-booking-slots) — runtime availability for booking products - [Categories](/api/rest-api/shop/categories/get-categories) - [Attributes](/api/rest-api/shop/attributes/get-attributes) --- # Products URL: /api/rest-api/shop/products/get-products --- outline: false examples: - id: list-products title: List Products description: Default paginated list — no filters, default sort, page 1, 30 items. request: | curl -X GET "http://localhost/api/shop/products" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK X-Total-Count: 84 X-Page: 1 X-Per-Page: 30 X-Total-Pages: 3 [ { "id": 1, "sku": "COASTALBREEZEMENSHOODIE", "type": "simple", "bookingType": null, "name": "Coastal Breeze Men's Blue Zipper Hoodie", "urlKey": "coastal-breeze-mens-blue-zipper-hoodie", "status": true, "shortDescription": "Stay warm and stylish…", "price": 100, "specialPrice": null, "new": true, "featured": true, "minimumPrice": 100, "maximumPrice": 100, "formattedPrice": "$100.00", "formattedSpecialPrice": null, "formattedMinimumPrice": "$100.00", "formattedMaximumPrice": "$100.00", "baseImageUrl": "http://localhost/storage/product/1/zKcWZTLDjcawJmaNg8g1cpARqwVONgEKEflabstT.webp", "isInWishlist": 0, "isInCompare": 0 } ] commonErrors: - error: 401 Unauthorized cause: Missing or invalid `X-STOREFRONT-KEY` solution: Send a valid storefront API key. - error: 403 Forbidden cause: Storefront key inactive or rate-limited solution: Activate the key or wait for the rate limit window to reset. - id: list-products-wishlist-compare-flags title: List Products with Wishlist & Compare Flags description: Every card carries `isInWishlist` and `isInCompare` so you can highlight the wishlist / compare icon directly from the listing — no need to separately fetch and cross-reference the wishlist or compare lists. These are per-customer flags (`1` = in the list, `0` = not), so include the customer Bearer token; without it (guest) both are always `0`. request: | curl -X GET "http://localhost/api/shop/products?per_page=3" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \ -H "Authorization: Bearer 1|customer_token_xxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK X-Total-Count: 84 X-Page: 1 X-Per-Page: 3 X-Total-Pages: 28 [ { "id": 1, "sku": "COASTALBREEZEMENSHOODIE", "type": "simple", "name": "Coastal Breeze Men's Blue Zipper Hoodie", "price": 100, "baseImageUrl": "http://localhost/storage/product/1/zKcWZTLDjcawJmaNg8g1cpARqwVONgEKEflabstT.webp", "isInWishlist": 1, "isInCompare": 0 }, { "id": 22, "sku": "ACME-DRAWBAG-001", "type": "simple", "name": "Acme Drawstring Bag", "price": 3000, "baseImageUrl": "http://localhost/storage/product/22/acme-drawbag.webp", "isInWishlist": 0, "isInCompare": 1 }, { "id": 92, "sku": "bagisto-sticker", "type": "simple", "name": "Bagisto Sticker", "price": 10, "baseImageUrl": "http://localhost/storage/product/92/sticker.webp", "isInWishlist": 0, "isInCompare": 0 } ] commonErrors: - error: 401 Unauthorized cause: Missing or invalid `X-STOREFRONT-KEY` solution: Send a valid storefront API key. - id: paginated-products title: Paginated List description: Use `page` and `per_page` to walk the full catalog. Pagination metadata is always emitted as headers. request: | curl -X GET "http://localhost/api/shop/products?page=2&per_page=20" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK X-Total-Count: 84 X-Page: 2 X-Per-Page: 20 X-Total-Pages: 5 [ { "id": 21, "sku": "...", "type": "simple", "name": "...", "price": 49, "...": "..." }, { "id": 22, "sku": "...", "type": "simple", "name": "...", "price": 89, "...": "..." } ] commonErrors: - error: 401 Unauthorized cause: Missing or invalid `X-STOREFRONT-KEY` solution: Send a valid storefront API key. --- # Products Paginated list of catalog products. The response is a **slim card-level payload** — fields needed to render listing/grid/search results. For the full PDP shape (categories, variants, bundle options, booking config, customizable options, related products, …) use [Single Product](/api/rest-api/shop/products/get-product). ## Endpoint ``` GET /api/shop/products ``` This same endpoint also powers all filtering, sorting and search — see [Search Products](/api/rest-api/shop/products/search-product) for the full set of query parameters. ## Request Headers | Header | Required | Description | |--------------------|----------|------------------------------------------| | `Accept` | Yes | `application/json` | | `X-STOREFRONT-KEY` | Yes | Storefront API key (`pk_storefront_…`) | | `X-Locale` | No | Override request locale | | `X-Channel` | No | Override channel scope | | `X-Currency` | No | Override currency in `formattedPrice` etc. | ## Query Parameters | Parameter | Type | Default | Description | |-------------|---------|---------|---------------------------------------------------------------------| | `page` | integer | 1 | Page number (1-based) | | `per_page` | integer | 30 | Items per page. Capped at 50. | For all filter / search / sort parameters (`query`, `sort`, `type`, `category_id`, `price`, attribute filters …) see [Search Products](/api/rest-api/shop/products/search-product). Pagination headers (`X-Total-Count`, `X-Page`, `X-Per-Page`, `X-Total-Pages`) are always emitted — see [Pagination](/api/rest-api/introduction#pagination). ## Card-level fields These ~20 fields are returned for every product in the list. The PDP endpoint returns these **plus** every related resource inlined. | Field | Type | Description | |--------------------------|-----------------|----------------------------------------------------------------------| | `id` | integer | Product primary key | | `sku` | string | Stock-keeping unit | | `type` | string | `simple`, `configurable`, `bundle`, `grouped`, `virtual`, `downloadable`, `booking` | | `bookingType` | string \| null | When `type = booking`: `default`, `appointment`, `rental`, `event`, `table` — otherwise `null` | | `name` | string | Localized name | | `urlKey` | string | URL slug | | `status` | boolean | Whether the product is published | | `shortDescription` | string | Short marketing description | | `price` | number | Default price | | `specialPrice` | number \| null | Sale price when in promotion window | | `new` | boolean | "New" flag set in admin | | `featured` | boolean | "Featured" flag set in admin | | `minimumPrice` | number | Lowest price across variants (configurable / bundle) | | `maximumPrice` | number | Highest price across variants | | `formattedPrice` | string | Currency-formatted `price` | | `formattedSpecialPrice` | string \| null | Currency-formatted `specialPrice` | | `formattedMinimumPrice` | string | Currency-formatted `minimumPrice` | | `formattedMaximumPrice` | string | Currency-formatted `maximumPrice` | | `baseImageUrl` | string \| null | Primary thumbnail URL | | `isInWishlist` | integer (0/1) | `1` if this product is in the signed-in customer's wishlist (active channel), else `0`. `0` for guests. | | `isInCompare` | integer (0/1) | `1` if this product is in the signed-in customer's compare list, else `0`. `0` for guests. | > Heavy relations (`images`, `videos`, `categories`, `channels`, `variants`, `bookingProducts`, `bundleOptions`, `customizableOptions`, `relatedProducts`, etc.) are **omitted** from the list response. Fetch the [Single Product](/api/rest-api/shop/products/get-product) to get them inlined. ## Wishlist & compare flags Every product card carries two per-customer booleans, `isInWishlist` and `isInCompare`, so you can render the wishlist and compare icon states straight from the listing response. Why they exist: the wishlist and compare lists are their own endpoints and paginate independently of the catalog — a product on catalog page 1 may have its wishlist entry on a different wishlist page, so matching the two lists on the client is unreliable. These flags answer the question per product, in the same response, so the wishlist/compare icon can be highlighted without any extra requests. - **Authentication is required.** Include the customer Bearer token alongside the storefront key. For guests (no customer token) both flags are always `0`. - **`isInWishlist`** is scoped to the active channel; **`isInCompare`** applies across the store. - A product is flagged (`1`) when its own product ID is in the customer's wishlist / compare list — so a configurable parent is flagged when the parent itself was added. - The REST API returns these as `1` / `0` integers. (Over GraphQL the same flags are returned as the strings `"1"` / `"0"`.) ## Use Cases - Render a category page / grid view / search results with paginated cards. - Implement infinite scroll using `?page=N&per_page=20` and `X-Total-Pages`. - Pre-load the next page based on `X-Page < X-Total-Pages`. ## Related Resources - [Single Product](/api/rest-api/shop/products/get-product) — full PDP-ready document with every relation embedded - [Search Products](/api/rest-api/shop/products/search-product) — full filter / sort / search reference - [Booking Slots](/api/rest-api/shop/products/get-booking-slots) — runtime availability for `booking` products - [Categories](/api/rest-api/shop/categories/get-categories) — `?category_id=N` to scope by category - [Attributes](/api/rest-api/shop/attributes/get-attributes) — discover filterable attribute codes --- # Product Sub-Resources (`Product` tag) URL: /api/rest-api/shop/products/product-subresources --- outline: false examples: - id: list-product-images title: Images — List for a product description: | All images attached to a single product. The same data is also inlined under `images[]` in the [Single Product](/api/rest-api/shop/products/get-product) response — call this only when you need just the gallery. request: | curl -X GET "http://localhost/api/shop/products/2/images" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK [ { "id": 969, "type": "images", "path": "product/2/XmdfIafCjuYEhHiBkHvzmOuDT0mpGHDTi9QhnUoY.webp", "productId": 2, "position": 1, "publicPath": "http://localhost/storage/product/2/XmdfIafCjuYEhHiBkHvzmOuDT0mpGHDTi9QhnUoY.webp" } ] commonErrors: - error: 404 Not Found cause: Parent product `{productId}` doesn't exist solution: Discover product IDs via `GET /api/shop/products`. - id: list-product-images-root title: Images — Root collection description: Paginated list of every product image across all products. Useful for an admin gallery audit. request: | curl -X GET "http://localhost/api/shop/product-images?per_page=2" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK X-Total-Count: 1234 X-Page: 1 X-Per-Page: 2 X-Total-Pages: 617 [ { "id": 4, "type": "images", "path": "product/4/nRO8G2ljoUejAr6agYhx5eZTUwBIeft61dNRwNw6.webp", "productId": 4, "position": 1, "publicPath": "http://localhost/storage/product/4/nRO8G2ljoUejAr6agYhx5eZTUwBIeft61dNRwNw6.webp" } ] - id: get-product-image title: Images — Single by ID description: Resolve a single image by its global ID (e.g. an ID stored on a wishlist or order line). request: | curl -X GET "http://localhost/api/shop/product-images/969" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK { "id": 969, "type": "images", "path": "product/2/XmdfIafCjuYEhHiBkHvzmOuDT0mpGHDTi9QhnUoY.webp", "productId": 2, "position": 1, "publicPath": "http://localhost/storage/product/2/XmdfIafCjuYEhHiBkHvzmOuDT0mpGHDTi9QhnUoY.webp" } - id: list-product-videos title: Videos — List for a product description: Same shape as images, just video assets. Inlined under `videos[]` on the Single Product response. request: | curl -X GET "http://localhost/api/shop/products/2/videos" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK [ { "id": 12, "type": "videos", "path": "product/2/promo.mp4", "productId": 2, "position": 1, "publicPath": "http://localhost/storage/product/2/promo.mp4" } ] - id: list-customer-group-prices title: Customer Group Prices — List for a product description: | Tier (customer-group) prices configured on a product. Returns an empty array when no tier rules apply. **Customer group prices are not inlined** in the Single Product response — fetch them here when needed. request: | curl -X GET "http://localhost/api/shop/products/2/customer-group-prices" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK [ { "id": 4, "qty": 5, "valueType": "fixed", "value": 80, "customerGroupId": 2, "uniqueId": "2|2|5", "productId": 2 } ] - id: get-customer-group-price title: Customer Group Prices — Single (nested or flat) description: | Resolve a single tier-price row. Available under both `/products/{productId}/customer-group-prices/{id}` and `/customer-group-prices/{id}` — same response. request: | curl -X GET "http://localhost/api/shop/customer-group-prices/4" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK { "id": 4, "qty": 5, "valueType": "fixed", "value": 80, "customerGroupId": 2, "uniqueId": "2|2|5", "productId": 2 } - id: list-customizable-options title: Customizable Options — List for a product description: | Per-product custom inputs (text fields, file uploads, dropdowns added by the merchant). Inlined under `customizableOptions[]` on the Single Product response — fetch here only when you need the slice. request: | curl -X GET "http://localhost/api/shop/products/14/customizable-options" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK [ { "id": 1, "type": "field", "isRequired": 1, "sortOrder": 0, "product": "/api/shop/products/14", "prices": ["/api/shop/product_customizable_option_prices/1"], "translation": "/api/shop/product_customizable_option_translations/1", "translations": [ "/api/shop/product_customizable_option_translations/1", "/api/shop/product_customizable_option_translations/4" ] } ] - id: get-customizable-option title: Customizable Options — Single (snake_case URL) description: | Resolve a single customizable option by ID. **Note the underscored URL** — `product_customizable_options`, not `product-customizable-options`. request: | curl -X GET "http://localhost/api/shop/product_customizable_options/1" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK { "id": 1, "type": "field", "isRequired": 1, "sortOrder": 0, "product": "/api/shop/products/14", "prices": ["/api/shop/product_customizable_option_prices/1"], "translation": "/api/shop/product_customizable_option_translations/1", "translations": [ "/api/shop/product_customizable_option_translations/1", "/api/shop/product_customizable_option_translations/4" ] } - id: list-customizable-option-prices title: Customizable Option Prices description: | Sub-resource of customizable options — pricing rows referenced by the option's `prices[]` IRI array. request: | curl -X GET "http://localhost/api/shop/product_customizable_option_prices?per_page=2" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK X-Total-Count: 7 X-Page: 1 X-Per-Page: 2 X-Total-Pages: 4 [ { "id": 1, "price": 5, "priceType": "fixed", "customizableOption": "/api/shop/product_customizable_options/1" } ] - id: list-customizable-option-translations title: Customizable Option Translations description: | Locale-specific labels for a customizable option. Reached by following the `translation` / `translations[]` IRIs on the parent option. request: | curl -X GET "http://localhost/api/shop/product_customizable_option_translations?per_page=2" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK [ { "id": 1, "locale": "en", "label": "Engraving", "customizableOption": "/api/shop/product_customizable_options/1" } ] - id: list-attribute-values title: Attribute Values — List for a product description: | The raw EAV value rows for a product (one per attribute × locale × channel). The Single Product response surfaces typed fields (`name`, `description`, `price`, `color`, `size`, `brand`, …) instead — only call this endpoint when you need the underlying EAV record. request: | curl -X GET "http://localhost/api/shop/products/2/attribute-values" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK [ { "id": 25, "locale": "en", "channel": "default", "textValue": "Step into timeless style…", "value": "Step into timeless style…", "uniqueId": "default|en|2|9", "attribute": "/api/shop/attributes/9", "product": "/api/shop/products/2" } ] --- # Product Sub-Resources (`Product` tag) Sub-resources of a product — gallery assets, tier prices, customizable options and their pricing/translations, raw EAV values. Most of these are already inlined on the [Single Product](/api/rest-api/shop/products/get-product) response; the dedicated endpoints listed here exist when you need just one slice without re-fetching the whole PDP. > ⚠️ **Naming pattern alert**: > - `*-images`, `*-videos`, `*-bundle-option-products`, `*-downloadable-links`, `*-downloadable-samples` use **dashed** root URLs. > - `product_customizable_options`, `product_customizable_option_prices`, `product_customizable_option_translations`, `product_bundle_options`, `product_grouped_products` use **underscored** root URLs (API Platform's default snake-case route). > > Always copy the URL from a parent resource's IRI when in doubt — the IRIs are authoritative. ## Endpoints in this group ### Images (`type = images`) | Method | Path | Purpose | |--------|--------------------------------------------|------------------------------------------------| | GET | `/api/shop/products/{productId}/images` | All images for a product (nested) | | GET | `/api/shop/product-images` | Paginated, every image across the catalog (root) | | GET | `/api/shop/product-images/{id}` | Single image by global ID | ### Videos (`type = videos`) | Method | Path | Purpose | |--------|--------------------------------------------|------------------------------------------------| | GET | `/api/shop/products/{productId}/videos` | All videos for a product (nested) | | GET | `/api/shop/product-videos` | Paginated root collection | | GET | `/api/shop/product-videos/{id}` | Single video by global ID | Same response shape as Images, with `type: "videos"`. ### Customer Group Prices (tier prices) | Method | Path | Purpose | |--------|-----------------------------------------------------------------|----------------------------------------| | GET | `/api/shop/products/{productId}/customer-group-prices` | Tier prices for a product (nested) | | GET | `/api/shop/products/{productId}/customer-group-prices/{id}` | Single tier-price row (nested) | | GET | `/api/shop/customer-group-prices/{id}` | Single tier-price row by global ID | > The root collection at `/api/shop/customer-group-prices` is **not exposed** — it's an admin-only concept. Always scope by product or fetch by ID. ### Customizable Options + Prices + Translations | Method | Path | Purpose | |--------|-----------------------------------------------------------------|----------------------------------------| | GET | `/api/shop/products/{productId}/customizable-options` | All custom inputs for a product (nested) | | GET | `/api/shop/product_customizable_options/{id}` | Single option by ID | | GET | `/api/shop/product_customizable_option_prices` | Paginated pricing rows | | GET | `/api/shop/product_customizable_option_prices/{id}` | Single pricing row | | GET | `/api/shop/product_customizable_option_translations` | Paginated locale labels | | GET | `/api/shop/product_customizable_option_translations/{id}` | Single label | ### Attribute Values (raw EAV — sub-resource only) | Method | Path | Purpose | |--------|---------------------------------------------------------|----------------------------------------| | GET | `/api/shop/products/{productId}/attribute-values` | Every attribute value row for a product | > No root collection. The single-row endpoint isn't exposed either — fetch the parent product instead. ## Response field reference ### Image / Video | Field | Type | Description | |---------------|---------|------------------------------------------------------------| | `id` | integer | Asset primary key | | `type` | string | `images` or `videos` | | `path` | string | Storage path (relative) | | `productId` | integer | Owning product | | `position` | integer | Display order | | `publicPath` | string | Fully-qualified URL | ### Customer Group Price | Field | Type | Description | |-------------------|---------|--------------------------------------------------------------------------| | `id` | integer | Tier-price row ID | | `qty` | number | Threshold quantity for this tier | | `valueType` | string | `fixed` (replaces price) or `discount` (percentage off) | | `value` | number | Tier value — interpreted by `valueType` | | `customerGroupId` | integer | Customer group this tier applies to | | `uniqueId` | string | Internal lookup key (`<group>\|<product>\|<qty>`) | | `productId` | integer | Owning product | ### Customizable Option | Field | Type | Description | |-----------------|-----------------|--------------------------------------------------------------------------| | `id` | integer | Option primary key | | `type` | string | `field`, `area`, `drop_down`, `radio`, `checkbox`, `multiple`, `date`, `date_time`, `time`, `file` | | `isRequired` | boolean (0/1) | Whether the customer must fill the option | | `sortOrder` | integer | Display order on the PDP | | `product` | string (IRI) | Parent product | | `prices` | array (IRI) | One IRI per pricing row — dereference for the actual prices | | `translation` | string (IRI) | Translation for the request locale | | `translations` | array (IRI) | All locale translations | ### Customizable Option Price | Field | Type | Description | |----------------------|-----------------|------------------------------------------------------------| | `id` | integer | Pricing row primary key | | `price` | number | Price increment for this option | | `priceType` | string | `fixed` or `percent` | | `customizableOption` | string (IRI) | Parent customizable option | ### Customizable Option Translation | Field | Type | Description | |----------------------|-----------------|------------------------------------------------------------| | `id` | integer | Translation row primary key | | `locale` | string | Locale code | | `label` | string | Localized option label shown to the customer | | `customizableOption` | string (IRI) | Parent customizable option | ### Attribute Value | Field | Type | Description | |--------------|-----------------|------------------------------------------------------------------------------| | `id` | integer | EAV row primary key | | `locale` | string | Locale this value applies to | | `channel` | string | Channel code | | `textValue` / `integerValue` / `decimalValue` / `booleanValue` / `datetimeValue` / `dateValue` / `jsonValue` | mixed | The typed storage column — only one is populated per row, depending on the attribute's `columnName` | | `value` | mixed | Convenience accessor — same as the populated typed column | | `uniqueId` | string | `<channel>\|<locale>\|<product>\|<attribute>` — internal index | | `attribute` | string (IRI) | Parent attribute | | `product` | string (IRI) | Parent product | ## Use Cases - **Refresh the gallery** without re-fetching the entire PDP after the customer changes a configurable variant — `GET /api/shop/products/{variantId}/images`. - **Audit assets** across the catalog from an admin tool — `GET /api/shop/product-images?per_page=50` and walk the pagination. - **Display tier-price tables** on a B2B PDP — `GET /api/shop/products/{id}/customer-group-prices`. - **Render a customizable-option editor** — list the option from the PDP, then dereference each `prices[]` IRI for the per-option upcharge. - **Inspect the raw EAV record** during a data-quality audit — `GET /api/shop/products/{id}/attribute-values` shows exactly what's stored on `product_attribute_values`. ## Related Resources - [Single Product](/api/rest-api/shop/products/get-product) — embeds `images`, `videos`, `customizableOptions`, … inline - [Product Type Sub-Resources](/api/rest-api/shop/products/product-type-subresources) - [Introduction → IRIs & HATEOAS](/api/rest-api/introduction#iris-hateoas) --- # Product Type Sub-Resources (`Product Types` tag) URL: /api/rest-api/shop/products/product-type-subresources --- outline: false examples: - id: list-variants title: Variants — for a configurable product description: | List the child variant products of a configurable parent. Each variant is a `simple` product with the same card-level fields as the listing endpoint. request: | curl -X GET "http://localhost/api/shop/products/23/variants" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK [ { "id": 24, "sku": "LUGGAGE-RED", "type": "simple", "name": "Luggage bags-Red", "price": 100, "formattedPrice": "$100.00", "...": "..." } ] commonErrors: - error: 404 Not Found cause: Parent product `{productId}` doesn't exist or isn't configurable solution: Use `GET /api/shop/products?type=configurable` to find configurable products. - id: list-booking-products title: Booking Products — config for a product description: | Booking-specific configuration row(s) for a `booking`-type product. The `type` field on each row tells you which slot helper drives availability — pair it with [Booking Slots](/api/rest-api/shop/products/get-booking-slots) for a date. request: | curl -X GET "http://localhost/api/shop/products/2507/booking-products" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK [ { "id": 1, "type": "default", "qty": 150, "location": "Noida, Uttar Pradesh", "showLocation": 0, "availableFrom": "2026-04-06T12:00:00+05:30", "availableTo": "2026-12-31T12:00:00+05:30", "createdAt": "2026-04-03T00:24:30+05:30", "updatedAt": "2026-04-06T21:09:47+05:30", "slots": { "bookingType": "one", "sameSlotAllDays": null, "slots": [ { "id": "1", "from": "12:00", "to": "18:00", "from_day": "1", "to_day": "1" }, { "id": "2", "from": "12:00", "to": "18:00", "from_day": "2", "to_day": "2" } ] } } ] - id: get-booking-product title: Booking Products — single by ID description: | Fetch a single booking config row by its global ID (the value found in `bookingProducts[].id` on the parent product). Includes IRI references to the type-specific slot config. request: | curl -X GET "http://localhost/api/shop/booking-products/1" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK { "id": 1, "type": "default", "qty": 150, "location": "Noida, Uttar Pradesh", "showLocation": 0, "availableEveryWeek": null, "availableFrom": "2026-04-06T12:00:00+05:30", "availableTo": "2026-12-31T12:00:00+05:30", "slots": { "bookingType": "one", "slots": [ "..." ] }, "defaultSlot": "/api/shop/booking_product_default_slots/1", "appointmentSlot": null, "rentalSlot": null, "tableSlot": null, "eventTickets": [] } - id: get-booking-default-slot title: Booking Slot Config — `default` type description: | Static slot configuration for a `default`-type booking product. The `slots` field is a **JSON string** — parse it client-side. For runtime availability on a specific date, use [Booking Slots](/api/rest-api/shop/products/get-booking-slots). request: | curl -X GET "http://localhost/api/shop/booking_product_default_slots/1" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK { "id": 1, "bookingType": "one", "duration": null, "breakTime": null, "slots": "[{\"id\": \"1\", \"to\": \"18:00\", \"from\": \"12:00\", \"to_day\": \"1\", \"from_day\": \"1\"}]" } - id: get-booking-event-ticket title: Booking Slot Config — `event` ticket description: | Single ticket row for an `event`-type booking product. Each ticket is its own purchasable line with its own price, qty, and locale-specific `name` / `description`. request: | curl -X GET "http://localhost/api/shop/booking_product_event_tickets/7" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK { "id": 7, "bookingProductId": 2, "price": 120, "qty": 1500, "specialPrice": 115, "specialPriceFrom": "2026-04-06 12:00:00", "specialPriceTo": "2026-04-30 12:00:00", "formattedPrice": "$120.00", "formattedSpecialPrice": "$115.00", "translations": ["/api/booking_product_event_ticket_translations/7"], "translation": "/api/booking_product_event_ticket_translations/7" } - id: list-bundle-options title: Bundle Options — for a bundle product description: | Decision groups for a `bundle`-type product. Each option has its member products as IRIs (under `bundleOptionProducts[]`) — dereference for full member details. request: | curl -X GET "http://localhost/api/shop/products/2517/bundle-options" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK [ { "id": 1, "type": "radio", "isRequired": 1, "sortOrder": 0, "product": "/api/shop/products/2517", "bundleOptionProducts": ["/api/shop/product-bundle-option-products/1"], "translation": "/api/shop/product_bundle_option_translations/1", "translations": [ "/api/shop/product_bundle_option_translations/5", "/api/shop/product_bundle_option_translations/1" ] } ] - id: list-bundle-option-products title: Bundle Option Products description: | Member products inside a bundle option (the actual SKUs the customer can pick). Each row links back to its parent `bundleOption` and its `product`. request: | curl -X GET "http://localhost/api/shop/product-bundle-option-products?per_page=2" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK X-Total-Count: 12 X-Page: 1 X-Per-Page: 2 X-Total-Pages: 6 [ { "id": 1, "qty": 1, "isUserDefined": 1, "isDefault": 1, "sortOrder": 0, "bundleOption": "/api/shop/product_bundle_options/1", "product": "/api/shop/products/2512" } ] - id: list-grouped-products title: Grouped Products — members of a grouped product description: | Associated products inside a `grouped`-type product. The customer can add any subset to the cart with their own quantities. request: | curl -X GET "http://localhost/api/shop/products/2516/grouped-products" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK [ { "id": 1, "qty": 1, "sortOrder": 0, "product": "/api/shop/products/2516", "associatedProduct": "/api/shop/products/2512" }, { "id": 2, "qty": 1, "sortOrder": 1, "product": "/api/shop/products/2516", "associatedProduct": "/api/shop/products/2514" } ] - id: list-downloadable-links title: Downloadable Links — for a downloadable product description: | Purchasable downloadable links attached to a `downloadable`-type product. Each link carries its own price, sample file, and download limit. request: | curl -X GET "http://localhost/api/shop/products/2506/downloadable-links" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK [ { "id": 2, "url": "https://example.com/textbook.pdf", "type": "url", "price": 69, "sampleFile": "product_downloadable_links/2506/...pdf", "sampleFileName": "Personal Finance.pdf", "sampleType": "file", "downloads": 10, "sortOrder": 0, "createdAt": "2026-04-03T00:14:55+05:30", "updatedAt": "2026-04-03T00:14:55+05:30", "formattedPrice": "$69.00", "fileUrl": "http://localhost/storage/", "sampleFileUrl": "http://localhost/api/downloadable/download-sample/link/2", "product": "/api/shop/products/2506", "translation": "/api/shop/product_downloadable_link_translations/2", "translations": [ "/api/shop/product_downloadable_link_translations/2", "/api/shop/product_downloadable_link_translations/3" ] } ] - id: list-downloadable-samples title: Downloadable Samples — for a downloadable product description: | Free preview files attached to a downloadable product. Listed separately from the paid links so the customer can preview before buying. request: | curl -X GET "http://localhost/api/shop/products/2506/downloadable-samples" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK [ { "id": 1, "file": "product_downloadable_links/2506/....pdf", "fileName": "document.pdf", "type": "file", "sortOrder": 0, "createdAt": "2026-04-03T00:14:55+05:30", "updatedAt": "2026-04-03T00:14:55+05:30", "fileUrl": "http://localhost/api/downloadable/download-sample/sample/1", "product": "/api/shop/products/2506", "translation": "/api/shop/product_downloadable_sample_translations/1", "translations": ["/api/shop/product_downloadable_sample_translations/1"] } ] --- # Product Type Sub-Resources (`Product Types` tag) The endpoints in this group expose the type-specific configuration of products that aren't just `simple`: configurable variants, booking slots, bundle option groups, grouped associations, downloadable links and samples. Most of this data is also inlined on [Single Product](/api/rest-api/shop/products/get-product) — these dedicated endpoints exist for clients that need just one slice. > ⚠️ **Naming pattern alert**: > - `*-bundle-option-products`, `*-downloadable-links`, `*-downloadable-samples`, `booking-products`, `booking-slots` use **dashed** root URLs. > - `product_bundle_options`, `product_grouped_products`, `booking_product_default_slots`, `booking_product_appointment_slots`, `booking_product_rental_slots`, `booking_product_event_tickets`, `booking_product_table_slots`, plus all `*_translations` endpoints use **underscored** root URLs. > - When in doubt, copy the URL from a parent resource's IRI — they're authoritative. For runtime slot availability (a date-specific list of bookable times) see the dedicated [Booking Slots](/api/rest-api/shop/products/get-booking-slots) page — it's separate from the static **slot config** endpoints documented here. ## Endpoints in this group ### Variants (configurable type) | Method | Path | Purpose | |--------|---------------------------------------------------|----------------------------------------| | GET | `/api/shop/products/{productId}/variants` | Child variants of a configurable parent | ### Booking Products (booking type — slot config) | Method | Path | Purpose | |--------|-----------------------------------------------------|------------------------------------------| | GET | `/api/shop/products/{productId}/booking-products` | Booking config rows for a product | | GET | `/api/shop/booking-products/{id}` | Single booking config (with slot IRIs) | ### Booking Slot Configs (one model per type) These are the **static configuration rows** referenced by the `defaultSlot` / `appointmentSlot` / `rentalSlot` / `tableSlot` / `eventTickets[]` IRIs on a `BookingProduct`. They describe the schedule template — not the per-date runtime slots. | Method | Path | For booking type | |--------|--------------------------------------------------------------|------------------| | GET | `/api/shop/booking_product_default_slots` | `default` | | GET | `/api/shop/booking_product_default_slots/{id}` | `default` | | GET | `/api/shop/booking_product_appointment_slots` | `appointment` | | GET | `/api/shop/booking_product_appointment_slots/{id}` | `appointment` | | GET | `/api/shop/booking_product_rental_slots` | `rental` | | GET | `/api/shop/booking_product_rental_slots/{id}` | `rental` | | GET | `/api/shop/booking_product_table_slots` | `table` | | GET | `/api/shop/booking_product_table_slots/{id}` | `table` | | GET | `/api/shop/booking_product_event_tickets` | `event` (tickets) | | GET | `/api/shop/booking_product_event_tickets/{id}` | `event` (tickets) | ### Bundle Options + Member Products + Translations (bundle type) | Method | Path | Purpose | |--------|-------------------------------------------------------|----------------------------------------| | GET | `/api/shop/products/{productId}/bundle-options` | Decision groups for a bundle (nested) | | GET | `/api/shop/product_bundle_options/{id}` | Single bundle option | | GET | `/api/shop/product-bundle-option-products` | Paginated list of every member-row | | GET | `/api/shop/product-bundle-option-products/{id}` | Single member-row by global ID | | GET | `/api/shop/product_bundle_option_translations` | Paginated locale labels | | GET | `/api/shop/product_bundle_option_translations/{id}` | Single label | ### Grouped Products (grouped type) | Method | Path | Purpose | |--------|-------------------------------------------------------|----------------------------------------| | GET | `/api/shop/products/{productId}/grouped-products` | Members of a grouped product (nested) | | GET | `/api/shop/product_grouped_products/{id}` | Single grouped-product association | ### Downloadable Links + Samples + Translations (downloadable type) | Method | Path | Purpose | |--------|---------------------------------------------------------------|----------------------------------------| | GET | `/api/shop/products/{productId}/downloadable-links` | Purchasable links for a product | | GET | `/api/shop/product-downloadable-links` | Paginated root collection | | GET | `/api/shop/product-downloadable-links/{id}` | Single link | | GET | `/api/shop/product_downloadable_link_translations` | Paginated locale labels | | GET | `/api/shop/product_downloadable_link_translations/{id}` | Single label | | GET | `/api/shop/products/{productId}/downloadable-samples` | Preview samples for a product | | GET | `/api/shop/product-downloadable-samples` | Paginated root collection | | GET | `/api/shop/product-downloadable-samples/{id}` | Single sample | | GET | `/api/shop/product_downloadable_sample_translations` | Paginated locale labels | | GET | `/api/shop/product_downloadable_sample_translations/{id}` | Single label | ## Response field reference (key shapes) ### Variant Same shape as a card-level [Product](/api/rest-api/shop/products/get-products#card-level-fields) — variants are themselves `simple` products. ### Booking Product | Field | Type | Description | |----------------------|------------------|------------------------------------------------------------------------| | `id` | integer | Booking config row ID — pass to `/booking-slots?id=…` for runtime availability | | `type` | string | `default`, `appointment`, `rental`, `event`, `table` | | `qty` | integer | Total capacity (where applicable) | | `location` | string | Physical location for in-person bookings | | `showLocation` | boolean (0/1) | Whether to show `location` to the customer | | `availableFrom` / `availableTo` | string (ISO) | Allowed booking window | | `slots` | object \| null | Inline preview of the schedule template (also reachable via the type-specific IRI) | | `defaultSlot` / `appointmentSlot` / `rentalSlot` / `tableSlot` | string (IRI) \| null | Type-specific config IRI — only one is non-null per row | | `eventTickets` | array | For `event` type — list of ticket rows | ### Booking Slot Config (default / appointment / rental / table) | Field | Type | Description | |----------------|-----------------|--------------------------------------------------------------------------| | `id` | integer | Config primary key | | `bookingType` | string | `one` (single slot per day), `many` (multiple slots per day) | | `duration` | integer \| null | Slot length in minutes | | `breakTime` | integer \| null | Gap between slots in minutes | | `slots` | string (JSON) | Schedule template. **JSON-encoded string** — `JSON.parse()` on the client | ### Booking Event Ticket | Field | Type | Description | |--------------------|-----------------|----------------------------------------------------------| | `id` | integer | Ticket primary key | | `bookingProductId` | integer | Owning booking product | | `price` | number | Ticket price | | `qty` | integer | Ticket inventory | | `specialPrice` | number \| null | Sale price | | `specialPriceFrom` / `specialPriceTo` | string \| null | Sale window | | `formattedPrice` / `formattedSpecialPrice` | string \| null | Currency-formatted strings | | `translation` / `translations` | IRI / array | Locale-specific `name` and `description` | ### Bundle Option | Field | Type | Description | |--------------------------|-----------------|----------------------------------------------------------| | `id` | integer | Option primary key | | `type` | string | `radio`, `checkbox`, `select`, `multi` | | `isRequired` | boolean (0/1) | Whether the customer must pick at least one member | | `sortOrder` | integer | Display order | | `product` | string (IRI) | Parent bundle product | | `bundleOptionProducts` | array (IRI) | Member rows — dereference for actual SKUs | | `translation` | string (IRI) | Locale-specific label | | `translations` | array (IRI) | All locale labels | ### Bundle Option Product | Field | Type | Description | |------------------|-----------------|----------------------------------------------------------| | `id` | integer | Member row primary key | | `qty` | integer | Default quantity contributed when this member is selected | | `isUserDefined` | boolean (0/1) | Whether the customer can change `qty` | | `isDefault` | boolean (0/1) | Whether the member is pre-selected on the PDP | | `sortOrder` | integer | Display order | | `bundleOption` | string (IRI) | Parent bundle option | | `product` | string (IRI) | Member SKU — dereference for full product details | ### Grouped Product | Field | Type | Description | |---------------------|-----------------|----------------------------------------------------------| | `id` | integer | Association primary key | | `qty` | integer | Default quantity | | `sortOrder` | integer | Display order | | `product` | string (IRI) | Parent grouped product | | `associatedProduct` | string (IRI) | The simple product associated with the group | ### Downloadable Link | Field | Type | Description | |----------------------|-----------------|--------------------------------------------------------------| | `id` | integer | Link primary key | | `url` | string \| null | Externally-hosted file URL (`type = url`) | | `type` | string | `file` (uploaded) or `url` (external) | | `price` | number | Per-link price | | `formattedPrice` | string | Currency-formatted | | `sampleFile` | string \| null | Sample file storage path | | `sampleFileName` | string \| null | Original filename | | `sampleType` | string \| null | `file` or `url` | | `downloads` | integer | Download cap per purchase | | `sortOrder` | integer | Display order | | `fileUrl` | string | Public URL for the actual purchasable file | | `sampleFileUrl` | string | Public URL for the preview | | `product` | string (IRI) | Parent product | | `translation` | string (IRI) | Locale-specific name + description | | `translations` | array (IRI) | All locale translations | ### Downloadable Sample | Field | Type | Description | |-----------------|-----------------|--------------------------------------------------------------| | `id` | integer | Sample primary key | | `file` | string | Storage path | | `fileName` | string | Original filename | | `type` | string | `file` or `url` | | `sortOrder` | integer | Display order | | `fileUrl` | string | Public URL for the preview | | `product` | string (IRI) | Parent product | | `translation` / `translations` | IRI / array | Locale-specific labels | ## Use Cases - **Variant picker on a configurable PDP** — the parent's `superAttributes[]` describes the dimensions; `GET /products/{id}/variants` returns the inventory of combinations. - **Booking flow** — fetch `/products/{id}/booking-products` for the schedule template, then `/booking-slots?id=…&date=…` for the actual selectable times on a date. - **Event ticket purchase** — read `eventTickets[]` from the booking-product response; each row is its own purchasable line with its own qty and price. - **Bundle builder** — walk `bundleOptions[]` for the groups, dereference each `bundleOptionProducts[]` IRI for member details. - **Grouped product cart row** — list members from `/products/{id}/grouped-products`, render a qty input per `associatedProduct`. - **Downloadable PDP** — paid `downloadableLinks` and free `downloadableSamples` are separate lists; show the sample download button next to the paid link. ## Related Resources - [Single Product](/api/rest-api/shop/products/get-product) — embeds `variants`, `bookingProducts`, `bundleOptions`, `groupedProducts`, `downloadableLinks`, `downloadableSamples` inline - [Booking Slots](/api/rest-api/shop/products/get-booking-slots) — date-specific runtime availability - [Product Sub-Resources](/api/rest-api/shop/products/product-subresources) — the `Product` tag (images, videos, customer-group prices, customizable options, attribute values) - [Introduction → IRIs & HATEOAS](/api/rest-api/introduction#iris-hateoas) --- # Search Products URL: /api/rest-api/shop/products/search-product --- outline: false examples: - id: search-by-keyword title: Search by keyword description: Use `?query=` to match against product SKU and name. request: | curl -X GET "http://localhost/api/shop/products?query=hoodie&per_page=2" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK X-Total-Count: 3 X-Page: 1 X-Per-Page: 2 X-Total-Pages: 2 [ { "id": 1, "sku": "COASTALBREEZEMENSHOODIE", "type": "simple", "name": "Coastal Breeze Men's Blue Zipper Hoodie", "price": 100, "...": "..." } ] commonErrors: - error: 401 Unauthorized cause: Missing or invalid `X-STOREFRONT-KEY` solution: Send a valid storefront API key. - id: filter-by-type title: Filter by product type description: Restrict to a single product type (e.g. all configurable products). request: | curl -X GET "http://localhost/api/shop/products?type=configurable&per_page=10" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK X-Total-Count: 4 X-Page: 1 X-Per-Page: 10 X-Total-Pages: 1 [ { "id": 23, "sku": "NGJGAJSDGJ123123GJGJ", "type": "configurable", "name": "Luggage bags", "price": 0, "...": "..." } ] - id: filter-by-category title: Filter by category description: Scope the listing to a single category. Pass either `category_id` (snake_case) or `categoryId` (camelCase). request: | curl -X GET "http://localhost/api/shop/products?category_id=23&per_page=2" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK X-Total-Count: 14 X-Page: 1 X-Per-Page: 2 X-Total-Pages: 7 [ { "id": 279, "sku": "sofa2", "type": "simple", "name": "Imperial Velvet Comfort Sofa", "price": 4000, "...": "..." } ] - id: filter-by-price title: Filter by price range description: Use `?price=from,to` (compound) or `?price_from=` + `?price_to=` (separate). Both are equivalent. request: | curl -X GET "http://localhost/api/shop/products?price=10,200&per_page=2" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK X-Total-Count: 25 X-Page: 1 X-Per-Page: 2 X-Total-Pages: 13 [ { "id": 1, "sku": "COASTALBREEZEMENSHOODIE", "type": "simple", "name": "Coastal Breeze Men's Blue Zipper Hoodie", "price": 100, "...": "..." }, { "id": 2, "sku": "PUREWHTSNEAK2023", "type": "simple", "name": "PureStride Men's Classic White Sneakers", "price": 189, "...": "..." } ] - id: filter-by-attribute title: Filter by attribute (color / size / brand / …) description: | Pass any **filterable attribute code** as a query parameter. The value is the option ID. For multi-select, comma-separate the IDs. Discover the available attribute codes and their option IDs via [`GET /api/shop/attributes`](/api/rest-api/shop/attributes/get-attributes). request: | curl -X GET "http://localhost/api/shop/products?color=3&per_page=2" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK X-Total-Count: 13 X-Page: 1 X-Per-Page: 2 X-Total-Pages: 7 [ { "id": 12, "sku": "...", "type": "simple", "name": "...", "price": 250, "...": "..." } ] - id: sort-products title: Sort description: | Use `?sort=` with a compound token like `name-asc`, `price-desc`, `created_at-desc`. Or pass `?sort=name&order=desc` for the bare-key + direction form. request: | curl -X GET "http://localhost/api/shop/products?sort=name-asc&per_page=2" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK [ { "id": 11, "name": "A Best Shooes", "price": 1, "...": "..." }, { "id": 13, "name": "Acme Baby Cap", "price": 25, "...": "..." } ] - id: filter-new-featured title: Filter new / featured description: Restrict to "new" or "featured" flagged products. Combine with other filters. request: | curl -X GET "http://localhost/api/shop/products?new=1&featured=1&per_page=2" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK [ { "id": 1, "name": "Coastal Breeze…", "new": true, "featured": true, "price": 100, "...": "..." }, { "id": 2, "name": "PureStride…", "new": true, "featured": true, "price": 189, "...": "..." } ] - id: combined-filters title: Combine filters description: Filters are AND-combined. The example below restricts to color = 3, in category 5, sorted by price descending. request: | curl -X GET "http://localhost/api/shop/products?category_id=5&color=3&sort=price-desc&per_page=20" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK X-Total-Count: 6 X-Page: 1 X-Per-Page: 20 X-Total-Pages: 1 [ … ] --- # Search Products The list endpoint at `GET /api/shop/products` doubles as the **search & filter** endpoint. Every storefront facet — keyword search, category, price range, attribute filters, sort, "new" / "featured" — is just a query parameter. This page is the complete reference for those parameters. ## Endpoint ``` GET /api/shop/products ``` Same URL as [Products](/api/rest-api/shop/products/get-products); the only difference is which query parameters you send. ## Reserved parameters These names are interpreted by the search/sort/pagination layer — never as attribute filters: | Parameter | Type | Default | Description | |---------------------|---------|---------|--------------------------------------------------------------------------------------------------------------| | `query` | string | — | Search term. Matches against product SKU and product name. | | `sort` | string | — | Sort token. Either compound (`name-asc`, `price-desc`) or a bare key paired with `order` (`sort=name&order=desc`). See [Sort tokens](#sort-tokens) below. | | `order` | string | `asc` | Direction when `sort` is a bare key. Ignored if `sort` already has a `-asc` / `-desc` suffix. | | `page` | integer | `1` | 1-based page number. | | `per_page` | integer | `30` | Items per page. Max **50**. | | `locale` | string | — | Locale override (alternative to the `X-Locale` header). | | `channel` | string | — | Channel override (alternative to the `X-Channel` header). | | `filter` | string | — | JSON filter object — GraphQL parity. Example: `{"color":{"match":"3","match_type":"PARTIAL"}}`. Most clients prefer the simpler `?<attribute>=<id>` shorthand. | | `type` | string | — | Product type. One of `simple`, `configurable`, `bundle`, `grouped`, `virtual`, `downloadable`, `booking`. | | `category_id` | integer | — | Filter by category ID. `categoryId` (camelCase) is also accepted. | | `price` | string | — | Compound price range `from,to` (e.g. `10,200`). | | `price_from` | number | — | Minimum price (inclusive). Equivalent to the lower bound of `price`. | | `price_to` | number | — | Maximum price (inclusive). Equivalent to the upper bound of `price`. | | `new` | boolean | — | `1` to restrict to products with the "new" flag set. | | `featured` | boolean | — | `1` to restrict to products with the "featured" flag set. | ## Attribute filters (anything else) > **Any query parameter not listed above is treated as a filterable attribute.** No schema changes are required when admins add new filterable attributes — the URL just works. | Pattern | Example | Effect | |----------------------------|------------------------------------|--------------------------------------------------------------------------------------------------------------| | `?<code>=<option_id>` | `?color=3` | Single option ID match | | `?<code>=id1,id2,…` | `?size=4,5,6` | Multi-option (OR) match | | `?brand=N`, `?material=N` | `?material=12` | Same shape — works for any filterable attribute | | `?filter[color]=3&filter[size]=5` | (Swagger UI form) | API Platform's bracket-syntax form. Same semantics as the bare-key form. | To list which attributes are filterable and what their option IDs are, fetch [`GET /api/shop/attributes`](/api/rest-api/shop/attributes/get-attributes) and look for `isFilterable: 1`. Each attribute's `options[]` array has the IDs. ## Sort tokens The compound form (`<key>-<dir>`) is recommended: | Token | Sort by | |--------------------|--------------------------------| | `name-asc` | Product name, A→Z | | `name-desc` | Product name, Z→A | | `price-asc` | Price, low→high | | `price-desc` | Price, high→low | | `created_at-asc` | Oldest first | | `created_at-desc` | Newest first | | `updated_at-asc` | Oldest update first | | `updated_at-desc` | Most recently updated first | | `id-asc` | Ascending ID (catalog order) | | `id-desc` | Descending ID | Alternative form: `?sort=name&order=desc`. Pick one; mixing them isn't useful. ## Combining filters All filters are **AND-combined**. There is no OR across different filters — the only multi-value form is comma-separated values within a single attribute (`?size=4,5,6`). ``` GET /api/shop/products? category_id=5 &color=3 &size=4,5 &price=10,200 &new=1 &sort=price-desc &per_page=24 ``` ## Response `200 OK` — JSON array of card-level product objects (same shape as [Products](/api/rest-api/shop/products/get-products#card-level-fields)). Pagination headers always emitted. > An empty filter result returns an empty array `[]` with `200 OK` and `X-Total-Count: 0`. It does **not** 404. ## Common pitfalls - Sending `itemsPerPage` instead of `per_page` — the legacy API Platform name is **not accepted**. - Filtering by an attribute that isn't flagged `isFilterable=1` — the parameter is silently ignored. - Filtering by an attribute *code* but passing an option *label* (e.g. `?color=Red`) — values are matched against option **IDs**, not labels. - Combining `price` + `price_from`/`price_to` — pick one. The compound form wins if both are present. ## Related Resources - [Products](/api/rest-api/shop/products/get-products) — same endpoint with no filters - [Single Product](/api/rest-api/shop/products/get-product) — fetch one product after the user picks a result - [Categories](/api/rest-api/shop/categories/get-categories) — discover category IDs for `?category_id=N` - [Attributes](/api/rest-api/shop/attributes/get-attributes) — discover filterable attribute codes - [Pagination](/api/rest-api/introduction#pagination) --- # Get Product Details URL: /api/rest-api/shop/queries/get-product --- outline: false examples: - id: get-product-by-id title: Get Product Details description: Retrieve detailed information for a specific product. request: | GET /api/products/1 headers: Content-Type: application/json response: | { "@context": "/api/contexts/Product", "@id": "/api/products/1", "@type": "Product", "id": 1, "name": "Premium T-Shirt", "slug": "premium-t-shirt", "description": "Crafted from 100% organic cotton with premium finish.", "short_description": "High quality cotton t-shirt", "sku": "TSHIRT-001", "price": 29.99, "original_price": 39.99, "cost": 10.00, "weight": 0.5, "status": 1, "type": "simple", "images": [ { "id": 10, "url": "https://cdn.example.com/products/tshirt-1.jpg", "alt": "Front view", "position": 0 } ], "attributes": [ { "id": 1, "code": "color", "label": "Color", "value": "Blue" }, { "id": 2, "code": "size", "label": "Size", "value": "M" } ], "categories": [ { "id": 3, "name": "Clothing", "slug": "clothing" } ], "created_at": "2024-01-10T10:00:00Z", "updated_at": "2024-01-20T10:00:00Z" } commonErrors: - error: 404 Not Found cause: Product with specified ID does not exist solution: Verify the product ID and try again - error: 410 Gone cause: Product has been permanently deleted solution: Use a valid product ID - id: get-product-by-slug title: Get Product by Slug description: Retrieve product details using URL slug instead of ID. request: | GET /api/products/premium-t-shirt headers: Content-Type: application/json response: | { "@context": "/api/contexts/Product", "@id": "/api/products/1", "@type": "Product", "id": 1, "name": "Premium T-Shirt", "slug": "premium-t-shirt", "sku": "TSHIRT-001", "price": 29.99, "status": 1 } commonErrors: - error: 404 Not Found cause: Product slug does not exist solution: Verify the slug and try again --- # Get Product Details Retrieve detailed information for a specific product including images, attributes, categories, and pricing. ## Endpoint ``` GET /api/products/{id|slug} ``` ## Path Parameters | Parameter | Type | Description | |-----------|------|-------------| | `id` | integer | Product ID | | `slug` | string | URL-friendly product slug | ## Response Fields | Field | Type | Description | |-------|------|-------------| | `id` | integer | Product ID | | `name` | string | Product name | | `slug` | string | URL-friendly slug | | `description` | string | Full product description | | `short_description` | string | Short product description | | `sku` | string | Stock keeping unit | | `price` | number | Selling price | | `original_price` | number | Original price before discount | | `cost` | number | Product cost | | `weight` | number | Product weight | | `status` | integer | Active status (1=active, 0=inactive) | | `type` | string | Product type (simple, configurable, bundle) | | `images` | array | Product images with URLs | | `attributes` | array | Product attributes with values | | `categories` | array | Associated categories | | `created_at` | string | Creation timestamp | | `updated_at` | string | Last update timestamp | ## Usage Examples :::examples-selector ## Related Resources - [List Products](/api/rest-api/shop/queries/get-products) - [Update Product](/api/rest-api/shop/mutations/update-product) - [Delete Product](/api/rest-api/shop/mutations/delete-product) --- # List Products URL: /api/rest-api/shop/queries/get-products --- outline: false examples: - id: list-products-paginated title: List Products with Pagination description: Retrieve paginated list of all products from the catalog. request: | GET /api/products?page=1&limit=10 headers: Content-Type: application/json response: | { "@context": "/api/contexts/Product", "@id": "/api/products", "@type": "hydra:Collection", "hydra:member": [ { "@id": "/api/products/1", "@type": "Product", "id": 1, "name": "Premium T-Shirt", "slug": "premium-t-shirt", "sku": "TSHIRT-001", "price": 29.99, "status": 1, "type": "simple" } ], "hydra:totalItems": 45, "hydra:view": { "@id": "/api/products?page=1", "hydra:first": "/api/products?page=1", "hydra:last": "/api/products?page=5", "hydra:next": "/api/products?page=2" } } commonErrors: - error: 400 Bad Request cause: Invalid page or limit parameter solution: Ensure page and limit are positive integers - error: 404 Not Found cause: Page number exceeds available pages solution: Reduce page number to valid range - id: list-products-by-category title: List Products by Category description: Retrieve products filtered by specific category. request: | GET /api/products?category_id=3&page=1&limit=10 headers: Content-Type: application/json response: | { "@context": "/api/contexts/Product", "@type": "hydra:Collection", "hydra:member": [ { "@id": "/api/products/1", "@type": "Product", "id": 1, "name": "Blue T-Shirt", "category_id": 3, "price": 29.99 } ], "hydra:totalItems": 12 } commonErrors: - error: 404 Not Found cause: Category does not exist solution: Verify category_id exists - id: search-products title: Search Products description: Search products by keyword. request: | GET /api/products?search=shirt&page=1&limit=10 headers: Content-Type: application/json response: | { "@context": "/api/contexts/Product", "@type": "hydra:Collection", "hydra:member": [ { "@id": "/api/products/1", "@type": "Product", "id": 1, "name": "Premium T-Shirt", "sku": "TSHIRT-001", "price": 29.99 } ], "hydra:totalItems": 5 } commonErrors: - error: 400 Bad Request cause: Search term is empty solution: Provide a valid search term - id: list-products-sorted title: List Products - Sorted description: Retrieve products sorted by specific field. request: | GET /api/products?sort=price&order=asc&page=1&limit=10 headers: Content-Type: application/json response: | { "@context": "/api/contexts/Product", "@type": "hydra:Collection", "hydra:member": [ { "@id": "/api/products/5", "@type": "Product", "id": 5, "name": "Budget T-Shirt", "price": 9.99 } ], "hydra:totalItems": 45 } commonErrors: - error: 400 Bad Request cause: Invalid sort field solution: Use valid fields like id, name, price, created_at --- # List Products Retrieve a paginated list of products from your store catalog with support for filtering, searching, and sorting. ## Endpoint ``` GET /api/products ``` ## Query Parameters | Parameter | Type | Description | |-----------|------|-------------| | `page` | integer | Page number (default: 1) | | `limit` | integer | Results per page (default: 15, max: 100) | | `sort` | string | Sort field (id, name, price, created_at) | | `order` | string | Sort order (asc, desc) | | `search` | string | Search term for product name/SKU | | `category_id` | integer | Filter by category ID | | `status` | integer | Filter by status (1=active, 0=inactive) | ## Response Fields | Field | Type | Description | |-------|------|-------------| | `id` | integer | Product ID | | `name` | string | Product name | | `slug` | string | URL-friendly slug | | `sku` | string | Product SKU code | | `price` | number | Product price | | `status` | integer | Active status | | `type` | string | Product type (simple, configurable, bundle) | | `created_at` | string | Creation timestamp | | `updated_at` | string | Last update timestamp | ## Usage Examples :::examples-selector ## Related Resources - [Get Product Details](/api/rest-api/shop/queries/get-product) - [Create Product](/api/rest-api/shop/mutations/create-product) - [Update Product](/api/rest-api/shop/mutations/update-product) - [Shop Resources](/api/rest-api/introduction#shop-resources) --- # Theme Customizations URL: /api/rest-api/shop/theme-customizations/get-theme-customizations --- outline: false examples: - id: list-theme-customizations title: List Theme Customizations description: Retrieve a paginated list of every theme customization (carousels, footer links, static content blocks, …). request: | curl -X GET "http://localhost/api/shop/theme-customizations?per_page=2" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK X-Total-Count: 12 X-Page: 1 X-Per-Page: 2 X-Total-Pages: 6 [ { "id": 1, "themeCode": "default", "channelId": 1, "type": "image_carousel", "name": "Image Carousel", "sortOrder": 1, "status": 1, "createdAt": "2024-04-16T21:44:15+05:30", "updatedAt": "2026-04-06T19:15:53+05:30", "translation": { "id": 1, "themeCustomizationId": 1, "locale": "en", "options": "{\"images\": [{\"link\": \"fashion\", \"image\": \"storage/theme/1/...webp\", \"title\": \"Fashion\"}, {\"link\": \"furniture\", \"image\": \"storage/theme/1/...webp\", \"title\": \"Furniture\"}]}" }, "translations": [ { "id": 1, "themeCustomizationId": 1, "locale": "en", "options": "{\"images\": [...]}" }, { "id": 19, "themeCustomizationId": 1, "locale": "AR", "options": "{\"images\": [...]}" } ] }, { "id": 3, "themeCode": "default", "channelId": 1, "type": "category_carousel", "name": "Categories Collections", "sortOrder": 3, "status": 1, "createdAt": "2024-04-16T21:44:15+05:30", "updatedAt": "2026-04-07T18:05:39+05:30", "translation": { "id": 3, "themeCustomizationId": 3, "locale": "en", "options": "{\"filters\": {\"sort\": \"asc\", \"limit\": \"10\", \"parent_id\": \"1\"}}" }, "translations": [ { "id": 3, "themeCustomizationId": 3, "locale": "en", "options": "{\"filters\": {...}}" } ] } ] commonErrors: - error: 401 Unauthorized cause: Missing or invalid `X-STOREFRONT-KEY` solution: Send a valid storefront API key. - error: 403 Forbidden cause: Storefront key inactive or rate-limited solution: Activate the key or wait for the rate limit window to reset. - id: list-by-type-footer-links title: Filter by type — `footer_links` description: Use `?type=footer_links` to fetch only the footer-links blocks. The same `?type=…` filter works for any of the supported types. request: | curl -X GET "http://localhost/api/shop/theme-customizations?type=footer_links" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK X-Total-Count: 1 X-Page: 1 X-Per-Page: 10 X-Total-Pages: 1 [ { "id": 11, "themeCode": "default", "channelId": 1, "type": "footer_links", "name": "Footer Links", "sortOrder": 11, "status": 1, "createdAt": "2024-04-16T21:44:15+05:30", "updatedAt": "2026-04-23T23:54:54+05:30", "translation": { "id": 11, "themeCustomizationId": 11, "locale": "en", "options": "{\"column_1\": [{\"url\": \"https://example.com/page/privacy-policy\", \"title\": \"Privacy policy\", \"sort_order\": \"3\"}, {\"url\": \"https://example.com/page/whats-new\", \"title\": \"What's New\", \"sort_order\": \"3\"}], \"column_2\": [{\"url\": \"https://example.com/page/about-us\", \"title\": \"About Us\", \"sort_order\": \"8\"}]}" }, "translations": [ { "id": 11, "themeCustomizationId": 11, "locale": "en", "options": "{\"column_1\": [...], \"column_2\": [...]}" }, { "id": 21, "themeCustomizationId": 11, "locale": "AR", "options": "{\"column_1\": [...], \"column_2\": [...]}" } ] } ] commonErrors: - error: 401 Unauthorized cause: Missing or invalid `X-STOREFRONT-KEY` solution: Send a valid storefront API key. - id: get-theme-customization title: Get Single Theme Customization description: Retrieve a single theme customization by ID with its inline `translation` and `translations[]`. request: | curl -X GET "http://localhost/api/shop/theme-customizations/11" \ -H "Accept: application/json" \ -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" response: | HTTP/1.1 200 OK { "id": 11, "themeCode": "default", "channelId": 1, "type": "footer_links", "name": "Footer Links", "sortOrder": 11, "status": 1, "createdAt": "2024-04-16T21:44:15+05:30", "updatedAt": "2026-04-23T23:54:54+05:30", "translation": { "id": 11, "themeCustomizationId": 11, "locale": "en", "options": "{\"column_1\": [{\"url\": \"https://example.com/page/privacy-policy\", \"title\": \"Privacy policy\", \"sort_order\": \"3\"}], \"column_2\": [{\"url\": \"https://example.com/page/about-us\", \"title\": \"About Us\", \"sort_order\": \"8\"}]}" }, "translations": [ { "id": 11, "themeCustomizationId": 11, "locale": "en", "options": "{\"column_1\": [...], \"column_2\": [...]}" }, { "id": 21, "themeCustomizationId": 11, "locale": "AR", "options": "{\"column_1\": [...], \"column_2\": [...]}" } ] } commonErrors: - error: 404 Not Found cause: No theme customization with the given `{id}` exists or its `status=0` solution: List active customizations via `GET /api/shop/theme-customizations` to discover valid IDs. - error: 401 Unauthorized cause: Missing or invalid `X-STOREFRONT-KEY` solution: Send a valid storefront API key. --- # Theme Customizations Theme customizations are the configurable content blocks that drive the storefront home page and footer — image carousels, category carousels, product carousels, static-content slots, the footer-links columns, and the "services" / "USP" strip. Each block is scoped to a `themeCode` + `channelId` pair, with locale-specific `options` JSON inside its translations. ## Endpoints | Method | Path | Purpose | |--------|--------------------------------------------|------------------------------------------| | GET | `/api/shop/theme-customizations` | Paginated list — supports `?type=` filter | | GET | `/api/shop/theme-customizations/{id}` | Single customization by ID | Use the example switcher above to flip between the unfiltered list, the `?type=footer_links` filter, and a single fetch. ## Request Headers | Header | Required | Description | |--------------------|----------|------------------------------------------| | `Accept` | Yes | `application/json` | | `X-STOREFRONT-KEY` | Yes | Storefront API key (`pk_storefront_…`) | | `X-Locale` | No | Override request locale — affects which row populates `translation`. Default: channel locale. | | `X-Channel` | No | Override channel scope | ## Query Parameters (collection only) | Parameter | Type | Default | Description | |-------------|---------|---------|--------------------------------------------------------------------------------------| | `page` | integer | 1 | Page number (1-based) | | `per_page` | integer | 10 | Items per page. Max **100** for this endpoint. | | `type` | string | — | Exact-match filter on the `type` field (see [Supported types](#supported-types)) | Pagination headers (`X-Total-Count`, `X-Page`, `X-Per-Page`, `X-Total-Pages`) are emitted on the collection. See [Pagination](/api/rest-api/introduction#pagination). ## Supported types The `type` filter accepts any of these exact strings: | Type | Purpose | |---------------------|--------------------------------------------------------------------| | `image_carousel` | Hero / banner image carousel on the home page | | `category_carousel` | Horizontally-scrolling list of categories | | `product_carousel` | Horizontally-scrolling list of products | | `static_content` | Free-form HTML / Markdown block | | `footer_links` | Footer link columns (privacy, terms, about, customer service, …) | | `services_content` | "Services" / USP strip (free shipping, 24×7 support, refund policy, …) | > Unsupported values are accepted by the filter but return an empty array (`[]`) — the validation isn't strict. ## Customization Object Fields Both endpoints return the same shape — the collection wraps an array of these objects, the single endpoint returns one. | Field | Type | Description | |----------------|-----------------------|------------------------------------------------------------------------------------------| | `id` | integer | Customization primary key | | `themeCode` | string | Theme this block belongs to (e.g. `default`) | | `channelId` | integer | Channel that owns this block — pair with `themeCode` | | `type` | string | One of the [supported types](#supported-types) | | `name` | string | Admin-facing name | | `sortOrder` | integer | Display order within the home page / footer | | `status` | boolean (0/1) | Whether the block is published — only published rows are returned | | `createdAt`, `updatedAt` | string (ISO-8601) | Timestamps | | `translation` | object \| null | Inline translation for the request locale: `{ id, themeCustomizationId, locale, options }` | | `translations` | array of objects | All locale translations as **inline objects** (not IRI strings) | > Both `translation` and `translations[]` are inlined — there are no IRIs to follow on this resource. The `options` field inside each translation is a **JSON string**, not a parsed object — you have to `JSON.parse(translation.options)` on the client. ### Shape of the `options` payload (per type) The `options` JSON string varies by `type`. Common shapes: | `type` | Parsed `options` shape | |---------------------|----------------------------------------------------------------------------------------------| | `image_carousel` | `{ "images": [{ "link": "...", "image": "storage/theme/.../*.webp", "title": "..." }, …] }` | | `category_carousel` | `{ "filters": { "sort": "asc", "limit": "10", "parent_id": "1" } }` | | `product_carousel` | `{ "filters": { … } }` (similar to category) | | `footer_links` | `{ "column_1": [{ "url": "...", "title": "...", "sort_order": "3" }, …], "column_2": […], … }` | | `services_content` | `{ "services": [{ "icon": "...", "title": "...", "description": "..." }, …] }` | | `static_content` | `{ "html_content": "<p>…</p>" }` | > The client must **parse the JSON string** before using it. The API stores `options` as a TEXT column and returns it verbatim. ## Use Cases - Render the storefront home page in a single round trip: fetch the unfiltered collection, group by `type`, sort by `sortOrder` per group. - Render the footer in a focused request: `?type=footer_links` returns just one row whose `options` carries the column data. - Build an admin preview of every locale: read all `translations[]` entries side-by-side. - Auto-detect new home-page blocks added by store admins by polling the collection — anything with `status=1` will appear. ## Related Resources - [Channels](/api/rest-api/shop/channels/get-channels) — `channelId` on each customization points at one of these - [Categories](/api/rest-api/shop/categories/get-categories) — referenced by `category_carousel` and `product_carousel` filters - [Introduction → IRIs & HATEOAS](/api/rest-api/introduction#iris-hateoas) --- # Testing & Debugging REST APIs URL: /api/rest-api/testing-debugging # Testing & Debugging REST APIs This guide covers how to test and debug Bagisto REST API endpoints using various tools and methods. ## API Configuration The documentation uses centralized API configuration. Update these URLs in `.vitepress/theme/config/api.config.ts`: ```typescript // REST API Base URL export const REST_API_URL = 'https://api-demo.bagisto.com' // GraphQL API Base URL export const GRAPHQL_API_URL = 'https://api-demo.bagisto.com' ``` Replace with your actual Bagisto instance URLs, and all documentation examples will automatically use the updated URLs. ## Interactive Testing Tools ### 1. Swagger UI (Built-in) The fastest way to test REST APIs! Bagisto includes Swagger UI for interactive API documentation. **Access Swagger UI:** ``` http://your-domain.com/api/docs ``` **Features:** - ✅ Try API requests directly from the browser - ✅ Automatic token/authentication handling - ✅ Request and response inspection - ✅ Full endpoint documentation - ✅ Real-time error messages **Steps to Test:** 1. Navigate to `/api/docs` 2. Click "Authorize" button (top right) 3. Enter your Bearer token or credentials 4. Click any endpoint to expand it 5. Click "Try it out" button 6. Modify parameters as needed 7. Click "Execute" to send the request 8. View response in the UI ### 2. ReDoc Alternative Alternative API documentation viewer: ``` http://your-domain.com/api/docs.html ``` **Best for:** - Reading comprehensive endpoint documentation - Understanding request/response schemas - Offline documentation reference ### 3. GraphQL Playground (Same Instance) If GraphQL is enabled on the same instance: ``` http://your-domain.com/api/graphql ``` **Note:** GraphQL and REST APIs are complementary - use GraphQL for complex queries and mutations, REST APIs for simple CRUD operations. ## Testing with cURL The most reliable method for API testing and CI/CD pipelines. ### Basic GET Request ```bash curl -X GET 'https://api-demo.bagisto.com/api/shop/locales/1' \ -H 'Accept: application/json' ``` ### GET with Authentication ```bash curl -X GET 'https://api-demo.bagisto.com/api/customer_profiles' \ -H 'Accept: application/json' \ -H 'Authorization: Bearer YOUR_ACCESS_TOKEN' ``` ### POST Request with Body ```bash curl -X POST 'https://api-demo.bagisto.com/api/shop/customers/login' \ -H 'Content-Type: application/json' \ -d '{ "email": "customer@example.com", "password": "password123" }' ``` ### With Custom Headers ```bash curl -X POST 'https://api-demo.bagisto.com/api/shop/add-product-in-cart' \ -H 'Content-Type: application/json' \ -H 'X-Cart-Token: YOUR_CART_TOKEN' \ -H 'Accept-Language: en_US' \ -d '{ "productId": 1, "quantity": 2 }' ``` ### Extracting Token from Response ```bash # Login and extract token TOKEN=$(curl -s -X POST 'https://api-demo.bagisto.com/api/shop/customers/login' \ -H 'Content-Type: application/json' \ -d '{ "email": "customer@example.com", "password": "password123" }' | grep -o '"access_token":"[^"]*' | cut -d'"' -f4) echo "Token: $TOKEN" # Use token in subsequent request curl -X GET 'https://api-demo.bagisto.com/api/customer_profiles' \ -H 'Authorization: Bearer '$TOKEN \ -H 'Accept: application/json' ``` ### Viewing Response Headers ```bash # Show response headers along with body curl -i -X GET 'https://api-demo.bagisto.com/api/shop/products' \ -H 'Accept: application/json' ``` ### Pretty-Print JSON Response ```bash # Install jq: apt-get install jq (Linux) or brew install jq (Mac) curl -s -X GET 'https://api-demo.bagisto.com/api/shop/products' \ -H 'Accept: application/json' | jq ``` ## Postman Collection [Download Postman Collection](https://postman.bagisto.com/bagisto-api.json) ### Setting Up Postman 1. **Import Collection:** - Open Postman - Click "Import" button - Paste collection URL or upload JSON file - Click "Import" 2. **Configure Environment Variables:** - Click "Environments" (left sidebar) - Click "Create" - Add variables: ``` base_url: https://api-demo.bagisto.com access_token: YOUR_TOKEN_HERE cart_token: YOUR_CART_TOKEN_HERE ``` 3. **Run Tests:** - Select environment from dropdown - Click any request - Click "Send" - View response in "Body" tab 4. **Using Variables in Requests:** ``` {{base_url}}/api/shop/products Authorization: Bearer {{access_token}} X-Cart-Token: {{cart_token}} ``` ## Insomnia Client Similar to Postman with a cleaner interface: 1. **Create New Request:** - Click "+" button - Select HTTP method (GET, POST, etc.) - Enter URL: `https://api-demo.bagisto.com/api/shop/locales/1` 2. **Add Headers:** - Click "Header" tab - Add: `Accept: application/json` - Add: `Authorization: Bearer TOKEN` 3. **Add Body (for POST/PATCH):** - Click "Body" tab - Select "JSON" format - Paste JSON payload 4. **Send Request:** - Click "Send" button - View response below ## JavaScript/Fetch Testing Use browser console or Node.js to test: ### Basic Fetch ```javascript // Simple GET request fetch('https://api-demo.bagisto.com/api/shop/locales/1', { method: 'GET', headers: { 'Accept': 'application/json' } }) .then(response => response.json()) .then(data => console.log(data)) .catch(error => console.error('Error:', error)); ``` ### With Authentication ```javascript const token = 'YOUR_ACCESS_TOKEN'; fetch('https://api-demo.bagisto.com/api/customer_profiles', { method: 'GET', headers: { 'Accept': 'application/json', 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' } }) .then(response => { console.log('Status:', response.status); console.log('Headers:', response.headers); return response.json(); }) .then(data => console.log('Data:', data)) .catch(error => console.error('Error:', error)); ``` ### POST Request ```javascript const payload = { email: 'customer@example.com', password: 'password123' }; fetch('https://api-demo.bagisto.com/api/shop/customers/login', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' }, body: JSON.stringify(payload) }) .then(response => response.json()) .then(data => { console.log('Login Response:', data); // Extract and store token const token = data.access_token; localStorage.setItem('token', token); }) .catch(error => console.error('Error:', error)); ``` ### Error Handling ```javascript async function testAPI(url, options = {}) { try { const response = await fetch(url, { headers: { 'Accept': 'application/json', 'Content-Type': 'application/json', ...options.headers }, ...options }); // Check if response is ok if (!response.ok) { const error = await response.json(); throw new Error(`HTTP ${response.status}: ${error.hydra:description || 'Unknown error'}`); } const data = await response.json(); return data; } catch (error) { console.error('API Error:', error.message); return null; } } // Usage testAPI('https://api-demo.bagisto.com/api/shop/products') .then(data => console.log(data)); ``` ## Browser Network Inspector Built-in browser tools for API debugging: ### Chrome/Firefox/Edge DevTools 1. **Open DevTools:** - Press `F12` or `Ctrl+Shift+I` (Windows/Linux) - Press `Cmd+Option+I` (Mac) 2. **Go to Network Tab:** - Click "Network" tab - Perform API action in your app - Find the API request in the list 3. **Inspect Request:** - Click on the request - View: - **Headers**: Request headers and cookies - **Request Body**: POST/PATCH data sent - **Response**: JSON response from API - **Cookies**: Authentication tokens - **Timing**: Performance metrics 4. **Copy as cURL:** - Right-click on request - Select "Copy" > "Copy as cURL" - Paste in terminal to reproduce issue ## Debugging Checklist When an API request fails, check these items: ### ✅ Authentication Issues ```bash # Verify token is valid curl -X GET 'https://api-demo.bagisto.com/api/customer_profiles' \ -H 'Authorization: Bearer YOUR_TOKEN' # Check token expiration curl -X POST 'https://api-demo.bagisto.com/api/shop/refresh-token' \ -H 'Authorization: Bearer YOUR_TOKEN' ``` ### ✅ CORS Issues ```bash # Check if CORS headers are present curl -i -X OPTIONS 'https://api-demo.bagisto.com/api/shop/products' \ -H 'Origin: https://api-demo.bagisto.com' # Response should include: # Access-Control-Allow-Origin: * # Access-Control-Allow-Methods: GET, POST, PATCH, DELETE ``` ### ✅ Content-Type Issues ```bash # Always include Content-Type for POST/PATCH curl -X POST 'https://api-demo.bagisto.com/api/shop/customers/login' \ -H 'Content-Type: application/json' \ -d '{"email":"user@example.com","password":"pass"}' ``` ### ✅ Invalid Payload ```bash # Validate JSON before sending echo '{"email":"user@example.com","password":"pass"}' | jq . # Check for missing required fields curl -X POST 'https://api-demo.bagisto.com/api/shop/customers' \ -H 'Content-Type: application/json' \ -d '{"email":"user@example.com"}' # Missing password ``` ### ✅ Rate Limiting ```bash # Check rate limit headers curl -i -X GET 'https://api-demo.bagisto.com/api/shop/products' \ -H 'Accept: application/json' # Response headers: # X-RateLimit-Limit: 1000 # X-RateLimit-Remaining: 999 # X-RateLimit-Reset: 1702000000 # If limit exceeded: wait until Reset time ``` ### ✅ Timeout Issues ```bash # Increase timeout for slow requests curl --max-time 30 -X GET 'https://api-demo.bagisto.com/api/shop/products?itemsPerPage=1000' # Node.js timeout fetch(url, { signal: AbortSignal.timeout(5000) }) ``` ## Common Error Responses ### 401 Unauthorized ```json { "@context": "/api/contexts/Error", "@type": "Error", "hydra:title": "Unauthorized", "hydra:description": "Invalid token or missing Authorization header" } ``` **Solution:** Check token validity, refresh if expired, re-authenticate ### 403 Forbidden ```json { "@context": "/api/contexts/Error", "@type": "Error", "hydra:title": "Forbidden", "hydra:description": "Access denied to this resource" } ``` **Solution:** User doesn't have permission, check roles and ACL ### 404 Not Found ```json { "@context": "/api/contexts/Error", "@type": "Error", "hydra:title": "Not Found", "hydra:description": "Resource not found" } ``` **Solution:** Verify endpoint path and resource ID exist ### 422 Unprocessable Entity ```json { "@context": "/api/contexts/ConstraintViolationList", "@type": "ConstraintViolationList", "hydra:title": "Validation Failed", "violations": [ { "propertyPath": "email", "message": "This value is not a valid email address." } ] } ``` **Solution:** Check request payload validation, ensure required fields are present and valid ### 429 Too Many Requests ```json { "@context": "/api/contexts/Error", "@type": "Error", "hydra:title": "Rate Limit Exceeded", "hydra:description": "Too many requests. Try again later." } ``` **Solution:** Wait for rate limit reset, implement exponential backoff ## REST vs GraphQL Testing ### When to Use REST APIs - ✅ Simple CRUD operations (Create, Read, Update, Delete) - ✅ Fetching single resources by ID - ✅ Standard HTTP operations - ✅ File uploads/downloads - ✅ Mobile apps with limited bandwidth ### When to Use GraphQL - ✅ Complex nested data queries - ✅ Multiple resources in single request - ✅ Client-defined response structure - ✅ Real-time subscriptions - ✅ Reducing over-fetching **Testing both:** ```bash # REST - Get single locale curl -X GET 'https://api-demo.bagisto.com/api/shop/locales/1' # GraphQL - Query with flexibility curl -X POST 'https://api-demo.bagisto.com/graphql' \ -H 'Content-Type: application/json' \ -d '{"query":"{ locale(id:1) { id code name direction } }"}' ``` ## Monitoring API Performance ### Response Time Tracking ```bash # Measure endpoint response time time curl -s -X GET 'https://api-demo.bagisto.com/api/shop/products?page=1' \ -H 'Accept: application/json' | jq . > /dev/null # Output: real 0m0.234s (234ms) ``` ### Load Testing with Apache Bench ```bash # Install: apt-get install apache2-utils # Test endpoint with 100 requests, 10 concurrent ab -n 100 -c 10 'https://api-demo.bagisto.com/api/shop/products' ``` ### Load Testing with wrk ```bash # Install: apt-get install wrk or brew install wrk # Test with 4 threads, 100 connections, 30 second duration wrk -t4 -c100 -d30s 'https://api-demo.bagisto.com/api/shop/products' ``` ## Automated Testing with Jest ```javascript describe('REST API - Locales', () => { const baseURL = 'https://api-demo.bagisto.com'; test('GET single locale', async () => { const response = await fetch(`${baseURL}/api/shop/locales/1`); expect(response.status).toBe(200); const data = await response.json(); expect(data.id).toBe(1); expect(data).toHaveProperty('code'); expect(data).toHaveProperty('name'); }); test('Invalid locale ID returns 404', async () => { const response = await fetch(`${baseURL}/api/shop/locales/9999`); expect(response.status).toBe(404); }); }); ``` ## API Documentation Schema Access the OpenAPI schema for automated tools: ``` http://your-domain.com/api/docs.json ``` This JSON schema can be used with: - Swagger/OpenAPI tools - API code generators - Documentation generators - API testing frameworks ## Next Steps - 🧪 [Run tests](https://github.com/bagisto/bagisto#testing) - 📊 [Monitor performance](/api/rest-api/best-practices#performance) - 🔒 [Secure your API](/api/rest-api/best-practices#security) - 📚 [Explore endpoints](/api/rest-api/shop-resources) ## Resources - 🌐 [Swagger UI](http://api-demo.bagisto.com/api) - 💬 [Community Forum](https://forums.bagisto.com) - 🐛 [Report Issues](https://github.com/bagisto/bagisto/issues) --- # Setup & Configuration URL: /api/setup # Setup & Configuration Get the Bagisto API up and running in just a few minutes. Choose the installation method that works best for your setup. ## Prerequisites Before installing, ensure you have: - **Bagisto v2.0 or higher** ([Bagisto Installation Guide](https://devdocs.bagisto.com/getting-started/installation)) - **Composer 2.0+** ## Installation Methods ### Method 1: Quick Start (Composer Installation – Recommended) The fastest way to get started: ```bash # 1. Install the Bagisto API package composer require bagisto/bagisto-api # 2. Run the installer php artisan bagisto-api-platform:install ``` Your APIs are now ready! Access them at: - **API Documentation**: `https://your-domain.com/api` <br> (e.g., [https://api-demo.bagisto.com/api](https://api-demo.bagisto.com/api)) ### Method 2: Manual Installation Use this method if you need more control over the setup. #### Step 1: Download and Extract 1. Download the BagistoApi package from [GitHub](https://github.com/bagisto/bagisto-api) 2. Extract it to: `packages/Webkul/BagistoApi/` #### Step 2: Register Service Provider Edit `bootstrap/providers.php`: ```php <?php return [ // ...existing providers... Webkul\BagistoApi\Providers\BagistoApiServiceProvider::class, // ...rest of providers... ]; ``` #### Step 3: Update Autoloading Edit `composer.json` and update the `autoload` section: ```json { "autoload": { "psr-4": { "Webkul\\BagistoApi\\": "packages/Webkul/BagistoApi/src" } } } ``` #### Step 4: Install Dependencies ```bash # Install required packages composer require api-platform/laravel:v4.1.25 composer require api-platform/graphql:v4.2.3 ``` #### Step 5: Run the installation ```bash composer dump-autoload php artisan bagisto-api-platform:install ``` #### Step 9: Environment Setup (Update in the .env) ``` STOREFRONT_DEFAULT_RATE_LIMIT=100 STOREFRONT_CACHE_TTL=60 STOREFRONT_KEY_PREFIX=storefront_key_ STOREFRONT_PLAYGROUND_KEY=pk_storefront_vxLIYv5PIp7jkujPNGLFQoDvIdsh2RMF API_PLAYGROUND_AUTO_INJECT_STOREFRONT_KEY=true ``` ### Access Points Once verified, access the APIs at: - **API Documentation**: [https://your-domain.com/api](https://api-demo.bagisto.com/api) - **REST API (Shop)**: [https://your-domain.com/api/shop](https://api-demo.bagisto.com/api/shop) - **REST API (Admin)**: [https://your-domain.com/api/admin](https://api-demo.bagisto.com/api/admin) - **GraphQL Endpoint**: `https://your-domain.com/api/graphql` - **GraphQL Playground**: [https://your-domain.com/api/graphiql](https://api-demo.bagisto.com/api/graphiql) ## Troubleshooting ### Provider Not Found **Error**: `Class 'Webkul\BagistoApi\Providers\BagistoApiServiceProvider' not found` **Solution**: ```bash composer dump-autoload php artisan cache:clear php artisan config:clear ``` ### 404 Errors on API Endpoints **Error**: API endpoints return 404 Not Found **Solutions**: 1. Ensure routes are published: `php artisan vendor:publish --tag=routes` 2. Clear route cache: `php artisan route:clear` 3. Check `.htaccess` file is present in your web root 4. Verify `APP_URL` in `.env` matches your domain ### Permission Denied Errors **Error**: `Permission denied` on file operations **Solution**: ```bash # Set proper permissions chmod -R 775 storage bootstrap/cache chmod -R 755 public # If using Docker/VM chown -R www-data:www-data storage bootstrap ``` ### Database Connection Errors **Error**: `SQLSTATE[HY000]: General error: 1030 Got error` **Solutions**: 1. Verify database credentials in `.env` 2. Run migrations: `php artisan migrate` 3. Check database encoding: `utf8mb4` 4. Ensure sufficient disk space ### Rate Limiting Issues **Error**: `429 Too Many Requests` **Solutions**: 1. Check rate limit configuration: `php artisan config:show bagisto-api` 2. Update rate limit in `.env`: `BAGISTO_API_RATE_LIMIT=200` 3. Clear rate limit cache: `php artisan cache:forget rate_limit` ### CORS Errors in Browser **Error**: `Access to XMLHttpRequest blocked by CORS policy` **Solutions**: 1. Verify CORS is configured in `config/cors.php` 2. Check `FRONTEND_URL` environment variable 3. Ensure `supports_credentials` is set properly 4. Clear browser cache ## Performance Optimization Ensure the application is running in a production environment and that APP_DEBUG is set to false. ```bash # Clear cached configuration and other optimized files php artisan optimize:clear # Rebuild and optimize caches php artisan optimize ``` ## What's Next? Ready to start using the APIs? - 🔐 [Authentication Guide](./authentication) - Learn about authentication methods - 🔗 [REST API Guide](./rest-api/introduction.html) - Explore REST API endpoints - ⚡ [GraphQL API Guide](./graphql-api/introduction.html) - Discover GraphQL capabilities - 🔑 [API Key Management](./storefront-api-key-management-guide) - Generate and manage API keys --- # Storefront API Key Management Guide URL: /api/storefront-api-key-management-guide # Storefront API Key Management Guide ## Overview Bagisto uses **API keys** to authenticate requests to your storefront and shop API endpoints. Think of your API key as a secure password that identifies your application to Bagisto. > **⚡ Quick Fact:** Storefront API keys are used for public API access and read-only operations on product catalogs, categories, and storefront data. **Header Format:** ``` X-STOREFRONT-KEY: sk_live_xxxxxxxxxxxxx ``` **Quick Example:** ```bash curl -X GET "https://your-domain.com/api/shop/products" \ -H "X-STOREFRONT-KEY: sk_live_xxxxxxxxxxxxx" ``` ## Key Management Notes - 🔓 **Public APIs Only** - Storefront keys are intended for public, read-only access - 👤 **Customer Operations** - Use Bearer tokens (from authentication) for customer-specific operations - 👨‍💼 **Admin Operations** - Use admin Bearer tokens for administrative operations - 📊 **Rate Limited** - Each key has configurable rate limits to protect your API --- ## Quick Reference | Task | Command | |------|---------| | Create new key | `php artisan bagisto-api:generate-key --name="My App"` | | Check key status | `php artisan bagisto-api:key:manage status --key="My App"` | | Rotate key | `php artisan bagisto-api:key:manage rotate --key="My App"` | | Deactivate key | `php artisan bagisto-api:key:manage deactivate --key="Old Key"` | | View all keys | `php artisan bagisto-api:key:manage summary` | | Run maintenance | `php artisan bagisto-api:key:maintain --all` | --- ## Getting Started ### Step 1: Create Your First API Key Run this command in your terminal: ```bash php artisan bagisto-api:generate-key --name="My App" ``` **You'll see:** ``` ✓ API key generated successfully! Key: sk_live_xxxxxxxxxxxxx Name: My App Rate Limit: 100 requests/minute Status: Active ``` > **🔐 Important:** Save this key immediately. You won't be able to view it again after closing this terminal. ### Step 2: Store the Key Safely Add it to your `.env` file: ```bash # .env file BAGISTO_API_KEY=sk_live_xxxxxxxxxxxxx ``` Or store it in your secret manager (AWS Secrets Manager, HashiCorp Vault, etc.). ### Step 3: Start Making API Requests **REST API Example:** ```bash curl -X GET "https://your-domain.com/api/shop/products" \ -H "X-STOREFRONT-API: sk_live_xxxxxxxxxxxxx" ``` **GraphQL API Example:** ```bash curl -X POST "https://your-domain.com/api/graphql" \ -H "X-STOREFRONT-API: sk_live_xxxxxxxxxxxxx" \ -H "Content-Type: application/json" \ -d '{"query": "{ products { id name } }"}' ``` **JavaScript/Node.js Example:** ```javascript const apiKey = process.env.BAGISTO_API_KEY; const response = await fetch('https://your-domain.com/api/shop/products', { method: 'GET', headers: { 'X-STOREFRONT-API': apiKey, 'Content-Type': 'application/json' } }); const products = await response.json(); ``` --- ## Complete Command Reference ### Generate API Key Create new API keys for different environments and applications. ```bash php artisan bagisto-api:generate-key {--name=} {--rate-limit=100} {--no-activation} ``` **Parameters:** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `--name` | string | required | Descriptive name for your key (e.g., "Mobile App", "Third Party") | | `--rate-limit` | integer \| string | 100 | Requests per minute. Set null for unlimited (up to 5000 max). Default: 100 requests/minute | | `--no-activation` | flag | false | Create key in inactive state (activate later) | > **📌 Rate Limit Note:** The maximum allowed rate limit is **5,000 requests/minute**. If you request a higher limit, it will be capped at 5,000. For unlimited access within this ceiling, leave `--rate-limit` empty. **Examples:** ```bash # Basic key generation php artisan bagisto-api:generate-key --name="Mobile App" # High-traffic application with custom rate limit (500 req/min) php artisan bagisto-api:generate-key --name="Partner API" --rate-limit=500 # Unlimited rate limit (up to 5000 max) php artisan bagisto-api:generate-key --name="Premium Integration" --rate-limit=null # Create inactive key (for later activation) php artisan bagisto-api:generate-key --name="Staging Environment" --no-activation # Multiple environments php artisan bagisto-api:generate-key --name="Production" --rate-limit=1000 php artisan bagisto-api:generate-key --name="Development" --rate-limit=200 php artisan bagisto-api:generate-key --name="High-Volume Partner" --rate-limit=5000 # Unlimited request php artisan bagisto-api:generate-key --name="Partner API" --rate-limit=null ``` **Response:** ``` ✓ API key generated successfully! Key: sk_live_xxxxxxxxxxxxx Name: Mobile App Rate Limit: 100 requests/minute Status: Active Created: 2024-01-20 10:30:00 Last Used: Never ``` **Tips:** - 🏷️ **Name your keys clearly** — "Mobile App", "Website Frontend", "Partner Integration" - 📊 **Match rate limits to your needs** — Start with 100, increase as you grow - 🔄 **Rotate quarterly** — Change keys every 3 months for security - 🔐 **Never commit to Git** — Use `.env` files with `.gitignore` - 💡 **Use meaningful names** — Makes key management easier when referencing by name --- ### Finding and Identifying Keys Keys can be referenced by either their **numeric ID** or **name** in all management commands: ```bash # By ID (numeric) php artisan bagisto-api:key:manage status --key=1 # By name (more convenient) php artisan bagisto-api:key:manage status --key="Mobile App" # Either approach works with any command: php artisan bagisto-api:key:manage rotate --key="My Integration" php artisan bagisto-api:key:manage deactivate --key=5 ``` The system automatically detects whether you're using an ID or name and looks up the key accordingly. --- ## Manage API Keys Monitor, rotate, and control your API keys throughout their lifecycle. ```bash php artisan bagisto-api:key:manage {action} {--key=} {--reason=} {--days=7} {--unused=90} ``` **Available Actions:** | Action | Purpose | Example | |--------|---------|---------| | `rotate` | Generate new key (old key becomes inactive) | `rotate --key="Mobile App"` | | `deactivate` | Disable a key immediately | `deactivate --key="Old Key" --reason="Compromised"` | | `status` | Check key status and usage | `status --key="Mobile App"` | | `expiring` | List keys expiring soon | `expiring --days=30` | | `unused` | Find keys not used recently | `unused --days=90` | | `cleanup` | Remove expired/inactive keys | `cleanup` | | `summary` | View all keys summary | `summary` | **Parameters:** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `--key` | string | optional | Key ID or name to manage (supports both numeric ID and key name) | | `--reason` | string | optional | Reason for deactivation (logged for audit) | | `--days` | integer | 7 | Days threshold for "expiring" action | | `--unused` | integer | 90 | Days threshold for "unused" action | **Examples:** ```bash # Rotate a key (security best practice) php artisan bagisto-api:key:manage rotate --key="Mobile App" # By name - find key by its name instead of ID php artisan bagisto-api:key:manage status --key="My Custom API Key" # Deactivate a compromised key php artisan bagisto-api:key:manage deactivate --key="Old Integration" \ --reason="Service discontinued - replaced by new key" # Check key status and usage stats (by ID or name) php artisan bagisto-api:key:manage status --key="Mobile App" # Find keys expiring within 30 days php artisan bagisto-api:key:manage expiring --days=30 # Find unused keys (not accessed in 90 days) php artisan bagisto-api:key:manage unused --days=90 # View summary of all keys php artisan bagisto-api:key:manage summary # Clean up old inactive keys php artisan bagisto-api:key:manage cleanup ``` > **💡 Pro Tip:** You can reference keys by either their numeric ID or by their name. The system will automatically search by ID first, then by name if not found numerically. **Output Examples:** ``` # Status output Key: Mobile App ID: 1 Status: Active Created: 2024-01-20 10:30:00 Last Used: 2024-01-25 15:45:30 Total Requests: 15,234 Average RPM: 125 Rate Limit: 100/minute # Summary output Total Keys: 8 Active: 5 Inactive: 2 Expired: 1 Last 30 Days: 3 rotations ``` **Security Maintenance Schedule:** | Frequency | Action | Command | |-----------|--------|---------| | **Monthly** | Review unused keys | `php artisan bagisto-api:key:manage unused --days=90` | | **Monthly** | Check expiring keys | `php artisan bagisto-api:key:manage expiring --days=30` | | **Quarterly** | Rotate production keys | `php artisan bagisto-api:key:manage rotate --key="Production"` | | **Immediately** | Deactivate compromised | `php artisan bagisto-api:key:manage deactivate --key="Old Key" --reason="Compromised"` | --- ### Automate Maintenance Set up automatic cleanup, notifications, and key management. ```bash php artisan bagisto-api:key:maintain {--cleanup} {--invalidate} {--notify} {--all} ``` **What Each Option Does:** | Option | What It Does | When to Use | |--------|-------------|-------------| | `--cleanup` | Deletes expired and old keys | Scheduled daily maintenance | | `--invalidate` | Disables deprecated keys | Policy enforcement | | `--notify` | Sends expiration warnings | Proactive team notifications | | `--all` | Runs all tasks above | Comprehensive maintenance ✅ | **Examples:** ```bash # Clean up expired keys php artisan bagisto-api:key:maintain --cleanup # Send expiration notifications php artisan bagisto-api:key:maintain --notify # Run complete maintenance (recommended) php artisan bagisto-api:key:maintain --all # Schedule in Laravel Task Scheduler (in app/Console/Kernel.php) $schedule->command('bagisto-api:key:maintain --all') ->daily() ->at('02:00'); ``` **Output:** ``` ✓ Maintenance completed successfully Cleanup: - Removed 2 expired keys - Freed up space: 4.2 KB Invalidation: - Deactivated 1 deprecated key - Audit logged Notifications: - Sent 3 expiration reminders - 3 emails queued ``` **Recommended Scheduler Setup:** Add this to your `app/Console/Kernel.php` to automate key management: ```php protected function schedule(Schedule $schedule) { // Daily maintenance: cleanup, invalidate, notify $schedule->command('bagisto-api:key:maintain --all') ->daily() ->at('02:00') // 2 AM UTC ->onOneServer(); // Weekly check for expiring keys $schedule->command('bagisto-api:key:manage expiring --days=30') ->weeklyOn(1, '09:00'); // Every Monday at 9 AM // Monthly review of unused keys $schedule->command('bagisto-api:key:manage unused --days=90') ->monthlyOn(1, '10:00'); // 1st of month at 10 AM } ``` > **Note:** Make sure your Laravel scheduler is running with `php artisan schedule:work` (development) or set up a cron job (production). --- ## Security Best Practices ### ✅ Do This - **Store in `.env`** — Keep keys out of your codebase ```bash BAGISTO_API_KEY=sk_live_xxxxxxxxxxxxx ``` - **Use environment-specific keys** — Different keys for dev/staging/production ```bash BAGISTO_API_KEY_DEV=sk_test_xxxxxxx BAGISTO_API_KEY_PROD=sk_live_yyyyyyy ``` - **Access via config** — Use Laravel's config system ```php $apiKey = config('services.bagisto.api_key'); ``` - **Rotate quarterly** — Change keys every 3 months ```bash php artisan bagisto-api:key:manage rotate --key="Production" ``` - **Use secret managers** — AWS Secrets Manager, HashiCorp Vault, etc. - **Monitor usage** — Check for unusual activity ```bash php artisan bagisto-api:key:manage status --key="Production" ``` ### ❌ Don't Do This - ⛔ **Hardcode keys in source files** - ⛔ **Log API keys in error messages** - ⛔ **Share keys via email or chat** - ⛔ **Commit `.env` to Git** - ⛔ **Use the same key for multiple environments** - ⛔ **Ignore expired or unused keys** --- ## Troubleshooting ### "Invalid API Key" Error **Problem:** Your API request is being rejected. **Solution:** ```bash # 1. Check if key exists and is active php artisan bagisto-api:key:manage status --key="Your Key Name" # 2. Verify the key hasn't expired # Look for Status: Active in the output # 3. Make sure you're using the correct header # Should be: X-STOREFRONT-API: sk_live_xxxxxxxxxxxxx ``` ### "Rate Limit Exceeded" Error **Problem:** You're hitting too many requests per minute. **Symptoms:** - HTTP 429 response - `X-RateLimit-Remaining: 0` header - Message includes `retry_after` value **Quick Fixes:** 1. **Increase the key's rate limit:** ```bash php artisan bagisto-api:generate-key --name="High Volume App" --rate-limit=500 ``` 2. **Or use an unlimited key (if appropriate):** ```bash php artisan bagisto-api:generate-key --name="Premium Integration" --rate-limit=null ``` **Better Solution:** - Implement request queuing in your application - Add exponential backoff retry logic - Batch multiple operations into single requests - Consider upgrading to unlimited access for legitimate high-volume integrations **Check Current Rate Limits:** ```bash php artisan bagisto-api:key:manage status --key="Your Key Name" ``` ### Lost or Exposed Key **Problem:** You've lost your key or think it's been compromised. **Immediate Action:** ```bash # Deactivate the compromised key immediately php artisan bagisto-api:key:manage deactivate --key="My App" \ --reason="Suspected compromise - exposed in logs" ``` **Next Steps:** ```bash # Create a new key to replace it php artisan bagisto-api:generate-key --name="My App (New)" # Save the new key safely # Update your .env or secret manager # Test that new key works # Monitor old key for unauthorized use ``` ### Key Not Appearing in Summary **Problem:** You created a key but it doesn't show up. **Solution:** ```bash # Check if it was created in inactive state php artisan bagisto-api:key:manage status --key="Your Key" # If using --no-activation flag, you need to check recent keys php artisan bagisto-api:key:manage summary ``` **If key was created with `--no-activation`:** - It exists but is inactive - It won't process API requests until activated - You can find it in the summary but it won't be listed as "active" - Activate it by rotating or deactivating/reactivating through the database ### Requests Working Locally But Not in Production **Problem:** API works in development but fails in production. **Check These:** 1. **Different keys?** — Make sure you're using the production key ```bash echo $BAGISTO_API_KEY # Check what's loaded ``` 2. **Environment variables?** — Verify `.env` is loaded ```php dd(config('services.bagisto.api_key')); // Should not be null ``` 3. **Network issues?** — Check firewall/security groups ```bash curl -I https://your-domain.com/api/shop/products ``` 4. **Expired or inactive key?** — Check key status ```bash php artisan bagisto-api:key:manage status --key="Production Key Name" ``` 5. **Rate limit hit?** — Check if you're exceeding per-minute requests ```bash # Review last few requests in your logs # Look for HTTP 429 responses ``` 6. **Key name vs ID?** — Ensure correct key identification ```bash # View summary of all active keys php artisan bagisto-api:key:manage summary ``` ## Understanding Rate Limits Each API key has a configured rate limit that controls how many requests you can make per minute. ### Rate Limit Fundamentals **Rate Limit Types:** - **Per-Minute Limits:** Resets every 60 seconds - **Default Limit:** 100 requests per minute - **Unlimited Access:** Leave rate limit empty when creating keys (no capping at system limits) - **Maximum Ceiling:** If you request a rate limit above 5,000 req/min, it will be automatically capped at 5,000 **Key Behaviors:** - Null/empty rate limit = Unlimited access (within system capacity) - Numeric rate limit = Enforced per minute - Rate limit exceeded = HTTP 429 response with `retry_after` header ### Setting Rate Limits **When Creating Keys:** ```bash # Default: 100 requests/minute php artisan bagisto-api:generate-key --name="My App" # Custom limit: 250 requests/minute php artisan bagisto-api:generate-key --name="High-Volume App" --rate-limit=250 # Unlimited access (no per-minute restriction) php artisan bagisto-api:generate-key --name="Premium Partner" --rate-limit=null # Max allowed: 5000 requests/minute (will cap anything higher) php artisan bagisto-api:generate-key --name="Max Throughput" --rate-limit=10000 # Result: Will cap to 5,000 req/min with a warning ``` **Checking Current Limits:** ```bash php artisan bagisto-api:key:manage status --key="My App" ``` **Output shows:** ``` Rate Limit: 100/minute # Limited key Rate Limit: Unlimited # Unlimited key ``` ### Monitoring Rate Limit Usage **Response Headers:** Your API responses include rate limit information in the headers: ```bash curl -X GET 'https://your-domain.com/api/shop/products' \ -H 'X-STOREFRONT-KEY: pk_storefront_xxxxx' \ -i ``` **Headers included:** - `X-RateLimit-Limit`: Your rate limit (e.g., 100) - `X-RateLimit-Remaining`: Requests remaining in current minute - `X-RateLimit-Reset`: Unix timestamp when limit resets ### Handling Rate Limit Errors **When Rate Limit is Exceeded:** ```json { "status": 429, "message": "Rate limit exceeded", "retry_after": 45 } ``` **Solutions:** 1. **Wait and retry** — Implement exponential backoff ```javascript const retry_after = response.headers['X-RateLimit-Reset']; await sleep(retry_after * 1000); // retry request ``` 2. **Optimize requests** — Batch operations, use pagination ```bash # Instead of 100 individual requests curl -X GET "https://your-domain.com/api/shop/products?limit=100&include=details" ``` 3. **Increase rate limit** — For legitimate high-volume needs ```bash php artisan bagisto-api:generate-key --name="High Volume" --rate-limit=2000 ``` 4. **Use unlimited keys** — For trusted integrations ```bash php artisan bagisto-api:generate-key --name="Internal Service" --rate-limit=null ``` ### Rate Limiting Best Practices ✅ **Do This:** - Monitor the `X-RateLimit-Remaining` header - Implement automatic retry logic with backoff - Batch requests where possible - Use unlimited keys only for internal services - Review rate limits for each key periodically ❌ **Don't Do This:** - Hammer the API hoping to get lucky - Ignore 429 responses - Set unlimited limits for external integrations - Use the same high-limit key for all applications ## What's Next? - 📊 [Rate Limiting Guide](./rate-limiting) - Understand and handle rate limits in detail - 🔐 [Authentication Guide](./authentication) - Learn about API authentication methods - 🔗 [REST API Guide](./rest-api/introduction.html) - Explore REST API endpoints - ⚡ [GraphQL API Guide](./graphql-api/introduction.html) - Discover GraphQL capabilities - 🚀 [Integration Guides](./integrations) - Real-world integration examples - 📈 [Rate Limit Headers](./rate-limiting#headers) - Understanding rate limit response headers