Metadata
Store your own data on a seller (plan, region, CRM id, tags) and filter the list by it.
metadata is a space of yours inside the seller. You store what you need as keys and values, then list the sellers filtering by that data.
Examples: the customer's plan, the region, their id in your CRM, tags like vip and b2b.
{
"id": "6f0c…",
"name": "Acme Store",
"metadata": { "plan": "pro", "region": "south", "tags": ["vip", "b2b"] }
}Every seller carries metadata. With nothing stored, it is {}. The value of each key can be any JSON: text, number, true/false, array or object.
Write
On create (POST /v1/sellers)
Send metadata in the body. Without metadata, the seller is born with {}.
The null rule: null at the top level means "no such key". The key is simply not stored.
| You send | The seller ends up with |
|---|---|
(no metadata) | {} |
"metadata": null | {} |
{ "a": 1, "b": null } | { "a": 1 } |
curl -X POST "$BACKEND_URL/v1/sellers" \
-H "Authorization: Bearer mk_your_api_key" \
-H "Content-Type: application/json" \
-d '{ "name": "Acme Store", "metadata": { "plan": "pro", "region": null } }'Response (region was not stored):
{ "id": "6f0c…", "name": "Acme Store", "metadata": { "plan": "pro" } }On update (PATCH /v1/sellers/{id})
PATCH merges at the top level. Only the keys you send change.
Before: { "plan": "free", "region": "south", "tags": ["vip"] }
| You send | After | What happened |
|---|---|---|
(no metadata) | { "plan": "free", "region": "south", "tags": ["vip"] } | nothing changes |
"metadata": { "plan": "pro" } | { "plan": "pro", "region": "south", "tags": ["vip"] } | plan changed; the others stayed |
"metadata": { "region": null } | { "plan": "free", "tags": ["vip"] } | region was removed |
"metadata": { "tags": ["b2b"] } | { "plan": "free", "region": "south", "tags": ["b2b"] } | a key you send replaces the whole value |
"metadata": {} | { "plan": "free", "region": "south", "tags": ["vip"] } | no key sent, nothing changes |
"metadata": null | {} | clears everything |
Three points to avoid mistakes (and a warning: key order is not preserved, the database may return the keys in another order; do not rely on it):
- A missing
metadatachanges nothing.metadata: nullclears everything. They are not the same. - The merge is top-level only. An object you send replaces the previous one whole, with no joining inside. Before
{ "address": { "city": "Porto Alegre", "state": "RS" } }, you send{ "address": { "state": "SC" } }, after{ "address": { "state": "SC" } }(cityis gone). nullinside an object or array is a normal value. Only anulldirectly on a top-level key removes it.
curl -X PATCH "$BACKEND_URL/v1/sellers/$SELLER_ID" \
-H "Authorization: Bearer mk_your_api_key" \
-H "Content-Type: application/json" \
-d '{ "metadata": { "plan": "max", "region": null } }'Filter the list
GET /v1/sellers accepts metadata[key]=value. In curl, the brackets need -g (or --globoff); without it curl tries to interpret them. In code, you don't need it.
One key:
curl -g "$BACKEND_URL/v1/sellers?metadata[plan]=pro" \
-H "Authorization: Bearer mk_your_api_key"OR: repeating the same key accepts any of the values. Here, plan pro or max:
curl -g "$BACKEND_URL/v1/sellers?metadata[plan]=pro&metadata[plan]=max" \
-H "Authorization: Bearer mk_your_api_key"AND: different keys must all match. Here, plan pro and region south:
curl -g "$BACKEND_URL/v1/sellers?metadata[plan]=pro&metadata[region]=south" \
-H "Authorization: Bearer mk_your_api_key"The rules, in a list:
- Stored simple value (text, number, boolean): it must be equal.
- Stored array: it must contain at least one of the requested values.
- A number compares by value:
metadata[stores]=3andmetadata[stores]=3.0both find the number3. - Text compares exactly, case counts:
metadata[stores]=3finds the text"3", but does not find"3.0". trueandfalsefind the boolean and the text"true"/"false".- A filter value with the NUL character (
metadata[plan]=%00) returns400. A huge number (metadata[x]=1e400) is not an error: it compares only as text. - An object never matches. A key the seller does not have never matches.
- It combines with
externalIdandsort.pagination.totalItemscounts only the filtered sellers.
Table of cases
Each row is a seller with the metadata of the first column, and the filter of the second.
| Stored on the seller | Filter | Shows up? | Why |
|---|---|---|---|
{"plan":"pro"} | metadata[plan]=pro | yes | equal string |
{"plan":"pro"} | metadata[plan]=Pro | no | a string compares exactly, case counts |
{"plan":"pro"} | metadata[plan]=pro&metadata[plan]=max | yes | same key repeated = OR |
{"stores":3} | metadata[stores]=3 | yes | a number compares by value |
{"stores":3} | metadata[stores]=3.0 | yes | 3.0 and 3 are the same number |
{"stores":"3"} | metadata[stores]=3 | yes | the text is equal |
{"stores":"3.0"} | metadata[stores]=3 | no | a string compares exactly: "3.0" is not "3" |
{"active":true} | metadata[active]=true | yes | true and false compare as text |
{"tags":["vip","b2b"]} | metadata[tags]=vip | yes | the array contains the value |
{"tags":["vip","b2b"]} | metadata[tags]=gold&metadata[tags]=b2b | yes | the array contains at least one of the values |
{"tags":["vip","b2b"]} | metadata[tags]=gold | no | the array does not contain the value |
{"ids":[10,20]} | metadata[ids]=20 | yes | number inside an array |
{"address":{"state":"RS"}} | metadata[address]=RS | no | an object cannot be filtered |
{"plan":"pro","region":"south"} | metadata[plan]=pro&metadata[region]=south | yes | different keys = AND, both match |
{"plan":"pro","region":"south"} | metadata[plan]=pro&metadata[region]=north | no | different keys = AND, one does not match |
{"region":"south"} | metadata[plan]=pro | no | the seller does not have the key |
A malformed filter returns 400
A filter that does not follow metadata[key]=value is refused, not ignored. If we ignored it, you would get every seller without noticing. These return 400: metadata=x, metadata[]=x, metadata[a][b]=x and metadata[__proto__]=x.
Filter limits:
| Rule | Limit |
|---|---|
| Keys in the filter | up to 10 |
| Values per key | up to 20 |
| Length of each value | 1 to 500 characters |
Limits
They apply to the metadata after the merge, that is, to what the seller will have in the end.
| Rule | Limit |
|---|---|
| Top-level keys | up to 50 |
| Key name (top level) | 1 to 40 characters: letters, digits, _ and - (__proto__ is reserved) |
| Size | up to 8192 bytes of compact JSON (no spaces, UTF-8) |
| Depth | up to 5 levels (an object and an array each count one level) |
The limit of 50 keys and the key-name format apply to the top-level keys. Keys inside nested objects are free: they count only for size and depth.
Depth: {"a":1} is 1 level, {"a":{"b":1}} is 2, and an array counts as a level.
A top-level null value does not count as a key: it only removes.
Also 400: text with the NUL character (\u0000) or an unpaired surrogate, in any value or nested key, and a number that does not fit JSON, like 1e400.
Big numbers: every number goes through the API's JSON, so an integer above 2^53 is stored with JavaScript precision: 12345678901234567890 becomes 12345678901234567000, and the filter compares the same way. If the id does not fit in a number, store it as text.
When it goes over a limit
The response is 400 VALIDATION_ERROR, and nothing is stored. details lists the problems, with path starting at metadata. Example: a PATCH that would leave the seller with 51 keys.
{
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid request.",
"details": [
{
"code": "custom",
"path": ["metadata"],
"message": "Metadata has 51 keys; the maximum is 50."
}
]
}
}Other messages: Metadata text cannot contain the NUL character (\u0000) or an unpaired surrogate., Metadata has a number too large to store as JSON., Metadata keys are 1 to 40 letters, digits, "_" or "-". (invalid key, with the key name in path), Metadata is nested more than 5 levels deep. and Metadata takes 9000 bytes as JSON; the maximum is 8192.. See the general error format in Errors.