# Analysis Tools

Monetization checks, video analysis, and terminated-channel search.

### `GET /api/channel-monetization/:channel_id`

Full **external** monetization profile for one channel — what it sells, each method with its own evidence and source link, products, store URLs, and funnel metrics like member counts, sales counts, ratings and prices per platform. By default this **always** runs the full classifier live (≤60s) so the answer reflects the channel's current state, and persists the result on the way through. Use the query parameters below when freshness matters less than latency. For a live check of whether the channel runs YouTube ads, use [/api/channel-monetized/:channel_id](#channel-monetized) instead.

**Also reachable as** `GET /api/channel-external-monetization/:channel_id`. Same endpoint, same response — the longer name says plainly that this covers what a channel sells off-platform, not its ad revenue.

**Path Parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| channel_id | string | Required | YouTube channel id (UC…, 24 chars). Use /api/channels/resolve for handles/URLs. |

**Query Parameters**

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| skip_live | boolean | Optional | false | Skip the live classifier entirely and return whatever is already stored. Fast, but the answer can be stale, and returns `classified: false` if we hold nothing. Best for bulk reads. |
| cached_ok_secs | integer | Optional | — | Middle ground between the two. Reuse the stored result only when it is fresher than this many seconds and already at the current classifier version; otherwise re-run live. |

**Example Request**

```
curl "https://api.algrow.online/api/channel-monetization/UCX6OQ3DkcsbYNE6H8uQQuVA" \
-H "Authorization: Bearer YOUR_API_KEY"
```

200 Success 401 Auth failed

### `GET /api/channel-monetized/:channel_id`

Is this channel actually running YouTube ads right now? We check live rather than reading a cached label: we pull the channel's newest longform watch pages and look for the decisive signal that ads are served. The verdict is stored, so repeat callers and our own sweep stay in agreement.

