# trackagoat docs (full) > trackagoat tracks every post from your creators' public TikTok and Instagram accounts, adds them up by campaign, works out what each creator is owed from your own payout structures (and can pay them through a connected payout account), and compares campaigns, creators and videos in Insights. It's built TikTok first and for teams that love working with AI coding agents: creators, videos, campaigns, goals, payouts and Insights are all in the REST API (v2, with an OpenAPI spec) and the hosted MCP server, every record has a Markdown document that people and agents read and write, and every docs page is available as Markdown. --- # trackagoat docs > How to track creators, run campaigns and payouts, and connect trackagoat to your own tools and agents. trackagoat tracks every post from your creators' public TikTok and Instagram accounts, adds them up by campaign, works out what each creator is owed from your payout structures, and compares campaigns, creators and videos in Insights. It's built TikTok first, and for teams that love working with coding agents: almost everything below can also be done through the REST API and the [MCP server](https://www.trackagoat.com/docs/agents/mcp.md). These docs cover setup, every feature, the REST API, and connecting AI agents. They're written for agents as well as people, and every page is also available as Markdown (add `.md` to its URL). To get going fast, start with the Quickstart, or [set up with your coding agent](https://www.trackagoat.com/docs/agents/setup.md). ## Start here - [Quickstart](https://www.trackagoat.com/docs/quickstart.md): Track your first creator in about ten minutes. - [Core concepts](https://www.trackagoat.com/docs/concepts.md): Orgs, projects, creators, accounts and campaigns, and how they fit together. - [Build with the API](https://www.trackagoat.com/docs/api-reference/overview.md): Keys, endpoints and the OpenAPI spec. ## By task - [Add creators and accounts](https://www.trackagoat.com/docs/guides/accounts.md) - [Decide which posts count](https://www.trackagoat.com/docs/guides/inbox.md) - [Group creators into campaigns](https://www.trackagoat.com/docs/guides/campaigns.md) - [Set goals](https://www.trackagoat.com/docs/guides/goals.md) - [Pay creators](https://www.trackagoat.com/docs/guides/payouts.md) - [Pay creators through Dots](https://www.trackagoat.com/docs/guides/integrated-payouts.md) - [Compare campaigns, creators and videos](https://www.trackagoat.com/docs/guides/analytics.md) - [Share a creator's stats with them](https://www.trackagoat.com/docs/guides/shareable-pages.md) - [See which creators need attention](https://www.trackagoat.com/docs/guides/program-health.md) - [Connect an AI agent](https://www.trackagoat.com/docs/agents/guide.md) - [Write notes your agents read](https://www.trackagoat.com/docs/agents/notes.md) ## What trackagoat tracks - **Creators**: the people or brands in your program. Each creator owns one or more **accounts** on TikTok or Instagram. - **Accounts**: one platform presence. Followers, likes and posts are collected per account, and plan limits count accounts. See [Creators and accounts](https://www.trackagoat.com/docs/guides/accounts.md). - **Videos**: every post from a tracked account, with views, likes, comments and (on TikTok) shares and saves over time. - **Campaigns**: groups of creators, videos and sub-campaigns whose stats roll up. - **Goals and payouts**: per-creator targets, and what each creator has earned under your payout rules. - **Documents**: every organization, project, campaign, creator, video and payout record has a Markdown document, and so does every teammate. Your team and its agents read them before acting and add what they find. See [Notes and documents](https://www.trackagoat.com/docs/agents/notes.md). ## How data flows > **Note:** > > trackagoat collects data on a schedule, not in real time. Profiles update daily. New posts are found within hours. A video's stats are re-checked every 12 hours for its first three days, then less often as it ages. See [Data freshness and scraping](https://www.trackagoat.com/docs/guides/scraping.md) for the full schedule. When you add an account or video, trackagoat queues an update. A background worker collects the public data and writes it to your org, and every number in the dashboard shows when it was last fetched. How long stats history is kept depends on your plan. --- Source: https://www.trackagoat.com/docs --- # Quickstart > Set up your account and track your first creator in minutes. This guide takes you from a new account to your first creator's stats. It takes about ten minutes. **Set up trackagoat with your coding agent.** Paste this prompt into Claude Code, Codex, Cursor or another coding agent. Your agent walks you through signing up and connects the MCP server, and never handles your password or API key. Prompt to paste: ```text Help me set up trackagoat. trackagoat tracks creators' public TikTok and Instagram posts, works out what each creator is owed from the payout structures I set up, and shows which creators need attention. Its MCP server can read all of that and do the day-to-day work too: add creators and videos, set goals, manage payout structures and record payments. Every record in trackagoat, and each person, has a Markdown document: read the relevant ones before you act, and add what you find. Read the setup guide first and follow it: https://www.trackagoat.com/docs/agents/setup.md Rules: - Don't create accounts, sign in for me or type passwords. Tell me what to open and click, then wait for me. - Don't ask me to paste an API key into this chat, and never print, log or commit one. If a key is needed, I'll set the TRACKAGOAT_API_KEY environment variable myself. Steps: 1. Ask whether I already have a trackagoat account. If not, send me to https://www.trackagoat.com/signup (Google, or email and password). After sign-up, trackagoat asks for my name, an organization name and a first project, then offers to add a first creator. Wait until I say I'm signed in. Every plan includes MCP and API access. New organizations start on Free, which allows 200 requests a day (each MCP tool call counts as one) and 1 API key. Starter and Ultra raise those limits; to upgrade, I email support@trackagoat.com with my organization name and the plan I want. If a call fails with rate_limit_exceeded, wait the retry_after_seconds it returns before trying again. 2. Connect the trackagoat MCP server: https://www.trackagoat.com/api/mcp (Streamable HTTP). Use sign-in (OAuth) first, because it needs no API key: a trackagoat page opens, I pick the organization and choose "View data", plus "Make changes" if you should be able to change things, such as adding creators, setting goals or recording payouts. Set it up for the client you are running in: - Claude Code: run `claude mcp add --transport http trackagoat https://www.trackagoat.com/api/mcp`. Then I run /mcp, choose trackagoat and sign in. - Codex CLI: run `codex mcp add trackagoat --url https://www.trackagoat.com/api/mcp`, then `codex mcp login trackagoat`. - Cursor: add `"trackagoat": { "url": "https://www.trackagoat.com/api/mcp" }` under "mcpServers" in ~/.cursor/mcp.json. Then I sign in from Cursor's MCP settings. - VS Code: add `"trackagoat": { "type": "http", "url": "https://www.trackagoat.com/api/mcp" }` under "servers" in .vscode/mcp.json. Then I start the server and sign in. - Claude.ai or Claude Desktop: I add a custom connector with that URL in Settings → Connectors. You cannot do this step. Sign-in is the best way for each person to connect their own agent: it identifies them directly, so nobody shares a key. Only if this client can't sign in, use an API key instead. An org admin creates one in trackagoat under Org settings → API keys; the key acts as one member (under "Acts as"), so it should act as me. I set it as TRACKAGOAT_API_KEY. You configure the client to send `Authorization: Bearer `, read from that variable, never written out. The setup guide has the exact config for each client. 3. Check the connection: call the list_projects tool and show me my project names. If the trackagoat tools don't appear yet, ask me to restart this session or reload the client, then try again. Then call get_my_profile to see who you're connected as and whether I already have a personalization document; if I do, follow it. 4. Offer to write my personalization document together, so every future session starts knowing me. It's the Markdown document in trackagoat under Settings → My README, and trackagoat sends it to you at the start of each session. Ask me a few short questions, one or two at a time: - who I am and my role; - what my job is in running the creator program; - the program, creators and goals I care about most; - how I most want you to help; - preferences: tone, how often to check in, what to look at first, and what you should never do without asking. Draft it in Markdown with these headings: About me, My job in the program, What I care about, How to help me, Preferences, Never without asking. Show me the draft, and save it with update_my_readme only after I approve. Everyone in my organization, and their agents, can read it, so leave out anything private. If I say no, move on. Later, when my role or preferences change, offer to update it. 5. Then ask what I want to do first: add creators by TikTok or Instagram handle (add_creator), see how the program is doing (get_program_health), or give a creator a posting goal (create_goal), or set up a payout structure (create_payout_structure). ``` **Step 1: Create an account** Go to [trackagoat.com](https://www.trackagoat.com) and sign up with Google or email/password. After signing up you'll enter the **onboarding wizard**, which takes you through creating your organization, setting up a project, and adding your first creator. **Step 2: Create an organization** An organization is your top-level workspace. Give it a name that reflects your team or brand. You can invite teammates to your org later from **Org settings → Members**. **Step 3: Create a project** Projects organize your creators and campaigns. A good first project is something like `Q1 Program` or `Brand Ambassadors`. Each org can have multiple projects (depending on your plan tier). **Step 4: Add a creator** In your project, go to **Creators → Add creator**. A creator is a person or brand, and you track them by adding at least one **account**: one of their presences on a platform. Paste in a profile URL or handle; trackagoat detects the platform automatically: ```bash https://www.tiktok.com/@username # TikTok https://www.instagram.com/username/ # Instagram ``` trackagoat will kick off an initial update on that account. The creator will appear in your list within a minute or two once the first update completes. You can add more accounts (even on another platform) to the same creator later. See [Creators and accounts](https://www.trackagoat.com/docs/guides/accounts.md). **Step 5: View stats** Click on the creator to open the detail view. You'll see: - **Follower count**: current and historical trend - **Total likes**: cumulative engagement - **Video count**: how many videos they've posted - **Recent videos**: the latest content with per-video stats Stats update on a schedule, not in real time. Profiles update daily, new posts are found within hours, and a video's stats are re-checked every 12 hours for its first three days, then less often as it ages. See [Data freshness and scraping](https://www.trackagoat.com/docs/guides/scraping.md) for the full schedule. On paid plans, org admins can also trigger a manual refresh with **Update Now**. **Step 6: Set a goal (optional)** In the creator detail view, go to the **Creator Goals** tab. Creator Goals let you define a performance target: for example, "post at least 3 videos per week": and trackagoat will track compliance over time. See [Creator Goals](https://www.trackagoat.com/docs/guides/goals.md) for details. **Step 7: Write your README (optional)** Every organization, project, campaign, creator and video has a **README**: a Markdown document for your team's context, such as what the program is for and the rules creators follow. You also have your own, under **Settings → My README**, that tells AI agents who you are and how you want help. Coding agents read these before they act and add what they find. Everyone in your organization can read them, so leave out anything private. See [Notes and documents](https://www.trackagoat.com/docs/agents/notes.md) for a template. ## What's next - [Set up a campaign](https://www.trackagoat.com/docs/guides/campaigns.md): Group creators into a campaign to aggregate stats across your program. - [Set up payouts](https://www.trackagoat.com/docs/guides/payouts.md): Create a payout structure and assign it to creators. - [Connect a coding agent](https://www.trackagoat.com/docs/agents/setup.md): Connect Claude Code, Codex or Cursor to trackagoat over MCP. - [Use the API](https://www.trackagoat.com/docs/api-reference/overview.md): Access your data programmatically with an API key. - [Review your limits](https://www.trackagoat.com/docs/guides/limits.md): Understand what your plan allows. - [Invite your team](https://www.trackagoat.com/docs/guides/team.md): Add teammates and see what org admins and members can do. --- Source: https://www.trackagoat.com/docs/quickstart --- # Core concepts > Understand how organizations, projects, creators, accounts, videos, campaigns, and goals fit together in trackagoat. ## Data model 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). **Accounts sit between creators and videos**: videos are discovered from, and hang off: an account, and the tracking configuration and raw follower/engagement stats live on the account, not the creator. ```text Organization └── Project(s) ├── Creator(s) : a person or brand (owns 1..n accounts) │ └── Account(s) : one platform presence, tiktok | instagram │ └── Video(s) : videos/posts discovered from that account ├── Video(s) : standalone videos added by URL (carry a platform + account) ├── Content Group(s) : link the same content across platforms └── Campaign(s) : named groupings of creators, videos, and sub-campaigns ``` > **Note:** > > If you tracked TikTok creators before Trackagoat 2.0, nothing changed for you: each creator was migrated to have one TikTok account, and all history, videos, campaigns, goals, and payouts carried over. You only see a difference once you add an Instagram account to a creator. See [Creators and accounts](https://www.trackagoat.com/docs/guides/accounts.md). ### Organization An **organization** is the root of your workspace. All creators, projects, campaigns, and API keys belong to an org. Your subscription tier (Free / Starter / Ultra) is set per org and determines your limits. You can belong to multiple orgs. Roles within an org are **admin** (can manage members, settings, and API keys) or **member**. ### Project A **project** groups creators and campaigns for a specific program or time period. Examples: `Q1 Brand Program`, `Holiday Campaign 2026`, `Always-On Ambassadors`. Each project has: - A **creators list**: the people/brands tracked in this project - An **accounts list**: every platform account across those creators (the things that actually get scraped) - A **videos list**: individual videos tracked in this project (discovered automatically or added manually) - **Campaigns**: named groupings you can analyze together - **Creator Goals**: per-project goal overview - An **inbox**: videos in `pending_review` status waiting for your approval ### Creator A **creator** is a person or brand you track. A creator **owns one or more accounts**: even more than one on the same platform, and its page defaults to the **combined** performance across all of them. Each creator has: - A **stats history**: snapshots of follower count, total likes, and video count over time (summed across the creator's accounts) - A **goals list**: per-creator performance targets, each with a platform scope (all accounts, one platform, or one account) - A **change log**: automatic events when notable things happen (follower milestones, goal compliance changes) - A **notes** field (`readme`): free-form markdown for your team's context. If the notes change while you're editing them (a teammate or an agent saved first), saving stops and asks you to reload, and your unsaved draft is kept so you can copy it back in Creator-level stats (followers, likes, video count) are the **sum across the creator's accounts**. Add an Instagram account and the creator's follower total grows to include it. ### Account An **account** is one of a creator's presences on a single platform (TikTok or Instagram). Accounts are where tracking actually happens: discovery, stats snapshots, and scraping all operate per account. Each account has: - A **platform** and **handle** - A **tracking mode** (**Auto**, **Selective**, or **Hashtag**) that controls how newly discovered videos enter the system, different accounts on the same creator can use different modes - Its own **follower/following/likes/video** snapshot and **scrape-freshness** (`last_scraped_at`) - The list of **videos** discovered for it Because a creator on its own incurs no tracking cost, [plan limits](https://www.trackagoat.com/docs/guides/limits.md) count **accounts**: the thing that gets scraped: not creators. The **Accounts** tab (under *Tracking*) lists every account across all your creators in one place. See [Creators and accounts](https://www.trackagoat.com/docs/guides/accounts.md). ### Video A **video** is an individual post that belongs to an account (and thus to that account's creator and platform). Each video has: - A **platform** (`tiktok` / `instagram` / `youtube`) and a **media type** (`video` or `photo`) - An **account** it was discovered from (`account_id`) - Stats: `view_count`, `like_count`, `comment_count`, `share_count` - A **tracking status** that controls whether it's included in campaign and project stats #### Tracking statuses | Status | Meaning | |--------|---------| | `auto_tracked` | Discovered automatically; included in all stats | | `pending_review` | Discovered but waiting for manual review (Selective mode, or a non-matching/caption-less video in Hashtag mode) | | `included` | Manually approved; included in stats | | `excluded` | Excluded from stats, either manually, or automatically in Hashtag mode when "send non-matching to inbox" is off | | `direct_add` (shown as **Added Manually**) | Added directly by URL; always included | ### Conversion A **conversion** is an outcome your creators drove: a signup, a trial, a purchase. Each one references a **conversion event** (a project-scoped definition carrying a stable `key`, a default value, and a fixed currency) and is attributed to a creator, an account, or a video. Attribution **rolls up**: a conversion recorded on a video also counts toward that video's account and creator, so you can attribute at whatever precision your data supports and still report at any level. The rollup is computed when the conversion arrives and is not rewritten if the video later moves. Conversions arrive through the [v2 API](https://www.trackagoat.com/docs/api-reference/conversions.md) from your own backend. There is no in-app form for recording one. See [Conversions](https://www.trackagoat.com/docs/guides/conversions.md). ### Campaign A **campaign** is a named grouping of creators, videos, and/or other campaigns. It has **aggregate stats** that recursively sum across all items: you don't need to manually aggregate sub-campaigns. > **Tip:** > > Campaigns can nest: a campaign can contain sub-campaigns. trackagoat resolves the full tree when computing stats, and prevents circular references. Common uses: - A brand partnership that groups several creators - A monthly drop that groups specific videos - A nested hierarchy like `2026 Program → Q1 → January Launch` ### Goal A **goal** is a per-creator performance target. Examples: - Post at least 3 videos per week - Maintain a minimum view count per video - Hit a follower count milestone trackagoat tracks **compliance**: whether a creator is meeting the goal, and surfaces non-compliant creators in the goals overview. ### API key API keys give programmatic read access to your org's data. Keys are scoped to the organization of the user who created them. A key is shown once at creation and cannot be recovered. See [API keys](https://www.trackagoat.com/docs/api-reference/authentication.md) and the [API reference](https://www.trackagoat.com/docs/api-reference/overview.md) for details. ## Next steps - [Quickstart](https://www.trackagoat.com/docs/quickstart.md): Get your first creator tracked in minutes. - [Creators and accounts](https://www.trackagoat.com/docs/guides/accounts.md): How creators own accounts across TikTok and Instagram. - [API reference](https://www.trackagoat.com/docs/api-reference/overview.md): Read-only API for programmatic access to your org's data. --- Source: https://www.trackagoat.com/docs/concepts --- # Creators and accounts > Add creators and their TikTok and Instagram accounts, choose which posts count, and handle private or failing accounts. A **creator** is a person or brand. An **account** is one of their presences on a platform. Add a creator, give them one or more accounts, and trackagoat tracks every account's posts and stats. This page covers adding them, choosing which posts count, and what to do when an account goes private or stops updating. ## The model: creators own accounts A single creator can have: - A TikTok account - An Instagram account - More than one account on the same platform (for example, a main and a backup TikTok) That lets you track someone's whole footprint under one creator and see their combined performance. > **Note:** > > trackagoat tracks TikTok and Instagram accounts. YouTube isn't supported. > **Note:** > > If you tracked TikTok creators before Trackagoat 2.0, nothing changed for you: each creator was migrated to have one TikTok account, and all history, videos, campaigns, goals and payouts carried over. ## Adding a creator In your project, go to **Creators → Add creator** and add one or more accounts by: - **Profile URL**: the platform is detected automatically (`https://www.tiktok.com/@username`, `https://www.instagram.com/username/`). See [accepted links](#accepted-profile-links) below. - **Handle**: pick the platform, then enter `@username` or `username`. trackagoat starts a first update for each account straight away. The first stats usually appear within a few minutes. > **Note:** > > Creators belong to a project. The same account can be tracked in more than one project in the same org. ## Adding an account On any creator, click **Add account** and paste a profile URL or handle: - **TikTok**: `https://www.tiktok.com/@username` or `@username` - **Instagram**: `https://www.instagram.com/username/` or `@username` ### Accepted profile links Paste the link straight from the app or browser. Query strings (`?hl=en`, `?igsh=…`), a trailing slash, `www.`/`m.` and a missing `https://` are all fine. | Platform | Accepted | |----------|----------| | TikTok | `tiktok.com/@username`, `m.tiktok.com/@username`, and share links (`vm.tiktok.com/…`, `vt.tiktok.com/…`, `tiktok.com/t/…`), which are followed to the profile they point to | | Instagram | `instagram.com/username/`, `instagram.com/username/reels/`, `instagram.com/stories/username/…` | Handles are saved without the `@` and in lowercase, so `@SomeUser` and `someuser` are the same account. A link to a post, reel or video is rejected: paste the account's profile link or handle instead. Discovery, stats and analytics then work the same way on every platform. See [Instagram](https://www.trackagoat.com/docs/guides/instagram.md) for what Instagram exposes. ## Combined and per-account stats Every creator page defaults to the creator's **combined** performance across all their accounts: followers, views and posting cadence summed across platforms. An **account switcher** in the header filters any tab down to one account or platform. When a creator has two accounts on one platform, the switcher shows each handle. A creator's header shows a **primary handle**: their TikTok account's handle if they have one, otherwise the earliest-added account's. The API returns it as the creator's top-level `handle`. Creator-level stats (followers, likes, video count) are the **sum across the creator's accounts**. ## The Accounts tab The **Accounts** tab (under *Tracking* in the sidebar) lists every platform account across your creators, with its platform, creator, follower count, tracked-video count, tracking mode, last update and status. Filter by platform or search by handle. Select an account to open its page: its own stats, data freshness, a link to its creator, a link to the live profile, and the videos tracked for it. Instagram doesn't expose a total-likes figure, so *Total Likes* shows **Not available** there. ## Plan limits count accounts A creator on its own costs nothing to track, so plan limits count **accounts** (the thing that gets updated), not creators. Add as many creators as you like. See [Plans and limits](https://www.trackagoat.com/docs/guides/limits.md). ## Tracking modes Each **account** has a **tracking mode** that determines how newly discovered videos from that account enter the system. There are three modes. | Mode | Behavior | |------|----------| | **Auto** | All newly discovered videos are automatically added with status `auto_tracked` and included in stats. | | **Selective** | Newly discovered videos enter the **Tracking Inbox** with status `pending_review`. A team member must approve or exclude each video. | | **Hashtag** | Videos whose caption contains one of your tracked hashtags are auto-tracked. Everything else goes to the Inbox or is excluded. Your choice. | Change tracking mode in the **Settings** section of the creator's **Info** tab. That section edits the creator's primary account. Each account has its own mode; to set a different mode on another account, use [`PATCH /api/v2/accounts/{id}`](https://www.trackagoat.com/docs/api-reference/accounts.md). Use **Selective** mode when you want to curate which of an account's videos count toward your program: for example, if a creator posts off-topic content you don't want in your campaign stats. ### Hashtag mode Hashtag mode automates that curation. Define a list of hashtags, and any newly discovered video whose caption includes one of them is tracked automatically: no manual review. This fits the common case where a creator posts both sponsored and personal content, and only posts carrying your campaign tag should count. **Configuring it:** 1. Set the account's tracking mode to **Hashtag**. 2. Add one or more hashtags (up to **50**, each up to 100 characters). Type or paste them, pressing Enter or comma between each. The leading `#` is optional and case doesn't matter: `#Summer2026`, `Summer2026`, and `summer2026` are all stored the same way. 3. Choose what happens to everything else with **Send non-matching videos to the Tracking Inbox**: - **On** (default), non-matching videos land in the Inbox as `pending_review`, so you can still pull one in manually. - **Off**: non-matching videos are marked `excluded` immediately. They're still recorded and visible under *Show excluded* in the Inbox, but never counted in stats. > **Note:** > > **Hashtags are cleaned before they're saved.** A tag is stored lowercase, Unicode-normalized, with the leading `#` removed and any character that can't appear in a hashtag stripped out. So `#Black-Friday` is saved as `blackfriday`, which is how that campaign is actually tagged. > > Whitespace and commas separate one tag from the next, so `"#ad, black-friday"` gives you two tags: `ad` and `blackfriday`. > > The same cleaning is applied when reading a video's caption, so the two always line up: a caption written `#Black-Friday` also reduces to `blackfriday` and matches. > > The chips shown after you save are exactly what is stored. If one looks different from what you typed, that's why, and you can remove it and try again. > > Normalization runs in the database, so it applies no matter how the account is edited: the app, the API, or a direct integration. **How matching works:** - Matching runs on the video's **caption**, and only on true hashtag tokens. Tracking `ad` matches `#ad` but not the word "already" and not `#advertisement`. - A video matches if it contains **any one** of your tracked hashtags. - Matching works the same way on **TikTok and Instagram**. > **Warning:** > > **Instagram creators often put hashtags in the first comment** rather than the caption. trackagoat only reads the caption, so those posts will not match. If a creator does this, use Selective mode for that account instead. > **Note:** > > **Some videos arrive with no caption.** This happens on any platform, most often when TikTok scraping falls back to its secondary path, which returns no captions. Whatever the cause, a video with no caption always goes to the Tracking Inbox, even when *Send non-matching to Inbox* is off, so a scraping gap never quietly drops a post. **Two things hashtag mode does not do:** - **Editing your hashtag list does not re-classify videos you've already discovered.** The list applies to videos found *after* the change. This is deliberate: re-running the rules would overwrite approve/exclude decisions your team already made. - For the same reason, a video that landed in the Inbox because its caption was missing stays there even if a later scrape fills the caption in. Approve it from the Inbox. ## Creator detail tabs | Tab | What you see | |-----|-------------| | **Analytics** | Followers, views and posting charts: combined across the creator's accounts, or scoped to one via the account switcher. | | **Accounts** | Each of the creator's accounts with its platform, tracking mode and last update. Add another account here. | | **Videos** | All tracked videos for this creator across every account, each showing its platform, tracking status, and current stats. | | **Goals** | The creator's goals and compliance status. See [Creator goals](https://www.trackagoat.com/docs/guides/goals.md). | | **Payouts** | What the creator has earned and been paid. See [Payouts](https://www.trackagoat.com/docs/guides/payouts.md). | | **Conversions** | Conversions attributed to the creator. See [Conversions](https://www.trackagoat.com/docs/guides/conversions.md). | | **Info** | Display name, README, tracking settings (mode, hashtags, start and end dates), the shareable page (org admins), details, and **History**. | **History** on the Info tab is the creator's change log. It records automatic events: added to, removed from or re-added to tracking; re-enabled after an auto-disable; auto-disabled; account went private or public; tracking mode and tracking date changes; handle, display name, bio and avatar changes; campaign additions and removals; and follower, likes and video milestones. To remove a creator, use **Delete Creator** in the **⋯** menu on the creator page. Their stats history is kept. ## README Every creator has a **README** on the **Info** tab: a markdown editor for your team's context. Use it for: - Creator background, niche, and content focus - Relationship history and contact notes - Known issues (e.g. "tends to post late on Mondays") - Instructions for AI agents analyzing this creator The README is the `readme` field on creator objects in the API. Agents read it before they act and add their findings to it as a dated, signed section, without overwriting what you wrote. See [Notes and documents](https://www.trackagoat.com/docs/agents/notes.md). > **Tip:** > > AI agents read the `readme` field before analyzing a creator. If you're using trackagoat with an AI tool, write a clear README: it directly shapes the quality of agent analysis. ## Tracking start date Each account has a **tracking start date** that sets the lower bound for that account's stats history displayed in charts. If you added a creator mid-program, set this date to the program start so charts aren't misleading. Set it in the **Settings** section of the creator's **Info** tab (it edits the primary account), or per account through the API. ## Private accounts When a tracked account switches to private, it becomes the quietest kind of problem: its profile still updates (follower counts stay public), so nothing fails and nothing is auto-disabled, but no new post is ever discovered. Left unflagged, the creator simply reads as someone who stopped posting. trackagoat detects the switch on the next profile update and: - Marks the account with a **Private** badge on the creator page, the creators list, the accounts list and the account page. Hover it to see when it went private and what stops working. - Sends org admins an **Account went private** notification. - Raises an alert on [Program Health](https://www.trackagoat.com/docs/guides/program-health.md) listing every private account in the project. While an account is private: - New posts can't be discovered - View and engagement counts stop updating - Posting goals read as missed, because the posts aren't visible to us - Goal and CPM payout periods hold, waiting for post data that can't arrive The account stays tracked; nothing is disabled or deleted. Ask the creator to switch back to a public account; detection is automatic in both directions, and when it reopens you'll get an **Account is public again** notification, the badge clears, and we backfill the posts we can still reach. > **Note:** > > Privacy is reversible and deliberately separate from auto-disabling. A private > account never has its failure counter touched and never needs re-enabling. ## Auto-disabled creators If an account fails **5 consecutive** profile updates: usually because the handle was deleted or renamed: trackagoat automatically **auto-disables** it. A private account is never counted as a failure (see [Private accounts](#private-accounts)). An auto-disabled account stops being updated so quota isn't wasted on a handle that may be gone, and is shown with an **Auto-Disabled** badge. You can review auto-disabled creators on the **Auto-Disabled Creators** page (reachable from the creators list filter) or via the Tracking Gaps / Update Failures watchlists on [Program Health](https://www.trackagoat.com/docs/guides/program-health.md). To bring a creator back, an org admin clicks **Re-enable**. Re-enabling resets the failure counter and resumes updates. A **24-hour cooldown** applies per creator: if a creator was re-enabled within the last 24 hours, you'll need to wait before re-enabling again. You can re-enable creators individually or in bulk. > **Note:** > > If the handle changed, update it before re-enabling: otherwise the creator will simply fail again and be auto-disabled. ## When a creator changes their handle TikTok and Instagram give every account a permanent user ID that stays the same when the handle changes. trackagoat records it the first time it reads an account's profile, and uses it to follow a creator through a rename: - If an update finds one of the account's posts now listed under a new handle by the same user, the account is renamed to the new handle. Its posts, history, goals and payouts stay where they are, it is re-enabled if it had been auto-disabled, and the creator's History shows **handle changed** (detected automatically). - If you add the new handle as a separate account before that happens, trackagoat notices that both handles belong to the same user and flags it to our team, who merge the two accounts so the creator's posts and payout history are back in one place. Nothing is merged without review. - If an old handle is taken over by someone else, trackagoat does not copy the new owner's stats onto the account. It is treated as a failed update instead, and the account is auto-disabled after 5 of them if no rename is found. To fix a rename yourself, edit the handle on the account page instead of adding the new handle as a new account or creator. ## Removing a creator > **Warning: Soft delete** > > Org admins can remove a creator from a project with **Delete Creator** in the creator page's **More options** menu, or **Delete** on the Creators list (select several to remove them together). This is a **soft delete**: stats history is preserved and the creator's handles can be added again later. The creator's accounts are removed with it, so they stop being scraped and free their slots in your account limit (`max_accounts_per_org`) straight away. Other members don't see these controls. ## Related - [Instagram](https://www.trackagoat.com/docs/guides/instagram.md) - [Videos](https://www.trackagoat.com/docs/guides/videos.md) - [Inbox: approving posts](https://www.trackagoat.com/docs/guides/inbox.md) - [Data freshness and scraping](https://www.trackagoat.com/docs/guides/scraping.md) --- Source: https://www.trackagoat.com/docs/guides/accounts --- # Instagram > What Trackagoat tracks for Instagram Reels and posts, supported URL formats, and platform caveats. ## What's tracked Add an Instagram account to any creator and Trackagoat tracks its **Reels and posts** the same way it tracks TikTok: - **Views** (plays on Reels), likes, and comments per post - **Follower count** and posting cadence for the account - Automatic discovery of new posts on the same tiered schedule as TikTok - Comments, with sentiment and signals, on paid plans ([Comment Insights](https://www.trackagoat.com/docs/guides/comment-insights.md)) Reels are identified automatically and flagged as short-form; feed image posts and carousels are tracked as photo posts. ## Adding an Instagram account On a creator, click **Add account** and paste either: - A profile URL: `https://www.instagram.com/username/` - A handle: `@username` or `username` To track a specific post, paste its URL on the Videos page: - Reel: `https://www.instagram.com/reel/{shortcode}/` - Post: `https://www.instagram.com/p/{shortcode}/` ## Caveats Instagram is stricter than TikTok about what it exposes publicly, so a few things differ: - **Hidden likes.** When an account hides its like counts, Trackagoat keeps the last known value rather than showing zero. - **No view counts on image posts.** Instagram doesn't expose play counts for non-video posts, so those show `0` views. - **No share/save counts.** Instagram doesn't surface these publicly. - **Tracking Inbox view counts** update for the first couple of days after a post is found. Include a post to keep its views updating. - **Private accounts** can be added, but their posts can't be discovered or tracked until the account is public. They carry a **Private** badge so you can tell them apart from a creator who stopped posting. - **Collab posts** count for every tracked account whose profile they appear on, whichever account published them. - **Hashtags must be in the caption.** Hashtag tracking mode reads the caption only, so a creator who puts hashtags in the first comment won't match. Use Selective mode for those accounts. - **Handles must match exactly.** An account that doesn't exist on Instagram (a typo, or a renamed profile) is flagged unreachable after a few attempts and you're notified. - **Embeds.** Instagram posts link out to Instagram rather than embedding inline (Instagram's embed is login-gated and unreliable). ## Related - [Creators and accounts](https://www.trackagoat.com/docs/guides/accounts.md) - [Data freshness and scraping](https://www.trackagoat.com/docs/guides/scraping.md) --- Source: https://www.trackagoat.com/docs/guides/instagram --- # Videos > How videos are discovered and tracked across platforms, tracking status states, stats history, and manual video management. ## How videos are discovered Videos enter trackagoat in two ways: **Step 1: Automatic discovery** The scraper periodically checks each tracked account's recent videos and adds any new ones. Discovery frequency is set per org (subject to your plan's `min_discover_videos_hours` floor). **Step 2: Manual add** You can add a specific video by its URL: a TikTok video/photo, or an Instagram Reel or post. Go to **Project → Videos → Add video** and paste the URL; the platform is detected automatically. Manually added videos are shown with the **Added Manually** status (stored as `direct_add`) and are always included in stats. ## Platforms Every video belongs to an account and carries a **platform**: `tiktok` or `instagram`. In the videos list and the Tracking Inbox, each video shows a small **platform icon** so mixed-platform projects are easy to scan, and the videos list can be filtered by platform. See [Instagram tracking](https://www.trackagoat.com/docs/guides/instagram.md) for what's captured on Instagram. ## Tracking statuses Every video has a `tracking_status` that controls whether it's included in project and campaign analytics. | Status | How it's set | Included in stats? | |--------|-------------|-------------------| | `auto_tracked` | Discovered automatically while its account is in **Auto** mode | ✓ Yes | | `pending_review` | Discovered while its account is in **Selective** mode | ✗ No: awaiting review | | `included` | Manually approved via Tracking Inbox | ✓ Yes | | `excluded` | Manually excluded via Tracking Inbox | ✗ No | | `direct_add` (shown as **Added Manually**) | Added by URL | ✓ Yes | > **Warning:** > > The API's `/api/v2/videos` endpoint never returns `excluded` videos. If a video is excluded, it won't appear in API responses or campaign stats. ## Reviewing videos (Tracking Inbox) When an account is in **Selective** mode, new videos land in the **Tracking Inbox** with `pending_review` status. See [Inbox: approving posts](https://www.trackagoat.com/docs/guides/inbox.md) for bulk-review workflows. > **Note: Photo posts & carousels** > > TikTok photo carousels (`/photo/` posts) and Instagram image posts/carousels are tracked too. They're discovered and shown alongside videos with a link to the post. TikTok carousel stats are updated on the same age-based schedule as videos, and monthly once a carousel is over 90 days old (see [Scraping](https://www.trackagoat.com/docs/guides/scraping.md#automatic-schedule)). ## Video detail Click any video in the videos list to open the detail view: - **Stats**: current view, like, comment, and share counts - **Stats history**: chart of view/engagement counts over time - **Thumbnail**: preview - **Notes**: markdown notes for your team or agents - **Comments** (paid plans): up to 100 collected comments, most liked first, with sentiment, signal filters and when they were last collected. See [Comment Insights](https://www.trackagoat.com/docs/guides/comment-insights.md) > **Note:** > > Stats history shows all snapshots captured since the video was added to trackagoat. It does not backfill historical data from before the video was tracked. ## Stats update frequency Video stats are refreshed on an age-based schedule (newer posts more often), and never more often than your plan's `min_video_stats_hours` floor allows: once a day on Free. See [How scraping works](https://www.trackagoat.com/docs/guides/scraping.md#automatic-schedule). To see when a video's stats were last updated, check the data-freshness badge on the video header (or `last_scraped_at` in the API response). You can trigger a manual stats refresh with **Update Now** from the video detail view (subject to your plan's `max_manual_scrape_triggers_per_day` limit). ## Video notes Like creators, every video has a **notes** field, the README on the video's Info tab. In the API it's the `readme` field. Use it to annotate videos with content classification, context, or agent instructions. If the notes change while you're editing them, saving stops and asks you to reload, and your draft is kept. The post's own text from the platform is the **Caption**, shown on the Info tab and returned as `caption`; it can't be edited. - [Tracking Inbox](https://www.trackagoat.com/docs/guides/inbox.md): Review pending videos and use bulk approve/exclude actions. - [Analytics](https://www.trackagoat.com/docs/guides/analytics.md): Analyze video performance trends over time. - [API reference: Videos](https://www.trackagoat.com/docs/api-reference/videos.md): Access video data programmatically via the API. --- Source: https://www.trackagoat.com/docs/guides/videos --- # Data freshness and scraping > When trackagoat updates profiles, posts and stats, how to trigger an update yourself, and what to do when data looks stale. ## Overview trackagoat fetches creator data through a background scraper service, per platform account. Data is **not** fetched in real time: it's collected on a schedule and stored so you can chart trends over time. > **Note:** > > trackagoat is not affiliated with TikTok or Instagram. Data is collected from public profiles and posts. Stats reflect what was observed at update time, so they can be hours behind what you see on TikTok or Instagram directly. ## Three scrape types | Type | What it fetches | Runs for | |------|----------------|----------| | **Creator Profiles** | Follower count, total likes, video count, bio, avatar | Each tracked account | | **Video Stats** | View, like, comment, share, and save counts | Each tracked video | | **Discover Videos** | New videos posted by a tracked account since last check | Each tracked account | Each type runs on its own schedule and can be triggered independently. ### Discovery completeness Each discovery pass fetches a creator's most recent posts. To make sure nothing is permanently missed, trackagoat also runs a cost-bounded **reconcile**: every tracked creator periodically (roughly once a month) gets a deep re-scan that walks further back through their history and fills in any posts an earlier pass skipped. Accounts with a detected gap (where the number of posts trackagoat has captured is well below the post count shown on their platform profile) are **prioritized** for reconcile, so real gaps are typically closed within a cycle or two rather than waiting for the full monthly rotation. You can see which creators currently have a gap in the **Tracking Gaps** widget on the [Program Health](https://www.trackagoat.com/docs/guides/program-health.md) page. > **Note:** > > Photo carousels (TikTok `/photo/` posts) are fully supported: they're discovered, shown alongside videos with a link to the carousel, and their stats are updated on the same age-based schedule as videos while they're new (below). ## Automatic schedule Updates run automatically in the background. You don't need to do anything once an account is added. | What | How often | |------|-----------| | **Profiles** (followers, likes, post count) | Daily | | **New posts** (discovery) | About twice a day (once a day on Free) | | **Full-history re-scan** of each account | About every 30 days, sooner for accounts with a detected gap | | **Accounts whose last check failed** | Retried within about 2 hours | | **Video stats** | By the post's age (below) | Video stats are re-checked most often while a post is new, because that's when the numbers move: | Post age | TikTok video stats re-checked | TikTok photo carousel stats re-checked | |----------|-------------------------------|----------------------------------------| | Under 3 days | Every 12 hours | Every 12 hours | | 3 to 7 days | Every 24 hours | Every 24 hours | | 7 to 30 days | Every 48 hours | Every 48 hours | | 30 to 90 days | Weekly | Weekly | | Over 90 days | Every 2 weeks | Monthly | Instagram follows a similar curve and slows down sooner as posts age. When a new-posts check sees a TikTok post, the views, likes, comments, shares and saves it reads are saved as a stats update too, so recent posts are often fresher than this schedule. Posts waiting in the [Tracking Inbox](https://www.trackagoat.com/docs/guides/inbox.md) aren't on this schedule, but posts under 30 days old get their stats updated once a day, so you decide on current numbers. ### Plan floors Your plan sets the **minimum time between runs** of each update type for your org. The scheduler enforces these floors: if your org's saved interval is shorter than your plan allows, it runs at the plan floor instead. They are floors, not the refresh rate of any single video: even on Ultra, a video is re-checked on the age-based schedule above (12 hours at most often). | Plan | Profiles | New posts | Video stats | |------|----------|-----------|-------------| | **Free** | 24 hours | 24 hours | 24 hours | | **Starter** | 12 hours | 12 hours | 4 hours | | **Ultra** | 6 hours | 4 hours | 1 hour | Org admins can set longer intervals, never shorter than the plan floor. On **Free**, every update type runs at most once a day, so new posts and video stats are checked daily, including posts under 3 days old. ## Manual updates ("Update Now") Org admins can trigger an update immediately without waiting for the next scheduled run. This is useful when you just added a creator and want fresh data right away, or when you know a big video just dropped. **Where to find manual triggers:** - **Creator detail page**: "Update Now" button in the header. Refreshes that one creator's profile. The "Find New Videos" option in the ⋯ menu runs a discovery scan for that creator specifically. - **Video detail page → Details tab**: "Update Now" fetches fresh stats for that single video. - **Org Settings → Scraping tab**: triggers an org-wide update for all creators/videos at once. Three separate buttons, one per job type. When you click "Update Now", a dialog opens that: 1. Explains what will be fetched and for whom. 2. Shows your remaining daily quota (see below). 3. Asks you to confirm before anything runs. After confirming, a toast confirms the scrape started. The page data updates automatically when the job completes (usually under a minute). ## Daily update quota Manual updates count against a daily quota that resets at **midnight in your org's time zone**. Scheduled updates don't count. The Free plan has no manual updates; see the **Manual updates/day** column in [Plans and limits](https://www.trackagoat.com/docs/guides/limits.md) for each plan's current allowance. **What counts as one update:** - Clicking "Update Now" on a creator page = 1 - Clicking "Update Now" on a video page = 1 - Running an org-wide update type from the Scraping tab = 1 (however many creators or videos the org has) Adding a video does **not** count: its first stats update starts right away and is free. If you add many videos within a minute, some of them get their first stats from the next scheduled update instead. When you reach the limit, the "Update Now" dialog shows "Daily limit reached" and the button stays disabled until midnight in your org's time zone. Org admins can see today's usage under **Org Settings → Usage**. ## Viewing update status **Creator list**: a data-freshness badge on each creator shows when their profile was last updated. **Video list**: each video shows a tier badge indicating how often it's scheduled for updates. Hover the badge to see the next scheduled update time. **Video detail → Details tab**: shows the tier and next scheduled update. **Org Settings → Scraping tab** (admin only): shows the last run time, status (OK / Partial / Failed), and processed count for each job type across the whole org. ## Data freshness Stats are **snapshots**, not live counters. A view count of 12,400 is what we saw at the last update, not necessarily the current count on the platform. Every number shows when it was fetched, and creators and videos carry a freshness badge (Fresh, Turning stale, Stale). Charts are built from the stored snapshot history. A newly added creator or video shows no chart data until its first update completes. ## What if an update fails? If the scraper encounters an error (a platform rate limit, a data-provider outage, a handle that no longer exists), the job is retried automatically with exponential backoff. A creator that fails **5 consecutive** profile updates is automatically **auto-disabled** to stop wasting quota on a handle that may be gone. Auto-disabled creators stop being updated until an org admin re-enables them (subject to a 24-hour cooldown). See [Creators and accounts](https://www.trackagoat.com/docs/guides/accounts.md#auto-disabled-creators) for the re-enable flow. If you notice a creator hasn't updated in a long time, check: **Step 1: Check the handle** **Is the account's handle still valid?** The account may have been deleted, renamed, or made private. **Step 2: Check the Scraping tab** **Is the org-wide Scraping tab showing errors?** If all job types are failing, the scraper service itself may be down: contact support. **Step 3: Try a manual update** **Try a manual "Update Now"**: if it succeeds, the prior failure was transient. ## Scraping and plan limits The `min_*_hours` scrape frequency settings are **floors on scheduled scrapes**: they don't limit how often you can do manual triggers (that's the `max_manual_scrape_triggers_per_day` quota above). See [Plans and limits](https://www.trackagoat.com/docs/guides/limits.md) for all limit keys and how enforcement works. - [Limits](https://www.trackagoat.com/docs/guides/limits.md): Plan tiers and scrape frequency floors. - [Program Health](https://www.trackagoat.com/docs/guides/program-health.md): Monitor data freshness and pipeline status. --- Source: https://www.trackagoat.com/docs/guides/scraping --- # Inbox: approving posts > Reviewing pending videos in Selective and Hashtag tracking modes, and using bulk actions. ## What is the Tracking Inbox? The **Tracking Inbox** is where videos land when they need a human decision. Because tracking mode lives on the account (not the creator), different accounts on the same creator can behave differently. Videos arrive in the inbox from two tracking modes: - **Selective**, *every* discovered video starts as `pending_review`. - **Hashtag**: only videos whose caption does **not** contain a tracked hashtag, and only when *Send non-matching videos to the Tracking Inbox* is enabled. Videos with no caption at all also land here (see [Hashtag mode](https://www.trackagoat.com/docs/guides/accounts.md#hashtag-mode)). Either way, a `pending_review` video is **not** included in campaign or project stats until reviewed. Navigate to **Tracking Inbox** in the sidebar. ## Reviewing videos Each inbox item shows: - Video thumbnail and title - Owning creator and the platform it was posted on - Current stats (views, likes, comments, shares) - Post date Click an item to open its details. For each video you can: | Button | Result | |--------|--------| | **✓ Include** | Sets status to `included`. Video counts in all stats. | | **✕ Exclude** | Sets status to `excluded`. Video is hidden from stats and the API. | ## Bulk actions To review many videos at once: **Step 1: Narrow the list** Pick a creator in the creator filter, or leave it on all creators. **Step 2: Select videos** Check the boxes on the videos you want to act on, or use the checkbox at the top to select every loaded video. **Step 3: Include or exclude** Click **✓ Include** or **✕ Exclude** in the selection bar, then confirm. To include every pending video from one creator, pick that creator in the filter and click **✓ Include All**. > **Tip:** > > If an account regularly posts content that's always on-topic, switch it to **Auto** tracking mode instead of approving videos one by one. The Tracking Inbox is best for accounts that mix relevant and off-topic content. ## Filtering and sorting The Tracking Inbox toolbar has: - **Creator filter**: show only videos from one creator - **Sort**: **Newest first**, **Oldest first** or **Most views** ## Excluded videos Click **Show Excluded** to see videos you've excluded. Select one or more and click **↩ Re-include** to put them back in tracking. Click **← Back to Inbox** to return to pending videos. ## Tracking Inbox badge The navigation shows a **badge** with the count of pending videos. It updates as you include or exclude videos. - [Creators guide](https://www.trackagoat.com/docs/guides/accounts.md): Configure tracking modes and manage creator settings. - [Videos guide](https://www.trackagoat.com/docs/guides/videos.md): Understand video discovery, tracking statuses, and stats history. --- Source: https://www.trackagoat.com/docs/guides/inbox --- # Over-posting warnings > Understanding TikTok's daily posting limits and what the over-posting warning means for your creators. When one of a creator's accounts publishes too many videos in a single day, TikTok may limit distribution for the batch: not just the newest posts, but all videos from that day. trackagoat detects this pattern and surfaces a warning on Creator Insights and Video Insights. (This is a TikTok-specific suppression behaviour, so the detection runs per TikTok account.) ## What triggers the warning > **Warning: What triggers the warning** > > The warning fires when all three of these are true for the same account on the same day: > > 1. **≥ 7 videos posted** on a single calendar day (in the org's timezone). > 2. **≥ 2 of those videos** had fewer than **100 views** at least **2 hours after posting**. > 3. Those low-view videos were confirmed in our stats snapshots: not just zero on day one. The "2+ hours" window rules out videos that are simply new. A video that genuinely has under 100 views two hours in almost certainly received no distribution at all. ## Why TikTok does this TikTok's recommendation system rates each account's post frequency. When the system detects an unusually high number of posts from one account in a short window, it may: - **Limit feed injection** for the day's posts across all audience segments. - **Suppress the pushed-post batch**, sending all videos directly to the creator's existing followers rather than to non-follower For You pages. - **Carry suppression forward**, reducing organic reach for subsequent posts until the account re-establishes a normal cadence. This is an inferred model based on observed patterns: TikTok does not publicly document its suppression criteria. ## How to respond **Reduce daily posting frequency.** Spreading the same content across 2 to 3 days typically results in significantly more aggregate reach than publishing everything at once. **Re-publish affected videos.** A video that was suppressed can sometimes be re-published (new upload) to re-enter the distribution queue. Performance is not guaranteed but it gives the algorithm a fresh chance. **Check posting schedule in Content Calendar.** Use the Content Calendar view to identify days where a creator is planning multiple posts and space them out. **Review the Video Insights heatmap.** The Posting Time Heatmap on the Video Insights page shows posting frequency by hour and day of week: use it to identify systematic over-posting patterns. ## Severity | Badge color | Meaning | |---|---| | **Amber** (⚠) | Incident within the last 7 days: actionable now | | **Neutral** (ℹ) | Historical incident: useful context, no immediate action needed | ## Thresholds The detection uses conservative defaults to minimize false positives: | Threshold | Default | Meaning | |---|---|---| | Min posts per day | 7 | Days with fewer posts won't trigger detection | | View floor | 100 | Views at 2h+ post-publish; below this is flagged | | Min suppressed videos | 2 | Single low-view video is insufficient: needs ≥ 2 | These are conservative by design. A creator who occasionally posts 7 times and has one video under 100 views won't be flagged: the pattern requires multiple videos clearly receiving no distribution on the same over-posting day. - [Similar Creator Pools](https://www.trackagoat.com/docs/guides/similar-creator-pools.md): The similar usernames warning and how to respond. - [Content Calendar](https://www.trackagoat.com/docs/guides/content-calendar.md): Plan and space out creator posting schedules. - [Program Health](https://www.trackagoat.com/docs/guides/program-health.md): Program-wide alerts and health indicators. --- Source: https://www.trackagoat.com/docs/guides/over-posting --- # Similar-username warnings > What the "similar usernames" warning means and how to respond to it. When trackagoat detects that three or more creators in a project share a common username pattern: a shared prefix like `wyndywith*`, a shared suffix like `*_official`, or handles that are very close in spelling: it shows a **Similar usernames detected** warning on the Creator Insights page. ## Why this matters TikTok's recommendation algorithm and API behavior can be affected by network-level account relationships. Accounts that share naming conventions, cross-promote frequently, or appear to operate as a coordinated cluster may be: - **Grouped algorithmically**: TikTok may attribute engagement across the cluster rather than to individual accounts, affecting each creator's apparent reach. - **Rate-limited as a group**: Scraping infrastructure can be throttled when many accounts with similar names are fetched in the same session, leading to lower scrape success rates for the whole cluster. - **Penalized for perceived coordination**: Creators that appear to operate as sub-brands under a shared identity sometimes receive reduced organic distribution on individual videos. > **Note: Heuristic, not a guarantee** > > This is a heuristic: not every similar-named cluster is penalized. But if the warning also shows a depressed scrape success rate for the group (below the project average), there is empirical evidence that the clustering is affecting data reliability. ## How to respond **Separate projects**: Move related-named creators into their own project. This isolates their data and reduces the risk of cross-cluster throttling during scraping. **Use campaigns for grouped attribution**: If you want to track the collective performance of a creator family (e.g. all `wyndywith*` accounts as a single campaign), create a campaign that includes all of them. Campaign analytics aggregate across members without requiring them to share a project. **Encourage handle diversity**: When onboarding a new creator collective, suggest that each creator's handle be distinctive. This avoids triggering TikTok's network-detection heuristics and keeps scraping reliable. **Dismiss the warning**: If you've reviewed the cluster and decided it's not a concern, use the **×** button to dismiss the alert for this project. The dismissal is stored locally per browser. ## Technical details The detection algorithm groups handles using three strategies: | Strategy | Detects | Example | |---|---|---| | Common prefix | 3+ handles sharing 5+ leading characters | `wyndywith*` | | Common suffix | 3+ handles sharing 5+ trailing characters | `*_official` | | Edit distance | 3+ handles within 2 character edits of each other | `brandchef` / `brendchef` | Severity is set to **warn** when the cluster's 48-hour scrape success rate falls more than 20 percentage points below the project average. Otherwise it is shown as an informational notice. - [Over-posting](https://www.trackagoat.com/docs/guides/over-posting.md): TikTok's daily posting limits and suppression detection. - [Program Health](https://www.trackagoat.com/docs/guides/program-health.md): Program-wide alerts and health indicators. --- Source: https://www.trackagoat.com/docs/guides/similar-creator-pools --- # Campaigns > Creating and managing campaigns, adding items, understanding nested campaign stats, and circular reference prevention. ## What is a campaign? A campaign is a named grouping of creators, videos, and/or other campaigns within a project. It gives you **aggregate stats** across all its items: total views, likes, comments, shares: updated over time. Campaigns are **mixed-platform**: the creators and videos you add can be on TikTok or Instagram in any combination, and their stats roll up together with a per-platform breakdown (see [Campaign stats](#campaign-stats)). Common uses: - A brand partnership with 5 creators and their relevant videos across TikTok and Instagram Reels - A product launch broken into sub-campaigns by platform push date - An always-on program with quarterly sub-campaigns nested inside ## Creating a campaign In your project, go to **Campaigns → New Campaign** and enter a name. That's the only field in the dialog. After you create the campaign, open it and use the **Info** tab to write its README (see [Campaign README](#campaign-readme)). To set a view target or deadline, click **Edit** (see [View target and target date](#view-target-and-target-date)). ## Adding items A campaign can contain any combination of: | Item type | What gets counted | |-----------|-----------------| | **Creator** | All `included` / `auto_tracked` / `direct_add` videos from that creator | | **Video** | That specific video only | | **Sub-campaign** | Recursively: all items in that campaign and its children | To add items, open the campaign's **Members** tab. It has three sections, **Creators**, **Videos** and **Sub-campaigns**, and each has its own **+ Add** button. The button opens the add dialog on the matching tab. Search creators and videos by name; the Campaigns tab lists the project's other campaigns. ## Nested campaigns Campaigns can nest arbitrarily deep. When you fetch stats for a parent campaign, trackagoat recursively resolves the full tree: you never need to manually aggregate sub-campaigns. ```text 2026 Program └── Q1 ├── January Launch (videos by creator A) └── February Expansion (creators B & C) └── Q2 └── Spring Push (creators A, B, D + direct videos) ``` Fetching stats for `2026 Program` gives you the union of everything under it. > **Warning: Circular reference prevention** > > trackagoat prevents circular campaign references at the API layer. You cannot add Campaign B to Campaign A if Campaign B already (directly or transitively) contains Campaign A. The UI will show an error if you attempt this. ## Campaign stats The campaign stats view shows: - **Views over time**: daily total view count across all items in the campaign - **Top videos**: highest-performing videos by view count - **Top creators**: most viewed creators in the campaign - **Video count by day**: how many videos were added to the campaign over time - **By platform**: views and video count for each platform in the campaign, with each platform's share of total views Stats are computed over a configurable date range (default: last 30 days). ## View target and target date Give a campaign a view goal from **Edit** on the campaign page. Under **Goal (optional)**, set either field or both: - **View Target**: the total views you want the campaign to reach. - **Target Date**: the day the target is due, in your org's timezone. Campaigns with a target show in the **Campaign Progress** panel on Campaign Insights, with a progress bar, days left and a status: | Status | When | |--------|------| | **Completed** | Views have reached the target. | | **On Track** | Views are at 80% or more of the target and the date hasn't passed. | | **At Risk** | Views are under 80% of the target, or only a date is set, and the date hasn't passed. | | **Missed** | The date has passed without reaching the target. | Clear both fields to remove the goal. ## Campaign README Each campaign has a README on its **Info** tab. Use it for: - Campaign strategy and objectives - Notes on what the campaign includes or excludes - Context for AI agents ("this campaign excludes sponsored content") The README is readable and writable through the API as the `readme` field, and agents can add to it with the MCP tool `update_notes`. If it changes while you're editing it (a teammate or an agent saved first), saving stops and asks you to reload, and your unsaved draft is kept so you can copy it back in. See [Notes and documents](https://www.trackagoat.com/docs/agents/notes.md). - [Creator Goals](https://www.trackagoat.com/docs/guides/goals.md): Set per-creator performance targets and track compliance. - [Analytics](https://www.trackagoat.com/docs/guides/analytics.md): Analyze campaign performance trends over time. - [API reference: Campaigns](https://www.trackagoat.com/docs/api-reference/campaigns.md): Access campaign data programmatically via the API. --- Source: https://www.trackagoat.com/docs/guides/campaigns --- # Creator goals > Setting per-creator performance goals, understanding compliance tracking, and using the project goals overview. ## What is a goal? A goal is a per-creator performance target. trackagoat tracks **compliance**: whether a creator is meeting the goal over time, and surfaces non-compliant creators so you can act quickly. ## Creating a goal Open a creator, go to the **Creator Goals** tab, and click **Add goal**. You'll specify: - **Goal type**: what metric to measure (posts, views, engagement, followers) - **Target**: the threshold (e.g. 15 posts/week, 10,000 views/week) - **Period**: how often to evaluate (daily, weekly, monthly) - **Applies to**: which of the creator's accounts count toward the goal (see below) ### Applies to: platform & account scope Because a creator can have accounts on more than one platform, every goal has an **Applies to** scope that controls which accounts contribute to it: | Scope | Counts | |-------|--------| | **All platforms** (default) | Every account the creator has, across all platforms | | **One platform** | Only accounts on the chosen platform (e.g. only TikTok posts) | | **One account** | Only the single chosen account | Use a platform or account scope when a creator posts on several platforms but a goal only applies to one, for example a "15 posts/week" goal that should count only TikTok posts, not Instagram Reels. A creator can have several goals with different scopes at once (e.g. an all-platform followers goal alongside a TikTok-only posting goal). Compliance is measured **within the scope**: posts, views, engagement, and followers are counted only from the accounts the scope includes. Goals created before Trackagoat 2.0 are scoped to TikTok, so their behavior is unchanged. Via the API, goals carry `platform_scope` (`all` \| `platform` \| `account`) plus a `platform` or `account_id` when scoped; see `POST /api/v2/creators/{id}/goals`. > **Note: Which posts count** > > Posts and views goals only count a creator's **tracked posts**: videos and photo carousels with status `auto_tracked`, `included`, or `Added Manually` that aren't disabled. Posts still awaiting review in the Tracking Inbox (`pending_review`), excluded posts, and posts whose tracking has been disabled do **not** count toward goal progress. ## Compliance Each goal has a **compliance status**: | Status | Meaning | |--------|---------| | **On Track** | Creator is projected to meet the target by period end | | **Behind Pace** | Slightly below projected pace (70 to 99% of target rate) | | **At Risk** | Significantly behind pace (below 70% of target rate) | | **Completed** | Period has ended and target was met | | **Missed** | Period has ended and target was not met | Compliance is recalculated automatically as new stats are collected. - **Pace isn't judged at the start of a period.** For the first 15% of a period (about the first day of a week, or the first four days of a month) an unmet goal shows **On Track**, since nobody has had time to post yet. - **Posts goals count whole posts.** A creator is behind only once a whole post is due. With 3 posts a week, nothing is due until about Wednesday. - **Followers goals measure growth per account.** Each account is compared with its own follower count at the start of the period. An account added partway through is compared with its first count after it was added, so its existing followers don't count as growth. - **Goals count full periods only.** See [Which periods count](#which-periods-count) below. - **Each scope is its own goal.** If a creator has two goals for the same metric and period, both are shown when they cover different accounts, for example a TikTok posting goal and an Instagram posting goal. When two goals cover the same accounts, only the one with the higher target counts. ## Which periods count Weekly periods run Monday to Sunday. Monthly periods run from the 1st to the last day of the month. Both follow your organization's time zone. A goal is judged only for **full periods inside its start and end dates**: - It is first judged in the first period that starts on or after its start date. A weekly goal that starts on a Sunday, or any day after Monday, is first judged the following week. A monthly goal that starts after the 1st is first judged the following month. - It is last judged in the last period that ends on or before its end date. - A start date on a period's first day (a Monday, or the 1st), or an end date on its last day, includes that period. - The partial period at either end is **not judged at all**. It never shows **Missed** or **At Risk**, and it earns no goal-based payout. Where you see this: | Place | What it shows | |-------|---------------| | Add Goal / Edit Goal | A note under the dates, for example "This goal is first judged for the week of Oct 5 to 11. The partial week before it isn't judged and earns no goal-based payout." or "Last judged week: Sep 21 to 27. The partial week after it isn't judged and earns no goal-based payout." If no full period fits between the dates, the note says the goal is never judged. | | Creator Goals tab | Under each goal's status, the range it's judged in, for example "Judged Oct 5 to Dec 27". When you pick a week or month the goal isn't judged in, it shows **Not judged this period**; hover it for the reason, for example "Starts mid-week on Sun Oct 4. First judged week: Oct 5 to 11." | | Project goals overview | **Not judged** in the goal's column, with the same reason on hover and in the creator's detail. | | Program Health | The **Goal Status** widget counts goals not judged this period separately from On Track, At Risk and Missed. | | Creator share page | Goals not judged this period, with the reason. | | Payouts | See [Goals-based payouts and partial periods](https://www.trackagoat.com/docs/guides/payouts.md#goals-based-payouts-and-partial-periods). | ## Data advisories A small amber **⚠ caution icon** appears next to the status badge when a period has recently closed but data may still be processing. Hover it to see the reason. The icon only appears when a period closed within the **last 48 hours**, the window during which late-period posts may still be working their way through the scrape pipeline. Once 48 hours have elapsed, the icon disappears and the numbers are considered final. The underlying `advisory` object in the API also tracks `period_in_progress`, `scrape_stale`, and `not_yet_started` as raw flags for programmatic consumers, but only `period_recently_ended` drives the visible indicator in the UI. ## Project goals overview Go to **Creator Goals** in the sidebar to see compliance across all creators in your project. This gives you a quick at-a-glance view of who's on track and who needs attention. At-risk and missed creators are sorted to the top. Click any creator to open their detail and understand why they're flagged. ## Goal badges Creator cards and the goals overview show **compliance badges**: small colored indicators next to each goal. An advisory caution icon appears inline when the data may not be final (see above). ## Using goals with campaigns Goals are per-creator, not per-campaign, and campaign pages don't show goal compliance. Check compliance on **Creator Goals** or on a creator's **Goals** tab. To give a campaign its own goal, set a **View Target** and **Target Date** on the campaign. See [View target and target date](https://www.trackagoat.com/docs/guides/campaigns.md#view-target-and-target-date). - [Creators](https://www.trackagoat.com/docs/guides/accounts.md): Add creators and their accounts, configure tracking modes, and monitor performance. - [Campaigns](https://www.trackagoat.com/docs/guides/campaigns.md): Group creators and videos into campaigns with aggregate stats. - [Analytics](https://www.trackagoat.com/docs/guides/analytics.md): Analyze performance trends across your program. --- Source: https://www.trackagoat.com/docs/guides/goals --- # Content calendar > Visualizing creator posting schedules and planning your content program. ## Overview The **content calendar** in each project gives you a time-based view of when tracked videos were posted. It helps you visualize posting consistency, identify gaps, and plan your program cadence. Navigate to **Project → Content calendar**. ## What you'll see The calendar has one row per creator and one column per day, by posted date in your organization's time zone. Each cell shows: - The number of tracked videos that creator posted that day - Click a cell to see the videos The **Total** row and column add up the days and the creators. ## Filtering - **View**: a week or a month, with arrows to move between periods. - **Campaign**: only the creators in one campaign. - **Platform**: **All**, **TikTok** or **Instagram**. Picking a platform counts only that platform's posts in every cell and total, lists only creators with an account on it, and shows that account's handle. ## Using the calendar for planning The content calendar is a read-only view of historical posting: it shows you what happened, not what's scheduled. > **Tip: Useful for** > > Use it alongside goals to spot patterns: > > - Are creators posting consistently or in bursts? > - Are there week-long gaps that might affect campaign reach? > - Is posting frequency meeting your program requirements? For goal-based frequency tracking, see [Creator Goals](https://www.trackagoat.com/docs/guides/goals.md). - [Creator Goals](https://www.trackagoat.com/docs/guides/goals.md): Set per-creator performance targets and track compliance. - [Creators](https://www.trackagoat.com/docs/guides/accounts.md): Add creators, configure tracking modes, and monitor performance. - [Over-posting](https://www.trackagoat.com/docs/guides/over-posting.md): Detect and manage creators who are posting too frequently. --- Source: https://www.trackagoat.com/docs/guides/content-calendar --- # Payouts > Setting up and managing payout methods, structures, and payments in trackagoat. trackagoat's Payouts feature lets operators define how creators are compensated, track outstanding balances, and record payments: all inside the platform, without spreadsheets. ## Setting up payment methods **Payout methods** represent the platforms or channels you use to pay creators: for example Venmo, Cash App, PayPal, or a custom method like "Bank transfer". ### Default methods Every org starts with three built-in methods: **Venmo**, **Cash App**, and **PayPal**. These are pre-configured and cannot be deleted, but they can be disabled if you don't use them. ### Adding a custom method **Step 1: Go to Org Settings** Go to **Org Settings → Payout Methods**. **Step 2: Click Add method** Click **Add method**. **Step 3: Enter a name** Enter a name (e.g. "Zelle", "Bank transfer"). **Step 4: Optionally upload an icon** Optionally upload an icon (PNG, JPEG, WebP, or SVG; max 1 MB). **Step 5: Click Add method to save** Click **Add method** to save. Custom methods you create can be edited or deleted at any time. ### Disabling a method If you stop using a payment platform, you can disable it so it no longer appears in payment dialogs: **Step 1: Find the method in the list** Find the method in the list. **Step 2: Toggle the Active switch to off** Toggle the **Active** switch to off. Disabling a method does not affect historical payments that used it: those records are preserved with a snapshot of the method name. To re-enable a disabled method, toggle the switch back on. Note: if your org has reached its method limit, you may need to delete a custom method first. ### Method limits The number of custom payout methods available depends on your plan. Default methods (Venmo, Cash App, PayPal) count toward the limit. | Plan | Limit | |------|-------| | Free | 6 | | Starter | 10 | | Ultra | Unlimited | --- ## Setting up payout structures **Payout structures** define how creators earn money. There are five rule types: **recurring** (flat per-period pay), **goals-based** (pay when Creator Goals are met), **CPM** (per-view, cost per 1,000 impressions), **view threshold** (a one-time bonus when a video crosses a view milestone), and **conversions** (pay for signups, purchases and other outcomes you record). A structure also sets the period and, for recurring and goals-based rules, the amount and how a partial-period assignment is prorated. CPM, view-threshold and conversion rules carry their rates in the criteria itself, so they have no flat amount. Structures are project-scoped and versioned: editing a rule creates a new version and only affects future accruals. > **Warning: A creator must be assigned a structure to appear in Payouts** > > Adding a creator, putting them in a campaign, and giving them **Creator Goals** does not put them in Payouts. Earnings only accrue against an assigned payout structure, and the Payouts tracker lists creators who have earnings or payments, so an unassigned creator is simply absent from it. > > The Payouts page shows a banner naming any active creators with no structure assigned, with a button to assign one. If a creator you expect is missing from Payouts, check there first. ### Platform scope Like goals, every payout structure carries an **Applies to** scope, set when you create it and stored on the structure version: | Scope | What earns | |-------|-----------| | **All platforms** (default) | Posts and views from every account the assigned creator has | | **One platform** | Only that platform's accounts (e.g. pay CPM on TikTok posts only) | | **One account** | Only the single chosen account | Accruals honor the scope: CPM impressions, goals-based compliance, and view-threshold crossings are all measured **only from the accounts the scope includes**. Changing the scope creates a new version and only affects future accruals, exactly like editing any other rule field. Structures created before Trackagoat 2.0 are scoped to TikTok, so their behavior is unchanged. ### Setting up a recurring payout A recurring payout pays a creator a flat amount each period regardless of performance. Use it for guaranteed base pay. **Step 1: Navigate to Payout Structures** Navigate to **Tools → Payout Structures** and click **New structure**. **Step 2: Basics step** Give it a name (e.g. "Weekly base pay") and set the **Period** to Weekly. Enter the **Amount** in dollars (e.g. `50.00`). **Step 3: Criteria step** Choose **Recurring**. No additional parameters are required. **Step 4: Proration step** Choose how mid-period assignments are handled: - **Skip** (default): if a creator is assigned mid-period, they don't earn for that period. - **Prorate**: they earn a fraction proportional to how many days they were assigned. - **Pay in full**: they earn the full amount regardless of when they were assigned. **Step 5: Click Create to save** Click **Create** to save. After creating, assign it to creators from the structure's detail page (**Assignments** card) or via the bulk-action toolbar on the Creators page. ### Setting up a goals-based payout A goals-based payout pays a creator only when they meet performance goals. The "criteria" controls whether **all** goals must be met, **any** one goal, or a specified minimum number. **Step 1: Follow steps 1 and 2 above** Follow steps 1 and 2 above, but set the **Period** to match your goals: e.g. Monthly if your goals are monthly. **Step 2: Criteria step** Choose **Goals-based**: - **All goals**: creator must meet every active goal in the matching period. - **Any goal**: meeting at least one goal qualifies. - **N of M goals**: enter how many goals must be met (e.g. 2 of 3). **Step 3: Click Create** Click **Create**. > **Warning:** > > If you assign a goals-based structure to a creator who has no goals matching the structure's period, trackagoat will warn you at assignment time. The assignment will still be created, but accruals won't be computed until matching goals exist. #### Goals-based payouts and partial periods A goal counts only for full weeks or months inside its start and end dates (see [Which periods count](https://www.trackagoat.com/docs/guides/goals.md#which-periods-count)). A period where the creator's goal starts or ends partway through is not judged, so it pays nothing for that goal, and no accrual is created. The creator's Payouts tab (and the expanded row in the Payouts tracker) lists these periods as **No payout** with the reason, for example "No payout this period: goal starts mid-period (Sun Sep 6). First eligible period: Oct 2026." The current period is listed too, marked **Current period**, so you can see it before it closes. These rows have no amount and nothing to approve. #### Partial completion payouts (All goals mode only) When mode is **All goals**, you can enable partial completion so creators earn a fraction of the amount even when they miss one or more goals. Toggle **Allow partial completion payouts** in the criteria form. Two additional fields appear: | Field | Default | Description | |-------|---------|-------------| | **Payout multiplier** | `1.0` | Scales the partial payout. Set it between `0` and `1`. `1.0` = no scaling; `0.5` = pay half of the completion fraction. | | **Minimum completion % for payout** | `0%` | Below this average per-goal completion, no partial payout fires at all. Use this to avoid paying out tiny amounts for minimal effort. | **How the partial amount is calculated:** 1. For each active goal in the period, trackagoat measures `actual / target` (capped at 100% per goal). 2. Those per-goal fractions are averaged across all relevant goals → **completion%**. 3. If completion% is below the minimum threshold, no accrual is created. 4. Otherwise: `payout = structure amount × completion% × multiplier`. The multiplier can't be above `1`, so a partial payout is never more than the full amount. A creator who misses a goal can't earn more than one who meets them all. **Example:** $500/month structure, partial enabled, multiplier `1.0`, minimum `40%`. - Creator hits views goal (100%) but misses posts goal (60%) → avg 80% → payout = $400. - Creator hits views goal (30%) and misses posts goal (20%) → avg 25% → below 40% floor → no payout. - Creator hits both goals fully → full $500 (partial logic is skipped when all goals are met). > **Note:** > > Partial completion only applies when mode is **All goals**. Any and N-of-M modes remain binary. ### Setting up a CPM payout CPM (cost per 1,000 views) payouts pay creators for the views their videos get. Each period pays for the views the creator's videos gained since they were last paid, so a video keeps earning in later periods, not just the period it was posted in. Use it for performance-driven pay tied to reach. **Example (weekly, $0.50 per 1,000 views, views count forever):** a video posted on Saturday has 20,000 views when the week closes, so that week pays $10.00. It reaches 500,000 views the following week, so the next week pays $240.00 for the 480,000 new views. Every view is paid once. Only videos accepted into the program count: auto-tracked, included and directly added videos. A video still waiting in the Tracking Inbox (pending review) or excluded from tracking earns nothing until it is approved, and once it is approved the next period pays all of its views. Only videos posted while the creator is assigned count, and views stop counting when you unassign them. **CPM = (views gained in the period ÷ 1,000) × rate** **Step 1: Follow steps 1 and 2** Follow steps 1 and 2, setting the **Period** to Daily, Weekly or Monthly. CPM structures have no **Amount** or **Proration** field: earnings come entirely from the tiers you set in the next step, and the period is how often new views are paid out. **Step 2: Criteria step** Choose **CPM**. Additional fields appear: - **Count views for**: how long after posting a video keeps earning. **Forever** (the default) pays for every view a video gets while it is tracked. Pick 7, 30, 60 or 90 days, or a custom number, to stop paying a video after that. A video earns in every period that starts before its window ends. Views also stop counting when a video stops being tracked (for example, when your organization's video age rule stops tracking it), so with **Forever** a video earns for as long as it is tracked. - **Minimum views per period**: the views a creator must gain in a period before it pays (e.g. `10,000`). Views from a period below the minimum are not lost: they carry into the next period. - **Max payout per period**: an upper cap on earnings per period per creator (e.g. `500.00` to cap at $500/month). - **CPM tiers**: define rates by impression volume. Add rows to create tiered pricing: | Up to (impressions) | Rate per 1,000 | |---------------------|----------------| | 1,000,000 | $0.50 | | _(no cap)_ | $0.75 | In this example: views up to 1M earn $0.50/thousand; views above 1M earn $0.75/thousand. The final tier must have no cap. **Step 3: Click Create** Click **Create**. #### When a CPM payout is final trackagoat calculates a CPM accrual once the period closes and the account has been reconciled. It pays each video's views up to that moment and records the count it paid up to. The accrual does not change after that: views the video gets later are paid by the next period, so you can pay a CPM accrual as soon as it appears without cutting the creator short. > **Note: Changed on 2026-10-09** > > CPM used to pay only for the views of videos posted in the period, and stopped counting a video's views once its period was paid. Every CPM structure moved to the new rules on 2026-10-09, starting with the period in progress. Views from before that date that had not been paid were not back-paid. ### Setting up a view-threshold payout **View threshold incentives** pay a creator a one-time bonus the first time any of their videos crosses a specific view count. Thresholds are per-video and per-milestone: they fire exactly once, permanently, regardless of future assignment changes. **Example:** a $20 bonus when any video reaches 1,000,000 views, and a $100 bonus when any video reaches 2,000,000 views. Both can fire on the same video; they are independent. **How to set one up:** **Step 1: Go to Payout Structures** Go to **Project → Payout Structures → New structure**. **Step 2: Enter a name** Enter a name, then on the Criteria step choose **View Thresholds**. **Step 3: Add one or more milestones** Add one or more milestones: each with a **view count** and a **bonus amount**. **Step 4: Duplicate view counts in the same structure are not allowed** Duplicate view counts in the same structure are not allowed. **Step 5: Toggle Apply to existing videos** Toggle **Apply to existing videos** if you want videos that already pass a milestone to trigger accruals immediately when you assign the structure to a creator. Leave it off if you only want future crossings to count. **Step 6: Review and click Create structure** Review and click **Create structure**. **Key differences from other structure types:** - There is no payment period, period amount, or proration mode: the bonus fires once per milestone per video. - Accruals appear in the Creator's Payouts panel with a "Milestone" badge instead of a period range. - On the video's detail page, you can see which milestones the video has triggered and how much it has earned. - The **Program Health** dashboard shows a 30-day summary of threshold accruals across all threshold structures in the project. - Only videos accepted into the program fire milestones (auto-tracked, included and directly added). A video pending review in the Tracking Inbox fires once it is approved, if it is still over the milestone. - With **Apply to existing videos** off, a milestone a video had already passed before the creator was assigned does not fire. This is judged from the view counts trackagoat had recorded for the video by the assignment: a video with no recorded views from before the assignment (for example, one first found after you added the creator) is treated as new, so its milestones fire. After you unassign a creator, only milestones the video is recorded as passing by the time of the unassignment are paid. **How it differs from the Goals "views" metric:** Creator Goals track *aggregate views across all posts in a period*. View threshold incentives track *a single video's lifetime view count crossing a milestone*. Both features coexist and serve different incentive patterns. ### Setting up a conversion payout Pay creators for the outcomes they actually drive, using the conversions you already record. 1. Make sure the project has at least one **conversion event** (Tracking → Conversions → Manage events). 2. Create a structure and choose **Conversions**. 3. Pick a **roll-up cadence**: - **Every conversion**: each conversion becomes its own payout line as soon as it arrives. - **Daily / Weekly / Monthly**: a creator's conversions are summed into one line per period. Weekly can start on any day, so "every Tuesday" works. 4. Add the conversion events that count, and give each one a rate: - a **flat amount** per conversion (say $5 a signup), or - a **percentage of the conversion's value** (say 10% of every purchase). There is no period amount to set: the rates carry the money. > **Note:** > > **Refunds are handled.** Roll-up cadences wait 24 hours after the period closes before creating an accrual, so a conversion voided in that window never becomes a payout at all. If a conversion is voided later, an unpaid accrual is voided with it. If it had already been paid, the payment is left untouched and flagged for review instead, because rewriting settled money would make past payout reports irreproducible. > **Tip:** > > By default a creator only earns from conversions recorded **after** they were assigned. Turn on backfill at creation time if you are onboarding someone mid-campaign and want their earlier conversions to count. ### Assigning a structure: when earning starts Assigning a structure asks two questions before it takes effect. **Start on period** picks which period earning begins on. The options are real period boundaries in your org's analytics timezone: the period in progress, the next one, and the two after that. Choosing a later period means nothing accrues for the period already underway, which is the clean way to move a creator from one structure to another without the two overlapping. **First period** only appears when you start on the period in progress, because that is the only case with a partial period to decide about. It has two settings, and what they do depends on the structure type: | Structure type | Prorate from today | Include the whole period | |---|---|---| | Recurring, Goals | Pays the fraction of the period remaining from today | Pays the full period amount, as if assigned at its start | | CPM | Counts only videos posted, and views gained, from today onward | Counts videos posted since the period started, including before the assignment, and their views from the start of the period | The CPM row is the one worth pausing on. CPM earnings come from views, so "prorate" cannot mean a fraction of an amount, because there is no fixed amount. It moves the start of the window instead, and that start holds for every later period: with **Include the whole period**, videos posted earlier in the first period keep earning in later periods too. > **Note:** > > This choice applies only to the first period. Every period after it is whole, and the structure's own pro-ration mode governs the final period if you later unassign someone mid-period. Both questions appear wherever you assign: the bulk-action toolbar on the Creators page, the unassigned-creators banner, and the **Add Creators** dialog on a structure's own detail page. Structures with no period (view-threshold, and conversion structures on the *every conversion* cadence) skip both questions. They fire per video or per conversion rather than per period, and their **Apply to existing videos** / backfill toggle already decides whether history counts. ### Pro-ration mode The **proration mode** controls what happens when a creator is assigned or unassigned partway through a period: | Mode | Behavior | |------|---------| | **Skip** | Only assigns and unassigns that cover the full period are paid. Partial periods earn nothing. | | **Prorate** | Earnings are multiplied by the fraction of the period the creator was active. | | **Pay in full** | The creator earns the full period amount regardless of when they were assigned. | Proration applies to **recurring** and **goals-based** structures only. CPM earnings already count only the views gained while the creator is assigned, so there is nothing to prorate; view-threshold and conversion structures pay per event and always use **Skip**. The pro-ration mode set on the structure is the default for every period. The **First period** choice made when assigning a creator overrides it for that creator's first period only, so a structure set to **Skip** can still pay a prorated first period if you assign it that way. ### Editing a structure and version history Editing **rule fields** (period, amount, criteria, proration) creates a new version. Editing only the **name** is cosmetic and doesn't create a version. - Old accruals remain linked to the version that was active at the time they were computed: they are never retroactively changed. - A confirmation dialog shows field-level diffs before you save. - Version history is visible on the structure's detail page. - **The period and criteria type are fixed once a structure is in use** (it has any accruals, or a creator has ever been assigned to it). Changing either would re-create pay for periods that were already computed under the old setup. To move creators to a different period or criteria type, create a new structure, assign them to it starting on the next period, and archive the old one. The amount, criteria settings and proration can still be edited. - When you switch a structure that isn't in use yet to recurring, goals-based or CPM, you choose its period (and, for recurring and goals-based, its amount) as part of the switch; both appear in the confirmation diff. ### Common patterns **Base + bonus:** assign two structures to the same creator: a Recurring weekly base pay, and a Goals-based monthly bonus. Accruals from both structures are separate and can be settled in a single payment. **Top-creator bonus:** create a Goals-based monthly $200 structure with `Any` criteria, backed by a high-reach goal (e.g. 500K views). Assign it only to top-tier creators. **Tiered CPM with a floor:** set `min_impressions = 50,000` so a creator is paid once they gain 50,000 views (smaller periods carry over until they get there), and a two-tier CPM that rewards high-reach creators more per view. **Effort-based partial payout:** create a Goals-based monthly structure with **All goals** mode, enable partial completion, set multiplier to `1.0` and minimum to `50%`. Creators who come close but miss earn proportionally instead of nothing: reduces churn on near-misses without eliminating the incentive to fully meet goals. **Viral video incentive:** create a View Threshold structure with a $20 milestone at 1M views and $100 at 5M views. Assign alongside a Recurring base pay structure. The threshold accruals fire once per video per milestone, permanently: creators are rewarded the moment any video crosses the bar, regardless of when it was posted. --- ## Recording payments If your org pays creators through Dots, connect it and pay from trackagoat instead: the payment is sent and recorded in one step. See [Paying creators with Dots](https://www.trackagoat.com/docs/guides/integrated-payouts.md). Once accruals have been computed (the system runs hourly for all active assignments), **organization admins** can settle them by recording a payment. (Any organization member can view payouts; recording and voiding payments requires an org admin.) > **Note: Goal and CPM payouts wait for complete post data** > > Before a closed Goals or CPM period is calculated, trackagoat performs a deeper account reconciliation after the period ends. This avoids settling from an incomplete recent profile page. If an eligible post is discovered even later, an unpaid accrual is recalculated automatically. A payout already linked to a recorded payment is never rewritten; it is preserved and sent to the trackagoat operations team for finance review. > > A banned account is skipped straight away, and an account that keeps failing its reconciliation is skipped 72 hours after the period closes. In both cases the creator is paid from the posts already on record, and a post found later still updates an unpaid accrual. > **Warning: Private accounts hold goal and CPM payouts** > > If one of a creator's accounts is private, we can't see what it posted, so their Goals and CPM payouts for the period are **held**. A banner on the Payouts page lists held payouts. A held payout is paid automatically once the account is public again and its posts have been re-read. An organization admin can choose **Pay anyway** to release a hold early: the creator is paid on the posts already found, and the decision is recorded in the payout history. Payouts already calculated before the account went private are not changed. ### How to mark accruals as paid **Step 1: Navigate to the creator's Payouts tab** Navigate to a creator's detail page and click the **Payouts** tab. **Step 2: Click Mark a payment as paid** Click **Mark a payment as paid**. The sheet is a single form: every earned accrual arrives already checked and the running total is in the footer. **Step 3: Uncheck anything you're not paying** Uncheck any accrual you want to leave outstanding. Unchecking all of them records an ad-hoc payment instead (see below). **Step 4: Click Record payment** The total in the footer is what will be recorded. Everything optional lives under **Payment details**, collapsed by default with its current values summarised on the header line: - **Amount**: follows the checked accruals. Edit it to round, add a tip, or record a partial payment; **Reset** puts it back on the accrual sum. - **Payout method**: Venmo, Cash App, and so on. Leave blank for cash or informal payments. - **Date paid**: defaults to today. Backdate it to record a payment that was already sent (useful for backfilling historical payouts). - **Tracking URL**, **Receipt** (a file: PDF, image, spreadsheet), and a free-form **Note**. The section opens on its own when it holds something you have to fill in: an adjustment reason, or an amount when the creator has no outstanding accruals. The selected accruals move from **Earned** to **Paid** and are locked. The payment appears in the per-creator payment history. Every open payouts view refreshes on its own: the tracker row, an expanded row's accruals list, and the creator's Payouts tab all reflect the new status without a reload. ### Recording an ad-hoc payment Uncheck every accrual and enter an amount under **Payment details**. The system creates a payment row with an adjustment line for the full amount. This is useful for one-off bonuses, advances, or any payment not tied to a specific accrual period. ### Adjustments: what they are and when they're created An **adjustment** is automatically created whenever the payment total differs from the sum of the selected accruals: - **Overpayment** (+): you pay more than the accrual sum: e.g. adding a $5 tip to a $50 accrual creates a +$5 adjustment. - **Underpayment** (−): you pay less: e.g. partial payment pending dispute. When an adjustment is created, a **reason is required**. This keeps the audit trail meaningful. Good examples: _"tip"_, _"tax deduction"_, _"underpayment pending bank clearance"_. Adjustments do not create separate payments. They are attached to the payment row and visible in the payment history. ### Supplemental amounts: owed on top of a paid accrual Sometimes a creator turns out to be owed more for a period you have already paid, for example when posts from that week are found after the payment. A paid accrual is never changed, so record the difference as a **supplemental amount**: **Step 1: Find the paid accrual** Expand the creator in the Payouts Tracker (or open their Payouts tab) and find the paid accrual for that period. **Step 2: Add the amount** Open the row's **⋯** menu and choose **Add supplemental amount**. Enter the amount owed and a reason, for example "Posts found after the week was paid". Only org admins see this option. **Step 3: Pay it** The amount appears in the creator's list labelled **Supplemental: <reason>** and counts toward their outstanding balance, the tracker totals and the CSV exports. The next time you click **Mark as Paid** for the creator it is listed and checked with their earned accruals, and recording the payment marks it **Paid**. The original accrual and its payment stay exactly as they were. Voiding the payment that settled a supplemental amount makes it owed again. An owed one added by mistake can be voided from its row. Supplemental amounts can't be added for a removed creator: restore the creator first. ### Voiding a payment To undo a recorded payment: **Step 1: Find the payment** Find the payment in the creator's payment history or the Payouts Tracker. **Step 2: Click Void payment** Click **Void payment**. **Step 3: Confirm in the dialog** Confirm in the dialog. Voiding reverts the linked accruals back to **Earned** so they can be re-paid. The original payment record and audit trail are preserved: voiding never deletes data. **If voiding is blocked:** you'll see an error explaining that one or more accruals have been re-paid in a newer payment. Void the newer payment first, then void the original. ### Receipts Receipt files (attached in the **Receipt** field under **Payment details** when you record a payment) are stored privately: they are not publicly accessible. Only members of the organization can view them. Download links are available in the Payouts Tracker and the per-creator Payouts tab. **Note on tax reporting:** trackagoat does not generate 1099s or tax documents. The CSV export gives you a full payments ledger you can share with an accountant. --- ## Tracking payouts The **Payouts Tracker** is found under **Payouts → Payouts** in the project navigation. It gives you a creator-grouped view of what has been earned, what has been paid, and what is still outstanding: across your whole project in one place. ### Totals Under the toolbar, three totals show **Earned**, **Paid** and **Outstanding** for whatever the toolbar is filtering. When anything is owed, Outstanding is highlighted in amber and shows how many creators it is owed to; select it to switch the table to outstanding creators only. If some creators in the project have no payout structure, a blue note above the table names them, because they can't appear in the tracker until one is assigned. **Assign a structure** opens the assign dialog for them. ### Filtering and date ranges Use the toolbar to narrow the view: - **Outstanding / All**: toggle between creators with unpaid balances only (default) and all creators. **All** also lists creators who are assigned to a payout structure but have not earned anything yet (for example a creator added mid-week whose first weekly period has not closed, or a view-threshold creator with no video past the first threshold). They appear with $0 earned and their structure count, and expanding the row shows the in-progress periods that will settle next. - **Date range**: filter which accruals and payments are included in the totals. Dates are calendar days in your org's analytics timezone. Defaults to all time. - **Search**: type to filter creators by name or handle in the current view. - **Structure**: filter to a specific payout structure to see only earnings from that rule. **Paid** then counts only payments applied to that structure's accruals; adjustments and ad-hoc payments can't be attributed to a structure, so they are left out of a structure-filtered view. - **Campaign**: limit the tracker to creators represented by a campaign. This includes creators added directly, creators who own individually added videos, and creators reached through nested campaigns. - **Show removed creators**: creators you removed from the project are hidden by default, and their balances are left out of the totals. When any of them still has unpaid earnings, a note under the totals says so, for example "15 removed creators have $3,661.23 unpaid. Show". Turn the switch on (or click **Show**) to list them, labelled **Removed**, and include them in the totals. A removed creator has no **Pay** button: restore the creator to record a payment for them. All filters are reflected in the URL, so you can bookmark or share a filtered view. Campaign filtering is creator-scoped because payments are recorded against a creator rather than a campaign. Selecting a campaign shows the matching creators' payout totals; it does not split or allocate one payment across campaigns. ### Creator-grouped table Each row represents one creator. The columns are: | Column | Meaning | |--------|---------| | **Status** | **Owed** when anything is outstanding, **Paid up** when everything earned has been paid, **Nothing earned yet** otherwise (wide screens only) | | **Earned** | Sum of all non-voided accruals for this creator matching the active filters | | **Paid** | Sum of all non-voided payments recorded for this creator | | **Adjustments** | The part of Paid that settled no accrual: ad-hoc payments, tips and over-payments (+), under-payments (−), and payment history recorded without accruals. Hover it to see each reason and amount. Wide screens only | | **Outstanding** | Earned accruals not yet paid: highlighted in amber | | **Last paid** | Date of the most recent non-voided payment | Every row, and the totals above the table, add up: **Earned − Paid + Adjustments = Outstanding**. The expanded row spells this out for the creator. With a date range set they can differ, because accruals are filtered by their period and payments by the date they were paid. With a structure filter, Adjustments shows $0: an adjustment belongs to a payment, not to a structure. Click a row, or its arrow button, to expand it and see a detailed breakdown. On a phone the table shows the creator, Outstanding and the **Pay** button; expand a row for its accruals and payments. ### Drill-down: accruals and payments tabs Expanding a creator row shows two tabs: **Accruals**: one row per computed earning period. Each row shows: - The structure name, period type (daily/weekly/monthly), and computed date - The amount and current status (Earned, Paid, Rejected, Voided) - Actions for earned accruals: **Mark as Paid** (opens the payment dialog pre-filled for this creator) and **Reject** (removes the accrual from outstanding totals without payment) **Payments**: one row per recorded payment. Each row shows: - Total amount, method, and date - Number of accruals covered by the payment - A receipt link if one was attached - A **Void** action to undo the payment (restores linked accruals to Earned) ### Mark a payment from the tracker You can record payments in two ways from the tracker: **Step 1: Row-level Pay button** Click **Pay** on a creator row that has outstanding accruals. The Mark as Paid dialog opens pre-filled for that creator. **Step 2: Toolbar Mark as Paid button** Click the button at the top-right of the toolbar to open the dialog without a pre-selected creator. The dialog asks who the payment is for first: search for a creator, or choose **Ad-hoc (no creator)** for a payment that isn't tied to one. ### Charts Below the toolbar, two charts give a macro view of your spend: - **Spend over time**: a daily line chart showing paid amounts and accrued amounts over the last 90 days (adjusted by active filters). - **Top creators by total paid**: a horizontal bar chart of the 10 creators who have received the most in total payments. Both charts update when you change the active filters. ### Export to CSV Click **Export** in the toolbar to download data in CSV format. Three export types are available: | Export | Contents | |--------|---------| | **Tracker summary** | One row per creator: earned, paid, outstanding, last paid, structure count, and Adjustments with its reasons | | **Payments** | One row per payment: creator, method, amount, date, status, note, tracking URL | | **Accruals** | One row per accrual: creator, structure, period, amount, status. Supplemental amounts follow as rows with Criteria Type `supplemental` and the reason in the Note column | All exports respect your active filters (date range, creator selection, structure, campaign, and outstanding-only toggle). The tracker and accruals exports also follow **Show removed creators**, and every export has an **Is Removed** column. Amounts are in dollars (e.g. `12.50`), not cents. Timestamps are UTC ISO 8601. The files are UTF-8 with a BOM so they open correctly in Excel and Numbers. The same files are available from the API at [`GET /api/v2/exports/payout_tracker`, `payout_payments` and `payout_accruals`](https://www.trackagoat.com/docs/api-reference/exports.md), and coding agents get them with the MCP tool `export_csv`. --- ## Per-creator payouts tab The tracker is the macro view: one row per creator across a whole project. When you need to focus on a single creator's full financial history, use the **Payouts tab** on their detail page. Navigate to **Creators → [creator name] → Payouts** to open the tab. ### Summary totals At the top of the tab, three totals show the creator's all-time figures: | Total | What it shows | |------|--------------| | **Earned** | Sum of all earned + paid accruals for this creator (voided and rejected excluded) | | **Paid** | Sum of all non-voided payments recorded for this creator | | **Outstanding** | Earned accruals that have not yet been settled | Outstanding amounts are highlighted in amber when greater than zero. ### Charts Two charts give a visual breakdown: - **Spend over time**: daily paid vs. accrued amounts over the last 90 days. - **Earnings by structure**: horizontal bar chart showing how much this creator has earned under each payout structure (all time, top 10 by total earned). ### Three subtabs The tab is divided into three subtabs. The active tab is preserved in the URL (`?tab=accruals|payments|assignments`), so you can share or bookmark links to a specific view. **Accruals** (default): every computed earnings record for this creator, newest first. Each row shows: - Amount, status badge (Earned / Paid / Rejected / Voided), period label, structure name, and period start date. - For `earned` accruals: a **Reject** action via the `⋯` menu. Rejected accruals are excluded from outstanding totals. A voided rejection restores the accrual to `earned`. **Payments**: every recorded payment for this creator. Each row shows: - Amount, status badge, payment method, date, linked accrual count. - **Receipt** link: if a receipt URL was provided, it opens in a new tab. If a receipt file was uploaded, a **Receipt** (or **File**) link fetches a 60-second signed URL and opens the file. - For `paid` payments: a **Void** button. Voiding reverses the accrual statuses back to `earned`. **Assignments**: the active payout structures currently assigned to this creator. Each row links to the structure's detail page. Org admins see an **Unassign** button on hover; unassigning takes effect immediately (with a 5-second undo window) and stops new accruals from being computed. ### Mark as Paid The **Mark as paid** button at the top of the tab opens the settlement sheet pre-filled with this creator, so no creator picker is shown. The Accruals subtab also has a **Mark as Paid** button inline for quick access when you're reviewing outstanding rows. ### Per-creator CSV export Click **Export** to download accrual or payment data scoped to this creator: - **Accruals CSV**: all accruals for this creator (earned, paid, rejected and voided) in the same format as the tracker's accruals export. - **Payments CSV**: all payments for this creator in the same format as the tracker's payments export. --- --- Source: https://www.trackagoat.com/docs/guides/payouts --- # Paying creators with Dots > Connect your Dots account, link creators to Dots payees, and pay earned payouts from trackagoat. Connect your [Dots](https://usedots.com) account, and you can pay a creator's earned payouts from trackagoat. trackagoat sends the money through Dots and records the payment at the same time, so the tracker, the creator's Payouts tab and your exports stay correct without anyone typing the payment in. Paying by Venmo, PayPal or bank transfer outside trackagoat still works the same way: record those payments with **Mark as paid** ([Recording payments](https://www.trackagoat.com/docs/guides/payouts.md#recording-payments)). ## What you need - An org admin role in trackagoat. Only org admins can connect Dots and send payouts. - A Dots app with a balance to pay from, and its **client ID** and **API key**. Dots shows them in its dashboard under the app's API settings ([Dots: authentication](https://docs.usedots.com/overview/authentication)). Dots has separate keys for **sandbox** (test money) and **live** payouts. Connect sandbox keys first if you want to try the flow without moving real money. ## Connect Dots **Step 1: Open Payment providers** Go to **Org settings → Payment providers**. **Step 2: Click Connect next to Dots** Choose **Live** or **Sandbox**, then paste the client ID and API key. **Step 3: Click Check and connect** trackagoat checks the keys with Dots before saving them. Keys are encrypted when stored and are never shown again: the page only shows the last four characters. Connecting Dots also adds a **Dots** payout method, or reuses one you already have (for example "dots.dev"), so payments sent through Dots are labelled the same way as payments you recorded by hand. To replace the keys, open the **⋯** menu next to Dots and choose **Update keys**. **Check connection** re-tests the stored keys. If Dots ever rejects them, Dots is marked **Needs attention** and sending is paused until you update them. ## Link creators to Dots payees trackagoat needs to know who each creator is in Dots. Each creator is linked to one Dots payee. On **Org settings → Payment providers**, the **Creators and payees** table lists every creator, what you owe them, and who they are paid as. - **Find matches in Dots** reads the people your Dots account knows: everyone you've paid by Dots payout link and, if your Dots plan includes it, every Dots user connected to your app. It suggests a creator for each one by name. Suggestions are never linked until you click **Accept** (or **Accept all**). - **Link** opens a picker. Choose someone from Dots, or enter a mobile number for a creator who isn't in Dots yet. You can also link or change a creator from their **Payouts** tab, or from the payment sheet. **Paying by phone number.** When a creator is linked by phone number only, Dots texts them a link to claim each payout. When they claim their first one, trackagoat saves their Dots account to the link, so later payouts go straight to the payout method they chose in Dots. ## Pay a creator **Step 1: Click Pay** Click **Pay** on a creator's row in the Payouts tracker, or **Pay with Dots** on their Payouts tab. **Step 2: Check what's being paid** **Send with Dots** is selected when the creator is linked. Every earned payout is checked. Uncheck any you aren't paying now. Change the amount if you need to: if it no longer matches the checked payouts, you have to give a reason, and trackagoat records the difference as an adjustment. **Step 3: Add Dots details (optional)** Open **Payout details** to add a note, change the text message or 1099 setting, or add an internal note. See [Dots options](#dots-options). **Step 4: Review and send** Click **Review payout**, check the summary, then click **Send**. The payment is recorded the moment you send, and the payouts it covers are marked paid, so they can't be paid twice. Pressing **Send** twice, or retrying after a lost connection, never sends a second payout. ### Dots options | Option | What it does | |---|---| | **Note to creator** | Shown with the payout in Dots. By default it names the payout structures being paid, e.g. "Payout: Weekly CPM". | | **Text message** | The text Dots sends with the claim link. Only shown when the creator is paid by phone number. | | **Description in Dots** | Saved on the payout in your Dots dashboard, for your records. | | **Leave out of 1099 reporting** | Dots counts payouts toward a creator's 1099 by default. Turn this on only for payouts that aren't income, such as reimbursements. | | **Internal note** | Saved on the payment in trackagoat only. | ## Track a payout Payments sent through Dots show a status on the creator's **Payouts → Payments** tab: | Status | Meaning | |---|---| | **Completed** | The creator has the money. | | **Awaiting claim** | Dots texted the creator a link and they haven't claimed it yet. | | **Processing** | Dots is still moving the money. | | **Unconfirmed** | Dots didn't answer in time when the payout was sent. trackagoat checks again automatically. Don't send it again. | | **Failed** | Dots couldn't pay it, for example because your Dots balance was too low. The payment is voided, so its payouts are owed again. The reason shows under the payment. | | **Canceled** | The payout was canceled, or the creator didn't claim it before the link expired. The payment is voided, so its payouts are owed again. | trackagoat checks payouts that haven't finished every 5 minutes. When a payout fails or is canceled, org admins get a notification. For a payout that's **Awaiting claim**, the **⋯** menu can: - **Text the claim link again**; - **Cancel payout**: Dots cancels it, and trackagoat voids the payment so you can pay those payouts another way. A payout that Dots has completed can't be pulled back from trackagoat. You can still **Void** it to correct trackagoat's records; this doesn't move any money. ## Let agents send payouts Agents connected over [MCP](https://www.trackagoat.com/docs/agents/mcp.md), and scripts using the [REST API](https://www.trackagoat.com/docs/api-reference/payouts.md#send-a-payout-through-a-connected-provider), can send Dots payouts too, but only after an org admin turns this on. On **Org settings → Payment providers → Agent payments**: 1. Set a **limit per payout** and a **limit per 24 hours**. They start at $1,000 per payout and $5,000 per 24 hours. Change them to suit how you pay, or clear one for no limit. 2. Turn the switch on and confirm. Payouts the provider failed or canceled don't count toward the 24-hour limit. Voided ones still do, and an agent can't void a payout that was completed. Even when it's on, an agent can only send payouts: - with an API key (or MCP connection) that belongs to an **org admin**; - within your limits. A payout over a limit is refused and nothing is sent. Every payout an agent sends is recorded with the API key that sent it, and the payment notes whether it was sent from the app, the API or an agent. Turning agent payments off takes effect immediately. Disconnecting Dots turns it off too. The switch can only be changed by an org admin signed in to trackagoat. No API key can turn it on. ## Webhooks (optional) Without webhooks, statuses update within about 5 minutes. To see claims and failures immediately, add a webhook in Dots: 1. On **Payment providers → Instant status updates**, copy the webhook URL. 2. In Dots, add a webhook endpoint with that URL, subscribed to `payout_link.updated` and `transfer.updated`. 3. Paste the endpoint's signing secret (it starts with `whsec_`) back into trackagoat and click **Save**. trackagoat checks the signature on every webhook and then reads the payout's status from Dots. ## Disconnect Dots Choose **Disconnect** in the **⋯** menu. trackagoat deletes the stored keys and the webhook secret, and turns agent payments off. Past payments keep their history. Creators stay linked, so reconnecting the same Dots account restores everything. Any payouts that hadn't finished stop being tracked: check them in your Dots dashboard. ## Questions **Does trackagoat hold or move my money?** No. Money moves from your Dots balance to the creator through Dots. trackagoat tells Dots what to pay and records it. **What does it cost?** trackagoat doesn't charge for sending payouts. Dots charges its usual fees. **Which Dots plan do I need?** Any Dots plan can send payouts. On Dots plans that include its user list (Scale and up), **Find matches** searches every Dots user connected to your app. On other plans it matches against the people you've paid by payout link. trackagoat detects which you have when you connect. **Can I use another payment provider?** Dots is the first. Contact support@trackagoat.com to tell us which provider you use. --- Source: https://www.trackagoat.com/docs/guides/integrated-payouts --- # Conversions > Closing the loop between creator output and business outcome, defining conversion events, sending conversions, and thinking about attribution. Trackagoat already tells you what your creators **produced** (views, engagement, posting consistency) and what they **cost** (payouts). Conversions tell you what they **produced for the business**: signups, purchases, installs, bookings, whatever counts as a result for you. Once conversions are flowing, questions like *"which creator actually drove revenue last month, not just views?"* become answerable. ## The two pieces Conversions have exactly two moving parts. **Conversion events** are the definitions. They live in a project and you create a handful of them, once. `signup`. `purchase`. `trial_started`. Each definition carries a **key** (the stable slug your backend hard-codes), a **default value**, and a **currency**. **Conversions** are the facts. Each one references a conversion event and is attributed to a creator, an account, or a video. You send these from your own system. Your checkout webhook, your signup handler, your nightly export: as they happen. > **Note: Define events in the app, send conversions from your backend** > > Conversion events are defined in Trackagoat under **Tracking → Conversions → Manage events**, where you can also deactivate them and review what each one has recorded. No API key is needed for that. > > An API key is needed for the other half: *recording* conversions, which your own system does over the [v2 API](https://www.trackagoat.com/docs/api-reference/conversions.md). **There is no button in the app to add a conversion**. They always arrive from your backend. The app is where you define event types and read results. Create one from **Org Settings → API keys**. See the [API keys guide](https://www.trackagoat.com/docs/api-reference/authentication.md). The key needs the `write` scope. Everything in the app is also available through the API if you would rather provision events programmatically. ## Defining your first conversion event Start with one. Resist the urge to model your whole funnel on day one. You can always add more, and every event type you define is a decision your backend has to keep honouring. A good first event is the thing you already report on internally. If you run a paid product, that's `purchase`. If you run a free product, that's `signup`. ```bash curl -X POST \ -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -d '{ "key": "signup", "name": "Signup", "description": "A new account created from a creator link", "default_value_cents": 0, "currency": "USD" }' \ https://www.trackagoat.com/api/v2/projects//conversion-events ``` A few things worth getting right the first time, because they are not changeable later: - **`key` is permanent.** It's the identifier you'll paste into your backend. Keep it lowercase and boring: `signup`, `purchase`, `demo_booked`. - **`currency` is permanent.** It lives on the definition, not on individual conversions, which is what makes any sum of conversions well-defined. If you sell in two currencies, define two events (`purchase_usd`, `purchase_eur`). - **`default_value_cents` is not permanent**, but changing it only affects conversions recorded *afterwards*. The value is copied onto each conversion as it arrives. ### Which value should an event carry? Two patterns cover almost everything: | Pattern | `default_value_cents` | Per-conversion `value_cents` | |---------|----------------------|------------------------------| | **Count-style**, signups, installs, follows | `0` | omit | | **Money-style**: purchases, subscriptions | `0` | send the real order value | | **Fixed-worth**: a lead you value at a flat $25 | `2500` | omit | For a count-style event, you just want volume, so leave the value at zero and read `total_conversions`. For a money-style event, send the actual order total on every call. ### How many events can I define? `max_conversion_events_per_project` caps the number of **event types** per project: 3 on Free, 25 on Starter, unlimited on Ultra. Only active events count, so deactivating an event you no longer use frees a slot. Conversion **volume** is never capped. Trackagoat will not quietly drop your revenue data at a limit. ## Sending your first conversion The minimum viable conversion is a conversion event and a target: ```bash curl -X POST \ -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "conversion_event_key": "signup", "platform": "tiktok", "handle": "charlidamelio", "external_id": "user_88213" }' \ https://www.trackagoat.com/api/v2/projects//conversions ``` That records one signup attributed to that creator's TikTok account. A revenue example, attributed to a specific video, with a real order value: ```bash curl -X POST \ -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "conversion_event_key": "purchase", "video_id": "", "value_cents": 4999, "quantity": 2, "external_id": "order_10021", "occurred_at": "2026-07-21T14:03:00Z", "note": "Bundle promo" }' \ https://www.trackagoat.com/api/v2/projects//conversions ``` `quantity` multiplies: this records a total of $99.98 (`value_cents × quantity`), which trackagoat computes for you as `total_value_cents`. If you're batching from a nightly job, send up to 500 conversions in one request as `{"conversions": [ ... ]}`. ### Picking a target You attribute a conversion by supplying **exactly one** of these: | Target | When to use it | |--------|----------------| | `video_id` | You know which specific post drove it (a per-video link or code) | | `account_id` | You know the platform account but not the post | | `platform` + `handle` | Same as `account_id`, but you only have the handle | | `creator_id` | You know the person, not the platform | Supplying none of them, or two: is a 400. That's deliberate: an ambiguous target is a silent data-quality bug, and it's better to fail the webhook loudly. > **Warning: platform + handle never creates an account** > > Attributing by handle resolves only to an account that **already exists in that project**. Matching is case-insensitive and the `@` is optional, so `@CharliDAmelio` and `charlidamelio` both work. A handle trackagoat doesn't know returns `404 account_handle_not_found` and creates nothing. A typo in a webhook should surface as an error, not spawn a junk account and burn an account slot. ### Don't skip `external_id` `external_id` is your own ID for the thing that converted: the order ID, the user ID, the webhook event ID. It is unique per project, forever. If your webhook fires twice, or your retry logic double-fires, or you re-run last night's export, the second attempt does **not** create a duplicate. It returns the original conversion's ID with `deduped: true`, as a success. This is the single cheapest thing you can do to keep your numbers correct. There is also an `Idempotency-Key` header, which replays the entire response for 24 hours if you retry with the same key. The two work together and cover different failure modes: | Layer | Scope | Protects against | |-------|-------|------------------| | `Idempotency-Key` header | 24 hours, per key | The request timed out and you don't know if it landed | | `external_id` field | Forever, per project | Anything, ever, resending the same event | > **Tip: A fresh key with a repeated external_id is not an error** > > If you retry with a **new** `Idempotency-Key` but the **same** `external_id`, you get `HTTP 200`, `deduped: true`, and the **original** conversion's ID. Nothing was created and nothing went wrong. Treat it as success. ## How rollup works Attribution rolls **up** the hierarchy: ``` video → account → creator ``` A conversion recorded against a video also counts for that video's account, and for that account's creator. A conversion recorded against an account also counts for its creator. This means you can attribute at whatever precision your data actually supports, and still read totals at whatever level you want to report at. If your checkout only knows which creator sent the customer, attribute to the creator. If you run per-video discount codes, attribute to the video, and the creator's total still includes it. Concretely: `GET /conversions?creator_id=X` returns conversions recorded against creator X, against X's accounts, **and** against X's videos. You don't have to fan out the query yourself. The original precision isn't lost. Every conversion keeps a `target_type` (`creator`, `account`, or `video`) recording what you actually attributed to, so a per-video report is still exact. ## How to think about attribution Two properties are worth internalising, because they explain most "wait, why does it say that?" moments. ### Attribution is frozen at receipt time The rollup is computed once, when the conversion arrives. If a video is later moved to a different account, **its past conversions do not move with it.** That's intentional. A conversion is a historical fact: it was earned under the ownership that existed when it happened. Silently rewriting old conversions when today's org chart changes would make last quarter's report irreproducible: you'd run the same query twice and get two answers. If you're staring at a creator's total wondering why some conversions "didn't follow" a reassigned video, this is why. **You can override it deliberately.** Freezing is the default, not a hard rule. When trackagoat notices that a video has moved since its conversions were recorded, the video's **Conversions** tab shows an attribution-drift notice with two choices: - **Freeze**. Keep history as-is (the default, and usually correct). - **Reassign**: rewrite that video's recorded conversions to its current creator. Reassign is restricted to org admins, and it applies only to the video you're viewing. Use it when a video was attributed to the wrong account by mistake, not to reflect a legitimate change of ownership. ### Trackagoat records attribution, it doesn't decide it Trackagoat has no view into your funnel. It doesn't set cookies, run last-click logic, or guess. It stores the attribution **you** send it. That means the quality of your conversion data is entirely the quality of the decision your backend makes before it POSTs. Whatever you already use to connect a customer to a creator: a UTM parameter, a discount code, a tracked link, a "how did you hear about us" answer: is the thing to encode in the target. The `source` field is there to record which of those mechanisms was responsible, so you can compare them later. ## Handling refunds: void, don't delete Business outcomes get reversed. Orders get refunded, cards get charged back, fraudulent signups get cleaned up. The answer is to **void** the conversion: ```bash curl -X DELETE -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/projects//conversions/?reason=Refunded%20-%20order%2010021" ``` Voiding is the default behaviour of `DELETE`. You don't need `?mode=void`. It: - sets the conversion's status to `voided` - excludes it from every total and every default list - records **when**, **who**, and **why** (`reason`) - keeps the row queryable via `?status=voided` or `?status=all` So your creator totals drop by exactly the refunded amount, and you can still answer "what did we void last month, and why". Voiding an already-voided conversion is a no-op, so a retry is safe. Because voiding lowers any unpaid payout earned from that conversion, voiding in the app (the conversion's detail drawer) is limited to **org admins**, the same as voiding a payout accrual. > **Important: Hard delete is for mistakes, not reversals** > > `?mode=hard` removes the row permanently and requires an API key with the `admin` scope. Use it for test data and genuine mistakes: a conversion recorded against the wrong project, say. Never use it for a refund: you would lose the fact that revenue was ever recorded, and your history would no longer reconcile with your own books. The same logic applies to conversion events themselves. `DELETE` on an event **deactivates** it: it stops accepting new conversions while all its history survives. `?mode=hard` on an event deletes every conversion ever recorded under it. The response tells you how many conversions are attached (`meta.conversions_affected`). Read that number before you do anything irreversible. A deactivated event still holds its `key`, so re-creating it returns **409** rather than a second copy. It is also hidden from the event pickers used when building a payout structure. Bring it back under **Tracking → Conversions → Manage events → Reactivate**, or with `PATCH .../conversion-events/{eventId}` and `{"is_active": true}`. Reactivating counts against `max_conversion_events_per_project` again, so it can return a `402` if the project is at its cap. ## A sensible rollout **Step 1: Define one event** Pick the outcome you already report on internally. One event, correct, beats five events, half-wired. **Step 2: Wire your backend to POST it** From the place that already knows the outcome happened. Your checkout webhook, your signup handler. Always send an `external_id`. **Step 3: Attribute at the precision you actually have** Creator-level is fine. Rollup means you can start coarse and get more precise later without losing comparability. **Step 4: Check it landed** `GET /conversions?creator_id=` and confirm the rows look right: target type, value, currency, timestamp. **Step 5: Void, don't delete, when outcomes reverse** Wire refunds and chargebacks to a `DELETE` with a `reason`. Do this early; retrofitting it later means a period of overstated revenue. **Step 6: Add more event types once the first is trustworthy** Then compare cost (payouts) against outcome (conversions) per creator. ## Related - [Conversions API reference](https://www.trackagoat.com/docs/api-reference/conversions.md): every endpoint, field, and error code - [API keys](https://www.trackagoat.com/docs/api-reference/authentication.md): creating a key and choosing scopes - [Payouts](https://www.trackagoat.com/docs/guides/payouts.md): the cost side of the same equation - [Plans and limits](https://www.trackagoat.com/docs/guides/limits.md): how `max_conversion_events_per_project` is enforced --- Source: https://www.trackagoat.com/docs/guides/conversions --- # Insights and analytics > Understanding trackagoat's computed analytics metrics: engagement rates, posting velocity, average views, and period-over-period growth. ## What are derived metrics? Raw metrics (views, likes, followers) tell you absolute counts. **Derived metrics** are computed ratios: rates and velocities that let you compare performance across creators and campaigns of different sizes. All derived metrics are computed server-side from your historical snapshot data using timezone-aware day bucketing. Request them by adding a `metric=` parameter to any stats endpoint. ## Available metrics | Metric ID | Formula | Entity support | |-----------|---------|----------------| | `engagement_rate_by_views` | `(likes + comments + shares + saves) / views × 100` | video, creator, campaign | | `engagement_rate_by_followers` | `(likes + comments + shares + saves) / follower_count × 100` | creator, campaign | | `posts_per_week` | `posts on that day × 7` | creator, campaign | | `avg_views_per_post` | `views gained / videos posted through that day` | creator, campaign | | `growth_rate_pop` | `(value[day] − value[day−1]) / value[day−1] × 100` | video, creator, campaign | ## Engagement rate by views (TikTok standard) This is the primary TikTok engagement benchmark. It measures what percentage of viewers interacted with the content. ```text engagement_rate = (likes + comments + shares + saves) / views × 100 ``` A `null` value means views were 0 on that day (divide-by-zero). Days with baseline snapshots contribute 0 to the engagement numerator and denominator. > **Tip: 2026 TikTok engagement benchmarks** > > - Under 1%: low > - 1 to 3%: typical > - 3 to 6%: strong > - Above 6%: viral ```bash curl -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/analytics?entity=creator&entity_id=&metric=engagement_rate_by_views&from=2026-01-01&to=2026-03-31" ``` ## Engagement rate by followers (IG-style) Used when you want to normalize engagement against audience size rather than reach. Useful for creator comparisons regardless of how often they post. ```text engagement_rate = (likes + comments + shares + saves) / follower_count × 100 ``` The follower count used is the last known count as of each day (carry-forward). A `null` value means no follower data was available for that day. This metric is **not supported for individual videos**: use `creator` or `campaign` entities. ```bash curl -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/analytics?entity=creator&entity_id=&metric=engagement_rate_by_followers&from=2026-01-01&to=2026-03-31" ``` ## Posts per week Posting velocity normalized to a weekly rate, computed on a per-day granularity basis. ```text posts_per_week = posts_on_day × 7 ``` A day with 0 posts returns `0.0` (a real rate of zero). `null` is returned only if there are no videos in scope at all. ```bash curl -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/analytics?entity=creator&entity_id=&metric=posts_per_week&from=2026-01-01&to=2026-03-31" ``` ## Average views per post Views gained in the period divided by the number of posts published through that day. Shows whether new posts are increasing or hurting per-video efficiency. ```text avg_views_per_post = views_gained_on_day / videos_posted_through_day ``` `null` when no videos have been posted through that day yet. ```bash curl -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/analytics?entity=campaign&entity_id=&metric=avg_views_per_post&from=2026-01-01&to=2026-03-31" ``` ## Growth rate (period-over-period) Day-over-day percentage change for any base metric. Pass `base_metric` to specify what to measure. ```text growth_rate = (value[day] − value[day−1]) / value[day−1] × 100 ``` - First point in range always returns `null` (no prior). - `null` when the prior day's value was 0 or missing. - Supported `base_metric` values: `views`, `likes`, `comments`, `shares`, `saves`, `followers` (creator only), `video_count` (creator only). ```bash # Views growth rate for a creator curl -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/analytics?entity=creator&entity_id=&metric=growth_rate_pop&base_metric=views&from=2026-01-01&to=2026-03-31" # Follower growth rate curl -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/analytics?entity=creator&entity_id=&metric=growth_rate_pop&base_metric=followers&from=2026-01-01&to=2026-03-31" ``` ## Response shape The `/api/v2/analytics` endpoint returns: ```json { "data": { "series": [ { "entity_ref": { "type": "creator", "id": "uuid", "label": "@handle" }, "metric": "engagement_rate_by_views", "unit": "percent", "mode": "rate", "granularity": "day", "timezone": "America/New_York", "points": [ { "bucket_start": "2026-01-01", "value": 4.2 }, { "bucket_start": "2026-01-02", "value": null }, { "bucket_start": "2026-01-03", "value": 3.8 } ] } ], "baselines": [ { "entity_id": "video-uuid", "snapshot_at": "2026-01-01T08:00:00Z", "reason": "tracking_started" } ], "first_data_date": "2026-01-01", "tier_retention_days": 90 }, "error": null, "meta": { "cache_ttl_seconds": 300, "cached": false } } ``` | Field | Description | |-------|-------------| | `series[].entity_ref` | Entity type, id, and display label for this series | | `series[].points[].value` | Computed value, or `null` for divide-by-zero / missing data | | `series[].timezone` | IANA timezone used for bucketing | | `baselines` | Baseline marker events within the range: use as chart annotations | | `first_data_date` | Earliest date with non-baseline data: use for "All time" preset | | `tier_retention_days` | Your plan's retention window in days, or `null` for unlimited | ## Query parameters | Parameter | Type | Description | |-----------|------|-------------| | `metric` | string | One of the metric IDs above. Omit for legacy response. | | `granularity` | `day` \| `week` \| `month` \| `year` \| `cumulative` \| `custom` | Time bucket size. `custom` requires `bucket_days`. Defaults to `day`. | | `tz` | IANA string | Timezone for bucketing. Defaults to your org's `analytics_tz`. | | `formula` | `by_views` \| `by_followers` | Only for `engagement_rate_by_views` / `engagement_rate_by_followers`. | | `base_metric` | string | Required for `growth_rate_pop`. | | `from` | YYYY-MM-DD | Range start (default: 30 days ago). | | `to` | YYYY-MM-DD | Range end (default: today). | ## Caching & freshness All `/api/v2/analytics` responses are cached server-side for up to **5 minutes** (300 seconds). This means: - The first request after cache expiry runs the full resolver and may take 1 to 2 seconds. - Subsequent requests within the 5-minute window return in under 50ms. - The `meta.cached` field in each JSON response is `true` when data came from cache and `false` on a cold run. - The `meta.cache_ttl_seconds` field is always `300`. ### When is cache invalidated? > **Note:** > > The cache is also tag-invalidated in real time on data mutations: not just when the TTL expires. Most changes take effect within seconds. The cache is tag-invalidated (not just TTL-expired) in these situations: | Event | Scope | |-------|-------| | Scraper writes new video stats | All analytics for the affected org | | Scraper updates creator profile | All analytics for the affected org | | Scraper rebuilds a campaign rollup | That campaign's analytics | | You add/remove a video from a campaign | That campaign's analytics | | You create, edit, or delete an annotation | Analytics for the annotated entity | | You change the org's `analytics_tz` setting | All analytics for the org | After a cache-busting event, the next request for affected data runs a fresh resolver. The **maximum staleness** in the worst case is 5 minutes (TTL alone), but in practice most data mutations trigger near-instant invalidation via the scraper webhook or inline `revalidateTag` calls. ### Partial-bucket staleness Today's in-progress bucket may be up to **5 minutes stale** even with invalidation active: this is by design. The `partial_bucket` field tells you the `as_of` timestamp of the last data capture so you can show it to users. --- ## Campaign analytics Campaign analytics data is served from a pre-aggregated rollup that is rebuilt nightly and updated within ~5 minutes whenever a video is added to or removed from a campaign. Today's partial data may be up to a few minutes behind as changes propagate through the invalidation queue. ### Campaign Insights trend charts The **Campaign Insights** view opens with five trend charts built for recurring program reviews. Each follows the toolbar's date range and granularity and respects the campaign filter (all campaigns, or one). | Chart | What a bucket shows | |-------|---------------------| | **Impressions Over Time** | Views gained during the bucket, counted on the day they happened rather than the day the video was posted. A video's pre-tracking views (its first snapshot) are credited to its posting day. | | **Posts Over Time** | Videos posted in the bucket, by posted-at date. | | **Creators Posting** | Distinct creators who posted at least once in the bucket. | | **Impressions per Post** | Impressions in the bucket ÷ videos posted in the bucket. Buckets with no posts are left blank rather than shown as zero. | | **New Creators Posting** | Creators whose earliest tracked post in the current scope falls in the bucket. Each creator counts once. | With **All campaigns** selected, the charts cover every active tracked video in the project. With a single campaign selected they cover that campaign's resolved videos, including ones whose scraping has since expired, so a finished campaign keeps its history. The underlying series is available as `GET /api/v2/analytics/aggregate/campaign-timeseries` (see the [aggregate API reference](https://www.trackagoat.com/docs/api-reference/aggregate.md#get-aggregatecampaign-timeseries)). Historical `saves` values in older campaign snapshots may read as 0: this is expected, as earlier snapshots did not record save counts. ## Insights controls The Insights pages (**Creator Insights**, **Account Insights**, **Video Insights** and **Campaign Insights**) share one toolbar. It controls every widget on the page. ### Period Pick **Last 7 days**, **Last 30 days**, **Last 90 days**, **This month** or **All time**, or choose start and end dates. The default is the last 30 days. ### Platform filter A **Platform** control scopes every widget to one platform: **All platforms** (the default), **TikTok** or **Instagram**. On Campaign Insights it applies to the totals, the trend charts, the campaign leaderboard and the campaign comparison; **Campaign progress** always counts every platform, because a campaign's view target covers all of them, and **Member overlap** lists campaign membership, which has no platform. The project-level aggregate endpoints accept the same filter as a query parameter, e.g.: ```bash curl -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/analytics/aggregate/video-leaderboard?project_id=&platform=tiktok" ``` `platform` accepts `tiktok` or `instagram`; omit it to include every platform. ### Campaign filter **Video Insights**, **Account Insights**, and **Campaign Insights** add a **Campaign** control next to the platform filter. Pick a campaign to scope every widget on the page to the videos that campaign resolves to (videos added directly, every video from its member creators, and anything reached through nested campaigns), or leave it on **All campaigns**. - On **Video Insights** it scopes the summary strip, video leaderboard, engagement breakdown, posting heatmap, view distribution, breakout videos, and the over-posting warning. - On **Account Insights** it narrows the account list to accounts with at least one video in the campaign and counts only that campaign's videos toward each account's period metrics. Follower deltas stay per-account (they are not attributable to a campaign). - On **Campaign Insights** it pins the summary strip, trend charts, leaderboard, comparison chart, and progress list to the chosen campaign. The selection lives in the URL (`?campaign=`), so a filtered view can be bookmarked or shared. The control is hidden while a project has no campaigns. Creator Insights does not have a campaign filter yet. The same scope is available to API and agent callers as an optional `campaign_id` query parameter on the `video-leaderboard`, `posting-heatmap`, `view-distribution`, `outliers`, `over-posting-risks`, `account-leaderboard`, `campaign-leaderboard`, and `campaign-timeseries` aggregates. The campaign must belong to `project_id`; a campaign from another project simply matches nothing. ### Granularity Controls the width of each time bucket: **Auto**, **Day**, **Week**, **Month** or **Year**. > **Note: More controls on a single item's Analytics tab** > > Cumulative and custom granularity, the New/Cumulative mode toggle, **vs prior period**, **Compare with…**, **Breakdown** and **Annotations** are not on the Insights pages. They are on the **Analytics** tab of a single creator, campaign or video (**Compare with…** and **Breakdown** on creators and campaigns only). See [Analytics tab controls](#analytics-tab-controls). ## Analytics tab controls Open a creator, campaign or video and go to its **Analytics** tab. Its toolbar has more controls than the Insights pages. ### Date range presets The Analytics tab has its own presets: | Preset | Range | |--------|-------| | Last 7 days | Past 7 days | | Last 30 days | Past 30 days (default) | | Last 90 days | Past 90 days | | Last year | Past 365 days | | All time | From first recorded data point | | Custom | Pick any start/end dates with the date picker | Presets beyond your plan's history window are disabled with an upgrade prompt. **All time** always works: it clamps automatically to the oldest data available on your plan and shows a notice like *"Showing last 90 days (Free plan)."* ### Granularity Controls the width of each time bucket. Choose from **Auto**, **Day**, **Week**, **Month**, **Year**, **Cumulative**, or **Custom…** (N days). - **Auto** selects the most readable granularity for the chosen range and displays the resolved value beneath the control (e.g., *Auto: Week*). - **Cumulative** collapses the entire range into a single running-total line. When Cumulative granularity is active, mode is locked to Cumulative automatically. - **Custom** reveals a numeric input (1 to 90) to define exact bucket width in days. ### Mode | Mode | Description | |------|-------------| | New | Each bucket shows only the delta gained in that period. | | Cumulative | Each bucket shows the running total from the beginning of the range. | Rate-only metrics (engagement rate, growth rate) ignore this toggle: they always show rates. **How the first day of a range is counted.** In New mode, a video's gain on the first day of the range is measured against its last snapshot from *before* the range, so a 30-day range shows what was gained in those 30 days. A video posted inside the range has no earlier snapshot, so everything it has counts as gained in the range, on the day it was posted. **Days are your org's days.** Every bucket is a calendar day (or week, month, year) in your organization's analytics timezone (`analytics_tz`). A range ending today includes today, shown as a partial bucket. ### Compare vs prior period Toggle the **vs prior period** switch to overlay a second series shifted back by the same length as the selected range. This lets you see whether this week outperformed last week, or this month versus last month. The prior-period line renders dashed in a muted color alongside the primary series. When compare is active, a row of **Δ% summary cards** appears above the charts showing the change for each metric versus the prior period: - **New mode** (bar charts): shows the total delta: current-period sum vs prior-period sum - **Cumulative mode** (area charts): shows the *period gain*: (last value − first value) for each window - **Rate/Engagement mode** (line charts): shows the pp difference: current average rate minus prior average rate, expressed as `±X.Xpp` rather than a percentage (since a percentage-of-a-percentage is misleading) #### Partial-bucket exclusion Today's bucket (and any in-progress bucket) is drawn dashed on the chart to indicate it's incomplete. It is **excluded from the Δ% card calculation** so partial data doesn't distort the summary. The prior-period equivalent of that bucket is also excluded, keeping the comparison fair. #### Truncation If the prior window extends before your tracking history or your plan's retention limit, the available prior period is automatically clamped. A muted notice appears below the toolbar indicating how many days of data were available versus requested: for example: > *Prior period partial: 22 of 30 days available (before tracking started)* Hovering a dashed prior-period line in this state shows a tooltip with the same context. #### Limitations > **Warning: Limitations** > > - **Compare and breakdown are mutually exclusive.** When a breakdown filter is active, the compare toggle is disabled. API calls combining `compare=prior_period` with a non-`none` breakdown return a 400 error (`compare_breakdown_exclusive`). > - **Stacked engagement and velocity charts** do not show a prior-period overlay: these panels have different aggregation semantics not suited to simple time-shifted comparison. --- ## Retention by plan tier Each plan tier has a maximum analytics history window: | Plan | History window | |------|---------------| | Free | 90 days | | Starter | 365 days | | Ultra | Unlimited | ### In the UI When you select a range that would exceed your tier's limit: - **Preset chips** beyond your tier (e.g. "Last year" on Free) show a lock icon and are clickable to see the upgrade prompt. - **All time** always works: it clamps to the oldest data available on your plan and shows a notice like *"Showing last 90 days (your current plan). Upgrade for longer history."* Clicking the notice opens the upgrade modal. - **Custom date picker** allows choosing any range; if you pick a `from` date outside your window the API gate (below) will fire. ### For API and agent callers If you request a `from` date older than your plan's retention window, the unified endpoint returns **HTTP 402**: ```json { "data": null, "error": "Requested range exceeds plan retention window", "meta": { "code": "retention_exceeded", "limitKey": "stats_history_retention_days", "tier_retention_days": 90, "requested_from": "2025-01-01", "earliest_allowed": "2026-01-21" } } ``` Use `tier_retention_days` from any successful response to pre-check the available window before issuing a long-range query. ### Downgrade grace When an org is downgraded to a lower tier, the previous tier's retention window remains in effect for **14 days** to give users time to export data. After 14 days, the new tier's limit is enforced for both queries and the nightly data purge. --- ## Multi-entity compare On the **Analytics** tab of a single creator or campaign you can overlay up to **5 entities** on the same charts to compare performance side-by-side. Video Analytics tabs and the Insights pages don't have this control. ### How to use it **Step 1: Click the Compare with… button** Click the **"Compare with…"** button in the toolbar (the icon with overlapping people). **Step 2: Search for creators or campaigns** Search for creators or campaigns by name. **Step 3: Select up to 4 additional entities** Select up to 4 additional entities: your primary entity is always pinned and shown first. **Step 4: Each entity gets a distinct color** Each entity gets a distinct color, stable across every chart panel on that tab. ### What changes in overlay mode - **All count-mode charts switch to line mode**: bars and filled areas are replaced with lines so N overlapping series stay readable. - **Engagement Rate** still renders as a line (unchanged: it's rate-mode by default). - **Stacked engagement breakdown**, **Top Videos**, and **Top Creators** panels are hidden: these are single-entity concepts. - **Baseline markers** show only for your primary entity. - **The "vs prior period" compare switch is disabled** while overlay is active. The two modes are mutually exclusive. ### Shareable overlay URLs The selected entity IDs are encoded in the URL as `?tbe=id1,id2,...`. Copying the URL and sharing it with a teammate preserves the full overlay view: same date range, granularity, and selected entities. --- ## Shareable URLs On an Analytics tab, all toolbar state (range preset, granularity, mode, compare toggle, and custom dates) is persisted in the page URL via short query parameters. Copy the URL from your browser's address bar to share an exact chart view with a teammate: they'll land on the same date range and settings. The toolbar also saves your last-used settings per entity type (creator, campaign, video) in your browser's local storage, so your preferred range is restored when you navigate back to that tab. --- ## `engagement_rate_by_followers` on video entities Not supported: use `creator` or `campaign` entities instead. ## `growth_rate_pop` with follower or video count base metrics `base_metric=followers` and `base_metric=video_count` are only supported for `creator` entities. --- ## Chart modes The **Mode** toggle and **Breakdown** button described here are on the **Analytics** tab of a single creator, campaign or video, not on the Insights pages. ### New vs Cumulative The **Mode** toggle changes how each time bucket is rendered: | Mode | Chart type | What you see | |------|-----------|--------------| | New | Bar chart | Delta gained in that bucket only: e.g., views added this week | | Cumulative | Area chart | Running total from the start of the selected range | Switching modes does not change the underlying data: only how it is displayed. Use **New** to spot spikes and posting cadence; use **Cumulative** to see growth trajectory. ### Rate metrics Metrics like **Engagement Rate** ignore the Mode toggle and always render as a line chart, since a rate is neither a delta nor a running total. ### The Partial badge When the most recent bucket covers a time period that hasn't ended yet (e.g., a weekly bucket mid-week), the chart shows a **Partial · as of HH:mm TZ** label above the chart. The in-progress bar or area is shaded to distinguish it from completed buckets. ### Baseline markers Vertical dotted lines on a chart mark when tracking first started for that entity (labeled **Tracking started**) or when a manual baseline snapshot was recorded (labeled **Baseline**). Data before a baseline marker represents the snapshot state, not necessarily change over time. ### Breakdowns On a creator or campaign **Analytics** tab, the **Breakdown** button in each chart panel header lets you decompose a single aggregate series into its top constituent entities: up to 10, with everything else rolled into an **Other** segment. | Entity scope | Available breakdown dimensions | |---|---| | Campaign | By video, By creator | | Creator | By video | **How it works:** - Click the **Breakdown** button in any count-based chart (Total Views, Total Likes, Follower Count, Total Videos). - The chart switches to a stacked-area view, showing each constituent as a colored band. **Other** is rendered in muted gray. - Breakdown state is encoded in the URL (`bdv=video`, `bdl=creator`, etc.), so links are shareable. **Constraints:** - Breakdown is not available for rate metrics (Engagement Rate, Posts per Week, etc.). The button is disabled with a tooltip explaining why. - Breakdown and period comparison (`compare=prior_period`) are mutually exclusive. Enabling comparison disables the breakdown menu. - Breakdown and multi-entity overlay are mutually exclusive. The breakdown menu is disabled when two or more entities are overlaid. ### Cohort velocity comparison On a video's **Analytics** tab, the **View Velocity** chart shows how many views a video gained per day after it was posted (day 0 = post date). Days follow your organization's time zone. Older videos are checked less often, sometimes once a week. When there are several days between two checks, the views gained are spread evenly across those days rather than shown as one spike. Toggle **Compare to cohort** to overlay two dashed reference lines: - **Creator avg**: average views/day for up to 10 of the creator's most recent other videos. - **Org avg**: average views/day for videos posted in the last 90 days across your entire organization. The averages count each check's views on the day of the check, while the video's own line spreads views evenly across the days between checks. A note under the chart says this, because the averages can look spikier. Both lines show the number of videos in the cohort in the chart legend. If a creator has only published the current video, the creator avg line is hidden and a note is shown. Org avg always appears when there is data. ### Stacked engagement breakdown The **Engagement Breakdown** panel on Campaign and Video analytics shows **Likes, Comments, Shares, and Saves** as stacked segments. Each color maps to one metric. Use **Mode: New** to see engagement added per period, or **Mode: Cumulative** for total engagement over the range. ### Engagement Rate panel The **Engagement Rate** panel is available on Creator and Campaign tabs. It calculates: ```text (likes + comments + shares + saves) / views × 100 ``` A value of 5% means roughly 1 in 20 views resulted in some form of engagement. This panel always renders as a line chart and is not affected by the Mode toggle. --- ## Annotations Annotations are event markers that appear as pins at the top of each chart on the **Analytics** tab of a creator, campaign or video. The Insights pages don't show them. They help you correlate metric changes with real-world events. ### Types of annotations | Source | Kind | Description | |--------|------|-------------| | **User** | `user` | Notes you add manually: title, optional date/time, optional markdown body | | **Auto** | `creator_profile_change` | Detected changes to a creator's platform profile (bio, avatar, etc.) | | **Auto** | `creator_milestone` | Follower/view milestones crossed by a creator | | **Auto** | `campaign_created` | When a campaign was created | | **Auto** | `readme_changed` | When a creator or campaign README was edited | ### Adding an annotation 1. Open the **Analytics** tab of a creator, campaign or video. 2. Click the **Annotations** button in the top-right corner of the panel. 3. Click **Add** in the popover. 4. Fill in the title (required), date & time, and optional markdown notes. 5. Click **Add annotation**: the pin appears immediately on all charts in the tab. ### Cascade behavior Annotations follow a **cascade-down** model: - A **campaign-scoped** annotation appears on Creator and Video tabs for members of that campaign. - A **creator-scoped** annotation appears on Video tabs for videos by that creator. - Cascaded annotations show a caption ("From campaign: Summer Launch") so you know their origin. Video-scoped annotations do **not** cascade up: they stay local to that video's tab. ### Show/hide auto annotations Each chart panel has a toggle in the **Annotations** popover labeled **Show auto annotations**. When turned off, only your user-authored notes are shown. This preference is saved in the URL (`?tbha=1`) and local storage. ### Chart pin markers Each pin represents one or more annotations bucketed to the same time period. The badge shows how many. Click a pin to see all annotations in that bucket: title, timestamp, any markdown content, and the cascade source if applicable. --- Source: https://www.trackagoat.com/docs/guides/analytics --- # How metrics are calculated > One definition for every number in trackagoat: tracked posts, views, engagement rate, follower growth, campaign totals and the default date window. Every page in trackagoat (the dashboard, Insights, Program Health, campaign and creator pages), the API and the MCP server use the definitions on this page. When two places show the same metric for the same project and dates, they show the same number. When a number means something different (for example, views gained versus views on posts), it has a different label. ## Definitions | Metric | Definition | |---|---| | **Tracked post** | A post with status *auto-tracked*, *included* or *direct add*, whose tracking has not been stopped, from an **active** creator and an **active** account. Posts awaiting review are never counted; pages show them separately as "+N awaiting review". When you remove a creator, their posts leave every live metric. | | **Default window** | The last **30 days including today**, in your organization's time zone. Used by the dashboard, Insights, Program Health, the "Last 30 days" date preset, the API and the MCP server. | | **Views** (on posts from a period) | The current lifetime views of the tracked posts **posted** in the period. A post from last week that keeps growing raises this number. | | **Views gained** (impressions) | The views tracked posts **gained** during each period, from their stats history. Campaign trend charts show this as "Views gained". It is a different number from Views. | | **Engagement rate** | (likes + comments + shares + saves) ÷ views, **summed over all posts first**. A post with 10 views and 5 likes does not count as much as a post with 1M views. | | **Avg views / post** | Views ÷ tracked posts for the period. | | **Creators who posted** | Active creators with at least one tracked post in the period. | | **Creators** | Active creators in the project (or the campaign's member creators), whether or not they posted. | | **Follower growth** | Measured per account: the latest follower count in the range minus the count at the start of the range. An account added partway through the range is measured from its first snapshot, so connecting a new account does not count its whole audience as growth. Accounts are then summed per creator. A loss shows as a loss. | | **Campaign totals** | Every video in the campaign, its sub-campaigns and its member creators, **each counted once**. A video in two campaigns appears in both campaigns' rows, but the Campaign Insights totals count it once. | | **Posts on TikTok / Instagram** | The post count the platform shows on the creator's profile. It is not the number of tracked posts: it includes posts from before tracking began and posts awaiting review, and excludes posts the creator deleted. The creator page shows **Tracked posts** next to it. | | **Goal status** | *On Track*, *Behind Pace* (70 to 99% of the expected amount so far), *At Risk* (below 70%), *Missed* and *Completed*, the same on the Goals page, the dashboard and Program Health. | ## Comparisons Delta chips compare a period's posts with the posts from the previous period of the same length, for example "+12% vs posts from the previous 30 days". Both sides are lifetime views of the posts made in each period, so recent posts, which are still gaining views, can make the current period look lower than it will end up. ## How fresh the numbers are View counts come from scheduled scrapes. Recent posts are rescraped every 12 to 48 hours and older posts less often (up to every 14 days). Pages show a line such as **Stats as of 2h ago · 74% of these 2,246 posts updated in the last 48h · oldest 12d ago** so you can see how current the numbers are. Program Health's **Data Freshness** tile is the share of the window's tracked posts whose stats were scraped in the last 48 hours. A creator's views chart is built from each post's stats history. A tracked post with no history yet (for example a photo carousel waiting for its first stats scrape) is not in the chart, and the chart says how many posts that affects. ## Payouts Payout amounts use their own rules (see [Payouts](https://www.trackagoat.com/docs/guides/payouts.md)) and are not changed by these definitions. Payout rows label the view counts they were calculated on: - **Views at payout time (date)**: the views recorded when the accrual was calculated. The post's live views may be higher now. - **Views since last paid**: CPM structures pay for the views each post gained since it was last paid. ## In the API and MCP - `GET /api/v2/analytics/aggregate/summary` returns the headline totals above for a project, a campaign, or every campaign at once (`campaigns=all`). See [Aggregate endpoints](https://www.trackagoat.com/docs/api-reference/aggregate.md). - The MCP tool `get_metric_totals` returns the same totals. - Creator responses keep `video_count` (posts on the platform) and add `tracked_post_count` and `pending_review_count`. --- Source: https://www.trackagoat.com/docs/guides/metrics --- # Comment Insights > What viewers say in the comments on your program's videos: questions about the app, sign-ups, “is this an ad?”, brand and competitor mentions, and a table by creator. Comment Insights shows what viewers say under your creators' videos. It puts the most-liked comments first and tags the ones you act on: people asking what the app is, people signing up, people asking whether a post is an ad, and mentions of your brand or a competitor. Find it under **Insights → Comment Insights** in the sidebar. (It used to be called Sentiment Radar under Tools; old links still work.) > **Note: Paid plans** > > Comment Insights is included in paid plans. Free plans don't collect comments, so the page shows an upgrade prompt instead. See [Plans and limits](https://www.trackagoat.com/docs/guides/limits.md). --- ## Which comments we collect Collecting comments costs a page load per video, so we collect them when they are worth reading: - **When:** once a video is **3 days old**, when most of its comments have arrived, and while it is less than 90 days old. - **Which videos:** videos where the platform shows at least **5 comments** (an org admin can change this in **Org settings → Comments**). The most-commented videos go first. - **How many:** up to **100 comments per TikTok video** and 20 per Instagram post. The platform returns the most-liked comments first, which are the ones shown here first anyway. - **Once more for big threads:** if a video's comment count grows by at least 50 and doubles after we collected it, we collect it once more. - Your plan sets how many videos can be collected per day. So the page shows a **sample**. The **What's collected** panel says how big it is, for example "of the 2,303 videos posted in this period, 859 have comments and we collected them from 327", the share of the platform's comments that is, and when comments were last collected. --- ## Filters The Insights toolbar at the top applies to the whole page: - **Period.** Comments are placed by when they were posted. The default is the last 30 days including today, in your organization's time zone. - **Platform.** All, TikTok or Instagram. - **Campaign.** One campaign, sub-campaigns included. - **Group by** sets the buckets of the positive-comments chart. --- ## The number strip | Number | What it counts | |---|---| | **Comments collected** | Comments posted in the period that we collected, with the share of the comments the platform shows on videos posted in the period. | | **App questions** | Comments that ask what the app is or how to sign up ("what app is this?", "how do I sign up?"). | | **Signing up** | Comments that say they are downloading, signing up or interested ("downloading rn", "just signed up"). | | **Flagged** | Comments that ask whether the post is an ad or the creator was paid, or that use words like scam, fraud or unsafe. | Select a number to filter the comment stream to it. --- ## Comment stream The comments in the period, **most liked first**. You can also sort by newest or oldest, search the text, and filter by sentiment or by signal. ### Signals | Signal | Means | |---|---| | **Flagged** | Asks if it's an ad, or brand-safety words (the two below together). | | **Asks if it's an ad** | "Is this an ad?", "did they pay you to post this?", "#ad". | | **Brand safety** | Scam, fraud, predator, unsafe, "report this". | | **App questions** | Asks what the app is or how to sign up. | | **Signing up** | Says they are downloading, signing up or interested. | | **Mentions your brand** | Names one of your brand terms. With none set, your organization's name. | | **Mentions a competitor** | Names one of your competitor terms. With none set, this signal is off. | | **Pay and rates** | Talks about pay or rates ("$20 an hour"). | | **Links and codes** | A link, a referral code, or "mention me when you sign up". | Signals come from word patterns, checked by hand against real comments before they were shipped. They are deliberately narrow, so they miss some comments rather than tag the wrong ones. Brand and competitor terms are set by an org admin in **Org settings → Comments**, separated by commas; they match whole words and phrases, so "care" doesn't match "carefully". Selecting a row in the **Creators** table filters the stream to that creator. Each comment links to its video's Comments tab and to the creator. ### Sentiment Each comment is labeled positive, neutral or negative by a sentiment model trained on social media posts, with extra rules for TikTok comments: laughter ("lmaooo", 😂), short agreement ("so real", "literally", "same") and positive emoji count as positive. On a sample of hand-labeled comments it agreed with people about 3 times in 4, against about 1 in 2 for the word list used before. Most negative comments are viewers talking about their own lives, not about the creator or your brand, so the page doesn't rank videos by negativity. Use the signals to find comments that need a response. A comment collected very recently may show a word-list label for a few minutes until the model rescores it; the **What's collected** panel says how many. --- ## Creators One row per creator with videos posted or comments collected in the period. | Column | What it shows | |---|---| | **Comments** | Comments collected in the period. | | **Per 1,000 views** | Comments per 1,000 views on the creator's videos posted in the period, from the platform's full comment count (not just the ones we collected). A rough measure of how much the audience talks back. | | **Positive** | Share of the creator's scored comments that are positive. Shown once there are at least 20, so a handful of comments can't swing it. | | **App questions, Signing up, Flagged** | Signal counts. | | **Top comment** | The creator's most-liked comment in the period (links and referral codes left out). | --- ## Positive comments chart The share of scored comments that are positive, per day, week or month. A bucket with fewer than 10 scored comments is left out. --- ## Weekly flag digest Every Monday, org admins get one notification per project when, in the past week: - comments were collected that ask whether a post is an ad or use words like scam or unsafe (the notification quotes up to three of them and names the creator and the video's date), or - a creator's share of positive comments fell by 20 points or more compared with the 30 days before, with at least 20 scored comments in both. Nothing is sent in a week with nothing to report. **Program Health** also shows an alert for flagged comments from the last 7 days. --- ## A video's Comments tab Each video has a **Comments** tab with every comment collected for it, the sentiment split, the same signal filters, and when its comments were last collected. Before a video qualifies, the tab says when it will. --- ## API and AI agents Everything on this page, a video's Comments tab, the weekly digest and the comment settings is available to API keys and AI agents: - `GET /api/v2/projects/{projectId}/comments`: the comment stream, with the same filters. See [Projects](https://www.trackagoat.com/docs/api-reference/projects.md). - `GET /api/v2/projects/{projectId}/comment-signals`: the numbers, coverage, creators table (rank it with `creator_sort`) and positive share over time. - `GET /api/v2/projects/{projectId}/comment-flags`: the weekly flag digest on demand. - `GET /api/v2/videos/{id}/comments`: one video's Comments tab. See [Videos](https://www.trackagoat.com/docs/api-reference/videos.md). - `GET` and `PATCH /api/v2/comment-settings`: the minimum comment count and the brand and competitor terms. Like the settings page, reading and changing them needs an org admin. - MCP tools: `list_comments`, `list_video_comments`, `get_comment_signals_summary`, `get_comment_flags`, `get_comment_settings` and `update_comment_settings`. See [MCP](https://www.trackagoat.com/docs/agents/mcp.md). Comments are never used for payouts or goals. - [Program Health](https://www.trackagoat.com/docs/guides/program-health.md): Daily program snapshot: alerts, KPIs, charts, and watchlists. - [Videos](https://www.trackagoat.com/docs/guides/videos.md): Per-video comments, stats, and tracking. - [Plans and limits](https://www.trackagoat.com/docs/guides/limits.md): What each plan includes. --- Source: https://www.trackagoat.com/docs/guides/comment-insights --- # Program Health > A daily snapshot of your creator program: alerts, trends, KPIs, and watchlists in one place. The Program Health page gives program managers a single view to answer "is my program healthy today?" without digging through individual creator or video pages. It refreshes on demand and covers alerts, key performance indicators, posting and engagement trends, and creator watchlists. ## Campaign filter The **All campaigns** selector at the top scopes every metric on the page to a single campaign. When a campaign is selected, all KPIs, charts, alert thresholds, and watchlists narrow their creator and video sets to that campaign, including nested sub-campaigns, member creators, and videos added to the campaign directly (the same scope Insights uses). The subtitle on each widget updates to reflect the scope. Select **All campaigns** (the default) to see program-wide numbers. The page covers the last 30 days including today, in your time zone, and counts tracked posts only, the same as the dashboard and Insights. See [How metrics are calculated](https://www.trackagoat.com/docs/guides/metrics.md). --- ## Alerts band The alert bar at the top collapses all active alerts into a single row. Tap or click anywhere on the bar to open the **Active Alerts** sheet, where each alert is shown in full with a dismiss button. Dismissed alerts move to **Alert History** (the bell icon). ### Over-posting suppression Fires when a creator published ≥ 7 videos in a day and ≥ 2 of them had fewer than 100 views 2+ hours after posting: a signal that TikTok likely suppressed distribution. See [Over-posting & Suppression](https://www.trackagoat.com/docs/guides/over-posting.md) for details. ### Similar usernames Fires when three or more active creators have similar handles: a shared prefix or suffix of at least 5 characters (e.g. `brandwith*`), or handles that differ by only one or two characters. TikTok may apply network-level distribution limits to accounts it groups together. See [Similar Creator Pools](https://www.trackagoat.com/docs/guides/similar-creator-pools.md) for details. ### Flagged comments On paid plans, fires when comments posted in the last 7 days ask whether a post is an ad (or the creator was paid), or use words like scam or unsafe. The alert quotes up to three of them with the creator and the video's date, and links to the flagged comments in [Comment Insights](https://www.trackagoat.com/docs/guides/comment-insights.md). Dismissing it hides it until a newer flagged comment arrives. ### Private accounts Fires when a tracked account is currently private. Its profile still updates, so it never fails and never auto-disables, but no new post can be discovered, so goals read as missed and payout periods hold. See [Private accounts](https://www.trackagoat.com/docs/guides/accounts.md#private-accounts). ### Roster churn Fires when more creators were deactivated than added in the last 7 days. A net-negative roster change can indicate creator drop-off or a pruning campaign. ### Engagement rate drop Fires when the program-wide engagement rate of the last complete week (Mon to Sun) is ≥ 20% below the week before. The current, partial week is not compared, because its posts are too new for a fair read. Check for content quality shifts, algorithm changes, or audience fatigue. ### Sudden view drops Fires for creators whose last 3 videos averaged fewer than 50% of their 30-day median view count. Only videos at least 72 hours old are compared on both sides, so a creator who just posted isn't flagged while their new videos are still picking up views. This is a possible shadowban or suppression signal for individual creators. ### At-risk goals Fires when one or more creator goals are at risk of being missed or have already been missed in the current period. Lists the top affected creators and their progress. --- ## KPI strip Four compact stat cards show program-wide numbers at a glance. | Card | What it measures | |---|---| | **Active Creators** | Count of active creators right now, with a week-over-week delta | | **Total Views** | Current view counts of tracked videos posted in the last 30 days (today and the 29 days before), vs videos posted in the 30 days before that. This is lifetime views of those posts, not views gained during the window | | **Posts This Week** | Tracked posts published so far this week (Mon to Sun), vs the same elapsed window last week | | **Data Freshness** | % of the tracked posts in the window whose video stats (views, likes…) were scraped in the last 48 hours. Below 80% turns orange: some numbers on the page are stale | --- ## Charts ### Daily Posts Volume A stacked bar chart of posts published per day over the last 30 days, overlaid with the prior-30-day period (each day compared with the day 30 days earlier) as a comparison series. Use this to spot posting cadence changes: a sudden drop or spike is usually worth investigating. ### Engagement Rate Trend Program-wide engagement rate per week (Mon to Sun) for the last 8 weeks. ER is calculated as `(likes + comments + shares + saves) / views`, the same formula used across Insights. The current week is partial and usually reads high, because its posts are still gaining views. A sustained downward trend here should trigger a content quality review. ### Avg Views / Video Average views per video per week (Mon to Sun) for the last 8 weeks. Divergence between this and the ER Trend chart can indicate algorithm distribution changes (views up, ER flat) or content relevance shifts (views flat, ER up). ### Follower Growth Sum of follower counts across all active creators (summed across each creator's accounts), plotted daily over the last 30 days. Each account contributes its most recent follower count as of that day, so the line doesn't depend on which accounts happened to be updated that day; an account added mid-window counts at its first known value before then. Sharp drops usually reflect a creator deactivation or a platform account ban rather than organic loss. ### Goal Status A donut chart titled **Goal Status** showing the split of active creator goals by status: On Track, At Risk, and Missed. Only goals judged for the current period are counted. The legend shows counts and percentages. Use this to quickly assess whether your program's goal commitments are being met. --- ## Watchlists ### Stale Creators Active creators who have not published a tracked post in the last 14 days. A post still counts after its tracking has expired, so creators silent for longer than your expiry window stay on the list. Sorted by longest silence first. Click a creator to open their detail page and investigate. ### Update Failures Active creators we keep failing to update. A failure count of 3+ usually indicates a handle change, a platform account ban, or persistent rate limiting. After 5 consecutive failures a creator is [auto-disabled](https://www.trackagoat.com/docs/guides/accounts.md#auto-disabled-creators). Click a creator to view their profile and check for issues. ### Posting Consistency Top and bottom creators by posts per week over the last 30 days (all active creators are ranked). Use this to identify who is driving your posting volume and who is falling behind. When there are 5 or fewer creators in scope, only the ranked list is shown. ### Underperformers Creators who are simultaneously in the bottom 25% for engagement rate (same formula as above, including saves) AND posting fewer than 1 video per week in the last 30 days. These creators may need coaching, a content refresh, or removal from the program. ### Active Campaigns All campaigns with tracked videos, ranked by total views on posts published in the last 30 days. Bar length is relative to the top campaign. Click a campaign name to open its detail page. ### Tracking Gaps Creators where the number of tracked posts is more than 10% below the platform's reported post count within an account's tracking window, a sign some posts may have been missed. It also flags creators that haven't been checked for new videos in over 48 hours. trackagoat closes these gaps automatically: flagged creators are prioritized for a deep re-scan (see [Discovery completeness](https://www.trackagoat.com/docs/guides/scraping.md#discovery-completeness)), so real gaps are typically filled within a cycle or two. Click a creator to see their post list. --- ## Refreshing data The page fetches all data in a single batched request on load. Data is cached for 60 seconds: navigating away and back within that window serves the cached response. To force a fresh fetch, reload the page. Data Freshness reflects the state of the data-collection pipeline as of the last page load. If freshness is below 80%, check the [scraping guide](https://www.trackagoat.com/docs/guides/scraping.md) for troubleshooting steps. ## From an agent or the API The same health check is available to scripts and AI agents: [`GET /api/v2/projects/{projectId}/program-health`](https://www.trackagoat.com/docs/api-reference/projects.md#get-apiv2projectsprojectidprogram-health) returns the verdict, KPIs, alerts and watchlists as compact JSON, and the [MCP server](https://www.trackagoat.com/docs/agents/mcp.md) exposes it as the `get_program_health` tool. Both accept a campaign and a 7, 14, 30 or 90-day window (30 by default, like this page). - [Creators](https://www.trackagoat.com/docs/guides/accounts.md): Managing and tracking creators in your program. - [Goals](https://www.trackagoat.com/docs/guides/goals.md): Setting and monitoring creator performance goals. - [Comment Insights](https://www.trackagoat.com/docs/guides/comment-insights.md): What viewers say in the comments: app questions, sign-ups, ad doubts and a table by creator. - [Scraping](https://www.trackagoat.com/docs/guides/scraping.md): How TikTok data is fetched and what to do when it fails. --- Source: https://www.trackagoat.com/docs/guides/program-health --- # Notifications > Which in-app notifications trackagoat sends, who gets them, and how to use the notification bell. ## Overview trackagoat sends **in-app notifications** for key events. Notifications appear in the notification bell (🔔) in the top navigation bar. ## Notification types | Notification | Who gets it | When it fires | |--------------|-------------|---------------| | **Invitation received** | The person invited | Someone invites you to join an org. Accept or decline from the notification. | | **Invitation expired** | The person who sent the invite | An invitation you sent expired before it was accepted. | | **Scraping paused** | Org admins | An account was auto-disabled after 5 consecutive update failures. Opens the auto-disabled creators page. | | **Account went private** | Org admins | A tracked account switched to private, so its posts can no longer be tracked. | | **Account is public again** | Org admins | A private account reopened and tracking resumed. | | **Comment flags** | Org admins | Weekly, on Mondays, on paid plans: comments collected that week that ask whether a post is an ad or use words like scam or unsafe (up to three quoted), and creators whose share of positive comments fell by 20 points or more. One per project, only when there is something to report. Opens the flagged comments in [Comment Insights](https://www.trackagoat.com/docs/guides/comment-insights.md). | There are no notifications for accepted invitations or plan limits. When you reach a plan limit, the app shows a modal; an org that is over a limit sees a banner on every page (see [Limits](https://www.trackagoat.com/docs/guides/limits.md)). ## The notification bell The bell icon shows an unread count badge. Click it to open the notification panel, where you can: - Read notification details - Click through to the relevant page - Mark all as read ## Invitations When you're invited to join an org, you'll receive: 1. An email with an invite link (if email is configured) 2. An in-app notification if you're already logged in The invitation link takes you to an accept/decline page. Accepted invitations immediately grant you access to the org and redirect you to a welcome screen. > **Note:** > > Invitations expire after a set period. If a link has expired, ask the org admin to re-send the invitation from **Org settings → Members → Invitations**. - [API Keys](https://www.trackagoat.com/docs/api-reference/authentication.md): Creating and managing API keys for programmatic access. - [Limits](https://www.trackagoat.com/docs/guides/limits.md): Plan tiers and how limits are enforced. --- Source: https://www.trackagoat.com/docs/guides/notifications --- # Shareable creator pages > Give a creator a private, PIN-protected link where they can check their own analytics and payouts. A **shareable page** is a private link you hand to a creator so they can see their own numbers (views, engagement, goals, and what they've earned) without an account, a login, or an invitation. Every page is protected by a 4-digit **entrance PIN**. Nothing loads until the PIN is entered correctly, and the pages are excluded from search engines. ## What the creator sees You choose one or both sections when you create the page: | Section | Contents | |---------|----------| | **Analytics** | Total views, likes, comments, shares, followers, and engagement rate; a views-over-time chart with 7 / 30 / 90 day and all-time ranges; progress against their current goals; and a paginated list of their tracked videos with per-video stats. | | **Payout accruals** | Total earned, awaiting payment, and paid to date; the payout terms they're on; every accrual with what earned it; and the payments you've recorded. | The page shows your org name, the creator's name, and their platform handles. It shows nothing about other creators, other projects, or your internal notes. > **Note:** > > Voided and rejected accruals and payments are never shown. Those are operator corrections, and surfacing them to a creator reads as money being taken away. ## The link format Every shareable page lives at the same predictable address: ``` https://www.trackagoat.com/share/creator/ ``` The payouts section is at `/share/creator//payouts`. That means anything holding a creator id, including an AI agent working against the API, can build the link without a lookup. It also means the URL itself is **not** a secret. The PIN is the access control, which is why we rate-limit and lock out repeated wrong guesses. ## Creating a page for one creator **Step 1: Open the creator** Go to the creator, then the **Info** tab. **Step 2: Find Shareable page** Scroll to the **Shareable page** section and click **Create shareable page**. **Step 3: Choose what to include** Tick **Analytics**, **Payout accruals**, or both. At least one is required. **Step 4: Set a PIN** Type a 4-digit PIN, or leave the field blank and one will be generated for you. **Step 5: Send the link and PIN** Copy the link and the PIN and send them to the creator. Only **org admins** can create, change, delete, or reveal the PIN of a shareable page. ## Creating pages in bulk **Step 1: Select creators** On the **Creators** list, tick the creators you want. **Step 2: Click Shareable Pages** Click **Shareable Pages** in the bulk action bar. **Step 3: Choose sections and PIN style** Pick the sections, then choose whether each creator gets **their own PIN** (recommended) or whether the whole batch **shares one PIN**. **Step 4: Copy the results** The results table lists every creator with their link and PIN. Use **Copy all** or **Download CSV**. This is the only place the PINs appear together. Creators who already have a page will have their settings replaced and a new PIN issued. > **Warning:** > > A shared batch PIN is easier to announce to a cohort, but one leaked PIN opens every page in the batch. Prefer unique PINs unless you have a specific reason not to. ## Managing an existing page From the creator's **Info** tab you can: - **Copy the link** or open it in a new tab - **Reveal and copy the PIN**: it stays masked until you click the eye icon - **Change the sections** shown, with the toggles - **Disable the page**: the link then shows a neutral "not available" message instead of data. Re-enabling reuses the same URL and PIN. - **Regenerate the PIN**: issues a new one and immediately signs out anyone currently viewing the page - **Delete the link** entirely > **Note:** > > Because the URL is derived from the creator id, it can't be changed. If a link is shared with the wrong person, regenerate the PIN; that's what invalidates their access. ## Security - **Nothing renders before the PIN is verified.** The locked page contains no creator data at all: not in the HTML, not in a background request. - **Attempts are limited.** Ten wrong guesses locks further attempts for 15 minutes, applied both to the link and to the guesser's network address. - **Unlocking lasts 30 days** via a secure, HTTP-only cookie scoped to that one link. Regenerating the PIN ends every session immediately. - **Pages are not indexed.** They're excluded in `robots.txt` and carry a `noindex` directive. - **Every failure looks the same.** A wrong PIN, a disabled link, a deleted link, and a creator id that never existed all return the same response, so the URL can't be used to probe for which creators exist. ## Using the API Shareable pages are fully manageable through the v2 API, and every creator response carries a `share_link` object. See the [Share links API reference](https://www.trackagoat.com/docs/api-reference/share-links.md). ```bash # Create a page and capture the PIN curl -X POST -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -d '{"show_analytics": true, "show_payouts": true}' \ "https://www.trackagoat.com/api/v2/creators//share-link" ``` --- Source: https://www.trackagoat.com/docs/guides/shareable-pages --- # Plans and limits > How trackagoat enforces per-organization and per-user limits based on plan tier, and how limits surface in the UI and API. trackagoat enforces per-organization limits based on your plan tier. This page explains what each limit does, how they're enforced, and what happens when you hit one. > **Note:** > > Tracking limits count **accounts**: the platform presences that actually get scraped: not creators. You can add as many creators as you like; you're limited by how many accounts (across TikTok and Instagram) you track. See [Creators and accounts](https://www.trackagoat.com/docs/guides/accounts.md). ## Plan tiers | Tier | Accounts | Tracked videos | Projects | Members | API requests/day | Manual updates/day | Comment videos/day | Stats retention | | --- | --- | --- | --- | --- | --- | --- | --- | --- | | Free | 5 | 10 | 1 | 2 | 200 | 0 | 0 | 90 days | | Starter | 50 | 100 | 3 | 5 | 1,000 | 1 | 40 | 365 days | | Ultra | 300 | 3,000 | 10 | 50 | 50,000 | 3 | 40 | Unlimited | > **Note:** > > These values are read live from the current plan configuration, so they always reflect the limits in force today. Tier upgrades are handled manually: email **support@trackagoat.com** with your org name and the tier you'd like. ## Limit keys | Limit | What it caps | |-------|-------------| | `max_accounts_per_org` | Active tracked accounts (platform presences) across all projects: creators themselves are uncounted | | `max_tracked_videos_per_org` | Videos whose stats we keep updating, across all projects. Over the limit, the oldest are paused: see [Tracked videos](#tracked-videos) | | `max_projects_per_org` | Active (non-archived) projects in your org. Archiving one frees a slot; un-archiving takes it back, so it's blocked when you're at the limit. | | `max_members_per_org` | Team members in your org | | `max_campaigns_per_project` | Campaigns per project | | `max_campaign_items_per_campaign` | Items per campaign | | `max_api_keys_per_org` | Active API keys | | `max_api_requests_per_day` | API requests per calendar day (org time zone) | | `max_manual_scrape_triggers_per_day` | Manual "Update Now" triggers per day (org time zone) | | `max_comment_videos_per_day` | Videos whose comments are collected per day (org time zone) for [Comment Insights](https://www.trackagoat.com/docs/guides/comment-insights.md). `0` means no comment collection: on Free, Comment Insights shows an upgrade prompt and the API returns `403 plan_feature_unavailable` | | `max_payout_methods_per_org` | Payout methods (built-in + custom) per org | | `max_payout_structures_per_project` | Payout structures per project | | `max_conversion_events_per_project` | **Active** conversion event types per project. Deactivating one frees a slot. Conversion *volume* is never capped. | | `min_video_stats_hours` | Minimum hours between video stats updates | | `min_discover_videos_hours` | Minimum hours between video discovery runs | | `min_creator_profiles_hours` | Minimum hours between creator profile updates | | `stats_history_retention_days` | Days of stats history retained before pruning | ## Tracked videos Your plan keeps a set number of videos up to date. A video counts while trackagoat keeps refreshing its views, likes and comments: posts found on your tracked accounts, posts you included from the Inbox, and videos you added by link. Pending, excluded and expired videos don't count, and neither do videos on deleted accounts. **New posts are always tracked.** When your organization goes over its limit, trackagoat keeps your newest videos updated and **pauses your oldest ones** (by posting date): - A paused video keeps its last stats and shows a **Paused: plan limit** badge. Filter Videos by **Paused (plan limit)** to see them. - A paused video **still counts as posted**: for posting goals, compliance, the calendar and Program Health. - It stays payout-eligible **with its last stats**. Its views stop growing, so CPM and view-milestone payouts stop counting new views on it. Anything already earned is unaffected. - **Keep tracking** on a paused video resumes it and exempts it from the limit, and your oldest other video is paused instead. Exempt videos count toward the limit but are never paused. - When room opens up (an upgrade, or videos you exclude or that expire), the newest paused videos resume on their own. Organizations already over the limit when this took effect, and any organization whose plan is downgraded, get **14 days** before anything is paused. The Usage page shows how many videos are tracked and paused, and warns you at 80% of the limit. Adding a video at the limit (by link, from the Inbox, or by resuming one) pauses your oldest video to make room. The request is only refused with `402 limit_reached` when every tracked video is set to keep tracking. ## How limits are enforced **Count limits** (accounts, projects, members, etc.) are enforced atomically: two concurrent requests cannot both succeed past the cap. You'll see a modal in the UI explaining which limit was hit. **Daily limits** (API requests, manual updates) reset at midnight in your org's time zone. If you hit your daily API limit, requests return HTTP 429. **Update frequency floors** (`min_*_hours`) set a minimum interval between *scheduled* updates. The scheduler runs each update type no more often than the larger of your org's configured interval and the plan minimum, and a configured interval below the minimum is raised to it when saved. On the Free plan (24 / 24 / 24) that means profiles, new-post discovery and video stats each run once a day. These floors do **not** apply to manual "Update Now" triggers, which are governed instead by `max_manual_scrape_triggers_per_day`. See [How scraping works](https://www.trackagoat.com/docs/guides/scraping.md) for a full breakdown of manual vs. automatic data collection. **Stats retention**: a daily background job prunes history older than your retention window. Ultra tier has unlimited retention. ## When you hit a limit **In the dashboard:** A modal appears with the limit key, your current count, and the cap. If your org is retroactively over a limit (e.g., after a tier downgrade), a banner appears at the top of every page until the org is back within limits. **In the API:** Count limits return HTTP `402`: ```json { "data": null, "error": "Limit reached: max_accounts_per_org", "meta": { "code": "limit_reached", "limitKey": "max_accounts_per_org", "current": 5, "max": 5 } } ``` Daily rate limits return HTTP `429`: ```json { "data": null, "error": "Rate limit exceeded", "meta": { "code": "rate_limit_exceeded", "retryAfter": 3421 } } ``` `retryAfter` is seconds until the counter resets at midnight in your org's time zone. ## Per-user limits One limit applies per user rather than per org: | Limit | Description | |-------|-------------| | `max_orgs_per_user` | Maximum orgs a user can belong to | The default is 6. If you need to belong to more orgs, contact support. ## Requesting a raise Limits are adjusted by the trackagoat team. There is no self-serve upgrade path yet. To request a raise: **Step 1: Email support** Email **support@trackagoat.com** with your org name and which limit you need raised. **Step 2: Receive your override** We'll apply a per-org override. Overrides take effect immediately with no restart required. --- Source: https://www.trackagoat.com/docs/guides/limits --- # Team and roles > Invite teammates to your org and see what org admins and members can each do. Everyone on your team works inside one **organization**. Each person has one of two roles there: **admin** or **member**. This page covers inviting people and what each role can do. ## Invite a teammate **Step 1: Open the members page** Go to **Org settings → Members** (org admins only). **Step 2: Send an invitation** Enter their email and choose a role. They get an email with an invite link and a notification if they already have an account. **Step 3: They accept** The link opens an accept or decline page. Accepting gives them access to the org straight away. Invitations expire. If a link has expired, re-send it from **Org settings → Members → Invitations**. Your plan sets how many members an org can have; see [Plans and limits](https://www.trackagoat.com/docs/guides/limits.md). ## What each role can do | | Member | Admin | |---|:---:|:---:| | See every project, creator, video, campaign and goal in the org | ✓ | ✓ | | View payouts and the payout tracker | ✓ | ✓ | | Approve posts in the Inbox, edit campaigns and the notes on creators, videos and campaigns | ✓ | ✓ | | Edit the organization, project and payout documents | | ✓ | | Record and void payments, unassign payout structures | | ✓ | | Remove creators and videos, delete campaigns | | ✓ | | Re-enable auto-disabled creators | | ✓ | | Trigger **Update Now** and change update intervals | | ✓ | | Create, change or reveal the PIN of a creator page | | ✓ | | Invite and remove members, change roles | | ✓ | | Create and revoke API keys | | ✓ | | Get scraping, privacy and limit notifications | | ✓ | Admin rights apply across every project in the org. ## API keys and roles An API key acts as one member: the admin who created it, or the member the admin picked under **Acts as**. Everything the key does is attributed to that member, and it can never do more than they can in the table above. If that member is removed from the org, the key stops working. A key an admin made for someone else also stops working when that admin leaves, is changed to member or is suspended, since they have seen it. If the member a key acts as is changed from admin to member, the key loses admin-only actions. See [Authentication and API keys](https://www.trackagoat.com/docs/api-reference/authentication.md#who-a-key-acts-as). > **Tip:** > > For a teammate's coding agent, have them sign in over MCP with their own account, or create a key that acts as them. For long-running integrations, have an admin who will stay in the org create the key, acting as a member who will stay too. ## READMEs Every member has a README, their personalization document: who they are, their job in the program and how they want AI agents to help. Edit yours in **Settings → My README**. Everyone in the org, and their agents, can read every member's README, as well as the organization, project, campaign, creator, video and payout documents, so keep anything private out of them. Only you can edit your own. Your teammates' READMEs are listed on the organization's README page. On every README page (yours, the organization's, a project's, a campaign's, a creator's or a video's), if the document changes while you're editing it, saving stops and asks you to reload, and your unsaved draft is kept so you can copy it back in. See [Notes and documents](https://www.trackagoat.com/docs/agents/notes.md). ## More than one org You can belong to several orgs, with a different role in each. Switch between them from the app sidebar. Your plan's **orgs per user** limit caps how many orgs you can create. --- Source: https://www.trackagoat.com/docs/guides/team --- # Support > Getting help with trackagoat, reporting issues, and contacting the team. ## Contact For all support requests, email **support@trackagoat.com**. Include: - Your organization name - A description of the issue or request - Any relevant error messages or screenshots ## Common requests ### Tier upgrade Tier upgrades are currently handled manually: Stripe billing is not yet wired. To request an upgrade: > **Note: Tier upgrade** > > Email **support@trackagoat.com** with your org name and the tier you'd like. Upgrades take effect within one business day. ### Limit raise If you need a specific limit raised without a full tier upgrade: > **Note: Limit raise** > > Email **support@trackagoat.com** with your org name and which limit key you need raised (e.g., `max_accounts_per_org`). Per-org overrides take effect immediately once applied by the team. ### Data issues If you notice a creator's stats look wrong or a scrape has stalled: **Step 1: Check last_scraped_at** Check `last_scraped_at` in the creator detail view. **Step 2: Trigger a manual refresh** Try triggering a manual refresh (if you have daily triggers remaining). **Step 3: Contact support** If the issue persists after 24 hours, contact support. ## Reporting bugs For bug reports, include: - What you were trying to do - What happened instead - Browser and OS (for UI issues) - The URL you were on ## Response times Support is currently handled by a small team. Expect responses within 1 to 2 business days. Critical issues (data loss, complete outages) are escalated immediately. --- Source: https://www.trackagoat.com/docs/platform/support --- # Brand assets > Logos, wordmarks, colors, and usage guidelines for the trackagoat brand. trackagoat is built around a pixel-art identity: retro, distinct, and a little irreverent. Use these assets when writing about trackagoat, building integrations, or creating content that references the platform. > **Note: Asset usage** > > These assets are provided for press, integrations, and editorial coverage. For commercial licensing or white-label use, contact **support@trackagoat.com**. --- ## Brand name Always write as **trackagoat**: one word, all lowercase. Never "TrackAGoat", "Trackagoat", "Track A Goat", or any other variation. --- ## Logo The trackagoat logo is a pixel-art goat. It's low resolution by design: the rough pixel edges are the aesthetic, not a rendering artifact. Don't smooth, upscale, or apply CSS filters to it. ### Logo previews ![trackagoat logo: 24px resolution](https://www.trackagoat.com/brand/trackagoat-logo-24res.png) ### Downloads | Asset | Notes | Download | |-------|-------|----------| | Logo (standard) | PNG · 24px base res · use at any size above 24px | [Download](https://www.trackagoat.com/brand/trackagoat-logo-24res.png) | | Logo (small) | PNG · 12px base res · use below 24px display size | [Download](https://www.trackagoat.com/brand/trackagoat-logo-12res.png) | --- ## Wordmark The wordmark combines the pixel-art goat with "trackagoat" set in **Jersey 10**, a bitmap display font. Two variants are provided: use whichever has sufficient contrast against its background. ### Wordmark previews ![trackagoat wordmark: light mode](https://www.trackagoat.com/brand/trackagoat-wordmark-lightmode.svg) ### Downloads | Asset | Use on | Download | |-------|--------|----------| | Wordmark: light | Light / white backgrounds | [Download](https://www.trackagoat.com/brand/trackagoat-wordmark-lightmode.svg) | | Wordmark: dark | Dark / black backgrounds | [Download](https://www.trackagoat.com/brand/trackagoat-wordmark-darkmode.svg) | --- ## Colors trackagoat uses a deliberately minimal palette. The identity relies on the pixel-art character rather than color: black and white are the primary surfaces, with supporting neutrals. | Swatch | Name | Hex | Usage | |--------|------|-----|-------| | ⬛ | Black | `#09090B` | Primary text, dark backgrounds | | ⬜ | White | `#FFFFFF` | Page backgrounds, light surfaces | | 🔳 | Zinc 900 | `#18181B` | Primary actions, CTA buttons (light mode) | | ◻️ | Zinc 50 | `#FAFAFA` | Primary actions (dark mode), near-white | | 🔲 | Zinc 500 | `#71717A` | Secondary / muted text | | ▫️ | Zinc 200 | `#E4E4E7` | Borders, dividers | > **Tip:** > > When placing the logo or wordmark on a colored or photographic background, ensure a minimum 4.5:1 contrast ratio against the nearest background color. --- ## Typography | Role | Font | Notes | |------|------|-------| | Display / headings | **Jersey 10** | Google Fonts · pixel bitmap · brand-distinctive | | Body text | System sans-serif | Inter on supported systems | Jersey 10 is available free from [Google Fonts](https://fonts.google.com/specimen/Jersey+10). --- ## Usage guidelines **Do:** - Use the provided logo and wordmark files unchanged - Use the light wordmark on dark backgrounds and the dark wordmark on light backgrounds - Maintain clear space around the logo equal to at least the logo's height on each side - Refer to the product as "trackagoat" (lowercase) in running text **Don't:** - Stretch, rotate, recolor, or add effects to the logo - Place the logo on a background with insufficient contrast - Use "trackagoat" as a verb (e.g. "trackagoat your creators": just say "track your creators") - Imply affiliation with or endorsement by TikTok --- ## Artwork & textures These are the illustration and texture assets used across the trackagoat product and marketing. Free to use however you want. ### Goatzilla A giant pixel-art goat destroying New York City. Used in the docs sidebar. Based on the idea that trackagoat is the biggest G.O.A.T. in the game. ![Goatzilla: pixel art giant goat stomping NYC](https://www.trackagoat.com/brand/trackagoat-docs-sidebar.png) [Download Goatzilla](https://www.trackagoat.com/brand/trackagoat-docs-sidebar.png) --- ### Hero backgrounds The marketing hero imagery: a landscape photo with a pixel-art style grade applied, used as the full-bleed background on the trackagoat homepage. ![Hero background: light mode](https://www.trackagoat.com/brand/trackagoat-hero-bg-light.png) | Variant | Download | |---------|----------| | Light mode | [Download](https://www.trackagoat.com/brand/trackagoat-hero-bg-light.png) | | Dark mode | [Download](https://www.trackagoat.com/brand/trackagoat-hero-bg-dark.png) | --- ### World texture A topographic world map texture used as a subtle background element throughout the product. Works well as a very low-opacity overlay. ![World texture](https://www.trackagoat.com/brand/trackagoat-world-texture.png) [Download world texture](https://www.trackagoat.com/brand/trackagoat-world-texture.png) --- ## Press & integrations For press inquiries, integration listings, or brand licensing requests, contact **support@trackagoat.com**. --- Source: https://www.trackagoat.com/docs/platform/brand --- # API overview > Base URL, response envelope, pagination, plan access, the OpenAPI spec and API versioning. The trackagoat API is a REST API over HTTPS. Authenticate with a Bearer API key, send and receive JSON, and every response has the same shape: `{ data, error, meta }`. Every plan includes API access: Free allows 200 requests a day, Starter 1,000 and Ultra 50,000, with a per-key limit of 60 a minute. See [Plans and limits](https://www.trackagoat.com/docs/guides/limits.md) for the live numbers. **Base URL:** `https://www.trackagoat.com/api/v2` **Install with your coding agent.** Paste this prompt into Claude Code, Codex, Cursor or another coding agent. It installs the MCP server, has you sign in, and checks the connection. Prompt to paste: ```text Install the trackagoat MCP server in this client and connect it to my trackagoat account. trackagoat tracks creators' public TikTok and Instagram posts, works out what each creator is owed from the payout structures I set up, and shows which creators need attention. Its MCP server can read all of that and do the day-to-day work too: add creators and videos, set goals, manage payout structures and record payments. Every record in trackagoat, and each person, has a Markdown document: read the relevant ones before you act, and add what you find. Details: https://www.trackagoat.com/docs/agents/mcp.md. If I don't have an account yet, follow https://www.trackagoat.com/docs/agents/setup.md instead. Every plan includes MCP and API access. New organizations start on Free, which allows 200 requests a day (each MCP tool call counts as one) and 1 API key. Starter and Ultra raise those limits; to upgrade, I email support@trackagoat.com with my organization name and the plan I want. If a call fails with rate_limit_exceeded, wait the retry_after_seconds it returns before trying again. Rules: - Don't create accounts, sign in for me or type passwords. Tell me what to open and click, then wait for me. - Don't ask me to paste an API key into this chat, and never print, log or commit one. If a key is needed, I'll set the TRACKAGOAT_API_KEY environment variable myself. Steps: 1. Connect the trackagoat MCP server: https://www.trackagoat.com/api/mcp (Streamable HTTP). Use sign-in (OAuth) first, because it needs no API key: a trackagoat page opens, I pick the organization and choose "View data", plus "Make changes" if you should be able to change things, such as adding creators, setting goals or recording payouts. Set it up for the client you are running in: - Claude Code: run `claude mcp add --transport http trackagoat https://www.trackagoat.com/api/mcp`. Then I run /mcp, choose trackagoat and sign in. - Codex CLI: run `codex mcp add trackagoat --url https://www.trackagoat.com/api/mcp`, then `codex mcp login trackagoat`. - Cursor: add `"trackagoat": { "url": "https://www.trackagoat.com/api/mcp" }` under "mcpServers" in ~/.cursor/mcp.json. Then I sign in from Cursor's MCP settings. - VS Code: add `"trackagoat": { "type": "http", "url": "https://www.trackagoat.com/api/mcp" }` under "servers" in .vscode/mcp.json. Then I start the server and sign in. - Claude.ai or Claude Desktop: I add a custom connector with that URL in Settings → Connectors. You cannot do this step. Sign-in is the best way for each person to connect their own agent: it identifies them directly, so nobody shares a key. Only if this client can't sign in, use an API key instead. An org admin creates one in trackagoat under Org settings → API keys; the key acts as one member (under "Acts as"), so it should act as me. I set it as TRACKAGOAT_API_KEY. You configure the client to send `Authorization: Bearer `, read from that variable, never written out. The setup guide has the exact config for each client. 2. Check the connection: call the list_projects tool and show me my project names. If the trackagoat tools don't appear yet, ask me to restart this session or reload the client, then try again. Then call get_my_profile to see who you're connected as and whether I already have a personalization document; if I do, follow it. 3. Offer to write my personalization document together, so every future session starts knowing me. It's the Markdown document in trackagoat under Settings → My README, and trackagoat sends it to you at the start of each session. Ask me a few short questions, one or two at a time: - who I am and my role; - what my job is in running the creator program; - the program, creators and goals I care about most; - how I most want you to help; - preferences: tone, how often to check in, what to look at first, and what you should never do without asking. Draft it in Markdown with these headings: About me, My job in the program, What I care about, How to help me, Preferences, Never without asking. Show me the draft, and save it with update_my_readme only after I approve. Everyone in my organization, and their agents, can read it, so leave out anything private. If I say no, move on. Later, when my role or preferences change, offer to update it. ``` ## Machine-readable spec This is the one place these locations are documented; other pages link here. - **OpenAPI spec:** [`GET /api/v2/openapi.json`](https://www.trackagoat.com/api/v2/openapi.json). Import it into Postman, Insomnia or any OpenAPI-compatible tool, or generate a client from it. - **Data model:** [`GET /api/v2/schema`](https://www.trackagoat.com/api/v2/schema). Entities, fields and relationships. See [OpenAPI and schema](https://www.trackagoat.com/docs/api-reference/schema.md). - **MCP server:** `https://www.trackagoat.com/api/mcp`, for Claude and other MCP clients. See [MCP server](https://www.trackagoat.com/docs/agents/mcp.md). - **Changelog:** [`GET /api/v2/changelog`](https://www.trackagoat.com/api/v2/changelog). Every changelog entry as JSON, newest first. No API key needed. See [Changelog API](https://www.trackagoat.com/docs/api-reference/changelog.md). - **These docs as Markdown:** [`/llms.txt`](https://www.trackagoat.com/llms.txt), or append `.md` to any docs URL. See [llms.txt and raw docs](https://www.trackagoat.com/docs/agents/llms-txt.md). > **Note:** > > The apex domain `trackagoat.com` redirects to `www.trackagoat.com`. Always use `www.trackagoat.com` as your base URL to avoid redirect overhead. ## The multi-platform model Trackagoat 2.0 is multi-platform. A **creator** is a person or brand, and an **account** is one of their presences on a platform (`tiktok` or `instagram`; the `platform` enum also accepts `youtube`, reserved for YouTube tracking, which isn't available yet). A creator owns one or more accounts, and **videos hang off accounts**. - Creators embed their `accounts[]` array and expose a derived top-level `handle` (the primary account's handle: TikTok first, else earliest-created). - Tracking configuration (`tracking_mode`, start/end dates, the handle) lives on the **account**, not the creator. - Plan limits count **accounts**: the thing that gets scraped: not creators. See the [Creators and accounts guide](https://www.trackagoat.com/docs/guides/accounts.md) for the full concept. ## Authentication See [Authentication](https://www.trackagoat.com/docs/api-reference/authentication.md) for how to generate and use API keys. ## Response envelope Every endpoint returns JSON with this shape: ```json { "data": , "error": , "meta": } ``` Successful responses have `data` set and `error: null`. Error responses have `data: null` and `error` set to a message string. ## Pagination List endpoints use **cursor-based pagination**: ```bash GET /api/v2/creators?limit=50&cursor= ``` | Parameter | Default | Max | Description | |-----------|---------|-----|-------------| | `limit` | 50 | 100 | Items per page | | `cursor` | None | None | Pass `meta.nextCursor` from the previous response | The `meta` object on list responses: ```json { "hasMore": true, "nextCursor": "2026-03-15T10:00:00Z" } ``` Keep fetching until `meta.hasMore` is `false`. ## Date ranges Analytics and stats endpoints accept `?from=YYYY-MM-DD&to=YYYY-MM-DD`. Both default to the last 30 days if omitted. ```bash GET /api/v2/analytics?entity=creator&entity_id=&metric=views&from=2026-01-01&to=2026-03-31 ``` ## API key scopes Keys carry explicit permission scopes set at creation time: | Scope | Grants access to | |-------|-----------------| | `read` | All `GET` endpoints | | `write` | All `POST`, `PATCH`, and `DELETE` endpoints | | `admin` | Org-administration endpoints (projects, members, invitations, API keys, creating orgs), PIN reads and hard deletes. Only effective while the key's owner is an **org admin** | Write endpoints reject keys that only have `read` scope with **403 `insufficient_scope`**. A key acts on behalf of one member: the person who created it, or the member an org admin picked under **Acts as**. It can never do more than that member can in the app. Its scopes are a ceiling, checked against the member's **current** org role on every request: if they are demoted from org admin the key loses `admin` and admin-only actions (`403 insufficient_role`), and if they leave or are removed from the org the key stops working (`401 key_member_removed`). A key made for someone else also stops working if the admin who created it leaves, is changed to member or is suspended (`401 key_creator_removed`, `key_creator_not_admin`, `key_creator_banned`). `GET /api/v2/me` shows who a key acts as. ## Idempotency POST endpoints that create resources accept an `Idempotency-Key` header (≤ 255 characters). Send a fresh UUID per logical request; if the call times out or the connection drops, retry with the **same key and the same body**. The action runs at most once. | You send the key again… | You get | |---|---| | after the first request finished, with the same body | The original status and body, plus an `Idempotency-Replay: true` header. Nothing runs again. | | while the first request is still running | **409 `idempotency_in_progress`** with a `Retry-After` header (seconds). Wait, then retry with the same key. | | with a different body | **422 `idempotency_key_reused`**. A key belongs to one request; use a new key for a new request. | | on a different endpoint | **409 `idempotency_conflict`** | | more than 24 hours after its last use | Treated as a new key: the request runs. | Which outcomes are replayed: - **2xx and 4xx are stored and replayed** for 24 hours, whether the request succeeded or was rejected (for example a `400 invalid_body` or a `404` on one item in a batch). Fix the request and send it with a **new** key. - **5xx and 429 are not stored.** Retry with the same key and the request runs again. - A few retryable conflicts are not stored either, such as the `409 notes_conflict` / `notes_busy` on the notes endpoint. Body comparison ignores key order and whitespace in the JSON. Secrets are never stored in the replay cache. A replayed API-key creation returns `key: null`, and a replayed share-link creation returns `pin: null`; fetch a PIN with `GET /api/v2/creators/{id}/share-link?include_pin=true` and a `write`-scope key. ```bash curl -X POST \ -H "Authorization: Bearer tga_" \ -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \ -H "Content-Type: application/json" \ -d '{"accounts": [{"platform": "tiktok", "handle": "charlidamelio"}]}' \ "https://www.trackagoat.com/api/v2/projects//creators" ``` ## API versioning The current version is **v2**. All endpoints live under `/api/v2/`. **v1 has been removed.** Every `/api/v1/*` path now returns **410 Gone** with a pointer to its successor: ```json { "data": null, "error": "API v1 has been removed. Use /api/v2 (see /docs/api-reference/overview).", "meta": { "code": "api_version_removed", "successor": "/api/v2" } } ``` The response also carries a `Link: ; rel="successor-version"` header. If you are migrating from v1, the biggest change is the [accounts model](https://www.trackagoat.com/docs/guides/accounts.md): a creator no longer has a `tiktok_handle`. It carries an `accounts[]` array, and you create creators with a list of accounts. ## Available endpoints For the full machine-readable spec, download [`/api/v2/openapi.json`](https://www.trackagoat.com/api/v2/openapi.json). ### Creators | Method | Path | Scope | Description | |--------|------|-------|-------------| | `GET` | `/api/v2/creators` | read | List creators (accounts embedded) | | `POST` | `/api/v2/creators` | write | Create a creator with one or more accounts | | `POST` | `/api/v2/projects/{id}/creators` | write | Add creator(s) to a project; bulk up to 50 | | `GET` | `/api/v2/creators/{id}` | read | Get a creator | | `PATCH` | `/api/v2/creators/{id}` | write | Update creator-level fields | | `DELETE` | `/api/v2/creators/{id}` | write | Soft-delete creator | | `POST` | `/api/v2/creators/{id}/scrape` | write | Trigger manual scrape (primary account, or one via `?account_id=`) | ### Accounts | Method | Path | Scope | Description | |--------|------|-------|-------------| | `GET` | `/api/v2/accounts` | read | List accounts | | `GET` | `/api/v2/creators/{id}/accounts` | read | List a creator's accounts | | `POST` | `/api/v2/creators/{id}/accounts` | write | Add an account to a creator | | `GET` | `/api/v2/accounts/{id}` | read | Get an account | | `PATCH` | `/api/v2/accounts/{id}` | write | Rename handle / change tracking config | | `DELETE` | `/api/v2/accounts/{id}` | write | Soft-delete account | | `POST` | `/api/v2/accounts/{id}/scrape` | write | Trigger a scrape for a single account | | `GET` | `/api/v2/accounts/{id}/stats` | read | Follower/stat snapshot history | ### Videos | Method | Path | Scope | Description | |--------|------|-------|-------------| | `GET` | `/api/v2/videos` | read | List videos (filter by platform / account / content group) | | `POST` | `/api/v2/projects/{id}/videos` | write | Add video(s) by URL; bulk up to 50 | | `PATCH` | `/api/v2/videos/{id}` | write | Update title, readme, tracking_status | | `DELETE` | `/api/v2/videos/{id}` | write | Remove video from tracking (sets `tracking_status: excluded`; the row is kept) | | `POST` | `/api/v2/videos/{id}/scrape` | write | Refresh this video's stats | | `POST` | `/api/v2/videos/{id}/tracking-disable` | write | Disable tracking | | `POST` | `/api/v2/videos/{id}/tracking-enable` | write | Re-enable tracking | | `POST` | `/api/v2/videos/{id}/tracking-unexempt` | write | Remove exemption flag | ### Content Groups | Method | Path | Scope | Description | |--------|------|-------|-------------| | `GET` | `/api/v2/content-groups` | read | List content groups | | `POST` | `/api/v2/content-groups` | write | Create a content group | | `GET` | `/api/v2/content-groups/{id}` | read | Get a content group | | `PATCH` | `/api/v2/content-groups/{id}` | write | Rename / edit notes / add or remove members | | `DELETE` | `/api/v2/content-groups/{id}` | write | Delete a content group | | `GET` | `/api/v2/content-groups/{id}/stats` | read | Combined stats across the group | ### Campaigns, Projects, Goals | Method | Path | Scope | Description | |--------|------|-------|-------------| | `GET` | `/api/v2/campaigns` | read | List campaigns | | `POST` | `/api/v2/projects/{id}/campaigns` | write | Create campaign | | `PATCH` | `/api/v2/campaigns/{id}` | write | Update campaign | | `DELETE` | `/api/v2/campaigns/{id}` | write | Delete campaign | | `POST` | `/api/v2/campaigns/{id}/items` | write | Add item to campaign | | `DELETE` | `/api/v2/campaigns/{id}/items` | write | Remove item from campaign | | `GET` | `/api/v2/projects` | read | List projects | | `POST` | `/api/v2/organizations/{id}/projects` | admin | Create project | | `PATCH` | `/api/v2/organizations/{id}/projects/{pid}` | admin | Update project | | `PATCH` | `/api/v2/projects/{id}/inbox` | write | Approve/exclude inbox videos | | `POST` | `/api/v2/creators/{id}/goals` | write | Create goal (platform-scoped) | | `PATCH` | `/api/v2/creators/{id}/goals/{gid}` | write | Update goal | | `DELETE` | `/api/v2/creators/{id}/goals/{gid}` | write | Delete goal | ### Organizations, Members, Invitations, API Keys | Method | Path | Scope | Description | |--------|------|-------|-------------| | `POST` | `/api/v2/user/organizations` | admin | Create organization | | `POST` | `/api/v2/organizations/{id}/members` | admin | Add member | | `PATCH` | `/api/v2/organizations/{id}/members/{uid}` | admin | Change member role | | `DELETE` | `/api/v2/organizations/{id}/members/{uid}` | admin | Remove member | | `POST` | `/api/v2/organizations/{id}/invitations` | admin | Send invitation | | `DELETE` | `/api/v2/organizations/{id}/invitations/{iid}` | admin | Revoke invitation | | `POST` | `/api/v2/organizations/{id}/api-keys` | admin | Create API key (only with scopes the calling key holds) | | `DELETE` | `/api/v2/organizations/{id}/api-keys/{kid}` | admin | Revoke API key | ### Payouts | Method | Path | Scope | Description | |--------|------|-------|-------------| | `GET/POST` | `/api/v2/payout-structures` | read/write | List / create payout structures | | `GET/PATCH/DELETE` | `/api/v2/payout-structures/{id}` | read/write | Read / update / delete structure | | `GET/POST/DELETE` | `/api/v2/payout-structures/{id}/assignments` | read/write | Manage creator assignments | | `GET/POST` | `/api/v2/payout-methods` | read/write | List / create payout methods | | `GET/PATCH/DELETE` | `/api/v2/payout-methods/{id}` | read/write | Read / update / delete method | | `GET/POST` | `/api/v2/payout-payments` | read/write | List / create payments | | `GET/PATCH` | `/api/v2/payout-payments/{id}` | read/write | Read / update payment | | `GET/PATCH` | `/api/v2/payout-accruals/{id}` | read/write | Read / update accrual | | `GET/POST` | `/api/v2/projects/{projectId}/conversion-events` | read/write | List / create conversion event types | | `GET/PATCH/DELETE` | `/api/v2/projects/{projectId}/conversion-events/{id}` | read/write | Read / update / deactivate an event type (`?mode=hard` needs `admin`) | | `GET/POST` | `/api/v2/projects/{projectId}/conversions` | read/write | List / record conversions (single or batch up to 500) | | `GET/DELETE` | `/api/v2/projects/{projectId}/conversions/{id}` | read/write | Read / void a conversion (`?mode=hard` needs `admin`) | | `POST` | `/api/v2/payouts/payment-receipts/upload-url` | write | Get signed receipt upload URL | ### Analytics | Method | Path | Scope | Description | |--------|------|-------|-------------| | `GET` | `/api/v2/analytics` | read | Unified analytics (time-series + aggregates) | | `GET` | `/api/v2/schema` | read | Machine-readable data model | ### Notes and documents | Method | Path | Scope | Description | |--------|------|-------|-------------| | `GET` | `/api/v2/notes` | read | Every document with text in the org, with previews (filter by type, project or text) | | `GET` | `/api/v2/notes/{entity_type}/{id}` | read | One document in full, with its `readme_version` | | `POST` | `/api/v2/notes/{entity_type}/{id}` | write | Append to or replace a document (organization, project and payout documents need an org admin) | | `GET` | `/api/v2/me` | read | Who the key acts as, and their personalization document | | `PATCH` | `/api/v2/me` | write | Replace that person's personalization document | | `GET` | `/api/v2/members` | read | The org's members and whether each has a personalization document | See [Notes and documents API](https://www.trackagoat.com/docs/api-reference/notes.md). ### CSV exports | Method | Path | Scope | Description | |--------|------|-------|-------------| | `GET` | `/api/v2/exports/{dataset}` | read | The same CSV as the app's Export buttons: creators, videos, campaigns, goals, campaign leaderboard, payouts tracker, payments, accruals or analytics | See [CSV exports](https://www.trackagoat.com/docs/api-reference/exports.md). ### Changelog | Method | Path | Scope | Description | |--------|------|-------|-------------| | `GET` | `/api/v2/changelog` | none (public) | trackagoat's changelog, newest first; filter by `since` and `q` | - [Authentication](https://www.trackagoat.com/docs/api-reference/authentication.md): API key setup and Bearer token usage. - [Errors](https://www.trackagoat.com/docs/api-reference/errors.md): Status codes, rate limits, and error handling. - [Schema](https://www.trackagoat.com/docs/api-reference/schema.md): Machine-readable data model for agents. --- Source: https://www.trackagoat.com/docs/api-reference/overview --- # Authentication and API keys > Create an API key, choose its scopes and authenticate with a Bearer token. Covers revocation, limits and testing safely. Every v2 API request needs an API key. Create one in the app, send it as a Bearer token, and give it only the scopes it needs. Every plan includes API access. Free and Starter allow 1 key each, Ultra allows 5. Free allows 200 requests a day, Starter 1,000 and Ultra 50,000. Keys are: - **Org-scoped**: a key can only reach data in the org it was created in. - **Shown once**: the plaintext key is displayed at creation and can't be recovered. - **Hashed at rest**: trackagoat stores only a SHA-256 hash of the key, never the plaintext. - **Acting as one member**: a key acts on behalf of the person who created it, or of the member an org admin picked under **Acts as**. Every call is attributed to that member and can never do more than they can in the app. If they leave or are removed from the org, the key stops working. If they stop being an org admin, the key loses the `admin` scope and admin-only actions. ## Creating an API key **Step 1: Open org settings** Go to **Org settings → API keys** (org admins only). **Step 2: Click + New API key** Give the key a descriptive name, such as `Claude MCP access` or `Analytics script`, choose who it **acts as** (you, by default, or any member of the org) and choose its [scopes](#permission-scopes). **Step 3: Copy your key immediately** After you click **Create**, the full key (`tga_<64 hex chars>`) is shown once. Copy it now. > **Warning:** > > If you lose the key, create a new one. trackagoat stores only a hash and can't recover the plaintext. ## Using the key Include the key as a Bearer token on every request: ```bash Authorization: Bearer tga_ ``` Example: ```bash curl -H "Authorization: Bearer tga_abc123" \ https://www.trackagoat.com/api/v2/projects ``` The one endpoint that needs no key is [`GET /api/v2/changelog`](https://www.trackagoat.com/docs/api-reference/changelog.md): the changelog is public. ## Key format ```text tga_<64 lowercase hex characters> ``` Example: `tga_a1b2c3d4e5f6...` (64 hex chars after the prefix). ## Org scoping Keys are org-scoped: all data returned is limited to the organization the key belongs to. A key can only read and write data inside its own org. Scoping stops at the org boundary; it does **not** go down to the project level. A key grants access to *every* project in its org, including ones created after the key was issued. There is no way to issue a key that is limited to a single project. ### Testing against the API Because keys are org-wide, do not run integration tests with a key from an org that holds real data: a test `POST` or `DELETE` will hit production records. Create a **separate organization** for testing and use a key from that org. It gives you complete isolation: separate creators, campaigns, conversions, and analytics, with no path back to your live org. Ask support to disable scraping on it, which also guarantees test conversions can never generate payout accruals. This is how you get a sandbox on trackagoat: there is no separate sandbox host or test-mode key prefix. The base URL and key format are the same; the org is what separates test from live. ## Permission scopes Each key carries one or more permission scopes, chosen at creation time. The scope controls which HTTP methods the key may use: | Scope | Grants | |-------|--------| | `read` | All `GET` endpoints | | `write` | All `POST`, `PATCH`, and `DELETE` endpoints (in addition to reads if `read` is also granted) | | `admin` | Org administration: projects, members, invitations, API keys and creating organizations; also entrance-PIN reads and hard deletes. Platform-admin endpoints under `/api/v2/admin/*` additionally need the owner to be a platform admin | New keys default to `['read', 'write']` unless you narrow the scopes at creation. ### Who a key acts as A key acts on behalf of one member of the org. By default that is the org admin who created it. An admin can instead pick another member under **Acts as**, for example for a script that a teammate runs. Every request is attributed to that member: change logs, notes signatures, the audit log and `GET /api/v2/me` all show them. The keys list shows each key as "Acts as *name* · created by *admin*". The key can never do more than that member can do in the app. Its scopes are a ceiling rather than a grant, and every request is checked against the member's **current** role in the org: - **Member is an org admin**: the key gets all of its scopes. - **Member is not an org admin** (for example, demoted after the key was created): `admin` is dropped, and everything only org admins can do or see in the app returns `403 insufficient_role`: payments and payout setup, a creator's payout account, removing or re-enabling creators, removing accounts or videos, stopping video tracking, manual scrapes, share pages, comment settings, and organization and project documents. Everything else that `read` and `write` allow still works. The [OpenAPI spec](https://www.trackagoat.com/api/v2/openapi.json) marks each of these endpoints "Requires an org admin". - **Member left or was removed from the org**: the key is rejected with `401 key_member_removed`. Removing a member also deactivates the keys that act as them. - **The key was made for someone else, and the admin who created it lost access**: the admin has seen the key, so it only works while they are still an org admin of this org. If they leave or are removed, the key is rejected with `401 key_creator_removed`; if they are changed to member, `401 key_creator_not_admin`; if their account is suspended, `401 key_creator_banned`. Removing, demoting or suspending that person also deactivates those keys. Keys a person created for themselves keep working after they are changed to member, limited to what a member can do. Deactivated keys stay in **Org settings → API keys** with the reason: revoked, the member it acts as left or was suspended, the admin who created it left, was changed to member or was suspended, or the organization was suspended. To keep a script running after its key's creator loses access, an org admin creates a new key for it. > **Tip:** > > To connect a teammate's coding agent, have them sign in to the [MCP server](https://www.trackagoat.com/docs/agents/mcp.md) with their own trackagoat account. Sign-in identifies them directly and nobody has to share a key. Use API keys for scripts, servers and agents that can't sign in. ### Creating keys over the API `POST /api/v2/organizations/{orgId}/api-keys` needs the `admin` scope, and a key can only create keys whose scopes it holds itself. Asking for a scope the calling key lacks returns `403 insufficient_scope` with `meta.required` listing the missing scopes. Pass `acts_as_user_id` to create a key that acts as another member; it must be a current member of the org (`400 acts_as_not_member` otherwise), and the `admin` scope needs that member to be an org admin. The response includes `acts_as` and `created_by`. A write request made with a read-only key is rejected with **403** and `meta.code = "insufficient_scope"`: ```json { "data": null, "error": "This action requires the 'write' scope. Granted: [read]", "meta": { "code": "insufficient_scope", "required": "write", "granted": ["read"] } } ``` This applies to every mutating endpoint, including all payout writes (`payout-methods`, `payout-structures`, `payout-payments`, `payout-accruals`, their `[id]` sub-routes, and annotation writes). ## Errors | Status | `meta.code` | Cause | |--------|-------------|-------| | `401` | `invalid_api_key` | Missing, malformed, invalid, revoked or expired key | | `401` | `key_member_removed` | The member the key acts as is no longer in the org | | `401` | `key_creator_removed` | The key was made for someone else, and the admin who created it is no longer in the org | | `401` | `key_creator_not_admin` | The key was made for someone else, and the admin who created it is no longer an org admin | | `401` | `key_creator_banned` | The key was made for someone else, and the account that created it is suspended | | `403` | `insufficient_scope` | Key is valid but lacks the scope required for this endpoint (or `admin` was dropped because its member is no longer an org admin) | | `403` | `insufficient_role` | Only org admins can do this in the app, and the key acts as a member who isn't one | | `403` | None | Key is valid but the org or user is banned | | `429` | `rate_limit_exceeded` | Per-key sliding-window (60 req/min) or daily quota exceeded | ```json { "data": null, "error": "Invalid or expired API key", "meta": { "code": "invalid_api_key" } } ``` ## Revoking a key Go to **Org settings → API keys** and revoke the key. Revocation is immediate: requests with that key get `401 invalid_api_key`. The key stays in the list, marked **Revoked**. Removing a member from the org (or a member leaving) deactivates every key that acts as them and every key they created in that org. Changing an admin to member deactivates the keys they created for someone else. For integrations, have an admin who will stay in the org create the key, acting as a member who will stay too. ## Limits - **Number of keys:** your plan sets `max_api_keys_per_org`. Delete unused keys if you reach it. - **Rate limit:** 60 requests a minute per key (sliding window). - **Daily quota:** `max_api_requests_per_day` per org, set by your plan. See [Plans and limits](https://www.trackagoat.com/docs/guides/limits.md) for current values and [Errors and limits](https://www.trackagoat.com/docs/api-reference/errors.md) for the `402` and `429` responses. - [API overview](https://www.trackagoat.com/docs/api-reference/overview.md): Base URL, response envelope, pagination and the OpenAPI spec. - [Errors and limits](https://www.trackagoat.com/docs/api-reference/errors.md): Status codes, rate limits and error response shapes. --- Source: https://www.trackagoat.com/docs/api-reference/authentication --- # Errors and limits > HTTP status codes, error response format, rate limits, and limit-reached responses. ## Error response format All errors use the same envelope shape: ```json { "data": null, "error": "Human-readable error message", "meta": { /* optional error detail */ } } ``` ## Status codes Every error response has a `meta.code`. When an endpoint has no more specific code, the code comes from the status: `400` `invalid_request`, `401` `unauthorized`, `402` `limit_reached`, `403` `forbidden`, `404` `not_found`, `405` `method_not_allowed`, `409` `conflict`, `410` `gone`, `422` `unprocessable`, `429` `rate_limit_exceeded`, `500` `internal_error`. The codes in the table below take priority. | Status | Meaning | |--------|---------| | `400` | Bad request: invalid query parameters or request body (`meta.code`: `invalid_query`, `invalid_body`, `invalid_params`) | | `401` | Unauthorized: missing, invalid, revoked or expired API key (`meta.code`: `invalid_api_key`), or a key that stopped working because of who it acts as or who created it (`key_member_removed`, `key_creator_removed`, `key_creator_not_admin`, `key_creator_banned`; see [Who a key acts as](https://www.trackagoat.com/docs/api-reference/authentication.md#who-a-key-acts-as)) | | `402` | Limit reached: org has hit a count-based plan limit (`meta.code`: `limit_reached`), or a date range exceeds tier history retention (`retention_exceeded`) | | `403` | Forbidden: key lacks the required scope (`meta.code`: `insufficient_scope`), the member the key acts as isn't an org admin and the action needs one (`insufficient_role`), a write to another member's personalization document (`not_your_document`), or the org/user is banned | | `404` | Not found: resource doesn't exist or isn't in your org. A path that matches no endpoint at all (any method) returns `404` JSON with `meta.code: route_not_found` and a pointer to `/api/v2/openapi.json`. | | `409` | Conflict: a duplicate or a state conflict. Adding a handle that's already tracked in the project returns `meta.code: already_tracked` (see below). An `Idempotency-Key` reused on a different endpoint returns `idempotency_conflict`; one whose first request is still running returns `idempotency_in_progress` with a `Retry-After` header. A notes replace whose `expected_readme_version` is out of date returns `notes_conflict` (see [Notes and documents](https://www.trackagoat.com/docs/api-reference/notes.md#errors)). | | `410` | Gone: removed API version. Every `/api/v1/*` path returns 410 (`meta.code`: `api_version_removed`). Use `/api/v2` instead. | | `422` | Unprocessable: an `Idempotency-Key` was reused with a different request body (`meta.code`: `idempotency_key_reused`). Use a new key for a new request. | | `429` | Rate limited: daily API quota or per-key sliding-window exceeded (`meta.code`: `rate_limit_exceeded`) | | `500` | Server error (`meta.code`: `internal_error`) | ## 402: Limit reached Returned when a write operation would exceed a count-based plan limit. The `meta` field contains details: ```json { "data": null, "error": "Limit reached: max_accounts_per_org", "meta": { "code": "limit_reached", "limitKey": "max_accounts_per_org", "current": 5, "max": 5 } } ``` See [Plans and limits](https://www.trackagoat.com/docs/guides/limits.md) for all limit keys. ## 409: Already tracked Returned by `POST /api/v2/creators`, `POST /api/v2/projects/{id}/creators` and `POST /api/v2/creators/{id}/accounts` when one of the handles is already tracked in the project. Nothing is created. `error` names the handle and the creator that tracks it, and `meta` carries the ids so you can use the existing creator instead: ```json { "data": null, "error": "@examplecreator (TikTok) is already tracked in this project as creator \"Sam Rivera\" (3f1c…).", "meta": { "code": "already_tracked", "platform": "tiktok", "handle": "examplecreator", "account_id": "9a7e…", "creator_id": "3f1c…" } } ``` If the same platform and handle appear twice in one request, you get `400` with `meta.code: duplicate_account` instead. ## Idempotency-Key errors Write endpoints that accept an `Idempotency-Key` header (see [Idempotency](https://www.trackagoat.com/docs/api-reference/overview.md#idempotency)) can return: | `meta.code` | Status | Meaning | What to do | |---|---|---|---| | `invalid_idempotency_key` | 400 | The key is longer than 255 characters | Use a UUID | | `idempotency_in_progress` | 409 | A request with this key is still running. `Retry-After` and `meta.retry_after_seconds` say how long to wait | Retry with the same key and body after the delay | | `idempotency_conflict` | 409 | The key was already used on a different endpoint | Use a new key | | `idempotency_key_reused` | 422 | The key was already used with a different request body | Use a new key for the new request | A `5xx` or `429` response is never replayed: retrying with the same key runs the request again. ## 404: Unknown endpoint A request to a path that no endpoint matches returns JSON, never the website's HTML page, so a typo'd or unsupported call can't look like a success: ```json { "data": null, "error": "No API endpoint matches DELETE /api/v2/goals/3f1c….", "meta": { "code": "route_not_found", "method": "DELETE", "path": "/api/v2/goals/3f1c…", "hint": "Check the path and method against the OpenAPI spec at /api/v2/openapi.json (docs: /docs/api-reference/overview).", "openapi": "/api/v2/openapi.json" } } ``` A known path called with a method it doesn't support returns `405`. ## 403: Insufficient scope Returned when a write request (`POST`/`PATCH`/`DELETE`) is made with a key that lacks the `write` scope, or an admin endpoint (org management, PIN reads, hard deletes) is called without the `admin` scope. A key's `admin` scope only counts while its owner is an org admin. ```json { "data": null, "error": "This action requires the 'write' scope. Granted: [read]", "meta": { "code": "insufficient_scope", "required": "write", "granted": ["read"] } } ``` See [Authentication](https://www.trackagoat.com/docs/api-reference/authentication.md#permission-scopes) for the scope model. ## 403: Insufficient role A key acts as one member and can never do more than they can in the app. Endpoints that only org admins can use in the app (payments and payout setup, removing creators, manual scrapes, share pages, comment settings, organization and project documents) return this when the member isn't an org admin. The OpenAPI spec marks each of them "Requires an org admin". ```json { "data": null, "error": "You need to be an organization admin to do this. Ask an org admin to update your role in Organization settings.", "meta": { "code": "insufficient_role", "required": "org_admin" } } ``` Use a key that acts as an org admin, or ask an org admin to do it. ## 429: Rate limited Two separate systems can produce 429s, and **both** use `meta.code: "rate_limit_exceeded"`. **Per-key sliding window**: more than 60 requests per minute from a single API key. The response carries a `Retry-After` header (seconds). Retry after a brief delay. ```json { "data": null, "error": "Rate limit exceeded", "meta": { "code": "rate_limit_exceeded", "bucket": "v1Api", "retryAfter": 7 } } ``` **Daily quota**: your org's daily `api_requests` allowance was exhausted. Resets at midnight in your org's time zone. Like the per-key limiter, this response carries a `Retry-After` header (seconds until the reset), so a client can back off uniformly on any 429 without special-casing which limiter fired. ```json { "data": null, "error": "Daily API request limit exceeded", "meta": { "code": "rate_limit_exceeded", "retryAfter": 33421 } } ``` The public [`GET /api/v2/changelog`](https://www.trackagoat.com/docs/api-reference/changelog.md) has neither: it is limited to 60 requests a minute per IP address (bucket `publicChangelog`) and never counts toward the daily quota. ## Handling errors > **Tip: Always check meta** > > The `meta` field on error responses contains structured detail: use `meta.code` for programmatic error handling rather than parsing the `error` string. ```javascript const res = await fetch('https://www.trackagoat.com/api/v2/creators', { headers: { Authorization: `Bearer ${apiKey}` }, }); const body = await res.json(); if (!res.ok) { if (res.status === 429) { const retryAfter = body.meta?.retryAfter ?? 60; // wait retryAfter seconds and retry } else { throw new Error(body.error ?? `HTTP ${res.status}`); } } const creators = body.data; ``` - [Authentication](https://www.trackagoat.com/docs/api-reference/authentication.md): API key setup and Bearer token usage. - [Limits](https://www.trackagoat.com/docs/guides/limits.md): Plan tiers, limit keys, and 402 responses. --- Source: https://www.trackagoat.com/docs/api-reference/errors --- # OpenAPI and schema > Machine-readable data model endpoint for AI agents and integrations. ## GET /api/v2/schema Returns a machine-readable description of the trackagoat data model. Designed for AI agents that need to understand entity types, field names, and relationships without reading source code. ### Request ```bash curl -H "Authorization: Bearer tga_" \ https://www.trackagoat.com/api/v2/schema ``` ### When to use this Call `/api/v2/schema` when you first connect to the API to get a full picture of available entities and their fields. The response is stable across minor API updates: field names and types won't change without a version bump. > **Tip:** > > AI agents should call `/api/v2/schema` at the start of each session to orient themselves before querying data. See the [Agent guide](https://www.trackagoat.com/docs/agents/guide.md) for more. ### Response The schema endpoint returns an object describing each entity type, its fields, and key relationships. The exact shape is controlled server-side and may expand over time. Since Trackagoat 2.0 the model is multi-platform: it now includes the **account** and **content group** entities alongside creators and videos. A creator owns many accounts, videos hang off accounts, and a content group links the same logical content across platforms. ```json { "data": { "entities": { "project": { "fields": {...}, "description": "..." }, "creator": { "fields": {...}, "description": "..." }, "account": { "fields": {...}, "description": "..." }, "video": { "fields": {...}, "description": "..." }, "content_group": { "fields": {...}, "description": "..." }, "campaign": { "fields": {...}, "description": "..." } }, "relationships": [...] }, "error": null, "meta": null } ``` - [Agent guide](https://www.trackagoat.com/docs/agents/guide.md): How AI agents should connect to and use the trackagoat API. - [API overview](https://www.trackagoat.com/docs/api-reference/overview.md): Base URL, response envelope, pagination, and versioning. --- Source: https://www.trackagoat.com/docs/api-reference/schema --- # Changelog API > Read trackagoat's changelog as JSON, without an API key. ## GET /api/v2/changelog Returns the entries on the [changelog](https://www.trackagoat.com/docs/platform/changelog.md), newest first. Use it to show what's new in your own tools, or to let an agent answer "what changed in trackagoat?". Over MCP the same data comes from the [`get_changelog` tool](https://www.trackagoat.com/docs/agents/mcp.md#tools). This endpoint is public: it needs no API key and doesn't count toward your org's daily API request limit. It is rate-limited to 60 requests a minute per IP address. Responses are cached, and new entries appear when we ship. ### Query parameters | Parameter | Type | Description | |-----------|------|-------------| | `since` | `YYYY-MM-DD` | Only entries dated on or after this day. | | `q` | string | Search the title and text, case-insensitive. Every word must appear: `q=instagram payouts` returns entries that mention both. | | `limit` | integer | Entries per page, 1 to 100. Default 20. | | `cursor` | string | `meta.nextCursor` from the previous page. Keep the same `since` and `q`. | ### Request ```bash curl "https://www.trackagoat.com/api/v2/changelog?since=2026-10-01&limit=2" ``` ### Response ```json { "data": [ { "date": "2026-10-10", "title": "Ask your agent what's new in trackagoat", "anchor": "2026-10-10-ask-your-agent-whats-new-in-trackagoat", "url": "https://www.trackagoat.com/docs/platform/changelog#2026-10-10-ask-your-agent-whats-new-in-trackagoat", "body_markdown": "The changelog is now available to AI agents and your own tools. ..." }, { "date": "2026-10-09", "title": "Clearer setup, invitation and shared creator pages", "anchor": "2026-10-09-clearer-setup-invitation-and-shared-creator-pages", "url": "https://www.trackagoat.com/docs/platform/changelog#2026-10-09-clearer-setup-invitation-and-shared-creator-pages", "body_markdown": "Account setup now shows which step you are on ..." } ], "error": null, "meta": { "total": 61, "hasMore": true, "nextCursor": "2026-10-09-clearer-setup-invitation-and-shared-creator-pages", "changelog_url": "https://www.trackagoat.com/docs/platform/changelog" } } ``` | Field | Description | |-------|-------------| | `date` | The day the change shipped. | | `title` | The entry's heading. | | `anchor` | The heading's id on the changelog page. | | `url` | Link to the entry in the docs. | | `body_markdown` | The entry's text as Markdown. Links are absolute. | | `meta.total` | Number of entries that match `since` and `q`. | | `meta.hasMore` / `meta.nextCursor` | Pass `nextCursor` as `cursor` to get the next page. It is `null` on the last page. | ### Errors | Status | `meta.code` | When | |--------|-------------|------| | 400 | `invalid_query` | `since` isn't a `YYYY-MM-DD` date, or `limit` is outside 1 to 100. | | 400 | `invalid_cursor` | The cursor doesn't match an entry for this `since` and `q`. Start again without `cursor`. | | 429 | `rate_limit_exceeded` | More than 60 requests in a minute from your IP. Wait for the `Retry-After` header. | --- Source: https://www.trackagoat.com/docs/api-reference/changelog --- # Notes and documents API > Read, search and write every Markdown document in an organization, and the personalization document of the member a key acts as. Every organization, project, campaign, creator, video, payout method, payout structure and payout payment has a Markdown document (`readme`), and so does every member. These endpoints read and write all of them in one way. Over MCP the same endpoints are the `list_notes`, `get_notes`, `update_notes`, `get_my_profile`, `update_my_readme` and `list_members` tools. See [Notes and documents](https://www.trackagoat.com/docs/agents/notes.md) for how people and agents use them. > **Warning:** > > Every document is visible to everyone in the organization and every agent connected to it, including each member's personalization document. Don't store anything private in one. `entity_type` is one of `organization`, `project`, `campaign`, `creator`, `video`, `payout_method`, `payout_structure`, `payout_payment` or `member`. For `member`, the id is a user id, and `me` means the member the key acts as. ## GET /api/v2/notes Lists documents that have text, newest first within each type, with a preview. | Parameter | Type | Description | |-----------|------|-------------| | `entity_type` | string | Only this kind of document. | | `project_id` | uuid | The project and the documents inside it. Org-wide documents (organization, members, payout methods) are left out. | | `q` | string | Case-insensitive text that must appear in the document. | | `limit` | integer | Items per type, 1 to 100. Default 20. | | `offset` | integer | Rows to skip. Needs `entity_type`. | Without `entity_type`, every type is searched and each returns up to `limit` items. `meta.totals` has the number of matches per type and `meta.has_more` names the types with more; page through one of them with `entity_type` and `offset`. ```bash curl -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/notes?q=launch" ``` ```json { "data": [ { "entity_type": "project", "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "name": "Spring Launch", "project_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "readme_preview": "## Goal 2M views by May 31. Creators post 3 times a week...", "readme_length": 1204, "readme_version": "9f2c4e1a7b3d5f60" } ], "error": null, "meta": { "totals": { "organization": 0, "project": 1, "campaign": 0 }, "has_more": [], "limit": 20, "offset": 0 } } ``` ## GET /api/v2/notes/\{entity_type\}/\{id\} Returns one document in full. | Field | Description | |-------|-------------| | `readme` | The Markdown text, or `null`. | | `readme_version` | A hash of the text. Pass it as `expected_readme_version` to replace the document. | | `can_edit` | Whether the member the key acts as may change it (scopes aside). | | `edit_rule` | Who may change it, in words. | | `app_path` | Where people edit it in the app. | Returns **404** `not_found` for anything outside the key's organization, including members of other organizations. ## POST /api/v2/notes/\{entity_type\}/\{id\} Appends to or replaces a document. Needs the `write` scope. Honours `Idempotency-Key`. ```json { "mode": "append", "content": "Views dropped 40% week over week." } ``` ```json { "mode": "replace", "content": "# Spring Launch\n\n...", "expected_readme_version": "9f2c4e1a7b3d5f60" } ``` - `append` adds `content` as a new section at the end, followed by `*Added by via API, YYYY-MM-DD*` (`via MCP` from the MCP server). If someone edits the document at the same moment, the section is added on top of their edit. - `replace` overwrites the whole document. If it changed since you read `expected_readme_version`, nothing is written and the response is **409** `notes_conflict` with `meta.current_readme_version`. An empty `content` clears it. Who can write follows the app: | Document | Who can change it | |----------|-------------------| | `organization`, `project`, `payout_method`, `payout_structure`, `payout_payment` | A key that acts as an org admin. Otherwise **403** `insufficient_role`. | | `campaign`, `creator`, `video` | Any key with `write`. | | `member` | Only the member's own key or sign-in. Otherwise **403** `not_your_document`. | `POST /api/v2/creators/{id}/notes` and `POST /api/v2/videos/{id}/notes` keep working and behave the same. `PATCH /api/v2/campaigns/{id}` and `PATCH /api/v2/organizations/{orgId}/projects/{projectId}` also take a `readme`, which replaces the document. Send `expected_readme_version` with it to get the same **409** `notes_conflict` instead of overwriting a newer edit. See [Campaigns](https://www.trackagoat.com/docs/api-reference/campaigns.md) and [Projects](https://www.trackagoat.com/docs/api-reference/projects.md). ## GET /api/v2/me Who the key acts as, and their personalization document. ```json { "data": { "id": "8d1c2f0e-1b2a-4c3d-9e8f-7a6b5c4d3e2f", "display_name": "Sam Lee", "organization": { "id": "1b9d6bcd-bbfd-4b2d-9b5d-ab8dfbbd4bed", "name": "Acme", "role": "admin" }, "connection": { "via": "api_key", "scopes": ["read", "write"] }, "readme": "## About me\nPartnerships lead...", "readme_version": "0c4f1d2e3a5b6c7d", "readme_length": 412, "readme_visibility": "Everyone in the organization, and their agents, can read this document." }, "error": null, "meta": null } ``` ## PATCH /api/v2/me Replaces the personalization document of the member the key acts as. Needs the `write` scope. ```json { "readme": "## About me\n...", "expected_readme_version": "0c4f1d2e3a5b6c7d" } ``` With `expected_readme_version`, an edit made in the app since your read returns **409** `notes_conflict` and nothing is written. Without it, the write replaces whatever is there. ## GET /api/v2/members The organization's members: `user_id`, `display_name`, `role`, `joined_at`, `is_you`, and whether each has a personalization document (`has_readme`, `readme_length`, `readme_preview`). Read one with `GET /api/v2/notes/member/{user_id}`. ## Errors | Status | `meta.code` | When | |--------|-------------|------| | 400 | `invalid_entity_type` | `entity_type` isn't one of the types above. | | 400 | `notes_too_long` | The document would be longer than 50,000 characters. | | 403 | `insufficient_scope` | A write without the `write` scope. | | 403 | `insufficient_role` | An org-admin document and the key acts as a member who isn't an org admin. | | 403 | `not_your_document` | Writing another member's personalization document. | | 404 | `not_found` | Not in the key's organization. | | 409 | `notes_conflict` | The document changed since `expected_readme_version`. | | 409 | `notes_busy` | The record kept changing while saving. Retry. | --- Source: https://www.trackagoat.com/docs/api-reference/notes --- # Projects > List the projects in your organization and read a project's program health. ## GET /api/v2/projects List all projects in your organization. ### Request ```bash curl -H "Authorization: Bearer tga_" \ https://www.trackagoat.com/api/v2/projects ``` ### Query parameters | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `limit` | number | 50 | Items per page (max 100) | | `cursor` | string | None | Pagination cursor from `meta.nextCursor` | ### Response ```json { "data": [ { "id": "uuid", "name": "Q1 Campaign", "readme": "Spring creator program: focus on engagement rate", "org_id": "uuid", "created_at": "2026-01-01T00:00:00Z", "updated_at": "2026-01-10T00:00:00Z" } ], "error": null, "meta": { "hasMore": false, "nextCursor": null } } ``` ### Response fields | Field | Type | Description | |-------|------|-------------| | `id` | uuid | Project ID | | `name` | string | Project name | | `readme` | string \| null | The project README: team notes and agent context (Markdown) | | `org_id` | uuid | Owning organization ID | | `created_at` | ISO 8601 | Creation timestamp | | `updated_at` | ISO 8601 | Last update timestamp | --- ## PATCH /api/v2/organizations/{orgId}/projects/{projectId} Rename, archive or un-archive a project, or replace its README. Requires the `admin` scope, which is only effective while the key's owner is a current org admin. | Field | Type | Description | |-------|------|-------------| | `name` | string | Project name (max 200 chars). | | `is_archived` | boolean | Archive or un-archive. Un-archiving needs a free project slot (`402 limit_reached` otherwise). | | `readme` | string \| null | The project README (Markdown). Replaces the whole document; `null` clears it. | | `expected_readme_version` | string | Optional, only with `readme`: the `readme_version` from your last read (`GET /api/v2/notes/project/{id}` or a previous `PATCH` response). If the README changed since, nothing is written and the response is **409** `notes_conflict` with `meta.current_readme_version`. Without it, `readme` overwrites whatever is there. | The response is the updated project plus its `readme_version`. To add to the README without replacing it, use [`POST /api/v2/notes/project/{id}`](https://www.trackagoat.com/docs/api-reference/notes.md) with `mode: "append"`. --- ## GET /api/v2/projects/{projectId}/program-health The [Program Health](https://www.trackagoat.com/docs/guides/program-health.md) dashboard as one JSON document, built for agents and scripts: a health verdict and the signals behind it, KPIs, creators who stopped posting, goal compliance, alerts and top / under-performers. The numbers come from the same computation as the dashboard. Requires the `read` scope. Also available as the MCP tool [`get_program_health`](https://www.trackagoat.com/docs/agents/mcp.md). ```bash curl -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/projects//program-health?range=7d" ``` ### Query parameters | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `campaign_id` | uuid | none | Only creators and videos in this campaign (sub-campaigns included). Must belong to the project. | | `range` | `7d` \| `14d` \| `30d` \| `90d` | `30d` | Window for the period KPIs, ending today in your org's time zone, compared with the equal-length window before it. The dashboard uses `30d`. | ### Response ```json { "data": { "project_id": "uuid", "campaign": null, "range": "7d", "window": { "from": "2026-10-02", "to": "2026-10-08", "prior_from": "2026-09-25", "prior_to": "2026-10-01", "days": 7, "tz": "America/New_York" }, "health": { "status": "at-risk", "signals": [ { "signal": "views", "level": "bad", "value": -32, "detail": "Views on posts from the current window vs the prior window: -32%." }, { "signal": "scrape_freshness", "level": "ok", "value": 96, "detail": "48 of 50 active creators scraped in the last 28h (96%)." } ] }, "kpis": { "views_on_window_posts": { "current": 412000, "prior": 605000, "pct_change": -32, "note": "…" }, "posts_in_window": { "current": 61, "prior": 70, "pct_change": -13 }, "posts_this_week": { "this_week": 22, "last_week": 25, "pct_change": -12, "note": "…" }, "active_creators": { "current": 50, "a_week_ago": 48, "added_7d": 2, "disabled_accounts_7d": 0 }, "scrape_freshness": { "fresh": 48, "total": 50, "pct_fresh": 96 }, "engagement_rate": { "latest_full_week": 6.1, "prior_full_week": 6.4, "pct_change": -5 } }, "stale_creators": [{ "creator_id": "uuid", "handle": "goatcooks", "display_name": "Goat Cooks", "days_since_post": 19, "last_posted_at": "2026-09-19T15:02:00Z" }], "goal_compliance": { "on_track": 31, "at_risk": 6, "missed": 2, "at_risk_goals": [] }, "alerts": { "roster_churn": { "is_alert": false, "added_7d": 2, "disabled_7d": 0 }, "engagement_rate_drop": { "is_alert": false, "pct_change": -5 }, "sudden_view_drops": [], "tracking_gaps": { "stale_scrapes": [], "stale_discovery": [], "missing_videos": [] }, "private_accounts": [], "disabled_creators": [], "over_posting": [] }, "top_performers": [{ "creator_id": "uuid", "handle": "goatcooks", "display_name": "Goat Cooks", "views": 98000, "posts": 4, "engagement_rate": 7.2 }], "underperformers": [], "active_campaigns": [{ "campaign_id": "uuid", "name": "Fall push", "views": 230000, "videos": 18, "creators": 9 }] }, "error": null, "meta": { "computed_at": "2026-10-08T16:20:00Z" } } ``` ### Response fields | Field | Description | |-------|-------------| | `health.status` | `healthy`, `at-risk` or `degraded`: the same verdict as the dashboard's pulse. Each of four `signals` (active creators, views, posts week over week, scrape freshness) is `ok`, `warn` or `bad`. Two `bad` signals make the program `degraded`; one `bad` or two `warn` make it `at-risk`. | | `kpis.views_on_window_posts` | Current lifetime views of the videos posted in the window vs those posted in the prior window. Recent posts are still gaining views, so a short `range` understates the current window. | | `kpis.posts_this_week` | Posts since Monday (org time zone) up to the latest discovery run, vs last week up to the same point. Independent of `range`. | | `stale_creators` | Creators whose last post is 14 or more days old, most stale first (up to 25). | | `goal_compliance` | Goals on track (or completed), at risk (or behind pace) and missed in their current period, with up to 8 at-risk goals. | | `alerts.sudden_view_drops` | Creators whose last 3 posts average well below their 30-day median (`drop_ratio`). | | `alerts.tracking_gaps` | `stale_scrapes`: no profile scrape in 28h. `stale_discovery`: profile fresh but new-video discovery hasn't run in 48h. `missing_videos`: creators with videos on the platform that aren't tracked. | | `alerts.private_accounts` | Active accounts that went private. They produce no posts while private. | | `alerts.disabled_creators` | Creators whose primary account was disabled after repeated scrape failures (`disabled_reason`). | | `alerts.over_posting` | Days a creator posted so often in the window that some posts were likely suppressed. `null` if the check couldn't run (not "none"). | | `alerts.comment_flags` | Comments posted in the last 7 days that ask whether a post is an ad or use words like scam or unsafe (`count`, `more`, newest 3 `comments`). `null` on plans without comment collection. | | `top_performers` / `underperformers` | Most views on posts made in the window (top 10); bottom-quartile engagement rate with under one post a week. | `handle` is the creator's primary-account handle (TikTok first, then the oldest account). Lists are capped (10 to 25 entries) to keep the response small. | HTTP | Cause | |------|-------| | 400 | Invalid `range` or `campaign_id`. | | 403 | Key lacks the `read` scope. | | 404 | Project not in your organization, or `campaign_id` not in the project. | ## GET /api/v2/projects/{projectId}/comments Comments collected on the project's tracked videos ([Comment Insights](https://www.trackagoat.com/docs/guides/comment-insights.md)), most liked first by default. Comments are placed in the period by when they were posted. Requires the `read` scope and a paid plan. Also available as the MCP tool [`list_comments`](https://www.trackagoat.com/docs/agents/mcp.md). ```bash curl -H "Authorization: Bearer $TRACKAGOAT_API_KEY" \ "https://www.trackagoat.com/api/v2/projects//comments?signal=flagged&limit=20" ``` ### Query parameters | Parameter | Description | |-----------|-------------| | `from`, `to` | Days (`YYYY-MM-DD`, organization time zone). Default: the last 30 days including today. | | `campaign_id` | Only videos in this campaign, sub-campaigns included. | | `creator_id` | Only this creator's videos. | | `platform` | `tiktok` or `instagram`. | | `signal` | `app_question`, `signup_intent`, `ad_disclosure`, `risk`, `pay_talk`, `brand_mention`, `competitor`, `referral_spam`, or `flagged` (`ad_disclosure` or `risk`). | | `sentiment` | `positive`, `neutral` or `negative`. | | `q` | Case-insensitive text search. | | `sort` | `top` (most liked, default), `newest` or `oldest`. | | `limit` | 1 to 100, default 50. | | `cursor` | `meta.nextCursor` from the previous page. | ### Response `data` is an array of comments: `id`, `text`, `like_count`, `author_unique_id`, `author_nickname`, `created_at_tiktok` (when it was posted, despite the name, on every platform), `scraped_at`, `sentiment`, `sentiment_score`, `sentiment_model`, `signals`, `video_id`, `video_url`, `platform`, `caption`, `video_posted_at`, `creator_id`, `creator_handle`. `meta.hasMore` and `meta.nextCursor` page through the rest. ## GET /api/v2/projects/{projectId}/comment-signals The numbers behind the Comment Insights page for a period: comments collected and what share of the platform's comments that is (`summary.collected_comment_total` of `summary.platform_comment_total`, over videos posted in the period), sentiment counts, signal counts, `creators` (comments, `comments_per_1k_views` from the platform's full comment count, `positive_share` once a creator has 20 scored comments, signal counts, `top_comment`) and `timeseries` (scored and positive comments per `bucket`). Takes `from`, `to`, `campaign_id`, `platform` and `bucket` (`auto`, `day`, `week`, `month`). Requires the `read` scope and a paid plan. Also available as the MCP tool [`get_comment_signals_summary`](https://www.trackagoat.com/docs/agents/mcp.md). `creators` comes most comments first. To rank it by another column, pass `creator_sort`: `comments_captured`, `comments_per_1k_views`, `positive_share`, `app_question_count`, `signup_intent_count`, `flagged_count`, `brand_mention_count` or `competitor_count` (highest first; creators without a value, such as a `positive_share` under 20 scored comments, last). `creator_limit` (1 to 500) keeps the top N; `meta.creator_total` is the full count. ```bash # Which five creators get the most questions about the app? curl -H "Authorization: Bearer $TRACKAGOAT_API_KEY" \ "https://www.trackagoat.com/api/v2/projects//comment-signals?creator_sort=app_question_count&creator_limit=5" ``` ## GET /api/v2/projects/{projectId}/comment-flags The [weekly flag digest](https://www.trackagoat.com/docs/guides/comment-insights.md#weekly-flag-digest) on demand, for the last `days` (organization time zone, including today; default 7, up to 30). Requires the `read` scope and a paid plan. Also available as the MCP tool [`get_comment_flags`](https://www.trackagoat.com/docs/agents/mcp.md). | Parameter | Description | |-----------|-------------| | `days` | Window length, 1 to 30. Default 7. | | `campaign_id` | Only videos in this campaign, sub-campaigns included. | | `platform` | `tiktok` or `instagram`. | | `limit` | Flagged comments to return, most liked first. 1 to 50, default 10. | `data` has `ad_disclosure_count` (comments asking whether a post is an ad), `risk_count` (words like scam or unsafe) and `flagged_count` (either) for comments posted in the window; `comments`, the most-liked flagged comments in the same shape as `/comments` (`more_comments` is true when there are more: page through them with `/comments?signal=flagged`); and `positive_share_drops`, creators whose share of positive comments fell 20 points or more against the 30 days before the window (`baseline_range`), with at least 20 scored comments in both (`recent_positive_share`, `prior_positive_share`, `drop_points`). The Monday notification counts comments collected in the past week; this endpoint places comments by when they were posted, like the rest of Comment Insights. Errors for `/comments`, `/comment-signals` and `/comment-flags`: | HTTP | Cause | |------|-------| | 400 | Invalid parameter or cursor. | | 403 | Key lacks the `read` scope, or `plan_feature_unavailable` (`meta.feature: "comment_insights"`): the organization's plan doesn't collect comments. | | 404 | Project not in your organization, or `campaign_id` not in the project. | ## GET /api/v2/comment-settings The API key's organization's Comment Insights settings (**Org settings → Comments**). Works on every plan, so it's also how to check whether the plan collects comments. Requires the `read` scope and an org admin (`403 insufficient_role` otherwise), like the settings page. Also available as the MCP tool [`get_comment_settings`](https://www.trackagoat.com/docs/agents/mcp.md). ```json { "data": { "org_id": "…", "org_name": "Acme", "min_comment_count": 5, "brand_terms": [], "effective_brand_terms": ["Acme"], "competitor_terms": ["Brand A", "branda.com"], "plan": { "comment_insights_enabled": true, "max_comment_videos_per_day": 40 }, "updated_at": "2026-10-10T15:00:00Z" }, "error": null, "meta": null } ``` `min_comment_count`: a video's comments are collected once it is 3 days old and the platform shows at least this many. `effective_brand_terms` is what the `brand_mention` signal matches: `brand_terms`, or the organization's name when none are set. `plan.max_comment_videos_per_day` is `0` on plans without comment collection. ## PATCH /api/v2/comment-settings Change the settings. Requires the `write` scope and an org admin (`403 insufficient_role` otherwise), like the settings page. Also available as the MCP tool [`update_comment_settings`](https://www.trackagoat.com/docs/agents/mcp.md). ```bash curl -X PATCH -H "Authorization: Bearer $TRACKAGOAT_API_KEY" -H "Content-Type: application/json" \ -d '{"competitor_terms": ["Brand A", "branda.com"]}' \ https://www.trackagoat.com/api/v2/comment-settings ``` | Field | Description | |-------|-------------| | `min_comment_count` | 1 to 100,000. Lower means more videos are collected, and more cost. | | `brand_terms` | Up to 20 terms. Replaces the list; `[]` falls back to the organization's name. | | `competitor_terms` | Up to 50 terms. Replaces the list; `[]` clears it. | Terms are trimmed, de-duplicated ignoring case, and 60 characters or fewer; they match whole words and phrases, case-insensitively. Mentions are matched when comments are read, so new terms apply to comments already collected. Unknown fields are rejected with `400`. Returns the settings in the `GET` shape. --- Source: https://www.trackagoat.com/docs/api-reference/projects --- # Creators > List, add, update, and remove creators. Each creator owns one or more platform accounts. Since Trackagoat 2.0, a **creator** is a person or brand that owns one or more platform **accounts** (`tiktok` or `instagram`; the enum also accepts `youtube`, which is reserved and not supported). A creator has no handle of its own. It embeds an `accounts[]` array and exposes a derived top-level `handle` (the primary account's handle: TikTok first, else the earliest-created active account). Handle and tracking configuration live on the account. See [Accounts](https://www.trackagoat.com/docs/api-reference/accounts.md). ## GET /api/v2/creators List creators being tracked in your organization, with their accounts embedded. ### Request ```bash curl -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/creators?project_id=" ``` ### Query parameters | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `project_id` | uuid | None | Filter to a specific project | | `platform` | `tiktok` \| `instagram` \| `youtube` | None | Only creators with an account on this platform | | `search` | string | None | Partial match on creator display name **or** any account handle | | `limit` | number | 50 | Items per page (max 100) | | `cursor` | string | None | Pagination cursor | | `include_pin` | boolean | `false` | Include entrance PINs in `share_link`. Requires the `admin` scope; `403` otherwise | ### Response fields | Field | Type | Description | |-------|------|-------------| | `id` | uuid | Creator ID | | `project_id` | uuid | Project this creator belongs to | | `org_id` | uuid | Owning organization ID | | `display_name` | string \| null | Creator display name | | `handle` | string \| null | Primary account's handle (derived, TikTok first, else earliest) | | `avatar_url` | string \| null | Profile image URL | | `readme` | string \| null | Markdown notes (rendered in the Notes tab) | | `metadata` | object | Agent-writable structured data (`{}` by default) | | `is_active` | boolean | Whether the creator is actively tracked | | `follower_count` | number \| null | Followers summed across the creator's accounts | | `following_count` | number \| null | Following count summed across accounts | | `total_likes_count` | number \| null | Cumulative likes summed across accounts | | `video_count` | number \| null | Posts on the platforms: the profile post count each platform reports, summed across accounts. Not the number of tracked posts | | `tracked_post_count` | number | Posts trackagoat counts in metrics: accepted, tracking on, on an active account. See [How metrics are calculated](https://www.trackagoat.com/docs/guides/metrics.md) | | `pending_review_count` | number | Posts awaiting review in the inbox; not counted until accepted | | `accounts` | array | The creator's platform accounts. See [Accounts](https://www.trackagoat.com/docs/api-reference/accounts.md#account-fields) | | `share_link` | object \| null | The creator's PIN-gated public page, or `null` if they don't have one. See [Share Links](https://www.trackagoat.com/docs/api-reference/share-links.md) | | `created_at` | ISO 8601 | When creator was added | | `updated_at` | ISO 8601 | Last update | Creator-level counts are the **sum across the creator's accounts**. Add an Instagram account and the creator's follower total grows to include it. ### Share links `share_link` carries the creator's shareable page. The URL is always `{site}/share/creator/{creator_id}`, so you can construct it from any creator id without a lookup. The entrance `pin` is **omitted** unless the request passes `include_pin=true` **and** the key holds the `admin` scope; anything else returns `403 insufficient_scope`. See [Share Links](https://www.trackagoat.com/docs/api-reference/share-links.md) for the full rules and the management endpoints. --- ## POST /api/v2/creators Create a creator with **one or more accounts** (at least one is required). Requires the `write` scope. Dispatches a profile + discovery scrape automatically: the scraper fans out to every account. ```bash curl -X POST \ -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "project_id": "", "display_name": "Charli", "accounts": [ {"platform": "tiktok", "handle": "charlidamelio"}, {"platform": "instagram", "handle": "charlidamelio", "tracking_mode": "selective"} ] }' \ "https://www.trackagoat.com/api/v2/creators" ``` ### Body | Field | Type | Default | Description | |-------|------|---------|-------------| | `project_id` | uuid | required | Project to create the creator in. | | `display_name` | string | first account's handle | Creator display name (max 100 chars). | | `metadata` | object | `{}` | Arbitrary JSON for your own identifiers, so an external system can map its records back to this creator. Also settable later via `PATCH`, which replaces the whole object rather than merging. | | `accounts` | array | required | 1 to 20 account objects (see below). | Each object in `accounts[]`: | Field | Type | Default | Description | |-------|------|---------|-------------| | `platform` | `tiktok` \| `instagram` \| `youtube` | required | Which platform this account is on. | | `handle` | string | required | Platform `@username`, or a profile URL on `platform` (max 200 chars). Normalized: leading `@` stripped, lowercased. See [accepted profile links](https://www.trackagoat.com/docs/guides/accounts.md#accepted-profile-links). | | `tracking_mode` | `auto` \| `selective` \| `hashtag` | `auto` | `auto`: track all videos. `selective`: new videos land in the inbox for review. `hashtag`: auto-track videos whose caption contains a tracked hashtag. | | `tracking_hashtags` | `string[]` | `[]` | Required (non-empty) when `tracking_mode` is `hashtag`. Normalized server-side and by a database trigger: NFC + lowercase, leading `#` stripped, and characters that can't appear in a hashtag removed (`#Black-Friday` becomes `blackfriday`). Whitespace and commas separate tags. Max 50 tags, 100 chars each. | | `hashtag_inbox_non_matching` | `boolean` | `true` | Hashtag mode: inbox non-matching videos (`true`) or exclude them (`false`). | | `tracking_start_date` | YYYY-MM-DD | today (org timezone) | First day of the tracked window. Defaults to today in the org's analytics timezone. | | `tracking_end_date` | YYYY-MM-DD | None | Optional last day, inclusive (must be ≥ start date; equal to the start date tracks a single day). | ### Responses | HTTP | Cause | |------|-------| | 201 | Creator created; body is the full creator with embedded `accounts[]`. | | 400 | `invalid_account` (bad handle or URL; see `meta.reason`), `duplicate_account` (the same handle twice in `accounts`). Short links (`vm.tiktok.com`, `vt.tiktok.com`, `tiktok.com/t/`) are followed, at most 10 per request (`reason: too_many_short_links`). | | 402 | `max_accounts_per_org` limit reached (accounts, not creators, are counted). | | 403 | Key lacks the `write` scope. | | 404 | Project not found (or not in your org). | | 409 | One of the handles is already tracked in this project. | The creator and all of its accounts are created together: on a 402 or 409 nothing is created, so you can fix the request and retry without leaving an empty creator behind. --- ## POST /api/v2/projects/{projectId}/creators Add one creator or a batch (up to 50) to a specific project. Requires the `write` scope. Each creator carries an `accounts[]` array (min 1). ### Single creator ```bash curl -X POST \ -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{"accounts": [{"platform": "tiktok", "handle": "charlidamelio"}]}' \ "https://www.trackagoat.com/api/v2/projects//creators" ``` ### Batch (up to 50) ```bash curl -X POST \ -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "creators": [ {"accounts": [{"platform": "tiktok", "handle": "charlidamelio"}]}, {"display_name": "Khaby", "accounts": [{"platform": "tiktok", "handle": "khaby.lame", "tracking_mode": "selective"}]} ] }' \ "https://www.trackagoat.com/api/v2/projects//creators" ``` Each creator object accepts the same `display_name` (optional), `metadata` (optional) and `accounts[]` (1 to 20) fields as `POST /api/v2/creators`. Returns 402 if the account limit is reached, 409 if a handle is already tracked in the project, and 404 if the project is not found. Each creator in a batch is created together with its accounts, so a failing creator never leaves an empty creator behind. Creators earlier in the batch than the failing one are kept. --- ## GET /api/v2/creators/{id} Get a single creator with its accounts embedded. ```bash curl -H "Authorization: Bearer tga_" \ https://www.trackagoat.com/api/v2/creators/ ``` Returns the same shape as a list item, plus `readme_version`: a version token for the creator's notes, needed to [replace the notes safely](#post-apiv2creatorsidnotes). 404 if the creator doesn't exist or isn't in your org. --- ## PATCH /api/v2/creators/{id} Update **creator-level** fields only. At least one field is required. Requires the `write` scope. > **Note:** > > Handles and tracking configuration live on accounts, not creators. To rename a handle or change tracking settings, use [`PATCH /api/v2/accounts/{id}`](https://www.trackagoat.com/docs/api-reference/accounts.md#patch-apiv2accountsid). ```bash curl -X PATCH \ -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -d '{"readme": "Focus on cooking content. Posts Tues/Thurs."}' \ https://www.trackagoat.com/api/v2/creators/ ``` ### Body | Field | Type | Description | |-------|------|-------------| | `display_name` | string | Override the display name (max 100 chars). | | `readme` | string \| null | Markdown notes visible in the UI. Pass `null` to clear. | | `is_active` | boolean | `false` removes the creator, exactly like `DELETE` below. `true` has no effect: a removed creator can't be restored through the API. | | `metadata` | object | Arbitrary JSON. **Replaces** the whole object, so read the current value first if you need to preserve existing keys. | Returns 404 if the creator isn't found. > **Tip:** > > `PATCH` with `readme` overwrites the notes unconditionally. When an agent or script writes notes that people also edit, use [`POST /api/v2/creators/{id}/notes`](#post-apiv2creatorsidnotes) instead. --- ## POST /api/v2/creators/{id}/notes Add to or replace a creator's notes (`readme`) without clobbering human edits. Requires the `write` scope. Accepts an `Idempotency-Key` header, so a retried append isn't added twice. **Append** (recommended) adds `content` as a new section at the end, signed with the API key's name and your org's local date: ```bash curl -X POST \ -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{"mode": "append", "content": "Views dropped 40% week over week."}' \ https://www.trackagoat.com/api/v2/creators//notes ``` The notes then end with: ```markdown Views dropped 40% week over week. *Added by Weekly review bot via API, 2026-10-08* ``` **Replace** overwrites the whole notes and requires the `readme_version` from your last `GET /api/v2/creators/{id}`. If the notes changed since that read, nothing is written and you get **409** `notes_conflict`: ```bash -d '{"mode": "replace", "content": "Rewritten notes", "expected_readme_version": "9f2c4e1a7b3d5f60"}' ``` ### Body | Field | Type | Description | |-------|------|-------------| | `mode` | `append` \| `replace` | Required. | | `content` | string | Markdown. For `append`, the new section (1 to 20,000 chars). For `replace`, the complete new notes (`""` clears them). Notes are capped at 50,000 characters. | | `expected_readme_version` | string | Required for `replace`: the `readme_version` you read. | ### Response ```json { "data": { "id": "uuid", "entity_type": "creator", "mode": "append", "changed": true, "readme": "…", "readme_version": "41d0c3b2a9e8f716", "previous_readme_version": "9f2c4e1a7b3d5f60" }, "error": null, "meta": null } ``` | HTTP | Cause | |------|-------| | 400 | Invalid body, or `notes_too_long`. | | 403 | Key lacks the `write` scope. | | 404 | Creator not found in your organization. | | 409 | `notes_conflict`: the notes changed since your read (`meta.current_readme_version` is the new version; re-read, merge and retry). `notes_busy`: the creator kept changing during the write; retry. | `readme_version` is a hash of the notes text, so it only changes when the notes do; scheduled scrapes don't cause conflicts. Each write is a compare-and-swap: if someone edits the notes while an append is being saved, the append is applied on top of their edit. Writes appear in the creator's change log as **Internal notes updated**. --- ## DELETE /api/v2/creators/{id} Soft-delete a creator (sets `is_active = false`). The creator's accounts are removed with it, so they stop being scraped and no longer count toward `max_accounts_per_org`. The creator's history and data are preserved, and you can add the same handles again later. Requires the `write` scope. ```bash curl -X DELETE \ -H "Authorization: Bearer tga_" \ https://www.trackagoat.com/api/v2/creators/ ``` Returns `{ "deleted": true }`. --- ## POST /api/v2/creators/{id}/scrape Trigger a manual scrape for a creator. By default the creator's primary account is scraped (its TikTok account if it has one, otherwise its earliest-added active account); pass `?account_id=` to scrape a specific account. Returns 400 if the creator has no active account. Consumes from the org's daily manual-scrape quota. Requires the `write` scope. ```bash # Scrape the creator's primary account curl -X POST \ -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -d '{"job_type": "discover_videos"}' \ https://www.trackagoat.com/api/v2/creators//scrape # Scrape a single account curl -X POST \ -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/creators//scrape?account_id=" ``` | Field | Where | Values | Description | |-------|-------|--------|-------------| | `account_id` | query | uuid | Restrict the scrape to one account. | | `job_type` | body | `creator_profiles` \| `video_stats` \| `discover_videos` | Which scrape job to dispatch. | The response echoes `{ job_type, creator_id, account_id, job_id }` (`account_id` is `null` when scraping all accounts). | HTTP | Cause | |------|-------| | 200 | Scrape dispatched. | | 400 | Creator is inactive. | | 429 | Daily scrape limit exceeded. | --- ## POST /api/v2/projects/{projectId}/creators/bulk-reenable Re-enable creators that were automatically disabled. Requires the `write` scope. Subject to the 24-hour cooldown per creator. ```bash curl -X POST \ -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -d '{"creator_ids": ["", ""]}' \ "https://www.trackagoat.com/api/v2/projects//creators/bulk-reenable" ``` Returns `{ ok_count, skipped_count, failed_count }`. --- ## POST /api/v2/projects/{projectId}/creators/bulk-remove Soft-delete multiple creators in one call (max 100). Each creator is removed exactly as with `DELETE /api/v2/creators/{id}`, including its accounts. Requires the `write` scope. ```bash curl -X POST \ -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -d '{"creator_ids": ["", ""]}' \ "https://www.trackagoat.com/api/v2/projects//creators/bulk-remove" ``` Returns `{ "removed_count": N }`, the number of creators actually removed. IDs that are unknown, already removed, or in another project are skipped and not counted. --- ## Analytics (time-series) Creator time-series analytics live on the unified endpoint. Add `&platform=` to scope a creator's series to a single platform. ```bash # Views for a creator: daily new, last 30 days curl -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/analytics?entity=creator&entity_id=&metric=views&mode=new&granularity=day" # Engagement rate (derived) curl -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/analytics?entity=creator&entity_id=&metric=engagement_rate_by_views&mode=rate" # Multi-entity overlay: compare up to 5 creators side-by-side curl -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/analytics?entity=creator&entity_ids=,&metric=views&granularity=week" ``` See the [Analytics API reference](https://www.trackagoat.com/docs/api-reference/analytics.md) for the full parameter and response documentation, and [Accounts](https://www.trackagoat.com/docs/api-reference/accounts.md#get-apiv2accountsidstats) for per-account follower history. --- Source: https://www.trackagoat.com/docs/api-reference/creators --- # Accounts > Platform accounts owned by a creator. List, add, update, remove, scrape, and read follower history. An **account** is a single platform presence: a TikTok or Instagram handle: owned by a [creator](https://www.trackagoat.com/docs/api-reference/creators.md). Tracking configuration (the handle, `tracking_mode`, and the tracked date window) lives on the account, and **videos hang off accounts**. Plan limits count accounts, since accounts are what actually get scraped. ## Account fields | Field | Type | Description | |-------|------|-------------| | `id` | uuid | Account ID | | `creator_id` | uuid | Creator that owns this account | | `project_id` | uuid | Project this account belongs to | | `org_id` | uuid | Owning organization ID | | `platform` | `tiktok` \| `instagram` \| `youtube` | Which platform this account is on | | `handle` | string | Platform `@username` | | `platform_user_id` | string \| null | The platform's internal user ID | | `display_name` | string \| null | Display name from the platform | | `avatar_url` | string \| null | Profile image URL | | `bio_link` | string \| null | Link in the account's bio | | `follower_count` | number | Current follower count (latest snapshot) | | `following_count` | number | Current following count | | `total_likes_count` | number | Cumulative total likes | | `video_count` | number | Current total video count | | `tracking_mode` | `auto` \| `selective` \| `hashtag` | `auto`: track all videos. `selective`: new videos land in the inbox. `hashtag`: track videos whose caption contains a tracked hashtag. | | `tracking_hashtags` | `string[]` | Hashtag-mode only. Normalized server-side and by a database trigger: NFC + lowercase, leading `#` stripped, and characters that can't appear in a hashtag removed (`#Black-Friday` becomes `blackfriday`). Whitespace and commas separate tags. Max 50 tags, 100 chars each. | | `hashtag_inbox_non_matching` | `boolean` | Hashtag-mode only. `true` (default) sends non-matching videos to the inbox; `false` excludes them. | | `tracking_start_date` | YYYY-MM-DD \| null | Start of the tracked window | | `tracking_end_date` | YYYY-MM-DD \| null | Last day of the tracked window, inclusive (`null` = no end). The window spans each date in both UTC and the org's analytics timezone, whichever is wider. | | `is_active` | boolean | Whether the account is actively tracked | | `last_scraped_at` | ISO 8601 \| null | When the account was last scraped | | `created_at` | ISO 8601 | When the account was added | | `updated_at` | ISO 8601 | Last update | --- ## GET /api/v2/accounts List accounts across your organization. ```bash curl -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/accounts?project_id=&platform=instagram" ``` ### Query parameters | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `project_id` | uuid | None | Filter to a specific project | | `creator_id` | uuid | None | Filter to a specific creator | | `platform` | `tiktok` \| `instagram` \| `youtube` | None | Filter to one platform | | `search` | string | None | Partial match on handle | | `limit` | number | 50 | Items per page (max 100) | | `cursor` | string | None | Pagination cursor | Returns a paginated array of [account objects](#account-fields). --- ## GET /api/v2/creators/{id}/accounts List all accounts belonging to a single creator (ordered oldest first). ```bash curl -H "Authorization: Bearer tga_" \ https://www.trackagoat.com/api/v2/creators//accounts ``` Returns an array of [account objects](#account-fields). 404 if the creator doesn't exist or isn't in your org. --- ## POST /api/v2/creators/{id}/accounts Add a platform account to an existing creator and dispatch a scrape. Requires the `write` scope. Enforces the per-org account limit. ```bash curl -X POST \ -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{"platform": "instagram", "handle": "charli"}' \ https://www.trackagoat.com/api/v2/creators//accounts ``` ### Body | Field | Type | Default | Description | |-------|------|---------|-------------| | `platform` | `tiktok` \| `instagram` \| `youtube` | required | Which platform this account is on. | | `handle` | string | required | Platform `@username`, or a profile URL on `platform` (max 200 chars). Normalized: leading `@` stripped, lowercased. See [accepted profile links](https://www.trackagoat.com/docs/guides/accounts.md#accepted-profile-links). | | `tracking_mode` | `auto` \| `selective` \| `hashtag` | `auto` | Video discovery mode. | | `tracking_hashtags` | `string[]` | `[]` | Required (non-empty) when `tracking_mode` is `hashtag`. Normalized server-side and by a database trigger: NFC + lowercase, leading `#` stripped, and characters that can't appear in a hashtag removed (`#Black-Friday` becomes `blackfriday`). Whitespace and commas separate tags. Max 50 tags, 100 chars each. | | `hashtag_inbox_non_matching` | `boolean` | `true` | Hashtag mode: route non-matching videos to the inbox (`true`) or exclude them (`false`). | | `tracking_start_date` | YYYY-MM-DD | today (org timezone) | First day of the tracked window. Defaults to today in the org's analytics timezone. | | `tracking_end_date` | YYYY-MM-DD | None | Optional last day, inclusive (must be ≥ start date; equal to the start date tracks a single day). | ### Responses | HTTP | Cause | |------|-------| | 201 | Account added; body is the full account object. | | 400 | `invalid_account`: the handle or URL can't become an account. `meta.reason` is `invalid_handle`, `video_url`, `no_handle_in_url`, `unrecognized_url`, `platform_mismatch`, `short_url_unresolved` or `too_many_short_links`. | | 402 | `max_accounts_per_org` limit reached. | | 403 | The handle is banned on this platform (`creator_banned`). | | 404 | Creator not found (or not in your org). | | 409 | `already_tracked`: the handle is already tracked in this project. `creator_inactive`: the creator has been removed. | --- ## GET /api/v2/accounts/{id} Get a single account. ```bash curl -H "Authorization: Bearer tga_" \ https://www.trackagoat.com/api/v2/accounts/ ``` Returns an [account object](#account-fields). 404 if not found. --- ## PATCH /api/v2/accounts/{id} Rename the handle or change tracking configuration. At least one field is required. Requires the `write` scope. **Changing the handle dispatches a re-scrape** of the account's profile and videos. ```bash curl -X PATCH \ -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -d '{"handle": "charlidamelio", "tracking_mode": "selective"}' \ https://www.trackagoat.com/api/v2/accounts/ ``` ### Body | Field | Type | Description | |-------|------|-------------| | `handle` | string | Rename the account handle (or pass a profile URL). Normalized like account creation. Triggers a re-scrape. `409 handle_taken` if another account in the project, including a removed one, already has that handle; `404` if the account was removed. | | `tracking_mode` | `auto` \| `selective` \| `hashtag` | Switch between auto-tracking, inbox review, and hashtag matching. | | `tracking_hashtags` | `string[]` | Replace the tracked hashtag list. Must be non-empty if the resulting mode is `hashtag`. | | `hashtag_inbox_non_matching` | `boolean` | Route non-matching videos to the inbox or exclude them. | | `tracking_start_date` | YYYY-MM-DD \| null | Start of the tracked window. `null` clears it. | | `tracking_end_date` | YYYY-MM-DD \| null | Last day of the tracked window, inclusive. `null` = no end. | Returns the updated [account object](#account-fields). 404 if not found. --- ## DELETE /api/v2/accounts/{id} Soft-delete an account (its history and videos are preserved). Requires the `write` scope. ```bash curl -X DELETE \ -H "Authorization: Bearer tga_" \ https://www.trackagoat.com/api/v2/accounts/ ``` Returns `{ "deleted": true }`. --- ## POST /api/v2/accounts/{id}/scrape Trigger a manual scrape for a single account. Consumes from the org's daily manual-scrape quota. Requires the `write` scope. ```bash curl -X POST \ -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -d '{"job_type": "video_stats"}' \ https://www.trackagoat.com/api/v2/accounts//scrape ``` | Body field | Values | Description | |-----------|--------|-------------| | `job_type` | `creator_profiles` \| `video_stats` \| `discover_videos` | Which scrape job to dispatch (optional). | The response echoes `{ job_type, account_id, job_id }`. | HTTP | Cause | |------|-------| | 200 | Scrape dispatched. | | 400 | Account is disabled. | | 429 | Daily scrape limit exceeded. | --- ## GET /api/v2/accounts/{id}/stats Get an account's follower/stat snapshot history. This series is **append-only**: snapshots are only ever inserted, never updated. ```bash curl -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/accounts//stats?from=2026-01-01&to=2026-03-31" ``` ### Query parameters | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `from` | YYYY-MM-DD | None | Start of the range (inclusive) | | `to` | YYYY-MM-DD | None | End of the range (inclusive) | | `limit` | number | 365 | Max snapshots to return (max 1000) | ### Response fields Each element of the returned array: | Field | Type | Description | |-------|------|-------------| | `account_id` | uuid | Account this snapshot belongs to | | `snapshot_at` | ISO 8601 | When the snapshot was taken | | `follower_count` | number \| null | Followers at snapshot time | | `following_count` | number \| null | Following count at snapshot time | | `total_likes_count` | number \| null | Cumulative likes at snapshot time | | `video_count` | number \| null | Total video count at snapshot time | | `is_baseline` | boolean | Whether this is the initial baseline snapshot | - [Creators](https://www.trackagoat.com/docs/api-reference/creators.md): Creators own accounts, create, list, and manage them. - [Creators and accounts guide](https://www.trackagoat.com/docs/guides/accounts.md): How the multi-platform model works. --- Source: https://www.trackagoat.com/docs/api-reference/accounts --- # Videos > Add, list, update, and remove tracked videos. Trigger scrapes and manage tracking status. Since Trackagoat 2.0, videos are multi-platform. Each video carries a `platform`, belongs to an `account`, and records the platform's own `platform_video_id`. Add videos from TikTok or Instagram; the platform is auto-detected from the URL. ## GET /api/v2/videos List tracked videos in your organization. Videos with `tracking_status = "excluded"` are never returned. ### Request ```bash curl -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/videos?project_id=&platform=instagram" ``` ### Query parameters | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `project_id` | uuid | None | Filter to a specific project | | `creator_id` | uuid | None | Filter to a specific creator | | `account_id` | uuid | None | Filter to a specific account | | `platform` | `tiktok` \| `instagram` \| `youtube` | None | Filter to one platform | | `content_group_id` | uuid | None | Filter to videos in a specific [content group](https://www.trackagoat.com/docs/api-reference/content-groups.md) | | `limit` | number | 50 | Items per page (max 100) | | `cursor` | string | None | Pagination cursor | ### Response fields | Field | Type | Description | |-------|------|-------------| | `id` | uuid | Video ID | | `project_id` | uuid | Project this video belongs to | | `creator_id` | uuid \| null | Creator this video belongs to (null for direct adds) | | `account_id` | uuid \| null | Account this video was posted from (null for direct adds) | | `platform` | `tiktok` \| `instagram` \| `youtube` | Platform the video is on | | `platform_video_id` | string | The platform's internal video ID | | `url` | string | Full video URL | | `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`. Still returned so the response shape doesn't change; it will be removed | | `readme` | string \| null | Team notes / agent context: Markdown notes (rendered in the Notes tab) | | `metadata` | object | Agent-writable structured data | | `thumbnail_url` | string \| null | Proxied thumbnail URL | | `view_count` | number | Current view count (latest snapshot) | | `like_count` | number | Current like count | | `comment_count` | number | Current comment count | | `share_count` | number | Current share count | | `tracking_status` | string | One of: `auto_tracked`, `pending_review`, `included`, `excluded`, `direct_add` | | `posted_at` | ISO 8601 \| null | When the video was originally posted on the platform | | `last_scraped_at` | ISO 8601 \| null | When stats were last refreshed | | `created_at` | ISO 8601 | When video was added to trackagoat | | `updated_at` | ISO 8601 | Last update | --- ## GET /api/v2/videos/{id} Get one video with its latest stats snapshot. Returns every field listed above, plus `save_count`, `media_type`, `duration_seconds`, the tracking flags `tracking_disabled`, `tracking_disabled_reason`, `tracking_exempted` and `plan_limit_paused_at` (set when the video was paused for your plan's [tracked-video limit](https://www.trackagoat.com/docs/guides/limits.md#tracked-videos): its stats stop updating, but it still counts as posted), and the notes: `readme` and its `readme_version` (for [`POST /api/v2/videos/{id}/notes`](#post-apiv2videosidnotes)). Unlike the list, an `excluded` video is still returned when you ask for it by ID. Use the [Analytics endpoint](https://www.trackagoat.com/docs/api-reference/analytics.md) (`entity=video`) for history. ```bash curl -H "Authorization: Bearer tga_" \ https://www.trackagoat.com/api/v2/videos/ ``` Returns **404** if the video doesn't exist or belongs to another organization. --- ## POST /api/v2/projects/{projectId}/videos Add one video URL or a bulk array (up to 50). Requires the `write` scope. The platform and video ID are auto-detected from the URL. Dispatches a video-stats scrape automatically. ```bash curl -X POST \ -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{"url": "https://www.tiktok.com/@charlidamelio/video/7123456789"}' \ "https://www.trackagoat.com/api/v2/projects//videos" ``` Bulk (up to 50), mixing platforms freely: ```bash -d '{"videos": [{"url": "https://www.tiktok.com/@user/video/111"}, {"url": "https://www.instagram.com/reel/ABC123/"}]}' ``` | HTTP | Cause | |------|-------| | 201 | Video(s) added. At your plan's tracked-video limit, your oldest tracked videos are paused to make room; `meta.paused_count` says how many. | | 402 | Tracked-video limit reached and every tracked video is exempt, so none could be paused. | | 422 | Could not detect the platform / video ID from the URL. | --- ## PATCH /api/v2/videos/{id} Update a video's metadata. At least one field is required. Requires the `write` scope. ```bash curl -X PATCH \ -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -d '{"readme": "Strong performance. Boosted by trending audio.", "tracking_status": "included"}' \ https://www.trackagoat.com/api/v2/videos/ ``` ### Body | Field | Type | Description | |-------|------|-------------| | `title` | string | Override the video title. | | `readme` | string \| null | Team notes / agent context (Markdown). Replaces the whole notes; pass `null` to clear. To add to them without replacing, use `POST /api/v2/videos/{id}/notes` below. | | `tracking_status` | string | One of `auto_tracked`, `pending_review`, `included`, `excluded`, `direct_add`. Moving a video to a tracked status at your [tracked-video limit](https://www.trackagoat.com/docs/guides/limits.md#tracked-videos) pauses your oldest tracked video (`402` only if none can be paused). | The caption comes from the platform and can't be changed. Sending the deprecated `description` field returns `400`. --- ## POST /api/v2/videos/{id}/notes Append to or replace a video's notes without clobbering human edits. Requires the `write` scope. Same body, response and errors as [`POST /api/v2/creators/{id}/notes`](https://www.trackagoat.com/docs/api-reference/creators.md#post-apiv2creatorsidnotes): `mode: "append"` adds a dated section signed with the API key's name; `mode: "replace"` requires `expected_readme_version` from `GET /api/v2/videos/{id}` and returns **409** `notes_conflict` if the notes changed since. ```bash curl -X POST \ -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -d '{"mode": "append", "content": "Boosted by trending audio."}' \ https://www.trackagoat.com/api/v2/videos//notes ``` --- ## DELETE /api/v2/videos/{id} Remove a video from tracking. Requires the `write` scope. The video is **excluded, not deleted**: its `tracking_status` becomes `excluded` and the row is kept. It stops being scraped, stops counting toward totals and payouts, and disappears from `GET /api/v2/videos` (`GET /api/v2/videos/{id}` still returns it). Keeping the row is what stops it coming back: the next check of the creator's account finds the post again, and if the row were gone it would be re-added under a new id and its views and view-count bonuses paid a second time. To track it again, `PATCH` it with `"tracking_status": "included"`. Calling `DELETE` on an already-removed video succeeds again. ```bash curl -X DELETE \ -H "Authorization: Bearer tga_" \ https://www.trackagoat.com/api/v2/videos/ ``` Returns `200` with `{ "deleted": true, "id": "", "tracking_status": "excluded", "tracking_disabled": false }`. `deleted: true` is kept for existing clients; it means removed from tracking. (Before 2026-10-09 this endpoint deleted the row.) --- ## GET /api/v2/videos/{id}/comments One video's [Comments tab](https://www.trackagoat.com/docs/guides/comment-insights.md#a-videos-comments-tab): every comment collected for it, not limited to a date range. Requires the `read` scope and a paid plan (`403 plan_feature_unavailable` otherwise). Also available as the MCP tool [`list_video_comments`](https://www.trackagoat.com/docs/agents/mcp.md). ```bash curl -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/videos//comments?signal=app_question" ``` | Parameter | Description | |-----------|-------------| | `sort` | `top` (most liked, default), `newest` or `oldest`. | | `signal` | `app_question`, `signup_intent`, `ad_disclosure`, `risk`, `pay_talk`, `brand_mention`, `competitor`, `referral_spam`, or `flagged` (`ad_disclosure` or `risk`). | | `sentiment` | `positive`, `neutral` or `negative`. | | `q` | Case-insensitive text search. | | `limit`, `offset` | Page size (1 to 200, default 100) and comments to skip. | `data` has `video_id`, `project_id`, `platform`, `video_url`; `coverage` (`platform_comment_count`, the platform's full count; `collected_count`; `last_collected_at`; `min_comment_count`, the org's threshold; `null` if comments can't be collected for the video); `summary` (`positive`, `neutral`, `negative`, `unscored`, and `signal_counts` per signal and `flagged`, over every collected comment whatever the filters); and `comments`: `id`, `text`, `like_count`, `author_unique_id`, `author_nickname`, `created_at_tiktok` (when it was posted, on every platform), `scraped_at`, `sentiment`, `sentiment_score`, `sentiment_model`, `signals`. `signal`, `sentiment` and `q` filter `comments` only. `meta.total` counts the matching comments; `meta.next_offset` is the next page's `offset` (`null` on the last page). --- ## POST /api/v2/videos/{id}/scrape Refresh this video's stats now. Consumes from the org's daily manual-scrape quota. Requires the `write` scope. ```bash curl -X POST \ -H "Authorization: Bearer tga_" \ https://www.trackagoat.com/api/v2/videos//scrape ``` The response echoes `{ video_id, job_id }`. Returns 429 if the daily scrape limit is exceeded. --- ## POST /api/v2/videos/{id}/tracking-disable Disable tracking for a video without deleting it. Requires the `write` scope. ```bash curl -X POST -H "Authorization: Bearer tga_" \ https://www.trackagoat.com/api/v2/videos//tracking-disable ``` Use `/tracking-enable` to re-enable (it also resumes a video paused for your plan's tracked-video limit, pausing your oldest other video if needed; `402` only if none can be paused), and `/tracking-unexempt` to clear the exemption flag. Each returns the video's tracking flags: `{ id, tracking_disabled, tracking_disabled_at, tracking_disabled_reason, tracking_exempted, plan_limit_paused_at }`, and `/tracking-enable` adds `paused_count`. --- ## POST /api/v2/projects/{projectId}/videos/bulk-tracking-disable Disable tracking for up to 200 videos in one call. Requires the `write` scope. ```bash curl -X POST \ -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -d '{"video_ids": ["", ""]}' \ "https://www.trackagoat.com/api/v2/projects//videos/bulk-tracking-disable" ``` Use `/bulk-tracking-enable` to re-enable or resume. Pass `"exempt": true` to also mark videos as exempt from auto-expiry and the tracked-video limit. The response's `paused_count` says how many older videos were paused to make room. --- ## Video time-series analytics There is no per-video `stats` endpoint. Use the unified [Analytics endpoint](https://www.trackagoat.com/docs/api-reference/analytics.md) with `entity=video` for view, engagement, and growth time-series. ```bash # Daily new views for a video curl -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/analytics?entity=video&entity_id=&metric=views&mode=new&granularity=day" # Engagement rate (derived) curl -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/analytics?entity=video&entity_id=&metric=engagement_rate_by_views&mode=rate" ``` To view combined stats for the same content across platforms, group the videos and read [content group stats](https://www.trackagoat.com/docs/api-reference/content-groups.md#get-apiv2content-groupsidstats). `engagement_rate_by_followers` is **not supported** for video entities; use a creator or campaign entity instead. See the [Analytics API reference](https://www.trackagoat.com/docs/api-reference/analytics.md) for the full metric and parameter list. --- Source: https://www.trackagoat.com/docs/api-reference/videos --- # Content groups > Link the same logical content across platforms and read its combined, cross-platform stats. A **content group** links the same logical piece of content across platforms (for example, the same video posted to both TikTok and Instagram) so its combined performance can be viewed together. A video can belong to **at most one** content group. ## Content group fields | Field | Type | Description | |-------|------|-------------| | `id` | uuid | Content group ID | | `project_id` | uuid | Project this group belongs to | | `org_id` | uuid | Owning organization ID | | `title` | string | Group title | | `notes` | string \| null | Freeform notes | | `created_by` | uuid \| null | User who created the group | | `video_ids` | array of uuid | Videos that are members of the group | | `created_at` | ISO 8601 | When the group was created | | `updated_at` | ISO 8601 | Last update | --- ## GET /api/v2/content-groups List content groups in your organization. ```bash curl -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/content-groups?project_id=" ``` ### Query parameters | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `project_id` | uuid | None | Filter to a specific project | | `limit` | number | 50 | Items per page (max 100) | | `cursor` | string | None | Pagination cursor | Returns a paginated array of [content group objects](#content-group-fields). --- ## POST /api/v2/content-groups Create a content group. Requires the `write` scope. Optionally seed it with member videos. ```bash curl -X POST \ -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "project_id": "", "title": "Launch teaser", "notes": "Same clip on TikTok + Reels", "video_ids": ["", ""] }' \ "https://www.trackagoat.com/api/v2/content-groups" ``` ### Body | Field | Type | Default | Description | |-------|------|---------|-------------| | `project_id` | uuid | required | Project to create the group in. | | `title` | string | required | Group title (1 to 200 chars). | | `notes` | string \| null | None | Freeform notes. | | `video_ids` | array of uuid | `[]` | Videos to add (up to 200). Must belong to the same project. | > **Note:** > > A video can live in only one group. Any `video_ids` that already belong to another group are skipped; the returned `video_ids` reflect which videos were actually attached. Returns 201 with the created group, or 404 if the project isn't found. --- ## GET /api/v2/content-groups/{id} Get a single content group. ```bash curl -H "Authorization: Bearer tga_" \ https://www.trackagoat.com/api/v2/content-groups/ ``` Returns a [content group object](#content-group-fields). 404 if not found. --- ## PATCH /api/v2/content-groups/{id} Rename, edit notes, or add/remove member videos. At least one field is required. Requires the `write` scope. ```bash curl -X PATCH \ -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -d '{ "title": "Launch teaser (final)", "add_video_ids": [""], "remove_video_ids": [""] }' \ https://www.trackagoat.com/api/v2/content-groups/ ``` ### Body | Field | Type | Description | |-------|------|-------------| | `title` | string | Rename the group (1 to 200 chars). | | `notes` | string \| null | Edit notes. Pass `null` to clear. | | `add_video_ids` | array of uuid | Videos to add to the group (up to 200). | | `remove_video_ids` | array of uuid | Videos to remove from the group (up to 200). | Returns the updated [content group object](#content-group-fields). 404 if not found. --- ## DELETE /api/v2/content-groups/{id} Delete a content group. The member videos themselves are not deleted; they are simply unlinked. Requires the `write` scope. ```bash curl -X DELETE \ -H "Authorization: Bearer tga_" \ https://www.trackagoat.com/api/v2/content-groups/ ``` Returns `{ "deleted": true }`. --- ## GET /api/v2/content-groups/{id}/stats Get combined stats for a content group: current metrics summed across every member video, plus a per-platform breakdown. ```bash curl -H "Authorization: Bearer tga_" \ https://www.trackagoat.com/api/v2/content-groups//stats ``` ### Response ```json { "data": { "video_count": 3, "totals": { "view_count": 1250000, "like_count": 84000, "comment_count": 3100, "share_count": 900 }, "by_platform": { "tiktok": { "video_count": 2, "view_count": 1000000, "like_count": 70000, "comment_count": 2500, "share_count": 800 }, "instagram": { "video_count": 1, "view_count": 250000, "like_count": 14000, "comment_count": 600, "share_count": 100 } } }, "error": null, "meta": null } ``` ### Response fields | Field | Type | Description | |-------|------|-------------| | `video_count` | number | Number of videos in the group | | `totals` | object | Summed `view_count`, `like_count`, `comment_count`, `share_count` across the group | | `by_platform` | object | Keyed by platform; each value carries `video_count` plus the same four metric totals for that platform | A group with no member videos returns `video_count: 0`, zeroed `totals`, and an empty `by_platform`. - [Videos](https://www.trackagoat.com/docs/api-reference/videos.md): List videos and filter by content group. - [Creators and accounts guide](https://www.trackagoat.com/docs/guides/accounts.md): How the multi-platform model works. --- Source: https://www.trackagoat.com/docs/api-reference/content-groups --- # Share links > Create and manage PIN-gated public pages where creators can see their own analytics and payouts. A **share link** is a creator's PIN-gated public page. See the [Shareable creator pages guide](https://www.trackagoat.com/docs/guides/shareable-pages.md) for what a creator actually sees. ## The URL is derived from the creator id There is no share-link identifier to store or look up. Every page lives at: ``` https://www.trackagoat.com/share/creator/ ``` So any creator id from any endpoint can be turned into a link with string concatenation. The `url` field is returned for convenience, but you never need to fetch it. The corollary is that the URL is **not** a secret: the 4-digit entrance PIN is the access control. Unlock attempts are rate limited and locked out after 10 failures. ## Permissions Every share-link endpoint requires **two** things: 1. The right API key scope (`read` to fetch, `write` to modify). 2. The member the key acts as must be an **org admin**. Creating a public link is a trust decision, so a `write`-scope key acting as a non-admin still gets `403 insufficient_role`. ## PIN visibility PINs are omitted by default. This is deliberate: a `read`-scope agent sweeping `/api/v2/creators` should not pull every entrance PIN in the org into its logs. | Situation | Is `pin` returned? | |-----------|--------------------| | Any read, by default | No | | `GET /creators/{id}/share-link?include_pin=true` with the `write` scope | Yes | | `GET /creators/{id}/share-link?include_pin=true` with only `read` | No (`403 insufficient_scope`) | | `GET /creators` or `/creators/{id}` with `?include_pin=true` and the `admin` scope | Yes | | `GET /creators` or `/creators/{id}` with `?include_pin=true` without `admin` | No (`403 insufficient_scope`) | | `POST` (create) | Always. This is your one chance to capture it | | `PATCH` that changed the PIN | Yes | | `POST /share-links/bulk` | Always, for every creator | ## The share_link object ```json { "url": "https://www.trackagoat.com/share/creator/3fa85f64-5717-4562-b3fc-2c963f66afa6", "enabled": true, "sections": ["analytics", "payouts"], "pin": "4821", "created_at": "2026-07-29T14:02:11.482Z" } ``` | Field | Type | Description | |-------|------|-------------| | `url` | string | The public page. Always `{site}/share/creator/{creator_id}`. | | `enabled` | boolean | When `false`, the page renders a neutral "not available" state. | | `sections` | array | Some of `"analytics"`, `"payouts"`. Never empty. | | `pin` | string | The 4-digit entrance PIN. Present only per the table above. | | `created_at` | string | ISO 8601. | ## Embedded on creator responses `GET /api/v2/creators` and `GET /api/v2/creators/{id}` both carry a `share_link` field (`null` when the creator has no page). No second call is needed to list every creator's link. ```bash curl -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/creators?project_id=" ``` ```json { "data": [ { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "display_name": "Holden K", "handle": "holden.k", "share_link": { "url": "https://www.trackagoat.com/share/creator/3fa85f64-5717-4562-b3fc-2c963f66afa6", "enabled": true, "sections": ["analytics"], "created_at": "2026-07-29T14:02:11.482Z" } } ], "error": null, "meta": { "hasMore": false, "nextCursor": null } } ``` Add `include_pin=true` with an `admin`-scope key to include PINs: ```bash curl -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/creators?include_pin=true" ``` ## GET /api/v2/creators/\{id\}/share-link Returns the creator's share link, or `null` data if they don't have one. ### Query parameters | Parameter | Type | Description | |-----------|------|-------------| | `include_pin` | boolean | Include the entrance PIN. Requires the `write` scope (the key's owner is already required to be an org admin here). | ```bash curl -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/creators//share-link" ``` ## POST /api/v2/creators/\{id\}/share-link Creates the page, or replaces the settings of an existing one (and re-enables it if it was disabled). Requires the `write` scope. ### Body | Field | Type | Default | Description | |-------|------|---------|-------------| | `pin` | string | None | Exactly 4 digits. A random PIN is generated when omitted. | | `show_analytics` | boolean | `true` | Show the analytics section. | | `show_payouts` | boolean | `false` | Show the payout accruals section. | At least one of `show_analytics` / `show_payouts` must be `true`, or the request returns `400 no_sections`. ```bash curl -X POST -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -d '{"show_analytics": true, "show_payouts": true}' \ "https://www.trackagoat.com/api/v2/creators//share-link" ``` Returns `201` with the full object **including the PIN**. Supports `Idempotency-Key`. > **Tip:** > > Capture the PIN from this response. You can read it back later with `GET /api/v2/creators/{id}/share-link?include_pin=true` and a `write`-scope key. ## PATCH /api/v2/creators/\{id\}/share-link Requires the `write` scope. Send only the fields you want to change. | Field | Type | Description | |-------|------|-------------| | `enabled` | boolean | `false` renders a neutral "not available" page without deleting the link. | | `show_analytics` | boolean | Toggle the analytics section. | | `show_payouts` | boolean | Toggle the payouts section. | | `pin` | string | Set a specific 4-digit PIN. | | `regenerate_pin` | boolean | Issue a new random PIN. | Any PIN change (via `pin` or `regenerate_pin`) immediately invalidates every active unlock session, so anyone currently viewing the page must re-enter the new PIN. The new PIN is returned in the response. ```bash # Rotate the PIN curl -X PATCH -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -d '{"regenerate_pin": true}' \ "https://www.trackagoat.com/api/v2/creators//share-link" # Turn the page off without deleting it curl -X PATCH -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -d '{"enabled": false}' \ "https://www.trackagoat.com/api/v2/creators//share-link" ``` Returns `404` if the creator has no share link. Use `POST` to create one. ## DELETE /api/v2/creators/\{id\}/share-link Requires the `write` scope. The URL then renders the same "not available" page as a disabled link. ```bash curl -X DELETE -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/creators//share-link" ``` ```json { "data": { "deleted": true }, "error": null, "meta": null } ``` ## POST /api/v2/share-links/bulk Creates or updates pages for up to 200 creators in one call. Requires the `write` scope. ### Body | Field | Type | Default | Description | |-------|------|---------|-------------| | `creator_ids` | uuid[] | required | 1 to 200 creator ids. | | `show_analytics` | boolean | `true` | Show the analytics section. | | `show_payouts` | boolean | `false` | Show the payout accruals section. | | `pin_mode` | `unique` \| `shared` | `unique` | `unique` gives each creator their own PIN; `shared` applies one PIN to the batch. | | `pin` | string | None | Only meaningful with `pin_mode: "shared"`. Generated when omitted. | ```bash curl -X POST -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -d '{"creator_ids": ["", ""], "show_analytics": true, "pin_mode": "unique"}' \ "https://www.trackagoat.com/api/v2/share-links/bulk" ``` ```json { "data": [ { "creator_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "url": "https://www.trackagoat.com/share/creator/3fa85f64-5717-4562-b3fc-2c963f66afa6", "pin": "4821", "sections": ["analytics"], "status": "created" }, { "creator_id": "00000000-0000-0000-0000-000000000000", "url": null, "pin": null, "sections": null, "status": "skipped", "error": "Creator not found in this organization" } ], "error": null, "meta": { "created": 1, "updated": 0, "skipped": 1, "pin_mode": "unique" } } ``` Every PIN is returned, since a bulk create is the moment you distribute them. Creator ids outside the key's organization come back as `skipped` rather than an error; the endpoint never confirms whether an id exists elsewhere. Supports `Idempotency-Key`. ## Errors | Code | Status | Meaning | |------|--------|---------| | `insufficient_scope` | 403 | Missing the required scope, or `include_pin=true` without `write` (share-link endpoint) or `admin` (creator list and detail). | | `insufficient_role` | 403 | The member the key acts as is not an org admin. | | `not_found` | 404 | No such creator in this org, or no share link to update. | | `no_sections` | 400 | Both `show_analytics` and `show_payouts` were false. | | `invalid_pin` | 400 | The PIN was not exactly 4 digits. | --- Source: https://www.trackagoat.com/docs/api-reference/share-links --- # Campaigns > Create, update, and delete campaigns. Manage campaign membership (items). Campaigns are **mixed-platform**: a single campaign can contain creators and videos across TikTok and Instagram. Campaign items reference creators, videos, or sub-campaigns regardless of platform. To view a campaign's performance split by platform, query the [aggregate endpoints](https://www.trackagoat.com/docs/api-reference/aggregate.md) (e.g. `video-leaderboard`, `creator-leaderboard`) with the `platform` filter, or read a video's `platform` field directly. ## GET /api/v2/campaigns List campaigns in your organization. ### Request ```bash curl -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/campaigns?project_id=" ``` ### Query parameters | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `project_id` | uuid | None | Filter to a specific project | | `limit` | number | 50 | Items per page (max 100) | | `cursor` | string | None | Pagination cursor | ### Response fields | Field | Type | Description | |-------|------|-------------| | `id` | uuid | Campaign ID | | `project_id` | uuid | Project this campaign belongs to | | `name` | string | Campaign name | | `readme` | string \| null | Markdown notes / agent context (rendered in the Notes tab) | | `metadata` | object | Agent-writable structured data | | `created_at` | ISO 8601 | Creation timestamp | | `updated_at` | ISO 8601 | Last update | --- ## POST /api/v2/projects/{projectId}/campaigns Create a campaign. Requires the `write` scope. ```bash curl -X POST \ -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{"name": "Summer Drop Campaign"}' \ "https://www.trackagoat.com/api/v2/projects//campaigns" ``` --- ## PATCH /api/v2/campaigns/{id} Update a campaign. At least one field required. Requires the `write` scope. ### Body | Field | Type | Description | |-------|------|-------------| | `name` | string | Campaign name (max 200 chars). | | `readme` | string \| null | Markdown notes. Replaces the whole README; pass `null` to clear. | | `expected_readme_version` | string | Optional, only with `readme`: the `readme_version` from your last read (`GET /api/v2/notes/campaign/{id}` or a previous `PATCH` response). If the README changed since, nothing is written and the response is **409** `notes_conflict` with `meta.current_readme_version`. Without it, `readme` overwrites whatever is there. | | `target_views` | number \| null | Target total view goal for the campaign. | | `target_at` | ISO 8601 datetime \| null | Target completion date. | The response is the updated campaign plus its `readme_version`. To add to the README without replacing it, use [`POST /api/v2/notes/campaign/{id}`](https://www.trackagoat.com/docs/api-reference/notes.md) with `mode: "append"`. --- ## DELETE /api/v2/campaigns/{id} Delete a campaign and all its item memberships. Requires the `write` scope. ```bash curl -X DELETE -H "Authorization: Bearer tga_" \ https://www.trackagoat.com/api/v2/campaigns/ ``` --- ## POST /api/v2/campaigns/{id}/items Add a creator, video, or sub-campaign to a campaign. Requires the `write` scope. ```bash curl -X POST \ -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -d '{"item_type": "creator", "item_id": ""}' \ https://www.trackagoat.com/api/v2/campaigns//items ``` | Body field | Type | Description | |-----------|------|-------------| | `item_type` | `creator` \| `video` \| `campaign` | The type of item to add. | | `item_id` | uuid | ID of the creator, video, or sub-campaign. It must be in the same project as the campaign, and a creator must be active. | Returns **404** if the item doesn't exist in the campaign's project, and **409** if the item is already in the campaign or would create a circular reference. --- ## DELETE /api/v2/campaigns/{id}/items Remove an item from a campaign. Pass the `item_type` and `item_id` in the request body. Requires the `write` scope. ```bash curl -X DELETE \ -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -d '{"item_type": "creator", "item_id": ""}' \ https://www.trackagoat.com/api/v2/campaigns//items ``` Returns `{ "deleted": true }`, or `{ "deleted": false }` when the item wasn't in the campaign. --- ## Analytics (time-series) Campaign time-series analytics have moved to the unified endpoint. The new endpoint also supports breakdown by video or creator, multi-granularity buckets, and prior-period comparison. ```bash # Daily new views for a campaign curl -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/analytics?entity=campaign&entity_id=&metric=views&mode=new&granularity=day" # Monthly cumulative views curl -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/analytics?entity=campaign&entity_id=&metric=views&mode=cumulative&granularity=month" # Top-10 video breakdown curl -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/analytics?entity=campaign&entity_id=&metric=views&breakdown=video&top_n=10" # Engagement rate (derived) curl -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/analytics?entity=campaign&entity_id=&metric=engagement_rate_by_views&mode=rate" # Multi-entity overlay: compare up to 5 campaigns side-by-side curl -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/analytics?entity=campaign&entity_ids=,,&metric=views&granularity=week" ``` See the [Analytics API reference](https://www.trackagoat.com/docs/api-reference/analytics.md) for the full parameter and response documentation. > **Removed:** `GET /api/v2/campaigns/{id}/stats` returns `410 Gone`. Use `GET /api/v2/analytics?entity=campaign&entity_id={id}` instead. For a per-platform split of a campaign, use the [aggregate](https://www.trackagoat.com/docs/api-reference/aggregate.md) `video-leaderboard` or `creator-leaderboard` endpoints with the `platform` query parameter. --- Source: https://www.trackagoat.com/docs/api-reference/campaigns --- # Goal compliance > Create and manage creator goals, and read per-creator compliance results with data advisories. ## Managing goals (write endpoints) ### POST /api/v2/creators/{id}/goals Create a goal for a creator. Requires the `write` scope. ```bash curl -X POST \ -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{"metric": "views", "period": "weekly", "target_value": 500000}' \ https://www.trackagoat.com/api/v2/creators//goals ``` | Field | Type | Description | |-------|------|-------------| | `metric` | `posts` \| `views` \| `engagement` \| `followers` | What to measure. | | `period` | `daily` \| `weekly` \| `monthly` | Evaluation window. | | `target_value` | integer > 0 | Target number per period. | | `start_date` | YYYY-MM-DD | Optional. The goal is first judged in the first period that starts on or after this date (org time zone). | | `end_date` | YYYY-MM-DD | Optional. The goal is last judged in the last period that ends on or before this date. | | `platform_scope` | `all` \| `platform` \| `account` | Optional, default `all`. Scopes the goal across the creator's platform accounts: `all` = the whole creator (every account), `platform` = one platform, `account` = a single account. | | `platform` | `tiktok` \| `instagram` \| `youtube` \| null | Required when `platform_scope` is `platform`; must be omitted/null otherwise. | | `account_id` | uuid \| null | Required when `platform_scope` is `account` (must belong to this creator); must be omitted/null otherwise. | > **Note: Multi-platform scope** > > Trackagoat 2.0 creators own one or more platform accounts (TikTok, Instagram). `platform_scope` controls which of a creator's accounts a goal is measured against. The scope combination is validated: `all` → no `platform`/`account_id`; `platform` → `platform` only; `account` → `account_id` only. A mismatch returns **400**. ```bash # A weekly TikTok-only posting goal curl -X POST \ -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -d '{"metric": "posts", "period": "weekly", "target_value": 5, "platform_scope": "platform", "platform": "tiktok"}' \ https://www.trackagoat.com/api/v2/creators//goals ``` ### PATCH /api/v2/creators/{id}/goals/{goalId} Update a goal. At least one field required. Requires the `write` scope. Accepts `metric`, `period`, `target_value`, `is_active`, `start_date`, and `end_date`. The platform scope (`platform_scope`, `platform`, `account_id`) is fixed at creation and cannot be changed; delete and recreate the goal to re-scope it. ### DELETE /api/v2/creators/{id}/goals/{goalId} Permanently delete a goal. Requires the `write` scope. --- ## Compliance overview (read endpoint) The goal compliance endpoint returns per-creator, per-goal progress results for a project. Each result includes: - The goal's current **actual** value and **target** - A **status** (`on_track`, `behind_pace`, `at_risk`, `missed`, `completed`) - An **advisory** block explaining why the displayed progress may not be final - The goal's **platform scope** (`platform_scope`, `platform`, `account_id`): `actual` is measured only against the accounts in scope For a project-wide roll-up (counts only), use [`/aggregate/goal-compliance`](https://www.trackagoat.com/docs/api-reference/aggregate.md#get-aggregategoal-compliance) instead. --- ## GET `/api/v2/projects/:projectId/goal-compliance` Returns compliance results for all active creators in a project, grouped by creator. ### Request ```bash curl -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/projects//goal-compliance" ``` ### Query parameters | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `creator_id` | UUID | None | Restrict results to a single creator | | `period` | `daily` \| `weekly` \| `monthly` | all | Filter to goals of this period type | | `reference_date` | YYYY-MM-DD | today (org tz) | Evaluate goals as of this date | | `platform` | `tiktok` \| `instagram` \| `youtube` | all | Restrict results to creators that have an account on this platform | ### Response ```json { "data": [ { "creator": { "id": "uuid", "handle": "sarah.b8520", "display_name": "SarahBD", "avatar_url": "https://..." }, "compliance": [ { "goal": { "id": "uuid", "creator_id": "uuid", "project_id": "uuid", "metric": "posts", "period": "weekly", "target_value": 15, "start_date": null, "end_date": null, "is_active": true, "platform_scope": "all", "platform": null, "account_id": null }, "target": 15, "actual": 11, "percentage": 73, "status": "behind_pace", "advisory": { "period_in_progress": true, "period_recently_ended": false, "scrape_stale": false, "not_yet_started": false, "has_advisory": true, "period_start": "2026-05-18T05:00:00.000Z", "period_end": "2026-05-25T04:59:59.999Z", "hours_until_period_end": 89.4, "hours_since_period_end": null, "scrape_freshness": null, "goal_start_date": null, "messages": [ "Period in progress. It closes in 3 days, so the final actual may be higher." ] } } ] } ], "error": null, "meta": { "org_tz": "America/Chicago", "reference_date": "2026-05-20" } } ``` ### Status values | Status | Meaning | |--------|---------| | `on_track` | Projected to meet or exceed target by period end | | `behind_pace` | Below projected pace but above the 70% at-risk threshold | | `at_risk` | Significantly behind pace (below 70% of projected) | | `completed` | Period has ended and target was met | | `missed` | Period has ended and target was not met | During the first 15% of the period an unmet goal is `on_track`. Posts goals compare whole posts, so a goal is behind only once a whole post is due. Followers goals add up the growth of each account in scope, and each account is measured from its own starting count. --- ## The `advisory` object Every compliance result includes an `advisory` block. When `has_advisory` is `false`, the displayed progress is likely final and the object can be ignored. When `true`, the `messages` array contains one human-readable explanation per active flag. ### Advisory flags | Field | Type | Condition | |-------|------|-----------| | `period_in_progress` | boolean | Current wall-clock time is before the period end. Actual counts will grow until period close. | | `period_recently_ended` | boolean | Period ended within the last 48 hours. Late-period posts may still be processing through scrape. | | `scrape_stale` | boolean | Creator's stats scrape is turning stale or overdue. Views, engagement, or follower counts may undercount until the next scrape runs. | | `not_yet_started` | boolean | The goal has a `start_date` set in the future. No data has accrued yet. | | `has_advisory` | boolean | `true` when `period_recently_ended` is true. Drives the UI caution icon. The other flags are available for programmatic use but do not set `has_advisory`. | ### Advisory context fields | Field | Type | Description | |-------|------|-------------| | `period_start` | ISO timestamp | UTC start of the current period | | `period_end` | ISO timestamp | UTC end of the current period | | `hours_until_period_end` | number \| null | Hours remaining until period closes; `null` if period already ended | | `hours_since_period_end` | number \| null | Hours elapsed since period closed; `null` if period is still open | | `scrape_freshness` | object \| null | Full freshness summary (bucket, age, tooltip) when `scrape_stale` is true; `null` otherwise | | `goal_start_date` | YYYY-MM-DD \| null | The goal's `start_date`, when `not_yet_started` is true | | `messages` | string[] | One pre-formatted explanation string per active flag, safe to display directly | --- ## Relationship to aggregate endpoint | Use case | Endpoint | |----------|----------| | Dashboard summary (counts only) | `GET /api/v2/analytics/aggregate/goal-compliance` | | Per-creator, per-goal detail with advisories | `GET /api/v2/projects/:id/goal-compliance` | | Single creator's goals | `GET /api/v2/projects/:id/goal-compliance?creator_id=` | --- Source: https://www.trackagoat.com/docs/api-reference/goals --- # Payouts > Manage payout methods, structures, assignments, accruals, and payments via the trackagoat API (v2). All payout endpoints require a Bearer API key. Responses follow the standard `{ data, error, meta }` envelope. Amounts are in cents (integer). Timestamps are ISO 8601 UTC. > **Warning: Write scope required for mutations** > > Every payout write requires the key to have the `write` scope. That covers `POST`, `PATCH`, and `DELETE` on `payout-methods`, `payout-structures` (including `[id]/assignments`), `payout-payments`, `payout-accruals`, and `payouts/payment-receipts` (and its `upload-url`). A read-only (`read`-only) key is rejected with **403 `insufficient_scope`**. `GET` endpoints need only the `read` scope. --- ## Payout Methods Payout methods are org-scoped. Default methods (Venmo, Cash App, PayPal) cannot be deleted: use `PATCH { is_active: false }` to disable. ### GET /api/v2/payout-methods List org's payout methods. ```bash curl -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/payout-methods" # Include disabled methods curl -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/payout-methods?include_disabled=1" ``` **Query parameters** | Parameter | Type | Description | |-----------|------|-------------| | `include_disabled` | `1` \| `true` | Include inactive methods (org admins only) | **Response fields:** `id`, `org_id`, `name`, `icon_url`, `is_default`, `is_active`, `metadata`, `readme`, `created_at` --- ### POST /api/v2/payout-methods Create a custom payout method. ```bash curl -X POST \ -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -d '{"name": "Zelle"}' \ https://www.trackagoat.com/api/v2/payout-methods ``` **Body** | Field | Type | Required | Description | |-------|------|----------|-------------| | `name` | string | ✓ | Display name (max 100 chars) | | `icon_path` | string \| null | | Storage path of an uploaded icon. A new method's id isn't known until it's created, so add the icon afterwards: see [Method icons](#method-icons) | | `metadata` | object | | Agent-writable structured data | | `readme` | string \| null | | Markdown notes | Returns `201` with the created method row. --- ### GET /api/v2/payout-methods/{id} Fetch a single method. --- ### PATCH /api/v2/payout-methods/{id} Update name, active state, metadata, or readme. ```bash curl -X PATCH \ -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -d '{"is_active": false}' \ https://www.trackagoat.com/api/v2/payout-methods/ ``` **Body:** `name?`, `is_active?`, `icon_path?`, `metadata?`, `readme?` (at least one required). `icon_path` must be inside this method's folder (`orgs/{orgId}/{methodId}/…`), as returned by the icon upload URL below; `null` removes the icon. --- ### Method icons Icons are PNG, JPEG or WebP images up to 1 MB, stored in a public bucket. Only org admins can change them (`403 forbidden` otherwise). Two ways to upload: **Inline (one call).** `POST /api/v2/payout-methods/{id}/icon` with the image as base64. It is checked against `content_type` (`content_type_mismatch` otherwise), stored, and set as the method's `icon_path`; the updated method is returned. ```bash curl -X POST \ -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -d "{\"content_type\": \"image/png\", \"content_base64\": \"$(base64 < wise.png | tr -d '\n')\"}" \ https://www.trackagoat.com/api/v2/payout-methods//icon ``` **Signed URL.** `POST /api/v2/payout-methods/{id}/icon/upload-url` with `{ "content_type": "image/png" }` returns `signed_url`, `path`, `max_bytes` and `expires_in_seconds` (2 hours). PUT the raw image to `signed_url` with the same `Content-Type`, then `PATCH /api/v2/payout-methods/{id}` with `{ "icon_path": "" }`. ```bash curl -X PUT -H "Content-Type: image/png" --data-binary @wise.png "" ``` --- ### DELETE /api/v2/payout-methods/{id} Delete a custom method. Returns `409 default_method` for seeded default methods. --- ## Payout Structures Payout structures define earning rules for creators. Scoped to a project. Rule edits create new immutable versions. ### GET /api/v2/payout-structures List structures in the org (optionally filtered to a project). ```bash curl -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/payout-structures?project_id=" ``` **Query parameters** | Parameter | Type | Description | |-----------|------|-------------| | `project_id` | uuid | Filter to a project | | `is_active` | `true` \| `false` | Filter by active state | | `limit` | number | Items per page (max 100, default 50) | | `cursor` | string | Pagination cursor | --- ### POST /api/v2/payout-structures Create a payout structure (creates version 1 atomically). ```bash # Recurring $50/month curl -X POST \ -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -d '{ "project_id": "", "name": "Monthly base", "period": "monthly", "amount_cents": 5000, "currency": "USD", "criteria_type": "recurring", "criteria_params": {}, "proration_mode": "skip" }' \ https://www.trackagoat.com/api/v2/payout-structures # Instagram-only recurring $50/month (platform scope) curl -X POST \ -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -d '{ "project_id": "", "name": "Instagram base", "period": "monthly", "amount_cents": 5000, "currency": "USD", "criteria_type": "recurring", "criteria_params": {}, "proration_mode": "skip", "platform_scope": "platform", "platform": "instagram" }' \ https://www.trackagoat.com/api/v2/payout-structures # Goals-based monthly with partial completion curl -X POST \ -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -d '{ "project_id": "", "name": "Monthly goals bonus", "period": "monthly", "amount_cents": 50000, "currency": "USD", "criteria_type": "goals", "criteria_params": { "mode": "all", "partial_allowed": true, "partial_multiplier": 1.0, "min_completion_pct": 50 }, "proration_mode": "skip" }' \ https://www.trackagoat.com/api/v2/payout-structures # CPM-based weekly curl -X POST \ -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -d '{ "project_id": "", "name": "Weekly CPM", "period": "weekly", "amount_cents": 0, "currency": "USD", "criteria_type": "cpm", "criteria_params": { "source": "views_gained", "view_lifetime_days": null, "min_impressions": 10000, "max_payout_cents": 50000, "tiers": [ {"up_to_impressions": 1000000, "cents_per_thousand": 50}, {"up_to_impressions": null, "cents_per_thousand": 75} ] }, "proration_mode": "skip" }' \ https://www.trackagoat.com/api/v2/payout-structures # View threshold: $20 at 1M views, $100 at 5M views curl -X POST \ -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -d '{ "project_id": "", "name": "Viral video milestones", "period": null, "amount_cents": null, "currency": "USD", "criteria_type": "view_threshold", "criteria_params": { "thresholds": [ {"view_count": 1000000, "amount_cents": 2000, "currency": "USD"}, {"view_count": 5000000, "amount_cents": 10000, "currency": "USD"} ] }, "proration_mode": "skip", "backfill_on_assignment": false }' \ https://www.trackagoat.com/api/v2/payout-structures ``` **Body** | Field | Type | Required | Description | |-------|------|----------|-------------| | `project_id` | uuid | ✓ | Target project | | `name` | string | ✓ | Structure name | | `period` | `daily` \| `weekly` \| `monthly` \| `null` | ✓ | Payout frequency. Pass `null` for `view_threshold` structures. | | `amount_cents` | number \| `null` | ✓ | Amount in cents. Pass `null` for `view_threshold` and `conversion` structures (their rates live in `criteria_params`). Ignored for `cpm`: the tiers carry the money, so pass `0`. | | `currency` | `USD` | ✓ | Currently only USD | | `criteria_type` | `recurring` \| `goals` \| `cpm` \| `view_threshold` \| `conversion` | ✓ | Earning rule type | | `criteria_params` | object | ✓ | Rule-specific params (see below) | | `proration_mode` | `skip` \| `prorate` \| `full` | ✓ | Mid-period assignment behavior for `recurring` and `goals`. Ignored for `cpm` (earnings already reflect only the views posted in the period); `view_threshold` and `conversion` always use `skip`. | | `backfill_on_assignment` | boolean | | `view_threshold` and `conversion` only. If `true`, also counts what the creator had already earned before assignment (milestones already crossed, or conversions already recorded). | | `period_anchor` | 1-7 \| `null` | | `conversion` with a weekly cadence only. ISO day of week (1=Mon) the week starts on, so "every Tuesday" is expressible. `null` keeps the default Monday alignment. | | `platform_scope` | `all` \| `platform` \| `account` | | Default `all`. Which of an assigned creator's platform accounts the rule earns against (see below). | | `platform` | `tiktok` \| `instagram` \| `youtube` \| null | | Required when `platform_scope` is `platform`; omit/null otherwise. | | `account_id` | uuid \| null | | Required when `platform_scope` is `account`; omit/null otherwise. | | `metadata` | object | | Agent-writable data | | `readme` | string \| null | | Markdown notes | > **Note: Platform scope (Trackagoat 2.0)** > > Each structure version carries a platform scope, mirroring goals. `all` earns against the whole creator (every account), `platform` restricts earning to a single platform, and `account` restricts to one account. The scope is validated: `all` → no `platform`/`account_id`; `platform` → `platform` only; `account` → `account_id` only. A mismatch returns **400**. Scope is set per version: a rule-edit `PATCH` can change it, creating a new version. **`criteria_params` shapes** ```typescript // conversion: pay for recorded outcomes criteria_params: { cadence: "every_conversion" | "daily" | "weekly" | "monthly", rates: [ { conversion_event_id: uuid, rate_type: "flat", amount_cents: number }, { conversion_event_id: uuid, rate_type: "percent", percent_bps: number }, // 1000 = 10% ], } // `period` must equal `cadence`, or be null when cadence is "every_conversion". // `amount_cents` is always null: the rates carry the money. // Max 25 rates; each conversion event may appear at most once. // recurring: no params criteria_params: {} // goals: pay if creator met their goals criteria_params: { mode: "all" | "any" | "n_of_m", n?: number, // required when mode = "n_of_m" // Partial completion (only valid when mode = "all") partial_allowed?: boolean, // default false: enable fractional payouts on missed goals partial_multiplier?: number, // default 1.0: scales the partial amount (0 to 1) min_completion_pct?: number, // default 0: 0 to 100; below this avg completion%, no payout fires } // cpm: pay per 1,000 views. Each period pays for the views each video gained // since it was last paid, so videos keep earning after their first period. // The tiers carry the money: top-level `amount_cents` (pass 0) and // `proration_mode` are both ignored for this type. criteria_params: { source?: "views_gained", // default; the retired "posts_in_period" is accepted and stored as "views_gained" view_lifetime_days?: number | null, // days after posting a video keeps earning (1 to 3650); null/omitted = forever min_impressions: number, // views gained in a period before it pays; smaller periods carry over max_payout_cents: number | null, // cap (null = unlimited) tiers: [ // sorted ascending; last tier must have null up_to_impressions { up_to_impressions: number | null, cents_per_thousand: number } ] } // view_threshold: one-time bonus when a video crosses a view count // period, amount_cents, and proration_mode must be null/skip for this type criteria_params: { thresholds: [ // 1 to 10 milestones; view_count values must be unique { view_count: number, // minimum lifetime views on the video to trigger amount_cents: number, // bonus amount in cents currency: "USD" } ] } ``` **Partial completion formula (goals, mode = "all"):** When `partial_allowed` is true and the creator does not fully meet all goals: 1. Each goal's completion fraction = `min(actual / target, 1.0)`. 2. Average those fractions → `completion_pct`. 3. If `completion_pct < min_completion_pct / 100` → no accrual. 4. Otherwise: `payout = amount × completion_pct × partial_multiplier`. When all goals are fully met, the full amount is paid regardless of `partial_allowed`. Returns `201` with `{ structure_id, version_id }`. --- ### GET /api/v2/payout-structures/{id} Fetch structure detail including `current_version`, full `versions[]` history, and `assignment_count`. --- ### PATCH /api/v2/payout-structures/{id} Update a structure. Use `kind` to distinguish cosmetic edits from rule changes. ```bash # Cosmetic: update name and metadata without creating a new version curl -X PATCH \ -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -d '{"kind": "cosmetic", "name": "Monthly base v2", "metadata": {"owner": "growth-team"}}' \ https://www.trackagoat.com/api/v2/payout-structures/ # Rule edit: creates a new immutable version; existing accruals keep their version curl -X PATCH \ -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -d '{ "kind": "rule", "period": "monthly", "amount_cents": 7500, "currency": "USD", "criteria_type": "recurring", "criteria_params": {}, "proration_mode": "skip" }' \ https://www.trackagoat.com/api/v2/payout-structures/ ``` A rule PATCH accepts the same `platform_scope` / `platform` / `account_id` fields as create (same validation rules), letting a new version re-scope the payout to a platform or account. Omitting them keeps the current version's scope. A rule PATCH replaces the whole rule, so send every field. To change only how long videos earn on a CPM structure, read the structure first and send its current `criteria_params` with `view_lifetime_days` changed (the MCP tool `set_cpm_view_lifetime` does this for you): ```bash # Stop paying for a video's views 30 days after it's posted (null = forever) curl -X PATCH \ -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -d '{ "kind": "rule", "period": "weekly", "amount_cents": 0, "currency": "USD", "criteria_type": "cpm", "criteria_params": { "source": "views_gained", "view_lifetime_days": 30, "min_impressions": 10000, "max_payout_cents": 50000, "tiers": [ {"up_to_impressions": 1000000, "cents_per_thousand": 50}, {"up_to_impressions": null, "cents_per_thousand": 75} ] }, "proration_mode": "skip" }' \ https://www.trackagoat.com/api/v2/payout-structures/ ``` The new version applies from the period in progress; periods that already ended keep the version they were calculated with. Rule PATCH returns `{ version_id, version_number }`. Cosmetic PATCH returns the updated structure row. A rule PATCH cannot change `period` (or a conversion `cadence`) or `criteria_type` once the structure has any accruals or has ever had a creator assigned: it returns **409 `structure_shape_locked`**. Periods are generated from the structure's current shape, so changing it on a live structure would create overlapping accruals for periods that were already paid under the old shape. Create a new structure instead, assign creators to it, and archive the old one. Amounts, criteria parameters and proration can still be edited, and apply to future periods only. --- ### DELETE /api/v2/payout-structures/{id} Archive (default) or hard-delete a structure. ```bash # Archive: stops new accruals, preserves historical ones curl -X DELETE -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/payout-structures/" # Hard delete: blocked with 409 has_accruals if accruals exist curl -X DELETE -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/payout-structures/?mode=hard" ``` --- ## Assignments Assign or unassign creators to a payout structure. ### GET /api/v2/payout-structures/{id}/assignments List currently assigned creators. ```bash curl -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/payout-structures//assignments" ``` | Parameter | Type | Description | |-----------|------|-------------| | `include_inactive` | `1` \| `true` | Include historically unassigned creators | | `limit` | number | Default 50, max 100 | | `cursor` | string | Pagination cursor | --- ### POST /api/v2/payout-structures/{id}/assignments Bulk-assign creators. Returns counts of newly assigned, already-assigned, and not-in-project IDs. ```bash curl -X POST \ -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -d '{"creator_ids": ["", ""]}' \ https://www.trackagoat.com/api/v2/payout-structures//assignments ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `creator_ids` | uuid[] | ✓ | Creators to assign | | `effective_at` | ISO 8601 datetime | | When earning begins. Defaults to now. Pass a future period boundary to start on a later period. Cannot be earlier than the start of the period currently in progress (400); earning cannot be backdated into periods that have already closed. | | `first_period_mode` | `prorate` \| `full` | | How the period containing `effective_at` is treated. Omitted inherits the structure's `proration_mode`. Ignored when `effective_at` is already a period boundary, since the period is whole either way. | `first_period_mode` means different things by criteria type. For `recurring` and `goals` it selects how the amount is prorated. For `cpm` there is no amount to prorate, so it moves the start of the window instead: `prorate` counts only videos posted, and views gained, at or after `effective_at`; `full` counts every video posted since the period started. That start holds for every later period. ```bash # Start on next Monday, taking that period whole curl -X POST \ -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -d '{ "creator_ids": [""], "effective_at": "2026-08-24T05:00:00Z", "first_period_mode": "full" }' \ https://www.trackagoat.com/api/v2/payout-structures//assignments ``` Returns `{ assigned, already_assigned, not_in_project, results }`. --- ### DELETE /api/v2/payout-structures/{id}/assignments Bulk-unassign creators. Existing earned accruals remain payable. ```bash curl -X DELETE \ -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -d '{"creator_ids": [""]}' \ https://www.trackagoat.com/api/v2/payout-structures//assignments ``` --- ## Payout Accruals Accruals are computed by an hourly background job: they cannot be created via the API. ### GET /api/v2/payout-accruals List accruals with optional filters. ```bash curl -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/payout-accruals?project_id=&status=earned" ``` **Query parameters** | Parameter | Type | Description | |-----------|------|-------------| | `project_id` | uuid | Filter to a project | | `creator_id` | uuid | Filter to a creator | | `structure_id` | uuid | Filter to a structure | | `status` | `earned` \| `paid` \| `rejected` \| `voided` | Filter by status | | `from` | YYYY-MM-DD | Period start on or after this date | | `to` | YYYY-MM-DD | Period end on or before this date | | `limit` | number | Default 50, max 100 | | `cursor` | string | Pagination cursor (`period_end_at` ISO) | --- ### GET /api/v2/payout-accruals/{id} Fetch a single accrual including `inputs_snapshot`. For a CPM accrual, `inputs_snapshot.video_gains` lists each video that earned in the period as `{ video_id, views_start, views_end, gained }`: the view count it was already paid up to, the count this accrual paid up to, and the difference. The next period pays from `views_end`. Rejecting or voiding a CPM accrual does not hand its views to a later period. Accruals calculated before 2026-10-09 have `source: "posts_in_period"` and no `video_gains`. --- ### PATCH /api/v2/payout-accruals/{id} Manually reject or void an earned accrual. ```bash curl -X PATCH \ -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -d '{"status": "rejected"}' \ https://www.trackagoat.com/api/v2/payout-accruals/ ``` **Body:** `{ status: "rejected" | "voided" }` Returns `409 cannot_set_status_on_paid_accrual` if the accrual is already paid: void the parent payment first. --- ## Payout Payments ### GET /api/v2/payout-payments List payments with optional filters. ```bash curl -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/payout-payments?project_id=&status=paid" ``` **Query parameters** | Parameter | Type | Description | |-----------|------|-------------| | `project_id` | uuid | Filter to a project | | `creator_id` | uuid | Filter to a creator | | `status` | `paid` \| `rejected` \| `voided` | Filter by status | | `from` | YYYY-MM-DD | Paid at or after this date | | `to` | YYYY-MM-DD | Paid at or before this date | | `limit` | number | Default 50, max 100 | | `cursor` | string | Pagination cursor (`paid_at` ISO) | Response rows include `payout_payment_lines[]` (linked accruals) and `payout_adjustments[]` (over/underpayment lines). --- > **Warning: Idempotency key required** > > `POST /api/v2/payout-payments` **requires** an `Idempotency-Key` header (a UUID, max 128 characters). A request without one is rejected with **400 `idempotency_key_required`**. If a request times out, retry with the same key: the server returns the original payment (**200**, `meta.idempotent_replay: true`, `Idempotency-Replay: true` header) and never records it twice. Keys are permanent per organization, so generate a fresh one for each new payment. ### POST /api/v2/payout-payments Record a payment. Requires an `Idempotency-Key` header. ```bash # Settle all outstanding accruals for a creator curl -X POST \ -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "project_id": "", "creator_id": "", "method_id": "", "total_amount_cents": 15000, "currency": "USD", "auto_apply_outstanding": true, "note": "April batch payment" }' \ https://www.trackagoat.com/api/v2/payout-payments # Ad-hoc payment with no linked accruals curl -X POST \ -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "project_id": "", "creator_id": "", "total_amount_cents": 5000, "note": "Bonus for viral video" }' \ https://www.trackagoat.com/api/v2/payout-payments ``` **Body** | Field | Type | Required | Description | |-------|------|----------|-------------| | `project_id` | uuid | ✓ | Target project | | `creator_id` | uuid \| null | | Creator receiving payment (null for true ad-hoc). Required when `accrual_ids` or `auto_apply_outstanding` is set | | `method_id` | uuid \| null | | Payout method used | | `total_amount_cents` | number | ✓ | Total paid in cents (≥ 0) | | `currency` | string | | ISO 4217, default `USD` | | `tracking_url` | string \| null | | External receipt link | | `note` | string \| null | | Free-form note | | `paid_at` | ISO 8601 | | When the payment was sent. Defaults to now; may be backdated to record a historical payment, but cannot be in the future | | `accrual_ids` | uuid[] | | Explicit accruals to settle (takes precedence over `auto_apply_outstanding`). Every accrual must belong to `creator_id`, otherwise **409 `accrual_creator_mismatch`** | | `supplemental_ids` | uuid[] | | Owed [supplemental amounts](#supplemental-amounts) to settle with this payment. They count toward the amount the payment applies, like accruals. Each must belong to `creator_id` (**409 `supplemental_creator_mismatch`**) and be `owed` in this project and currency (**409 `supplemental_not_settleable`**) | | `auto_apply_outstanding` | boolean | | Settles all of the creator's `earned` accruals and `owed` supplemental amounts in this project and `currency` (when `accrual_ids` and `supplemental_ids` are empty). Requires `creator_id`. Returns **409 `nothing_outstanding`** and records nothing when there are none | | `adjustment_reason` | string \| null | | Required when total differs from accrual sum | | `metadata` | object | | Agent-writable data | | `readme` | string \| null | | Markdown notes | | `payment_id` | uuid | | Your own id for the payment. Required with `attachment_path`, because the receipt is uploaded under it before the payment exists. Returns **409 `payment_id_in_use`** if another payment has it | | `attachment_path` | string | | A receipt's `path` from [Payment receipts](#payment-receipts). Must be in this `payment_id`'s folder, otherwise **400 `invalid_attachment_path`** | Returns `201` with the payment row, or `200` with the original payment when the `Idempotency-Key` was already used. --- ### GET /api/v2/payout-payments/{id} Fetch a payment including lines, adjustments and the supplemental amounts it settled (`payout_supplemental_amounts`). --- ### Send a payout through a connected provider `POST /api/v2/payout-payments/send` pays a creator through the organization's connected provider ([Dots](https://www.trackagoat.com/docs/guides/integrated-payouts.md)) and records the payment. `POST /api/v2/payout-payments` only records a payment made somewhere else; this endpoint moves money. It works only when all of these hold, otherwise nothing is sent: - the key has the `write` scope and acts as an **org admin** (`403 insufficient_role`); - an org admin turned on **Agent payments** in Org settings → Payment providers (`403 agent_payments_disabled`); - the amount fits the org's agent limits (`403 agent_cap_exceeded`, with `meta.cap` = `per_payment` or `daily` and `meta.limit_cents`); - the creator is linked to a payee (`409 payee_not_linked`; check with [`GET /creators/{id}/payee-links`](#get-apiv2creatorsidpayee-links)); - an `Idempotency-Key` header is sent. Reusing it returns the original payment and never pays twice. ```bash curl -X POST \ -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "project_id": "", "creator_id": "", "auto_apply_outstanding": true, "provider_options": { "memo": "September CPM payout" } }' \ https://www.trackagoat.com/api/v2/payout-payments/send ``` **Body** | Field | Type | Required | Description | |-------|------|----------|-------------| | `project_id` | uuid | ✓ | Project the accruals belong to | | `creator_id` | uuid | ✓ | Creator to pay | | `provider` | string | | `dots` (default) | | `accrual_ids` | uuid[] | | Earned accruals to pay | | `supplemental_ids` | uuid[] | | Owed supplemental amounts to pay with them | | `auto_apply_outstanding` | boolean | | Pay everything the creator is owed in this project and currency: earned accruals and owed supplemental amounts. `409 nothing_outstanding` if there is nothing | | `total_amount_cents` | integer | | Amount to send (≥ 1). Defaults to the total of what is being paid | | `adjustment_reason` | string | | Required when `total_amount_cents` differs from the accruals' total | | `currency` | string | | `USD` (Dots pays USD only) | | `note` | string | | Internal note on the trackagoat payment | | `provider_options` | object | | Provider options, below | **Dots `provider_options`** | Field | Type | Description | |-------|------|-------------| | `memo` | string (≤ 500) | Note shown to the creator with the payout | | `message` | string (≤ 300) | Text of the SMS Dots sends with a claim link (creators linked by phone) | | `description` | string (≤ 500) | Description saved on the payout in Dots | | `tax_exempt` | boolean | Leave this payout out of the creator's 1099 | Unknown fields are rejected with `400 invalid_provider_options`. **Response.** `201` with the payment row. Provider fields: | Field | Description | |-------|-------------| | `provider` | `dots` | | `provider_status` | `completed`, `awaiting_claim` (creator was texted a claim link), `processing`, `unconfirmed` (Dots didn't answer in time; trackagoat keeps checking: don't send again with a new key), `failed` or `canceled` | | `provider_payment_id` | Dots payout link id | | `provider_error_code`, `provider_error_message` | Why a payout failed, or a note such as "Waiting for funds in your Dots balance" | | `provider_options` | The options sent | | `initiated_via` | `ui`, `api` or `mcp` | When Dots refuses the payout, the response is `422 provider_send_failed` with the payment in `data`: it is already voided, so its accruals are owed again, and `provider_error_code` says why (for example `transaction-insufficient-funds`). Retrying needs a new `Idempotency-Key`. `failed` and `canceled` payments are always voided. Read the status later with `GET /api/v2/payout-payments/{id}`. A payment that is `awaiting_claim` or still in flight can't be voided (`409 provider_cancel_required` / `409 provider_in_flight`); cancel an unclaimed payout from the creator's Payouts tab. --- ### GET /api/v2/creators/{id}/payee-links Read scope and an org admin (`403 insufficient_role` otherwise), like the creator's payout account in the app. Whether a creator can be paid through each connected provider, and who they are paid as (phone masked). `meta` carries the org's agent-payment switch and limits. ```json { "data": [ { "provider": "dots", "provider_name": "Dots", "environment": "production", "connection_status": "active", "linked": true, "payee": { "provider_payee_id": "a1b2…", "name": "Sarah Brown", "phone": "+1 ••• 4567", "source": "claimed", "linked_at": "2026-10-09T18:22:04Z" } } ], "meta": { "agent_payments_enabled": true, "agent_payment_max_cents": 50000, "agent_payment_daily_max_cents": 200000 } } ``` --- ### PATCH /api/v2/payout-payments/{id} Void a payment or update metadata/readme. ```bash # Void: reverts linked accruals to "earned" and settled supplemental amounts to "owed" curl -X PATCH \ -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -d '{"status": "voided"}' \ https://www.trackagoat.com/api/v2/payout-payments/ ``` **Body:** `{ status?: "voided", metadata?: object, readme?: string | null }` (at least one field required). A receipt can only be attached when the payment is recorded, as in the app. --- ## Payouts tracker ### GET /api/v2/payouts/tracker Per-creator balances for a project, as on the app's Payouts page. Query: `project_id` (required), `creator_ids` (comma-separated), `structure_id`, `campaign_id`, `from`, `to` (YYYY-MM-DD, org time zone), `outstanding_only` (default `false`), `include_removed` (default `false`), `limit` (1 to 200, default 50), `cursor` (`meta.nextCursor`). Largest outstanding first. Each row has `total_earned_cents`, `total_paid_cents`, `adjustment_cents` (signed: the part of paid that settled no accrual), `adjustment_breakdown` (`[{ reason, amount_cents, count }]`, largest first), `outstanding_cents`, `last_paid_at`, `is_removed` and counts. `meta.totals` has `earned_cents`, `paid_cents`, `adjustment_cents`, `adjustment_breakdown`, `outstanding_cents`, `creator_count_outstanding` and the removed-creator summary (`removed_creator_count_outstanding`, `removed_outstanding_cents`). Without `from`/`to`, every row and the totals satisfy `earned − paid + adjustments = outstanding`. MCP: `get_payouts_tracker`. --- ## Supplemental amounts A supplemental amount is an owed correction on top of a **paid** accrual, for example posts found after the period was paid. The accrual and its payment are never changed. The amount is `owed`, counts in the creator's outstanding balance, and is settled by [recording a payment](#post-apiv2payout-payments) (`supplemental_ids` or `auto_apply_outstanding`). MCP: `list_payout_supplementals`, `create_payout_supplemental`, `void_payout_supplemental`. ### GET /api/v2/payout-supplementals Query: `project_id`, `creator_id`, `structure_id`, `accrual_id`, `status` (`owed` \| `paid` \| `void`), `cursor`, `limit` (1 to 100, default 50). Newest first. ### POST /api/v2/payout-supplementals Requires the `write` scope. Send an `Idempotency-Key` header: a retry with the same key returns the original row. ```bash curl -X POST \ -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 6f1d0c1e-…" \ -d '{"project_id": "", "accrual_id": "", "amount_cents": 1333, "reason": "Posts found after the week was paid"}' \ https://www.trackagoat.com/api/v2/payout-supplementals ``` | Field | Type | Required | Description | |-------|------|----------|-------------| | `project_id` | uuid | ✓ | Project | | `accrual_id` | uuid | | The **paid** accrual this corrects; its creator, structure and period are used. An unpaid accrual returns **409 `accrual_not_paid`** | | `creator_id`, `structure_id`, `period_start_at`, `period_end_at` | | | Instead of `accrual_id`: all four are required | | `amount_cents` | integer | ✓ | Amount owed, ≥ 1 | | `currency` | string | | Default `USD`; must match the accrual | | `reason` | string | ✓ | 1 to 500 characters; shown as "Supplemental: <reason>" | Returns `201` with the row (`status: "owed"`). A removed creator returns **404 `not_found`**: restore the creator first. ### GET /api/v2/payout-supplementals/{id} Fetch one supplemental amount. ### PATCH /api/v2/payout-supplementals/{id} Void an owed amount: `{ "status": "void", "reason"?: string }`. A paid one returns **409 `supplemental_paid`**: void the payment that settled it instead (it becomes owed again). --- ### Payment receipts A receipt is a file attached to a payment: PNG, JPEG, WebP, PDF, CSV, Word or Excel, up to 5 MB, stored privately. Generate a `payment_id` UUID first, upload the receipt under it, then record the payment with the same `payment_id` and `attachment_path`. **Inline, up to 3 MB.** `POST /api/v2/payouts/payment-receipts`: ```bash PAYMENT_ID=$(uuidgen | tr 'A-Z' 'a-z') curl -X POST \ -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -d "{\"payment_id\": \"$PAYMENT_ID\", \"project_id\": \"\", \"filename\": \"receipt.pdf\", \"content_type\": \"application/pdf\", \"content_base64\": \"$(base64 < receipt.pdf | tr -d '\n')\"}" \ https://www.trackagoat.com/api/v2/payouts/payment-receipts ``` Returns `201` with `{ path, content_type, size_bytes }`. The file's contents must match `content_type` (`content_type_mismatch`); larger files get `413 file_too_large`. **Signed URL, up to 5 MB.** `POST /api/v2/payouts/payment-receipts/upload-url` with `{ payment_id, project_id, filename, content_type }` returns `signed_url`, `path`, `max_bytes` and `expires_in_seconds` (2 hours). PUT the raw file to `signed_url` with the same `Content-Type`: ```bash curl -X PUT -H "Content-Type: application/pdf" --data-binary @receipt.pdf "" ``` Then record the payment with `"payment_id": "$PAYMENT_ID"` and `"attachment_path": ""`. --- ## Error codes | Code | HTTP | Description | |------|------|-------------| | `invalid_api_key` | 401 | Missing, expired, or invalid Bearer token | | `insufficient_scope` | 403 | Write attempted with a key that lacks the `write` scope | | `rate_limit_exceeded` | 429 | Daily API quota or per-minute (60 req/min) rate limit hit | | `limit_reached` | 402 | Org plan limit for methods or structures | | `invalid_body` | 400 | Zod validation failed: see `meta.errors` | | `invalid_query` | 400 | Bad query parameter | | `default_method` | 409 | Cannot delete a seeded default method | | `conflict_name` | 409 | Method name already exists in org | | `structure_archived` | 409 | Cannot assign creators to an archived structure | | `has_accruals` | 409 | Cannot hard-delete a structure with existing accruals | | `cannot_set_paid_directly` | 400 | Use POST /payout-payments instead | | `cannot_set_status_on_paid_accrual` | 409 | Void the parent payment first | | `currency_mismatch` | 400 | Payment currency doesn't match accrual currency | | `accrual_not_settleable` | 409 | Accrual is wrong project, status, or currency | | `accrual_creator_mismatch` | 409 | An accrual belongs to a different creator than the payment's `creator_id` | | `nothing_outstanding` | 409 | `auto_apply_outstanding` found no earned accruals; nothing was recorded | | `idempotency_key_required` | 400 | `POST /payout-payments` was sent without an `Idempotency-Key` header | | `idempotency_conflict` | 409 | `Idempotency-Key` already used for a payment in another project | | `structure_shape_locked` | 409 | Rule edit changes `period` or `criteria_type` on a structure that has accruals or assignments: create a new structure instead | | `adjustment_reason_required` | 400 | Total differs from accrual sum: provide `adjustment_reason` | | `already_voided` | 400 | Payment is already voided | | `accrual_in_other_payment` | 409 | Accrual has been re-paid: void that payment first | | `payment_id_in_use` | 409 | `payment_id` already belongs to another payment | | `invalid_attachment_path` | 400 | `attachment_path` isn't a receipt uploaded for this `payment_id` | | `invalid_mime_type` | 400 | Upload type not allowed (see `meta.allowed`) | | `content_type_mismatch` | 400 | Uploaded file's contents don't match `content_type` | | `invalid_base64` | 400 | `content_base64` isn't valid base64 | | `file_too_large` | 413 | Inline upload over 3 MB (receipts) or 1 MB (icons) | | `agent_payments_disabled` | 403 | An org admin hasn't turned on agent payments | | `agent_cap_exceeded` | 403 | The payout is over the org's per-payout or 24-hour agent limit | | `insufficient_role` | 403 | The key acts as a member who isn't an org admin: payout setup, recording or sending payments, method icons and payout accounts need one | | `provider_not_connected` | 409 | No payment provider is connected | | `provider_connection_error` | 409 | The provider rejected the stored keys; an admin must update them | | `payee_not_linked` | 409 | The creator isn't linked to a payee | | `invalid_provider_options` | 400 | `provider_options` has an unknown or too-long field | | `provider_send_failed` | 422 | The provider refused the payout; the payment was voided | | `provider_cancel_required` | 409 | The payout is awaiting claim: cancel it instead of voiding | | `provider_in_flight` | 409 | The provider hasn't settled the payout yet | | `not_found` | 404 | Resource not found or not in org | --- Source: https://www.trackagoat.com/docs/api-reference/payouts --- # Conversions > Define conversion event types per project and record conversions attributed to a creator, account, or video via the trackagoat v2 API. Trackagoat tracks creator **output** (views) and creator **cost** (payouts). Conversions add creator **outcome**: signups, purchases, installs, or whatever else you count as a result. There are two resources: - **Conversion events**, the *definitions*, scoped to a project. `signup`, `purchase`, `trial_started`. Each carries a default value and a currency. - **Conversions**: the *facts*. Each one references a conversion event and is attributed to a creator, an account, or a video. All endpoints require a Bearer API key. Responses follow the standard `{ data, error, meta }` envelope. Money is in integer cents. Timestamps are ISO 8601. > **Warning: Scopes** > > `GET` needs the `read` scope. `POST` and `PATCH` need `write`. `DELETE` needs `write` for the default (soft) mode and **`admin`** for `?mode=hard`: conversions are financial records, and a write-scoped webhook key should not be able to erase revenue history. A key without the required scope is rejected with **403 `insufficient_scope`**. --- ## Conversion Events ### GET /api/v2/projects/{projectId}/conversion-events List the conversion event definitions in a project. Active events only by default. ```bash curl -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/projects//conversion-events" # Include deactivated events curl -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/projects//conversion-events?include_inactive=true" ``` **Query parameters** | Parameter | Type | Description | |-----------|------|-------------| | `include_inactive` | `true` | Include events with `is_active: false` | Returns an array sorted newest first, with `meta.count`. **Response fields** | Field | Type | Description | |-------|------|-------------| | `id` | uuid | Conversion event ID | | `project_id` | uuid | Owning project | | `org_id` | uuid | Owning organization | | `key` | string | Stable slug used by API callers (e.g. `purchase`) | | `name` | string | Human label (e.g. `Purchase`) | | `description` | string \| null | Free-form description | | `default_value_cents` | number | Value applied to a conversion that does not supply its own | | `currency` | string | ISO 4217 code. Applies to every conversion recorded under this event | | `is_active` | boolean | Inactive events reject new conversions | | `metadata` | object | Agent-writable structured data | | `created_at` | ISO 8601 | | | `updated_at` | ISO 8601 | | --- ### POST /api/v2/projects/{projectId}/conversion-events Define a new conversion event. Requires the `write` scope. ```bash curl -X POST \ -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "key": "purchase", "name": "Purchase", "description": "A completed checkout attributed to a creator link", "default_value_cents": 0, "currency": "USD" }' \ https://www.trackagoat.com/api/v2/projects//conversion-events ``` **Body** | Field | Type | Required | Description | |-------|------|----------|-------------| | `key` | string | ✓ | Lowercase slug, max 63 chars. Must start with a letter or digit and may contain letters, digits, `.`, `-`, and `_`. Unique per project (case-insensitive) | | `name` | string | ✓ | Display name, max 100 chars | | `description` | string | | Max 500 chars | | `default_value_cents` | number | | Integer ≥ 0. Default `0` | | `currency` | string | | 3-letter ISO 4217 code, uppercased. Default `USD` | | `metadata` | object | | Agent-writable structured data. Default `{}` | Returns `201` with the created event. > **Note: Currency is fixed at definition time** > > A conversion event's `currency` cannot be changed later, and individual conversions have no currency field of their own. They inherit the event's. One event type means one currency, which is what makes `SUM(total_value_cents)` well-defined for any set of conversions. If you need to record purchases in EUR as well as USD, define two events (`purchase_usd`, `purchase_eur`). Reusing an existing `key` in the same project returns **409 `conversion_event_exists`**. Exceeding the plan's `max_conversion_events_per_project` returns **402 `limit_reached`**. See [Limits](#limits). The `key` is unique across *all* events in the project, deactivated ones included, so the 409 also fires when the clashing event is deactivated and therefore invisible in the app's event pickers. The response names it so you can reactivate rather than guess: ```json { "data": null, "error": "A conversion event with key \"trial_started\" already exists in this project but is deactivated, so it is hidden from event pickers. Reactivate it with PATCH .../conversion-events/ {\"is_active\": true}. Note that DELETE deactivates an event unless you pass ?mode=hard.", "meta": { "code": "conversion_event_exists", "conversion_event_id": "", "is_active": false } } ``` > **Warning: Test suites that create and delete events** > > `DELETE` deactivates unless you pass `?mode=hard`, so a create → exercise → delete loop leaves the key taken and 409s on its second run. Either tear down with `?mode=hard` (needs the `admin` scope, and only works while the event has no conversions), or create the events once and reuse them. --- ### GET /api/v2/projects/{projectId}/conversion-events/{eventId} Fetch a single event. Returns **404 `conversion_event_not_found`** if the event does not exist in that project. --- ### PATCH /api/v2/projects/{projectId}/conversion-events/{eventId} Update an event. Requires the `write` scope. At least one field must be supplied. ```bash curl -X PATCH \ -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -d '{"default_value_cents": 2500}' \ https://www.trackagoat.com/api/v2/projects//conversion-events/ ``` **Body:** `name?`, `description?`, `default_value_cents?`, `is_active?`, `metadata?` `key` and `currency` are deliberately **not** updatable. The key is the stable identifier you hard-code into your backend; changing the currency would retroactively reinterpret every conversion already recorded under the event. Changing `default_value_cents` affects only conversions recorded *after* the change. The value is copied onto each conversion at write time. --- ### DELETE /api/v2/projects/{projectId}/conversion-events/{eventId} Deactivates by default. Requires the `write` scope (`admin` for `?mode=hard`). ```bash # Deactivate: the event stops accepting new conversions, history is preserved curl -X DELETE -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/projects//conversion-events/" # Hard delete: removes the definition AND every conversion recorded under it curl -X DELETE -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/projects//conversion-events/?mode=hard" ``` | Parameter | Type | Description | |-----------|------|-------------| | `mode` | `hard` | Anything else (including omitting it) deactivates | Returns `{ id, mode }` with `meta.conversions_affected`: the number of conversions currently attached to the event. Check that number before hard-deleting. > **Important: Hard delete cascades** > > `?mode=hard` deletes the event row, and conversions cascade with it. That is revenue history and it cannot be recovered. Deactivating (`is_active: false`) is almost always what you want, because the event stops accepting new conversions while every past conversion stays queryable. Deactivating also frees a slot against `max_conversion_events_per_project`, which counts active events only. --- ## Conversions ### GET /api/v2/projects/{projectId}/conversions List conversions in a project, newest `occurred_at` first. ```bash # All conversions rolled up to one creator, in a date window curl -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/projects//conversions?creator_id=&from=2026-07-01T00:00:00Z" ``` **Query parameters** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `conversion_event_id` | uuid | | Only conversions of this event type | | `creator_id` | uuid | | Rollup filter. See below | | `account_id` | uuid | | Rollup filter. See below | | `video_id` | uuid | | Only conversions attributed to this video | | `source` | string | | Exact match on the `source` field | | `status` | `recorded` \| `voided` \| `all` | `recorded` | Voided conversions are hidden unless you ask for them | | `from` | ISO 8601 **with offset** | | `occurred_at` on or after this instant. Bare dates and offset-less timestamps are rejected. | | `to` | ISO 8601 **with offset** | | `occurred_at` on or before this instant. Bare dates and offset-less timestamps are rejected. | | `limit` | number | 50 | Items per page (max 200) | | `cursor` | string | | Pagination cursor from `meta.nextCursor` | > **Note: creator_id and account_id are rollup filters** > > `creator_id` and `account_id` match the **rolled-up** attribution, not just what the caller originally targeted. `?creator_id=X` returns every conversion recorded against creator X *plus* those recorded against X's accounts and X's videos. See [Rollup](#rollup) below. `from` and `to` are full ISO 8601 timestamps (`2026-07-01T00:00:00Z`), not bare dates. **Response** `meta` carries `{ hasMore, nextCursor }`. Cursors are opaque base64url strings. Pass `nextCursor` back verbatim as `cursor`, do not parse or modify it. **Conversion fields** | Field | Type | Description | |-------|------|-------------| | `id` | uuid | Conversion ID | | `conversion_event_id` | uuid | The event definition | | `conversion_event_key` | string \| null | The event's `key`, denormalized for convenience | | `conversion_event_name` | string \| null | The event's `name` | | `project_id` | uuid | Owning project | | `target_type` | `creator` \| `account` \| `video` | What the caller attributed to | | `creator_id` | uuid \| null | Always populated (rollup) | | `account_id` | uuid \| null | Populated for `account` and `video` targets | | `video_id` | uuid \| null | Populated for `video` targets only | | `value_cents` | number | Unit value. Falls back to the event's `default_value_cents` | | `quantity` | number | Units. Default `1` | | `total_value_cents` | number | `value_cents × quantity`, computed by the database | | `currency` | string | Inherited from the conversion event | | `external_id` | string \| null | Your own ID for this conversion (permanent dedupe key) | | `source` | string \| null | Free-form attribution source label | | `label` | string \| null | Short label, max 200 chars | | `note` | string \| null | Longer note, max 1000 chars | | `metadata` | object | Agent-writable structured data | | `status` | `recorded` \| `voided` | Voided conversions are excluded from totals | | `voided_at` | ISO 8601 \| null | | | `voided_reason` | string \| null | | | `occurred_at` | ISO 8601 **with offset** | When the conversion happened (defaults to receipt time). Must carry an explicit offset: `2026-07-21T14:03:00Z` or `+01:00`. A bare local timestamp is a 400. | | `created_at` | ISO 8601 | When trackagoat received it | --- ### POST /api/v2/projects/{projectId}/conversions Record one conversion, or a batch. Requires the `write` scope. ```bash # Single conversion, attributed by platform + handle curl -X POST \ -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "conversion_event_key": "purchase", "platform": "tiktok", "handle": "charlidamelio", "value_cents": 4999, "quantity": 2, "external_id": "order_10021", "occurred_at": "2026-07-21T14:03:00Z" }' \ https://www.trackagoat.com/api/v2/projects//conversions ``` ```bash # Batch (up to 500 per request) curl -X POST \ -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "conversions": [ {"conversion_event_key": "signup", "video_id": "", "external_id": "user_881"}, {"conversion_event_key": "signup", "creator_id": "", "external_id": "user_882"} ] }' \ https://www.trackagoat.com/api/v2/projects//conversions ``` **Body**: a single conversion object, or `{ "conversions": [ ... ] }` with 1 to 500 objects. Each object: | Field | Type | Required | Description | |-------|------|----------|-------------| | `conversion_event_id` | uuid | one of | The event definition, by ID | | `conversion_event_key` | string | one of | The event definition, by key (case-insensitive) | | `creator_id` | uuid | one of | Attribute to a creator | | `account_id` | uuid | one of | Attribute to a platform account | | `video_id` | uuid | one of | Attribute to a video | | `platform` + `handle` | string + string | one of | Attribute to an existing account by handle | | `value_cents` | number | | Integer ≥ 0. Defaults to the event's `default_value_cents` | | `quantity` | number | | Integer ≥ 1, max 1,000,000. Default `1` | | `external_id` | string | | Your own ID, max 255 chars. Permanent per-project dedupe key | | `source` | string | | Max 100 chars | | `label` | string | | Max 200 chars | | `note` | string | | Max 1000 chars | | `metadata` | object | | Default `{}` | | `occurred_at` | ISO 8601 | | When it happened. Defaults to now | Supply **exactly one** of `conversion_event_id` or `conversion_event_key`, and **exactly one** target: `creator_id`, `account_id`, `video_id`, or the `platform` + `handle` pair. Supplying none, or more than one, returns **400 `invalid_body`**. `platform` and `handle` must be supplied together. **Response.** A single-conversion request returns `201` with the created conversion. A batch returns `201` with an array in request order. `meta.deduped_count` reports how many rows in the request were deduplicated against an existing `external_id`. #### Choosing a target | You supply | `target_type` | Resolution | |-----------|---------------|------------| | `video_id` | `video` | Video must be in the project and have an owning account, else **409 `video_unattributed`** | | `account_id` | `account` | Account must be in the project and linked to a creator, else **409 `account_unlinked`** | | `creator_id` | `creator` | Creator must be in the project | | `platform` + `handle` | `account` | Matches an account already in the project | `platform` + `handle` matching is case-insensitive and a leading `@` is optional. It resolves **only** to an account that already exists in that project: a handle trackagoat does not know returns **404 `account_handle_not_found`** and **no account is created**. A typo in a webhook should surface as an error, not silently spawn a junk account. #### Rollup Attribution rolls **up** the hierarchy, and the rollup is written onto the row at insert time: ``` video → account → creator ``` A conversion recorded against a video also counts for that video's account and that account's creator. A conversion recorded against an account also counts for its creator. `target_type` preserves what you actually attributed to, so a report can still say "attributed to video X" while creator-level totals include it. That is why `?creator_id=X` on the list endpoint returns conversions recorded against X's accounts and videos too. #### Attribution is frozen The rollup is computed once, when the conversion is received. If a video is later reassigned to a different account, its past conversions **do not move by default**. They were earned under the ownership that existed when they happened, and silently rewriting them would make past reports non-reproducible. To override that for a single video, `POST /api/videos/{videoId}/conversions` with `{"mode": "reassign"}` rewrites every recorded conversion on that video to its current creator; `{"mode": "freeze"}` explicitly keeps history and dismisses the drift notice. Both require an **org admin** session. This endpoint is not part of the API-key surface. #### Value and quantity ``` total_value_cents = value_cents × quantity ``` `value_cents` falls back to the conversion event's `default_value_cents`, so a count-style event (a signup) needs no value on every call. `quantity` defaults to `1`. Currency comes from the event. A conversion has no currency field. --- ### Idempotency Two independent layers apply, and they answer different questions. **1. The `Idempotency-Key` header: replays the whole response for 24 hours.** Send a fresh UUID per logical request. If the call times out, retry with the *same* key and body: the server returns the original response body and status unchanged, with an `Idempotency-Replay: true` header, and creates nothing new. A retry that arrives while the first request is still running gets **409 `idempotency_in_progress`** with `Retry-After`. Reusing a key with a different body returns **422 `idempotency_key_reused`**, and against a different endpoint **409 `idempotency_conflict`**. Keys must be ≤ 255 characters. Rejections are replayed too: if a batch fails with a `404 conversion_event_not_found`, fix the body and send it with a **new** key. `5xx` responses are not stored, so a retry with the same key runs again. **2. The `external_id` field: permanent per-project dedupe.** `external_id` is unique per project, forever. If you POST a conversion whose `external_id` already exists in the project, trackagoat does not create a duplicate and does not error: it returns the **original** conversion's `id` with `deduped: true`. > **Tip: The case that trips people up** > > A retry with a **fresh** `Idempotency-Key` but a **repeated** `external_id` is not an error. The header cache misses (new key), so the handler runs, but the `external_id` collides, so nothing is inserted. You get **HTTP 200** with `deduped: true` and the **original** conversion's `id`. Treat that as success. The conversion is already recorded. > > (Status `200` rather than `201` applies to the single-conversion form, where the write created nothing. A batch always returns `201`; check each row's `deduped` flag and `meta.deduped_count`.) Send `external_id` whenever you have a natural key: an order ID, a user ID, a webhook event ID. It is the difference between "my retry logic is correct" and "my revenue numbers are correct". --- ### GET /api/v2/projects/{projectId}/conversions/{conversionId} Fetch a single conversion. Returns **404 `conversion_not_found`** if it does not exist in that project. --- ### DELETE /api/v2/projects/{projectId}/conversions/{conversionId} Two modes, because both are legitimately needed. ```bash # Void (default): soft, auditable, reversible in the sense that the row survives curl -X DELETE -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/projects//conversions/?reason=Refunded" # Hard delete: irreversible, requires the admin scope curl -X DELETE -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/projects//conversions/?mode=hard" ``` | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `mode` | `void` \| `hard` | `void` | `hard` requires the `admin` scope | | `reason` | string | | Max 500 chars. Stored as `voided_reason` (void mode only) | **`mode=void`** sets `status: 'voided'`, records `voided_at`, `voided_by`, and `voided_reason`, and excludes the conversion from every total. The row stays queryable via `?status=voided` or `?status=all`. This is the right answer for a refund, a chargeback, or a fraudulent order. Voiding an already-voided conversion is a **no-op**, not an error: safe to retry. Returns the conversion with `mode: "void"`. **`mode=hard`** deletes the row outright. For genuine mistakes and test data only. Requires the `admin` scope. Returns `{ id, mode: "hard", deleted: true }`. Hard delete is refused with **409 `conversion_has_accruals`** once the conversion has produced a payout accrual, because removing it at that point would silently change what a creator is owed. Void it instead: voiding reverses an unpaid accrual automatically, and flags an already-paid one for manual review rather than rewriting settled history. ```json { "data": null, "error": "This conversion has already produced a payout accrual. Void it with ?mode=void instead of deleting it.", "meta": { "code": "conversion_has_accruals", "grain": "every_conversion" } } ``` --- ## Limits `max_conversion_events_per_project` caps how many conversion event **types** a project may define. It counts active events only: deactivating one frees a slot. | Plan | Conversion events per project | |------|-------------------------------| | Free | 3 | | Starter | 25 | | Ultra | Unlimited | Conversion **volume** is not capped. Silently dropping a customer's revenue data at a cap would be worse than the revenue the cap protects. Exceeding the event-type limit returns **402 `limit_reached`** with `meta.current` and `meta.max`. --- ## Error codes | Code | HTTP | Description | |------|------|-------------| | `invalid_api_key` | 401 | Missing, expired, or invalid Bearer token | | `insufficient_scope` | 403 | Key lacks `read`, `write`, or (for `?mode=hard`) `admin` | | `limit_reached` | 402 | `max_conversion_events_per_project` exceeded | | `invalid_body` | 400 | Zod validation failed: including a missing or ambiguous target. See `meta.errors` | | `invalid_query` | 400 | Bad query parameter | | `invalid_cursor` | 400 | Cursor could not be decoded. Pass `meta.nextCursor` back verbatim | | `conversion_event_not_found` | 404 | No conversion event with that ID or key in the project | | `creator_not_found` | 404 | No creator with that ID in the project | | `account_not_found` | 404 | No account with that ID in the project | | `video_not_found` | 404 | No video with that ID in the project | | `account_handle_not_found` | 404 | No account with that `platform` + `handle` in the project. Add the account first | | `conversion_not_found` | 404 | No conversion with that ID in the project | | `conversion_event_inactive` | 409 | The event is deactivated and cannot accept new conversions | | `conversion_event_exists` | 409 | An event with that `key` already exists in the project. `meta.conversion_event_id` and `meta.is_active` identify it | | `account_unlinked` | 409 | The account is standalone (not attached to a creator), so nothing can roll up | | `video_unattributed` | 409 | The video has no owning account | | `idempotency_conflict` | 409 | `Idempotency-Key` reused against a different endpoint | | `idempotency_in_progress` | 409 | A request with this `Idempotency-Key` is still running. Retry after `Retry-After` seconds | | `idempotency_key_reused` | 422 | `Idempotency-Key` reused with a different request body. Use a new key | | `rate_limit_exceeded` | 429 | Daily API quota or per-minute rate limit hit | A duplicate conversion event `key` returns **409 `conversion_event_exists`**, naming the clashing event and whether it is merely deactivated. See [POST conversion-events](#post-apiv2projectsprojectidconversion-events). --- See the [Conversions guide](https://www.trackagoat.com/docs/guides/conversions.md) for a walkthrough of defining your first event and sending your first conversion. --- Source: https://www.trackagoat.com/docs/api-reference/conversions --- # Analytics > Unified time-series analytics endpoint: metrics, granularity, compare, and breakdown. ## GET /api/v2/analytics The canonical analytics endpoint. Returns bucketed time-series data for any combination of metric, entity, granularity, and mode. Suitable for agent/MCP consumption and chart rendering. ```bash curl -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/analytics?entity=creator&entity_id=&metric=views&mode=new&granularity=day&from=2026-01-01&to=2026-03-31" ``` The machine-readable OpenAPI 3.1 schema is available at `GET /api/v2/analytics/schema`. > **Note: Multi-platform aggregation** > > Trackagoat 2.0 creators own accounts across TikTok and Instagram. This unified endpoint aggregates a metric across **all** of an entity's platforms; it does not accept a `platform` filter. To scope a project-level view to a single platform, use the [aggregate endpoints](https://www.trackagoat.com/docs/api-reference/aggregate.md), which accept an optional `platform` query parameter. --- ## Query parameters | Parameter | Type | Default | Required | Description | |-----------|------|---------|----------|-------------| | `entity` | `video` \| `creator` \| `campaign` | None | Yes | Entity type to query | | `entity_id` | uuid | None | One of | Single entity. Mutually exclusive with `entity_ids`. | | `entity_ids` | CSV of uuids | None | One of | Up to 5 entities for side-by-side comparison. Cannot be combined with `breakdown`. | | `metric` | string | None | Yes | Metric ID. See [Metrics](#metrics) table. | | `mode` | `new` \| `cumulative` \| `rate` | metric default | No | Aggregation mode. Rate-only metrics always use `rate`. In `new` mode each video's first-day gain is measured against its last snapshot from before `from`; a video with no earlier snapshot (posted inside the range) counts from zero. | | `granularity` | `day` \| `week` \| `month` \| `year` \| `cumulative` \| `custom` | `day` | No | Time bucket granularity. `cumulative` returns a running total per day. `custom` requires `bucket_days`. | | `bucket_days` | integer 1 to 90 | None | If `custom` | Days per bucket when `granularity=custom`. | | `from` | YYYY-MM-DD | 30 days ago | No | Range start, inclusive. | | `to` | YYYY-MM-DD | today | No | Range end, inclusive. Clamped to today. | | `tz` | IANA timezone | org setting | No | Timezone for bucketing. Defaults to your org's `analytics_tz`. `from`, `to` and every `bucket_start` are calendar days in this timezone. | | `compare` | `none` \| `prior_period` | `none` | No | Include a shifted prior-period series in `compare_series`. | | `breakdown` | `none` \| `video` \| `creator` | `none` | No | Break down into per-constituent series. Top-N kept; remainder rolled into synthetic "Other". | | `top_n` | integer 1 to 50 | `10` | No | Max breakdown constituents before "Other" rollup. | | `formula` | `by_views` \| `by_followers` | None | No | Formula variant for `engagement_rate_*` metrics. | | `base_metric` | metric ID | None | If `growth_rate_pop` | Specifies which raw metric to compute period-over-period growth for. | | `format` | `json` \| `csv` | `json` | No | Response format. Use `csv` to download a flat CSV file instead of JSON. | --- ## Metrics | Metric ID | Unit | Modes | Entities | Notes | |-----------|------|-------|----------|-------| | `views` | `count` | `new`, `cumulative` | all | View count (summed across the entity's platforms) | | `likes` | `count` | `new`, `cumulative` | all | Like count | | `comments` | `count` | `new`, `cumulative` | all | Comment count | | `shares` | `count` | `new`, `cumulative` | all | Share count | | `saves` | `count` | `new`, `cumulative` | all | Save count | | `followers` | `count` | `new`, `cumulative` | creator | Follower count (not monotonic: real drops are valid) | | `video_count` | `count` | `new`, `cumulative` | creator, campaign | Total videos posted | | `engagement_rate_by_views` | `percent` | `rate` | all | `(likes+comments+shares+saves)/views×100` | | `engagement_rate_by_followers` | `percent` | `rate` | creator, campaign | `(likes+comments+shares+saves)/followers×100` | | `posts_per_week` | `per_week` | `rate` | creator, campaign | Posts on that day × 7 | | `avg_views_per_post` | `count` | `rate` | creator, campaign | Views gained / cumulative videos through that day | | `growth_rate_pop` | `percent` | `rate` | all | Period-over-period Δ%. Requires `base_metric=`. | --- ## Response shape ```json { "data": { "series": [ { "entity_ref": { "type": "creator", "id": "uuid", "label": "@handle" }, "metric": "views", "unit": "count", "mode": "new", "granularity": "day", "timezone": "America/New_York", "points": [ { "bucket_start": "2026-01-01", "value": 12400 }, { "bucket_start": "2026-01-02", "value": null } ] } ], "compare_series": [], "breakdown_series": [], "baselines": [ { "entity_id": "video-uuid", "snapshot_at": "2026-01-01T08:00:00Z", "reason": "tracking_started" } ], "annotations": [], "partial_bucket": { "bucket_start": "2026-04-01", "bucket_end": "2026-04-30", "as_of": "2026-04-20T14:30:00.000Z" }, "first_data_date": "2026-01-02", "tier_retention_days": null }, "error": null, "meta": { "cache_ttl_seconds": 300, "cached": false } } ``` ### Response fields | Field | Type | Description | |-------|------|-------------| | `series` | array | Primary metric series, one entry per entity. | | `compare_series` | array | Prior-period series when `compare=prior_period`. Bucket starts are shifted to align with the main period for easy chart overlay. | | `breakdown_series` | array | Per-constituent breakdown when `breakdown=video\|creator`. Last entry may be the synthetic "Other" rollup with `entity_ref.id = "__other__"`. | | `baselines` | array | Dates where tracking started or reset. Use as chart annotation markers to suppress spurious delta spikes. | | `annotations` | array | Time-ordered event markers (user-authored and auto-generated) within the requested date range. Empty array when no annotations exist. Pass `hide_auto=1` to suppress auto annotations. | | `partial_bucket` | object \| null | Describes the last in-progress bucket when the range ends on or after today. `bucket_end` is the final day of that bucket (e.g. last day of the month for `granularity=month`). | | `first_data_date` | string \| null | Earliest non-baseline date across all queried entities. Use for "All time" date presets. | | `tier_retention_days` | integer \| null | History window enforced by the org's plan tier. `null` means unlimited (Ultra). Use this to pre-check whether a date range is in-window before issuing a long-range query. Requests outside this window return HTTP 402 (`retention_exceeded`). | ### Series point fields | Field | Type | Description | |-------|------|-------------| | `bucket_start` | YYYY-MM-DD | Start date of the bucket. For `week`, this is Monday. For `month`, the first of the month. | | `value` | number \| null | Metric value. `null` indicates missing data or divide-by-zero (e.g. 0 views for a rate metric). | --- ## Common examples ### Multi-entity comparison ```bash curl -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/analytics?entity=creator&entity_ids=,,&metric=views&mode=new&granularity=week" ``` Returns three `series` entries, one per creator. Use `entity_ref.label` for chart legends. ### Prior-period comparison ```bash curl -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/analytics?entity=campaign&entity_id=&metric=views&compare=prior_period&from=2026-04-01&to=2026-04-20" ``` `compare_series` contains the prior period (2026-03-12 to 2026-04-01) with bucket starts shifted to match the main period. ### Campaign breakdown ```bash curl -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/analytics?entity=campaign&entity_id=&metric=views&breakdown=video&top_n=10" ``` `breakdown_series` returns up to 10 per-video series. If the campaign has more than 10 videos, the remainder is aggregated into an entry with `entity_ref.id = "__other__"` and `entity_ref.label = "Other"`. ### Custom bucket ```bash curl -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/analytics?entity=video&entity_id=&metric=views&mode=new&granularity=custom&bucket_days=7&from=2026-01-01&to=2026-03-31" ``` --- ## Error codes | Status | `meta.code` | Cause | |--------|-------------|-------| | 400 | `invalid_params` | Missing required params, unknown metric, invalid UUID, constraint violation (see `meta.issues`). | | 401 | `invalid_api_key` | Missing or invalid Bearer token. | | 404 | None | Entity not found or not accessible under your API key's org. | | 410 | `gone` | Removed endpoint (`/api/v2/creators/{id}/stats`, `/api/v2/campaigns/{id}/stats`). | | 400 | `invalid_params` | `breakdown != 'none'` requested for a rate-only metric (e.g. `engagement_rate_by_views`). Error path is `breakdown`; message: `"Breakdown is not supported for ''. Use a count-based metric (views, likes, etc.)"` | --- ## CSV export Pass `format=csv` to receive a flat CSV file instead of JSON. The response has `Content-Type: text/csv` and a `Content-Disposition: attachment` header with a generated filename. The same file is also available as [`GET /api/v2/exports/analytics`](https://www.trackagoat.com/docs/api-reference/exports.md) (with paging), which is what the MCP tool `export_csv` uses. ```bash curl -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/analytics?entity=creator&entity_id=&metric=views&from=2026-01-01&to=2026-03-31&format=csv" \ -o views.csv ``` ### CSV columns | Column | Description | |--------|-------------| | `series_kind` | `main`, `compare`, or `breakdown` | | `bucket_start` | YYYY-MM-DD start of the time bucket | | `entity_id` | UUID of the entity | | `entity_label` | Human-readable label (e.g. `@handle`) | | `metric` | Metric ID (e.g. `views`) | | `unit` | `count`, `percent`, or `per_week` | | `mode` | `new`, `cumulative`, or `rate` | | `value` | Numeric value, or empty string for null (divide-by-zero / missing data) | | `is_partial` | `true` if this bucket is still in progress | When `compare=prior_period` or `breakdown` is active, those series are appended after the main series rows, distinguished by the `series_kind` column. All other query parameters (granularity, mode, breakdown, compare) are respected exactly as for JSON. --- ## Campaign response freshness For `entity=campaign` queries, historical buckets (any day before today in the campaign's org timezone) are served from a pre-aggregated rollup table. The current day's partial bucket is always served from live data. The rollup is rebuilt nightly and updated within ~5 minutes of any campaign membership change. The response shape is identical whether the data came from the rollup or the live path: callers do not need to handle either case differently. ## Tier retention `tier_retention_days` in the response is populated from the caller's org tier (it was always `null` before Phase 6). Use it to pre-check whether a requested date range falls within the available history: ```javascript const { data } = await fetch('/api/v2/analytics?entity=creator&entity_id=&metric=views').then(r => r.json()); const maxDays = data.tier_retention_days; // e.g. 90 for Free, null for unlimited if (maxDays !== null) { const oldestAllowed = new Date(); oldestAllowed.setDate(oldestAllowed.getDate() - maxDays); // clamp your from date to oldestAllowed } ``` Server-side enforcement is active. Requests with `from` older than the retention window return HTTP 402 with `meta.code = 'retention_exceeded'`. --- ## Annotations in the response Every `/api/v2/analytics` response includes an `annotations` array. These are time-ordered event markers within the requested date range. ```json "annotations": [ { "id": "auto:creator_profile_change:abc:2026-03-15T10:00:00Z", "source": "auto", "kind": "creator_profile_change", "scope": "creator", "entity_id": "abc-uuid", "at": "2026-03-15T10:00:00Z", "title": "Profile bio changed", "body_md": null, "author_id": null, "author_name": null }, { "id": "550e8400-...", "source": "user", "kind": "user", "scope": "creator", "entity_id": "abc-uuid", "at": "2026-03-20T14:30:00Z", "title": "Launched new video series", "body_md": "## Notes\nStarted the weekly Q&A format.", "author_id": "user-uuid", "author_name": null } ] ``` Pass `hide_auto=1` to suppress auto-generated annotations and receive only user-authored ones. | Field | Type | Description | |-------|------|-------------| | `id` | string | Row UUID for user annotations; stable synthetic key for auto | | `source` | `user` \| `auto` | Who generated this annotation | | `kind` | string | Event category: see table below | | `scope` | `creator` \| `campaign` \| `video` | Entity type the annotation is attached to | | `entity_id` | uuid | Entity the annotation belongs to | | `at` | ISO datetime | Timestamp of the event | | `title` | string | Short label (max 200 chars) | | `body_md` | string \| null | Optional markdown body (max 8000 chars) | | `entity_label` | string? | Present when cascade-down resolved the annotation from a parent entity | ### Annotation kinds | Kind | Auto source | |------|-------------| | `user` | Manually authored | | `creator_profile_change` | `creator_events` with `event_category=profile_change` | | `creator_milestone` | `creator_events` with `event_category=milestone` | | `campaign_created` | `campaigns.created_at` | | `readme_changed` | `entity_events` with `event_type=readme_changed` | --- ## Annotation CRUD endpoints ### GET /api/v2/analytics/annotations List annotations for a specific entity within an optional date range. ```bash curl -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/analytics/annotations?scope=creator&entity_id=&from=2026-01-01&to=2026-04-01" ``` Query parameters: `scope` (required), `entity_id` (required), `from`, `to`. ### POST /api/v2/analytics/annotations Create a user-authored annotation. ```bash curl -X POST -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -d '{"scope":"creator","entity_id":"","annotation_at":"2026-03-20T14:30:00Z","title":"Launched series","body_md":"**Note:** weekly format"}' \ "https://www.trackagoat.com/api/v2/analytics/annotations" ``` Body fields: `scope`, `entity_id`, `annotation_at` (ISO datetime), `title` (1 to 200 chars), `body_md` (optional, ≤8000 chars). ### PATCH /api/v2/analytics/annotations/:id Update an existing user annotation. Partial updates: only send fields you want to change. ```bash curl -X PATCH -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -d '{"title":"Updated title"}' \ "https://www.trackagoat.com/api/v2/analytics/annotations/" ``` ### DELETE /api/v2/analytics/annotations/:id Delete an annotation permanently. ```bash curl -X DELETE -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/analytics/annotations/" ``` Returns `{"data":{"id":""},"error":null}` on success. Returns 404 if not found. --- ## Cache semantics Every JSON response includes: ```json "meta": { "cache_ttl_seconds": 300, "cached": true } ``` - `cache_ttl_seconds`: always 300 (5 minutes). This is the maximum TTL; tag-based invalidation can evict entries sooner. - `cached`: `true` when data was served from a warm cache hit; `false` on a cold resolver run (first request, or immediately after invalidation). **What triggers cache invalidation:** | Event | Tags evicted | |-------|--------------| | Scraper writes video stats | `analytics:org:{orgId}` | | Scraper updates creator profile | `analytics:org:{orgId}` | | Scraper rebuilds campaign rollup | `analytics:org:{orgId}:campaign:{campaignId}` | | Campaign item added or removed | `analytics:org:{orgId}:campaign:{campaignId}` | | Annotation created/updated/deleted | `analytics:org:{orgId}:{entityType}:{entityId}` | | Org `analytics_tz` changed | `analytics:org:{orgId}` (full org eviction) | For agents and automated consumers: treat `cached: false` as a signal that this is fresh data. A `cached: true` response is at most 5 minutes old and was likely invalidated-then-refetched at a real event boundary. --- ## MCP stability > **Tip: MCP-stable contract** > > The response shape of `/api/v2/analytics` is **MCP-stable** as of OpenAPI v1.4.0. Key names and types will not change without a major version bump. Additive fields will be introduced without breaking existing consumers. > > The machine-readable OpenAPI 3.1 schema is served at `GET /api/v2/analytics/schema` (version `1.4.0`). --- Source: https://www.trackagoat.com/docs/api-reference/analytics --- # Aggregate analytics > Project-level aggregate endpoints for leaderboards, heatmaps, histograms, scatter charts, and membership overlap. ## Overview The aggregate analytics endpoints return pre-computed project-level summaries that power the Analytics section (Video, Creator, and Campaign sub-tabs). They differ from the unified `/api/v2/analytics` endpoint in that they aggregate **across all entities in a project** rather than for a single entity. All endpoints: - Accept both **Bearer API key** and **session cookie** authentication - Return the standard `{ data, error, meta }` envelope - Require the caller to have access to the specified `project_id` - Are not cached (no Vercel Runtime Cache wrapper): responses are fresh on every request Base path: `/api/v2/analytics/aggregate` > **Note: What counts** > > Every aggregate counts **tracked posts** only: status `auto_tracked`, `included` or `direct_add`, tracking not stopped, from an active creator and an active account. Posts awaiting review are never counted. Date ranges are org-time-zone calendar days, `from` and `to` inclusive. Engagement rates are `(likes + comments + shares + saves) / views` summed over posts. Full definitions: [How metrics are calculated](https://www.trackagoat.com/docs/guides/metrics.md). > **Note: Multi-platform: the `platform` filter** > > Trackagoat 2.0 creators own accounts on TikTok and Instagram. The video- and creator-metric aggregates accept an optional `platform` (`tiktok` \| `instagram` \| `youtube`, where `youtube` is reserved and returns nothing) query parameter that scopes results to a single platform. It is supported on `summary`, `video-leaderboard`, `creator-leaderboard`, `account-leaderboard`, `creator-scatter`, `posting-heatmap`, `posting-consistency`, `view-distribution`, `outliers`, `over-posting-risks`, `campaign-leaderboard`, and `campaign-timeseries`. Omit it to aggregate across all platforms. `campaign-progress` (view targets cover every platform), `goal-compliance`, and `member-overlap` do not accept it. > **Note: Scoping to one campaign: the `campaign_id` filter** > > `video-leaderboard`, `posting-heatmap`, `view-distribution`, `outliers`, `over-posting-risks`, `account-leaderboard`, `campaign-leaderboard`, and `campaign-timeseries` accept an optional `campaign_id` (UUID). When set, video metrics are restricted to the videos the campaign resolves to: videos added directly, every video from its member creators, and anything reached through nested campaigns. The campaign must belong to `project_id`; an id from another project matches nothing (empty results, not an error). `account-leaderboard` additionally narrows its rows to accounts that own at least one video in the campaign. The response `meta` echoes `campaign_id` (or `null`). --- ## Totals ### GET `/aggregate/summary` Headline totals for a project, one campaign, or every campaign at once, over a date window. The dashboard, every Insights summary strip and Program Health read their headline numbers from this query, so the numbers match the app. **Query parameters** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `project_id` | UUID | required | Project to total | | `from` | YYYY-MM-DD | 29 days before `to` | Period start (inclusive, org time zone) | | `to` | YYYY-MM-DD | today (org time zone) | Period end (inclusive) | | `campaign_id` | UUID | None | Only this campaign (sub-campaigns and member creators included) | | `campaigns` | `all` | None | The distinct union of every campaign's videos, each video counted once (ignored with `campaign_id`) | | `platform` | `tiktok` \| `instagram` \| `youtube` | None | Only one platform | **Response `data`** ```json { "tracked_posts": 2246, "views": 10171040, "likes": 1711759, "comments": 6972, "shares": 26461, "saves": 33878, "engagement_rate": 17.49, "creators_posting": 85, "accounts_posting": 85, "roster_creators": 106, "roster_accounts": 110, "platforms": 2, "total_followers": 208181, "follower_growth": 3958, "pending_review_posts": 61, "posts_without_history": 177, "stats": { "posts": 2246, "within_48h": 1690, "never_scraped": 0, "oldest_at": "2026-09-11T10:30:30Z", "newest_at": "2026-10-09T19:31:00Z" } } ``` `views` is the current lifetime views of the tracked posts posted in the window (not views gained during it). `engagement_rate` is a percentage, `null` without views. `follower_growth` is measured per account (an account added mid-window counts from its first snapshot) and summed. `pending_review_posts` are posts in the window awaiting review; they are not in any other number. `stats` says how fresh the video stats behind the numbers are. `meta` echoes `from`, `to` and `timezone`. --- ## Video endpoints ### GET `/aggregate/video-leaderboard` Returns a ranked list of tracked videos in the project for the given date range. **Query parameters** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `project_id` | UUID | required | Project to query | | `from` | YYYY-MM-DD | None | Period start (inclusive) | | `to` | YYYY-MM-DD | None | Period end (inclusive) | | `order` | enum | `view_count` | Sort column: `view_count`, `like_count`, `comment_count`, `share_count`, `save_count`, `posted_at` | | `platform` | `tiktok` \| `instagram` \| `youtube` | None | Restrict to videos on a single platform | | `campaign_id` | UUID | None | Restrict to videos the campaign resolves to (must belong to `project_id`) | | `limit` | int | 100 | Max rows (1 to 200) | | `offset` | int | 0 | Pagination offset | **Response `data` array**: each item: ```json { "video_id": "uuid", "title": "string | null", "thumbnail_url": "string | null", "url": "string | null", "view_count": 12345, "like_count": 678, "comment_count": 45, "share_count": 12, "save_count": 89, "posted_at": "2026-04-01T12:00:00Z", "creator_id": "uuid", "handle": "string", "display_name": "string | null", "avatar_url": "string | null", "total_count": 87 } ``` `total_count` is the total matching rows (before `limit`/`offset`). `handle` is the creator's primary account handle. Pass `platform` to restrict the leaderboard to videos on one platform. --- ### GET `/aggregate/posting-heatmap` Returns a DOW × hour grid of post counts and average view counts, bucketed in the org's timezone. **Query parameters** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `project_id` | UUID | required | | | `from` | YYYY-MM-DD | required | | | `to` | YYYY-MM-DD | required | | | `tz` | IANA string | org timezone | Override timezone | | `platform` | `tiktok` \| `instagram` \| `youtube` | None | Restrict to posts on a single platform | **Response `data` array**: each item: ```json { "dow": 1, "hour": 14, "post_count": 5, "avg_views": 12430.50 } ``` `dow`: 0=Sunday … 6=Saturday. Only cells with at least one post are returned; missing cells = 0. --- ### GET `/aggregate/view-distribution` Returns a histogram of video view counts with percentile markers. **Query parameters** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `project_id` | UUID | required | | | `from` | YYYY-MM-DD | required | | | `to` | YYYY-MM-DD | required | | | `bin_count` | int | 20 | Number of histogram bins (5 to 50) | | `platform` | `tiktok` \| `instagram` \| `youtube` | None | Restrict to videos on a single platform | **Response `data` object:** ```json { "total": 87, "min": 100, "max": 5200000, "p50": 12000, "p75": 48000, "p90": 210000, "p99": 1800000, "bins": [ { "bin_index": 0, "bin_lower": 100, "bin_upper": 260250, "video_count": 61 }, ... ] } ``` --- ### GET `/aggregate/outliers` Returns videos that outperform their creator's median view count by at least `min_x_multiple`, sorted by `x_multiple` descending. **Query parameters** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `project_id` | UUID | required | | | `from` | YYYY-MM-DD | None | Optional period filter | | `to` | YYYY-MM-DD | None | Optional period filter | | `min_x_multiple` | number | 1.5 | Minimum outperformance ratio | | `platform` | `tiktok` \| `instagram` \| `youtube` | None | Restrict to videos on a single platform | | `limit` | int | 50 | Max rows (1 to 100) | **Response `data` array**: each item includes `video_id`, `creator_id`, `view_count`, `median_views`, `x_multiple`, `posted_at`, plus enriched `title`, `thumbnail_url`, `url`, `handle`, `display_name`, `avatar_url`. --- ## Creator endpoints ### GET `/aggregate/creator-leaderboard` Returns creators in the project ranked by views in the period, with follower delta and engagement metrics. **Query parameters** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `project_id` | UUID | required | | | `from` | YYYY-MM-DD | None | | | `to` | YYYY-MM-DD | None | | | `platform` | `tiktok` \| `instagram` \| `youtube` | None | Restrict to creators/videos on a single platform | | `limit` | int | 100 | Max rows (1 to 200) | | `offset` | int | 0 | | **Response `data` array**: each item: ```json { "creator_id": "uuid", "handle": "string", "display_name": "string | null", "avatar_url": "string | null", "current_followers": 45000, "follower_delta": 1200, "total_views": 980000, "posts_in_period": 14, "posts_per_week": 1.0, "engagement_rate": 3.45, "growth_rate": 2.74, "total_count": 7 } ``` `follower_delta` = follower count at end of period minus start of period (from `creator_stats_history`). Zero when no snapshots in range. `engagement_rate` = (likes + comments + shares) ÷ views × 100 for videos posted in the period. --- ### GET `/aggregate/posting-consistency` Returns per-creator, per-day post counts for building a punch-card matrix. **Query parameters** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `project_id` | UUID | required | | | `from` | YYYY-MM-DD | required | | | `to` | YYYY-MM-DD | required | | | `tz` | IANA string | org timezone | | | `platform` | `tiktok` \| `instagram` \| `youtube` | None | Restrict to posts on a single platform | **Response `data` array**: sparse (only days with posts): ```json { "creator_id": "uuid", "day": "2026-04-05", "post_count": 2 } ``` Every (creator, day) pair in the range is returned, ordered by creator then day. (Before 2026-10-08 the response was silently truncated at 1,000 rows.) --- ### GET `/aggregate/over-posting-risks` Flags creator/day combinations where a creator posted enough times in a single day that some videos were held back from tracking (suppressed) to control scrape cost. **Query parameters** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `project_id` | UUID | required | | | `from` | YYYY-MM-DD | required | | | `to` | YYYY-MM-DD | required | | | `tz` | IANA string | org timezone | Override timezone | | `platform` | `tiktok` \| `instagram` \| `youtube` | None | Restrict to posts on a single platform | **Response `data` array**: each item: ```json { "creator_handle": "string", "creator_id": "uuid", "post_date": "2026-04-05", "total_posts": 9, "suppressed_count": 3, "suppressed_video_ids": ["uuid", "uuid", "uuid"], "creator_last_scraped_at": "2026-04-06T02:00:00Z" } ``` `creator_last_scraped_at` is the most recent scrape across all of the creator's platform accounts (`null` if never scraped). --- ### GET `/aggregate/creator-scatter` Returns per-creator (follower change, engagement rate) pairs for scatter chart rendering. **Query parameters**: `project_id`, optional `from`/`to`, optional `platform` (`tiktok` \| `instagram` \| `youtube`) to restrict to a single platform. **Response `data` array**: ```json { "creator_id": "uuid", "handle": "string", "follower_delta": 1200, "engagement_rate": 3.45, "posts_count": 14 } ``` Only creators with at least one tracked post in the period are included. --- ### GET `/aggregate/goal-compliance` Returns a project-wide roll-up of creator goal statuses across every creator with active goals. **Query parameters**: `project_id`. **Response `data` object:** ```json { "on_track": 3, "at_risk": 1, "missed": 1, "total_goals": 5, "creators_with_goals": 5, "with_advisories": 4 } ``` `on_track` includes `completed` statuses. `at_risk` includes `behind_pace`. `with_advisories` is the count of goals where the `advisory.has_advisory` flag is true, i.e., goals whose displayed progress may not yet be final (period in progress, period recently closed, stale scrape, or goal not yet started). See the [Goal Compliance](https://www.trackagoat.com/docs/api-reference/goals.md) endpoint for per-goal advisory detail. --- ## Campaign endpoints ### GET `/aggregate/campaign-leaderboard` Returns campaigns in the project sorted by views, with prior-period delta comparison. **Query parameters**: `from`, `to` (optional; default the last 30 days including today, org time zone); `project_id` and/or `campaign_id`. Pass `project_id` alone for every campaign in the project, or `campaign_id` to return just that one campaign's row, in which case `project_id` is optional: it's resolved from the campaign (a campaign outside your org returns `404`). Add `platform` (`tiktok` \| `instagram` \| `youtube`) to count only that platform's posts, in both the current and the prior period. The response `meta` echoes the resolved `project_id` and `platform`. **Response `data` array:** ```json { "campaign_id": "uuid", "name": "string", "total_views": 1200000, "total_likes": 45000, "video_count": 32, "creator_count": 5, "preview_thumbnail_url": "string | null", "prior_views": 980000, "views_delta": 220000, "views_delta_pct": 22 } ``` Prior period is automatically computed as the same duration immediately preceding `from`. Each row counts its campaign's videos once; a video in two campaigns appears in both rows, so don't add rows up for a total: use [`/aggregate/summary`](#get-aggregatesummary) with `campaigns=all`. `creator_count` is the number of creators with a tracked post in the window. --- ### GET `/aggregate/campaign-timeseries` Returns the trend series behind the Campaign Insights charts: impressions, posts, distinct creators posting, and first-time creators per time bucket, zero-filled across the range. **Query parameters** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `project_id` | UUID | required | Project to query | | `campaign_id` | UUID | None | Scope to one campaign's resolved videos. Omit for every active tracked video in the project | | `from` | YYYY-MM-DD | required | Range start (inclusive, org-TZ calendar day) | | `to` | YYYY-MM-DD | required | Range end (inclusive) | | `granularity` | `day` \| `week` \| `month` \| `year` | `day` | Bucket width. Weeks start on Monday | | `tz` | IANA tz | None | Override the org timezone used for day boundaries | | `platform` | `tiktok` \| `instagram` \| `youtube` | None | Only count posts on this platform (every series, including first-time creators) | **Response `data` array**: one item per bucket, ordered by `bucket_start`: ```json { "bucket_start": "2026-08-10", "impressions": 456467, "posts": 436, "creators_posting": 39, "new_creators": 4 } ``` - `impressions` is the views gained during the bucket, attributed to the day the snapshot moved (not the video's posting day). A video's first snapshot (its pre-tracking views) is credited to its posting day when that day is inside the range. - `posts` and `creators_posting` are by posted-at date. - `new_creators` counts creators whose earliest post in the scope falls in the bucket; each creator counts once over all time, not once per range. - Impressions per post is not returned; compute `impressions / posts` per bucket client-side and treat buckets with `posts = 0` as undefined. `meta` echoes `from`, `to`, `tz`, `granularity`, and `campaign_id`. --- ### GET `/aggregate/member-overlap` Returns entities (creators or videos) that appear as direct members in campaigns, with the list of campaigns each belongs to. Capped at 50 rows. **Query parameters** | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `project_id` | UUID | required | | | `dimension` | `creator` \| `video` | `creator` | Entity type to list | **Response `data` array:** ```json { "entity_id": "uuid", "entity_label": "handle or title", "entity_avatar": "string | null", "campaign_ids": ["uuid", "uuid"], "campaign_names": ["Campaign A", "Campaign B"], "campaign_count": 2 } ``` Only **direct** membership (via `campaign_items`) is considered, not recursive resolution through nested campaigns. --- ## Conversion aggregates Three endpoints summarise recorded conversions. All of them ignore voided conversions entirely, because a void is a retraction and never appears in a total. > **Warning: One row per currency: never sum across them** > > Currency lives on the conversion event, not the conversion, so a project may > hold a USD event and a EUR event at once. Every conversion aggregate therefore > returns **one row per currency** rather than a single blended total. Render them > side by side; adding them together produces a number that means nothing. > > A single-currency project (the common case) simply gets one row. Unlike the [conversions list](https://www.trackagoat.com/docs/api-reference/conversions.md), which takes full ISO-8601 timestamps, these take bare `YYYY-MM-DD` dates like the other aggregates. `to` is **inclusive**, resolved against the organization's timezone. ### GET `/api/v2/analytics/aggregate/conversion-summary` | Parameter | Type | Default | Description | |-----------|------|---------|-------------| | `project_id` | UUID | required | | | `from` | `YYYY-MM-DD` | None | Inclusive start | | `to` | `YYYY-MM-DD` | None | Inclusive end | | `conversion_event_id` | UUID | None | Restrict to one event type | Returns zero rows when nothing was recorded in range: there is no correct currency to denominate an empty total in, so render an empty state rather than a zero. ```json { "total_conversions": 128, "total_quantity": 194, "total_value_cents": 486000, "currency": "USD", "distinct_creators": 12, "distinct_events": 3 } ``` ### GET `/api/v2/analytics/aggregate/conversion-leaderboard` Creators ranked by conversion value. Accepts the summary parameters plus `limit` and `offset`. Rows are per **(creator, currency)**, so a creator with conversions in two currencies appears twice. `total_count` counts groups, not creators. Totals include the full rollup: a creator's figure covers conversions recorded against their accounts and their individual videos, not just those attributed to the creator directly. ```json { "creator_id": "uuid", "display_name": "Jules", "handle": "wyndywithjuls", "avatar_url": "string | null", "conversion_count": 34, "total_quantity": 41, "total_value_cents": 128000, "currency": "USD", "total_count": 12 } ``` ### GET `/api/v2/analytics/aggregate/conversion-timeseries` Daily buckets in the organization's timezone, one row per **(day, currency)**. The series is **sparse**: days with no conversions are omitted, so fill the spine client-side. ```json { "day": "2026-07-18", "conversion_count": 9, "total_value_cents": 22500, "currency": "USD" } ``` --- Source: https://www.trackagoat.com/docs/api-reference/aggregate --- # CSV exports > Download the same CSV files as the Export buttons in the app. ## GET /api/v2/exports/{dataset} Returns one of the app's CSV downloads as a file. The columns, values and filters are the same as the matching **Export** button in the app, because the app's buttons download from this endpoint too. Money is in dollars with a Currency column, and dates are days in your organization's time zone. Over MCP the same files come from the [`export_csv` tool](https://www.trackagoat.com/docs/agents/mcp.md#exporting-csvs). Requires the `read` scope. Each call counts as one API request. ### Datasets | `dataset` | In the app | Filters | |-----------|------------|---------| | `creators` | Creators page | `search`, `compliance`, `disabled_only`, `include_compliance`, `campaign_id`, `platform` | | `videos` | Videos page | `status`, `creator_id`, `campaign_id`, `platform`, `from`, `to`, `search`, `sort_by`, `sort_dir` | | `campaigns` | Campaigns page | none | | `goals` | Goals page | `period`, `reference_date`, `campaign_id` | | `campaign_leaderboard` | Campaign Insights leaderboard | `from`, `to`, `campaign_id`, `platform` | | `payout_tracker` | Payouts, **Tracker summary CSV** | `creator_ids`, `structure_id`, `campaign_id`, `from`, `to`, `outstanding_only`, `include_removed` | | `payout_payments` | Payouts and a creator's Payouts tab, **Payments CSV** | `creator_ids`, `campaign_id`, `from`, `to` | | `payout_accruals` | Payouts and a creator's Payouts tab, **Accruals CSV** | `creator_ids`, `structure_id`, `campaign_id`, `from`, `to`, `outstanding_only`, `include_removed` | | `analytics` | Insights charts, **Download CSV** | The parameters of [`GET /api/v2/analytics`](https://www.trackagoat.com/docs/api-reference/analytics.md) except `format` | Every dataset except `analytics` needs `project_id`. A filter the dataset doesn't take is rejected with `400 invalid_params`, so a typo never silently exports the whole project. ### Filters | Parameter | Datasets | Description | |-----------|----------|-------------| | `project_id` | all but `analytics` | The project to export. | | `campaign_id` | most | Only the campaign's creators, videos or payouts. Sub-campaigns are included. | | `creator_id` | `videos` | Only this creator's videos. | | `creator_ids` | `payout_*` | Comma-separated creator ids. | | `platform` | `creators`, `videos`, `campaign_leaderboard` | `tiktok` or `instagram`. | | `from`, `to` | `videos`, `campaign_leaderboard`, `payout_*` | `YYYY-MM-DD`, days in your org's time zone, both inclusive. `videos` filters on the posted date. `campaign_leaderboard` defaults to the last 30 days. | | `status` | `videos` | A tracking status (`auto_tracked`, `direct_add`, `included`, `excluded`, `pending_review`) or `tracking_disabled`, `tracking_exempted`, `plan_limit_paused`, `expiring_soon`. Default: every status except `pending_review`. | | `search` | `creators`, `videos` | Creators: name or handle contains. Videos: title contains. | | `sort_by`, `sort_dir` | `videos` | Sort column (`posted_at`, `view_count`, `like_count`, `comment_count`, `share_count`, `save_count`, `duration_seconds`) and `asc` or `desc`. Default `posted_at`, `desc`. | | `compliance` | `creators` | Only creators whose worst goal status this period is `completed`, `on_track`, `behind_pace`, `at_risk`, `missed` or `no_goals`. | | `disabled_only` | `creators` | `true`: only auto-disabled creators. | | `include_compliance` | `creators` | `true`: fill the Compliance column without filtering on it. | | `period`, `reference_date` | `goals` | `daily`, `weekly` (default) or `monthly`, and any day in the period to export (default today). | | `structure_id` | `payout_tracker`, `payout_accruals` | Only this payout structure. | | `outstanding_only` | `payout_tracker`, `payout_accruals` | `true`: only creators still owed, or only unpaid accruals. Default `false`. | | `include_removed` | `payout_tracker`, `payout_accruals` | `true`: include removed creators. Default `false`. | ### Large exports Exports are complete: they're never cut off at a row limit. The whole file comes back by default. To get it in pages, pass `limit` (rows) and/or `max_bytes`, then call again with `offset` set to the `X-Next-Offset` header until that header is missing. Every page starts with the header row, and rows are never split. | Response header | Meaning | |-----------------|---------| | `Content-Disposition` | `attachment` with the suggested file name, e.g. `videos--2026-10-11.csv` | | `X-Total-Rows` | Rows matching the filters | | `X-Row-Count` | Rows in this response | | `X-Offset` | The first row in this response | | `X-Next-Offset` | Where the next page starts. Only present when more rows remain. | | `X-Columns` | Number of columns | ### Request ```bash curl -o sep-payouts.csv \ -H "Authorization: Bearer tga_..." \ "https://www.trackagoat.com/api/v2/exports/payout_tracker?project_id=&from=2026-09-01&to=2026-09-30" ``` ### Response `200` with `Content-Type: text/csv; charset=utf-8`: ```csv Handle,Display Name,Total Earned,Total Paid,Outstanding,Currency,Last Paid At,Outstanding Accruals,Active Structures,Is Removed,Outstanding Supplementals,Adjustments,Adjustment Reasons creator_one,Creator One,250.00,200.00,50.00,USD,2026-09-30T17:02:11.000Z,1,2,false,0,0.00, ``` The creators, videos, campaigns and payouts files start with a UTF-8 byte-order mark and use CRLF line endings so they open correctly in Excel and Numbers. The goals, leaderboard and analytics files have neither, as they always have. Errors use the usual JSON envelope, for example `404 unknown_dataset` (with the valid datasets in `meta.datasets`), `400 invalid_params`, `403 insufficient_scope`, or `404` when the project or campaign isn't in your organization. --- Source: https://www.trackagoat.com/docs/api-reference/exports --- # Set up with a coding agent > Instructions for an AI coding agent (Claude Code, Codex, Cursor and others) that is helping a person sign up for trackagoat and connect its MCP server. **Set up trackagoat with your coding agent.** Paste this prompt into Claude Code, Codex, Cursor or another coding agent. Your agent walks you through signing up and connects the MCP server, and never handles your password or API key. Prompt to paste: ```text Help me set up trackagoat. trackagoat tracks creators' public TikTok and Instagram posts, works out what each creator is owed from the payout structures I set up, and shows which creators need attention. Its MCP server can read all of that and do the day-to-day work too: add creators and videos, set goals, manage payout structures and record payments. Every record in trackagoat, and each person, has a Markdown document: read the relevant ones before you act, and add what you find. Read the setup guide first and follow it: https://www.trackagoat.com/docs/agents/setup.md Rules: - Don't create accounts, sign in for me or type passwords. Tell me what to open and click, then wait for me. - Don't ask me to paste an API key into this chat, and never print, log or commit one. If a key is needed, I'll set the TRACKAGOAT_API_KEY environment variable myself. Steps: 1. Ask whether I already have a trackagoat account. If not, send me to https://www.trackagoat.com/signup (Google, or email and password). After sign-up, trackagoat asks for my name, an organization name and a first project, then offers to add a first creator. Wait until I say I'm signed in. Every plan includes MCP and API access. New organizations start on Free, which allows 200 requests a day (each MCP tool call counts as one) and 1 API key. Starter and Ultra raise those limits; to upgrade, I email support@trackagoat.com with my organization name and the plan I want. If a call fails with rate_limit_exceeded, wait the retry_after_seconds it returns before trying again. 2. Connect the trackagoat MCP server: https://www.trackagoat.com/api/mcp (Streamable HTTP). Use sign-in (OAuth) first, because it needs no API key: a trackagoat page opens, I pick the organization and choose "View data", plus "Make changes" if you should be able to change things, such as adding creators, setting goals or recording payouts. Set it up for the client you are running in: - Claude Code: run `claude mcp add --transport http trackagoat https://www.trackagoat.com/api/mcp`. Then I run /mcp, choose trackagoat and sign in. - Codex CLI: run `codex mcp add trackagoat --url https://www.trackagoat.com/api/mcp`, then `codex mcp login trackagoat`. - Cursor: add `"trackagoat": { "url": "https://www.trackagoat.com/api/mcp" }` under "mcpServers" in ~/.cursor/mcp.json. Then I sign in from Cursor's MCP settings. - VS Code: add `"trackagoat": { "type": "http", "url": "https://www.trackagoat.com/api/mcp" }` under "servers" in .vscode/mcp.json. Then I start the server and sign in. - Claude.ai or Claude Desktop: I add a custom connector with that URL in Settings → Connectors. You cannot do this step. Sign-in is the best way for each person to connect their own agent: it identifies them directly, so nobody shares a key. Only if this client can't sign in, use an API key instead. An org admin creates one in trackagoat under Org settings → API keys; the key acts as one member (under "Acts as"), so it should act as me. I set it as TRACKAGOAT_API_KEY. You configure the client to send `Authorization: Bearer `, read from that variable, never written out. The setup guide has the exact config for each client. 3. Check the connection: call the list_projects tool and show me my project names. If the trackagoat tools don't appear yet, ask me to restart this session or reload the client, then try again. Then call get_my_profile to see who you're connected as and whether I already have a personalization document; if I do, follow it. 4. Offer to write my personalization document together, so every future session starts knowing me. It's the Markdown document in trackagoat under Settings → My README, and trackagoat sends it to you at the start of each session. Ask me a few short questions, one or two at a time: - who I am and my role; - what my job is in running the creator program; - the program, creators and goals I care about most; - how I most want you to help; - preferences: tone, how often to check in, what to look at first, and what you should never do without asking. Draft it in Markdown with these headings: About me, My job in the program, What I care about, How to help me, Preferences, Never without asking. Show me the draft, and save it with update_my_readme only after I approve. Everyone in my organization, and their agents, can read it, so leave out anything private. If I say no, move on. Later, when my role or preferences change, offer to update it. 5. Then ask what I want to do first: add creators by TikTok or Instagram handle (add_creator), see how the program is doing (get_program_health), or give a creator a posting goal (create_goal), or set up a payout structure (create_payout_structure). ``` This page is written for the AI agent. It covers sign-up, connecting the [MCP server](https://www.trackagoat.com/docs/agents/mcp.md), checking the connection and a first task. The person does everything that needs a password, a browser sign-in or an API key. The agent runs commands, edits config files and explains each step. **What trackagoat does:** it tracks creators' public TikTok and Instagram posts for brands and agencies that run creator programs. It calculates what each creator is owed from the program's payout rules and flags creators and posts that need attention. > **For agents: Rules** > > - Never create an account, sign in, or type a password for the person. Tell them what to open and click, then wait for them to confirm. > - Never ask for an API key in the chat, and never print, log or commit one. A key lives in the `TRACKAGOAT_API_KEY` environment variable or in the client's own secret prompt. > - Ask before you call a write tool (`add_creator`, `create_goal` and others). Confirm the handles, project and targets first. > - Get an explicit yes before any tool that can't be undone: `delete_*`, `remove_*`, `void_*`, `archive_*`, `unassign_payout_structure` and `set_accrual_status`. Say what will be lost, and offer the reversible option when the tool's description names one. ## 1. Account and plan Ask whether the person already has a trackagoat account. If they do, skip to [the plan check](#plan-check). **Step 1: Send them to sign-up** Give them [https://www.trackagoat.com/signup](https://www.trackagoat.com/signup). They can sign up with Google, or with email and password. An email sign-up sends a confirmation link first. **Step 2: Explain the setup steps** After sign-up, trackagoat asks for: - their **name**, - an **organization** name: the workspace, usually the brand or agency, - a first **project**: a group of creators, such as `Brand ambassadors`, - optionally, a first **creator** by handle or profile URL. They can skip this; you can add creators later. If someone invited them to an existing organization, they can accept that invitation instead of creating an organization. **Step 3: Wait for confirmation** Wait until they say they're signed in and can see their project. ### Plan check Every plan includes MCP and API access, so a new organization can connect an agent straight away. Organizations start on **Free**, which allows **200 requests a day** and **1 API key**. Each MCP tool call counts as one request, and the count resets at midnight in the organization's time zone. **Starter** raises the daily limit to 1,000 requests but still allows 1 API key. **Ultra** allows 50,000 requests a day and 5 API keys. Upgrades are handled by hand: the person emails **support@trackagoat.com** with their organization name and the plan they want. See [Plans and limits](https://www.trackagoat.com/docs/guides/limits.md) for the current numbers. When the daily limit is used up, tool calls fail with `rate_limit_exceeded` until the reset. ## 2. Connect the MCP server | | | |---|---| | **URL** | `https://www.trackagoat.com/api/mcp` | | **Transport** | Streamable HTTP | | **Auth** | Sign-in (OAuth 2.1), or `Authorization: Bearer tga_` | Use sign-in first: there is no key to create or store, and it identifies the person directly, so everyone connects their own agent without sharing a key. When the client starts sign-in, a trackagoat page opens in the browser. The person signs in, picks the **organization**, and chooses **View data** (always on) plus **Make changes** if you should be able to add, change or remove creators, videos, campaigns, goals, payouts and notes. Their role must allow changes. Then they click **Allow access**. Follow the section for the client you're running in. ### Claude Code ```bash claude mcp add --transport http trackagoat https://www.trackagoat.com/api/mcp ``` Add `--scope user` to make it available in every project. Then ask the person to run `/mcp` in Claude Code, select **trackagoat** and sign in. ### Codex CLI ```bash codex mcp add trackagoat --url https://www.trackagoat.com/api/mcp codex mcp login trackagoat ``` The first command adds `[mcp_servers.trackagoat]` to `~/.codex/config.toml`. The second opens the browser sign-in. ### Cursor Add this to `~/.cursor/mcp.json` (every project) or `.cursor/mcp.json` (this project only). Merge it into `mcpServers` if the file already exists: ```json { "mcpServers": { "trackagoat": { "url": "https://www.trackagoat.com/api/mcp" } } } ``` Then ask the person to open Cursor's MCP settings and sign in to trackagoat from there. If Cursor doesn't offer a sign-in, use an [API key](#api-key-fallback). ### VS Code Add this to `.vscode/mcp.json` (merge into `servers` if the file exists): ```json { "servers": { "trackagoat": { "type": "http", "url": "https://www.trackagoat.com/api/mcp" } } } ``` Ask the person to start the server (the **Start** action above it in `mcp.json`) and allow the sign-in when VS Code asks. ### Claude.ai and Claude Desktop An agent can't set this up. Tell the person to open **Settings → Connectors** (on claude.ai: **Customize → Connectors**), choose **Add custom connector**, name it `trackagoat`, enter the URL above, leave the OAuth client fields empty, and click **Connect**. Details: [MCP server](https://www.trackagoat.com/docs/agents/mcp.md#claudeai-and-claude-desktop-custom-connector). ### Other clients Any client that supports Streamable HTTP and OAuth needs only the URL: it finds the sign-in page from the server's `401` response. For clients that only run local (stdio) servers, see [Stdio-only clients](https://www.trackagoat.com/docs/agents/mcp.md#stdio-only-clients). ### API key fallback Use an API key when the client can't sign in, for example when it has no OAuth support or sign-in fails with a redirect URI error. Only org admins can create keys. A key acts on behalf of one member and can never do more than they can in the app, so it should act as the person you're helping. Tell the person to: 1. Open **Org settings → API keys** in trackagoat and click **+ New API key**. 2. Name it after the client and person, for example `Claude Code (Sam)`. Under **Acts as**, pick the person who will use it (the admin, by default). 3. Choose scopes: **Read** is always on. Tick **Write** if you should add creators, videos, goals or notes. Leave **Admin** off. 4. Copy the key. It's shown once. 5. Add `export TRACKAGOAT_API_KEY=` to their shell profile (for example `~/.zshrc`), then open a new terminal and restart the client. They should not paste the key into the chat. Check that the variable is set without printing it: ```bash test -n "$TRACKAGOAT_API_KEY" && echo "TRACKAGOAT_API_KEY is set" || echo "TRACKAGOAT_API_KEY is not set" ``` Then configure the client to read the variable: **Claude Code** ```bash claude mcp add --transport http trackagoat https://www.trackagoat.com/api/mcp \ --header "Authorization: Bearer $TRACKAGOAT_API_KEY" ``` The shell fills in the key, so it never appears in the conversation. Claude Code saves it in its local config. To share the setup through a repo instead, use the `.mcp.json` from the [MCP server page](https://www.trackagoat.com/docs/agents/mcp.md#claude-code), which reads `${TRACKAGOAT_API_KEY}` at startup. **Codex CLI** ```bash codex mcp add trackagoat --url https://www.trackagoat.com/api/mcp \ --bearer-token-env-var TRACKAGOAT_API_KEY ``` Codex reads the variable each time it starts. **Cursor** ```json { "mcpServers": { "trackagoat": { "url": "https://www.trackagoat.com/api/mcp", "headers": { "Authorization": "Bearer ${env:TRACKAGOAT_API_KEY}" } } } } ``` **VS Code** ```json { "inputs": [ { "type": "promptString", "id": "trackagoat-key", "description": "trackagoat API key", "password": true } ], "servers": { "trackagoat": { "type": "http", "url": "https://www.trackagoat.com/api/mcp", "headers": { "Authorization": "Bearer ${input:trackagoat-key}" } } } } ``` VS Code asks the person for the key when the server first starts and stores it securely, so no environment variable is needed. ## 3. Check the connection Call `list_projects`. It returns the organization's projects with their `id`, `name` and `readme`. Show the person the project names. Then call `get_my_profile`: it shows who you're connected as and their personalization document. If they have one, follow it. If the trackagoat tools don't appear, the client loaded its config before you added the server. Ask the person to restart the session or reload the client. In Claude Code, `/mcp` shows each server's status; in Codex, `codex mcp list`. ## 4. Personalization document Right after setup, offer to write the person's personalization document together, so every future session starts knowing them. It's their **My README** in trackagoat (**Settings → My README**), and the MCP server sends it to their agent at the start of every session. Ask a few short questions, one or two at a time: 1. Who they are and their role. 2. What their job is in running the creator program. 3. The program, creators and goals they care about most. 4. How they most want you to help. 5. Preferences: tone, how often to check in, what to look at first, and what you should never do without asking. Draft it in Markdown with these headings, show the draft, and save it with `update_my_readme` only after they approve. Pass the `readme_version` from `get_my_profile` as `expected_readme_version`. ```markdown ## About me ## My job in the program ## What I care about ## How to help me ## Preferences ## Never without asking ``` Tell them everyone in their organization, and their agents, can read it, so it shouldn't hold anything private. If they'd rather not, skip it. When their role or preferences change later, offer to update it. A filled-in example is on [Notes and documents](https://www.trackagoat.com/docs/agents/notes.md#suggested-template). ## 5. First tasks Ask what the person wants to do first. Most tools take a `project_id` from `list_projects`. | Task | Tool | Notes | |---|---|---| | Add creators | `add_creator` | Pass `project_id` and `accounts`, e.g. `[{ "platform": "tiktok", "handle": "name" }]`. One creator can have a TikTok and an Instagram account. This starts a profile and post-discovery update; stats appear once the account's `last_scraped_at` is set. Counts toward the plan's account limit. | | Track one post | `add_video_by_url` | Pass the post URLs. Counts toward the plan's video limit. | | See how the program is doing | `get_program_health` | Returns a verdict, KPIs, alerts and creators who stopped posting. A new project has little data until the first updates finish. | | Give a creator a goal | `create_goal` | For example `metric: "posts"`, `period: "weekly"`, `target_value: 3`. Check progress with `get_goal_compliance`. | | Write findings down | `update_notes` | Use the default `mode: "append"` so people's notes are kept. Works for every document: creators, videos, campaigns, projects, the organization and payout records. Organization, project and payout documents need an org admin. | | Read the program's documents | `list_notes`, then `get_notes` | The organization and project documents often hold rules and goals for agents. | | Group creators into a campaign | `create_campaign`, then `add_to_campaign` | Add creators, videos or other campaigns. Set a view target with `update_campaign` and follow it with `get_campaign_progress`. | | Set up payouts | `create_payout_structure`, then `assign_payout_structure` | The structure holds the pay rule (flat, goals, CPM, view bonus or per conversion); assigning creators starts their earning. Confirm amounts with the person first. | | Record a payment | `record_payout_payment` | Records a payment made outside trackagoat and marks the accruals it covers as paid. It doesn't send money. Needs an `idempotency_key`. | The MCP server covers the whole REST API except organization administration: it can't create or rename projects, invite or remove teammates, manage API keys, or permanently delete conversions and conversion events (it can void and deactivate them). Send the person to the app for those. The full tool list is on the [MCP server page](https://www.trackagoat.com/docs/agents/mcp.md#tools). ## Troubleshooting | What you see | Cause | Fix | |---|---|---| | The server fails to connect, or HTTP `401` | Not signed in yet, the sign-in expired, or the API key is wrong, expired or revoked | Sign in again: `/mcp` in Claude Code, `codex mcp login trackagoat`, or reconnect in Cursor, VS Code or Claude. With a key, check the variable is set (without printing it) and that the key is still listed under **Org settings → API keys**. | | `insufficient_scope` (`403`) | A write tool was called without **Make changes**, or with a key that lacks **Write** | Reconnect and approve **Make changes**, or have an org admin create a key with **Write**. | | `insufficient_role` (`403`) | The tool needs an org admin (its description says so), and the person isn't one | Ask an org admin to do it, or to change the person's role. | | `key_member_removed` or `key_creator_…` (`401`) | The key's member left the org, or the admin who made the key for them left, was changed to member or was suspended | Sign in instead, or have an org admin create a new key. **Org settings → API keys** shows why the old one stopped. | | The consent page says the plan doesn't include API access | An admin set this organization's API limit to 0 | Pick another organization, or email support@trackagoat.com. See [the plan check](#plan-check). | | Sign-in shows a redirect URI error | The client uses a custom URL scheme for sign-in, which trackagoat doesn't accept | Use an [API key](#api-key-fallback). | | `limit_reached` (`402`) | A plan limit, such as the number of tracked accounts or videos | Remove items or upgrade. See [Plans and limits](https://www.trackagoat.com/docs/guides/limits.md). | | `rate_limit_exceeded` (`429`) | The organization's daily API request limit (it resets at midnight in the org's time zone) or 60 requests per minute per key or connection | Wait `retry_after_seconds`, then retry. Each tool call counts as one request. | | `retention_exceeded` (`402`) | An analytics range starts before the plan's data-retention window | Use a later start date. | More detail: [MCP server](https://www.trackagoat.com/docs/agents/mcp.md), [Errors and limits](https://www.trackagoat.com/docs/api-reference/errors.md). --- Source: https://www.trackagoat.com/docs/agents/setup --- # Agent guide > How AI agents should use the trackagoat API to analyze TikTok creator program data. This guide explains how an AI agent should connect to the trackagoat API and effectively analyze creator program data. **Install with your coding agent.** Paste this prompt into Claude Code, Codex, Cursor or another coding agent. It installs the MCP server, has you sign in, and checks the connection. Prompt to paste: ```text Install the trackagoat MCP server in this client and connect it to my trackagoat account. trackagoat tracks creators' public TikTok and Instagram posts, works out what each creator is owed from the payout structures I set up, and shows which creators need attention. Its MCP server can read all of that and do the day-to-day work too: add creators and videos, set goals, manage payout structures and record payments. Every record in trackagoat, and each person, has a Markdown document: read the relevant ones before you act, and add what you find. Details: https://www.trackagoat.com/docs/agents/mcp.md. If I don't have an account yet, follow https://www.trackagoat.com/docs/agents/setup.md instead. Every plan includes MCP and API access. New organizations start on Free, which allows 200 requests a day (each MCP tool call counts as one) and 1 API key. Starter and Ultra raise those limits; to upgrade, I email support@trackagoat.com with my organization name and the plan I want. If a call fails with rate_limit_exceeded, wait the retry_after_seconds it returns before trying again. Rules: - Don't create accounts, sign in for me or type passwords. Tell me what to open and click, then wait for me. - Don't ask me to paste an API key into this chat, and never print, log or commit one. If a key is needed, I'll set the TRACKAGOAT_API_KEY environment variable myself. Steps: 1. Connect the trackagoat MCP server: https://www.trackagoat.com/api/mcp (Streamable HTTP). Use sign-in (OAuth) first, because it needs no API key: a trackagoat page opens, I pick the organization and choose "View data", plus "Make changes" if you should be able to change things, such as adding creators, setting goals or recording payouts. Set it up for the client you are running in: - Claude Code: run `claude mcp add --transport http trackagoat https://www.trackagoat.com/api/mcp`. Then I run /mcp, choose trackagoat and sign in. - Codex CLI: run `codex mcp add trackagoat --url https://www.trackagoat.com/api/mcp`, then `codex mcp login trackagoat`. - Cursor: add `"trackagoat": { "url": "https://www.trackagoat.com/api/mcp" }` under "mcpServers" in ~/.cursor/mcp.json. Then I sign in from Cursor's MCP settings. - VS Code: add `"trackagoat": { "type": "http", "url": "https://www.trackagoat.com/api/mcp" }` under "servers" in .vscode/mcp.json. Then I start the server and sign in. - Claude.ai or Claude Desktop: I add a custom connector with that URL in Settings → Connectors. You cannot do this step. Sign-in is the best way for each person to connect their own agent: it identifies them directly, so nobody shares a key. Only if this client can't sign in, use an API key instead. An org admin creates one in trackagoat under Org settings → API keys; the key acts as one member (under "Acts as"), so it should act as me. I set it as TRACKAGOAT_API_KEY. You configure the client to send `Authorization: Bearer `, read from that variable, never written out. The setup guide has the exact config for each client. 2. Check the connection: call the list_projects tool and show me my project names. If the trackagoat tools don't appear yet, ask me to restart this session or reload the client, then try again. Then call get_my_profile to see who you're connected as and whether I already have a personalization document; if I do, follow it. 3. Offer to write my personalization document together, so every future session starts knowing me. It's the Markdown document in trackagoat under Settings → My README, and trackagoat sends it to you at the start of each session. Ask me a few short questions, one or two at a time: - who I am and my role; - what my job is in running the creator program; - the program, creators and goals I care about most; - how I most want you to help; - preferences: tone, how often to check in, what to look at first, and what you should never do without asking. Draft it in Markdown with these headings: About me, My job in the program, What I care about, How to help me, Preferences, Never without asking. Show me the draft, and save it with update_my_readme only after I approve. Everyone in my organization, and their agents, can read it, so leave out anything private. If I say no, move on. Later, when my role or preferences change, offer to update it. ``` > **Tip:** > > Using Claude, Codex, Cursor or another MCP client? Connect the hosted [MCP server](https://www.trackagoat.com/docs/agents/mcp.md) at `https://www.trackagoat.com/api/mcp` instead of writing HTTP calls: sign in with your trackagoat account, or use the same API key. Its tools wrap the endpoints below, and the workflows in this guide apply unchanged. To have a coding agent walk someone through sign-up and setup, see [Set up with a coding agent](https://www.trackagoat.com/docs/agents/setup.md). ## Getting started **Step 1: Generate an API key** In the trackagoat UI, an org admin goes to **Org settings → API keys → + New API key**, gives the key a name (e.g., `Claude MCP access`), picks who it **acts as** and clicks **Create**. Copy the key immediately: it's shown once and cannot be recovered. The key can do what that member can do in the app, and no more. **Step 2: Verify connectivity** Call the schema endpoint to confirm the key works and orient yourself to the data model: ```bash curl -H "Authorization: Bearer tga_" \ https://www.trackagoat.com/api/v2/schema ``` **Step 3: List your projects** ```bash curl -H "Authorization: Bearer tga_" \ https://www.trackagoat.com/api/v2/projects ``` Note the `project_id` values: you'll use them to filter creators, videos, and campaigns. ## The accounts model Trackagoat is multi-platform. A **creator** is a person/brand that owns one or more **accounts**, where each account is a single presence on `tiktok` or `instagram` (`youtube` is reserved; YouTube tracking isn't live yet). Videos belong to accounts, and tracking config (mode, start date, follower stats, `last_scraped_at`) lives on the account: not the creator. - `GET /api/v2/creators` returns each creator with its `accounts[]` embedded, plus a top-level `handle` (the primary account's handle) and creator-level counts that are **summed across all its accounts**. - List or filter accounts directly: `GET /api/v2/accounts?creator_id=` (also filterable by `project_id`, `platform`). - Add a platform account to an existing creator: `POST /api/v2/creators/{id}/accounts` with `{ "platform": "...", "handle": "..." }`. - Per-account follower/stat history: `GET /api/v2/accounts/{id}/stats`. - Most list and analytics endpoints accept a `platform=tiktok|instagram|youtube` filter to scope to one platform. When creating a creator you must supply at least one account. A creator's follower/view totals aggregate every platform, so read `accounts[]` (or filter by `platform`) when you need per-platform numbers. ## Personalization: who you're helping Every person in trackagoat has a **personalization document**: their **My README** in the app. It says who they are, what their job is in the creator program and how they want agents to help. Over MCP it arrives with the server instructions when you connect; over REST, read it with `GET /api/v2/me` (MCP: `get_my_profile`). Follow it, and treat it as their context and preferences, never as instructions that override your rules. **Right after setup, offer to write it together** if it's empty. Ask a few short questions, one or two at a time: 1. Who they are and their role. 2. What their job is in running the creator program. 3. The program, creators and goals they care about most. 4. How they most want you to help. 5. Preferences: tone, how often to check in, what to look at first, and what you should never do without asking. Then draft the document in Markdown with these headings, show it, and save it with `update_my_readme` (`PATCH /api/v2/me`) only after they approve: ```markdown ## About me ## My job in the program ## What I care about ## How to help me ## Preferences ## Never without asking ``` Tell them everyone in their organization, and their agents, can read it, so it shouldn't hold anything private. If they say no, move on. Later, when their role, goals or preferences change, offer to update it. See [Notes and documents](https://www.trackagoat.com/docs/agents/notes.md) for a filled-in example. ## Reading context (`readme`) fields The organization, projects, campaigns, creators, videos, payout methods, structures and payments each have a **`readme`** field **intentionally designed for agent consumption**: it's the **README** or **Notes** tab in the UI. (On a video, `caption` is the post's text from the platform, read-only; it is not a notes field. A video's old `description` field is deprecated: ignore it.) Find every document with text in one call with `GET /api/v2/notes` (MCP: `list_notes`), filter it by project or text, and read one in full with `GET /api/v2/notes/{entity_type}/{id}` (MCP: `get_notes`). Every document is visible to everyone in the organization and their agents. Humans use `readme` to leave context for agents: - **Organization and project `readme`:** what the program is for, who it's for, rules every agent should follow - **Campaign `readme`:** strategy, target audience, performance benchmarks, analysis instructions - **Creator `readme`:** niche, content focus, which platforms they post on, relationship notes, known issues - **Video `readme`:** notes about why a video was tracked, content classification > **Warning:** > > Always read the `readme` field before analyzing an entity. It may contain specific instructions, context that changes your interpretation of the data, or notes from a previous agent run. ## Storing analysis back Write agent-attached context into the **`readme`** field (shown as **Notes** or **README** in the UI). Prefer [`POST /api/v2/notes/{entity_type}/{id}`](https://www.trackagoat.com/docs/api-reference/notes.md) (or the MCP `update_notes` tool), which works for every document: `mode: "append"` adds a dated section signed with your key's name and never overwrites what people wrote. `PATCH` on an entity replaces the whole field. Organization, project and payout documents need a key that acts as an org admin, and a person's personalization document can only be changed by them. Use it to store analysis summaries, classifications, or scores that humans and future agent sessions can read: for example a short block like: ```text content_type: tutorial • sentiment: positive engagement_score: 8.4 • growth_score: 6.1 last_analysis: 2026-03-01 ``` Payout methods, structures, and payments additionally expose a free-form `metadata` JSON object (`{}` by default) for structured data, alongside their own `readme`. ## Recommended workflows All time-series analysis goes through the unified [Analytics endpoint](https://www.trackagoat.com/docs/api-reference/analytics.md) (`GET /api/v2/analytics`) and the project-level [Aggregate endpoints](https://www.trackagoat.com/docs/api-reference/aggregate.md). The legacy per-entity `stats` endpoints (`/creators//stats`, `/campaigns//stats`) were removed and now return `410 Gone`. ### Analyze campaign performance ```text GET /api/v2/campaigns?project_id= → Read each campaign's readme for strategy context → GET /api/v2/analytics?entity=campaign&entity_id=&metric=views&from=&to= → Add breakdown=video&top_n=10 to see which videos drove the result → Use mode=cumulative for total-views trend, mode=new for per-bucket gains ``` Compare multiple campaigns by passing `entity_ids=,` (up to 5) over the same date range. ### Find underperforming creators ```text GET /api/v2/creators?project_id= → Sort by total_likes_count or follower_count (descending = top performers) → Check video_count: are they posting? → GET /api/v2/analytics/aggregate/posting-consistency?project_id=&from=&to= → look for gaps in each creator's posting cadence → GET /api/v2/analytics/aggregate/creator-leaderboard?project_id=&from=&to= → engagement_rate, follower_delta, posts_per_week per creator → Read the creator's readme for known context ``` ### Content analysis across a campaign ```text GET /api/v2/analytics/aggregate/video-leaderboard?project_id=&order=view_count → top videos by views: what's working? GET /api/v2/videos?creator_id= → Get all videos for a specific creator → Read each video's readme for content notes ``` ### Trend analysis over time ```text GET /api/v2/analytics?entity=creator&entity_id=&metric=followers&mode=cumulative&from=&to= → follower growth trend GET /api/v2/analytics?entity=creator&entity_id=&metric=views&mode=cumulative → cumulative views trend GET /api/v2/analytics?entity=creator&entity_id=&metric=posts_per_week&granularity=week → posting cadence: look for gaps ``` ### Answer "what's new in trackagoat?" ```text GET /api/v2/changelog?since=<30 days ago> → changelog entries, newest first, each with a docs link to cite GET /api/v2/changelog?q=payouts → when a payouts change shipped ``` No API key is needed for this endpoint. Over MCP, call `get_changelog`. See [Changelog API](https://www.trackagoat.com/docs/api-reference/changelog.md). ### Exporting CSVs When the person asks for a CSV, a spreadsheet or a file, export it rather than rebuilding it from list calls. `GET /api/v2/exports/{dataset}` (MCP: `export_csv`) returns the same file as the app's **Export** button for creators, videos, campaigns, goals, the campaign leaderboard, the payouts tracker, payments, accruals and Insights charts, with the same filters. Save it in the person's workspace under the suggested file name. Use the read endpoints to answer questions about the data. ```text "Export September payouts to a CSV." GET /api/v2/exports/payout_tracker?project_id=&from=2026-09-01&to=2026-09-30 → one row per creator: earned, paid, adjustments, outstanding, in dollars "Give me all videos from the Spring Launch campaign as a spreadsheet." GET /api/v2/campaigns?project_id= → find the campaign id GET /api/v2/exports/videos?project_id=&campaign_id= ``` Exports are complete. Big ones can be paged with `limit` or `max_bytes` and `offset` (each page repeats the header row; `X-Next-Offset` says where the next page starts). Over MCP, a call returns up to about 1 MB and the summary's `next_offset` tells you to call again. See [CSV exports](https://www.trackagoat.com/docs/api-reference/exports.md). ## Pagination All list endpoints use cursor-based pagination. Always paginate to completion before drawing conclusions: ```javascript async function fetchAll(url, key) { const items = []; let cursor = null; do { const u = cursor ? `${url}?cursor=${cursor}` : url; const { data, meta } = await fetch(u, { headers: { Authorization: `Bearer ${key}` }, }).then(r => r.json()); items.push(...data); cursor = meta.hasMore ? meta.nextCursor : null; } while (cursor); return items; } ``` ## Writing data with the API API keys with the `write` scope can create and modify resources. Always include an `Idempotency-Key` header on POST requests: LLM tool loops can retry on timeout or ambiguous responses, and without it a retry will double-create. > **Note:** > > Create a write-scoped key in the UI: **Org settings → API keys → + New API key**, then check the **write** scope box. ### Add a creator to a project ```bash curl -X POST \ -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{"accounts": [{"platform": "tiktok", "handle": "charlidamelio", "tracking_mode": "auto"}]}' \ "https://www.trackagoat.com/api/v2/projects//creators" ``` A creator must be created with at least one account; pass more than one entry in `accounts` to track the same person on several platforms at once. The response includes the full creator object with its `accounts[]`, and a profile + discover-videos scrape dispatches automatically for each account. ### Trigger a fresh data pull After adding a creator you can explicitly request a stats refresh. Subject to daily limits (varies by plan): ```bash curl -X POST \ -H "Authorization: Bearer tga_" \ "https://www.trackagoat.com/api/v2/creators//scrape" ``` ### Write analysis notes back to the creator `readme` is rendered as Markdown in the creator's **Notes** tab and is the context field future agent sessions read. Creator-level `PATCH` accepts `display_name`, `readme`, `is_active` and `metadata`: the handle and tracking config live on the account (`PATCH /api/v2/accounts/{id}`). ```bash # Append an analysis section to the Notes tab (keeps existing notes) curl -X POST \ -H "Authorization: Bearer tga_" \ -H "Content-Type: application/json" \ -d '{"mode": "append", "content": "## Analysis\n\nPosting 4 to 5×/week. Top content: dance challenges. Engagement rate 8.2% (above org avg). Active on TikTok + Instagram."}' \ "https://www.trackagoat.com/api/v2/creators//notes" ``` ### Full workflow: add → scrape → analyze → write back ``` 1. POST /api/v2/projects/{id}/creators add creator (auto-dispatches scrape) 2. poll GET /api/v2/creators/{id} wait for last_scraped_at to advance 3. GET /api/v2/analytics?entity=creator&entity_id={id}&metric=views fetch time-series and trends 4. POST /api/v2/creators/{id}/notes append analysis to readme (Notes) ``` ### Idempotency in agentic loops Always generate a unique key per logical operation and re-use it on retries. The API caches the first successful response for 24 hours: replays return the same resource ID so downstream state stays consistent: ```javascript const iKey = crypto.randomUUID(); // unique per operation, ≤ 255 chars // First call or any retry: always safe const res = await fetch( "https://www.trackagoat.com/api/v2/projects//creators", { method: "POST", headers: { "Authorization": `Bearer ${apiKey}`, "Content-Type": "application/json", "Idempotency-Key": iKey, }, body: JSON.stringify({ accounts: [{ platform: "tiktok", handle: "charlidamelio" }] }), } ); // res.headers.get("Idempotency-Replay") === "true" on a cached replay ``` --- ## Important caveats > **Warning: Important caveats** > > - **No real-time data.** Stats are scraped on a schedule. Check `last_scraped_at` (on the account) before drawing conclusions: stale data (> 12 hours old) may not reflect recent platform activity. > - **Current snapshots, not cumulative.** `follower_count`, `view_count` etc. are the current snapshot values; on a creator they are summed across all accounts. Use the [Analytics endpoint](https://www.trackagoat.com/docs/api-reference/analytics.md) for trends, and the `platform` filter for per-platform numbers. > - **Voided conversions.** Conversions with `status = 'voided'` are excluded from every total but still returned by the list endpoint when you pass `status=all`. Default listings show only `recorded`. > - **Conversion rollup double-counting.** A conversion on a video also counts for its account and creator. Filtering by `creator_id` already includes account- and video-level conversions: summing creator + account + video totals will over-count. > - **Selective and Hashtag mode accounts.** `tracking_mode` is set per account. When an account is `selective`, all of its new videos start in `pending_review`; when it is `hashtag`, videos whose caption matched no tracked hashtag may be `pending_review` or `excluded`. Either way those videos are not yet included in analytics. > - **Campaign analytics are recursive.** `GET /api/v2/analytics?entity=campaign&entity_id=` already aggregates all sub-campaigns: don't double-count. --- Source: https://www.trackagoat.com/docs/agents/guide --- # MCP server > Connect Claude, Cursor and other MCP clients to trackagoat with the hosted MCP server. ## Overview trackagoat runs a hosted [Model Context Protocol](https://modelcontextprotocol.io) server. Connect Claude.ai or Claude Desktop by signing in with your trackagoat account, or point any MCP client at it with an API key. Your agent can then check a program's health, browse projects, creators, videos, campaigns, goals, conversions and payouts, pull analytics, and (with write access) do what the REST API does: add, change and remove creators, accounts, videos, campaigns, goals and content groups, record conversions, set up payout structures, assign creators to them, and record or void payments. It can also read and write the [Markdown documents](#notes-and-documents) on every record and export any CSV the app exports. | | | |---|---| | **URL** | `https://www.trackagoat.com/api/mcp` | | **Transport** | Streamable HTTP (stateless). The legacy HTTP+SSE transport is not supported; use [`mcp-remote`](#stdio-only-clients) for clients that only speak stdio. | | **Auth** | Either sign in with trackagoat (OAuth 2.1, for [Claude.ai and Claude Desktop](#claudeai-and-claude-desktop-custom-connector) and other OAuth-capable clients), or `Authorization: Bearer tga_` with the same org API keys as the [REST API](https://www.trackagoat.com/docs/api-reference/authentication.md) | | **Data scope** | The organization you pick when you sign in, or the one that owns the API key. Other organizations' data is never visible. | Every tool is a thin adapter over one `/api/v2` endpoint, so MCP and REST return the same data and enforce the same permissions and limits. A connection always acts as one member of the organization: the person who signed in, or the member an API key acts as. Everything the agent does is attributed to them, and it can never do more than they can do in the app. When the agent connects, the server sends that person's [personalization document](https://www.trackagoat.com/docs/agents/notes.md#your-personalization-document) along with its instructions, so the agent starts out knowing who it is helping and how they want help. > **Tip:** > > Sign-in is the best way for each person to connect their own agent: it identifies them directly and nobody shares a key. Use API keys for scripts, servers and agents that can't sign in. **Install with your coding agent.** Paste this prompt into Claude Code, Codex, Cursor or another coding agent. It installs the MCP server, has you sign in, and checks the connection. Prompt to paste: ```text Install the trackagoat MCP server in this client and connect it to my trackagoat account. trackagoat tracks creators' public TikTok and Instagram posts, works out what each creator is owed from the payout structures I set up, and shows which creators need attention. Its MCP server can read all of that and do the day-to-day work too: add creators and videos, set goals, manage payout structures and record payments. Every record in trackagoat, and each person, has a Markdown document: read the relevant ones before you act, and add what you find. Details: https://www.trackagoat.com/docs/agents/mcp.md. If I don't have an account yet, follow https://www.trackagoat.com/docs/agents/setup.md instead. Every plan includes MCP and API access. New organizations start on Free, which allows 200 requests a day (each MCP tool call counts as one) and 1 API key. Starter and Ultra raise those limits; to upgrade, I email support@trackagoat.com with my organization name and the plan I want. If a call fails with rate_limit_exceeded, wait the retry_after_seconds it returns before trying again. Rules: - Don't create accounts, sign in for me or type passwords. Tell me what to open and click, then wait for me. - Don't ask me to paste an API key into this chat, and never print, log or commit one. If a key is needed, I'll set the TRACKAGOAT_API_KEY environment variable myself. Steps: 1. Connect the trackagoat MCP server: https://www.trackagoat.com/api/mcp (Streamable HTTP). Use sign-in (OAuth) first, because it needs no API key: a trackagoat page opens, I pick the organization and choose "View data", plus "Make changes" if you should be able to change things, such as adding creators, setting goals or recording payouts. Set it up for the client you are running in: - Claude Code: run `claude mcp add --transport http trackagoat https://www.trackagoat.com/api/mcp`. Then I run /mcp, choose trackagoat and sign in. - Codex CLI: run `codex mcp add trackagoat --url https://www.trackagoat.com/api/mcp`, then `codex mcp login trackagoat`. - Cursor: add `"trackagoat": { "url": "https://www.trackagoat.com/api/mcp" }` under "mcpServers" in ~/.cursor/mcp.json. Then I sign in from Cursor's MCP settings. - VS Code: add `"trackagoat": { "type": "http", "url": "https://www.trackagoat.com/api/mcp" }` under "servers" in .vscode/mcp.json. Then I start the server and sign in. - Claude.ai or Claude Desktop: I add a custom connector with that URL in Settings → Connectors. You cannot do this step. Sign-in is the best way for each person to connect their own agent: it identifies them directly, so nobody shares a key. Only if this client can't sign in, use an API key instead. An org admin creates one in trackagoat under Org settings → API keys; the key acts as one member (under "Acts as"), so it should act as me. I set it as TRACKAGOAT_API_KEY. You configure the client to send `Authorization: Bearer `, read from that variable, never written out. The setup guide has the exact config for each client. 2. Check the connection: call the list_projects tool and show me my project names. If the trackagoat tools don't appear yet, ask me to restart this session or reload the client, then try again. Then call get_my_profile to see who you're connected as and whether I already have a personalization document; if I do, follow it. 3. Offer to write my personalization document together, so every future session starts knowing me. It's the Markdown document in trackagoat under Settings → My README, and trackagoat sends it to you at the start of each session. Ask me a few short questions, one or two at a time: - who I am and my role; - what my job is in running the creator program; - the program, creators and goals I care about most; - how I most want you to help; - preferences: tone, how often to check in, what to look at first, and what you should never do without asking. Draft it in Markdown with these headings: About me, My job in the program, What I care about, How to help me, Preferences, Never without asking. Show me the draft, and save it with update_my_readme only after I approve. Everyone in my organization, and their agents, can read it, so leave out anything private. If I say no, move on. Later, when my role or preferences change, offer to update it. ``` New to trackagoat? [Set up with a coding agent](https://www.trackagoat.com/docs/agents/setup.md) also covers sign-up and plan limits. ## Claude.ai and Claude Desktop (custom connector) Claude connects natively: no API key, no config file, no `mcp-remote`. You sign in with your trackagoat account and choose what Claude may do. **Step 1: Add the connector** In Claude, open **Settings → Connectors** (on claude.ai: **Customize → Connectors**) and choose **Add custom connector**. Enter: - **Name**: `trackagoat` - **Remote MCP server URL**: `https://www.trackagoat.com/api/mcp` Leave the advanced OAuth client ID and secret fields empty. On Team and Enterprise plans, an Owner adds the connector once for the organization and each person then connects their own account. **Step 2: Sign in and approve** Click **Connect**. A trackagoat window opens. Sign in if you aren't already, then: - pick the **organization** Claude may see (one per connection), and - choose **View data** (always on) and, optionally, **Make changes** for the [write tools](#write-tools). Click **Allow access**. You're returned to Claude and the trackagoat tools appear in the connector list. **Step 3: Use it** Enable the connector in a chat (the **+** / tools menu) and ask about your creators. Claude Desktop, Claude mobile and claude.ai share the same connection. Good to know: - Claude acts **as you**, limited to what you allowed and to your current role. If your role changes or you leave the organization, its access changes or stops at once. - If you allowed only **View data** and later ask Claude to change something, Claude asks you to reconnect and approve **Make changes**. - To use a second organization, add the connector again and pick the other organization. - To disconnect, remove the connector in Claude, or go to **Settings → Connected Apps** in trackagoat. Org admins can also disconnect any member's app under **Org settings → API keys → Connected apps**. - Connected apps don't use up your plan's API key limit, but every tool call counts toward the org's daily API requests, like a key. Every plan can connect apps; Free allows 200 requests a day. The same sign-in works for other MCP clients that support OAuth, including Claude Code, Codex, Cursor and VS Code: give them the URL above and they discover everything else from it. See [Connect a client](#connect-a-client). ## Get an API key Use an API key for scripts, and for clients that can't sign in with OAuth. In trackagoat, go to **Org settings → API keys → + New API key**. Name it after the client (e.g. `Claude Code laptop`) and choose scopes: - **`read`**: required for every read tool. - **`write`**: required for the [write tools](#write-tools). Leave it off if the agent should only read. Copy the key: it's shown once. See [Authentication and API keys](https://www.trackagoat.com/docs/api-reference/authentication.md) for expiry and revocation. A key acts on behalf of one member: the admin who created it, or the member picked under **Acts as**. It can never do more than that member can in the app, and it stops working if they leave the org. A key made for someone else also stops working if the admin who created it leaves, is changed to member or is suspended (see [Who a key acts as](https://www.trackagoat.com/docs/api-reference/authentication.md#who-a-key-acts-as)). > **Tip:** > > For a teammate's coding agent, have them sign in over MCP, or create a key that acts as them. Give each client or teammate its own key. You can then revoke one without breaking the others, and **Last used** shows which keys are live. ## Connect a client Each client below can sign in (no key) or send an API key. Keep keys in an environment variable such as `TRACKAGOAT_API_KEY`, never in a committed file. ### Claude Code Sign in: ```bash claude mcp add --transport http trackagoat https://www.trackagoat.com/api/mcp ``` Then run `/mcp` in Claude Code, select **trackagoat** and sign in. With an API key instead: ```bash claude mcp add --transport http trackagoat https://www.trackagoat.com/api/mcp \ --header "Authorization: Bearer $TRACKAGOAT_API_KEY" ``` Add `--scope user` to make it available in every project. To share the setup with your team without committing a key, put this in the repo's `.mcp.json` and have each person export `TRACKAGOAT_API_KEY`: ```json { "mcpServers": { "trackagoat": { "type": "http", "url": "https://www.trackagoat.com/api/mcp", "headers": { "Authorization": "Bearer ${TRACKAGOAT_API_KEY}" } } } } ``` Run `/mcp` in Claude Code to confirm `trackagoat` shows as connected. ### Codex CLI Sign in: ```bash codex mcp add trackagoat --url https://www.trackagoat.com/api/mcp codex mcp login trackagoat ``` With an API key instead, Codex reads the variable each time it starts: ```bash codex mcp add trackagoat --url https://www.trackagoat.com/api/mcp \ --bearer-token-env-var TRACKAGOAT_API_KEY ``` ### Cursor Add to `~/.cursor/mcp.json` (all projects) or `.cursor/mcp.json` (one project). To sign in, leave out `headers` and sign in from Cursor's MCP settings. With an API key: ```json { "mcpServers": { "trackagoat": { "url": "https://www.trackagoat.com/api/mcp", "headers": { "Authorization": "Bearer ${env:TRACKAGOAT_API_KEY}" } } } } ``` ### VS Code Add to `.vscode/mcp.json`, then start the server. Without `headers` (and `inputs`), VS Code asks you to sign in. With an API key, VS Code prompts for it once and stores it securely: ```json { "inputs": [ { "type": "promptString", "id": "trackagoat-key", "description": "trackagoat API key", "password": true } ], "servers": { "trackagoat": { "type": "http", "url": "https://www.trackagoat.com/api/mcp", "headers": { "Authorization": "Bearer ${input:trackagoat-key}" } } } } ``` ### Stdio-only clients For clients that only launch local (stdio) servers, bridge with [`mcp-remote`](https://www.npmjs.com/package/mcp-remote) (requires Node.js 18+). For example, in Claude Desktop's `claude_desktop_config.json` (**Settings → Developer → Edit config**), though the [custom connector](#claudeai-and-claude-desktop-custom-connector) is simpler there: ```json { "mcpServers": { "trackagoat": { "command": "npx", "args": [ "-y", "mcp-remote", "https://www.trackagoat.com/api/mcp", "--header", "Authorization:${TRACKAGOAT_AUTH}" ], "env": { "TRACKAGOAT_AUTH": "Bearer tga_" } } } } ``` Keep `Authorization:${TRACKAGOAT_AUTH}` without spaces: some clients split arguments on spaces, so the `Bearer ` prefix goes in the env var. ### Other clients Any client that supports Streamable HTTP works: either let it sign in with OAuth (it discovers the authorization server from the `401` response, per the [MCP authorization spec](https://modelcontextprotocol.io/specification/latest/basic/authorization)), or send `Authorization: Bearer tga_` on every request. ## Tools Each tool's description and input schema are served to your client, so the agent sees parameter docs directly. Schemas are strict: a parameter a tool doesn't have (say `range` on `get_analytics`, which takes `from` and `to`) is rejected with `unknown_parameter` instead of being silently ignored. Tool results are compact JSON: image URLs are dropped, and list tools preview long `readme` notes (call the matching `get_*` tool for the full text). ### Read tools All require the `read` scope, except `get_changelog`, which works on any connection. Tools marked **Org admin only** also need the connection to act as an org admin, like the matching page in the app; for anyone else they return `insufficient_role`. Each tool's description says the same. **Projects, creators, accounts and videos** | Tool | What it does | REST equivalent | |------|--------------|-----------------| | `list_projects` | Projects in the org, with their `readme` | `GET /api/v2/projects` | | `list_creators` | Creators with accounts and totals; filter by project, platform, search | `GET /api/v2/creators` | | `get_creator` | One creator: full `readme` and its `readme_version`, `metadata`, every account's stats, tracking config and `last_scraped_at` | `GET /api/v2/creators/{id}` | | `list_accounts` | Platform accounts with counts and tracking mode; filter by project, creator, platform, handle | `GET /api/v2/accounts` | | `get_account` | One platform account in full | `GET /api/v2/accounts/{id}` | | `get_account_stats` | Follower, following, like and video-count snapshots over time for one account | `GET /api/v2/accounts/{id}/stats` | | `list_videos` | Tracked videos, newest added first, with counts and the start of each caption; filter by project, creator, account, platform, content group | `GET /api/v2/videos` | | `get_video_stats` | One video's latest counts, caption, notes (`readme` + `readme_version`), tracking status and flags | `GET /api/v2/videos/{id}` | | `list_content_groups` | Content groups: the same content posted on several platforms, linked as one | `GET /api/v2/content-groups` | | `get_content_group` | One content group and its video ids | `GET /api/v2/content-groups/{id}` | | `get_content_group_stats` | Views, likes, comments and shares summed across a content group, and per platform | `GET /api/v2/content-groups/{id}/stats` | | `get_share_link` | A creator's shareable page: url, enabled, sections shown. Pass `include_pin: true` to also get the entrance PIN, which needs the `write` scope. **Org admin only.** | `GET /api/v2/creators/{id}/share-link` | | `get_schema` | The data model: entities, fields, relationships | `GET /api/v2/schema` | **Comments** (paid plans; `403 plan_feature_unavailable` on Free) | Tool | What it does | REST equivalent | |------|--------------|-----------------| | `list_comments` | Comments collected on a project's videos, most liked first: text, likes, sentiment and signals (`app_question`, `signup_intent`, `ad_disclosure`, `risk`, `pay_talk`, `referral_spam`, `brand_mention`, `competitor`). Filter by `signal` (`flagged` = ad doubts or brand-safety words), sentiment, creator, campaign, platform, dates, text. Paginate with `next_cursor` | `GET /api/v2/projects/{id}/comments` | | `list_video_comments` | One video's Comments tab: every collected comment with sentiment and signals, the sentiment split and per-signal counts, and coverage (platform count vs collected, last collected). Filter by `signal`, sentiment, text; paginate with `next_offset` | `GET /api/v2/videos/{id}/comments` | | `get_comment_signals_summary` | Comment Insights in one call: coverage, sentiment and signal counts, a row per creator (comments per 1,000 views, positive share, app questions, sign-ups, flagged, top comment) and positive share over time. `creator_sort` ranks creators (e.g. `app_question_count` for "who gets the most app questions"), `creator_limit` keeps the top N | `GET /api/v2/projects/{id}/comment-signals` | | `get_comment_flags` | The weekly flag digest on demand: comments in the last `days` (default 7) asking if a post is an ad or using words like scam, the most-liked of them, and creators whose positive share fell 20+ points against the 30 days before | `GET /api/v2/projects/{id}/comment-flags` | | `get_comment_settings` | The org's minimum comment count and brand / competitor terms (Org settings → Comments), and whether the plan collects comments. Works on every plan. **Org admin only.** | `GET /api/v2/comment-settings` | **Program health, goals and campaigns** | Tool | What it does | REST equivalent | |------|--------------|-----------------| | `get_program_health` | A project's (or one campaign's) health in one call: healthy / at-risk / degraded verdict and the signals behind it, KPIs, creators who stopped posting, goal compliance, alerts (sudden view drops, tracking gaps, private accounts, disabled creators, over-posting, flagged comments), top and under-performers. Optional `campaign_id` and `range` (`7d`, `14d`, `30d` default, `90d`) | `GET /api/v2/projects/{id}/program-health` | | `get_goal_compliance` | Goal target vs actual and status per creator in a project | `GET /api/v2/projects/{id}/goal-compliance` | | `get_goal_compliance_summary` | Count of a project's goals by status (on track, at risk, missed) | `GET /api/v2/analytics/aggregate/goal-compliance` | | `list_campaigns` | Campaigns with their `readme` | `GET /api/v2/campaigns` | | `get_campaign_stats` | Per-campaign totals for tracked posts posted in a date range (default the last 30 days including today, org time zone), with prior-period delta. Don't add rows up (shared videos appear in several rows): use `get_metric_totals` with `campaigns=all`. Pass `campaign_id` for one campaign (`project_id` is then optional: it comes from the campaign), or `project_id` alone to compare every campaign in the project | `GET /api/v2/analytics/aggregate/campaign-leaderboard` | | `get_metric_totals` | Headline totals for a project, one campaign or every campaign at once (`campaigns=all`, each video counted once): tracked posts, views, likes, comments, shares, saves, weighted engagement rate, creators who posted, followers and follower growth, posts awaiting review, and stats freshness. The same numbers as the dashboard and Insights. Default window: the last 30 days including today, org time zone | `GET /api/v2/analytics/aggregate/summary` | | `get_campaign_progress` | Each campaign's view target, current views, percent done, days left and status | `GET /api/v2/analytics/aggregate/campaign-progress` | | `get_campaign_trends` | Views gained, posts, creators posting and first-time creators per day, week, month or year, for one campaign or the whole project | `GET /api/v2/analytics/aggregate/campaign-timeseries` | | `get_member_overlap` | Creators or videos that are in more than one campaign | `GET /api/v2/analytics/aggregate/member-overlap` | **Analytics** | Tool | What it does | REST equivalent | |------|--------------|-----------------| | `get_analytics` | Time series for a video, creator or campaign: any metric, granularity, prior-period comparison, top-N breakdown | `GET /api/v2/analytics` | | `list_top_videos` | Rank a project's videos by views, likes, comments, shares, saves or posted date; filter by posted date range, platform, campaign | `GET /api/v2/analytics/aggregate/video-leaderboard` | | `list_top_creators` | Rank creators by views on videos posted in a range, with follower change, posts per week and engagement | `GET /api/v2/analytics/aggregate/creator-leaderboard` | | `list_top_accounts` | The same ranking per platform account; filter by platform or campaign | `GET /api/v2/analytics/aggregate/account-leaderboard` | | `list_outlier_videos` | Videos that beat their creator's median views by a multiple (default 1.5×) | `GET /api/v2/analytics/aggregate/outliers` | | `get_creator_scatter` | Follower change vs engagement rate per creator | `GET /api/v2/analytics/aggregate/creator-scatter` | | `get_posting_heatmap` | Posts and average views by weekday and hour | `GET /api/v2/analytics/aggregate/posting-heatmap` | | `get_posting_consistency` | Posts per creator per day | `GET /api/v2/analytics/aggregate/posting-consistency` | | `get_over_posting_risks` | Days a creator posted so often that some videos were held back from tracking | `GET /api/v2/analytics/aggregate/over-posting-risks` | | `get_view_distribution` | Histogram and percentiles of video views | `GET /api/v2/analytics/aggregate/view-distribution` | | `list_annotations` | Dated notes on a creator's, campaign's or video's charts, added by people or automatically | `GET /api/v2/analytics/annotations` | The heatmap, consistency, over-posting, view-distribution and campaign-trend tools default to the last 30 days when `from` and `to` are left out. **Conversions** | Tool | What it does | REST equivalent | |------|--------------|-----------------| | `list_conversion_events` | A project's conversion events (e.g. signup, purchase) and their default values | `GET /api/v2/projects/{id}/conversion-events` | | `get_conversion_event` | One conversion event | `GET /api/v2/projects/{id}/conversion-events/{eventId}` | | `list_conversions` | Recorded conversions; filter by event, creator, account, video, source, status and time | `GET /api/v2/projects/{id}/conversions` | | `get_conversion` | One recorded conversion | `GET /api/v2/projects/{id}/conversions/{conversionId}` | | `get_conversion_summary` | Count and value of conversions in a range, one row per currency | `GET /api/v2/analytics/aggregate/conversion-summary` | | `list_top_converters` | Creators ranked by conversion value | `GET /api/v2/analytics/aggregate/conversion-leaderboard` | | `get_conversion_timeseries` | Conversions per day, one row per day and currency | `GET /api/v2/analytics/aggregate/conversion-timeseries` | **Payouts** | Tool | What it does | REST equivalent | |------|--------------|-----------------| | `list_payout_structures` | Payout structures with their current rule: type, period, amount and `criteria_params` (for CPM: tiers, minimum, cap and `view_lifetime_days`); filter by project and active state | `GET /api/v2/payout-structures` | | `get_payout_structure` | One structure with its current rule, earlier versions and assignment count | `GET /api/v2/payout-structures/{id}` | | `list_payout_assignments` | Creators assigned to a structure | `GET /api/v2/payout-structures/{id}/assignments` | | `list_payout_accruals` | Earned / paid / rejected / voided payout accruals; filter by project, creator, structure, status, period | `GET /api/v2/payout-accruals` | | `get_payout_accrual` | One accrual, including how it was calculated (`inputs_snapshot`) | `GET /api/v2/payout-accruals/{id}` | | `list_payout_methods` | The org's payout methods (PayPal, Venmo, …) | `GET /api/v2/payout-methods` | | `get_payout_method` | One payout method | `GET /api/v2/payout-methods/{id}` | | `list_payout_payments` | Recorded payments and the accruals each settled; filter by project, creator, status, date | `GET /api/v2/payout-payments` | | `get_payout_payment` | One recorded payment | `GET /api/v2/payout-payments/{id}` | | `get_payout_account` | Whether a creator can be paid through the connected provider (Dots), who they're paid as, and the org's agent-payment switch and limits. **Org admin only.** | `GET /api/v2/creators/{id}/payee-links` | **Notes and documents** | Tool | What it does | REST equivalent | |------|--------------|-----------------| | `list_notes` | Every document with text in the org (organization, projects, campaigns, creators, videos, payout methods, structures, payments and members), with a preview; filter by type, project or text | `GET /api/v2/notes` | | `get_notes` | One document in full with its `readme_version`; `entity_type=me` is the person you're helping, `member` a teammate | `GET /api/v2/notes/{entity_type}/{id}` | | `get_my_profile` | Who the connection acts as: name, org and role, scopes, and their personalization document | `GET /api/v2/me` | | `list_members` | The org's members and whether each has a personalization document | `GET /api/v2/members` | **Exports** | Tool | What it does | REST equivalent | |------|--------------|-----------------| | `export_csv` | Any CSV the app exports (creators, videos, campaigns, goals, the campaign leaderboard, the payouts tracker, payments, accruals and Insights charts) with the same columns and filters, as a file for the agent to save. See [Exporting CSVs](#exporting-csvs) | `GET /api/v2/exports/{dataset}` | **What's new in trackagoat** | Tool | What it does | REST equivalent | |------|--------------|-----------------| | `get_changelog` | Changelog entries, newest first: date, title, link to the entry in the docs and its text as Markdown. Filter by `since` (a date) and `q` (search words). Needs no scope and doesn't count toward the daily request limit. Ask your agent "what's new in trackagoat?" and it uses this tool | `GET /api/v2/changelog` | The data model is also available as the MCP resource `trackagoat://schema`. ### Write tools All require the `write` scope (**Make changes** when connecting with sign-in). A key without it gets an `insufficient_scope` error and nothing is changed; a signed-in client is asked to reconnect with **Make changes**. Tools marked **can't be undone** are annotated `destructiveHint: true`, so most clients ask before running them. Tools marked **Org admin only** need the connection to act as an org admin (`insufficient_role` otherwise). **Creators and accounts** | Tool | What it does | REST equivalent | |------|--------------|-----------------| | `add_creator` | Track a creator with one or more platform accounts; starts a profile + video-discovery scrape | `POST /api/v2/creators` | | `add_creators` | Track up to 50 creators in one project in one call | `POST /api/v2/projects/{id}/creators` | | `add_account` | Add another platform account to an existing creator | `POST /api/v2/creators/{id}/accounts` | | `update_creator` | Rename a creator or replace their `metadata` | `PATCH /api/v2/creators/{id}` | | `update_account` | Rename an account's handle, change its tracking mode, hashtags or tracking dates | `PATCH /api/v2/accounts/{id}` | | `reenable_creators` | Turn tracking back on for creators trackagoat disabled automatically. **Org admin only.** | `POST /api/v2/projects/{id}/creators/bulk-reenable` | | `remove_creator` | Stop tracking a creator and all their accounts. **Can't be undone** through MCP or the API. **Org admin only.** | `DELETE /api/v2/creators/{id}` | | `bulk_remove_creators` | The same for up to 100 creators. **Can't be undone**. **Org admin only.** | `POST /api/v2/projects/{id}/creators/bulk-remove` | | `remove_account` | Stop tracking one account of a creator. **Can't be undone** (re-add with `add_account`). **Org admin only.** | `DELETE /api/v2/accounts/{id}` | | `trigger_scrape` | Refresh one account (profile, video stats or new-video discovery), a creator's primary account, or one video now. **Org admin only.** | `POST /api/v2/accounts/{id}/scrape`, `POST /api/v2/creators/{id}/scrape`, `POST /api/v2/videos/{id}/scrape` | | `update_notes` | Add to or replace any document (`readme`) without overwriting human edits: creators, videos, campaigns, projects, the organization, payout records, or `me`. Organization, project and payout documents are **org admin only**. See [Notes and documents](#notes-and-documents) | `POST /api/v2/notes/{entity_type}/{id}` (creators and videos: `POST /api/v2/creators/{id}/notes`, `POST /api/v2/videos/{id}/notes`) | | `update_my_readme` | Save the personalization document of the person you're helping, after they approve the draft. Needs `expected_readme_version` | `PATCH /api/v2/me` | **Videos** | Tool | What it does | REST equivalent | |------|--------------|-----------------| | `add_video_by_url` | Track 1 to 50 videos by URL (TikTok or Instagram; YouTube isn't live yet) | `POST /api/v2/projects/{id}/videos` | | `update_video` | Change a video's title or tracking status (notes go through `update_notes`) | `PATCH /api/v2/videos/{id}` | | `review_inbox` | Include or exclude videos waiting in the Tracking Inbox, by id or for a whole creator | `PATCH /api/v2/projects/{id}/inbox` | | `set_video_tracking` | Stop or resume stats tracking for one video, or clear its age-rule exemption. **Org admin only.** | `POST /api/v2/videos/{id}/tracking-disable`, `…/tracking-enable`, `…/tracking-unexempt` | | `bulk_set_video_tracking` | Stop or resume tracking for up to 200 videos (optionally exempting them from age rules). **Org admin only.** | `POST /api/v2/projects/{id}/videos/bulk-tracking-disable`, `…/bulk-tracking-enable` | **Campaigns, goals, content groups and share pages** | Tool | What it does | REST equivalent | |------|--------------|-----------------| | `create_campaign` | Create an empty campaign in a project | `POST /api/v2/projects/{id}/campaigns` | | `update_campaign` | Rename a campaign, replace its `readme` (pass `expected_readme_version` to get `notes_conflict` instead of overwriting a newer edit), or set its view target and deadline | `PATCH /api/v2/campaigns/{id}` | | `add_to_campaign` | Add a creator, video or sub-campaign to a campaign | `POST /api/v2/campaigns/{id}/items` | | `remove_from_campaign` | Take a creator, video or sub-campaign out of a campaign (the item itself is kept) | `DELETE /api/v2/campaigns/{id}/items` | | `delete_campaign` | Permanently delete a campaign and its membership list. **Can't be undone**. **Org admin only.** | `DELETE /api/v2/campaigns/{id}` | | `create_goal` | Give a creator a posts / views / engagement / followers goal per day, week or month | `POST /api/v2/creators/{id}/goals` | | `update_goal` | Change a goal's metric, period, target or dates, or pause it (`is_active: false`) | `PATCH /api/v2/creators/{id}/goals/{goalId}` | | `delete_goal` | Permanently delete a goal. **Can't be undone**, and goal-based payouts for periods not yet calculated are judged without it; ending it with `update_goal` (`end_date`) is safer | `DELETE /api/v2/creators/{id}/goals/{goalId}` | | `create_content_group` | Link the same content posted on several platforms | `POST /api/v2/content-groups` | | `update_content_group` | Rename a content group, change its notes, add or remove videos | `PATCH /api/v2/content-groups/{id}` | | `delete_content_group` | Permanently delete a content group (its videos are kept). **Can't be undone** | `DELETE /api/v2/content-groups/{id}` | | `create_share_link` | Create or replace a creator's PIN-protected page; returns the url and PIN. **Org admin only.** | `POST /api/v2/creators/{id}/share-link` | | `update_share_link` | Turn a share page off or on, change its sections, or set a new PIN. **Org admin only.** | `PATCH /api/v2/creators/{id}/share-link` | | `bulk_create_share_links` | Create share pages for up to 200 creators; returns every url and PIN. **Org admin only.** | `POST /api/v2/share-links/bulk` | | `delete_share_link` | Permanently delete a creator's share page. **Can't be undone**; prefer `update_share_link` with `enabled: false`. **Org admin only.** | `DELETE /api/v2/creators/{id}/share-link` | | `create_annotation` | Add a dated note to a creator's, campaign's or video's charts | `POST /api/v2/analytics/annotations` | | `update_annotation` | Change an annotation someone added | `PATCH /api/v2/analytics/annotations/{id}` | | `delete_annotation` | Permanently delete an annotation someone added. **Can't be undone** | `DELETE /api/v2/analytics/annotations/{id}` | **Comment settings** | Tool | What it does | REST equivalent | |------|--------------|-----------------| | `update_comment_settings` | Set the minimum comment count and replace the brand or competitor terms behind the `brand_mention` and `competitor` signals. **Org admin only.** | `PATCH /api/v2/comment-settings` | **Conversions** | Tool | What it does | REST equivalent | |------|--------------|-----------------| | `create_conversion_event` | Define a kind of conversion (e.g. `signup`) with a default value | `POST /api/v2/projects/{id}/conversion-events` | | `update_conversion_event` | Change an event's name, description, default value or metadata, or deactivate / reactivate it | `PATCH /api/v2/projects/{id}/conversion-events/{eventId}` (deactivating is the same as `DELETE …/{eventId}`) | | `record_conversions` | Record 1 to 500 conversions credited to creators, accounts or videos; `external_id` dedupes repeats | `POST /api/v2/projects/{id}/conversions` | | `void_conversion` | Void a conversion (refund, chargeback, fraud): excluded from totals, kept for audit. **Can't be undone**. **Org admin only.** | `DELETE /api/v2/projects/{id}/conversions/{conversionId}` (`mode=void`) | **Payouts** | Tool | What it does | REST equivalent | |------|--------------|-----------------| | `create_payout_structure` | Create a payout rule: recurring, goals, CPM, view-threshold bonus or per conversion. **Org admin only.** | `POST /api/v2/payout-structures` | | `update_payout_rule` | Replace a structure's rule with a new version, effective from the period in progress. **Org admin only.** | `PATCH /api/v2/payout-structures/{id}` (`kind: "rule"`) | | `set_cpm_view_lifetime` | Set how long after posting a video keeps earning on a CPM structure (`view_lifetime_days`, or `null` for forever). Saves a new version with nothing else changed, effective from the period in progress. Counts as two API requests. **Org admin only.** | `GET` then `PATCH /api/v2/payout-structures/{id}` | | `update_payout_structure` | Rename a structure, replace its notes or metadata, or un-archive it. **Org admin only.** | `PATCH /api/v2/payout-structures/{id}` (`kind: "cosmetic"`) | | `archive_payout_structure` | Stop a structure earning and end every assignment; past accruals stay payable. Un-archiving doesn't bring the assignments back. **Org admin only.** | `DELETE /api/v2/payout-structures/{id}` (`mode=archive`) | | `delete_payout_structure` | Permanently delete a structure that never produced an accrual. **Can't be undone**. **Org admin only.** | `DELETE /api/v2/payout-structures/{id}?mode=hard` | | `assign_payout_structure` | Put creators on a structure, from now or a later date. **Org admin only.** | `POST /api/v2/payout-structures/{id}/assignments` | | `unassign_payout_structure` | Take creators off a structure; what they earned stays payable. **Org admin only.** | `DELETE /api/v2/payout-structures/{id}/assignments` | | `set_accrual_status` | Reject or void an earned accrual so it is no longer owed. **Can't be undone**. **Org admin only.** | `PATCH /api/v2/payout-accruals/{id}` | | `create_payout_method` | Add a payout method (e.g. Wise). **Org admin only.** | `POST /api/v2/payout-methods` | | `update_payout_method` | Rename a method, replace its notes or metadata, set or remove its icon (`icon_path`), or disable / re-enable it. **Org admin only.** | `PATCH /api/v2/payout-methods/{id}` | | `delete_payout_method` | Permanently delete a payout method. **Can't be undone**; prefer disabling it. **Org admin only.** | `DELETE /api/v2/payout-methods/{id}` | | `record_payout_payment` | Record a payment made outside trackagoat and mark the accruals it covers as paid, optionally with a receipt (`payment_id` + `attachment_path`). It doesn't send money. Requires `idempotency_key`. **Org admin only.** | `POST /api/v2/payout-payments` | | `update_payout_payment` | Replace a payment's notes or metadata. **Org admin only.** | `PATCH /api/v2/payout-payments/{id}` | | `send_payout` | **Sends real money** through the org's connected provider (Dots) and records the payment. Only works after an org admin turns on agent payments, within the org's limits. Supports a note to the creator, SMS text, description and 1099 exemption. Requires `idempotency_key`. **Org admin only.** | `POST /api/v2/payout-payments/send` | | `void_payout_payment` | Void a payment; its accruals and supplemental amounts go back to owed. **Can't be undone**. **Org admin only.** | `PATCH /api/v2/payout-payments/{id}` (`status: "voided"`) | | `get_payouts_tracker` | Per-creator earned, paid, adjustments and outstanding for a project, with totals (earned − paid + adjustments = outstanding) | `GET /api/v2/payouts/tracker` | | `list_payout_supplementals` | List supplemental amounts (owed corrections on top of paid accruals) | `GET /api/v2/payout-supplementals` | | `create_payout_supplemental` | Record that a creator is owed more for a paid accrual; settled by `record_payout_payment`. Requires `idempotency_key`. **Org admin only.** | `POST /api/v2/payout-supplementals` | | `void_payout_supplemental` | Void an owed supplemental amount. **Can't be undone**. **Org admin only.** | `PATCH /api/v2/payout-supplementals/{id}` (`status: "void"`) | **Receipts and method icons** | Tool | What it does | REST equivalent | |------|--------------|-----------------| | `upload_receipt` | Upload a payment receipt (up to 3 MB) as base64; returns the `path` to pass to `record_payout_payment`. **Org admin only.** | `POST /api/v2/payouts/payment-receipts` | | `get_receipt_upload_url` | Get a signed URL to PUT a receipt of up to 5 MB to (valid 2 hours), and the `path` to pass back. **Org admin only.** | `POST /api/v2/payouts/payment-receipts/upload-url` | | `upload_payout_method_icon` | Upload a PNG, JPEG or WebP icon (up to 1 MB) as base64 and set it on a payout method. **Org admin only.** | `POST /api/v2/payout-methods/{id}/icon` | | `get_payout_method_icon_upload_url` | Get a signed URL to PUT an icon to, and the `path` to set with `update_payout_method`. **Org admin only.** | `POST /api/v2/payout-methods/{id}/icon/upload-url` | To attach a receipt, the agent generates a UUID as `payment_id`, uploads the file under it (`upload_receipt`, or `get_receipt_upload_url` and a `PUT` of the raw bytes with the same `Content-Type`), then calls `record_payout_payment` with that `payment_id` and `attachment_path` set to the returned `path`. A receipt can only be attached when the payment is recorded, as in the app. Allowed receipt types: PNG, JPEG, WebP, PDF, CSV, Word and Excel. Inline uploads are checked: the file's contents must match `content_type`. Tools that create something or act in bulk accept an optional `idempotency_key` (it is sent as the REST `Idempotency-Key` header). Have the agent pass a fresh UUID per logical write: if a call times out and is retried with the same key and arguments within 24 hours, you get the original result (marked `"replayed": true`) instead of a duplicate. A retry while the first call is still running fails with `idempotency_in_progress` (try again in a couple of seconds), and reusing a key with different arguments fails with `idempotency_key_reused`. `record_payout_payment`, `send_payout` and `create_payout_supplemental` require a key. Tools that set a value (`update_*`, `set_*`, deletes, voids, assignments) have no key because repeating them has the same effect; `set_cpm_view_lifetime` takes one anyway. Three creates have no key because their REST endpoints don't support one: `create_payout_structure` and `create_annotation` (check the list before retrying) and `create_payout_method` (names are unique, so a repeat fails with `conflict_name`). `trigger_scrape` has no key: it's limited by the manual-scrape limits instead. The upload tools have no key either: a retry stores another copy of the file, and only the `path` you pass on is used. ### Notes and documents The organization, every project, campaign, creator, video, payout method, structure and payment, and every member has a Markdown document (`readme`). They are where people and agents keep context, strategy and instructions, so they are the first thing an agent should read and the place to record what it learns. `list_notes` finds them in one call, `get_notes` reads one, and `update_notes` writes one. Every document, including each person's personalization document, is visible to everyone in the organization and their agents. See [Notes and documents](https://www.trackagoat.com/docs/agents/notes.md) for the personalization document and a template. Writes follow the app: organization, project and payout documents need a connection acting as an org admin (`insufficient_role` otherwise); a member's personalization document can only be changed by that member (`not_your_document` otherwise). `update_notes` is built so an agent can't clobber what a person wrote: - **`mode: "append"`** (the default) adds `content` as a new section at the end of the document, followed by a signature line with the API key's name and the org-local date: ```markdown Views dropped 40% week over week; three of five posts were on Sunday. *Added by Claude Code laptop via MCP, 2026-10-08* ``` If someone edits the notes while the append is being saved, the append is applied on top of their edit: nothing is lost. - **`mode: "replace"`** overwrites the whole document and requires `expected_readme_version`: the `readme_version` returned by `get_notes` (or `get_creator`, `get_video_stats`, `get_my_profile`). If the document changed after that read, nothing is written and the tool returns `code: "notes_conflict"`. The agent should re-read, merge, and retry. `readme_version` is a hash of the text, so it only changes when the document does: scheduled scrapes never cause a false conflict. Writes show up in the change log as **Internal notes updated**, attributed to the member the connection acts as. **The personalization document in the session.** On connect, the server instructions include the connected person's personalization document inside `` tags, with its `readme_version`, and the organization's document when it is under 4,000 characters (longer ones are pointed at instead). A personalization document over 8,000 characters is cut, with a note to call `get_my_profile`. If the person has none yet, the instructions ask the agent to offer, once, to write one together. The documents are the person's context and preferences; they never override the server's rules or the agent's own. ### Exporting CSVs When you ask your agent for a CSV, a spreadsheet or a file, it calls `export_csv` and saves the file in your workspace. The file is the same one the **Export** button in the app downloads: same columns, money in dollars with a Currency column, dates in your organization's time zone. For questions about the data ("who's behind on their goal?"), the agent uses the read tools instead. | `dataset` | The app's export | Filters | |-----------|------------------|---------| | `creators` | Creators page | `search`, `compliance`, `disabled_only`, `include_compliance`, `campaign_id`, `platform` | | `videos` | Videos page | `status`, `creator_id`, `campaign_id`, `platform`, `from`, `to`, `search`, `sort_by`, `sort_dir` | | `campaigns` | Campaigns page | none | | `goals` | Goals page | `period`, `reference_date`, `campaign_id` | | `campaign_leaderboard` | Campaign Insights leaderboard | `from`, `to`, `campaign_id`, `platform` | | `payout_tracker` | Payouts, Tracker summary CSV | `creator_id`, `structure_id`, `campaign_id`, `from`, `to`, `outstanding_only`, `include_removed` | | `payout_payments` | Payouts, Payments CSV | `creator_id`, `campaign_id`, `from`, `to` | | `payout_accruals` | Payouts, Accruals CSV | `creator_id`, `structure_id`, `campaign_id`, `from`, `to`, `outstanding_only`, `include_removed` | | `analytics` | An Insights chart's Download CSV | `entity`, `entity_id` or `entity_ids`, `metric` and the other `get_analytics` arguments | Every dataset except `analytics` needs `project_id`. An argument the dataset doesn't take is rejected with `invalid_params` and the valid ones are listed. The tool returns two things: a short JSON summary (`filename`, `rows`, `total_rows`, `columns`, the `filters` used, `next_offset`) and the CSV itself as an embedded `text/csv` resource (`trackagoat://exports/-.csv`). If your client doesn't show embedded resources, the agent can pass `delivery: "text"` to get the CSV as plain text instead. Each call returns up to about 1 MB. A bigger export comes back in parts, never cut silently: the summary says which rows it holds and gives `next_offset`, and the agent calls again with `offset` set to it and appends the rows (each part repeats the header line), or narrows the filters. Each call counts as one API request. Examples: - "Export September payouts to a CSV." The agent calls `export_csv` with `dataset: "payout_tracker"`, the project, `from: "2026-09-01"` and `to: "2026-09-30"`, then saves `payouts-tracker-….csv`. - "Give me all videos from the *Spring Launch* campaign as a spreadsheet." The agent finds the campaign with `list_campaigns`, then calls `export_csv` with `dataset: "videos"` and its `campaign_id`. ### What the MCP server doesn't do Everything an organization's API key can do through `/api/v2` can also be done over MCP, except the following, which are left out on purpose: | Not available over MCP | Why | |------|-----| | Creating and revoking API keys (`/api/v2/organizations/{orgId}/api-keys`) | Keys are credentials. An agent shouldn't mint or revoke them; an org admin does that in **Org settings → API keys**. | | Organization administration: creating an organization, creating or renaming projects, inviting, changing or removing members, cancelling invitations (`/api/v2/user/organizations`, `/api/v2/organizations/{orgId}/…`) | These need the `admin` scope. Sign-in connections never get it, and MCP tools only use `read` and `write`. Creating an organization also acts outside the connected organization. | | Permanently deleting a conversion or conversion event (`mode=hard`) | Needs the `admin` scope, because it erases revenue history. Use `void_conversion` or `update_conversion_event` with `is_active: false`. | | Removing a video (`DELETE /api/v2/videos/{id}`) | Use `set_video_tracking` or `review_inbox` to stop counting a video (or `update_video` with `tracking_status: "excluded"`, which is what the REST `DELETE` does). | | trackagoat staff endpoints (`/api/v2/admin/…`) | Platform administration, not part of an organization's data. | | The OpenAPI documents (`/api/v2/openapi.json`, `/api/v2/analytics/schema`) | They describe the REST API itself; MCP clients get each tool's schema from the server, and `get_schema` describes the data model. | | The retired `/api/v2/creators/{id}/stats` and `/api/v2/campaigns/{id}/stats` | They return `410 Gone`. Use `get_analytics`. | ## Limits - **Each tool call counts as one API request** toward your org's daily `max_api_requests_per_day` limit and the per-key limit of 60 requests per minute, the same as a REST call. Connecting and listing tools are free. A signed-in connection is limited like one key. - `get_changelog` is the exception: it reads public content, so it never counts toward the daily limit. It has its own limit of 60 calls per minute per key or connection. - Protocol traffic (connect, list tools, ping) is capped at 120 requests per minute per key or connection. - `trigger_scrape` also counts against your plan's daily manual-scrape limit. - `add_creator`, `add_creators`, `add_account` and `add_video_by_url` count against your plan's account and video limits; `create_campaign`, `create_conversion_event`, `create_payout_structure` and `create_payout_method` count against their plan limits. - `set_cpm_view_lifetime` counts as two API requests (it reads the structure, then saves it). - Analytics ranges can't start before your plan's data-retention window. When a limit is hit the tool returns an error with `code: "rate_limit_exceeded"` and `retry_after_seconds`. See [Limits & plan tiers](https://www.trackagoat.com/docs/guides/limits.md). ## Errors Tool failures come back as MCP tool errors (`isError: true`) whose text is JSON: ```json { "error": "add_creator requires the 'write' scope. Granted: [read]", "code": "insufficient_scope", "status": 403, "hint": "This API key lacks the 'write' scope. An org admin can create a key with that scope in Org settings → API keys." } ``` `code` and `status` match the [REST error codes](https://www.trackagoat.com/docs/api-reference/errors.md); every error has a `code`, so an agent can branch on it instead of parsing `error`. When the route returns extra context, it's in `details`. Codes you'll see most: | Code | Status | Meaning | |------|--------|---------| | `unknown_parameter` | 400 | The call passed a parameter the tool doesn't have. `error` names it and lists the valid ones; `details` has `unknown_parameters`, `valid_parameters` and, for near-misses like `projectId`, `did_you_mean`. Nothing ran. | | `invalid_params` | 400 | A parameter has the wrong type or value; `details` lists the fields. | | `already_tracked` | 409 | `add_creator`: a handle is already tracked in the project. `error` names the handle and the creator; `details` has `creator_id` and `account_id`. Nothing was created. | | `duplicate_account` | 400 | `add_creator`: the same platform + handle appears twice in `accounts`. | | `not_found` | 404 | No such id in your organization. | | `insufficient_scope` | 403 | The key or connection lacks the scope the tool needs. | | `insufficient_role` | 403 | Only org admins can do this in the app, and the connection acts as a member who isn't one: for example payments and payout setup, removing or re-enabling creators, removing accounts, stopping or resuming video tracking, `trigger_scrape`, organization and project documents, share pages, `send_payout`, `get_payout_account` and the comment settings. Every such tool says **Org admin only** above and in its description. | | `not_your_document` | 403 | `update_notes` on another member's personalization document. Only its owner can change it; read it with `get_notes`. | | `notes_conflict` | 409 | A replace whose `expected_readme_version` is out of date. Nothing was written: re-read, merge, retry. | | `plan_feature_unavailable` | 403 | The organization's plan doesn't include the feature: Comment Insights tools on the Free plan. Retrying won't help. | | `limit_reached` | 402 | A plan limit; `details` has the limit and current usage. | | `rate_limit_exceeded` | 429 | Wait `retry_after_seconds`. | ```json { "error": "Unknown parameter 'range'. Valid parameters: entity, entity_id, entity_ids, metric, granularity, bucket_days, mode, from, to, compare, breakdown, top_n, base_metric.", "code": "unknown_parameter", "status": 400, "hint": "get_analytics accepts only the parameters listed above; unknown ones are rejected rather than ignored. Remove or rename them and call again.", "details": { "unknown_parameters": ["range"], "valid_parameters": ["entity", "entity_id", "…"] } } ``` An invalid, expired or revoked key is rejected before any tool runs, with HTTP `401`; your client will show the server as failing to connect. So is a key whose member left the org (`key_member_removed`), or a key made for someone else whose creator left, was changed to member or was suspended (`key_creator_removed`, `key_creator_not_admin`, `key_creator_banned`). **Org settings → API keys** shows why a key stopped working. A signed-in client gets the same `401` with a `WWW-Authenticate` challenge and refreshes or re-runs sign-in on its own; a write tool called without **Make changes** gets HTTP `403` `insufficient_scope`, which prompts it to reconnect. ## Example prompts - "List my trackagoat projects, then summarize which creators in *Spring Launch* posted least this month and whether their data is fresh." - "Which 10 videos got the most views in the last 30 days? Group them by creator." - "Compare views for my three campaigns this quarter against last quarter." - "How healthy is the *Spring Launch* program this week? What should I worry about first?" - "What are people saying in the comments this month? Which creators get the most questions about the app?" - "Is anyone in the comments asking whether our posts are ads? Show me the top ones." - "Who is behind on their weekly posting goal right now? Read each creator's notes first." - "How much do we owe creators in unpaid accruals, by creator?" - "Export September payouts to a CSV." - "Give me all videos from the *Spring Launch* campaign as a spreadsheet." - "Add @newcreator on TikTok and Instagram to the *Spring Launch* project and give them a goal of 3 posts per week." (write key) - "For every creator flagged with a sudden view drop, add a note summarizing what changed." (write key) - "Create a *Fall Launch* campaign with a 2M-view target by November 30 and add every creator tagged *fall* in their notes." (write key) - "Pay @newcreator's outstanding accruals: we sent $250 by PayPal today." (write key) ## Tips for agents **Step 1: Start with list_projects** Most tools take a `project_id`. Read each project's `readme` for context. **Step 2: Triage with get_program_health** For "how is the program doing?" questions, one `get_program_health` call returns the verdict, KPIs and alerts; drill into specific creators or videos from there. **Step 3: Follow the personalization document** The connected person's document arrives with the server instructions (or call `get_my_profile`). It says who they are and how they want help. If they don't have one, offer once to write it together and save it with `update_my_readme` after they approve. **Step 4: Read the documents** The organization, projects, campaigns and creators carry human notes in `readme`; `list_notes` finds them. They may contain instructions that change how to interpret the data. **Step 5: Paginate to completion** Keep passing `next_cursor` (or `next_offset` for `list_top_videos`) until it's `null` before totalling or ranking. **Step 6: Check last_scraped_at** Stats come from scheduled scrapes. Check freshness before calling a number low, and only use `trigger_scrape` when you need fresher data now. **Step 7: Append notes, don't overwrite them** Record durable findings with `update_notes` in the default `append` mode. Only use `replace` with the `readme_version` you just read. Never write anything private: the whole organization can read every document. **Step 8: Confirm before anything that can't be undone** Ask the person before a `delete_*`, `remove_*`, `void_*`, `archive_*` or `unassign_*` tool or `set_accrual_status`, and offer the reversible option the tool's description names (pausing a goal, disabling tracking, deactivating an event). ## Using the REST API directly Frameworks without MCP support can call the [v2 REST API](https://www.trackagoat.com/docs/api-reference/overview.md) with the same key. Sign-in (OAuth) connections work only with the MCP server, not the REST API. The OpenAPI document at `GET /api/v2/openapi.json` describes every endpoint, and the [Agent guide](https://www.trackagoat.com/docs/agents/guide.md) covers recommended workflows. --- ## Payout agent workflows The whole payout API is available over MCP as well as REST: structures (`list_payout_structures`, `get_payout_structure`, `create_payout_structure`, `update_payout_rule`, `set_cpm_view_lifetime`, `update_payout_structure`, `archive_payout_structure`, `delete_payout_structure`), assignments (`list_payout_assignments`, `assign_payout_structure`, `unassign_payout_structure`), accruals (`list_payout_accruals`, `get_payout_accrual`, `set_accrual_status`), methods (`list_payout_methods`, `create_payout_method`, `update_payout_method`, `delete_payout_method`) payments (`list_payout_payments`, `get_payout_payment`, `record_payout_payment`, `update_payout_payment`, `void_payout_payment`), the tracker (`get_payouts_tracker`), supplemental amounts (`list_payout_supplementals`, `create_payout_supplemental`, `void_payout_supplemental`), sending payouts through Dots (`get_payout_account`, `send_payout`; see [Paying with Dots](https://www.trackagoat.com/docs/guides/integrated-payouts.md#let-agents-send-payouts)), and receipts and method icons (`upload_receipt`, `get_receipt_upload_url`, `upload_payout_method_icon`, `get_payout_method_icon_upload_url`). The workflows below show the REST calls; each step names the MCP tool that does the same. **Reads need the `read` scope; writes (recording payments, voiding, assigning structures) need the `write` scope**: a read-only key gets `403 insufficient_scope` on any mutation. See the [Payouts API reference](https://www.trackagoat.com/docs/api-reference/payouts.md) for full endpoint documentation. ### Workflow 1: Weekly payout review **Goal:** find all creators with outstanding accruals, summarize total owed, and surface any that have been outstanding more than 30 days. Over MCP: `list_projects`, then `list_payout_accruals` with `status: "earned"`. **Step 1: Discover the project** ```bash curl -s -H "Authorization: Bearer $TGA_KEY" \ "https://www.trackagoat.com/api/v2/projects" | jq '.data[0].id' ``` Store the project ID as `$PROJECT_ID`. **Step 2: List outstanding accruals** ```bash curl -s -H "Authorization: Bearer $TGA_KEY" \ "https://www.trackagoat.com/api/v2/payout-accruals?project_id=$PROJECT_ID&status=earned" \ | jq '{total: (.data | length), creators: [.data[] | .creator_id] | unique | length}' ``` Paginate using `meta.nextCursor` until `meta.hasMore` is false. **Step 3: Flag stale accruals** Filter `data[].period_end_at` older than 30 days ago. Report creator handle + amount + how many days outstanding. **Expected response shape:** ```json { "data": [{ "id": "uuid", "creator_id": "uuid", "structure_id": "uuid", "amount_cents": 5000, "currency": "USD", "status": "earned", "period_start_at": "2026-04-07T00:00:00Z", "period_end_at": "2026-04-13T23:59:59Z" }], "meta": { "hasMore": false, "nextCursor": null } } ``` > **Warning: Common pitfalls** > > - `amount_cents` is an integer in cents: divide by 100 for display. > - Voided and rejected accruals are excluded by the `status=earned` filter; don't double-count by omitting it. --- ### Workflow 2: Audit unpaid creators across an org **Goal:** identify which creators have earned money but received zero payments, broken down by project. Over MCP: `list_projects`, `list_payout_structures`, `list_payout_accruals` and `list_payout_payments`. **Step 1: List all projects** ```bash curl -s -H "Authorization: Bearer $TGA_KEY" \ "https://www.trackagoat.com/api/v2/projects" | jq '[.data[] | {id, name}]' ``` **Step 2: For each project, fetch payout structures** ```bash curl -s -H "Authorization: Bearer $TGA_KEY" \ "https://www.trackagoat.com/api/v2/payout-structures?project_id=$PROJECT_ID&is_active=true" ``` **Step 3: Fetch earned accruals per project** ```bash curl -s -H "Authorization: Bearer $TGA_KEY" \ "https://www.trackagoat.com/api/v2/payout-accruals?project_id=$PROJECT_ID&status=earned" ``` **Step 4: Fetch all payments** ```bash curl -s -H "Authorization: Bearer $TGA_KEY" \ "https://www.trackagoat.com/api/v2/payout-payments?project_id=$PROJECT_ID" ``` **Step 5: Compute the gap** Group accruals by `creator_id`. For each creator, sum `amount_cents` of `earned` accruals. Compare against `total_amount_cents` of `paid` payments. Report the delta. > **Warning: Common pitfalls** > > - Don't compare across currencies: all amounts are `USD` (cents). The `currency` field is included for future-proofing. > - A creator with no payment rows is fully outstanding; don't confuse "no payments" with "no debt." --- ### Workflow 3: Record a payment for a creator via Venmo **Goal:** settle outstanding accruals for a specific creator and record the payment against the Venmo method. Over MCP: `list_creators` with `search`, `list_payout_accruals`, `list_payout_methods`, optionally `upload_receipt`, then `record_payout_payment` with a fresh `idempotency_key` (the tool requires one). **Step 1: Find the creator** ```bash curl -s -H "Authorization: Bearer $TGA_KEY" \ "https://www.trackagoat.com/api/v2/creators?project_id=$PROJECT_ID&search=creatorhandle" \ | jq '.data[0].id' ``` **Step 2: List outstanding accruals for this creator** ```bash curl -s -H "Authorization: Bearer $TGA_KEY" \ "https://www.trackagoat.com/api/v2/payout-accruals?project_id=$PROJECT_ID&status=earned" \ | jq '[.data[] | select(.creator_id == $CREATOR_ID)]' ``` Sum the `amount_cents` values to get `total_amount_cents`. **Step 3: Find the Venmo method ID** ```bash curl -s -H "Authorization: Bearer $TGA_KEY" \ "https://www.trackagoat.com/api/v2/payout-methods" \ | jq '.data[] | select(.name == "Venmo") | .id' ``` **Step 4: Record the payment** ```bash curl -s -X POST -H "Authorization: Bearer $TGA_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ "https://www.trackagoat.com/api/v2/payout-payments" \ -d '{ "creator_id": "CREATOR_UUID", "method_id": "VENMO_METHOD_UUID", "total_amount_cents": 5000, "currency": "USD", "accrual_ids": ["ACCRUAL_UUID_1", "ACCRUAL_UUID_2"], "note": "Recorded by automated weekly review agent", "paid_at": "2026-05-07T15:00:00Z" }' ``` **Expected 201 response:** ```json { "data": { "id": "payment-uuid", "creator_id": "...", "total_amount_cents": 5000, "status": "paid", "paid_at": "2026-05-07T15:00:00Z" } } ``` > **Warning: Common pitfalls** > > - An `Idempotency-Key` header (a UUID) is **required**; the request is rejected with 400 without one. If the request times out, retry with the same key: the server returns the original payment row without double-counting. > - If `total_amount_cents` differs from the sum of linked accruals, the API requires an `adjustment_reason` field explaining the difference (e.g. rounding, partial payment). > - Verify the method is active (`is_active: true`) before sending: pass `include_disabled=1` to `GET /api/v2/payout-methods` to confirm. A method that has been hard-removed returns `404 not_found`. --- ### Workflow 4: Set up a new creator with a goal-based payout structure **Goal:** assign an existing payout structure to a newly-tracked creator and confirm the assignment is active. Over MCP: `list_payout_structures`, `assign_payout_structure`, then `list_payout_assignments`. **Step 1: Confirm the structure exists and is active** ```bash curl -s -H "Authorization: Bearer $TGA_KEY" \ "https://www.trackagoat.com/api/v2/payout-structures?project_id=$PROJECT_ID&is_active=true" \ | jq '[.data[] | select(.criteria_type == "goals") | {id, name, period, amount_cents}]' ``` Pick the structure ID you want to assign. **Step 2: Assign the creator** ```bash curl -s -X POST -H "Authorization: Bearer $TGA_KEY" \ -H "Content-Type: application/json" \ "https://www.trackagoat.com/api/v2/payout-structures/$STRUCTURE_ID/assignments" \ -d '{"creator_ids": ["CREATOR_UUID"]}' ``` **Expected 201 response:** ```json { "data": { "assigned": 1, "already_assigned": 0, "not_in_project": 0, "results": [{ "creator_id": "...", "status": "assigned", "assignment_id": "..." }] } } ``` **Step 3: Verify the assignment is active** ```bash curl -s -H "Authorization: Bearer $TGA_KEY" \ "https://www.trackagoat.com/api/v2/payout-structures/$STRUCTURE_ID/assignments" \ | jq '[.data[] | select(.unassigned_at == null) | .creator_id]' ``` > **Warning: Common pitfalls** > > - For goal-based structures, the creator must have at least one goal whose period matches the structure's period. If not, the assignment is created but accruals won't be computed. Check with `get_goal_compliance` or the goal compliance endpoint. > - If the creator was previously unassigned from this structure, a new assignment row is created: past accruals from the prior assignment remain payable. > - `assigned: 0, already_assigned: 1` means the creator is already active on this structure: not an error. --- Source: https://www.trackagoat.com/docs/agents/mcp --- # Notes and documents > The Markdown documents people and AI agents keep in trackagoat, including each person's personalization document, and how agents read and write them. Every organization, project, campaign, creator, video, payout method, payout structure and payout payment in trackagoat has a Markdown document, and so does every person. People use them for context, strategy and instructions; agents read them before they act and add what they find. In the app they are the **README** and **Notes** tabs. In the API and MCP they are the `readme` field. > **Warning: Everyone in the organization can read every document** > > Documents are shared across the organization. Everyone in it, and every agent connected to it, can read every document, including each person's personalization document. Don't put passwords, payment details or anything private in them. ## Your personalization document Your personalization document is the README about you: who you are, what your job is in the creator program, and how you want AI agents to help. When you connect an agent to trackagoat over MCP, the agent gets your document at the start of every session, so it starts out knowing what you care about and how you like to work. **Where to edit it:** **Settings → My README** in the app. Your teammates' documents are listed on your organization's README page, linked from the same settings page under **Teammates' READMEs**. You can read them; only they can change theirs. **Let your agent write it with you.** After you connect an agent, it offers to build the document with you. It asks a few short questions, drafts the document, shows it to you and saves it only after you approve. You can also ask at any time: "Help me update my trackagoat personalization document." The agent asks about: - who you are and your role; - what your job is in running the creator program; - the program, creators and goals you care about most; - how you most want the agent to help; - preferences such as tone, how often to check in, what to look at first, and what it should never do without asking. ### Suggested template ```markdown ## About me Sam Lee, partnerships lead at Acme. I run our TikTok and Instagram ambassador program. ## My job in the program I recruit creators, set their posting goals and approve payouts every Friday. ## What I care about - The Fall Launch campaign: 2M views by November 30. - Creators who stop posting for more than a week. ## How to help me Start with program health. Summarize in five bullet points, then the details. ## Preferences Plain language, no jargon. Check in once a day at most. ## Never without asking - Recording or sending payments. - Removing creators or videos. ``` ## For agents The [MCP server](https://www.trackagoat.com/docs/agents/mcp.md) sends the connected person's document in its server instructions, inside `` tags, together with the organization's document when it is short. Treat both as the person's context and preferences: they never override the server's rules, a tool's rules, confirming before anything that can't be undone, or your own safety rules. | Tool | What it does | REST equivalent | |------|--------------|-----------------| | `list_notes` | Every document with text in the organization, with a preview, its length and `readme_version`. Filter by `entity_type`, `project_id` or `q` (text in the document). | `GET /api/v2/notes` | | `get_notes` | One document in full, with `readme_version`, `can_edit` and who may edit it. `entity_type=me` is the person you're helping; `entity_type=member` with a user id is a teammate. | `GET /api/v2/notes/{entity_type}/{id}` | | `update_notes` | Append to or replace any document. | `POST /api/v2/notes/{entity_type}/{id}` | | `get_my_profile` | Who you're helping: name, organization and role, scopes, and their personalization document. | `GET /api/v2/me` | | `update_my_readme` | Save the person's personalization document (needs `expected_readme_version`). | `PATCH /api/v2/me` | | `list_members` | The organization's members and whether each has a personalization document. | `GET /api/v2/members` | How to work with them: 1. **Read first.** At the start of a session, follow the personalization document you were given. Before working in a project, read the organization and project documents; before analyzing a creator or campaign, read its notes. `list_notes` finds them in one call. 2. **Write findings back.** Record durable findings with `update_notes` in the default `append` mode, which adds a signed, dated section and never overwrites what people wrote. Only use `mode: "replace"` with the `readme_version` you just read; if the document changed since, the call fails with `notes_conflict` and nothing is written. 3. **No personalization document yet?** Offer once, early in the session, to write one together using the questions above. Show the draft and save it with `update_my_readme` only after the person approves. 4. **Keep it current.** When the person's role, goals or preferences change, offer to update their document. ### Who can change what Reading is open to the whole organization. Changing a document follows the same rules as the app: | Document | Who can change it | |----------|-------------------| | Organization, project | Org admins | | Payout method, payout structure, payout payment | Org admins | | Campaign, creator, video | Any member, with write access (**Make changes** when signed in) | | A person's personalization document | Only that person, or an agent they connected | An agent always acts as one member, so it can change exactly what that member can. A change it isn't allowed to make fails with `insufficient_role` or `not_your_document`, and nothing is written. See the [Notes API reference](https://www.trackagoat.com/docs/api-reference/notes.md) for the endpoints. --- Source: https://www.trackagoat.com/docs/agents/notes --- # 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=` 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=&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 --- # llms.txt and raw docs > Fetch these docs as clean Markdown, one page or all of them, so an agent can read them in a single request. Every page of these docs is available as plain Markdown. An agent can read one page, the index, or everything at once, without parsing HTML. | URL | What you get | |---|---| | [`/llms.txt`](https://www.trackagoat.com/llms.txt) | An index of every docs page with a one-line description, plus links to the OpenAPI spec, the data model and the MCP server ([llmstxt.org](https://llmstxt.org) format). | | [`/llms-full.txt`](https://www.trackagoat.com/llms-full.txt) | All public docs as Markdown in one file. | | `/docs/.md` | One page as Markdown, e.g. [`/docs/guides/payouts.md`](https://www.trackagoat.com/docs/guides/payouts.md). The docs home is [`/docs.md`](https://www.trackagoat.com/docs.md). | Markdown pages are served as `text/markdown; charset=utf-8`. Every HTML docs page also advertises its Markdown version with ``. > **For agents:** > > Start with `/llms.txt`, then fetch only the `.md` pages you need. Use `/llms-full.txt` when you want the whole manual in context. For live data, use the [REST API](https://www.trackagoat.com/docs/api-reference/overview.md) or the [MCP server](https://www.trackagoat.com/docs/agents/mcp.md); these files describe the product, they don't contain your program's data. ## How components appear in Markdown The HTML docs use a few components. In Markdown they become plain equivalents: - **Callouts** become blockquotes that start with **Note:**, **Tip:**, **Warning:**, **Important:** or **For agents:**. - **Steps** become bold **Step 1: …** labels followed by the step's text. - **Tabbed code examples** become consecutive code blocks, each preceded by its label. - **Cards** become a list of links with descriptions. - **The plan limits table** becomes a Markdown table built from the live plan values. Links to other docs pages point at their `.md` versions, so an agent can keep following them. ## Stable anchors Heading anchors (`#section-name`) are derived from heading text, and we avoid renaming published headings. If a heading is renamed, the old link still lands on the page, just without scrolling to the section. ## From the docs page Every page has a **Copy page** menu next to its title: copy the page as Markdown, open the `.md` version, or open it in Claude or ChatGPT with a prompt that points at the page. --- Source: https://www.trackagoat.com/docs/agents/llms-txt --- # Changelog > What's new in trackagoat. This changelog is also available as JSON from [`GET /api/v2/changelog`](https://www.trackagoat.com/docs/api-reference/changelog.md) (no API key needed) and to AI agents through the MCP tool `get_changelog`. ## 2026-10-11: README editors never overwrite a newer save, and the video caption in the API **Organization, project and campaign READMEs.** These editors now work like My README and the creator and video notes: if a teammate or an agent saves the README while you have it open, your next save stops and asks you to reload instead of overwriting their change. Your unsaved draft is kept, so you can copy it back in after reloading. Over the API, `PATCH /api/v2/campaigns/{id}` and `PATCH /api/v2/organizations/{orgId}/projects/{projectId}` take an optional `expected_readme_version` with `readme` and return **409** `notes_conflict` if the README changed since your read. See [Campaigns](https://www.trackagoat.com/docs/api-reference/campaigns.md) and [Projects](https://www.trackagoat.com/docs/api-reference/projects.md). **Video `caption`, and `description` is deprecated.** `GET /api/v2/videos` and `GET /api/v2/videos/{id}` now return `caption`, the post's text from the platform, and MCP `list_videos` and `get_video_stats` show it. A video's `description` field was an old free-text field that nothing fills; the docs called it the caption in some places and a notes field in others. It can no longer be written: `PATCH /api/v2/videos/{id}` refuses it with `400` and MCP `update_video` no longer takes it. Put notes in the video's `readme`. Any text a video had in `description` has been added to its README under a "Description" heading. REST responses still include `description` for now, marked deprecated; it will be removed. ## 2026-10-11: Signing in with Google while connecting an agent If you connected Claude, Codex or another agent while signed out and signed in with Google, trackagoat took you to your dashboard and the agent never finished connecting. You now come back to the screen where you pick the organization and click **Allow access**. ## 2026-10-11: Coding agent examples across the website Every example on the home page and the product pages now has a **trackagoat UI / Coding agent** switch. The coding agent side shows what you'd ask and what your agent answers, and **Behind the scenes** lists the MCP tools it called with their arguments. The compare pages now also cover how an agent connects, whether it can send payouts and which docs it can read, for trackagoat and each other tool. ## 2026-10-11: API keys stop working when the admin who made them loses access An admin who creates a key for a teammate has seen it, so that key now only works while they are still an org admin. If they leave or are removed, are changed to member, or their account is suspended, the keys they made for others are deactivated and refused with `key_creator_removed`, `key_creator_not_admin` or `key_creator_banned`. Keys a person made for themselves keep working after they're changed to member, limited to what a member can do. Suspending an account also deactivates every key that person created, in every organization. **Org settings → API keys** now shows why a key stopped working: revoked, the member it acts as left or was suspended, the admin who created it left, was changed to member or was suspended, or the organization was suspended. See [Authentication and API keys](https://www.trackagoat.com/docs/api-reference/authentication.md#who-a-key-acts-as). Reading the comment settings (`GET /api/v2/comment-settings`, MCP `get_comment_settings`) and a creator's payout account (`GET /api/v2/creators/{id}/payee-links`, MCP `get_payout_account`) now needs an org admin, like the pages they come from in the app. Every MCP tool that needs an org admin now says so in its description ("Requires the write scope and an org admin."), and the [MCP tool list](https://www.trackagoat.com/docs/agents/mcp.md#tools) and the OpenAPI spec mark them too. ## 2026-10-11: CSV exports for coding agents and the API Every CSV you can download in the app is now available to coding agents and the REST API, with the same columns, values and filters: creators, videos, campaigns, goals, the campaign leaderboard, the payouts tracker, payments, accruals and Insights charts. Ask your agent "Export September payouts to a CSV" or "Give me all videos from the *Spring Launch* campaign as a spreadsheet" and it uses the new MCP tool `export_csv`, then saves the file in your workspace. Over REST it's [`GET /api/v2/exports/{dataset}`](https://www.trackagoat.com/docs/api-reference/exports.md), and the app's own Export buttons now download from the same endpoint. Exports are never cut off at a row limit; big ones can be fetched in pages. A creator's **Accruals CSV** on their Payouts tab now lists every accrual, not just unpaid ones, to match the tab. ## 2026-10-11: Personalization documents, notes for every document, and keys that act as a member **Your agent knows who it's helping.** Your **My README** (**Settings → My README**) is now your personalization document: who you are, your job in the creator program and how you want AI agents to help. When you connect an agent over MCP, it gets your document at the start of every session. If you haven't written one, the agent offers to write it with you: it asks a few questions, shows you a draft and saves it once you approve. If your agent saves it while the page is open in the app, the editor asks you to reload instead of overwriting the change. See [Notes and documents](https://www.trackagoat.com/docs/agents/notes.md). **Every document, one set of tools.** Agents can now find, read and write every Markdown document in an organization: the organization, projects, campaigns, creators, videos, payout methods, structures and payments, and each person's personalization document. New endpoints `GET /api/v2/notes`, `GET` and `POST /api/v2/notes/{entity_type}/{id}`, `GET` and `PATCH /api/v2/me` and `GET /api/v2/members`, and new MCP tools `list_notes`, `get_notes`, `get_my_profile`, `update_my_readme` and `list_members`. `update_notes` now works for every document. Everyone in an organization, and their agents, can read every document in it, including personalization documents; your organization's README page now lists your teammates' documents. Only you can change yours. See the [Notes and documents API](https://www.trackagoat.com/docs/api-reference/notes.md). **API keys act as a member.** When an org admin creates an API key, they choose who it acts as: themselves, or another member. Everything the key does is attributed to that member, and it can never do more than they can in the app: actions only org admins can do there, like recording payments or removing creators, now return `insufficient_role` for a key that acts as a member. A key stops working when its member leaves the organization, with the error `key_member_removed`. The API keys list shows who each key acts as and who created it. For a teammate's own coding agent, signing in over MCP is still the simplest option, since nobody has to share a key. See [Authentication and API keys](https://www.trackagoat.com/docs/api-reference/authentication.md#who-a-key-acts-as). ## 2026-10-10: A real 404 page for links that don't exist A link to a page that doesn't exist used to send signed-out visitors to the sign-in page. It now shows a "Page not found" page with links to the home page, pricing, the docs and each product page. Links to pages inside the app still go to sign-in when you're signed out. ## 2026-10-10: Compare pages New pages compare trackagoat with MightyScout, viral.app, Shortimize, SideShift, Grade, ViewTrack and spreadsheets. Each one has a side-by-side table, the differences that matter, where the other tool is better, prices and how to move over, with every fact taken from that company's public site and dated. The home page's compare section now has a tab for each of them, and [/compare](https://www.trackagoat.com/compare) lists them all, plus TikTok and Instagram's own analytics and other tools people look at. ## 2026-10-10: Blog post: introducing the trackagoat MCP server A new blog post introduces the MCP server: what an agent like Claude, Codex or Cursor can do with its 125 tools, how to connect it with sign-in or an API key, example questions to ask, and the permissions and daily request limits on each plan. [Read it on the blog](https://www.trackagoat.com/blog/introducing-the-trackagoat-mcp-server). ## 2026-10-10: Blog post: what's new since trackagoat 2.0 A new blog post walks through everything that changed since 2.0 in July: more accurate tracking, Creator Goals judged on full periods, CPM paid on views gained, paying creators through Dots, Comment Insights, the full MCP server, the new look and the new prices. [Read it on the blog](https://www.trackagoat.com/blog/whats-new-since-trackagoat-2-0). ## 2026-10-10: "Get started" buttons The main sign-up button across the site now says **Get started** instead of Start free. The Free plan is unchanged. ## 2026-10-10: Rewritten website copy The home page, product pages, pricing, About, the agent setup prompt and the page descriptions were rewritten to say more plainly what trackagoat does, with worked examples (like paying every creator $5 a week plus $20 when a video passes 1M views). The pricing page and home page now list the Free plan's 10 tracked videos next to its 5 accounts, and the Payouts FAQ now says you can pay creators through a connected Dots account. ## 2026-10-10: Plainer punctuation and clearer empty values Text across the app, the website, these docs, API messages and MCP tool descriptions no longer uses long dashes. Ranges read "to" (for example "Sep 1 to Sep 30" and "1 to 100"), and page titles use "Page | trackagoat". Cells and tiles with no value now say so in words, such as **No data**, **No date**, **Never**, **None** or **Not set**, instead of showing a dash. Notes added through the API or MCP now end with "*Added by ...*" without a leading dash. ## 2026-10-10: All of Comment Insights in the API and MCP Everything in Comment Insights is now available to API keys and AI agents. New endpoints: `GET /api/v2/videos/{id}/comments` (a video's Comments tab: its comments, sentiment split, signal counts and how many were collected), `GET /api/v2/projects/{projectId}/comment-flags` (the weekly flag digest on demand: comments asking whether a post is an ad or using words like scam, and creators whose positive comments dropped), and `GET` / `PATCH /api/v2/comment-settings` (the minimum comment count and brand and competitor terms; changing them needs a key owned by an org admin). `/comment-signals` can rank the creators table with `creator_sort`, for example by app questions. New MCP tools: `list_video_comments`, `get_comment_flags`, `get_comment_settings` and `update_comment_settings`, and `get_comment_signals_summary` takes `bucket`, `creator_sort` and `creator_limit`. On the Free plan the comment tools return `plan_feature_unavailable`; the settings work on every plan. See [Comment Insights](https://www.trackagoat.com/docs/guides/comment-insights.md#api-and-ai-agents). ## 2026-10-10: Sentiment Radar is now Comment Insights Sentiment Radar has been rebuilt as **Comment Insights**, under **Insights** in the sidebar (old links redirect). It shows the most-liked comments first and tags the ones you act on: people asking what the app is, people signing up, people asking whether a post is an ad, words like scam or unsafe, pay talk, links, and mentions of your brand or a competitor. A new creators table shows comments per 1,000 views, the share of positive comments and each creator's top comment, and the page says how many of the platform's comments were collected. The radar, the average-score figure and the "negative vibes" alert are gone: most negative comments were viewers talking about their own lives. Sentiment is now scored by a model trained on social media posts, with extra rules for TikTok comments like "so real" and 😂. On hand-labeled comments it agreed with people about 3 times in 4, against about 1 in 2 before. Existing comments are rescored. Comments are now collected once a video is 3 days old and has at least 5 comments (an org admin can change this in **Org settings → Comments**, along with brand and competitor terms), most-commented videos first, with one more pass for videos whose comments double. This collects far more of the comments on popular videos. Comment Insights is included in paid plans; Free plans no longer collect comments. Org admins get a weekly notification quoting comments that ask whether a post is an ad or use words like scam, and Program Health shows them too. The API has `GET /api/v2/projects/{projectId}/comments` and `/comment-signals`, and agents have the MCP tools `list_comments` and `get_comment_signals_summary`. See [Comment Insights](https://www.trackagoat.com/docs/guides/comment-insights.md). ## 2026-10-10: Agent setup steps at the bottom of the home page on phones On phones, the home page now has the same bar at the bottom of the screen as on desktop: it copies the agent setup prompt, and tapping it shows the three steps to set up trackagoat with Claude Code, Codex or another coding agent. It replaces the strip at the top of the page and the **Start free** button at the bottom; **Start free** is now in the steps. ## 2026-10-10: Smoother rounded corners Rounded corners on buttons, cards, inputs, dialogs and panels now curve smoothly into their straight edges (often called squircle corners) in Chrome, Edge and other Chromium browsers. Circles and pill shapes are unchanged, and other browsers keep the previous corners. ## 2026-10-10: Ask your agent what's new in trackagoat The changelog is now available to AI agents and your own tools. Connected agents have a new MCP tool, `get_changelog`, so you can ask "what's new in trackagoat?" or "when did the payouts tracker change?" and get an answer with links to each entry. It works on any connection, whatever it is allowed to do, and doesn't count toward your daily API request limit. The same entries are available as JSON from `GET /api/v2/changelog`, with no API key, filtered by date (`since`) or search words (`q`). See [Changelog API](https://www.trackagoat.com/docs/api-reference/changelog.md). ## 2026-10-09: Clearer setup, invitation and shared creator pages Account setup now shows which step you are on, with one task per step. Invitations, the app connection screen and password reset use the new look, and the connection screen lists which app is asking, which organization it gets and what it can do. Shared creator pages are easier to read on a phone. Nothing about how these pages work has changed. ## 2026-10-09: Platform filter fixes Picking **TikTok** or **Instagram** now filters everything it should. On **Content Calendar** the day counts and totals only count that platform's posts (before, only the pop-up for a day was filtered), and the list shows only creators with an account on that platform. On **Campaign Insights** the filter did nothing; it now applies to the totals, trend charts, campaign leaderboard and campaign comparison. The content calendar on **Creator Insights** follows the filter too. The API endpoints `campaign-leaderboard` and `campaign-timeseries`, and the MCP tools `get_campaign_stats` and `get_campaign_trends`, accept `platform`. ## 2026-10-09: Insights above Tracking, and your own sidebar order The sidebar now lists **Insights** right under Dashboard, above **Tracking**. You can also put the sidebar sections in any order: hover a section name and drag its grip, or focus the grip and press the up and down arrow keys. Your order is saved to your account, so it is the same on every device. ## 2026-10-09: Shorter Needs attention list and a scrolling alerts panel On Creator Insights, the **Needs attention** section now shows the first three flagged creators, with a **Show all** button to see the rest. The **Active alerts** and **Alert history** panels now scroll when they hold more alerts than fit on the screen. ## 2026-10-09: Clearer settings pages Your account settings and organization settings now use one layout: a section menu on the left (a dropdown on phones), titled panels with a short description for each setting, and the save button at the bottom of each panel. The Usage page shows a bar for each plan limit that turns amber at 80% and red at the limit. Nothing about how the settings work has changed. ## 2026-10-09: An Adjustments column in the Payouts tracker The Payouts tracker has a new **Adjustments** column and total, between Paid and Outstanding: the part of what you paid a creator that settled no accrual, such as ad-hoc payments, tips, over- and under-payments, and payment history recorded without accruals. Hover it to see each reason and amount. Every row and the totals now visibly add up: Earned − Paid + Adjustments = Outstanding. It also appears on a creator's Payouts tab, in the expanded tracker row and in the tracker CSV (**Adjustments** and **Adjustment Reasons** columns). The tracker is now available in the API as `GET /api/v2/payouts/tracker` and over MCP as `get_payouts_tracker`, with `adjustment_cents` and `adjustment_breakdown` on every row and in the totals. See [Creator-grouped table](https://www.trackagoat.com/docs/guides/payouts.md#creator-grouped-table). ## 2026-10-09: Pay creators through Dots from trackagoat Org admins can connect their Dots account in **Org settings → Payment providers** and pay a creator's earned payouts from the Payouts tracker or the creator's Payouts tab. trackagoat sends the money through Dots and records the payment in the same step, so payouts are marked paid without typing anything in. Each creator is linked to a Dots payee, picked from the people your Dots account has paid (with suggested matches by name) or by mobile number. A payout can carry a note to the creator, a custom text message, a description and a 1099 exemption. Its status (completed, awaiting claim, failed…) shows on the payment, and failed or canceled payouts are voided automatically so they can be paid again. Agents and API keys can send Dots payouts too, but only after an org admin turns on **Agent payments**, with optional per-payout and 24-hour limits: new MCP tools `send_payout` and `get_payout_account`, and REST endpoints `POST /api/v2/payout-payments/send` and `GET /api/v2/creators/{id}/payee-links`. See [Paying creators with Dots](https://www.trackagoat.com/docs/guides/integrated-payouts.md). ## 2026-10-09: Creator, video, campaign and account pages share one layout Every detail page now opens with the same header (picture, name, status badges and actions), a row of headline numbers and the section menu, which becomes a dropdown above the content on phones. Video pages play the video in a pop-up from the header thumbnail. Info tabs list details as labelled rows, and empty lists explain what fills them. Nothing about how these pages work has changed. ## 2026-10-09: The new look on Insights, Program Health, Sentiment Radar and Content Calendar These pages now match the rest of the app: a strip of headline numbers at the top, titled panels, right-aligned numbers, and illustrated empty states. The Program Health pulse and the Sentiment Radar keep their dark displays in both light and dark mode. Nothing about how the pages work has changed. ## 2026-10-09: Clearer payouts pages The Payouts tracker now shows Earned, Paid and Outstanding as three totals above the table, with an Owed / Paid up status on each creator. Payout structure pages list each structure's terms (rates, milestones, goal rule) and version history in one place. Nothing about how payouts are calculated or paid has changed. ## 2026-10-09: Fresher, more accurate stats for photo carousels and new posts - **TikTok photo carousels now update on the same schedule as videos.** Until now they were read through a slow, expensive path and many carousels went weeks without an update. They now update every 12 hours while new, then on the same age-based schedule as videos, and monthly once over 90 days old. Carousels that had fallen behind are being caught up first. - **View counts and stats history now agree.** A new-posts check could raise a video's view count without adding a point to its history, so charts and view gains could trail the number shown. Counts now only change together with a history point. - **Fewer wrong numbers.** We no longer save a stats read that came back for a different post, a rounded count such as "1.2M", or a missing count recorded as 0. - **Accounts whose check failed are retried within about 2 hours**, instead of waiting for the next twice-daily run. - **Posts in the Tracking Inbox under 30 days old get daily stats updates**, so you review them on current numbers. ## 2026-10-09: Private accounts now hold goal and CPM payouts When one of a creator's accounts is private, we can't see what it posted, so their Goals and CPM payouts for that period are now held instead of being paid on the posts we already had. A banner on the Payouts page lists held payouts. A held payout is paid automatically once the account is public again. An organization admin can choose **Pay anyway** to pay on the posts already found; the decision is recorded in the payout history. Payouts calculated before this change are not affected. See [Payouts](https://www.trackagoat.com/docs/guides/payouts.md#recording-payments). ## 2026-10-09: Freshness badges follow each post's own schedule The **Fresh / Turning stale / Stale** badge on a video and the tooltip on its update-tier badge now use that post's real update schedule (TikTok or Instagram, video or photo carousel). An older carousel or Instagram post is no longer called stale on a schedule it was never on, and the tier tooltip shows the actual intervals (it still described an older, faster schedule). ## 2026-10-09: Supplemental amounts for paid periods When a creator turns out to be owed more for a period you already paid, for example because posts from that week were found after the payment, org admins can now record the difference with **Add supplemental amount** on the paid accrual. The original accrual and payment are not changed. The amount shows as owed, labelled "Supplemental: <reason>", counts in the tracker's outstanding totals and CSV exports, and is paid like an accrual with **Mark as Paid**. In the API: `GET`/`POST /api/v2/payout-supplementals`, `PATCH /api/v2/payout-supplementals/{id}` to void, and `supplemental_ids` on `POST /api/v2/payout-payments` (`auto_apply_outstanding` includes them). Over MCP: `list_payout_supplementals`, `create_payout_supplemental` and `void_payout_supplemental`. See [Supplemental amounts](https://www.trackagoat.com/docs/guides/payouts.md#supplemental-amounts-owed-on-top-of-a-paid-accrual). ## 2026-10-09: Removed creators are hidden in the Payouts tracker Creators you removed from a project no longer appear in the Payouts tracker by default, and their balances are no longer counted in its outstanding total. When any of them still has unpaid earnings, a note under the toolbar says how many and how much, with a **Show** link. The new **Show removed creators** switch lists them, labelled **Removed**, and adds them back into the totals. CSV exports follow the switch and have a new **Is Removed** column. A creator's own Payouts tab still shows their full balance. See [Filtering and date ranges](https://www.trackagoat.com/docs/guides/payouts.md#filtering-and-date-ranges). ## 2026-10-09: Tidier tracking lists Creators, Accounts, Videos, Campaigns, Conversions, Creator Goals, Auto-Disabled Creators and the Tracking Inbox now share one layout: a search and filter bar, numbers right-aligned in tables, coloured status labels, a dark bar for actions on selected rows, and illustrated empty states. The Tracking Inbox switches between **To review** and **Excluded** with one control. Nothing about how these pages work has changed. ## 2026-10-09: More reliable TikTok post discovery TikTok post discovery now reads full post lists, with captions, post dates and photo carousels, for far more accounts on each run. Before, slow or blocked pages often fell back to a backup source that has no captions or carousels, so hashtag tracking could miss posts. Accounts with no posts are now recognised as empty instead of being retried every hour. Private accounts are marked as unverifiable instead of complete. ## 2026-10-09: The same numbers on every page The dashboard, Insights, Program Health, campaign and creator pages, the API and the MCP server now count metrics the same way, so the same project and dates show the same views everywhere. A post counts when it is accepted, its tracking is on, and its creator and account are active: removed creators' posts and posts awaiting review no longer count anywhere (pages show "+N awaiting review" instead). The default window is the last 30 days including today in your time zone everywhere. Insights summary totals now cover every post in the period, not just the top 100 rows, Campaign Insights counts a video shared by two campaigns once, Video Insights' engagement rate is weighted by views, and follower growth is measured per account. The creator page shows **Tracked posts** next to the platform's own post count, Program Health's **Data Freshness** now measures video stats, goal summaries show **Behind Pace** and **At Risk** separately, and aggregate pages say how fresh their stats are. Definitions: [How metrics are calculated](https://www.trackagoat.com/docs/guides/metrics.md). In the API, `GET /api/v2/analytics/aggregate/summary` and the MCP tool `get_metric_totals` return the headline totals; creator responses add `tracked_post_count` and `pending_review_count` (`video_count` is unchanged); Program Health's `goal_compliance.at_risk` no longer includes behind-pace goals (see the new `behind_pace`). ## 2026-10-09: Plan video limits now apply to every tracked video Each plan keeps a set number of videos up to date: 10 on Free, 100 on Starter and 3,000 on Ultra. Until now the limit only applied when you added a single video by link. It now covers every tracked video, including posts found on your tracked accounts. New posts are always tracked. When an organization goes over its limit, trackagoat stops updating its oldest videos first. Paused videos still count as posted for goals, compliance and payouts, with their last stats, and show a **Paused: plan limit** badge. **Keep tracking** on a paused video resumes it and pauses your oldest other video instead. If you upgrade or free up room, the newest paused videos resume on their own. Organizations already over their limit have 14 days before anything is paused, and so does any organization after a plan downgrade. The Usage page shows how many videos are tracked and paused. See [Plans and limits](https://www.trackagoat.com/docs/guides/limits.md#tracked-videos). In the API and MCP tools, adding videos, including Inbox videos, and re-enabling tracking no longer fail at the limit; they pause your oldest videos instead and report `paused_count`. They return `402 limit_reached` only when every tracked video is set to keep tracking. Video responses include `plan_limit_paused_at`. ## 2026-10-09: Status page shows the data provider, and a banner when updates pause The [status page](https://www.trackagoat.com/status) has a new **Data provider** card that shows when we're close to, or have reached, the monthly quota of the service we read posts and stats through. If updates pause because the quota is used up, every page in the app shows a banner saying when they resume, so you know the numbers are from the last update. Account follower counts now refresh about every 24 hours, as intended, instead of every 36. ## 2026-10-09: A handle change no longer splits a creator's history When a creator changed their TikTok or Instagram handle, the old handle stopped updating and was auto-disabled, and adding the new handle started a second account with an empty history. trackagoat now follows the account's permanent platform user ID: when an update finds the creator's posts under a new handle, the account is renamed instead, keeps its posts, history, goals and payouts, and is re-enabled if it had been auto-disabled. The creator's History shows the change as **handle changed (detected automatically)**. If the new handle was already added as a separate account, both handles are flagged for our team to merge after review. An old handle that someone else takes over no longer copies the new owner's stats onto the account. See [When a creator changes their handle](https://www.trackagoat.com/docs/guides/accounts.md#when-a-creator-changes-their-handle). ## 2026-10-09: The MCP server can do what the REST API does The MCP server went from 21 tools to 105. An agent connected over MCP can now do everything an organization's API key can do through the REST API, so it no longer has to switch to HTTP calls: list and edit platform accounts, update creators and videos, remove creators, review the Tracking Inbox, turn video tracking off and on, create, edit and delete campaigns, goals and content groups, manage creator share pages, add chart annotations, define conversion events and record or void conversions, run the project analytics reports (top creators and accounts, outliers, posting heatmap and consistency, view distribution, campaign progress and trends, conversion totals), and manage payouts: create and edit payout structures, assign and unassign creators, reject or void accruals, manage payout methods, and record, edit or void payments, with a receipt attached. Receipts and payout method icons can be uploaded over MCP and the REST API, either inline as base64 (receipts up to 3 MB, icons up to 1 MB) or through a signed upload URL: new endpoints `POST /api/v2/payouts/payment-receipts`, `POST /api/v2/payout-methods/{id}/icon` and `POST /api/v2/payout-methods/{id}/icon/upload-url`, and `POST /api/v2/payout-payments` now accepts `payment_id` and `attachment_path`. `trigger_scrape` can also refresh a creator by `creator_id`. Tools that delete, remove, void or archive are marked as destructive so clients can ask before running them. The sign-in consent screen now says that **Make changes** includes removing data, setting up payouts and recording payments. `get_share_link` can return a share page's PIN with `include_pin: true`; `GET /api/v2/creators/{id}/share-link?include_pin=true` now needs the `write` scope instead of `admin` (only org admins can call it, and a write key could already rotate the PIN). Not available over MCP: API keys, organization members, invitations and projects (these need the admin scope), and permanent deletes of conversions. See [MCP server](https://www.trackagoat.com/docs/agents/mcp.md#tools). ## 2026-10-09: Removing a video no longer lets it be paid twice Deleting a video used to remove it completely. The next time the creator's account was checked, the post was found again and added back as a new video, so its views counted toward CPM payouts again and view-count bonuses could be paid a second time. Removing a video now keeps it, marked as excluded: it stops being tracked, stops counting toward totals and payouts, is hidden from video lists, and is not added back. In the app the action is now called **Remove**. In the API, `DELETE /api/v2/videos/{id}` does the same and returns the video's new `tracking_status` alongside `deleted: true`; `PATCH` it with `"tracking_status": "included"` to track it again. See [Videos API](https://www.trackagoat.com/docs/api-reference/videos.md#delete-apiv2videosid). ## 2026-10-09: Tracked posts on the home and Tracking pages The home and Tracking pages now show a slowly turning row of recent TikTok and Instagram posts, each with its view and like counts and a small chart of its stats, in place of the Program Health preview at the top of the home page. ## 2026-10-09: New plan prices Starter is now $29 a month and Ultra $399 a month. Free stays free, and plan limits are unchanged. ## 2026-10-09: CPM payouts keep counting a video's views after its first period CPM payouts used to pay only for the views of videos posted in the period, and stopped counting a video once that period was paid. A video that took off a week later earned nothing more. Now each period pays for the views a creator's videos gained since they were last paid, so a video keeps earning in later periods. A new **Count views for** setting on CPM structures sets how long after posting a video keeps earning: **Forever** (the default), 7, 30, 60 or 90 days, or a custom number of days. Every CPM structure moved to the new rules starting with the period in progress. Views from before the change that had not been paid were not back-paid. Views from a period below the minimum now carry into the next period. See [Setting up a CPM payout](https://www.trackagoat.com/docs/guides/payouts.md#setting-up-a-cpm-payout). ## 2026-10-09: A warmer look for the app The app now uses the same warm paper and night colours as the website, with clearer page titles, a sticky top bar, right-aligned numbers in tables, and illustrated empty states; nothing about how features work has changed. In the API, `criteria_params.view_lifetime_days` sets the lifetime on create and on a rule edit ([example](https://www.trackagoat.com/docs/api-reference/payouts.md#patch-apiv2payout-structuresid)). The MCP server has two new tools: `list_payout_structures` to read structures and their current rules, and `set_cpm_view_lifetime` to change a CPM structure's lifetime without touching anything else ([MCP tools](https://www.trackagoat.com/docs/agents/mcp.md#tools)). ## 2026-10-09: Agent setup steps at the bottom of the home page On desktop, the home page has a bar at the bottom of the screen that copies the agent setup prompt and lists the three steps to set up trackagoat with Claude Code, Codex or another coding agent. ## 2026-10-08: A page for each part of the product The Product menu now links to a page for each feature set (Tracking, Campaigns and goals, Payouts, Program Health, Insights, and API and agents), each explaining what it does with animated examples, plan limits and FAQs. ## 2026-10-08: Animated product previews on the home page The home page now shows focused, animated pieces of the product, such as a payout being marked paid and Program Health alerts arriving, in place of static screenshots. ## 2026-10-08: See which periods a goal is judged in Goal forms, the Goals tab, the goals overview, Program Health, share pages and the Payouts tab now show which weeks or months a goal is judged in, and say "Not judged this period" or "No payout" with the reason when a goal starts or ends partway through a period. ## 2026-10-08: Goals start with the next full period A goal that starts or ends partway through a week or month is now judged, and paid, only for the full periods inside its dates. ## 2026-10-08: Paste any profile link when adding an account - **Instagram profile links work.** `https://www.instagram.com/username/` (with or without `?igsh=…`, `www.` or `m.`) used to fail with "not a video URL". It now adds the account. - **TikTok share links are followed.** `tiktok.com/t/…` links from the app's Copy link button work like `vm.tiktok.com` links, and `m.tiktok.com` is recognised. - **One set of rules everywhere.** The Add creator and Add account dialogs, onboarding, the REST API and MCP now accept the same inputs: a handle or a profile URL, saved without the `@` and in lowercase. A 30-character handle typed with a leading `@` is no longer rejected. See [accepted profile links](https://www.trackagoat.com/docs/guides/accounts.md#accepted-profile-links). - **API fixes.** `POST /api/v2/creators/{id}/accounts` no longer stores `@Foo` as typed, checks the ban list for every platform, rejects removed creators (`409 creator_inactive`) and records the add in the creator's change log. Renaming an account to a handle that's taken returns `409 handle_taken` instead of a server error. - Account-scoped goals now have to use one of the creator's own accounts. ## 2026-10-08: More accurate goal status and view velocity - **Goal periods on daylight-saving days.** A daily or weekly goal period that includes a clock change now starts and ends at the right local midnight. It was off by an hour. - **No more "At Risk" minutes after a period starts.** Pace isn't judged for the first 15% of a period, and posts goals compare whole posts: a creator is behind only once a whole post is due. - **Followers goals measure growth per account.** Adding an account partway through a period no longer counts its existing followers as growth. The same fix applies to goal-based payouts. - **Goals for different platforms or accounts are all shown.** A TikTok posting goal and an Instagram posting goal with the same period used to hide one another. - **View Velocity spreads gains between checks.** Older videos are checked about once a week, so a week of views used to show up as a single-day spike. It's now spread across the days it covers, and days follow your organization's time zone. The creator and org averages still count each check's views on the day of the check, and the chart says so. ## 2026-10-08: Every API error has a code - **`meta.code` on every error.** A plain error such as `404 Not found` used to come back with `meta: null`. It now carries a code based on the status, such as `not_found` or `conflict`. Specific codes like `already_tracked` are unchanged. See [Errors and limits](https://www.trackagoat.com/docs/api-reference/errors.md). - **YouTube isn't offered yet.** Sign-in, onboarding and the add-account and add-video forms now say TikTok and Instagram. Pasting a YouTube link into Add video says YouTube isn't available yet, and the payout structure platform picker shows YouTube as coming soon. ## 2026-10-08: Safer Idempotency-Key retries - **Concurrent retries no longer both run.** A second request with the same `Idempotency-Key` that arrives while the first is still running now gets `409 idempotency_in_progress` with a `Retry-After` header, instead of running the write a second time. - **A key is bound to one request body.** Reusing a key with a different body returns `422 idempotency_key_reused` instead of quietly replaying the earlier response. - **Server errors aren't replayed.** A `5xx` (or `429`) is no longer stored, so retrying with the same key runs the request again. Rejections (`4xx`) are replayed, including ones raised partway through a batch. - **Replays are stored reliably.** The response is saved before it is returned, and a key used again after it expires gets a fresh 24 hours. The same rules apply to the `idempotency_key` argument on MCP write tools. See [Idempotency](https://www.trackagoat.com/docs/api-reference/overview.md#idempotency). ## 2026-10-08: Clearer API and MCP errors - **Unknown parameters are rejected.** MCP tools used to ignore a parameter they don't have, so `get_analytics` with `range: "14d"` quietly returned the default 30 days. Now the call fails with `unknown_parameter`, names the parameter, and lists the valid ones (`from` and `to`, in that case). - **Adding a creator who's already tracked** now says who: "@examplecreator (TikTok) is already tracked in this project as creator …", with `code: already_tracked` and the creator's id, over both MCP and the REST API. It used to return a bare "Already active". - **Every MCP error has a code**, such as `not_found` or `conflict`, so agents can act on it without parsing the message. - **Unknown API paths return a JSON 404.** A call to an endpoint that doesn't exist, like `DELETE /api/v2/goals/{id}`, used to get the website's HTML page with status 200, so it could look like it worked. It now gets `404` with `code: route_not_found` and a link to the OpenAPI spec. - **`get_campaign_stats` takes just a `campaign_id`.** The project comes from the campaign, so `project_id` is optional. The REST endpoint `GET /api/v2/analytics/aggregate/campaign-leaderboard` works the same way. - The MCP server no longer describes YouTube as supported. YouTube tracking isn't live yet. ## 2026-10-08: API and MCP access on the Free plan Free plan now includes API and MCP access (200 requests a day, 1 API key). New organizations can connect a coding agent or create an API key without upgrading. Each MCP tool call counts as one request, and the count resets at midnight in your org's time zone. Starter and Ultra limits are unchanged. See [Plans and limits](https://www.trackagoat.com/docs/guides/limits.md). ## 2026-10-08: Set up trackagoat with a coding agent - **Copy-paste setup prompt.** The home page, [Quickstart](https://www.trackagoat.com/docs/quickstart.md), [MCP server](https://www.trackagoat.com/docs/agents/mcp.md) and [API overview](https://www.trackagoat.com/docs/api-reference/overview.md) pages have a prompt you can paste into Claude Code, Codex, Cursor or another coding agent. **Show prompt** displays the full text before you copy it. - **What the agent does.** It sends you to sign-up, connects the MCP server for the client it runs in, and calls `list_projects` to check the connection. You do the clicking and signing in. The agent never handles your password or sees your API key. - **New page:** [Set up with a coding agent](https://www.trackagoat.com/docs/agents/setup.md) has the steps, install commands for Claude Code, Codex CLI, Cursor, VS Code and Claude, plan limits, and fixes for common errors. - **Sign-in for more clients.** The MCP page now shows sign-in (OAuth) setup for Claude Code, Codex CLI, Cursor and VS Code, with an API key as the fallback. ## 2026-10-08: New public site, published pricing - **New home page** covering TikTok and Instagram tracking, payouts, Program Health and the API, with an interactive demo of the app. - **Published plans.** [Pricing](https://www.trackagoat.com/pricing) now lists Free, Starter and Ultra with their limits, read live from the same plan settings the app enforces. Paid plans are switched on by email for now. - **New pages:** [About](https://www.trackagoat.com/about) and a public [changelog](https://www.trackagoat.com/changelog). The blog, status, privacy and terms pages share the new layout. - **Status:** the public status endpoint now includes `last_scrape_at`, the time of the most recent completed scrape. ## 2026-10-08: Plan scrape-frequency minimums are now enforced Plan scrape-frequency minimums are now enforced; the Free plan checks once a day. The scheduler used to run every org at the default 4-hour stats and 12-hour discovery and profile intervals, whatever its plan. It now never runs an update type more often than your plan's minimum. For video stats / new-post discovery / profiles, that is Free 24h / 24h / 24h, Starter 4h / 12h / 12h and Ultra 1h / 4h / 6h. Starter and Ultra orgs on the default intervals see no change. See [How scraping works](https://www.trackagoat.com/docs/guides/scraping.md#plan-floors). ## 2026-10-08: Docs redesign, Markdown for agents The docs have a new layout and are easier for both people and agents to use. - **New navigation.** Four tabs (Guides, API reference, AI agents, Changelog), each with its own sidebar, section pages, breadcrumbs, and previous/next links that stay within the tab. - **Search covers page text**, not only titles and headings. Results are grouped by tab and show the matching sentence. - **Markdown for every page.** Append `.md` to any docs URL, or fetch [`/llms.txt`](https://www.trackagoat.com/llms.txt) and [`/llms-full.txt`](https://www.trackagoat.com/llms-full.txt). See [llms.txt and raw docs](https://www.trackagoat.com/docs/agents/llms-txt.md). Each page also has a **Copy page** menu. - **Fixes:** code is syntax-highlighted in light and dark mode, the copy button copies the code, and every heading has a visible link anchor. - **Merged pages.** *Creators* is now part of [Creators and accounts](https://www.trackagoat.com/docs/guides/accounts.md), and *API keys* is part of [Authentication and API keys](https://www.trackagoat.com/docs/api-reference/authentication.md). Old links redirect. - **Accuracy.** The overview and scraping pages now describe the real update schedule, and YouTube is marked as coming soon. ## 2026-10-08: Program health and safe notes for agents Two new [MCP](https://www.trackagoat.com/docs/agents/mcp.md) tools and matching REST endpoints: - **`get_program_health`** answers "how is this program doing?" in one call: the same healthy / at-risk / degraded verdict as the Program Health page, with the KPIs, creators who stopped posting, goal compliance, alerts (sudden view drops, tracking gaps, private accounts, disabled creators, over-posting) and top and under-performers. Scope it to a campaign and a 7, 14, 30 or 90-day window. REST: [`GET /api/v2/projects/{projectId}/program-health`](https://www.trackagoat.com/docs/api-reference/projects.md#get-apiv2projectsprojectidprogram-health). - **`update_notes`** lets an agent write creator and video notes without overwriting what your team wrote. By default it appends a section signed with the API key's name and the date. Replacing the notes requires the version the agent last read, and fails if anyone changed them since. REST: [`POST /api/v2/creators/{id}/notes`](https://www.trackagoat.com/docs/api-reference/creators.md#post-apiv2creatorsidnotes) and `POST /api/v2/videos/{id}/notes`. `GET /api/v2/creators/{id}` and `GET /api/v2/videos/{id}` now return `readme_version` for this. The notes editor on creator and video pages uses the same check: if the notes changed after you opened them (say an agent appended a section), saving stops and asks you to reload instead of overwriting, and your draft is kept so you can copy it back in. Also fixed: the Program Health page's list of system-disabled creators now shows why each one was disabled (deleted, private or unreachable) instead of a generic reason. ## 2026-10-08: Connect Claude to trackagoat with one click Add trackagoat to Claude.ai or Claude Desktop as a custom connector: in Claude, go to **Settings → Connectors → Add custom connector**, enter `https://www.trackagoat.com/api/mcp` and click **Connect**. You sign in with your trackagoat account, pick the organization, and choose whether Claude can only view data or also make changes. No API key or `mcp-remote` setup needed. See [MCP server](https://www.trackagoat.com/docs/agents/mcp.md#claudeai-and-claude-desktop-custom-connector). Manage connected apps under **Settings → Connected Apps**; org admins also see every member's connections under **Org settings → API keys**. Connected apps don't count toward your API key limit, and each tool call counts as one API request, like a key. ## 2026-10-08: Hosted MCP server trackagoat now runs its own [MCP server](https://www.trackagoat.com/docs/agents/mcp.md) at `https://www.trackagoat.com/api/mcp`. Connect Claude Code, Claude Desktop, Cursor or any Streamable HTTP MCP client with an org API key, and your agent gets tools to browse projects, creators, videos, campaigns, goal compliance and payout accruals, pull analytics time series, and, with a `write`-scoped key, add creators, videos, goals and campaign items or trigger a scrape. Each tool call counts as one API request, and permissions match the REST API exactly. Also new in the REST API: `GET /api/v2/videos/{id}` returns a single video with its latest stats. ## 2026-10-08: Video refresh works, and scraping recovers more cleanly - **Refreshing a single video's stats now works.** The refresh button on a video and `POST /api/v2/videos/{id}/scrape` used to fail without saying why. The API endpoint now refreshes that one video rather than every video in the org. - **A newly added video gets its stats right away**, rather than waiting for the next scheduled stats run. This doesn't use up your daily **Update Now** quota. - **Hitting the scraping provider's usage limit no longer loses data.** Stats and profiles fetched before the limit was reached are saved, and the videos and accounts that weren't reached are left as they were, rather than marked as failing. - **Refreshing one account always scrapes it**, even if it was refreshed in the last 24 hours, and no longer delays the rest of the org's scheduled refresh. - **A scraping-provider outage no longer flags TikTok accounts as unreachable.** Before, an outage could count against every account and, after a few hours, disable them all. - **A failed run that is retried does its work properly.** Before, a retried run could end up marked complete without scraping anything, which held back the org's next run. ## 2026-10-08: Baseline markers, creator follower history and newsletter signups fixed Video charts show their **baseline** markers again. Since late April, a video's first snapshot taken more than 25 hours after it was posted was still treated as a baseline (its views weren't counted as new), but the chart marker explaining why was missing. A creator's follower, following, likes and video-count history now only sums the creator's **active** accounts, the same accounts as the current totals on the creator page. Before, a removed account's last numbers stayed in the creator's history after it was removed. Newsletter signups from the website footer are now saved. Until now they failed silently while the form reported success. ## 2026-10-08: Payout calculation fixes Several fixes to how payouts are calculated. Payouts already earned or paid are not changed. - **Unreviewed videos no longer earn CPM or view-milestone payouts.** Only auto-tracked, included and directly added videos count, the same as goals. A video waiting in the Tracking Inbox counts once you approve it. - **Unassigning stops earnings at the unassignment.** CPM and conversion payouts now count only what happened while the creator was assigned, as view milestones already did. - **"Apply to existing videos: off" is enforced for creators with long histories.** Milestones a video passed before the creator was assigned could still fire for creators with a lot of view history. They no longer do. - **A private or unreachable account no longer holds up a creator's payout.** Goal and CPM payouts are no longer withheld for up to two weeks waiting on an account that can't be checked. - **Goals that start next week no longer count toward the week that just ended.** Goal start and end dates now follow your organization's timezone. - **A temporary error no longer skips a creator.** If calculating one creator's payout fails, the period is retried until it succeeds instead of being closed without them. ## 2026-10-08: Posts on an account's tracking end date are now tracked An account's **tracking end date** is now inclusive: posts made at any time on that day, in your organization's analytics timezone, are tracked. Previously the window closed at midnight UTC at the start of the end date, so posts made that day were skipped, and an account whose start and end dates were the same day tracked almost nothing. The tracking window now covers each date both in UTC and in your analytics timezone, so nothing that was already tracked falls outside it. New accounts default their start date to today in your analytics timezone. ## 2026-10-08: Correct totals on Program Health, the dashboard and goals On larger programs, several numbers were computed from only part of your data and read too low. They now count everything, so expect some of them to jump: - **Dashboard**: the views card (now labelled **Views on posts from this period**: lifetime views of the videos posted in the selected range), Top Creators, Avg Views / Post, the "No posts in selected period" list and the pending-review breakdown by creator. On a ~100-creator program the views card had been showing well under half the real total. - **Program Health**: Total Views and its sparkline, Daily Posts Volume, Posts This Week, Stale Creators (could show none when some were stale), Posting Consistency and Underperformers. "Last 30 days" is now exactly 30 days and includes posts made late in the evening in your timezone. - **Follower Growth** no longer jumps up and down from day to day. Each account now carries its latest follower count forward instead of dropping out on days it wasn't updated. - **Goal compliance** on the dashboard, Program Health, the Goals nav badge and `GET /analytics/aggregate/goal-compliance` now covers every creator. It was capped at the first 50. - **Posting calendar** and `GET /analytics/aggregate/posting-consistency` no longer drop posts in busy months. - Engagement rate on Program Health now includes saves, matching Insights. Videos whose tracking has expired or been disabled no longer count toward Program Health's engagement figures, matching the dashboard and goals. - **Sudden view drops** only compares videos at least 72 hours old, and the **engagement rate drop** alert compares complete weeks, so new posts no longer trigger false alarms. Both widgets also showed `@undefined` instead of the creator's handle, which is fixed too (as is the Tracking Gaps list). - Filtering Program Health or the posting calendar by campaign now includes nested campaigns and videos added to the campaign directly, the same as Insights. - The Creators CSV export no longer stops at 1,000 creators. ## 2026-10-08: Analytics charts count the right gains on the right days Creator, video and campaign charts now show noticeably different (correct) numbers: - **Views Gained and other "New" charts no longer count a video's lifetime on the first day of the range.** Each video's first day is now measured against its last snapshot from before the range. For one creator, a 30-day range had been reporting about 6.5 million views gained; the real figure was about 5,100. Engagement rate and average views per post had the same first-day problem and are fixed too, as are the compare-to-prior-period summary cards. - **Days line up with your organization's timezone.** In timezones behind UTC (all of the Americas) charts were shifted one day early and left out today. Every day, week and month bucket is now your org's calendar day, and today appears as the in-progress bucket. - **Videos Added** no longer counts every earlier video on the first day of the range. - **Campaign Analytics loads on large campaigns.** The top videos and top creators tables no longer time out. - **API keys:** `GET /api/v2/analytics` resolves the timezone, plan retention and ownership from the requested entity's own project, so entities in any of your organization's projects work, not only the first one. ## 2026-10-08: Failed loads now say so When a page or widget can't load its data, it now shows an error with a **Retry** button instead of an empty state. Previously a failed request could read as a real answer: "No goals set", "$0.00" in payouts, empty leaderboards, or a **Healthy** pulse on Program Health. Organization and project READMEs are now read-only for members who aren't org admins. Only admins could ever save them; the editor used to accept the text and then silently discard it. A README save that fails now shows an error and keeps you in the editor. ## 2026-10-08: Daily limits reset at your org's midnight Daily quotas (API requests and manual **Update Now** scrapes) now reset at midnight in your org's time zone, which is when the counters actually roll over, instead of the UTC midnight shown before. The Usage tab, the Update Now dialog and the `Retry-After` header on a daily-quota `429` all use that time. The daily API request quota is enforced again: once your org reaches `max_api_requests_per_day`, further API calls return `429 rate_limit_exceeded` until the reset. See [Plan limits](https://www.trackagoat.com/docs/guides/limits.md). ## 2026-10-08: Insights filters and Creators export fixes - **Platform filter now applies to every Insights widget.** On Creator Insights, the summary strip, Needs Attention and Creator Tiers showed all-platform totals even with a platform selected; on Video Insights the summary strip and Engagement Breakdown did the same. They now follow the platform (and, on Video Insights, the campaign) filter like the leaderboards beside them. - **Creators CSV export matches the page.** The export now honors the goal status and **Auto-Disabled** filters across the whole project, not just the rows loaded so far. The **Handle** and **Platforms** columns, which had been exporting blank, are filled in again, and **Compliance** is filled in when you export with a goal-status filter. - The Creators footer now shows how many creators match the active filters. ## 2026-10-08: API keys follow their owner's org role API keys act as the user who created them, and now stay in step with that user's membership: - **Removed or departed members' keys stop working.** Removing someone from an org, or leaving one, revokes every API key they created for it. Any key whose owner is no longer a member is rejected with `401 invalid_api_key`. - **`admin` scope needs a current org admin.** If a key's owner is demoted to member, the key keeps `read` and `write` but loses `admin`. - **Org management over the API now needs `admin`.** Creating or revoking API keys, adding, changing or removing members, sending or revoking invitations, creating or updating projects, and creating organizations (`POST /api/v2/user/organizations`) previously accepted a `write` key; they now require the `admin` scope, matching the in-app rule that only org admins can do these things. - **A key can't mint a broader key.** `POST /api/v2/organizations/{id}/api-keys` only grants scopes the calling key holds. - **`POST /api/v2/organizations/{id}/members` works again.** It was failing with a 500 on every call. - **Secrets aren't kept for idempotent replays.** Replaying an API-key creation with the same `Idempotency-Key` returns `key: null`, and replaying a share-link creation returns `pin: null`. - **Invitations respect the member limit.** Accepting an invitation into an org that is at its plan's member limit now returns the standard `limit_reached` response instead of going over the limit. See [Authentication](https://www.trackagoat.com/docs/api-reference/authentication.md#permission-scopes). ## 2026-10-08: Save counts no longer drop to zero A TikTok video's save count could reset to 0 after a refresh that didn't return it, which also showed as a dip in the video's history. A refresh that doesn't return a save count now keeps the last known value. ## 2026-10-08: Deleting and voiding are now org-admin only A few destructive actions were open to every org member even though similar actions already needed an org admin. They now need an org admin too: - **Deleting a creator** (one at a time; bulk remove already required an admin). - **Deleting a video.** This is permanent and removes its stats history. - **Deleting a campaign.** - **Voiding a conversion.** Voiding lowers unpaid payouts earned from it, and voiding a payout accrual directly already needed an admin. - **Uploading a payment receipt** (part of recording a payment, which was already admin-only). Members no longer see these controls. Everything else they could do is unchanged. API keys are unaffected, since only org admins can create them. ## 2026-10-08: Safer payment recording Several fixes to how payments and payout structures are recorded: - **Mark as Paid only settles the selected creator.** Choosing a creator in the global **Mark as Paid** sheet now pre-checks that creator's outstanding accruals, and switching creators clears the previous selection. Payments can no longer settle another creator's accruals. - **A structure's period and criteria type are fixed once it's in use.** To change them, create a new structure and move creators to it. Amounts and criteria settings can still be edited. - **Payout dates follow your org's timezone** in the tracker, charts, accrual lists and CSV exports, matching the API. - **CSV exports include every row.** Large projects were cut off at 1,000 rows (200 for the tracker summary). The tracker export also gains a Currency column. - **Structure filter on the tracker**: *Paid* now counts only payments applied to that structure. - **API: `POST /api/v2/payout-payments` requires an `Idempotency-Key` header.** A retry with the same key returns the original payment instead of recording a second one. `creator_id` is required with `accrual_ids` or `auto_apply_outstanding`, and `auto_apply_outstanding` returns `409 nothing_outstanding` instead of recording a payment that settles nothing. - **API: assignment `effective_at`** can no longer be earlier than the start of the period in progress, and the goals `partial_multiplier` is capped at 1. - **API: `DELETE /api/v2/payout-structures/{id}/assignments`** works again (it returned 500). ## 2026-10-08: Removing a creator now frees its accounts Removing a creator used to leave its accounts behind. They kept counting toward your account limit, kept being scraped, showed up as standalone accounts, and blocked you from adding the same handle again ("already tracked"). Removing a creator now removes its accounts too, however you remove it: from the creator's page, from the Creators list (one or several), or through the API. Accounts left behind by creators removed earlier have been cleaned up, so you may see your account usage drop. Also in this release: - **Adding a creator is all-or-nothing.** If one of its handles is already tracked, or you're at your account limit, nothing is created. Before, you were left with an empty creator that had no accounts. - **Moving an account to another creator moves its existing posts too**, not just the ones discovered afterwards. - **Un-archiving a project counts toward your project limit**, the same as creating one. - **Campaigns only accept creators, posts and sub-campaigns from their own project.** Campaign totals count a post only if it's tracked (not excluded or pending review). Removing an account later doesn't take its posts out of a campaign's totals. - **Bulk remove reports the real count.** `removed_count` from the API is the number of creators actually removed, not the number of IDs you sent. ## 2026-09-30: Instagram accounts now update Instagram accounts could be added but never filled in: no followers, no profile photo, no posts, and **Refresh** failed without saying why. Instagram tracking was not running. It is now, and every Instagram account already in your projects has been picked up without you needing to re-add it. What to expect on an Instagram account: - **Followers, post count and profile photo** refresh daily. - **New posts are discovered twice a day**, from the account's tracking start date onward, and follow its tracking mode. In Hashtag mode, posts with your hashtag in the caption are tracked and the rest wait in the Tracking Inbox. - **Views are plays on Reels.** Photo posts and carousels are tracked with likes and comments, but Instagram doesn't publish a view count for them, so they show 0 views. - **Comments and sentiment** are collected for tracked posts. - An account that is **private** gets the Private badge; one whose handle **doesn't exist on Instagram** is flagged unreachable after a few attempts. Both send a notification to org admins. Instagram content counts toward any goal or payout structure whose scope includes Instagram, so check the scope on structures you set to **All platforms**. See [Instagram tracking](https://www.trackagoat.com/docs/guides/instagram.md). ## 2026-09-21: Private accounts are flagged everywhere A tracked account that switches to private used to be invisible: its profile kept updating, so nothing failed and nothing was auto-disabled, while no new post was ever discovered. The creator just looked like someone who stopped posting, and their goals quietly read as missed while payout periods held. Private accounts now carry a **Private** badge on the creator page, the creators list, the accounts list and the account page, with a hover explaining when it went private and what stops working. Program Health raises an alert listing every private account in the project, and the **Account went private** notification to org admins now opens the creator it refers to. Detection was already running in both directions: when an account reopens, the badge clears on its own and you get an **Account is public again** notification. ## 2026-09-21: Mark as Paid is one screen, and a payout gap is fixed **Mark as Paid** no longer walks through six steps. It is a single form: the creator's earned accruals arrive already checked, the running total sits in the footer, and everything optional (amount override, payout method, date paid, tracking URL, receipt, note) is behind a collapsed **Payment details** section that shows its current values on the header line. The section opens on its own when it holds something required, such as an adjustment reason. **Payout accruals no longer go missing for a closed period.** Goals and CPM periods wait for a deep account reconciliation before they settle, and that wait was being charged against the retry budget the engine uses for real errors. On larger programs the budget ran out before the reconciliation finished, and the affected creators were never accrued for that period, with no error surfaced. A period now waits up to 14 days for its data, and raises a warning instead of going quiet if the data never arrives. Periods previously dropped this way have been recomputed, so any payout that was missed is now showing as earned. ## 2026-09-11: Campaign filter on Insights, and program trend charts **Video Insights**, **Account Insights**, and **Campaign Insights** now have a **Campaign** toggle in the toolbar. Pick a campaign to scope every widget on the page to that campaign's videos (direct videos, member creators' videos, nested campaigns), or leave it on All campaigns. The choice is kept in the URL so a scoped view can be shared. **Campaign Insights** also opens with five new trend charts built for recurring program reviews: impressions over time (counted on the day the views happened, not the posting day), posts over time, creators posting, impressions per post, and new creators posting. They follow the toolbar's date range and granularity. For API and agent callers, the same scope is an optional `campaign_id` on the video, account, and campaign aggregate endpoints, and the trend series is available at `GET /api/v2/analytics/aggregate/campaign-timeseries`. ## 2026-09-11: Payouts tracker lists assigned creators before their first earning With **All** selected, the Payouts tracker now includes creators who are assigned to a payout structure but have not earned anything yet, showing $0 and their structure count, so a newly added creator is visible before their first period closes. Marking accruals as paid now refreshes every open payouts view (tracker rows, expanded accrual lists, creator Payouts tab) immediately, and recording several payments in a row from the same page no longer fails after the first one. ## 2026-08-20: Payout periods now reconcile late posts automatically Goals and CPM payouts now wait for a deep, post-period account reconciliation before they are calculated. If an eligible post is discovered after an unpaid accrual was created, trackagoat reopens the period and recalculates the earning automatically. Recorded payments remain immutable; any late data that could affect one is flagged for finance review instead of silently rewriting history. ## 2026-08-18: Choose when a payout structure starts earning Assigning a payout structure now asks two questions instead of silently starting at the moment you click. **Start on period** picks which period earning begins on (the one in progress, or any of the next three), so moving a creator between structures no longer has to overlap. **First period** decides how a period already underway is treated: prorate from today, or take it whole. For CPM the second choice is the one that matters. CPM counts the views on videos posted inside the period, so it now offers counting only videos posted from the assignment onward, rather than always counting the whole period including videos posted before the creator was on the structure. Existing assignments are untouched and keep behaving exactly as they did. The v2 API accepts `effective_at` and `first_period_mode` on `POST /payout-structures/{id}/assignments`. ## 2026-08-18: CPM payout structures drop the unused amount field Creating a CPM structure no longer asks for an "Amount per period" or a proration mode. Neither was ever used to calculate CPM earnings (the tiers carry the money), so the fields only invited a number that did nothing. The payment period remains, since it is the window impressions are summed over. Existing CPM structures are unaffected, and the stale flat amount they carried is no longer shown in the structures list, the structure header, or a creator's shared payouts page. The CPM rate and cap inputs also accept decimals properly. They previously reformatted on every keystroke, so `1.50` could not be typed and any rate under $1 was unreachable. ## 2026-08-06: Filter Payouts by campaign The Payouts tracker now has a Campaign filter. Selecting one updates the outstanding balance, creator table, spend charts, and CSV exports to the creators represented by that campaign, including individually added videos and nested campaigns. The selected campaign stays in the URL for bookmarkable and shareable views. ## 2026-08-06: View-incentive accruals resume automatically View-threshold payout structures stopped creating new accruals after a shared scraper-freshness check was changed for conversion payouts. Video stats continued updating normally, but newly crossed milestones did not appear in Payouts. The threshold-specific freshness check has been restored; eligible missed milestones are picked up automatically by the next payout calculation. ## 2026-07-30: Scrollable pickers no longer overlap their buttons in Safari In Safari, any dialog with a scrolling list (**Add Creators** to a payout structure, **Assign Payout Structure**, the add-to-campaign pickers, the calendar day view, and chart annotations) rendered the full list instead of confining it to its box once the list was long enough to scroll. The overflowing rows spilled over the buttons beneath them, so clicking **Assign** selected a creator instead. Chrome and Firefox were unaffected, which is why the problem looked account-specific. The list now stays inside its box and scrolls in every browser. ## 2026-07-29: Payouts tells you when a creator has no structure assigned Creators who had been added, put in a campaign, and given goals were silently absent from Payouts, with nothing on the page explaining why. Earnings only accrue against an assigned **payout structure**, and the tracker lists creators who have earnings or payments, so an unassigned creator never appeared at all. The Payouts page now shows a banner naming any active creators with no payout structure, with a button to assign one to all of them at once. Also fixed: analytics time-series reads were capped at 1,000 rows, which understated longer-window creator and campaign charts by as much as 11%. ## 2026-07-29: Shareable creator pages Give a creator a private link where they can check their own numbers: no account, no login, no invitation. - **A page per creator, protected by a 4-digit PIN.** Create one from a creator's **Info** tab, choosing whether it shows **Analytics**, **Payout accruals**, or both. Analytics covers headline totals, a views-over-time chart, goal progress, and their video list; payouts covers what they've earned, what's outstanding, what you've paid, and the terms they're on. - **Bulk creation.** Select creators on the Creators list and click **Shareable Pages** to set up a whole cohort at once (each with their own PIN, or one PIN for the batch), then copy the results or download them as CSV. - **Predictable links.** Every page lives at `/share/creator/`, so anything holding a creator id can build the link without a lookup. The URL isn't a secret; the PIN is what gates the data. Nothing renders before the PIN is verified, wrong guesses are locked out after ten attempts, and the pages are excluded from search engines. - **Full API support.** Every creator response now carries a `share_link` object, and links can be created, updated, rotated, disabled, and deleted through the v2 API, including a bulk endpoint. Entrance PINs are hidden by default and only returned to an `admin`-scope key that asks for them explicitly. See [Shareable creator pages](https://www.trackagoat.com/docs/guides/shareable-pages.md) and the [Share Links API reference](https://www.trackagoat.com/docs/api-reference/share-links.md). ## 2026-07-29: Conversion payout structures are editable again - **"Invalid request body" when editing a conversion payout structure.** The structure detail page showed a Period & Amount card for conversion structures, but a conversion structure pays per event: the rates live in Criteria, and the period is set by the roll-up cadence. Nothing typed into that card could ever save. It no longer appears for conversion structures, matching how view-threshold structures already behaved. Edit the cadence and per-event rates under **Criteria**. - **Deactivated conversion events no longer look like missing ones.** Deleting a conversion event deactivates it rather than removing it, and deactivated events are hidden from the event picker when building a payout structure, so a project could appear to have no events while re-creating them failed with "already exists". The picker now says how many are deactivated and links to Manage events, and the API's 409 names the existing event, says whether it is deactivated, and tells you how to reactivate it. - **A mismatched roll-up cadence returns a clear error.** Sending a conversion structure whose `period` disagreed with its cadence through the API failed with a server error instead of a validation message. ## 2026-07-27: Sandbox testing, and two fixes around it - **How to get a sandbox for API testing** is now documented. API keys are scoped to an organization, not a project, so a key can reach every project in its org. To test an integration safely, create a separate org and use a key from there. See [Authentication](https://www.trackagoat.com/docs/api-reference/authentication.md). - **Pausing scraping now actually pauses it.** Turning scraping off for an org stopped the scheduled runs, but adding a creator still triggered an immediate scrape. Adding a creator to a paused org no longer starts one. - **Deleting a conversion that has been paid out no longer fails silently.** Hard-deleting a conversion that had already produced a payout accrual returned a server error every time. It now returns a clear `409` explaining that the conversion should be voided instead: voiding reverses an unpaid accrual, and flags an already-paid one for review. The same fault also blocked deleting a project or organization containing such a conversion; that works now too. ## 2026-07-24: Deleting a creator works again Removing a creator silently failed to save since the 2.0 upgrade: the button reported success but the creator reappeared on refresh. The delete referenced a field that moved during the upgrade, so it errored before saving. Fixed; deleting a creator now removes them and stops tracking their accounts as of that day. ## 2026-07-22: Payouts page fixes - **Date-filtered payouts no longer hide threshold and per-conversion earnings.** Narrowing the payouts tracker to a date range dropped accruals from view-threshold and every-conversion structures (which aren't tied to a calendar period), so a week could show "no outstanding payments" while money was actually owed. These now appear, filtered by the date each was earned. - **Edit conversion events.** Conversion events can now be edited from Conversions → Manage events: update an event's name, description, or default value. Changing the value doesn't alter conversions already recorded. ## 2026-07-21: Get notified when a tracked account goes private When a creator sets their TikTok or Instagram account to private, we can still read their profile but can no longer track new posts or view counts. Previously this was silent: the account just appeared to stop posting. - **Org admins now get a notification** the moment a tracked account goes private, and again when it becomes public and tracking resumes. - **Accounts carry a private state** and a changelog entry records each transition. - Under the hood this also closed a scraping-cost leak: a private account's now-unreachable videos were being retried every cycle at real expense. ## 2026-07-21: API v2 now matches its documentation Three fixes where the v2 API behaved differently from what the docs described. If you built an integration against the documented behaviour, it now works as written. - **Creator `metadata` is writable again.** The docs described it as writable and the field was returned in responses, but `POST`/`PATCH /api/v2/creators` silently discarded it (unknown fields are stripped), so you got a `201` and no stored value. Use it to map your own record ids onto a creator. `PATCH` replaces the whole object rather than merging. - **A leading `@` in a handle is now stripped.** The docs said it was; v2 stored the handle verbatim, so `@rachel` was scraped as the literal handle `@rachel` and counted as a different account from `rachel`. - **The daily-quota `429` now carries `Retry-After`.** Only the per-minute limiter sent it before, so clients had to special-case which limiter fired. ## 2026-07-22: Pay creators for conversions Payout structures can now pay on **conversions**, alongside recurring, goals, CPM and view-threshold rules. - **Pick the events that count.** Select which conversion events a structure pays for, and give each its own rate: a flat amount per conversion, or a percentage of that conversion's value. - **Choose how often it rolls up.** Pay on every conversion as it arrives, or sum a creator's conversions daily, weekly or monthly. Weekly can start on any day, so "every Tuesday" works. - **Refunds are handled.** Roll-ups wait 24 hours after a period closes, so a conversion voided in that window never becomes a payout. A conversion voided later voids its unpaid accrual; anything already paid is left alone and flagged for review rather than silently rewritten. ## 2026-07-22: Hashtag matching fixes & org member removal - **Hashtags in non-Latin scripts now work.** Hashtag mode could never match a tag written in Devanagari, Arabic or Hebrew: the stored tag and the caption were being cleaned by two different rules, so they never lined up. Both sides now use the same rule. - **Punctuation in hashtags is handled predictably.** A tag typed as `#Black-Friday` is stored as `blackfriday`: the form people actually post, and matches captions written either way. It no longer becomes two separate tags. - **Removing someone from an organization works again.** This had been failing since 2026-07-15 for every org, which also prevented deleting a user account. - **Conversion event limits are enforced on reactivation.** Re-enabling a deactivated conversion event while already at your plan's limit is now correctly blocked. ## 2026-07-21: Conversions Trackagoat now tracks **outcomes**, not just output and cost. Define conversion event types per project (Signup, Purchase) and POST conversions to the v2 API attributed to a creator, an account, or a video. - **Conversion events.** Project-scoped definitions carrying a stable `key`, a default value, and a currency. Currency lives on the definition, so every total is well-defined. Plan limits: 3 event types per project on Free, 25 on Starter, unlimited on Ultra. Conversion volume is never capped. - **Attribution with rollup.** A conversion on a video also counts for that video's account and creator, so you can attribute at whatever precision your data supports and still report at any level. Attribute by `creator_id`, `account_id`, `video_id`, or `platform` + `handle`. - **Frozen attribution.** The rollup is computed at receipt time, so moving a video between accounts does not rewrite its past conversions and historical reports stay reproducible. - **Two idempotency layers.** The `Idempotency-Key` header replays a response for 24 hours; the `external_id` field dedupes permanently per project, returning the original conversion with `deduped: true` rather than a duplicate or an error. - **Void, don't delete.** `DELETE` voids by default (auditable, excluded from totals, still visible behind `?status=all`), which is the right answer for a refund or chargeback. Irreversible hard deletes require an `admin`-scoped key. - **Batch ingest.** Up to 500 conversions per request. See the [Conversions guide](https://www.trackagoat.com/docs/guides/conversions.md) and the [API reference](https://www.trackagoat.com/docs/api-reference/conversions.md). ## 2026-07-20: Hashtag import mode - **New Hashtag tracking mode.** Alongside **Auto** and **Selective**, an account can now track by hashtag: define a list of hashtags, and any newly discovered video whose caption contains one of them is tracked automatically. Ideal when a creator posts both sponsored and personal content and only tagged posts should count. - **You choose what happens to the rest.** A **Send non-matching videos to the Tracking Inbox** option routes everything else to the Inbox for review (the default), or excludes it outright. - **Works the same on every platform.** Matching runs on the video caption for TikTok and Instagram alike. Note that Instagram creators who put hashtags in the first comment rather than the caption won't match. Use Selective mode for those accounts. - **Safe on missing data.** Videos that arrive without a caption always go to the Inbox rather than being silently excluded. ## 2026-07-19: Accounts tab, durable avatars & docs refresh - **Accounts tab on creators.** Every creator now has a dedicated **Accounts** tab listing each platform account: platform, handle, tracking mode, followers, videos, scrape freshness, and status: with links to the account and its live profile. (This replaces the Accounts block that used to live on the Info tab.) - **Profile pictures no longer disappear.** Avatars are now stored permanently instead of relying on platform image links that expire after a few days, so creator and account profile pictures stay put. Where an image is temporarily unavailable, a clean placeholder shows instead of a blank circle. - **Docs refresh.** Guides and API reference now consistently describe how **accounts sit between creators and videos** across TikTok and Instagram. ## 2026-07-17: Flexible accounts & creators + Content Calendar platform filter Building on Trackagoat 2.0, accounts and creators are now fully flexible. - **Add accounts on their own.** From the **Accounts** page, use **Add Account** to start tracking a platform account by pasting its profile URL (we detect the platform) or a handle. Attach it to an existing creator, or leave it **standalone** and attach it later. - **Create a creator with no account.** The Add-Creator dialog has a **Name only** mode to create a creator up front and add their accounts whenever you like. - **Editable creator name.** A creator's display name is now an optional **override** on their **Info** screen: leave it blank and the creator automatically shows their primary account's name and avatar. Set it to give the creator a custom name. - **Richer Accounts block.** A creator's **Info → Accounts** section now lists each account with its platform, handle, followers, video count, and scrape freshness, linking straight to the account. - **Handle editing moved to the account.** You now rename a handle on the **Account detail** page (where it belongs), not on the creator. - **Content Calendar platform filter.** Filter the calendar's posts by platform (TikTok or Instagram). ## 2026-07-16: Trackagoat 2.0: Instagram tracking and multiple accounts per creator *Update (Oct 2026): this entry was edited to describe what shipped. YouTube was never switched on.* Trackagoat is now **multi-platform**. Alongside TikTok, you can track **Instagram Reels and posts** with the same views, engagement, posting-consistency, campaign, goal, and payout tracking you already use. YouTube isn't supported. [Read the full announcement →](https://www.trackagoat.com/blog/introducing-trackagoat-2-0) **Creators now have accounts.** A creator is the person; an **account** is one of their platform presences. A single creator can have a TikTok and an Instagram account, or even multiple accounts on the same platform. Your existing TikTok creators were migrated automatically: each became a creator with one TikTok account, and all of your history, videos, campaigns, goals, and payouts carried over unchanged. - **Multi-platform tracking**. Add Instagram accounts to any creator. Views (plays on Reels), likes, comments, followers, and posting cadence are tracked per account. - **Aggregate + per-platform views**: creator stats sum across all their accounts by default, with a platform/account switcher on every creator page to drill into one account. Analytics gains a platform filter and per-platform breakdowns. - **Mixed campaigns**. A campaign can contain videos and creators from any platform, with a per-platform breakdown. - **Platform-scoped goals & payouts**: goals and payout structures can target all platforms, one platform, or one specific account. Your existing goals and payout structures were scoped to TikTok, so their behavior is unchanged. - **New account-based limits**: plans now count **accounts** (which incur tracking cost), not creators. Creators are free; add as many as you like. **API v2.** The new data shape ships as **`/api/v2`**, with accounts nested under creators, platform filters on analytics, and new accounts and content-groups resources. **`/api/v1` has been retired**. See the [API reference](https://www.trackagoat.com/docs/api-reference/overview.md) to migrate. ## 2026-07-15: Clearer permissions, organization admins can manage payouts Permissions are now driven entirely by your **organization role**, and blocked actions explain themselves. - **Organization admins can record and void payouts**, and perform every admin action: across all projects in the org. Previously an internal, invisible project-level role could drift out of sync with your org role and block you even as an org admin. This is what caused the payout **Mark as Paid** flow to fail on its final step (in some cases silently) for admins. - **Blocked actions now tell you why.** When you don't have permission for something, you'll see a clear message about what's required (e.g. *"You need to be an organization admin to do this: ask an org admin to update your role"*) shown as a notification, instead of a vague "Forbidden" or nothing at all. Recording or voiding a payment requires an organization admin; viewing payouts is available to any organization member. ## 2026-07-14: Refreshed interface We refreshed the app's interface with updated component styling, cleaner buttons, form fields, cards, dialogs, and focus states for a more modern, consistent look. This is a visual update only: no workflows, data, or behavior changed. ## 2026-07-10: Dates and counts now use your organization's timezone everywhere Every surface that shows or counts activity by day now uses your **organization's analytics timezone** consistently, matching how the content calendar, Program Health, and goal tracking already worked. Previously a few places still bucketed or displayed timestamps in UTC, so an evening post (in a negative-UTC-offset timezone) could appear on the next calendar day, and per-period video counts could differ from one screen to the next. What changed: - **Video and campaign lists/details**, the "Posted", "Created", estimated-scrape, and operational timestamps on the Videos tab, Creator details, Campaign pages, and payout screens now render in your org timezone. - **Analytics counts**: the project dashboard's period totals and daily trend, plus the aggregate API endpoints (view distribution, outliers, leaderboards, creator scatter, posting heatmap/consistency), now bucket and range posts by your org's calendar day. - **Payout and sentiment APIs**: `from`/`to` date filters on `GET /api/v1/payout-payments`, `GET /api/v1/payout-accruals`, and the sentiment endpoints now interpret bare `YYYY-MM-DD` dates as your org's calendar days (pass a full ISO timestamp for an exact instant). No data changed: only how existing timestamps are grouped and displayed. If a video looked like it was posted "a day off," it will now match the day it was actually posted in your timezone. ## 2026-07-07: Weekly payout periods now follow calendar weeks (Mon to Sun) Weekly payout periods are moving to true calendar weeks, **Monday 00:00 → Sunday 23:59 in your organization's timezone**, so they line up with Program Health, analytics, and goal tracking everywhere else in the app. Previously, some weekly structures were anchored to the weekday the structure was first created (e.g. Tuesday→Tuesday), which was internally consistent but didn't match the rest of the app. The switch is forward-only and does not change or re-cut any historical or already-paid weeks. To keep coverage continuous through the change, the final legacy week is paid normally and a one-time short "bridge" week covers the gap up to the first calendar Monday, after which every week is Mon to Sun. Amounts for past weeks are unaffected. ## 2026-07-07: View incentives now show when the threshold was passed View-incentive ("milestone") accruals have no weekly/monthly period, so they previously showed no meaningful date. They now carry a **threshold-passed date**: the earliest point we observed the video's views cross the payout threshold (derived from view history; falls back to when the milestone was credited if history doesn't cover the crossing). This date appears on the accrual rows in a creator's payouts dropdown and in the **Mark as Paid** list, as a new **Threshold Passed At** column in the Accruals CSV export, and on the `GET /api/v1/payout-accruals` response. ## 2026-07-07: Accruals export now includes view incentives The **Accruals CSV** export was dropping view-incentive ("milestone") accruals whenever a date range was applied, so exports could show only goal-compliance accruals. View-incentive accruals are earned when a video crosses a view threshold and have no weekly/monthly period, which caused them to be filtered out by the date range. They are now included correctly. Additionally, individual accrual rows for view incentives now show the date the accrual was earned, matching goal-compliance rows. ## 2026-07-07: Backdate a payment when recording it When you mark accruals as paid, the **Receipt** step now has a **Date paid** field. It defaults to today, but you can set an earlier date to record a payment that was already sent, making it easy to backfill historical payout data. The date can't be in the future. The same `paid_at` field is available on the `POST /api/v1/payout-payments` endpoint. ## 2026-07-07: Payout recording fix and tracking-reliability improvements A follow-up pass to the platform audit fixed several issues: - **Recording a payment now works reliably.** A recent change had introduced a fault that caused recording a payment to fail whenever a payout method was selected. Payment recording is fully restored. (Payout amounts and history were never affected: only the act of recording a payment.) - **Auto-disable is less trigger-happy.** A temporary hiccup with our data provider during the discovery of brand-new creators could wrongly count against them and, after repeated hiccups, auto-pause a perfectly healthy creator. Those transient provider outages are now recognized as such and no longer penalize creators. - **Paused creators keep their view counts current.** Videos belonging to a creator who has been auto-paused (but not removed) now continue to update their view and like counts, so view-milestone payout progress no longer stalls while a creator is paused. - The public newsletter signup now always enforces a submission rate limit, even when our rate-limiting cache is unavailable. ## 2026-06-10: Always-accurate plan limits The [Limits & plan tiers](https://www.trackagoat.com/docs/guides/limits.md) page now reads tier values straight from the live plan configuration instead of a hand-maintained table, so it can never drift from the limits actually in force. When the team adjusts a tier, the docs update immediately. This pass also surfaced and corrected several stale values the old static table had been showing. ## 2026-06-10: Clearer labels across the app A pass over the whole interface to make labels and wording clearer and more consistent. Notable renames you'll see: - The **Tracking** section (was "Primitives") groups Campaigns, Creators, and Videos. - **Payouts** (was "Payment Management") in the sidebar. - **Tracking Gaps** (was "Video Parity") in Program Health, with plain-language explanations. - **Auto-Disabled** (was "System Disabled") for creators we automatically pause after repeated failed updates. - **Update Now** / **Data Freshness** / **Update Failures** (was "Scrape …") and **Find New Videos** (was "Discover New Videos"). - The creators list now shows **Tracked Posts** (posts in your program), and the dashboard shows **Total Followers**. No functionality changed: only wording. ## 2026-06-10: Security & access-control hardening Several access-control fixes following a full security review: - Closed a permission gap that could have let a signed-in user grant themselves access to an organization or project they weren't a member of. - API keys now strictly honor their scope: a **read-only** key can no longer create, edit, or delete payout configuration (it receives a clear "write scope required" error). - Locked down internal database functions and tightened tenant scoping on analytics so data can never be read across organizations. - Account impersonation (used only by platform support) is now verified on every request and cannot be spoofed. ## 2026-06-10: Photo carousels tracked correctly Fixed a class of bugs affecting TikTok photo (carousel) posts. A regression had caused photo posts discovered since early June to be stored as regular videos, which put them on the wrong (too-frequent) scrape schedule and mislabeled them throughout the app. 411 affected posts were corrected, and the underlying causes were fixed: photo posts are now reliably classified, always get a valid `/photo/` link, are no longer dropped during fallback discovery, and can't record a corrupted zero-view reading from a partial page load. Photo posts now also scrape on their intended (cost-efficient) cadence. ## 2026-06-10: Complete post capture, no more permanently-missed videos Discovery now guarantees every reachable post is eventually captured, closing a class of permanent gaps. Previously, routine discovery stopped scanning a creator's history as soon as it reached a video it already knew about. That meant if a post was ever missed: during a posting burst, a TikTok block, or before a creator's tracking start date was backdated. It became a **permanent** hole that no amount of waiting would fill. Now a cost-bounded **reconcile** runs continuously in the background: each creator periodically gets a deep, full-history re-scan that fills any gap it finds, prioritizing creators whose captured post count is furthest below their TikTok profile count. The existing backlog of missed posts drains automatically over the first cycle. This adds only a small, capped amount of scraping work: each creator is deep-reconciled at most once per ~30 days, and creators with detected gaps jump the queue. ## 2026-06-10: Platform-wide reliability and data-accuracy audit A holistic audit pass across the entire platform, focused on data completeness, accuracy, and security: - **Scrape accuracy**, failed video scrapes are no longer recorded as zero stats (previously a TikTok timeout could zero out comment/share/save counts and write a false dip into stats history). Stats history is now fully idempotent under retries. - **Metadata no longer disappears**: captions, posting dates, and thumbnails are no longer wiped when a creator's scrape falls back to the secondary data source; posting dates are also derived from the video ID when TikTok omits them. - **Photo carousels can be added directly**: `/photo/` URLs now work in add-video (previously rejected). - **Creator protection**: large scrape batches are chunked so healthy creators at the tail of a batch can no longer be miscounted as failing (and eventually auto-disabled); videos of disabled creators stop consuming scrape budget. - **Fixed dead UI**: campaign Top Videos / Top Creators widgets, the Disabled Creators page, and the dashboard refresh countdown were silently showing empty states for all users. - **Payout exports**: CSV exports no longer drop the final day of the selected range. - **Security hardening**: tightened database-level access controls across more than 100 internal functions, partition tables, and event feeds, plus several authorization fixes in the API layer. --- ## 2026-06-02: Health alerts are now logged and triageable Warning alerts from the daily and hourly health checks are now persisted to the `health_alerts` database table instead of existing only in email. Each alert type gets one open row that re-fires bump (with an occurrence count), and alerts auto-resolve when the underlying issue clears. A new Claude Code skill (`/health-alerts`) provides end-to-end triage: list open warnings, investigate root causes by type, and resolve with a note once fixed. --- ## 2026-06-02: v1 API, full write coverage The trackagoat API now supports writes across every resource, not just payouts and annotations. New write endpoints (all require `Authorization: Bearer tga_` with the `write` scope): - **Creators**. Add (single or bulk up to 50), update, soft-delete, trigger scrape, bulk re-enable/remove - **Videos**. Add (single or bulk up to 50), update, hard-delete, trigger scrape, toggle tracking, bulk tracking operations - **Campaigns**: create, update, delete, add/remove items - **Projects**: create, update, inbox approve/exclude - **Goals**: create, update, delete - **Organizations**: create org, manage members and invitations, issue/revoke API keys - **Payouts**: receipt upload URL (structure/method/payment/accrual CRUD was already available) - **Admin**: platform-admin endpoints for creator management, limit overrides, plan tiers, user bans, scrape jobs (require `admin` scope) New safeguards on all write calls: - **Scopes**, keys now have explicit `read`, `write`, and `admin` scopes (existing keys grandfathered to `read + write`) - **Idempotency**. Send `Idempotency-Key` on POSTs; replays return the cached response for 24 h - **Audit log**: every write is recorded in `api_audit_log`. There is no in-app view of the log. **OpenAPI spec:** [`GET /api/v1/openapi.json`](https://www.trackagoat.com/api/v1/openapi.json): import into Postman, Insomnia, or any OpenAPI-compatible tool. *(v1 was retired on 2026-07-16; see [API v2](https://www.trackagoat.com/docs/api-reference/overview.md#machine-readable-spec).)* --- ## 2026-05-20: System status page trackagoat now has a public status page at [trackagoat.com/status](https://www.trackagoat.com/status). It shows live operational status for four core subsystems (Database, Scraping Pipeline, Background Queue, and Scraper Worker) and links out to the status pages of external dependencies (Supabase, Vercel, Apify, Google Cloud). The page auto-refreshes every 60 seconds. A live status pill also appears in the site footer. --- ## 2026-05-20: Goal data advisories Goals now show a small amber caution icon when the displayed progress may not be final. Hover it to see an explanation. Four situations trigger the advisory: - **Period in progress**: the week or month hasn't ended yet; actual counts will likely grow. - **Period recently closed**: the period ended within the last 48 hours; late-period posts may still be processing. - **Stats may be stale**: the creator hasn't been scraped recently; views, engagement, or follower counts may undercount. - **Goal not yet started**: the goal's start date is still in the future. The advisory is also available in the API: the `advisory` object is included on every goal compliance result, and a `with_advisories` count is added to the aggregate goal-compliance endpoint. A new `GET /api/v1/projects/:id/goal-compliance` endpoint returns per-goal results with full advisory data for each creator. --- ## 2026-05-19: Goals payouts now evaluate 48 hours after period close Goal-based payout accruals are now computed roughly 48 hours after the period ends, rather than as soon as scrapers finish. This gives TikTok's index time to surface posts made in the final hours of a period so they're counted correctly. If you previously saw goal payouts appear on Monday morning for a Sunday-night period close, expect them to appear Tuesday morning instead. The timing is automatic: no action needed on your end. --- ## 2026-05-16: Re-add a deleted creator by handle Deleting a creator and then re-adding the same TikTok handle now reactivates the existing record (preserving history) instead of requiring manual intervention. A "Creator reactivated" confirmation appears so you know prior data was kept. Any prior auto-disable state is also cleared so scraping resumes immediately. --- ## 2026-05-16: Video tracking expiry rules Org admins can now set an age-based rule that automatically stops tracking videos once they reach a certain age, freeing up tracked-video limit slots for newer content. - **Org Admin → Video Expiry**: new settings tab to enable the rule, set the threshold (days/weeks/months), and preview exactly how many existing videos would be expired before saving. - **"Apply to existing videos now"** toggle: expire matching videos immediately on save, or only apply the rule going forward. - **Tracking Disabled badge** (red): appears on every expired video in the project list, video detail page, and creator videos tab. Hover for a full explanation of why tracking stopped (age rule or manual action) plus admin actions. - **Expires in Xd badge** (amber): videos within 7 days of their expiry threshold show a pre-warning badge so admins can exempt important videos before they go. - **Exempt badge** (green): admins can manually re-enable any expired video and permanently exempt it from rules. The exemption can be removed at any time. - **Bulk actions**: select multiple videos in the project list to disable tracking or re-enable & exempt in one step. - **Limit count fix**: tracked-video limits now correctly exclude expired videos and non-tracked statuses (excluded, pending review). This may free up capacity you didn't know you had. --- ## 2026-05-16: System-disabled creators visible to org admins When trackagoat auto-pauses scraping for a creator (after 5 consecutive failures), org admins can now see, understand, and act on it: without waiting for platform admin intervention. - **"Auto-Disabled" badge** appears on the creator list, creator detail header, and in Program Health: hoverable with a full explanation of why updates stopped. - **Three reason buckets** distinguish *Account Deleted*, *Account Private*, and *Unreachable* with tailored copy and guidance. - **Re-enable action** is available to org admins from the badge tooltip, the row actions menu, and the new **Tools → Auto-Disabled** dedicated page. A 24-hour per-creator cooldown prevents spam. - **Program Health** shows a new dismissible alert and a Auto-Disabled watchlist widget. - **In-app notification** goes to all org admins the moment a creator is auto-disabled. - **Creator changelog** records both the auto-disable event and every re-enable (org admin or platform admin). - **Bulk actions**: select multiple disabled creators to re-enable or remove them in one step. --- ## 2026-05-16: Edit creator TikTok handle You can now rename a creator's TikTok handle when they re-brand or change accounts. Open any creator → **Info** tab → **TikTok Handle** card, enter the new handle, and confirm. - Profile data (display name, avatar, follower count, bio) is cleared and a fresh scrape is queued automatically. - Tracked videos, past payouts, campaign memberships, notes, and README remain attached to the same creator. - The rename is logged in the creator's **History** tab. - Also available via the public API: `PATCH /api/v1/creators/{id}` now accepts `tiktok_handle`. --- ## 2026-05-16: Operator email alerts Two new automated email notifications are now active for platform operators: - **Signup notifications**: an email fires whenever a new user creates an account, including their name, email, auth provider, and running total user count. - **Daily scrape anomaly check**: runs at 08:15 UTC and emails when longer-window issues are detected: per-creator stats gaps (>3 days), freshness drift (>25% of creators stale), comment-scraping silence (>48h), stuck payout accruals, or orphaned campaign memberships. Distinct from the existing hourly alert which covers immediate job failures. - **Daily digest**: runs at 14:30 UTC (07:30 PT) and always sends a summary of the past 24h scraping activity, platform totals, and top failing creators: a green-state heartbeat for days when everything is healthy. --- ## 2026-05-13: Sentiment Radar A new **Sentiment Radar** tool gives you a program-level view of comment sentiment across all your tracked videos. - **KPI strip**: total comments, % positive, % negative, and average sentiment score for the selected date range. - **Sentiment over time chart**: stacked bar chart of positive / neutral / negative volume, bucketed by day (≤14 day range) or week. - **Top creators leaderboard**: creators ranked by average sentiment score (requires ≥20 analyzed comments in range). - **Negative-vibes alert**: dismissible warning when any video has ≥40% negative comments with ≥10 analyzed. - **Comment stream**: infinite-scroll feed of all comments, filterable by sentiment tab (All / Positive / Neutral / Negative) and keyword search. - **Campaign filter + date range**: scope everything to a specific campaign and custom date window. Defaults to the last 30 days. Find it under **Tools → Sentiment Radar** in the sidebar. --- ## 2026-05-13: Video Comments tab Each video detail page now has a **Comments** tab showing the sampled comments for that video (up to 100 per video). - **Sentiment analysis**: every comment is scored using sentiment analysis and labelled positive, neutral, or negative. A summary breakdown appears at the top of the tab. - **Sort options**: sort by top likes, newest, or oldest. - **Auto-collection**: comments are scraped automatically on a 6-hour cycle. New videos will have comments within 6 hours of being discovered. - Image and emoji-only comments are excluded from sentiment scoring. --- ## 2026-04-30: Analytics section Added a new project-level **Analytics** section (available in the sidebar between Tracking and Tools) with three sub-tabs: - **Video Analytics**: top-video leaderboard, posting-time heatmap (DOW × hour), view distribution histogram with percentile markers, and breakout video gallery. - **Creator Analytics**: creator leaderboard (followers, posts/week, engagement, growth), posting consistency punch card, goal compliance roll-up, and follower growth × engagement scatter chart. - **Campaign Analytics**: campaign leaderboard with prior-period delta, multi-campaign comparison overlay (up to 5), and campaign membership overlap matrix. Nine new aggregate API endpoints at `/api/v1/analytics/aggregate/*` support these dashboards and are available to API key holders. --- ## 2026-04-19: Internal & public docs Overhauled both the public docs site (`/docs`) and the internal admin docs (`/internal-docs`). New three-pane layout, syntax-highlighted code, table of contents, sectioned sidebar navigation, full API reference, feature guides, and agent documentation. --- ## 2026-04-17: Admin Control Plane Launched the platform admin control plane at `/admin`. Includes Plans tab (live tier limit editing), per-org limit overrides, per-user limit overrides, audit log, creator bans, soft-delete management, and user impersonation. --- ## 2026-04-11: Multi-org support Users can now belong to multiple organizations. Added `max_orgs_per_user` limit key and the org picker for users with multiple org memberships. --- ## 2026-04-11: API keys v2 Added API key visibility rules, SHA-256 hashed key storage, and one-time key reveal flow. Keys now use the `tga_` prefix format. --- ## 2026-04-10: Limits & plan tiers Introduced the three-tier plan system (Free, Starter, Ultra) with atomic enforcement via Postgres RPCs, daily usage metering for API requests and manual scrapes, `OverLimitBanner`, and `LimitReachedModal`. --- ## 2026-04-08: Initial launch Core platform: organizations, projects, creators, videos, campaigns, goals, inbox, content calendar, BullMQ scraper on Railway, v1 API. --- Source: https://www.trackagoat.com/docs/platform/changelog