# Channel Processing

Process channels into the Algrow database and download videos.

On-demand pipeline for ingesting a single YouTube channel into Algrow's database. Submit a job, then poll for the result. A dedicated worker picks the job up within seconds and runs the appropriate shorts or longform processor.

### `POST /api/process-channel`

Queue a single YouTube channel for on-demand ingestion. Resolves the input (UC… ID, `@handle`, channel URL, or video URL) to a canonical channel ID and inserts a row in the processing queue. A dedicated worker picks the job up within seconds and runs the appropriate shorts or longform processor. Returns immediately with a `job_id` — poll `GET /api/process-channel/:job_id` for the result.

**Headers**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| Authorization | string | Required | Bearer token: `Bearer YOUR_API_KEY` |
| Content-Type | string | Required | `application/json` |

**Request Body (JSON)**

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| channel | string | Required | — | Channel identifier — `UC...` ID, `@handle`, channel URL, or any video URL from that channel. |
| type | string | Required | — | Which processor to run: `shorts` or `longform`. The caller must choose — there is no auto-detection. |
| idempotency_key | string | Optional | — | If a job with this key already exists, that existing job is returned instead of a new one being queued. Use this to safely retry on network errors. |

**Example Request**

```
curl -X POST "https://api.algrow.online/api/process-channel" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"channel": "@MrBeast", "type": "longform"}'
```

**Response 200 200**

```
{
  "job_id": 1247,
  "status": "queued",
  "type": "longform",
  "channel_input": "@MrBeast",
  "resolved_channel_id": "UCX6OQ3DkcsbYNE6H8uQQuVA",
  "requested_at": "2026-05-24T18:42:11.213+00:00"
}
```

**Response Fields**

| Field | Type | Description |
| --- | --- | --- |
| job_id | integer | Unique job identifier. Use this to poll for status. |
| status | string | One of: `queued`, `running`, `completed`, `failed`. |
| type | string | Echo of the submitted `type` (`shorts` or `longform`). |
| channel_input | string | Echo of the raw input passed in the request. |
| resolved_channel_id | string | Canonical `UC...` channel ID we resolved the input to. |
| requested_at | string | ISO-8601 timestamp of when the job was enqueued. |
| idempotent | boolean | Present and `true` when an existing job was returned instead of inserting a new one. |

200 Queued 400 Validation error / could not resolve channel 401 Auth failed 429 Rate limit

### `GET /api/process-channel/:job_id`

Retrieve the current status and result of a channel processing job. Supports **server-side long-polling** via `?wait_seconds=N` (max 30) — pass it and the server holds the connection open until the job finishes or N seconds elapse, so you get the result in one call instead of polling in a loop. Typical processing time is 30 seconds to 2 minutes depending on channel size.

**Path Parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| job_id | integer | Required | The `job_id` returned from `POST /api/process-channel`. |

**Query Parameters**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| wait_seconds | integer | 0 | Server-side long-poll. `0` = return current state immediately. `1–30` = hold the connection open until the job hits a terminal state (`completed`/`failed`) or this many seconds pass, whichever comes first. Capped at 30 server-side regardless of input. Recommended: `wait_seconds=30` for typical polling. |

**Example Request**

```
# Single-call poll: server waits up to 30s for completion
curl "https://api.algrow.online/api/process-channel/1247?wait_seconds=30" \
          -H "Authorization: Bearer YOUR_API_KEY"
```

**Response — Running**

```
{
  "id": 1247,
  "status": "running",
  "type": "longform",
  "channel_input": "@MrBeast",
  "resolved_channel_id": "UCX6OQ3DkcsbYNE6H8uQQuVA",
  "claimed_at": "2026-05-24T18:42:13.001+00:00",
  "elapsed_seconds": 12
}
```

**Response — Completed**

```
{
  "id": 1247,
  "status": "completed",
  "type": "longform",
  "channel_input": "@MrBeast",
  "resolved_channel_id": "UCX6OQ3DkcsbYNE6H8uQQuVA",
  "requested_at": "2026-05-24T18:42:11.213+00:00",
  "claimed_at": "2026-05-24T18:42:13.001+00:00",
  "completed_at": "2026-05-24T18:43:02.482+00:00",
  "duration_seconds": 49,
  "eligible": true,
  "result": { ... }
}
```

**Response Fields**

