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

# Quickstart

> From an API key to your first enriched row.

This walks you through one complete loop: authenticate, find something, enrich a row you already
own. It takes about five minutes and costs a handful of credits.

## Prerequisites

* A Unif workspace. Create one at [app.unif.dev](https://app.unif.dev).
* A live API key from **Settings → API keys**. Keys look like `unif_sk_live_...`.
* `curl`, or any HTTP client.

<Warning>
  Treat the key like a password. It carries full access to the workspace, and Unif shows it only
  once. If it leaks, roll it from the dashboard.
</Warning>

## 1. Confirm the key works

Every request is a bearer token away.

```bash theme={null}
curl https://api.unif.dev/v1/me \
  -H "Authorization: Bearer $UNIF_API_KEY"
```

```json Response theme={null}
{
  "object": "workspace",
  "id": "wsp_01k3m9x7v2q8r4t6y0b1n5d7fa",
  "name": "Acme Commerce",
  "api_key_name": "Production",
  "environment": "live",
  "channels": ["tiktok_shop"],
  "rate_limit_per_minute": 600
}
```

The `channels` array is what your workspace is entitled to. Anything outside it returns a
`coverage_error` rather than silently empty results.

## 2. See what you can query

Markets and granularities differ per channel, and they change. Read them rather than hard-coding
them.

```bash theme={null}
curl https://api.unif.dev/v1/channels \
  -H "Authorization: Bearer $UNIF_API_KEY"
```

Each market reports its native currency, its time zone, the earliest date with data, and the
period granularities it supports. Keep this in your config sync, not in a constant.

## 3. Run your first search

Search starts from filters, not identifiers. This finds affordable US beauty products that broke
out this week and pay a decent commission.

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST https://api.unif.dev/v1/products/search \
    -H "Authorization: Bearer $UNIF_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "market": "US",
      "period": { "preset": "last_7d" },
      "currency": "USD",
      "filters": {
        "category_ids": ["cat_beauty_personal_care"],
        "price": { "gte": 15, "lte": 45 },
        "units_sold": { "gte": 500 },
        "revenue_growth_rate": { "gte": 0.5 },
        "commission_rate": { "gte": 0.15 }
      },
      "sort": [{ "field": "revenue", "direction": "desc" }],
      "limit": 25
    }'
  ```

  ```python Python theme={null}
  import os, requests

  resp = requests.post(
      "https://api.unif.dev/v1/products/search",
      headers={"Authorization": f"Bearer {os.environ['UNIF_API_KEY']}"},
      json={
          "market": "US",
          "period": {"preset": "last_7d"},
          "currency": "USD",
          "filters": {
              "category_ids": ["cat_beauty_personal_care"],
              "price": {"gte": 15, "lte": 45},
              "units_sold": {"gte": 500},
              "revenue_growth_rate": {"gte": 0.5},
              "commission_rate": {"gte": 0.15},
          },
          "sort": [{"field": "revenue", "direction": "desc"}],
          "limit": 25,
      },
      timeout=30,
  )
  resp.raise_for_status()
  for product in resp.json()["data"]:
      print(product["title"], product["metrics"]["revenue"])
  ```

  ```javascript Node theme={null}
  const resp = await fetch("https://api.unif.dev/v1/products/search", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.UNIF_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      market: "US",
      period: { preset: "last_7d" },
      currency: "USD",
      filters: {
        category_ids: ["cat_beauty_personal_care"],
        price: { gte: 15, lte: 45 },
        units_sold: { gte: 500 },
        revenue_growth_rate: { gte: 0.5 },
        commission_rate: { gte: 0.15 },
      },
      sort: [{ field: "revenue", direction: "desc" }],
      limit: 25,
    }),
  });

  if (!resp.ok) throw new Error(`Unif ${resp.status}: ${await resp.text()}`);
  const { data } = await resp.json();
  ```
</CodeGroup>

A trimmed response:

```json theme={null}
{
  "object": "list",
  "has_more": true,
  "next_cursor": "eyJvIjoyNX0",
  "total_count": 418,
  "period": { "start": "2026-09-08", "end": "2026-09-14", "granularity": "day", "adjusted": false },
  "currency": "USD",
  "data": [
    {
      "id": "prd_01k3m9x7v2q8r4t6y0b1n5d7fa",
      "object": "product",
      "channel": "tiktok_shop",
      "market": "US",
      "title": "Ceramide Barrier Repair Serum 30ml",
      "shop": { "id": "shp_01k3m9x7v2q8r4t6y0b1n5d7fa", "name": "GlowLab" },
      "price": { "current": 28.0, "min": 24.0, "max": 32.0 },
      "commission_rate": 0.2,
      "metrics": {
        "revenue": 184320.5,
        "units_sold": 6583,
        "revenue_growth_rate": 0.81,
        "creators_count": 214
      },
      "meta": { "refreshed_at": "2026-09-15T04:12:00Z", "completeness": 1 }
    }
  ]
}
```

Three things worth noticing, because they hold for every endpoint:

* `period` is what was **actually** measured. If `adjusted` is true, your dates were snapped to
  a boundary the channel supports. See [Periods and currency](/docs/concepts/periods).
* `next_cursor` drives [pagination](/docs/platform/pagination). Never build page offsets yourself.
* `meta.completeness` tells you how much of the record came back populated.

## 4. Enrich a row you already have

Searching is for discovery. Most production work is the other direction: you have a list, and you
want Unif data on 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": "row-1" },
      { "type": "shop", "url": "https://www.tiktok.com/shop/northpeak", "ref": "row-2" }
    ]
  }'
```

Each result echoes your `ref`, so joining back onto your own rows is a lookup rather than a
guess. Results arrive in the order you sent them, and one failure does not fail the batch:

```json theme={null}
{
  "object": "enrich_response",
  "period": { "start": "2026-08-16", "end": "2026-09-14", "granularity": "day", "adjusted": false },
  "currency": "USD",
  "credits_charged": 2,
  "data": [
    {
      "ref": "row-1",
      "status": "enriched",
      "type": "shop",
      "data": {
        "id": "shp_01k3m9x7v2q8r4t6y0b1n5d7fa",
        "name": "GlowLab",
        "metrics": { "revenue": 1284300.25, "revenue_growth_rate": 0.34 }
      }
    },
    { "ref": "row-2", "status": "not_found", "type": "shop", "data": null }
  ]
}
```

<Tip>
  Requesting `fields` is not just tidier — it lowers the credit cost of the call. And `row-2`
  above was free: the [cascade](/docs/concepts/waterfall) ran to the end without verifying a
  match, and you are only charged for hits. See [Credits](/docs/platform/credits).
</Tip>

## What to do next

<CardGroup cols={2}>
  <Card title="Store the IDs" icon="fingerprint" href="/docs/concepts/identifiers">
    Persist `shp_…` and `prd_…` IDs on your own rows so you never have to resolve twice.
  </Card>

  <Card title="Stop polling" icon="bell" href="/docs/workflows/trackers">
    Put the set behind a tracker and let webhooks tell you when something moves.
  </Card>

  <Card title="Go bulk" icon="layer-group" href="/docs/workflows/jobs">
    More than 100 rows, or an export? Hand it to a job.
  </Card>

  <Card title="Handle failure well" icon="triangle-exclamation" href="/docs/platform/errors">
    The error taxonomy, and which failures are worth retrying.
  </Card>
</CardGroup>
