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

# Metadata

> Store your own identifiers on any PracticeHub record — and filter by them

Every record on the API carries a `metadata` object: a set of key/value pairs — strings, numbers or booleans — that belong to you. PracticeHub never reads or displays them; they exist so an integration can tag records with its own ids (a CRM contact, an order number, a sync cursor) and find them again without keeping a mapping table.

```json theme={null}
{
  "id": 340,
  "first_name": "Ada",
  "metadata": { "crm_id": "hs-778", "source": "website" }
}
```

## Writing metadata

Include `metadata` in the body of any create or update on a writable resource. Keys are **merged**: keys you send are set, keys you omit are left alone, and a `null` value removes a key.

```bash theme={null}
# Set two keys
curl -X PATCH https://your-clinic.your-region.practicehub.io/v3/api/patients/340 \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{ "metadata": { "crm_id": "hs-778", "source": "website" } }'

# Later: change one key, remove another, leave crm_id untouched
curl -X PATCH https://your-clinic.your-region.practicehub.io/v3/api/patients/340 \
  -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
  -d '{ "metadata": { "source": "referral", "campaign": null } }'
```

| Limit           |                                                                                                                                         |
| --------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| Keys per record | 50                                                                                                                                      |
| Key             | 1–40 characters: letters, digits, `_`, `-`, `.`                                                                                         |
| Value           | A string (up to 500 characters), a number (below 1e17 in magnitude) or a boolean — read back with the type you wrote; no nested objects |

Metadata is shared across every integration on the account (there is one `metadata` object per record, not one per key), so prefix keys with your app name if several integrations may touch the same records. Metadata is removed with its record.

## Filtering by metadata

Any resource can be filtered on any metadata key with `metadata[key]=operator:value`, using the same [operators](/guides/querying#filters) as ordinary attributes:

```bash theme={null}
curl -g "https://your-clinic.your-region.practicehub.io/v3/api/patients?metadata[crm_id]=eq:hs-778" -H "Authorization: Bearer $KEY"
curl -g "https://your-clinic.your-region.practicehub.io/v3/api/appointments?metadata[source]=in:website,widget" -H "Authorization: Bearer $KEY"
curl -g "https://your-clinic.your-region.practicehub.io/v3/api/patients?metadata[crm_id]=null" -H "Authorization: Bearer $KEY"
```

`metadata[key]=null` matches records that do **not** have the key — useful for "everything I have not synced yet".

Comparisons are typed: when the operand is a number, `gt`/`gte`/`lt`/`lte`/`between` compare **numerically** against values you stored as numbers (`metadata[score]=gt:9` matches `10`, not the string `"10"`); with any other operand they compare as text, so ISO dates (`2026-09-01`) and times sort correctly. `eq`/`in` match on the value's canonical text (`10`, `true`, `hs-778`), whatever its type. `contains` is a case-insensitive substring match with nothing to escape:

```bash theme={null}
curl -g "…/v3/api/patients?metadata[score]=between:80,100" -H "Authorization: Bearer $KEY"
curl -g "…/v3/api/patients?metadata[vip]=eq:true" -H "Authorization: Bearer $KEY"
curl -g "…/v3/api/patients?metadata[note]=contains:web%20form" -H "Authorization: Bearer $KEY"
```

Equality, `in` and prefix `like` filters use an index and stay fast at any size; `contains` and leading-wildcard `like` scan the key's values for the account.

<Tip>
  With curl, pass `-g` (or URL-encode `[` and `]`) so the shell does not expand the brackets.
</Tip>
