Case metadata
Metadata are free key-value pairs that you attach to a case. Certificall never interprets them: they are returned to you as-is, on every channel, and let you break down your consumption.
This page covers the topic end to end. The details of each endpoint remain on their own page.
To find a case, use reportToken: it is the only field the API filters on, and the one the webhook returns as reportId. Metadata is a descriptive complement — site, branch, job type, campaign. Put your identifier in reportToken, the context in metadata.
Setting metadata
Three channels, one metadata field:
| Channel | Where to set it | Reference |
|---|---|---|
| API | metadata field of POST /cases/create | Case management |
| Certilink | context.metadata field when creating the link | Certilink |
| Mobile SDK | metadata option of the capture call | SDK reference |
Whatever the channel, metadata belongs to the case, not to the photo or the certificate.
Limits
| Constraint | Value |
|---|---|
| Number of keys | 20 maximum |
| Key length | 40 characters |
| Value length | 500 characters |
| Value type | text only |
| Characters forbidden in a key | [ and ] |
Exceeding any limit fails the request with a 400, naming the offending key.
Brackets are rejected in keys because they already carry meaning in the filter syntax metadata[key]=value: a key containing one would make the filter ambiguous.
Numbers and booleans must be sent as strings ("42", "true"). Nested objects and arrays are rejected: the field is flat, one key holds one value.
Reading them back
Metadata comes back everywhere the case is returned to you:
| Where | How |
|---|---|
| Case list | GET /cases?format=metadata — metadata field on each case (details) |
| Case webhook | metadata field of the body sent on closing (details) |
{
"id": 12345,
"cfRef": "CAS-1234_CMP-7",
"reportId": "JOB-2026-018",
"metadata": { "site": "Paris", "zone": "A1" }
}
A case without metadata has no metadata key in the response. Plan for a default when reading (const { metadata = {} } = case) rather than testing for an empty object.
Updating them
Metadata on an existing case is updated with PATCH /cases/update. Send only the keys that change:
{
"caseId": 12345,
"metadata": { "zone": "B2" }
}
The keys you send are merged into those already set: an existing key has its value overwritten, the others are kept. On a case holding {"site": "Paris", "zone": "A1"}, the call above leaves {"site": "Paris", "zone": "B2"}.
To clear all metadata on a case, pass null:
{ "caseId": 12345, "metadata": null }
Limits are checked on the block after merging. Ten keys added to fifteen existing ones exceed the cap of twenty and the request is rejected with a 400, even though neither payload is at fault on its own.
There is currently no way to delete one key on its own: null clears the whole block. To remove a key, clear the block and set the keys you want to keep.
To update several cases, use PATCH /cases/bulk-update: a single request against the rate limits.
Filtering your consumption
This is the only place where metadata acts as a filter: GET /companies/consumption counts only the cases carrying all the requested pairs.
curl -X GET \
'https://admin.certificall.app/certificall/api/companies/consumption?startDate=2026-08-01&endDate=2026-08-31&metadata[site]=Paris&metadata[zone]=A1' \
-H 'Authorization: Bearer $TOKEN'
The syntax is metadata[key]=value, repeated as many times as needed. Multiple pairs combine with AND: the example above counts only the cases from the Paris site and zone A1.
See Consumption for the period parameters and the response format.
metadata.site=Paris (dot instead of brackets) returns a 400. This is deliberate: otherwise you would receive the unfiltered total while believing it was filtered.
Two things not to confuse
format=metadata is unrelated
The format parameter of GET /cases and POST /reports/report/:reportToken accepts the value metadata. It describes the response format — JSON data rather than a ZIP archive — and has nothing to do with case metadata.
# JSON response format, no filtering on metadata whatsoever
GET /cases?format=metadata&hours=48
# Filtering on case metadata
GET /companies/consumption?startDate=…&endDate=…&metadata[site]=Paris
caseContext.metadata is not filterable
Some older integrations send their metadata inside the case context object (caseContext.metadata). Those values are still returned to you, so existing integrations keep working, but they are not taken into account by the consumption filter.
Only the case metadata field is filterable. If your consumption filter returns nothing although your cases do carry your keys, check that you are sending them in metadata and not in caseContext.
Summary
| Question | Answer |
|---|---|
| Find a case by metadata? | No — use reportToken |
| Update metadata after creation? | Yes, PATCH /cases/update — keys are merged |
| Remove a single key? | No — null clears the whole block |
| Filter a case list on it? | No — consumption only |
| Get it in the webhook? | Yes, metadata field |
| Numeric values? | As text ("42") |