> ## Documentation Index
> Fetch the complete documentation index at: https://docs.closient.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Metadata

> Attach your own key/value data to any Closient API resource.

Every Closient API resource accepts a `metadata` field — a flat dict of string keys and string values you can attach to bridge our objects to your own systems (internal IDs, workflow state, audit info, A/B-test buckets, feature flags, anything).

Closient never reads `metadata` for behavior. It's opaque developer storage, by design — the same trust contract Stripe makes about its `metadata`. We won't key any feature, billing, search ranking, or default behavior off the contents.

## Shape

* **Type:** flat dict, string keys mapped to string values. No nested objects, no arrays, no booleans, no numbers. If you want structured data, serialize it yourself (e.g. JSON-encode and store the string).
* **Default:** `{}` — never `null`. The field is always present in responses.

## Limits

| Limit               | Value              |
| ------------------- | ------------------ |
| Max keys per object | **50**             |
| Max key length      | **40** characters  |
| Max value length    | **500** characters |

Validation runs on every write. Exceeding any limit returns `422 Unprocessable Entity` with a descriptive `error_code: validation_error` envelope.

## Reading metadata

Every read response includes `metadata`:

```json theme={null}
{
  "id": "prod_abc123",
  "name": "Acme Widget",
  "metadata": {
    "internal_sku": "AW-12345",
    "warehouse": "east"
  }
}
```

If you've never set metadata, the field is `{}`.

## Writing metadata

Send `metadata` to the regular `PATCH /v1/{resource}/{id}` endpoint. There's no separate metadata sub-endpoint — that's how Stripe does it and we match.

### Set or overwrite a key

```bash theme={null}
curl -X PATCH https://www.closient.com/api/v1/products/prod_abc123 \
  -H "Authorization: Bearer ck_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{"metadata": {"internal_sku": "AW-12345"}}'
```

### Merge semantics — only changed keys are sent

`metadata` updates are always **merge**, never replace. Send only the keys you're changing; everything else is preserved.

```json theme={null}
{"metadata": {"warehouse": "west"}}
```

This sets `warehouse=west` and leaves any other existing keys untouched.

### Delete a key — empty-string value

```json theme={null}
{"metadata": {"deprecated_field": ""}}
```

An empty-string value is the delete sentinel. The key is removed from the object.

### Clear all metadata

Two equivalent forms:

```json theme={null}
{"metadata": {}}
```

```json theme={null}
{"metadata": null}
```

Both wipe every key on the object.

### Combined: set, delete, leave-alone in one request

```json theme={null}
{
  "metadata": {
    "internal_sku": "AW-67890",
    "warehouse": "",
    "campaign_id": "summer-2026"
  }
}
```

This sets `internal_sku` to a new value, deletes `warehouse`, and adds `campaign_id`. Any other existing key is left alone.

## Form-encoded shorthand

For curl-style usage, the `metadata[key]=value` form-encoded shorthand is also accepted:

```bash theme={null}
curl -X PATCH https://www.closient.com/api/v1/products/prod_abc123 \
  -H "Authorization: Bearer ck_live_xxx" \
  -d "metadata[internal_sku]=AW-67890" \
  -d "metadata[warehouse]="
```

A bare `metadata=` (no `[key]`) clears all metadata.

## Webhooks

`{resource}.updated` webhook events include the full updated `metadata`. Setting/deleting keys triggers the standard updated event — there's no separate `metadata.updated` event type.

## Resources that support metadata

Every CRUD-API resource in the Closient API supports `metadata` as of [C-2699](https://linear.app/closient/issue/C-2699):

* Products, Brands, Substances
* Retailers, Offers (in-store + online), Promotions
* Webhook Endpoints, Resolver Rules
* Organizations, Users, API Keys, Service Accounts
* Locations, Campaigns, Scan Sessions, QA Feedback Sessions
* Certifications and Claims

If you find one that doesn't, file an issue — we missed it.

## Common patterns

### Stamp every object with your internal ID

```python theme={null}
client.products.update("prod_abc123", metadata={"erp_id": "ERP-12345"})
```

Then look up the original product later by checking `metadata["erp_id"]`.

### Track migration progress

```python theme={null}
client.products.update(product_id, metadata={"migrated_to_v2": "true"})
```

(Use a string, not a boolean — `metadata` values must be strings.)

### A/B test bucketing

```python theme={null}
client.products.update(
    product_id,
    metadata={"experiment_2026q2_pricing": "control"},
)
```

### Cleanup of stale keys

```python theme={null}
client.products.update(
    product_id,
    metadata={"old_experiment_id": "", "new_experiment_id": "treatment"},
)
```

## What metadata is NOT for

* **Identifiers Closient should look up.** If you want us to find an object by your ID, that's a different feature (deep search) — we don't index `metadata` for behavioral lookups.
* **Sensitive data.** While transport is HTTPS, `metadata` payloads are stored in our database with the rest of the resource and visible to anyone who can read the resource. Don't put secrets, PII you don't already share with us, or anything you wouldn't put in our `name` field.
* **Schema-heavy data.** If you find yourself building rich structures inside `metadata`, that's a sign you want a real schema — talk to us about a first-class field on the resource.
