Query examples
Worked GraphQL query examples for the HotWax Commerce OMS - point lookups, external IDs, filtering, sorting, cursor pagination, and cost tuning.
Every example below runs against POST /rest/s1/graphql. Send the query as the query key of a JSON body and any variables as variables, with a Bearer token in the Authorization header. Field and root names come from the schema reference.
Your first query
curl -X POST 'https://<instance>.hotwax.io/rest/s1/graphql' \
-H 'Authorization: Bearer <token>' \
-H 'Content-Type: application/json' \
-d '{
"query": "query GetOrder($orderId: ID!) { order(orderId: $orderId) { orderName statusId orderDate grandTotal currencyUomId } }",
"variables": { "orderId": "10001" }
}'{
"data": {
"order": {
"orderName": "NN10001",
"statusId": "ORDER_APPROVED",
"orderDate": "2026-05-14T09:32:00Z",
"grandTotal": "129.00",
"currencyUomId": "USD"
}
},
"extensions": {
"cost": {
"requestedQueryCost": 6,
"actualQueryCost": 6,
"throttleStatus": { "maximumAvailable": 1000, "currentlyAvailable": 994, "restoreRate": 50 }
}
}
}Looking up an order by external ID
Orders imported from a sales channel can be reached three ways without knowing the OMS orderId.
The same externalId argument works on shipment, return, facility, and party.
Order detail in one request
This is the query behind an order detail screen: header, bill-to customer, line-item count, each ship group with its own items, origin address, and shipping method.
Three things to notice:
billToCustomerreturnsfirstNameandlastNameseparately. Compose the display name in your own code; the API returns data, not presentation.orderItemCountis the number of distinct sales-channel order lines, which is not the same as the number of OMS order items when a line has been split across facilities.shipGroupsreturns only ship groups that have at least one item. Empty ship groups are dropped.
The same items, grouped two ways
order.orderItems returns every line on the order as one flat collection. order.shipGroups.orderItems returns the same rows partitioned by ship group. Pick whichever matches your screen; you do not need both.
Filtering with query:
Collection roots accept a single query: string. It is a space-separated list of key:value terms, combined with AND.
key:value
equals
statusId:ORDER_APPROVED
key:a,b,c
in
statusId:ORDER_APPROVED,ORDER_HELD
key:>value
greater than
orderDate:>2026-05-01
key:>=value
greater than or equal
orderDate:>=2026-05-01
key:<value
less than
orderDate:<2026-06-01
key:<=value
less than or equal
orderDate:<=2026-05-31
Rules that catch people out:
Only declared keys work. Each root declares its own keys and, per key, which comparators it accepts. The schema reference lists them. An undeclared key is refused with
FIELD_NOT_FILTERABLE.Comparators are per key.
statusIdacceptseqandinonly, sostatusId:>ORDER_APPROVEDis refused withOPERATOR_NOT_ALLOWED. Date keys such asorderDateaccept the four range comparators and not equals.It is one string, not a structured argument. Bind the whole string as a variable rather than trying to pass a filter object.
Values are split on the first colon, so a value may contain colons:
externalId:shopify:4567890is a single term.Spaces separate terms, so a value cannot contain a space.
Sorting
Collection roots take sortKey: plus reverse:. sortKey is an enum whose values are listed per root in the schema reference; reverse: true flips the direction. There is no multi-key sort.
Paging with cursors
Collections are Relay-style connections. first: or last: is required on every one of them, root and nested, and the maximum is 100.
Page forward by passing the previous page's endCursor as after:
Cursors are keyset cursors, not offsets. Page 40 costs the same as page 1, and rows are never repeated or skipped as long as you follow endCursor. To page backwards, use last: with before: and the page's startCursor.
pageInfo.hasNextPage is the authoritative signal that there is more data. Use it rather than comparing the row count to the page size, and rather than testing endCursor, which is still set on the last page. Cost is charged for the page you asked for, not the rows you got back, so one request past the end of a result set is charged in full.
Inventory levels
inventoryLevels returns available-to-promise and quantity-on-hand for each configured product and facility pair. Unstocked or depleted combinations return 0, never null.
To check a specific set of products, pass them as one in term: productId:SKU_1,SKU_2,SKU_3. Because a page returns at most 100 rows, chunk longer ID lists.
Keeping a query inside budget
Nested collections multiply, and that is the only real difference from writing REST calls. This query looks reasonable and is refused before it reaches the database:
Here is where the 1366 comes from:
6 scalar fields on the order
6 × 1
6
orderItemCount
aggregate field
5
billToCustomer and its 3 fields
1 + 3
4
Each ship group's own fields
4 + (1 + 2) + (1 + 6)
14
orderItems inside each ship group
20 × (1 + 5)
120
One ship group node
14 + 120
134
shipGroups
10 × (1 + 134)
1350
The order object itself
1
1
Total
1366
Dropping the two page sizes to shipGroups(first: 5) and orderItems(first: 10) brings the same query to 431, well inside the budget, and returns everything a detail screen renders. The lever is almost always a nested page size, not the field list, because scalars cost 1 and collections cost a multiple.
Two habits that keep queries cheap:
Request the page size the screen shows. A panel that renders five ship groups should ask for five, not fifty.
Split unrelated work. Two queries of cost 400 are cheaper than one of 1400, and they can run in parallel.
Read extensions.cost.requestedQueryCost while you develop. It is the number the budget is checked against, and it tells you how much headroom a query has before it breaks.
Handling errors
THROTTLED is the only code worth retrying unchanged. Every other code describes a problem with the query itself, and the same query will be refused again.
Last updated
Was this helpful?