# Data model

> Entity reference for AI agents: fields, relationships, and the readme documents agents read and write.

## Hierarchy

Trackagoat is **multi-platform**. A **creator** is a person or brand; each creator owns one or more **accounts**, where an account is a single presence on a platform (TikTok or Instagram; `youtube` is reserved for YouTube tracking, which isn't available yet). Videos hang off accounts. Tracking configuration (mode, start/end dates, follower stats, `last_scraped_at`) lives on the **account**, not the creator. A creator can have several accounts, even more than one on the same platform.

```text
Organization                        : readme (the organization's document)
  ├── Member(s)                     : each with a personalization document (readme)
  └── Project(s)
        ├── Creator(s)              : a person/brand (1..n accounts)
        │     └── Account(s)        : one platform presence, tiktok | instagram | youtube
        │           └── Video(s)    : videos/posts from that account
        ├── Video(s)                : standalone videos added by URL (carry platform + account_id)
        ├── Content Group(s)        : link the same content across platforms
        └── Campaign(s)             : named groupings (can nest)
              ├── → Creator(s)
              ├── → Video(s)
              └── → Campaign(s) (recursive)
```

Data is scoped to the organization of the API key. A single API call fetches data across all projects unless you filter by `project_id`. Most list endpoints (`/api/v2/creators`, `/api/v2/accounts`, `/api/v2/videos`, the analytics aggregates) accept a `platform` filter.

The current API is **`/api/v2`**; the previous `v1` API has been retired.

## Entity reference

### Project

| Field | Type | Notes |
|-------|------|-------|
| `id` | uuid | |
| `name` | string | |
| `readme` | string \| null | **Agent context**: the project's document. Read it before working in the project |
| `org_id` | uuid | |
| `created_at` | ISO 8601 | |
| `updated_at` | ISO 8601 | |

### Creator

A creator embeds its `accounts[]`. Creator-level counts are the **aggregate across all its accounts**; the top-level `handle` is the primary account's handle (the TikTok account if present, otherwise the earliest-added).

| Field | Type | Notes |
|-------|------|-------|
| `id` | uuid | |
| `project_id` | uuid | |
| `org_id` | uuid | |
| `handle` | string \| null | Primary account's handle (without `@`) |
| `display_name` | string \| null | |
| `avatar_url` | string \| null | |
| `readme` | string \| null | **Agent context**: read before analyzing (the README on the Info tab; agents append with `POST /api/v2/creators/{id}/notes`) |
| `follower_count` | number \| null | Sum across the creator's accounts |
| `following_count` | number \| null | Sum across accounts |
| `total_likes_count` | number \| null | Sum across accounts |
| `video_count` | number \| null | Sum across accounts |
| `accounts` | Account[] | Embedded platform accounts (see below) |
| `is_active` | boolean | |
| `created_at` | ISO 8601 | |
| `updated_at` | ISO 8601 | |

Creator-level writes (`PATCH /api/v2/creators/{id}`) accept only `display_name`, `readme`, `is_active` and `metadata`. The handle and tracking configuration live on the **account**: change them via `PATCH /api/v2/accounts/{id}`.

### Account

One platform presence owned by a creator. Tracking config and the raw follower/engagement snapshot live here.

| Field | Type | Notes |
|-------|------|-------|
| `id` | uuid | |
| `creator_id` | uuid | Owning creator |
| `project_id` | uuid | |
| `org_id` | uuid | |
| `platform` | `tiktok` \| `instagram` \| `youtube` | |
| `handle` | string | The account's handle (without `@`) |
| `platform_user_id` | string \| null | Stable platform-side ID |
| `display_name` | string \| null | |
| `avatar_url` | string \| null | |
| `bio_link` | string \| null | Link in the account's bio |
| `follower_count` | number | Current snapshot (followers / subscribers) |
| `following_count` | number | |
| `total_likes_count` | number | |
| `video_count` | number | |
| `tracking_mode` | `auto` \| `selective` \| `hashtag` | Controls which discovered videos enter the system |
| `tracking_hashtags` | `string[]` | Hashtag-mode only: tags matched against the video caption. Stored normalized (NFC, lowercase, no `#`, invalid characters stripped, deduped); max 50 |
| `hashtag_inbox_non_matching` | `boolean` | Hashtag-mode only: inbox (`true`) vs exclude (`false`) for non-matching videos |
| `tracking_start_date` | ISO date \| null | Lower bound for this account's stats history |
| `tracking_end_date` | ISO date \| null | Last tracked day, inclusive. The window spans each date in both UTC and the org's analytics timezone (the wider of the two) |
| `is_active` | boolean | `false` when auto-disabled |
| `last_scraped_at` | ISO 8601 \| null | When this account was last refreshed |
| `created_at` | ISO 8601 | |
| `updated_at` | ISO 8601 | |

Fetch accounts via `GET /api/v2/accounts` (filter by `creator_id`, `project_id`, or `platform`), `GET /api/v2/creators/{id}/accounts`, or embedded in a creator. Per-account follower/stat history: `GET /api/v2/accounts/{id}/stats`.

> **Note:**
>
> When an account's `tracking_mode` is `selective`, some recent videos may be in `pending_review` and excluded from stats until reviewed. The same applies in `hashtag` mode for videos whose caption matched no tracked hashtag (or had no caption at all). Different accounts on the same creator can use different modes.

### Video

| Field | Type | Notes |
|-------|------|-------|
| `id` | uuid | |
| `creator_id` | uuid \| null | Null for direct-add videos with no matched creator |
| `account_id` | uuid \| null | The account this video belongs to |
| `platform` | `tiktok` \| `instagram` \| `youtube` | |
| `platform_video_id` | string | Platform-side video/post ID |
| `url` | string | Full URL on the platform |
| `media_type` | `video` \| `photo` | `photo` = a TikTok photo carousel or Instagram image post/carousel; otherwise `video` |
| `title` | string \| null | Short title. TikTok: the caption's first line (up to 100 characters). Instagram: null |
| `caption` | string \| null | The post's text as posted on the platform (YouTube: the video description). Filled by scraping, read-only |
| `description` | string \| null | **Deprecated.** An old free-text field that nothing fills and that can't be written any more. Any text it had was copied into `readme`. Ignore it; it will be removed |
| `readme` | string \| null | **Agent context** / notes (agents append with `POST /api/v2/videos/{id}/notes`) |
| `thumbnail_url` | string \| null | |
| `view_count` | number \| null | Current snapshot (plays on Reels/Shorts) |
| `like_count` | number \| null | |
| `comment_count` | number \| null | |
| `share_count` | number \| null | |
| `tracking_status` | string | `auto_tracked`, `pending_review`, `included`, `excluded`, `direct_add` |
| `posted_at` | ISO 8601 \| null | Original post date on the platform |
| `last_scraped_at` | ISO 8601 \| null | |

> **Note:**
>
> `excluded` videos are never returned by the API.
>
> A **"tracked post"** (a video that counts toward limits, goals, and accruals) is one whose `tracking_status` is `auto_tracked`, `included`, or `direct_add` **and** which is not individually tracking-disabled. `pending_review` and `excluded` videos, and tracking-disabled videos, are not tracked posts.

### Content Group

Links the same logical content across platforms (e.g. the same clip posted as a TikTok, a Reel, and a Short) so its combined stats can be viewed together. A video belongs to **at most one** content group.

| Field | Type | Notes |
|-------|------|-------|
| `id` | uuid | |
| `project_id` | uuid | |
| `org_id` | uuid | |
| `title` | string | |
| `notes` | string \| null | Free-form notes |
| `created_by` | uuid \| null | |
| `video_ids` | uuid[] | Member videos (across platforms) |
| `created_at` | ISO 8601 | |
| `updated_at` | ISO 8601 | |

Manage via `/api/v2/content-groups` (list/create) and `/api/v2/content-groups/{id}` (get/patch/delete; add or remove members with `add_video_ids` / `remove_video_ids`). Combined stats: `GET /api/v2/content-groups/{id}/stats` returns summed `totals` plus a `by_platform` breakdown.

### Campaign

Campaigns are **mixed-platform**: the creators and videos they contain can be on any platform, and stats roll up across all of them with a per-platform breakdown.

| Field | Type | Notes |
|-------|------|-------|
| `id` | uuid | |
| `project_id` | uuid | |
| `name` | string | |
| `readme` | string \| null | **Agent context**: strategy, benchmarks, instructions (campaigns use `readme`, not `description`) |
| `target_views` | number \| null | Optional total-view goal for the campaign |
| `created_at` | ISO 8601 | |
| `updated_at` | ISO 8601 | |

> **Note:**
>
> Campaigns can contain sub-campaigns. `GET /api/v2/analytics?entity=campaign&entity_id=<id>` recursively aggregates the full tree.

### Goal

A per-creator performance target. Goals carry a **platform scope** so a target can apply to all of a creator's accounts, one platform, or one account.

| Field | Type | Notes |
|-------|------|-------|
| `id` | uuid | |
| `metric` | `posts` \| `views` \| `engagement` \| `followers` | |
| `period` | `daily` \| `weekly` \| `monthly` | |
| `target_value` | number | |
| `platform_scope` | `all` \| `platform` \| `account` | `all` = whole creator (default) |
| `platform` | `tiktok` \| `instagram` \| `youtube` \| null | Required when `platform_scope` is `platform` |
| `account_id` | uuid \| null | Required when `platform_scope` is `account` |
| `start_date` | ISO date \| null | |
| `end_date` | ISO date \| null | |
| `is_active` | boolean | |

Compliance is measured only over the accounts the scope includes. Create via `POST /api/v2/creators/{id}/goals`.

## Context field conventions

Humans write free-text context for agents to read. The context field is **`readme`**, a Markdown document on the organization, every project, campaign, creator, video, payout method, payout structure and payout payment, and every member (their personalization document). It is the **README** or **Notes** in the app. Find them with `GET /api/v2/notes` (MCP `list_notes`), read one with `GET /api/v2/notes/{entity_type}/{id}` (`get_notes`), and add to one with `POST /api/v2/notes/{entity_type}/{id}` (`update_notes`), which appends by default and never overwrites what people wrote. Everyone in the organization, and their agents, can read every document. See [Notes and documents](https://www.trackagoat.com/docs/agents/notes.md). On a **video**, `caption` holds the post's text from the platform (read-only, not a notes field); the old `description` field is deprecated. Common patterns:

```text
# Campaign readme
"Q1 creator push for Product X. Target: 1M views by March 31.
Top performers from last quarter: @creator1, @creator2.
Exclude any videos not directly about Product X."

# Creator readme
"Fitness creator, 3x/week posting cadence. Best content: workout tutorials.
Posts on TikTok and Instagram; YouTube is dormant.
Known issue: also posts food content unrelated to program: use selective mode.
Last contract renewal: Jan 2026."

# Video readme
"Strong outlier: 10x normal views. Trending audio #fitnesscheck helped.
Not representative of creator's normal performance."
```

When a context field contains explicit analysis instructions, follow them over your general analysis approach.

---

## Conversion entities

### Conversion Event

A project-scoped definition of an outcome worth tracking. Conversions reference it.

| Field | Type | Meaning |
|-------|------|---------|
| `key` | string | Stable identifier, unique per project. Matches `^[a-z0-9][a-z0-9_.-]{0,62}$`. **Immutable** after creation |
| `name` | string | Human label |
| `default_value_cents` | integer | Used when a conversion omits `value_cents` |
| `currency` | string(3) | ISO code. Lives on the EVENT, not the conversion, so totals are always well defined. **Immutable** |
| `is_active` | boolean | Inactive events reject new conversions but keep existing ones in all totals |

Plan limit `max_conversion_events_per_project` caps **active** event types (free 3, starter 25, ultra unlimited). Conversion volume is never capped.

### Conversion

| Field | Type | Meaning |
|-------|------|---------|
| `conversion_event_id` | uuid | The event this records |
| `target_type` | `creator` \| `account` \| `video` | What it was attributed to |
| `creator_id` / `account_id` / `video_id` | uuid \| null | Rollup. A video-target conversion also populates account and creator |
| `value_cents` | integer | Falls back to the event's `default_value_cents` |
| `quantity` | integer | Default 1 |
| `total_value_cents` | integer | Generated: `value_cents * quantity` |
| `external_id` | string \| null | Your id. Permanently unique per project: a repeat returns the ORIGINAL with `deduped: true` |
| `status` | `recorded` \| `voided` | Voided conversions are excluded from totals but retained |
| `occurred_at` | timestamp | ISO 8601 **with offset** required on write |

**Attribution is frozen at receipt.** Moving a video to another account later does not rewrite its past conversions. An org admin can explicitly override this per video.

**Reading totals:** filtering by `creator_id` returns conversions recorded against that creator's accounts and videos too. Do not sum across levels or you will double count.

## Payout entities

### Payout Method

| Field | Type | Notes |
|-------|------|-------|
| `id` | uuid | |
| `org_id` | uuid | |
| `name` | string | e.g. "Venmo", "Cash App" |
| `icon_url` | string \| null | Public icon URL |
| `is_default` | boolean | Auto-seeded defaults cannot be deleted |
| `is_active` | boolean | Disabled methods won't appear in payment dialogs |
| `metadata` | object | Agent-writable |
| `readme` | string \| null | Agent-writable notes |

### Payout Structure

| Field | Type | Notes |
|-------|------|-------|
| `id` | uuid | |
| `project_id` | uuid | |
| `name` | string | |
| `is_active` | boolean | Archived structures stop generating accruals |
| `current_version` | object | Active rule snapshot: `period`, `amount_cents`, `currency`, `criteria_type`, `criteria_params`, `proration_mode`, `platform_scope`, `platform`, `account_id` |
| `versions` | array | Full version history (GET by ID only) |
| `assignment_count` | number | Currently assigned creators |
| `metadata` | object | Agent-writable |
| `readme` | string \| null | Agent-writable notes |

**criteria_type values:** `recurring` (flat-rate every period), `goals` (pays when creator meets their period goals), `cpm` (pays per 1,000 views; each period pays for the views each video gained since it was last paid, for `view_lifetime_days` after posting or forever), `view_threshold` (one-time bonus when an individual video crosses a view-count milestone, `period` and top-level `amount_cents` are `null`; each threshold defines its own amount).

**Platform scope:** a version's `platform_scope` (`all` \| `platform` \| `account`, default `all`) limits which of an assigned creator's accounts contribute, CPM impressions, goal compliance, and view-threshold crossings are all measured only from accounts in scope. When scoped, `platform` or `account_id` is set accordingly.

### Payout Accrual

Computed by an hourly background job after relevant scrapers complete. Cannot be created via the API.

| Field | Type | Notes |
|-------|------|-------|
| `id` | uuid | |
| `creator_id` | uuid | |
| `structure_id` | uuid | |
| `structure_version_id` | uuid | The rule version active at period end (pinned) |
| `period_start_at` | ISO 8601 \| null | Inclusive, UTC. `null` for `view_threshold` accruals (no period) |
| `period_end_at` | ISO 8601 \| null | Exclusive, UTC. `null` for `view_threshold` accruals |
| `video_id` | uuid \| null | Set only for `view_threshold` accruals: the video that crossed the milestone |
| `threshold_views` | number \| null | Set only for `view_threshold` accruals: the milestone view count that fired |
| `amount_cents` | number | Post-proration, in cents |
| `currency` | string | Currently always `USD` |
| `status` | string | `earned` → `paid` (via payment) or `rejected`/`voided` (manual) |
| `inputs_snapshot` | object \| null | Data used to compute (goals_met, impressions, etc.) |

> **Tip:**
>
> To find unpaid accruals for a creator: `GET /api/v2/payout-accruals?creator_id=<id>&status=earned`

### Payout Payment

| Field | Type | Notes |
|-------|------|-------|
| `id` | uuid | |
| `project_id` | uuid | |
| `creator_id` | uuid \| null | Null for ad-hoc payments |
| `method_id` | uuid \| null | |
| `method_name_snapshot` | string \| null | Captured at write time |
| `total_amount_cents` | number | |
| `status` | string | `paid` or `voided` |
| `paid_at` | ISO 8601 | |
| `payout_payment_lines` | array | M2M to accruals with `applied_amount_cents` |
| `payout_adjustments` | array | Over/underpayment lines (positive = overpayment, negative = underpayment) |
| `metadata` | object | Agent-writable |
| `readme` | string \| null | Agent-writable notes |

> **Tip:**
>
> Use `auto_apply_outstanding: true` (with `creator_id`) on POST to settle all earned accruals for a creator in one call; it returns 409 `nothing_outstanding` if there are none. The `Idempotency-Key` header is required, and a retry with the same key returns the original payment.

---
Source: https://www.trackagoat.com/docs/agents/data-model
