Filtering, search and sort

List endpoints accept a filter query parameter: a MongoDB-style JSON document compiled to a SQL WHERE clause. Pass it URL-encoded on GET requests, or as a plain object in a JSON body.

GET /customers?filter={"email":{"$includes":"@acme.com"}}

Most list endpoints accept search: a free-text string matched against the record's most identifying fields (customer: email, name, phone; order: number, remote ID, notes, customer; review: title, body, customer, product; survey: its order's number and customer; loyalty activity: the member's customer). search and filter combine with AND.

The string is split into words, and every word has to match a word on the record, from the start of that word, in any order and across fields. Case and accents are ignored, and FTS5 operators (OR, NEAR, *) are treated as ordinary text. Customers, orders, products and reviews match through a full-text index; the smaller lists still use substring containment. Results are not ranked by relevance. They come back in the endpoint's sort order.

Identifiers are the exception. A phone number, an order number and a remote ID also match on any substring, so pasting the last few digits of a phone number works.

GET /orders?search=anna@acme.com

Sort

sort takes a field name, prefixed with - to reverse. Each endpoint's reference page lists the fields it accepts; most default to -createdAt.

GET /orders?sort=-totalCents

Shape

A filter maps field names to a value or an operator object. Multiple keys are combined with AND. A bare value is shorthand for $eq, and null compares with IS NULL. The dashboard's Date field is createdAt on the wire.

{ "title": "Test Product", "createdAt": { "$gt": "2020-01-01" } }
{ "archivedAt": null }

Operators

Operators and plain keys cannot be mixed in the same object.

OperatorApplies toSQL
$eqany= value (or IS NULL for null)
$lt $lte $gt $gtecolumns, association fields, association counts< <= > >=
$includesstring columnsILIKE '%value%'
$incolumns, association fieldsIN (...) (an empty array matches nothing)
{ "title": { "$includes": "Test" } }
{ "status": { "$in": ["active", "trialing"] } }

Nested JSONB keys

A dot path filters on a key inside a JSON column. $eq and $in (any of the values) are supported:

{ "metadata.category": "electronics" }
{ "metadata.source": { "$in": ["storefront", "wishlist"] } }

A metadata key that is a custom field on customers, products or orders takes the operators its type allows instead: ranges on numbers and dates, $includes on text, and $includes with a list on a pick any field, which matches records holding any of them. The value is read the way the field stores it, so "12" matches the number 12.

{ "metadata.warrantyMonths": { "$gte": 12, "$lte": 36 } }
{ "metadata.woods": { "$includes": ["oak", "walnut"] } }

Associations

A dot path into a related table matches rows with at least one matching associated record. The bare association name with a comparison operator filters on the count:

{ "variants.title": "Variant 1" }
{ "variants": { "$gt": 0 } }

Logical operators

{ "$or": [{ "title": "A" }, { "title": "B" }] }
{ "$not": { "archivedAt": null } }
{ "$and": [{ "createdAt": { "$gt": "2020-01-01" } }, { "title": { "$includes": "x" } }] }

Paging

A list answers with one page (50 records by default) and a nextCursor. Pass it back as cursor for the next page; it is null after the last page.

GET /customers?cursor=eyJpZCI6...

Bulk updates

The customers, orders and products bulk update endpoints take the same filter and search to choose what they change. To change an exact list instead, send its ids in the body (up to 10,000), which keeps a long list out of the URL. Sent together, all three must match. Each record whose tags or metadata change fires its updated webhook.

curl -X PATCH "https://your-store.meisei.app/api/customers/bulk-update" \
  -H "Authorization: Bearer sk_..." -H "Content-Type: application/json" \
  -d '{"ids": ["k3Fq8Zb2", "Pq7rT2Vw"], "addTags": ["vip"]}'

Errors

An unknown field, mixed operators and plain keys, or a value that fails the column's schema returns a 400 validation error describing the problem. See Errors.