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

# Enrichment

> Turning rows you already have into rows with commerce data on them.

Enrichment is the other direction from search. You have a list — competitor storefronts from a
spreadsheet, creators from an outreach tool, products from a supplier catalogue — and you want
Unif data attached to it.

```bash theme={null}
curl -X POST https://api.unif.dev/v1/enrich \
  -H "Authorization: Bearer $UNIF_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "currency": "USD",
    "period": { "preset": "last_30d" },
    "fields": ["name", "categories", "metrics.revenue", "metrics.revenue_growth_rate"],
    "inputs": [
      { "type": "shop", "url": "https://www.tiktok.com/shop/glowlab",  "ref": "acct-4471" },
      { "type": "shop", "url": "https://www.tiktok.com/shop/northpeak", "ref": "acct-4472" }
    ]
  }'
```

One call does resolution and retrieval together, for up to **100 inputs**. Beyond that, use an
[enrichment job](/docs/workflows/jobs).

## Requesting fields

`fields` takes dot-paths. Omit it to get the full record.

```json theme={null}
{ "fields": ["name", "metrics.revenue", "metrics.revenue_by_source"] }
```

Narrowing the field set is worth doing for two reasons: it lowers the
[credit cost](/docs/platform/credits) of the call, and it makes `meta.completeness` meaningful —
completeness is measured against what you asked for, so a full-record request will almost always
report less than 1 for reasons you do not care about.

<Tip>
  Request the narrowest field set that satisfies your use case, then widen it when a feature needs
  more. Starting from the full record and trimming later means re-testing everything.
</Tip>

## Joining results back

Set `ref` on every input. Unif echoes it untouched, and results arrive in the order sent.

```python theme={null}
by_ref = {r["ref"]: r for r in response["data"]}

for row in my_rows:
    result = by_ref.get(row.id)
    if result and result["status"] in ("enriched", "partial"):
        row.revenue = result["data"]["metrics"]["revenue"]
        row.unif_id = result["data"]["id"]   # store this
```

Store `result["data"]["id"]` on your row the first time. Every later refresh is then a direct
lookup by ID, which is cheaper than resolving a URL again and immune to the entity being renamed.

## Handling each status

One input failing never fails the batch, so every result needs a branch.

<AccordionGroup>
  <Accordion title="enriched" icon="circle-check">
    Everything you asked for came back. `data` is populated.
  </Accordion>

  <Accordion title="partial" icon="circle-half-stroke">
    Resolved, but some requested fields were unavailable. `data` is populated and
    `meta.missing_fields` names the gaps. Usually a [coverage](/docs/concepts/coverage) limit in
    that market rather than a problem with the row.
  </Accordion>

  <Accordion title="not_found" icon="circle-question">
    The [cascade](/docs/concepts/waterfall) ran to the end and no source verified a match — a dead
    listing, a typo, or an entity outside your enabled markets. Every step missed, so the row cost
    nothing. Retrying will not help; route it to a human or drop it.
  </Accordion>

  <Accordion title="ambiguous" icon="code-branch">
    Several entities matched. `candidates` lists them. Pick one with your own rule and store the
    decision, or the same input stays ambiguous forever.
  </Accordion>

  <Accordion title="unsupported" icon="ban">
    The channel or entity type is not enabled for your workspace. An entitlement problem, not a
    data problem — check [`GET /channels`](/docs/concepts/channels).
  </Accordion>
</AccordionGroup>

```python theme={null}
from collections import Counter

counts = Counter(r["status"] for r in response["data"])
if counts["not_found"] / len(response["data"]) > 0.2:
    log.warning("high miss rate — are these URLs from the right market? %s", counts)
```

A miss rate that jumps is nearly always an input problem — a stale export, or URLs copied from a
different market — and it is worth alerting on rather than discovering in a dashboard.

## Refreshing on a schedule

Enrichment is not a one-time step. Metrics move, so a row enriched last month is stale.

<CardGroup cols={2}>
  <Card title="Scheduled re-enrichment" icon="arrows-rotate">
    Re-enrich by stored ID on your own cadence. Cheapest for a set you want a fresh number on
    every day regardless of whether it moved.
  </Card>

  <Card title="Trackers" icon="bell" href="/docs/workflows/trackers">
    Let Unif watch the set and post a webhook only when a metric crosses your threshold. Cheapest
    when you only care about change.
  </Card>
</CardGroup>

For a set you re-check often, put it in a [list](/docs/workflows/lists) — then a refresh is one
call to `GET /lists/{id}/items` with the period you want, rather than a batch you have to rebuild
each time.

## Idempotency

Enrich accepts an `Idempotency-Key` header. Retrying with the same key returns the original
response instead of doing — and charging for — the work twice.

```bash theme={null}
-H "Idempotency-Key: enrich-batch-2026-09-15-0042"
```

Use it whenever a retry could be triggered by something other than you: a queue redelivery, a
timeout your client retried, a job runner that restarts. See
[Idempotency](/docs/platform/idempotency).
