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

GET/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

ParameterTypeDescription
store_iduuidScope 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

GET/api/v1/data-health/issuespim:read

One 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

ParameterTypeRequiredDescription
sectionstringYesWhich issue list to read.
store_iduuidNoScope to one store; account-wide when omitted.
localestringNoNarrow the translation sections to one target language.
pageintegerNo1-based page number. Default 1.
page_sizeintegerNoRows 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

POST/api/v1/data-health/scanspim:write

Recompute 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

FieldTypeRequiredDescription
dimensionstringYesWhich dimension to recompute.
store_iduuidNoScope 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

POST/api/v1/data-health/completeness-fillspim:write

Write 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

FieldTypeRequiredDescription
entity_typestringYesThe kind of entity the batch targets.
operationsarray, max 500YesEach 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

POST/api/v1/data-health/divergence-reconcilespim:write

Bring 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

FieldTypeRequiredDescription
entity_typestringYesThe kind of entity the batch targets.
operations[].entity_iduuidYesThe entity to harmonize.
operations[].fieldstringYesThe field to bring into agreement.
operations[].winner_store_iduuidNoAdopt this store's value. Mutually exclusive with value.
operations[].valuestring or nullNoWrite this value to every store that differs, or to store_id alone when it is set. Mutually exclusive with winner_store_id.
operations[].store_iduuidNoNarrow 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.

200 means the batch ran — not that every operation succeeded.

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)

POST/api/v1/data-health/unused-deletionspim:delete

Deletes 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.

Reserved — not reachable today.

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

FieldTypeRequiredDescription
sectionstringYesWhich unused list to delete from.
store_iduuidNoScope the deletion candidates to one store's list.
entity_idsarray of uuid, max 100NoExplicit selection. Mutually exclusive with all.
allbooleanNoDelete one batch of every deletable candidate; the response's remaining drives the caller's loop.
exclude_entity_idsarray of uuid, max 500NoDelete-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