# Core concepts (/docs/developers/concepts)

Campaigns, cycles, spend, pairs, tags and creator groups — how the Campaign API models your brand.

The Campaign API describes your brand with a few building blocks: cycles, spend, pairs, tags and creator groups. Read this page before you call the endpoints, so the numbers they return make sense.

## Campaigns are sets of cycles [#campaigns-are-sets-of-cycles]

A <Term id="cycle">payout cycle</Term> is a date range your brand pays creators for. The API has no separate "campaign" object: a **campaign is whatever cycles you pass** in `cycleIds`. Get ids from [`GET /cycles`](/docs/developers/api-reference).

```text
?cycleIds=cyc_sep,cyc_oct   →  "Q3 launch" campaign
```

## Spend = base pay + CPM bonus [#spend--base-pay--cpm-bonus]

Spend is what your brand pays creators: the <Term id="base-pay">base pay</Term> for each qualifying pair plus the view-based <Term id="bonus">bonus</Term> — the same amounts creators see in their payouts. All money is in **USD**.

`GET /campaign/summary` can also track a budget: pass the client's `prepayment` and the response adds `metrics.totalRemains` (prepayment minus spend), `budget.budgetUsedPct`, and `budget.atThreshold` — `true` once usage reaches `thresholdPct` (default 70%).

## Pairs (sets) [#pairs-sets]

A **pair** — called a <Term id="set">set</Term> in the app — is one video a creator posted to up to four platforms (Instagram, TikTok, YouTube, Facebook). Pay lives on the pair, not on the individual posts. `GET /campaign/pairs` returns one row per pair with its posts nested under `videos`.

## Tags and creator groups [#tags-and-creator-groups]

Two filters narrow every campaign endpoint:

| Filter     | Scopes to                                         | Get ids from         |
| ---------- | ------------------------------------------------- | -------------------- |
| `tagIds`   | pairs carrying **any** of these tags              | `GET /campaign/tags` |
| `groupIds` | pairs whose creator is in **any** of these groups | `GET /groups`        |

- Ids **within** one filter combine as **ANY** (OR).
- The **two** filters combine as **AND**.
- Pass `__ungrouped__` in `groupIds` for creators in no group.

<Callout title="Daily spend and filters">

  `GET /campaign/daily` returns `spend: null` for every day when you filter by
  tag or group — per-day spend can't be split that way. Use `GET
  /campaign/summary` (filtered) for a group's total spend, or `GET
  /campaign/tags` for spend by tag.

</Callout>

## Dates and timezones [#dates-and-timezones]

Dates you send (`from`, `to`) are calendar days, `YYYY-MM-DD`. Daily figures are bucketed by **your brand's timezone** (set in <UiPath>Settings › Rules › Timezone</UiPath>), so "a day" matches what your team sees in Bloss.

## Pagination [#pagination]

`GET /campaign/pairs` is paginated:

- `limit` — rows per page, 1–500 (default 100).
- `cursor` — pass the previous response's `nextCursor`; omit it for the first page. `nextCursor` is `null` on the last page.
- `updatedSince` — only pairs changed since a timestamp. To keep a copy in sync, store the largest `updatedAt` you've seen and pass it next time.

```ts title="sync-pairs.ts"
let cursor: string | null = null;
do {
  const url = new URL('https://www.bloss.app/api/v0/campaign/pairs');
  url.searchParams.set('cycleIds', cycleIds.join(','));
  if (cursor) url.searchParams.set('cursor', cursor);

  const res = await fetch(url, {
    headers: { Authorization: `Bearer ${process.env.BLOSS_API_KEY}` },
  });
  const page = await res.json();
  await save(page.pairs);
  cursor = page.nextCursor;
} while (cursor);
```
