curl -G "https://www.merchkit.com/api/v1/products" \
-H "Authorization: Bearer $MERCHKIT_API_KEY" \
--data-urlencode "filter[sku]=ARIA-DT-72"
# -G with --data-urlencode is the reliable shape: curl encodes the
# brackets and any spaces in the value, so the query arrives intact.{
"data": [
{
"id": "9b2f6c1e-8a04-4c6e-b0d3-5f2f6f7a9e21",
"type": "product",
"label": "Aria Oak Dining Table",
"parent_id": null,
"created_at": "2026-06-02T14:11:09Z",
"updated_at": "2026-07-18T09:30:22Z",
"attributes": {
"sku": "ARIA-DT-72",
"price": 1299
}
}
],
"pagination": {
"total": 128,
"limit": 50,
"offset": 0,
"has_more": true
}
}{
"error": {
"code": "validation_failed",
"message": "Unknown attribute key(s): colour.",
"documentation_url": "https://docs.merchkit.com/developers/errors#validation_failed",
"is_retriable": false,
"retry_after_seconds": null,
"alternative_action": "List valid attribute keys via GET /v1/attributes?type=product.",
"request_id": "req_01j9x2k8",
"field_errors": [
{
"field": "colour",
"issue": "not_a_defined_attribute"
}
]
}
}List products
Returns a paginated page of products in the one-map resource shape (system fields top-level, every customer key under attributes). Reference attributes embed COMPLETE as labeled stubs — no expansion calls; inverse-reference keys never appear (query the other side via filters, or page /products/{id}/references). Filter with filter[<key>]=<value> / filter[<key>][<op>]=<value> (see the filter parameter for the grammar), sort with sort, page with limit/offset, and narrow the returned keys with fields=<key>,<key>.
Scope: read:products.
curl -G "https://www.merchkit.com/api/v1/products" \
-H "Authorization: Bearer $MERCHKIT_API_KEY" \
--data-urlencode "filter[sku]=ARIA-DT-72"
# -G with --data-urlencode is the reliable shape: curl encodes the
# brackets and any spaces in the value, so the query arrives intact.{
"data": [
{
"id": "9b2f6c1e-8a04-4c6e-b0d3-5f2f6f7a9e21",
"type": "product",
"label": "Aria Oak Dining Table",
"parent_id": null,
"created_at": "2026-06-02T14:11:09Z",
"updated_at": "2026-07-18T09:30:22Z",
"attributes": {
"sku": "ARIA-DT-72",
"price": 1299
}
}
],
"pagination": {
"total": 128,
"limit": 50,
"offset": 0,
"has_more": true
}
}{
"error": {
"code": "validation_failed",
"message": "Unknown attribute key(s): colour.",
"documentation_url": "https://docs.merchkit.com/developers/errors#validation_failed",
"is_retriable": false,
"retry_after_seconds": null,
"alternative_action": "List valid attribute keys via GET /v1/attributes?type=product.",
"request_id": "req_01j9x2k8",
"field_errors": [
{
"field": "colour",
"issue": "not_a_defined_attribute"
}
]
}
}Authorizations
Workspace-scoped API key (mk_live_...). Each operation lists the scope it requires in x-required-scopes. See the Authentication guide for the full scope table and key management.
Query Parameters
Rows per page, 1 to 200. Defaults to 50.
Larger pages mean fewer round-trips. 200 is also the cap on in
operator value lists, so refetching a full page of IDs with
filter[id][in]=... always fits in one call.
1 <= x <= 200Rows to skip. Zero-based. Echoed back in pagination.offset.
Page forward by adding limit each time. Stop when
pagination.has_more is false rather than comparing counts.
Default order is created_at ascending, which keeps long sweeps
stable: new rows land at the end instead of shifting pages you
have not read yet.
x >= 0Comma-separated keys. Prefix with - for descending:
sort=-updated_at,sku. Defaults to created_at ascending.
Sortable: text, number, and date attributes, plus id,
parent_id, created_at, and updated_at. Reference and
list-reference keys have no scalar order. Sorting on one returns
400 (not_sortable).
Narrow results by attribute value. Filtering runs in the database,
so pagination.total reflects matches, not the whole catalog.
Two forms: filter[sku]=ARIA-DT-72 is shorthand for equals.
filter[price][gte]=100 uses the bracket-operator form
filter[<key>][<op>]=<value>.
Keys: any attribute key from GET /attributes, plus id,
parent_id, created_at, and updated_at. Reference keys like
vendor match on UUID, not name.
Operators:
- Text:
eq,ne,contains,not_contains,starts_with,ends_with - Number, date, timestamp:
gt,gte,lt,lte. A bare date means UTC midnight. - Any type:
exists(=truehas a value,=falseis blank) - Text,
id, and reference keys:in(comma-separated OR-of-equals, up to 200 values)
Examples:
filter[sku]=ARIA-DT-72one product by your own identifierfilter[description][exists]=falseeverything missing a descriptionfilter[price][gte]=100&filter[material]=Oakboth must match (seefilter_join)filter[id][in]=<uuid1>,<uuid2>refetch a known set in one callfilter[vendor]=<uuid>&sort=-updated_atone vendor's catalog, newest first
A bad filter never silently matches everything. Unknown keys, unknown operators,
or type mismatches return 400 with field_errors naming the exact clause.
Brackets and spaces need URL-encoding. With curl, use
-G --data-urlencode "filter[vendor_name]=Nordic Timber Co.".
In JavaScript, let URLSearchParams handle it.
Show child attributes
Show child attributes
How multiple filter clauses combine. and means every clause
must match (the default). or means any clause can match.
Case-insensitive. Any other value returns 400.
Only matters with two or more clauses. One join applies to all of them. There is no grouping or nesting, so mixed AND/OR logic belongs on your side.
For OR across values of the same key, prefer
filter[<key>][in]=a,b,c. One clause, up to 200 values, and it
reads better in a log.
and, or Return only the attribute keys you name instead of every populated one. Useful when a workspace has hundreds of attributes and your integration reads a handful.
fields=sku,price (comma-separated) or fields=sku&fields=price
(repeated param, same result). Omit it entirely to get every
populated key.
Only the attributes map narrows. System fields (id, type,
label, parent_id, created_at, updated_at) always come back,
so naming one returns 400. label still resolves from the primary
attribute whether or not you select it. pagination.total is
unchanged. filter and sort are independent: filter on keys you
did not select.
Sparse still means sparse. A selected key with no value stays
absent. Keep reading attributes[key] ?? null.
Reference keys return {id, type, label} stubs as usual, but
fields does not reach through them.
fields=gallery_images.image_url is not supported. Select the
reference key, then batch-fetch:
GET /images?filter[id][in]=<ids>&fields=image_url.
Unknown keys return 400 with field_errors. Inverse-reference
keys are rejected the same way. They are queries, not stored values.
Read them via /{resource}/{id}/references?attribute=.
Selecting only non-reference keys skips the reference join entirely, which avoids loading every referenced entity just to attach its label.
Was this page helpful?