Sellers

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 sendThe 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 sendAfterWhat 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 metadata changes nothing. metadata: null clears 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" } } (city is gone).
  • null inside an object or array is a normal value. Only a null directly 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]=3 and metadata[stores]=3.0 both find the number 3.
  • Text compares exactly, case counts: metadata[stores]=3 finds the text "3", but does not find "3.0".
  • true and false find the boolean and the text "true"/"false".
  • A filter value with the NUL character (metadata[plan]=%00) returns 400. 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 externalId and sort. pagination.totalItems counts 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 sellerFilterShows up?Why
{"plan":"pro"}metadata[plan]=proyesequal string
{"plan":"pro"}metadata[plan]=Pronoa string compares exactly, case counts
{"plan":"pro"}metadata[plan]=pro&metadata[plan]=maxyessame key repeated = OR
{"stores":3}metadata[stores]=3yesa number compares by value
{"stores":3}metadata[stores]=3.0yes3.0 and 3 are the same number
{"stores":"3"}metadata[stores]=3yesthe text is equal
{"stores":"3.0"}metadata[stores]=3noa string compares exactly: "3.0" is not "3"
{"active":true}metadata[active]=trueyestrue and false compare as text
{"tags":["vip","b2b"]}metadata[tags]=vipyesthe array contains the value
{"tags":["vip","b2b"]}metadata[tags]=gold&metadata[tags]=b2byesthe array contains at least one of the values
{"tags":["vip","b2b"]}metadata[tags]=goldnothe array does not contain the value
{"ids":[10,20]}metadata[ids]=20yesnumber inside an array
{"address":{"state":"RS"}}metadata[address]=RSnoan object cannot be filtered
{"plan":"pro","region":"south"}metadata[plan]=pro&metadata[region]=southyesdifferent keys = AND, both match
{"plan":"pro","region":"south"}metadata[plan]=pro&metadata[region]=northnodifferent keys = AND, one does not match
{"region":"south"}metadata[plan]=pronothe 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:

RuleLimit
Keys in the filterup to 10
Values per keyup to 20
Length of each value1 to 500 characters

Limits

They apply to the metadata after the merge, that is, to what the seller will have in the end.

RuleLimit
Top-level keysup to 50
Key name (top level)1 to 40 characters: letters, digits, _ and - (__proto__ is reserved)
Sizeup to 8192 bytes of compact JSON (no spaces, UTF-8)
Depthup 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.