query
npx skills add https://github.com/crystallizeapi/ai --skill queryCrystallize Query Skill
Query product data, content, and commerce information from Crystallize using GraphQL APIs. This skill covers reading data for storefronts, admin interfaces, product pages, search, and cart operations.
Consultation Approach
Before writing queries, understand the context. Ask clarifying questions:
- What data do you need? Products, orders, customers, content, shapes?
- Is this for a storefront or admin interface? Discovery/Catalogue for storefronts, Shop API for carts and orders, Core for admin only — Core is rate limited and must not serve storefront traffic.
- Do you need search/filtering or exact path reads? Discovery for search and faceted navigation, Catalogue for deterministic reads by path.
- Do you have authentication configured? Core API requires access tokens. Discovery/Catalogue can be open but should be secured in production.
A public Discovery endpoint serves every price variant it exposes to anyone who knows the field name
(
nokPrice,wholesalePrice, …), so confidential terms — negotiated B2B prices in particular — belong in a price list read from the Catalogue API server-side, not in a price variant. See [[pricing]]. - What volume of results? Pagination strategy matters — cursor-based is recommended for all APIs.
Choosing the Right API
- Need search, filtering, or faceting? → Discovery API
- Know the exact path? Need strong consistency? → Catalogue API
- Admin interface? Orders, customers, shapes? → Core API
- Cart/checkout operations? → Shop API
- Need results ordered by relevance rules, personalization, or similarity? → Discovery API with
rankBy/context/nearestTo— see [[vector-ranking]]
Core Is Not a Storefront API
Core is heavily rate limited and is not meant for storefront traffic — not even server-side with a short cache, and never once per page view. A storefront reads the catalogue from Discovery (and Catalogue for deterministic path reads), and does carts, orders, customers and subscription contracts on the Shop API. Core is for the back office: imports, seeding, scheduled jobs and admin tools, behind your own server.
This costs nothing in capability. The Shop API is edge-distributed and scales near the shopper, and it syncs with Core asynchronously, so an order placed through the Shop API is in Core a few seconds later for the back office to work on.
How It Works
Crystallize provides four main APIs for querying data:
- Core API - Full read/write API with advanced filtering. Best for admin interfaces, customer/order management.
- Discovery API - Semantic API for search, browse, filter, and faceting. Best for storefronts.
- Catalogue API - Path-based reads for deterministic data access.
- Shop API - Cart and checkout queries at the edge.
API Endpoints
| API | Endpoint | Auth Required | Use For |
|---|---|---|---|
| Core | https://api.crystallize.com/@{tenant} |
Yes (access tokens) | Admin, customers, orders |
| Discovery | https://api.crystallize.com/{tenant}/discovery |
Optional (configurable) | Storefront search/filter |
| Catalogue | https://api.crystallize.com/{tenant}/catalogue |
Optional (configurable) | Storefront path-based reads |
| Shop /cart | https://shop-api.crystallize.com/{tenant}/cart |
Yes (JWT) | Cart hydration, checkout flows |
| Shop /order | https://shop-api.crystallize.com/{tenant}/order |
Yes (JWT) | Order queries by ID/customer |
Usage
Core API (Admin & Advanced Queries)
The Core API provides full read/write access with advanced filtering capabilities:
- Read items by ID with full component data
- List items with pagination and filtering
- Query customers and orders with complex filters
- Filter orders by customer, SKU, payment provider, metadata
- Access shape and piece definitions
- Manage flows and archives
# Example: Get item with componentsquery GetItem { item(id: "item-id", language: "en") { ... on Product { id name components { componentId content } defaultVariant { sku price stock } } }}
# Example: List orders filtered by customerquery ListOrders { orders( tenantId: "tenant-id" first: 20 filter: { customerIdentifier: "customer@example.com" } sort: { field: createdAt, direction: desc } ) { edges { node { id total { gross currency } customer { identifier } } } }}See Core API Queries Reference for complete documentation.
Discovery API (Recommended for Storefronts)
The Discovery API is the primary API for frontend development. It supports:
- Full-text search
- Filtering by any component or attribute
- Faceted navigation
- Sorting and cursor-based pagination
The Discovery API has three entry points: search for full-text queries across all shapes, browse for shape-typed access where each shape becomes its own query type, and autocomplete for type-ahead on name. A fourth query, topics, walks the topic map.
The Discovery schema is generated per tenant from its shapes and index settings — filter, facet and sort fields differ between tenants, and ranking arguments exist only on tenants served for ranking. Introspect rather than assume.
Note: The Discovery API uses lowercase type names in inline fragments (
... on product,... on category) because types are derived from your shape identifiers. You can still use it for interface (... on Product,... on Folder).
# Example: Browse products by shape with pagination{ browse { product(language: en, pagination: { limit: 25 }) { summary { totalHits hasMoreHits endCursor } hits { name path ... on Product { defaultVariant { sku defaultPrice firstImage { url } } } } } }}
# Example: Search with facets and filtering{ search( language: en term: "green" filters: { type_in: [product] } facets: { shape: { limit: 5 } } pagination: { limit: 20 } ) { summary { totalHits hasMoreHits endCursor facets } hits { name path ... on product { defaultVariant { defaultPrice } } } }}Catalogue API (Path-Based Reads)
Use for deterministic reads when you know the exact path:
{ catalogue(language: "en", path: "/shop/plants") { name ... on Product { variants { sku name price } } }}Shop API Queries
Retrieve cart and order intent data:
query { cart(id: "cart-id") { id items { sku name quantity price { gross net } } total { gross net } }}Best Practices
- Choose the right API - Core for admin, Discovery for storefront search, Catalogue for path-based reads
- Use Discovery API for storefronts - It’s optimized for performance and supports search/filter
- Include deterministic sorting - Always add a secondary sort field (like itemId) for stable pagination
- Request only needed fields - GraphQL allows precise field selection to minimize payload
- Handle async updates - Discovery API may have sub-second delay for recently published content
- Protect APIs in production - Configure authentication for sensitive data
- Use Core API for complex filters - Only Core API supports filtering orders by customer, SKU, payment provider
- Detect the Discovery schema, don’t hardcode it - Filter/sort/facet fields and the ranking arguments are tenant-generated; introspect before building a query
- Keep Core out of the storefront - Storefront reads are Discovery/Catalogue, and carts, orders, customers and subscription contracts are the Shop API
- Scope
customerIdentifieron the server - The Shop API token is per tenant, not per shopper:orders(customerIdentifier:)answers for any identifier it is given, so take it from the session and never from the client - Read an order list through one store - Orders edited in Core appear more than once in the Shop API’s list; group by
coreIdand keep the newest copy (see the Shop API Order Queries Reference)
References
- Core API Queries Reference - Items, customers, orders, shapes with advanced filtering
- Discovery API Reference - Search, browse, autocomplete, filters, facets, sorting, fuzzy matching, pagination and profiling
- Catalogue API Reference - Path-based query documentation
- Shop API Queries Reference - Cart and checkout query documentation (
/cartendpoint) - Shop API Order Queries Reference - Order queries by ID or customer (
/orderendpoint)
Related skills
Reading is only half of it — [[mutation]] covers writes across the same APIs, and [[js-api-client]] wraps all of them for JS/TS. For ranking Discovery results by relevance rules, a shopper’s taste, or similarity to another item, use [[vector-ranking]].
Reference Details
Catalogue API Reference
The Catalogue API provides path-based access to items in your Crystallize tenant. It offers strong consistency for deterministic reads.
Base URL
https://api.crystallize.com/{tenant-identifier}/catalogueReplace {tenant-identifier} with your tenant name.
Authentication
By default, the API is accessible without authentication. You can configure access in the Crystallize App:
- Go to Settings → API Access
- Choose your preferred access level
- Save changes
When to Use Catalogue API
Use the Catalogue API when you need:
- Deterministic exports - Payload must reflect exact current state
- Path-based reads - You know the exact path to the item
- Strong consistency - Cannot tolerate async delays
For search, filtering, and faceting, use the Discovery API instead.
Query Structure
Basic Path Query
{ catalogue(language: "en", path: "/shop/plants") { name path type }}Product Query with Variants
{ catalogue(language: "en", path: "/shop/furniture/dining-chair") { name ... on Product { variants { sku name price stock attributes { attribute value } } } }}Folder with Children
{ catalogue(language: "en", path: "/shop/plants") { name ... on Folder { children { name path type } } }}Document Query
{ catalogue(language: "en", path: "/blog/welcome-post") { name ... on Document { components { id name content { ... on RichTextContent { html } } } } }}Reading a Colors component
A colors component returns ColorsContent, holding a list of entries. Each entry carries whichever
notations were stored — they are the same colour expressed several ways, so select the ones your
frontend actually renders rather than all of them.
{ catalogue(language: "en", path: "/shop/furniture/dining-chair") { name components { id content { ... on ColorsContent { colors { label hex rgb { r g b a } pantone ral } } } } }}ColorEntry exposes label, hex, rgb { r g b a }, hsl { h s l a }, cmyk { c m y k },
pantone and ral. Every field is nullable — an entry only carries the notations that were authored,
so a storefront reading hex needs a fallback for entries stored as Pantone only.
Colour filtering does not belong here or in Discovery; filter on the Selection or topic that carries the colour name. See [[content-model]] for the modelling rule.
cURL Example
curl \ -X POST \ -H "Content-Type: application/json" \ --data '{ "query": "{ catalogue(language: \"en\", path: \"/shop/plants\") { name } }" }' \ https://api.crystallize.com/furniture/catalogueLimitations
The Catalogue API does not support:
- Full-text search
- Filtering
- Faceting
- Sorting across multiple items
Use the Discovery API for these capabilities.
Related Links
Prices for one customer
The Catalogue API is the only storefront API that resolves a price list for a customer. Discovery knows markets only, so this is where a B2B storefront reads an account’s agreed prices — server-side, cached per organisation:
query ContractPrice($skus: [String!]!, $customers: [String!]) { productVariants(skus: $skus, language: "en") { sku priceVariant(identifier: "nok") { price # the list price priceFor(count: 10, customerIdentifiers: $customers) { price # what this customer pays at this quantity identifier # the price list that applied modifier modifierType } } }}customerIdentifiers is a list, and priceFor also takes customerGroupIdentifiers and
marketIdentifiers. A percentage list applies on top of the variant’s volume tiers, and a list aimed at an
organisation resolves for its child customers too — see [[pricing]] for the measurements and the caveats
(productVariants(skus:) caps at 150 SKUs, and priceFor is slow enough to need caching).
Core API Queries Reference
The Core API provides comprehensive read access to items, customers, orders, shapes, and tenant configuration. Use this for admin interfaces, reporting, and complex filtering needs.
Table of Contents
- Base URL
- Authentication
- Core API vs Other APIs
- Item Queries — Get by ID, list with pagination, get components, check identifiers
- Customer Queries — Get by identifier, list, customer groups
- Order Queries — Get by ID, list with advanced filtering
- Shape and Piece Queries — Shape definitions, pieces
- Flow Queries — Flows and flow content
- Archive Queries — Archived versions
- Bulk Task Queries — Task status and listing
- File and Image Queries — Images
- Error Handling — Union return types
- TypeScript Types
Base URL
https://api.crystallize.com/@{tenant-identifier}Note the @ prefix before the tenant identifier.
Authentication
Authentication is required. Use access tokens generated in the Crystallize App:
curl -X POST 'https://api.crystallize.com/@your-tenant' \ -H 'Content-Type: application/json' \ -H 'X-Crystallize-Access-Token-Id: YOUR_TOKEN_ID' \ -H 'X-Crystallize-Access-Token-Secret: YOUR_TOKEN_SECRET' \ -d '{"query": "..."}'Core API vs Other APIs
Use Core API for:
- Admin interfaces and dashboards
- Complex order filtering (by customer, SKU, payment provider)
- Reading shape/piece definitions
- Customer management
- Bulk data access
Use Discovery/Catalogue API for:
- Storefront product listings
- Search and filtering
- Public content access
Item Queries
Get Item by ID
query GetItem { item(id: "item-id", language: "en") { ... on Product { id name shapeIdentifier tree { path parentId } components { componentId content } defaultVariant { sku name price stock images { url altText } } variants { sku name price stock } } ... on Document { id name shapeIdentifier components { componentId content } } ... on Folder { id name shapeIdentifier children { id name type } } ... on ItemNotFoundError { errorName message } }}List Items with Pagination
firstcaps at 100. Asking for more is refused —Invalid pagination options: Cannot list more than 100 items at a time.— and it arrives as anINTERNAL_SERVER_ERRORwith the path pointing at whatever field you selected, which makes it look like something else went wrong. Page withafterinstead. Verified on a live tenant:first: 100returned 100 rows of 540 withhasNextPage: true, whilefirst: 150andfirst: 500both errored.
query ListItems { items( language: "en" first: 20 after: "cursor" filter: { shapeIdentifiers: ["product", "bundle"] # path: { prefix: "/shop" } # includeDescendants: true } sort: { field: updatedAt, direction: desc } ) { edges { cursor node { id name type shapeIdentifier createdAt updatedAt ... on Product { defaultVariant { sku price stock } } } } pageInfo { hasNextPage hasPreviousPage startCursor endCursor } totalCount }}Get Item Component
Read a specific component from an item:
query GetItemComponent { item(id: "item-id", language: "en") { ... on Item { id component(id: "description") { componentId content } } }}Check Identifier Availability
query CheckIdentifier { isIdentifierAvailable(tenantId: "tenant-id", identifier: "new-product-slug") { ... on IdentifierAvailability { available } }}Customer Queries
Get Customer by Identifier
query GetCustomer { customer(identifier: "customer@example.com") { ... on Customer { id identifier tenantId ... on IndividualCustomer { firstName lastName email phone } ... on OrganizationCustomer { name taxId organizationNumber } addresses { type firstName lastName street street2 city state postalCode country phone email } meta { key value } } ... on CustomerNotFoundError { errorName message } }}List Customers
query ListCustomers { customers( tenantId: "tenant-id" first: 20 filter: { email: { contains: "@example.com" } # customerType: individual } ) { edges { node { id identifier ... on IndividualCustomer { firstName lastName email } ... on OrganizationCustomer { name } } } pageInfo { hasNextPage endCursor } totalCount }}Get Customer Groups
query GetCustomerGroup { customerGroup(id: "group-id") { ... on CustomerGroup { id name customerIdentifiers } }}
query ListCustomerGroups { customerGroups(tenantId: "tenant-id", first: 20) { edges { node { id name customerIdentifiers } } }}Order Queries
Get Order by ID
query GetOrder { order(id: "order-id") { ... on Order { id createdAt updatedAt customer { identifier ... on IndividualCustomer { firstName lastName email } } cart { sku name quantity price { net gross currency } imageUrl } total { net gross currency } payment { provider ... on StripePayment { paymentIntentId } } meta { key value } } ... on OrderNotFoundError { message } }}List Orders with Filtering
The Core API supports advanced order filtering by customer, SKU, payment provider, and metadata:
query ListOrders { orders( tenantId: "tenant-id" first: 20 filter: { customerIdentifier: "customer@example.com" # sku: "PROD-123" # paymentProvider: "stripe" # meta: [{ key: "source", value: "mobile-app" }] } sort: { field: createdAt, direction: desc } ) { edges { cursor node { id createdAt total { gross currency } customer { identifier } cart { sku name quantity price { gross } } } } pageInfo { hasNextPage endCursor } totalCount }}Filter Options:
customerIdentifier- Filter by customer email/identifiersku- Filter orders containing specific SKUpaymentProvider- Filter by payment method (stripe, klarna, etc.)meta- Filter by metadata key/value pairscreatedAt- Date range filtering
Shape and Piece Queries
Get Shape Definition
query GetShape { shape(identifier: "product") { ... on Shape { identifier name type components { id name type config } variantComponents { id name type config } } }}List All Shapes
query ListShapes { shapes(tenantId: "tenant-id") { identifier name type itemCount }}Get Piece Definition
query GetPiece { piece(identifier: "seo") { ... on Piece { identifier name components { id name type config } } }}List All Pieces
query ListPieces { pieces(tenantId: "tenant-id", first: 100) { edges { node { identifier name components { id name type } } } }}Flow Queries
Get Flow
query GetFlow { flow(id: "flow-id") { ... on Flow { id name stages { id name position } } }}Get Flow Content
List items in a flow stage:
query GetFlowContent { flowContent(flowId: "flow-id", stageId: "stage-id", language: "en", first: 20) { edges { node { item { id name type } assignedTo { id email } dueDate } } }}Archive Queries
Get Archived Version
query GetArchive { archive(id: "archive-id") { ... on ArchivedItemVersion { id number name archivedAt archivedBy { email } item { id name } } }}List Archived Versions
query ListArchives { archives(itemId: "item-id", language: "en", first: 10, sort: { field: number, direction: desc }) { edges { node { id number name archivedAt archivedBy { email } } } }}Bulk Task Queries
Get Bulk Task Status
query GetBulkTask { bulkTask(id: "task-id") { ... on BulkTask { id type status createdAt startedAt stoppedAt info actor { email } } }}List Bulk Tasks
query ListBulkTasks { bulkTasks(tenantId: "tenant-id", first: 20, filter: { status: running }) { edges { node { id type status createdAt } } }}File and Image Queries
Get Image
query GetImage { image(key: "image-key") { ... on Image { key url altText caption variants { url width height } } }}List Images
query ListImages { images(tenantId: "tenant-id", first: 20, filter: { path: { prefix: "/products/" } }) { edges { node { key url altText createdAt } } }}Error Handling
The Core API uses union return types. Always handle potential errors:
query { item(id: "item-id", language: "en") { ... on Product { id name } ... on ItemNotFoundError { errorName message } ... on UnauthorizedError { errorName message } }}Common error types:
UnauthorizedError- Invalid or missing credentialsItemNotFoundError- Item doesn’t existCustomerNotFoundError- Customer doesn’t existOrderNotFoundError- Order doesn’t existBasicError- Generic error
TypeScript Types
Use generated types for type safety:
import { Query, GetItemQuery, GetCustomerQuery, ListOrdersQuery } from "@/generated/core";Types are generated from the Core API schema via GraphQL Code Generator.
Related Links
Discovery API Reference
The Discovery API is the primary API for powering storefronts with product information and marketing content. It is a read-only API optimized for high performance.
Base URL
https://api.crystallize.com/{tenant-identifier}/discoveryReplace {tenant-identifier} with your tenant name — the bare identifier, no @ (that prefix
belongs to the Core API).
Authentication
By default, the Discovery API is open. If you configure restricted access for your Catalogue API, you need to provide authentication:
- Static token via header
- Access tokens for programmatic access
Important: Always secure your API with authentication in production environments.
The schema is generated per tenant
This is the single most important thing to know before writing a query. The Discovery schema is derived from the tenant’s shapes and index settings, so it differs between tenants and changes when the tenant is re-indexed:
- Every shape becomes a type and a
browsefield —product,category,brand, … - Filter, facet and sort inputs (
TenantFilter,ProductFacet,TenantSort, …) are generated from indexed component fields —price_default,stock_oslo,specs_label,variants_topics, … TenantLanguageis an enum of the tenant’s languages- Ranking inputs and their enums appear only on tenants served for ranking (see below)
Introspect, do not assume. An un-ignited tenant answers
{"success": false, "message": "There is no ignited Tenant for <tenant>."} rather than serving a
schema at all.
Note: The Discovery API uses lowercase type names in inline fragments (
... on product,... on category) because types are derived from your shape identifiers. Interface fragments keep their capital (... on Product,... on Folder,... on Document).
Queries
| Query | Use for |
|---|---|
search |
Full-text search across all shapes; polymorphic hits |
browse |
Shape-typed access — each shape becomes its own query with all its component fields |
autocomplete |
Type-ahead on name — it restricts, it does not rank (see below) |
topics |
Children of a topic in the topic map |
Never send an explicit
nullfor an optional argument.pagination: { after: null }is rejected withinvalid_type: after Invalid input: expected string, received null, and the same goes fortermandcontext. Leave the key out on the first page instead — with GraphQL variables that means building the argument object conditionally, not passingnull.
A hit type keeps the shape identifier’s case. A shape called
toolgives the Discovery typetool(lower case) while its inputs areToolFilter,ToolFacetandToolSort.__type(name: "Tool")answersnull, so a script that introspects the schema should read the hit type from the shape query’shitsrather than capitalise the identifier itself.
search, autocomplete and every field under browse take the same argument set:
language, publicationState, path, pathResolutionMethod, term,pagination, options, rankBy, context, nearestTo, filters, facets, sortingFolder.children and Topic.items take that same set, which is what makes nested category listings
filterable and rankable in one round trip.
Basic Search
{ search(language: en, filters: { type_in: [product] }, pagination: { limit: 20, after: "XXXX" }) { summary { totalHits hasMoreHits endCursor: endToken facets } hits { id name path shape score } }}Hits are polymorphic — use one inline fragment per shape, and read shape to tell them apart.
Filtering by Shape
{ search(language: en, filters: { shape: { equals: "sneaker" } }) { hits { name path } }}Price Range Filter
{ search(language: en, filters: { price_sales: { range: { gte: 20, lte: 100 } } }) { hits { name path } }}Full-Text Search
{ search(language: en, term: "plop") { hits { name path } }}Filter operators
Filters compose with AND and OR, each taking a list of nested filters.
| Input | Operators |
|---|---|
StringFilter |
exists, equals, not_equals, in, not_in, contains, not_contains, phrase, regex, not_regex |
StringFilterWithAutocomplete |
the above plus autocomplete: { term, options } |
NumberFilter |
exists, equals, not_equals, in, not_in, range: { gt, gte, lt, lte } |
DateFilter |
exists, equals, not_equals, in, not_in, range: { gt, gte, lt, lte } |
BooleanFilter |
exists, equals, not_equals |
type_in: [ItemType] (product, document, folder) is the common way to narrow a search.
Typo tolerance (fuzzy search)
Search is exact by default. Opt into typo tolerance through options.fuzzy:
{ search(language: en, term: "gren", options: { fuzzy: { fuzziness: SINGLE, prefixLength: 1 } }) { hits { name path } }}| Option | Default | Meaning |
|---|---|---|
fuzziness |
NONE |
Max single-character edits: NONE, SINGLE, DOUBLE |
prefixLength |
0 |
Leading characters that must match exactly before edits apply |
maxExpensions |
50 |
Max term variations generated |
maxExpansions |
50 |
The same option, spelled correctly — both exist on the input |
Two spelling traps, both confirmed by introspection: FuzzySearchOptions carries maxExpensions and
maxExpansions, and the enum type is Fuziness, not Fuzziness — a typed variable
($f: Fuzziness!) fails schema validation.
Raising fuzziness widens the candidate set and costs latency — prefer SINGLE before DOUBLE, and
use prefixLength to keep short, common terms precise. DOUBLE is very loose on short tokens: on a
540-document tenant a multi-word query matched almost everything, which makes counts and facets
meaningless. Keep a results page at SINGLE, and use DOUBLE only as a last-chance fallback when
SINGLE found nothing.
Autocomplete
{ autocomplete(language: en, term: "espr", pagination: { limit: 8 }) { hits { name path } }}autocomplete matches on name and otherwise takes the same arguments as search — including filters
and ranking.
Autocomplete restricts; it does not rank
This is the trap. Measured on a 540-document tenant:
| Query | totalHits |
Scores |
|---|---|---|
autocomplete(term: "milwaukee") |
540 | mixed |
autocomplete(term: "zzzzqq") — matches nothing |
540 | all 0 |
search(filters: { name: { autocomplete: { term: "milwaukee" } } }) |
146 | all 0 |
the same filter plus term and sorting: { score: desc } |
146 | 25.9 … 11.6 |
Two things follow:
- The top-level
autocompletequery answers with the whole index, padding the tail with score-0 hits, whatever the term. Drop hits withscore: 0, or add the name filter, or you will show a type-ahead list of everything you sell. name: { autocomplete: { term } }is a filter onStringFilterWithAutocomplete— onlynamehas it — and on its own every hit comes back scored0, in no useful order. Pass the same text astermas well and sort byscore: desc: the term scores the matches, the filter restricts the set.
Autocomplete is prefix matching per token, so it does not fix a typo inside a word (“nikkon”,
“hitli”), and it does not match inside a compound word (“kjernebor” does not come up for
“diamantkjernebor”). Full-text search(term:, options: { fuzzy: … }) does both. The pattern that works:
prefix autocomplete first, and full-text fuzzy as the fallback when it finds nothing.
Browse Queries
The browse API provides shape-typed access — each shape becomes its own query type with all
component fields available directly. This is the recommended approach for storefronts.
Browse by Shape
{ browse { product(language: en, pagination: { limit: 25 }) { summary { totalHits hasMoreHits endCursor } hits { name path defaultVariant { sku defaultPrice firstImage { url } } } } }}Browse by Path (Folder Listing)
{ browse { category(language: en, path: "/shop/*") { hits { name path children(language: en) { hits { ... on product { name path defaultVariant { sku defaultPrice } } } } } } }}- Use
path: "/shop/*"for direct children (wildcard) - Use
path: "/shop/exact-item"for a specific item pathResolutionMethod(canonical,alias,history,shortcut) controls how a path is resolved- Use aliases to combine multiple browse queries in one request
Combined Query with Aliases
{ folder: browse { category(language: en, path: "/shop") { hits { name path } } } products: browse { product(language: en, path: "/shop/*") { hits { name path defaultVariant { sku defaultPrice } } } }}Sorting
sorting takes one or more generated fields plus score, each asc or desc:
{ browse { product(language: en, sorting: { price_default: asc, itemId: asc }) { hits { name path } } }}Always add a deterministic secondary field (such as itemId) so pagination stays stable across
pages. Note that sorting does not compose predictably with ranking — see below.
Pagination
Cursor-Based Pagination (Recommended)
Use paginationToken (returned as endToken in summary) for efficient, consistent pagination:
{ browse { product(language: en, pagination: { limit: 25, after: "CURSOR_FROM_PREVIOUS_PAGE" }) { summary { totalHits hasMoreHits endCursor: endToken } hits { name path } } }}Flow:
- First request: omit
after(or set tonull) - Use
summary.endTokenas theaftervalue for the next page - Stop when
summary.hasMoreHitsisfalse
pagination accepts limit, after, before and skip.
Note:
skip-based pagination is deprecated for ordinary queries. Use cursor-based pagination (after).skipbecomes increasingly expensive on large result sets. The exception is ranked queries — see below.
Faceting
Get counts for filter values. StringFacet takes key and limit; NumberFacet and DateFacet also
require boundaries.
{ search(language: en, term: "blue", facets: { shape: { limit: 5 } }) { summary { facets } hits { name path } }}summary.facets is a Hash, and accepts an optional key argument to pull a single facet out.
summary.priceRange(priceIdentifier: "default", quantity: 1) { min max } gives the price bounds of the
current result set — useful for a range slider that matches the active filters.
Ranking, personalization and similarity
Ranking-enabled tenants additionally accept rankBy, context and nearestTo, and expose rankScore
and rankExplain on hits. That surface — vocabularies, setItemTaste, igniteDiscoApi, the five
rankBy signals, context.userTaste, nearestTo and the rerank window — is covered by the
[[vector-ranking]] skill.
Two things to know from here:
- The arguments are absent from the schema until the tenant is served for ranking. Referencing them
on an ordinary tenant is a GraphQL validation error, not an unranked result. Detect the capability:
{ __type(name: "RankByInput") { name } }. - Ranked queries page differently. When a rerank runs, cursor tokens fall back to offset
pagination — use
skip+limit, and remember thatskipoffsets into a bounded rerank window (options.rerankWindow, default 500, cap 2000).
Profiling
Every query can report how it was served:
{ search(language: en, term: "chair") { summary { profiling { executionTime queryEngine collection webNode lastIndexCompletedAt } } }}lastIndexCompletedAt is the reliable way to confirm a re-index actually landed.
Async Updates
The Discovery API is asynchronously updated from your published data and therefore eventually consistent:
- Typical delay: under 1 second
- Large imports may take longer to surface
- A full re-index (
igniteDiscoApi) takes minutes, not seconds, to propagate
For cases requiring exact current state, use the Catalogue API instead.
Related Links
- [[vector-ranking]] — ranking, personalization and similarity
- Crystallize Discovery API Documentation
- Demo tenant: Furnitut
Shop API Order Queries Reference
The Shop API /order scope provides queries for retrieving orders. This is a separate endpoint from the /cart scope.
Base URL
https://shop-api.crystallize.com/{tenant-identifier}/orderImportant: Order queries use the
/orderendpoint, NOT/cart.
Authentication
Requires JWT token with the order scope.
Authorization: Bearer YOUR_JWT_TOKENSee the Shop API Queries Reference for token generation details.
Get a Single Order
Retrieve an order by its Shop API UUID.
query GetOrder($id: UUID!) { order(id: $id) { id coreId reference type paymentStatus createdAt updatedAt additionalInformation customer { identifier firstName lastName email phone companyName type addresses { type street city postalCode country } } items { name sku productId quantity type imageUrl price { gross net taxAmount taxPercent currency } subTotal { gross net taxAmount currency } } total { gross net taxAmount taxPercent currency discounts { amount } taxBreakdown { taxRate amount } } payments { provider transactionId amount method createdAt } pipelines { identifier stage } appliedPromotions { identifier name } meta metaProperty(key: "fulfillment_status") }}Variables:
{ "id": "order-uuid-here"}Get Orders by Customer
Retrieve orders for a specific customer with pagination.
query GetCustomerOrders($customerIdentifier: String!, $limit: Int, $skip: Int) { orders(customerIdentifier: $customerIdentifier, limit: $limit, skip: $skip) { id coreId type paymentStatus createdAt total { gross net currency } items { name sku quantity price { gross net } } pipelines { identifier stage } }}Variables:
{ "customerIdentifier": "john@example.com", "limit": 100, "skip": 0}Scope this on the server. The JWT is per tenant, not per shopper:
ordersanswers for whatevercustomerIdentifierit is given. Take the identifier from the session and never from the client. The same holds forsubscriptionContracts(customerIdentifier:)on the/subscription-contractendpoint.
limit/skip paged inconsistently in the Tools Universe build: limit: 10 returned 5 orders and
then 4 for a customer with 9. Ask for everything a customer has in one page (limit: 100) rather than
walking pages.
For a company or a department there is no query by parent: read the people’s orders and filter on your
own meta (the ordering person, the cost centre).
The Order Store and Core
Shop API orders and Core orders are the same orders in two stores, and they do not converge. Two build findings decide how a storefront should read them.
Orders edited in Core appear more than once here. Three updateOrder calls on one seeded order
left three orders in orders(customerIdentifier:): new Shop ids, the same coreId, different
updatedAt. A storefront that sums an order list then counts the same money several times over — the Lab
Universe build saw a department budget of NOK 1,249,299 instead of 411,775. Two defences, use both:
- Group by
coreIdand keep the most recently updated copy whenever you read a list. - Change orders through the Shop API (
setMeta,addToStage) when a storefront reads them, so no copy is ever made.
The sync runs one way, Shop → Core. An order created with the Shop API gets its coreId in about ten
seconds, and addToStage on the Shop API moves the Core stage too. Nothing comes back the other way:
orders registered in Core with registerOrder showed up in the Shop store only sometimes, carrying the
pipelines they had at creation time, and a later updateOrderPipelineStage or deleteOrder in Core never
reached the Shop store at all (watched for minutes). So a storefront that lists orders with the Shop API
must have them created there, and moved between stages there. Seed demo orders with the Shop API,
not with registerOrder. (Tools Universe.)
Two smaller consequences:
pipelinescomes back as identifiers only ({ identifier, stage }). A storefront that shows a stage name has to map the ids to names itself; the names live in the admin APIs, so resolve them at build or seed time rather than at runtime.coreIdcan readnullon an order created with Shopcreateeven after it has reached Core. Don’t treat a nullcoreIdas “not synced yet”.
Order Response Types
Order
| Field | Type | Description |
|---|---|---|
id |
UUID! |
Shop API order ID |
coreId |
String |
Core API order ID (for PIM operations) |
reference |
String |
Order reference number |
type |
OrderType |
standard, draft, creditNote, etc. |
additionalInformation |
String |
Free-text additional info |
stockLocationIdentifier |
String |
Stock location identifier |
relatedOrderIds |
[String] |
IDs of related orders |
createdAt |
DateTime |
Creation timestamp |
updatedAt |
DateTime |
Last update timestamp |
context |
HashMap |
Order context (pricing, language) |
customer |
Customer |
Customer details |
items |
[Item] |
Order line items |
total |
TotalPrice! |
Order totals |
paymentStatus |
OrderPaymentStatus |
paid, unpaid, refunded, etc. |
payments |
[Payment] |
Payment records |
pipelines |
[Pipeline] |
Pipeline/workflow stage assignments |
appliedPromotions |
[PromotionSlim!] |
Applied promotion details |
meta |
HashMap |
All metadata as key-value map |
metaProperty(key) |
String |
Single metadata value by key |
Item (Order Line Item)
| Field | Type | Description |
|---|---|---|
name |
String! |
Item display name |
sku |
String |
Product SKU |
productId |
String |
Crystallize product ID |
quantity |
PositiveInt |
Quantity ordered |
group |
String |
Item grouping |
type |
CartItemType |
standard, shipping, fee, etc. |
imageUrl |
String |
Item image URL |
price |
ItemPrice! |
Unit price |
subTotal |
ItemPrice! |
Line total (price × quantity) |
subscriptionContractId |
String |
Subscription contract reference |
subscription |
Subscription |
Subscription details |
meta |
HashMap |
Item-level metadata |
ItemPrice / TotalPrice
| Field | Type | Description |
|---|---|---|
gross |
Float! |
Price including tax |
net |
Float! |
Price excluding tax |
taxAmount |
Float! |
Tax amount |
taxPercent |
Float! |
Tax percentage |
currency |
String! |
Currency code |
discounts |
[Discount!] |
Applied discounts |
TotalPrice also includes:
| Field | Type | Description |
|---|---|---|
taxBreakdown |
[TaxBreakdownEntry!] |
Tax by rate |
Customer
| Field | Type | Description |
|---|---|---|
isGuest |
Boolean! |
Whether customer is guest |
identifier |
String |
Unique customer ID |
firstName |
String |
First name |
lastName |
String |
Last name |
middleName |
String |
Middle name |
email |
String |
Email address |
phone |
String |
Phone number |
birthDate |
DateTime |
Date of birth |
companyName |
String |
Company name |
taxNumber |
String |
Tax/VAT number |
type |
CustomerType |
individual or organization |
externalReference |
String |
External reference |
externalReferences |
HashMap |
Multiple external refs |
addresses |
[Address] |
Customer addresses |
meta |
HashMap |
Customer metadata |
Payment
| Field | Type | Description |
|---|---|---|
provider |
String |
Payment provider (e.g., “stripe”) |
transactionId |
String |
Transaction reference |
amount |
Float |
Payment amount |
method |
String |
Payment method (e.g., “card”) |
createdAt |
String |
Payment timestamp |
meta |
HashMap |
Payment metadata |
Pipeline
| Field | Type | Description |
|---|---|---|
identifier |
String |
Pipeline identifier |
stage |
String |
Current stage |
Related
- Shop API Cart Queries - Cart queries (
/cartendpoint) - Shop API Order Mutations - Creating and managing orders (
/orderendpoint)
Shop API Queries Reference
The Shop API handles cart and checkout operations at the edge for low-latency commerce experiences.
Base URL
https://shop-api.crystallize.com/{tenant-identifier}/cartThe endpoint also serves as a GraphQL playground for documentation.
Authentication
The Shop API requires a JWT token. Obtain tokens from:
POST https://shop-api.crystallize.com/{tenant-identifier}/auth/tokenGetting a Token
curl -X POST 'https://shop-api.crystallize.com/YOUR_TENANT/auth/token' \ -H 'Accept: application/json' \ -H 'x-crystallize-access-token-id: YOUR_ACCESS_TOKEN_ID' \ -H 'x-crystallize-access-token-secret: YOUR_ACCESS_TOKEN_SECRET' \ -H 'Content-Type: application/json' \ -d '{"scopes":["cart","cart:admin","order"],"expiresIn":18000}'Token Scopes
| Scope | Description |
|---|---|
cart |
Manage a single cart |
cart:admin |
Manage multiple carts |
order |
Create and manage orders (/order endpoint) |
usage |
Access usage/metrics API |
lock |
Acquire/release locks |
Using the Token
Include in request headers:
Authorization: Bearer YOUR_JWT_TOKENCart Queries
Retrieve Cart
query { cart(id: "cart-id-here") { id state isStale isExpired createdAt updatedAt items { sku name quantity managed origin images { url } price { gross net taxAmount } } total { gross net taxAmount } }}Cart as Order Intent
Retrieve cart formatted for order creation:
query { cartAsOrderIntent(id: "cart-id-here") { customer { firstName lastName email } cart { sku name quantity price { gross net } } total { gross net } }}This format can be pushed directly to the Order API without transformation.
Cart Concepts
Hydration
The process where Shop API fetches and computes data on your behalf to construct or update the cart.
Cart Items
Two types of items can be in a cart:
- SKU Items - Items with a SKU that exist in Crystallize
- External Items - Items that don’t exist in Crystallize (e.g., shipping)
Managed vs Unmanaged Items
- Managed (true): Shop API fetches info from Crystallize
- Managed (false): You provide all item information
SKU items start as managed but become unmanaged if you override properties like pricing.
Expiration
- You can set cart expiration time
- Carts without updates for 3+ months are automatically deleted
Related Links
- Shop API Order Queries - Querying orders (
/orderendpoint) - Shop API Order Mutations - Creating and managing orders
- Crystallize Shop API Documentation
- Order API Documentation
Crystallize AI