**Path Parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| channel_id | string | Required | YouTube channel id (UC…, 24 chars). Use [/api/channels/resolve](/docs/api/workspace#resolve-channel) for handles and URLs. |

**`monetized` has three states, not two.** `true` means ads were confirmed on recent content. `false` means we read the pages cleanly and found none. `null` means no verdict this run — every fetch was blocked or the channel isn't in our longform index yet. Treat null as "unknown, ask again", never as "not monetized". The `reason` field says which case you got in plain words, and `hint` appears only when the verdict is null.

**`previous` is the state before this check.** It carries the last stored verdict and the timestamps of the last time the channel was seen monetized, seen demonetized, and last checked. Compare it against `monetized` to detect a flip. It is null for channels we have never held.

**Example Response**

```
{
  "success": true,
  "channel_id": "UCX6OQ3DkcsbYNE6H8uQQuVA",
  "monetized": true,
  "reason": "ads confirmed on recent content",
  "checked_live": true,
  "previous": {
    "monetized": true,
    "last_monetized_at": "2026-08-14 09:22:41",
    "last_demonetized_at": null,
    "last_checked_at": "2026-08-19 03:10:08"
  }
}
```

**Example Request**

```
curl "https://algrow.online/api/channel-monetized/UCX6OQ3DkcsbYNE6H8uQQuVA" \
-H "Authorization: Bearer algrow_..."
```

**503 means try again shortly.** The live check needs capacity to read watch pages. When that is unavailable you get a 503 rather than a guessed verdict — back off and retry rather than caching the failure.

200 Success 401 Auth failed 429 Rate limit 503 Check unavailable

### `GET /api/channel-cms/:channel_id`

Check whether a channel is run through a CMS / multi-channel network — is it managed, which network runs it, and what other channels that network operates. Backed by a database of 327,000+ pre-checked longform channels, so most lookups return instantly. A channel we haven't checked yet triggers a live check (10–60 seconds) and the verdict is saved for next time; pass `?refresh=1` to force a fresh live check on an already-checked channel. Pass `?full_network=1` to get the network's complete roster instead of the 5-channel sample.

**Path Parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| channel_id | string | Required | YouTube channel id (UC…, 24 chars). Use /api/channels/resolve for handles/URLs. |

**Query Parameters**

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| refresh | boolean | Optional | false | Run a fresh live check even if the channel already has a stored verdict. Counts against the hourly live-check limit. |
| full_network | boolean | Optional | false | Return the complete network roster as `network.members` — every managed channel we know under this owner, no cap — instead of the 5-channel `other_members_sample`. Professional and Ultimate only; ignored on Starter. |

**Same owner = same network.** Two channels are run by the same network if and only if their `owner.oid` values match. Use the oid, not the name, to group channels.

**Plan limits:** Professional and Ultimate get unlimited stored lookups; live checks are limited to 30 per hour. Starter gets 5 lookups per day, every one checked live (slower), and the response contains the verdict and owner only — no `network` object. Over the limit returns 429 with code `upgrade_required` (Starter daily cap) or `rate_limited` (hourly live-check cap).

**Example Request**

```
curl "https://api.algrow.online/api/channel-cms/UCX6OQ3DkcsbYNE6H8uQQuVA" \
-H "Authorization: Bearer YOUR_API_KEY"
```

**Response 200 (managed) 200**

```
{
  "success": true,
  "channel_id": "UCX6OQ3DkcsbYNE6H8uQQuVA",
  "status": "managed",
  "checked_at": "2026-08-12T14:03:22Z",
  "checked_live": false,
  "owner": {
    "oid": "a1b2c3d4e5f6a7b8c9d0e1f2",
    "name": "Example Network"
  },
  "network": {
    "member_count": 42,
    "other_members_sample": [
      {
        "channel_id": "UCyyyyyyyyyyyyyyyyyyyyyy",
        "name": "History Uncovered",
        "subscribers": 215000,
        "avg_views": 480000
      }
    ]
  }
}
```

**Response Fields**

| Field | Type | Description |
| --- | --- | --- |
| success | boolean | Whether the request succeeded |
| channel_id | string | YouTube channel ID that was checked |
| status | string | `managed`, `not_managed`, or `unchecked`. Unchecked responses include a hint explaining how to get a verdict (retry, or pass `?refresh=1`). |
| checked_at | string | When the verdict was recorded (ISO 8601) |
| checked_live | boolean | Whether this request ran a live check (vs a stored verdict) |
| owner | object | The network running the channel: `oid` (stable owner id), `name`, and the owner's `type` / `industry`. `name` can be `null` for a network we've never seen before. |
| note | string | Occasional plain-language line explaining a verdict that could otherwise look surprising. |
| network | object | Managed channels only: `member_count` plus `other_members_sample` — the 5 biggest other channels the same network operates, each with `channel_id`, `name`, `subscribers`, `avg_views`. With `?full_network=1` the sample is replaced by `members` — the complete roster, same fields per channel. Not included on Starter. |

200 Success 401 Auth failed 429 Limit reached

### `GET /api/cms-networks`

Browse and search CMS / multi-channel networks directly — the network-centric counterpart to /api/channel-cms, which starts from a channel. By default you get a list of networks matching your filters; pass `?oid=` to switch to detail mode and see one network with its top member channels (up to 100, biggest first). Counts include only channels verified as managed by that owner. Owner emails are never included.

**Query Parameters**

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| q | string | Optional | — | Filter networks by name (case-insensitive substring match). |
| oid | string | Optional | — | Switch to detail mode: return that one network plus its top member channels, up to 100, biggest first. Use the `oid` values from list responses or from /api/channel-cms. |
| min_channels | integer | Optional | 2 | List mode: only include networks with at least this many known channels. |
| sort | string | Optional | channels | List mode ordering: `channels` (most member channels first) or `subscribers` (largest combined subscriber count first). |
| limit | integer | Optional | 25 | List mode: number of networks to return, max 100. |

**Plan limits:** Professional and Ultimate only. On lower plans the endpoint returns 403 with code `upgrade_required`.

**Example Request (list, name search)**

```
curl "https://api.algrow.online/api/cms-networks?q=muse" \
-H "Authorization: Bearer YOUR_API_KEY"
```

**Response 200 (list mode) 200**

```
{
  "success": true,
  "count": 1,
  "networks": [
    {
      "oid": "a1b2c3d4e5f6a7b8c9d0e1f2",
      "name": "Example Network",
      "type": "CONTENT_OWNER_TYPE_COMPANY",
      "industry": "INDUSTRY_TYPE_WEB",
      "member_count": 42,
      "total_subscribers": 18400000
    }
  ]
}
```

**Response Fields**

| Field | Type | Description |
| --- | --- | --- |
| success | boolean | Whether the request succeeded |
| count | integer | List mode: number of networks returned |
| networks | array | List mode: networks matching your filters, each with `oid`, `name`, `type`, `industry`, `member_count`, `total_subscribers`. |
| network | object | Detail mode (`?oid=`): one network with the same fields as a list entry plus `members` — its member channels, up to 100, biggest first, each with `channel_id`, `name`, `subscribers`, `avg_views`, `language`, `category`. |

200 Success 401 Auth failed 403 Plan required 404 Network not found

### `POST /api/analyze-video`

Analyze a YouTube video with AI vision — hooks, pacing, visual storytelling, on-screen text, B-roll usage, content strategy, and any other prompt-driven breakdown. Returns structured analysis text. Costs 1 credit per 4 minutes of video (minimum 1 credit; `default` resolution costs 3x). Credits are charged when the job is queued and refunded if the analysis fails; the response includes `credits_charged`. Repeat prompts on the same video within 2 hours reuse a cached upload automatically (faster on the backend, returns `cached: true`).

**Request Body (JSON)**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| video_url | string | Required | YouTube video URL. Accepts watch links, `youtu.be` short links, and Shorts URLs. |
| prompt | string | Required | Plain-English instruction describing what to analyze. Max 4,000 characters. |
| media_resolution | string | Optional | Analysis fidelity: `low` (default, recommended for hook / pacing / strategy prompts) or `default` (higher visual detail at greater backend cost — useful when fine on-screen text or subtle visual cues matter). |

**Limits:** Maximum video length is 3 hours. Live streams, private, deleted, and age-restricted videos are rejected with clear error messages. Concurrency cap applies (5 / 10 / 20 active jobs by tier).

**Example Request (Hook breakdown)**

```
curl -X POST "https://api.algrow.online/api/analyze-video" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
  "video_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
  "prompt": "Break down the hook in the first 3 seconds. What grabs attention?",
  "media_resolution": "low"
}'
```

**Example Request (Pacing analysis on a Short)**

```
curl -X POST "https://api.algrow.online/api/analyze-video" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
  "video_url": "https://youtube.com/shorts/abc123XYZ",
  "prompt": "Identify the pacing, energy shifts, on-screen text timing, and where attention is most likely to drop."
}'
```

**Response 200 200**

```
{
  "success": true,
  "job_id": "f12d4a8b-2e1f-44e1-9aa1-2dfa20c5c7d3",
  "status": "pending",
  "duration_seconds": 187.4,
  "message": "Video analysis queued. Poll /api/job-status/{job_id} for progress."
}
```

**Completed Job Response (via /api/job-status/:job_id)**

```
{
  "success": true,
  "job_id": "f12d4a8b-2e1f-44e1-9aa1-2dfa20c5c7d3",
  "job_type": "video_analysis",
  "status": "completed",
  "analysis_text": "The opening 3 seconds use a hard cut from black to a wide shot of...",
  "duration_seconds": 187.4,
  "video_id": "dQw4w9WgXcQ",
  "cached": false,
  "completed_at": 1774203012.45
}
```

**Processing time:** Typically 5–25 seconds for Shorts, 30–90 seconds for 5-minute videos, several minutes for hour-long content. The first analysis on a video is slower than follow-up prompts on the same video (which hit the cache and skip download + upload). Poll `/api/job-status/:job_id` every 5 seconds for updates.

**Caching:** When you run multiple prompts on the same `video_url` within 2 hours, the source video is reused from cache — the response includes `cached: true`. Cache is keyed per (user, video) so each user pays the cold-path cost once per video per 2 hours.

200 Queued 400 Validation / unsupported video 401 Auth failed 429 Concurrency cap

### `GET /api/terminated-channels/search`

Search terminated/deleted YouTube channels. Returns channel metadata, growth metrics at time of termination, and up to 3 top videos per channel. Supports keyword matching and advanced filters. Available on all plans.

**Query Parameters**

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| q | string | Required | — | Search query. Matches against channel titles and video titles. Comma-separated for multiple keywords. |
| languages | string | Optional | — | Filter by language. Comma-separated (e.g. `English,Spanish`) |
| sort | string | Optional | date_desc | Sort field and direction. Format: `{field}_{asc\|desc}`. Fields: `subs`, `views`, `videos`, `age`, `views_24h`, `subs_24h`, `views_48h`, `date`, `similarity` (when using `q`) |
| page | integer | Optional | 1 | Page number (1-indexed, max 20) |
| per_page | integer | Optional | 20 | Results per page (max 50) |
| min_subs | integer | Optional | — | Minimum subscriber count |
| max_subs | integer | Optional | — | Maximum subscriber count |
| min_views | integer | Optional | — | Minimum total view count |
| max_views | integer | Optional | — | Maximum total view count |
| min_avg_views | integer | Optional | — | Minimum average views per video |
| max_avg_views | integer | Optional | — | Maximum average views per video |
| min_age | integer | Optional | — | Minimum channel age in days |
| max_age | integer | Optional | — | Maximum channel age in days |
| min_uploads | integer | Optional | — | Minimum number of videos |
| max_uploads | integer | Optional | — | Maximum number of videos |
| monetized | string | Optional | — | Filter by monetization status: `yes` or `no` |
| min_views_24h | integer | Optional | — | Minimum views gained in last 24h before termination |
| max_views_24h | integer | Optional | — | Maximum views gained in last 24h before termination |
| min_views_48h | integer | Optional | — | Minimum views gained in last 48h before termination |
| max_views_48h | integer | Optional | — | Maximum views gained in last 48h before termination |

**Example Requests**

```
# Search for terminated gaming channels
curl "https://api.algrow.online/api/terminated-channels/search?q=gaming&languages=English&per_page=10" \
  -H "Authorization: Bearer YOUR_API_KEY"

# High-sub terminated gaming channels sorted by subscribers
curl "https://api.algrow.online/api/terminated-channels/search?q=gaming&min_subs=100000&sort=subs_desc" \
  -H "Authorization: Bearer YOUR_API_KEY"

# Recently terminated motivation channels
curl "https://api.algrow.online/api/terminated-channels/search?q=motivation&sort=date_desc&per_page=20" \
  -H "Authorization: Bearer YOUR_API_KEY"
```

**Response 200 200**

```
{
  "success": true,
  "page": 1,
  "per_page": 20,
  "count": 20,
  "channels": [
    {
      "channel_id": "UCxxxxxxxxxxxxxxxxxxxxxx",
      "channel_title": "Deleted Gaming Channel",
      "subscriber_count": 340000,
      "view_count": 95000000,
      "avg_views_per_video": 2100000,
      "total_videos": 45,
      "primary_language": "English",
      "monetized": true,
      "is_low_quality": false,
      "thumbnail_url": "https://yt3.ggpht.com/...",
      "first_upload_date": "2025-06-12",
      "terminated_date": "2026-03-10",
      "terminated_days_ago": 15,
      "view_increase_24h": 120000,
      "sub_increase_24h": 800,
      "view_increase_48h": 210000,
      "sub_increase_48h": 1400,
      "similarity_score": 85,
      "recent_videos": [
        {
          "video_id": "abc123",
          "title": "Most Viewed Video Title",
          "view_count": 12000000,
          "thumbnail_url": "https://audio.algrow.online/thumbnails/...",
          "url": "https://www.youtube.com/watch?v=abc123"
        }
      ]
    }
  ]
}
```

**Response Fields**

| Field | Type | Description |
| --- | --- | --- |
| channel_id | string | YouTube channel ID |
| channel_title | string | Channel name at time of termination |
| subscriber_count | integer | Subscriber count at termination |
| view_count | integer | Total channel views at termination |
| avg_views_per_video | integer | Average views per video |
| total_videos | integer | Number of videos on the channel |
| primary_language | string | Detected content language |
| monetized | boolean | Whether the channel was monetized |
| is_low_quality | boolean | Whether the channel was flagged as low quality |
| thumbnail_url | string | Channel profile picture URL |
| first_upload_date | string | Date of the channel's first upload (ISO 8601) |
| terminated_date | string | Date the channel was terminated (ISO 8601) |
| terminated_days_ago | integer | Days since the channel was terminated |
| view_increase_24h | integer\|null | Views gained in last 24h before termination |
| sub_increase_24h | integer\|null | Subscribers gained in last 24h before termination |
| view_increase_48h | integer\|null | Views gained in last 48h before termination |
| sub_increase_48h | integer\|null | Subscribers gained in last 48h before termination |
| similarity_score | integer\|null | Similarity score (0–100) when using `q` search |
| recent_videos | array | Top 3 videos by views with `video_id`, `title`, `view_count`, `thumbnail_url`, `url` |

200 Success 401 Auth failed 402 Subscription inactive 500 Server error