| Field | Type | Description |
| --- | --- | --- |
| id | integer | Job ID. |
| status | string | One of: `queued`, `running`, `completed`, `failed`. |
| elapsed_seconds | integer | Seconds since the worker claimed the job. Only present while `status=running`. |
| duration_seconds | integer | End-to-end processing time. Present once `completed` or `failed`. |
| eligible | boolean | Whether the channel passed Algrow's inclusion rules (size, language, content type, etc.). Present once `completed`. |
| result | object | Worker payload — the scraped channel data. Shape depends on `type` (shorts vs longform). |
| error | string | Failure message. Only present when `status=failed`. |

200 Success 401 Auth failed 404 Job not found

### `POST /api/download-video`

Download a YouTube video, audio track, or subtitle file to Algrow’s storage and return a stable public URL the caller can hand to a browser. Each unique (video, format, quality, time-range) tuple is cached for ~30 days; repeat downloads are instant from cache. Requires the `generations` scope. Hourly cap is per-user — **Starter 5/hr, Professional 100/hr, Ultimate unlimited**. Every plan gets full quality up to 1080p. Parallel downloads are plan-gated: **Starter 1 at a time, Professional 3, Ultimate 6**. Capped at 3 hours of downloaded media and 500 MB output. That cap applies to the section you request rather than the length of the source, so pass `start` / `end` to pull a clip out of a video longer than 3 hours. Subtitles have no length limit.

**Request Body (JSON)**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| video_url | string | Required | YouTube video URL. Accepts watch links, `youtu.be` short links, and Shorts URLs. TikTok / Instagram are not supported here. |
| format | string | Optional | What to produce: `video` (mp4, default), `audio` (mp3), or `subtitles` (srt — English; human-authored when available, auto-generated otherwise). |
| quality | string | Optional | Video height: `360p`, `480p`, `720p` (default), or `1080p`. Ignored when `format` is `audio` or `subtitles`. |
| start | string \| number | Optional | Clip start timestamp. Accepts `"1:30"`, `"00:01:30"`, `"90"`, `"90s"`, or a raw number of seconds. Omit for the start of the video. |
| end | string \| number | Optional | Clip end timestamp. Same formats as `start`. Omit for the end of the video. Ignored when `format` is `subtitles` (always returns the full transcript). |

**Performance.** Cold downloads typically run ~60–120s for a full video. Clipped video / audio downloads scale with clip length — a 30s slice of a long video lands in ~15s. Subtitle fetches finish in ~10–15s regardless of source length. Cached hits return in <1s.

**Hourly cap by plan.** Counted per user (not per API key) and ticked only on successful 200 responses — cached repeats count, but a 400 like “video unavailable” does not. Limits: **Starter 5/hr, Professional 100/hr, Ultimate unlimited**. Parallel downloads are also capped per plan — **Starter 1 at a time, Professional 3, Ultimate 6**: a request past the parallel cap returns 429 immediately (nothing queues), so wait for a running download to finish and retry. When you hit either cap the endpoint returns 429 with the wait guidance and a link to [Subscription settings](https://algrow.online/settings/subscription) to upgrade.

**Example Request (full video, default 720p)**

```
curl -X POST "https://api.algrow.online/api/download-video" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
  "video_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"
}'
```

**Example Request (audio mp3 of a 30s clip)**

```
curl -X POST "https://api.algrow.online/api/download-video" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
  "video_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
  "format": "audio",
  "start": "1:00",
  "end": "1:30"
}'
```

**Example Request (1080p, full video)**

```
curl -X POST "https://api.algrow.online/api/download-video" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
  "video_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
  "quality": "1080p"
}'
```

**Example Request (subtitles SRT)**

```
curl -X POST "https://api.algrow.online/api/download-video" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
  "video_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
  "format": "subtitles"
}'
```

**Response 200 200**

```
{
  "success": true,
  "download_url": "https://audio.algrow.online/downloads/dQw4w9WgXcQ.mp4",
  "video_id": "dQw4w9WgXcQ",
  "title": "Rick Astley - Never Gonna Give You Up",
  "duration_seconds": 213.0,
  "size_bytes": 23456789,
  "cached": false,
  "format": "video",
  "quality": 720,
  "start_seconds": null,
  "end_seconds": null
}
```

**Returned URL.** The `download_url` points at Algrow’s R2 bucket (`audio.algrow.online`) and ships with `Content-Disposition attachment` so browsers trigger a save instead of inline playback. URLs are public and stable for the lifetime of the cache (~30 days); safe to share or embed.

200 Ready 400 Validation / unsupported video 401 Auth failed 429 Hourly download cap reached
