Operations
Data health
The account's catalog-quality state per dimension, the entities behind each
number, and the batches that fix them. Data health carries the first non-GET
endpoints on v1: scans and two remediation batches, all gated by
pim:write.
Read cached data health
/api/v1/data-healthpim:read
The account's catalog-quality state per dimension, as of the last scan. Data
health is derived, re-computable state, so this read never scans: a dimension
that has never been scanned reports scanned: false, which is a
different answer from "scanned, and clean".
Query parameters
| Parameter | Type | Description |
|---|---|---|
store_id | uuid | Scope to one store. Omitted, the answer is account-wide; a store that is not this account's is reported as 404 not_found. |
curl "https://api.peak-pim.com/api/v1/data-health" \
-H "Authorization: Bearer $PEAK_PIM_KEY"
Response — 200
{
"dimensions": [
{
"dimension": "completeness",
"scanned": true,
"result": { … }, // opaque, dimension-specific
"scanned_at": "2026-07-30T06:12:44Z"
},
{
"dimension": "divergence",
"scanned": false // never scanned — not the same as clean
}
]
}
Each dimension's result is an opaque object whose shape is specific
to that dimension and may gain fields within v1 — render what you recognize,
ignore the rest. result and scanned_at are absent when
scanned is false. A store-scoped read echoes the
store_id at the top level.
List data health issues
/api/v1/data-health/issuespim:readOne page of one section's issue rows — the entities behind a dimension's numbers. This is the one list on v1 that is page-numbered rather than cursor-paged: a section is a live query over changing state, not a stable list to walk.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
section | string | Yes | Which issue list to read. |
store_id | uuid | No | Scope to one store; account-wide when omitted. |
locale | string | No | Narrow the translation sections to one target language. |
page | integer | No | 1-based page number. Default 1. |
page_size | integer | No | Rows per page. Default 25, clamped to 100. |
curl "https://api.peak-pim.com/api/v1/data-health/issues?section=completeness&page=1&page_size=50" \
-H "Authorization: Bearer $PEAK_PIM_KEY"
Response — 200
{
"section": "completeness",
"data": [
{
"entity_id": "5b2e9c1a-7d4f-4a8b-9e6c-3f1d8a5b2c70",
"entity_type": "product",
"title": "Organic cotton tee",
"store_id": "1e8e4c2a-9f3b-4d7c-8a5e-6b2d9c1f7a30",
"detail": "2 empty fields",
"fields": ["body_html", "seo_description"]
}
],
"page": 1,
"has_next": true
}
Issue rows always carry entity_id, entity_type, and
title; the rest depends on the section. store_id
appears on store-scoped sections, locale on translation sections,
and coverage sections add missing_store_ids (stores with no version
at all) and never_published_store_ids (stores that were never
published to). The divergence section adds a top-level compared —
how many multi-store entities the scan compared to produce the list.
There is no total count: page forward while has_next is true. Rows
can shift between requests because the underlying state changes — that is
expected for a live query.
Run one scan
/api/v1/data-health/scanspim:writeRecompute one dimension and cache the result. This is the expensive path — it scans the catalog — so it is a request, never something to poll. It touches no merchant data.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
dimension | string | Yes | Which dimension to recompute. |
store_id | uuid | No | Scope the scan to one store, where the dimension is store-scoped. |
curl -X POST "https://api.peak-pim.com/api/v1/data-health/scans" \
-H "Authorization: Bearer $PEAK_PIM_KEY" \
-H "Content-Type: application/json" \
-d '{"dimension": "completeness"}'
Response — 200
{
"dimension": "completeness",
"scanned": true,
"result": { … },
"scanned_at": "2026-07-31T09:04:02Z"
}
The fresh snapshot — the same shape as one entry of the cached read's dimensions.
Fill empty fields
/api/v1/data-health/completeness-fillspim:writeWrite authored values into fields that are still empty. The batch re-checks the condition immediately before writing: a field that has been filled since the scan is skipped, never overwritten, so a stale batch cannot clobber newer values. Writes drafts only — nothing reaches Shopify, and publishing stays an explicit follow-up.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
entity_type | string | Yes | The kind of entity the batch targets. |
operations | array, max 500 | Yes | Each operation: entity_id (uuid), store_id (uuid), field (string), value (string) — all required. |
curl -X POST "https://api.peak-pim.com/api/v1/data-health/completeness-fills" \
-H "Authorization: Bearer $PEAK_PIM_KEY" \
-H "Content-Type: application/json" \
-d '{
"entity_type": "product",
"operations": [
{
"entity_id": "5b2e9c1a-7d4f-4a8b-9e6c-3f1d8a5b2c70",
"store_id": "1e8e4c2a-9f3b-4d7c-8a5e-6b2d9c1f7a30",
"field": "seo_description",
"value": "Soft organic cotton tee, cut for everyday wear."
}
]
}'
Response — 200
{
"filled": 1,
"results": [
{
"entity_id": "5b2e9c1a-7d4f-4a8b-9e6c-3f1d8a5b2c70",
"store_id": "1e8e4c2a-9f3b-4d7c-8a5e-6b2d9c1f7a30",
"field": "seo_description",
"ok": true,
"filled": true
}
]
}
Per operation, ok means the gap is settled and filled
means a value was actually written — ok: true with
filled: false means someone else filled the field first, and the
batch left it alone. Failures carry an error string.
Harmonize diverging store values
/api/v1/data-health/divergence-reconcilespim:writeBring one field into agreement across store versions, either by adopting a winner store's value or by writing an explicit one. Stores that already match are left untouched. Like fills, this re-checks before writing and writes drafts only.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
entity_type | string | Yes | The kind of entity the batch targets. |
operations[].entity_id | uuid | Yes | The entity to harmonize. |
operations[].field | string | Yes | The field to bring into agreement. |
operations[].winner_store_id | uuid | No | Adopt this store's value. Mutually exclusive with value. |
operations[].value | string or null | No | Write this value to every store that differs, or to store_id alone when it is set. Mutually exclusive with winner_store_id. |
operations[].store_id | uuid | No | Narrow an explicit value to one store. |
Response — 200
{
"patched": 2,
"results": [
{ "entity_id": "5b2e9c1a-…", "store_id": "1e8e4c2a-…", "ok": true },
{ "entity_id": "5b2e9c1a-…", "store_id": "8c4f2d9b-…", "ok": true }
]
}
patched counts store versions actually written; results
reports one outcome per store version touched, with an error string
on failures.
Both remediation batches report partial success. Read results per
operation, never just the status code. Both can also answer
423 pim_locked while an import or store refresh is being
reconciled — retry after the merchant finishes the review.
Delete unused entities (reserved)
/api/v1/data-health/unused-deletionspim:deleteDeletes entities across every store they live on. Every candidate is re-verified as still-unused immediately before deletion, so an entity that came back into use since the scan is skipped rather than removed.
This route requires pim:delete, which is not granted by the
consent flow or by key minting, so it answers 403 for every
credential that exists today. It is documented so the contract is honest about
what the surface would do if the scope were ever granted — routing it through
pim:write instead would have hollowed out the scope class that
gates destructive deletes.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
section | string | Yes | Which unused list to delete from. |
store_id | uuid | No | Scope the deletion candidates to one store's list. |
entity_ids | array of uuid, max 100 | No | Explicit selection. Mutually exclusive with all. |
all | boolean | No | Delete one batch of every deletable candidate; the response's remaining drives the caller's loop. |
exclude_entity_ids | array of uuid, max 500 | No | Delete-all mode only: ids earlier batches failed on, so a persistent failure cannot stall the run. |
Response — 200
{
"section": "unused_media",
"deleted": 8,
"skipped": 1,
"remaining": 42,
"results": [
{ "entity_id": "d2c7a9e4-…", "title": "hero-2023.jpg", "ok": true },
{ "entity_id": "f4b1e8c2-…", "title": "lookbook-01.jpg", "ok": false,
"message": "back in use" }
]
}
Notes
- Part of the 2026-07-31 operations surface (private beta) — the first non-GET endpoints on v1.
- Reads never scan; scans never touch merchant data; remediation batches write drafts only. Publishing is always a separate, explicit step.
- POSTs can answer
400 validation_failedfor malformed bodies and403 forbidden_scopefor a key without the route's scope — see Errors. - The data health guide walks the scan → issues → remediate → publish loop end to end.