# 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_<key>" \
  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_<key>" \
  "https://www.trackagoat.com/api/v2/projects/<projectId>/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/<projectId>/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/<projectId>/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
