# Subscribr API Reference **Base URL:** `https://subscribr.ai/api/v1` **Version:** v1.0 (Stable) Welcome to the Subscribr API reference. This API follows RESTful conventions and uses standard HTTP response codes, authentication, and verbs. ## Table of Contents - **Getting Started** - [Quick Start](#quick-start) - [Core Concepts](#core-concepts) - [Authentication](#authentication) - [Rate Limits](#rate-limits) - [Errors](#errors) - **YouTube Intel** - [Channel Lookup](#channel-lookup) - [Channel Search](#channel-search) - [Video Lookup](#video-lookup) - [Video Search](#video-search) - [Manage Bookmarks](#intel-bookmarks) - **Teams** - [Get Team Details](#get-team) - [Get Credits](#get-credits) - [List API Tokens](#list-tokens) - [Create API Token](#create-token) - [Delete API Token](#delete-token) - **Channels** - [List Channels](#list-channels) - [Get Channel](#get-channel) - [Channel Templates](#channel-templates) - [Channel Voices](#channel-voices) - [Manage Competitors](#channel-competitors) - **Ideas** - [Create Idea](#create-idea) - [List Channel Ideas](#list-ideas) - [Get Idea Details](#get-idea) - [Write Idea](#write-idea) - [Generate Ideas](#generate-ideas) - [Generate Ideas From Video](#generate-ideas-from-video) - [Generate Ideas From Channel](#generate-ideas-from-channel) - [Change Idea Topic](#change-idea-topic) - **Scripts** - [Create Script](#create-script) - [List Channel Scripts](#list-scripts) - [Get Script](#get-script) - [Get Script Content](#get-script-content) - [Generate Outline](#generate-outline) - [Generate Script](#generate-script) - [Humanize Script](#humanize-script) - [Poll Generation Status](#poll-generation) - [Export Script](#export-script) - [Agent Mode — Generate](#script-agent-generate) - [Agent Mode — Poll Status](#script-agent-poll) - [Agent Mode — Cancel Run](#script-agent-cancel) - **Thumbnails** - [Get Thumbnail Usage](#get-thumbnail-usage) - [Create Thumbnail Generation](#create-thumbnail-generation) - [Get Generation Status](#get-thumbnail-generation) - [List Thumbnail Generations](#list-thumbnail-generations) - **Webhooks** - [List Webhooks](#list-webhooks) - [Create Webhook](#create-webhook) - [Get Webhook](#get-webhook) - [Update Webhook](#update-webhook) - [Delete Webhook](#delete-webhook) - [Test Webhook](#test-webhook) - [Webhook Events](#webhook-events) - [Webhook Security](#webhook-security) ## Getting Started {#getting-started} ### Quick Start {#quick-start} Getting started with the Subscribr API is straightforward. Within a few minutes, you can generate your first API token and make your first request. The Subscribr API provides programmatic access to your team's channels, scripts, and YouTube intelligence data. All requests require Bearer token authentication and are subject to rate limits based on your subscription plan. **1. Generate an API token** Navigate to your Subscribr dashboard settings and generate an API token with the required permissions (abilities) for your use case. Tokens are tied to your team and inherit your plan's rate limits. **2. Make your first request** Use curl or any HTTP client to make a request to the API. Include your token in the Authorization header as a Bearer token. **3. Explore further** Check out the endpoint documentation below to see what you can do with the API. Most endpoints return JSON and include examples in their documentation. ```bash curl -X GET https://subscribr.ai/api/v1/team \ -H "Authorization: Bearer sk_live_your_token_here" \ -H "Accept: application/json" ``` > ⚠️ **Need help?** > > If you run into any issues getting started, check out the Authentication and Errors sections below, join our Discord community at discord.gg/ZdUA4jEnU2, or contact hello@subscribr.ai for assistance. ### Core Concepts {#core-concepts} Understanding how Subscribr's key resources relate to each other and how to work with them via the API. #### YouTube Intel YouTube Intelligence endpoints provide data about YouTube channels and videos. Look up specific channels by URL or handle, search for channels and videos, and bookmark your findings. This data is sourced from YouTube and external intelligence services. **API Endpoint:** `POST /api/v1/intel/channels/lookup` #### Teams Teams are the top-level organization unit in Subscribr. All API tokens are scoped to a team, and all resources (channels, scripts, ideas) belong to a team. Teams have subscription plans, credit balances, and can manage API tokens. **API Endpoint:** `GET /api/v1/team` #### Channels A Channel is the top-level container for your content creation work. Each channel represents either a YouTube channel or a content planning space. Channels hold voice profiles, audience information, and serve as the parent for scripts and ideas. **API Endpoint:** `GET /api/v1/channels` #### Ideas Ideas are content concepts within a channel. They can be created manually or generated by AI. Ideas serve as the basis for scripts and can be developed into full scripts with titles, outlines, and content. **API Endpoint:** `POST /api/v1/channels/{channel}/ideas` #### Scripts A Script is a video project within a channel. Scripts progress through multiple stages from outline to full draft. You can generate scripts from ideas, export them, and poll generation status. **API Endpoint:** `POST /api/v1/channels/{channel}/scripts` #### Thumbnails AI-powered thumbnail generation for your video ideas. Generate concept sketches via brainstorm mode, produce final 2K thumbnails from ideas, or clone the style of an existing thumbnail. Generations run asynchronously — poll for status or use a callback URL for notification. **API Endpoint:** `POST /api/v1/channels/{channel}/thumbnails/generations` #### Typical Workflow 1. List your channels to find the one you want to work with (GET /channels) 2. Create ideas manually or generate them from competitor videos 3. Write/develop ideas to add structure and details 4. Generate outlines and scripts for your ideas 5. Export the finished script in your preferred format 6. Generate AI thumbnails for your ideas — brainstorm concepts or produce final 2K images 7. Set up webhooks to be notified when generation is complete ### Authentication {#authentication} The Subscribr API uses Bearer token authentication. All requests must include a valid API token in the Authorization header. API authentication uses Bearer tokens issued by your Subscribr account. These tokens are tied to your team and include specific abilities (permissions) that control what the token can access. ```bash curl -X GET https://subscribr.ai/api/v1/team \ -H "Authorization: Bearer sk_live_your_token_here" ``` > ℹ️ **Team-scoped tokens** > > Each API token is bound to the team that created it. Tokens can only access resources (channels, scripts, ideas) that belong to their team. You cannot use a token from one team to access another team's data. #### Available Token Abilities | Ability | Description | | --- | --- | | intel:read | Read-only access to YouTube Intel lookup, search, and bookmark retrieval endpoints | | intel:write | Create and delete Intel bookmarks (requires intel:read) | | scripts:read | Read scripts, ideas, and script content | | scripts:write | Create scripts and ideas, generate outlines and scripts (requires scripts:read) | | channels:read | List and get channel details, templates, and voices | | thumbnails:read | Check thumbnail usage quota and poll generation status | | thumbnails:write | Create thumbnail generations (requires thumbnails:read) | | webhooks:read | List and get webhook details | | webhooks:write | Create, update, and delete webhooks (requires webhooks:read) | ### Rate Limits {#rate-limits} The Subscribr API implements rate limiting to ensure fair usage and maintain service quality. Rate limits are enforced per API token and are based on your subscription plan. Different endpoints have different rate limits depending on their computational cost. #### Rate Limits by Endpoint Category | Category | Limit | Notes | | --- | --- | --- | | General API | 50 requests/minute | Default limit for most endpoints | | Channel/Video Search | 5 requests/minute | Resource-intensive semantic search | | Script Generation | 5 requests/minute | AI generation endpoints | | Channel/Video Lookup | 50 requests/minute | Fast direct lookups by ID or handle | | Thumbnail Generation | 12 requests/minute | Async thumbnail creation | | Thumbnail Status Polling | 120 requests/minute | Generation status checks and listing | | Thumbnail Usage | 60 requests/minute | Quota and usage lookups | #### Rate Limit Response Headers | Header | Description | | --- | --- | | X-RateLimit-Limit | The maximum number of requests allowed in the current window | | X-RateLimit-Remaining | The number of requests remaining in the current window | | X-RateLimit-Reset | Unix timestamp when the rate limit window resets | | Retry-After | Seconds to wait before retrying (only present on 429 responses) | ### Errors {#errors} The API returns standard HTTP status codes to indicate the success or failure of requests. All API error responses include a JSON body with details about what went wrong. Use these responses to debug integration issues. Most successful JSON responses use `{ "success": true, "data": … }` with optional `pagination` or `meta` at the top level. Exceptions: GET /team and GET /team/credits return top-level `team` or `credits` objects; Agent Mode — Generate and Cancel return flat objects without a `success` wrapper; some errors return `{ "error": "…" }` only. #### Common response shapes | Pattern | Example endpoints | Notes | | --- | --- | --- | | { success, data } | Channels, ideas, scripts list, webhooks | Most read/write endpoints | | { success, data, pagination } | List channels, list ideas, list scripts | Laravel-style pagination object | | { success, message } | Delete bookmark, delete webhook | Confirmation without data payload | | { team } / { credits } | GET /team, GET /team/credits | No success wrapper | | Flat agent generate | POST …/agent/generate | run_id (integer), poll_url, cancel_url at top level | | Flat agent cancel | POST …/agent/runs/{id}/cancel | cancelled, refunded at top level | | { error } or { message, errors } | 403/422 failures | Validation uses Laravel errors object | #### HTTP Status Codes | Code | Meaning | Common Causes | | --- | --- | --- | | 200 | OK | Request succeeded | | 201 | Created | Resource was successfully created | | 400 | Bad Request | Invalid request parameters or malformed JSON | | 401 | Unauthorized | Missing or invalid API token | | 403 | Forbidden | Token lacks required abilities for this endpoint | | 404 | Not Found | Resource doesn't exist or belongs to different team | | 409 | Conflict | Resource state conflict (e.g., duplicate creation) | | 422 | Validation Failed | Request data failed validation | | 429 | Too Many Requests | Rate limit exceeded | | 500 | Server Error | Internal server error (please contact support) | ## Teams {#team-resources} ### Get Team Details {#get-team} Returns the authenticated user's current team including subscription summary and caller role. Requires Automation-tier API access. #### Request ``` GET https://subscribr.ai/api/v1/team ``` **Authentication Required:** Yes (Bearer Token) No request body required. No query parameters. #### Response **Status Code:** 200 Team object with current subscription and credit information **Example Response:** ```json { "team": { "id": 456, "name": "My Content Team", "user_role": "admin", "owner": { "id": 123, "name": "John Doe" }, "subscription": { "plan": "Creator", "status": "active", "trial_ends_at": null, "is_paused": false } } } ``` **Response Fields:** | Field | Type | Description | | --- | --- | --- | | team.id | integer | Unique team identifier | | team.name | string | Team name | | team.user_role | string | Caller role: owner, admin, editor, viewer, or member | | team.owner | object | Team owner information | | team.owner.id | integer | Owner user ID | | team.owner.name | string | Owner user name | | team.subscription | object | Team subscription details | | team.subscription.plan | string | Current subscription plan | | team.subscription.status | string | One of active, trial, inactive, or paused | | team.subscription.trial_ends_at | string\|null | Trial end date (ISO 8601) or null if not in trial | | team.subscription.is_paused | boolean | Whether the subscription is paused | #### Error Responses | Status | Error code | Description | | --- | --- | --- | | 401 | (unauthenticated) | Missing or invalid Bearer token | | 403 | API access not available | Plan below Automation | | 403 | No team associated with user | User has no current team | ### Get Credits {#get-credits} Returns your team's current credit balance and usage information. #### Request ``` GET https://subscribr.ai/api/v1/team/credits ``` **Authentication Required:** Yes (Bearer Token) No request body required. No query parameters. #### Response **Status Code:** 200 Credit information for the team **Example Response:** ```json { "credits": { "current_credits": 850, "plan_credits": 1000, "credits_used_this_period": 150, "credits_remaining": 850, "credits_expire_at": "2026-02-28T00:00:00Z", "next_credit_refresh": "2026-03-01T00:00:00Z" } } ``` **Response Fields:** | Field | Type | Description | | --- | --- | --- | | credits.current_credits | integer | Current available credits | | credits.plan_credits | integer | Total credits allocated for this period | | credits.credits_used_this_period | integer | Credits consumed in the current period | | credits.credits_remaining | integer | Credits remaining in the current period | | credits.credits_expire_at | string | ISO 8601 timestamp when current credits expire | | credits.next_credit_refresh | string\|null | Next refresh (often same as credits_expire_at) | #### Error Responses | Status | Error code | Description | | --- | --- | --- | | 401 | (unauthenticated) | Missing or invalid Bearer token | | 403 | API access not available | Plan below Automation | ### List API Tokens {#list-tokens} List API tokens created by the authenticated user for the current team. Does not include other members' tokens. Team administrator required. **Team admin required:** Yes #### Request ``` GET https://subscribr.ai/api/v1/team/tokens ``` **Authentication Required:** Yes (Bearer Token) No request body required. #### Response **Status Code:** 200 Array of API token objects **Example Response:** ```json { "tokens": [ { "id": 123, "name": "Zapier Integration", "abilities": [ "intel:read", "scripts:read" ], "last_used_at": "2026-02-01T18:22:00Z" } ] } ``` **Response Fields:** | Field | Type | Description | | --- | --- | --- | | tokens | array | Array of token objects | | tokens[].id | integer | Token identifier | | tokens[].name | string | Human-readable token name | | tokens[].abilities | array | Array of permission strings | | tokens[].last_used_at | string\|null | ISO 8601 last usage timestamp (null if never used) | #### Error Responses | Status | Error code | Description | | --- | --- | --- | | 403 | Only team administrators can manage API tokens | Caller is not team owner/admin | ### Create API Token {#create-token} Create a new API token. Team admin required. **Team admin required:** Yes #### Request ``` POST https://subscribr.ai/api/v1/team/tokens ``` **Authentication Required:** Yes (Bearer Token) JSON body with token name and abilities. **Request Body Fields:** | Field | Type | Required | Description | | --- | --- | --- | --- | | name | string | Yes | Token label. Max 255. Unique per user per team. | | abilities | array | Yes | Each element must be one of: intel:read, intel:write, scripts:read, scripts:write, thumbnails:read, thumbnails:write, channels:read, webhooks:read, webhooks:write | **Example Request Body:** ```json { "name": "Automation Token", "abilities": [ "intel:read", "scripts:read", "channels:read" ] } ``` #### Response **Status Code:** 201 The newly created token (token string only shown once) **Example Response:** ```json { "token": { "id": 456, "name": "Automation Token", "abilities": [ "intel:read", "scripts:read", "channels:read" ], "token": "plain-text-token-value" } } ``` **Response Fields:** | Field | Type | Description | | --- | --- | --- | | token.id | integer | Token identifier | | token.name | string | Token name as provided | | token.token | string | The full API token (shown only when created) | | token.abilities | array | Granted abilities | #### Error Responses | Status | Error code | Description | | --- | --- | --- | | 403 | Only team administrators can create API tokens | Caller is not team owner/admin | | 422 | (validation) | Invalid abilities or duplicate name | ### Delete API Token {#delete-token} Delete a token by ID. Team admin required. **Team admin required:** Yes #### Request ``` DELETE https://subscribr.ai/api/v1/team/tokens/{token} ``` **Authentication Required:** Yes (Bearer Token) No request body required. **Parameters:** | Name | Type | Description | | --- | --- | --- | | token | integer | The token ID to delete (path parameter) | #### Response **Status Code:** 200 Confirmation message **Example Response:** ```json { "message": "Token deleted successfully" } ``` **Response Fields:** | Field | Type | Description | | --- | --- | --- | | message | string | Success confirmation message (no `success` wrapper) | #### Error Responses | Status | Error code | Description | | --- | --- | --- | | 403 | Only team administrators can delete API tokens | Not team admin | | 404 | (not found) | Token not found or not owned by caller | ## YouTube Intel {#youtube-intel} ### Channel Lookup {#channel-lookup} Look up one or more YouTube channels by handle (with or without @), channel ID (UC…), or URL. Charges 1 team credit per request. Rate limit: 60 requests/minute (intel). **Rate limit:** 60 requests per minute #### Request ``` POST https://subscribr.ai/api/v1/intel/channels/lookup ``` **Authentication Required:** Yes (Bearer Token) **Required Abilities:** intel:read JSON body with 1–5 identifiers. Per-identifier failures appear in data.errors without failing the whole request (multi-ID only). **Request Body Fields:** | Field | Type | Required | Description | | --- | --- | --- | --- | | identifiers | array | Yes | 1–5 strings. Handle, UC channel ID, or YouTube URL. | **Example Request Body:** ```json { "identifiers": [ "@mkbhd", "UCBJycsmduvYEL83R_U4JriQ" ] } ``` #### Response **Status Code:** 200 Channel data in data. Multi-ID responses include data.errors for failed identifiers. **Example Response:** ```json { "success": true, "data": { "channels": [ { "channel_id": "UCBJycsmduvYEL83R_U4JriQ", "handle": "mkbhd", "title": "Marques Brownlee", "description": "Channel description (truncated to 500 chars)", "thumbnails": { "default": { "url": "https://i.ytimg.com/..." } }, "subscriber_count": 19000000, "video_count": 1650, "view_count": 4200000000, "published_at": "2008-01-20T00:00:00Z", "country": "US" } ], "errors": [] } } ``` **Response Fields:** | Field | Type | Description | | --- | --- | --- | | success | boolean | Whether the request succeeded | | data | object | Response data containing channels and errors | | data.channel_id | string | Single-ID response only — YouTube channel ID | | data.handle | string | Single-ID response — handle without @ | | data.channels | array | Multi-ID response — successful lookups | | data.channels[].channel_id | string | YouTube channel ID | | data.channels[].handle | string | Handle without @ symbol | | data.channels[].title | string | Channel title | | data.channels[].description | string | Channel description (truncated to 500 chars) | | data.channels[].thumbnails | object | Thumbnail URLs | | data.channels[].subscriber_count | integer | Subscriber count | | data.channels[].video_count | integer | Total videos | | data.channels[].view_count | integer | Total views | | data.channels[].published_at | string | Channel publish timestamp (ISO 8601) | | data.channels[].country | string | Country code | | data.errors | array | Array of lookup errors: identifier, error, error_type | #### Error Responses | Status | Error code | Description | | --- | --- | --- | | 403 | Token does not have intel:read permission | Missing intel:read ability | | 422 | (validation) | identifiers missing, empty, or more than 5 items | | 400 | Channel lookup failed | Upstream lookup error (entire request) | > **⚠️ Response shape: one vs many identifiers** > > With exactly one identifier, data is a flat channel object (channel_id, handle, title, …). With multiple identifiers, data is { channels: [...], errors: [...] } where errors lists per-identifier failures. ### Channel Search {#channel-search} AI-powered YouTube channel search. Charges 2 team credits per request. Rate limit: 60 requests/minute (intel). **Rate limit:** 60 requests per minute #### Request ``` POST https://subscribr.ai/api/v1/intel/channels/search ``` **Authentication Required:** Yes (Bearer Token) **Required Abilities:** intel:read JSON body with natural-language query. **Request Body Fields:** | Field | Type | Required | Description | | --- | --- | --- | --- | | query | string | Yes | Search query. Max 255 characters. | | limit | integer | No | 1–20 results. Default 10. | **Example Request Body:** ```json { "query": "tech reviews", "limit": 10 } ``` #### Response **Status Code:** 200 Array of matching YouTube channels with search metadata **Example Response:** ```json { "success": true, "data": { "channels": [ { "channel_id": "UCBJycsmduvYEL83R_U4JriQ", "handle": "mkbhd", "title": "Marques Brownlee", "description": "Channel description (truncated to 500 chars)", "thumbnails": { "default": { "url": "https://i.ytimg.com/..." } }, "published_at": "2008-01-20T00:00:00Z", "subscriber_count": 19000000, "video_count": 1650, "view_count": 4200000000, "country": "US" } ], "query": "tech reviews", "total_results": 1, "message": "Channels found matching your criteria." } } ``` **Response Fields:** | Field | Type | Description | | --- | --- | --- | | success | boolean | Whether the request succeeded | | data.channels[].channel_id | string | YouTube channel ID | | data.channels[].handle | string | Handle without @ | | data.channels[].title | string | Channel title | | data.channels[].description | string\|null | Channel description (nullable) | | data.channels[].thumbnails | object\|null | Thumbnail URLs (nullable) | | data.channels[].published_at | string | Channel publish timestamp | | data.channels[].subscriber_count | integer | Subscriber count | | data.channels[].video_count | integer | Total uploaded videos | | data.channels[].view_count | integer | Total views | | data.channels[].country | string\|null | Country code (nullable) | | data.query | string | The search query used | | data.total_results | integer | Total results found | | data.message | string | Search completion message | #### Error Responses | Status | Error code | Description | | --- | --- | --- | | 403 | Token does not have intel:read permission | Missing intel:read ability | | 422 | (validation) | Invalid query or limit | | 400 | Channel search failed | Search service error | ### Video Lookup {#video-lookup} Look up one or more YouTube videos by 11-character video ID or watch URL. Charges 1 team credit per request. Rate limit: 60 requests/minute (intel). **Rate limit:** 60 requests per minute #### Request ``` POST https://subscribr.ai/api/v1/intel/videos/lookup ``` **Authentication Required:** Yes (Bearer Token) **Required Abilities:** intel:read JSON body with 1–5 identifiers. **Request Body Fields:** | Field | Type | Required | Description | | --- | --- | --- | --- | | identifiers | array | Yes | 1–5 video IDs or YouTube watch URLs. | **Example Request Body:** ```json { "identifiers": [ "dQw4w9WgXcQ", "https://youtube.com/watch?v=abc123" ] } ``` #### Response **Status Code:** 200 Video data in data. Multi-ID responses include data.errors. **Example Response:** ```json { "success": true, "data": { "videos": [ { "video_id": "dQw4w9WgXcQ", "channel": { "channel_id": "UCuAXFkgsw1L7xaCfnd5JJOw", "handle": "RickAstleyVEVO", "title": "Rick Astley" }, "title": "Video Title", "description": "Video description", "published_at": "2009-10-25T06:57:33Z", "thumbnail_url": "https://i.ytimg.com/...", "duration": "PT3M33S", "view_count": 1000000, "like_count": 50000, "comment_count": 10000, "outlier_score": 1.25, "format": "Music", "topic": "Pop", "angle": "Nostalgia", "goals": "Entertainment" } ], "errors": [] } } ``` **Response Fields:** | Field | Type | Description | | --- | --- | --- | | success | boolean | Whether the request succeeded | | data.video_id | string | Single-ID response only | | data.videos | array | Multi-ID response — successful lookups | | data.videos[].video_id | string | YouTube video ID | | data.videos[].channel | object | Channel summary with channel_id, handle, and title | | data.videos[].title | string | Video title | | data.videos[].description | string | Video description | | data.videos[].published_at | string | Publish timestamp (ISO 8601) | | data.videos[].thumbnail_url | string | Best available thumbnail URL | | data.videos[].duration | string | ISO 8601 duration | | data.videos[].view_count | integer | Total views | | data.videos[].like_count | integer\|null | Total likes (nullable) | | data.videos[].comment_count | integer\|null | Total comments (nullable) | | data.videos[].outlier_score | float\|null | Performance vs channel baseline (nullable) | | data.videos[].format | string\|null | Parsed format (nullable) | | data.videos[].topic | string\|null | Parsed topic (nullable) | | data.videos[].angle | string\|null | Parsed angle (nullable) | | data.videos[].goals | string\|null | Parsed goals (nullable) | | data.errors | array | Array of lookup errors: identifier, error, error_type | #### Error Responses | Status | Error code | Description | | --- | --- | --- | | 403 | Token does not have intel:read permission | Missing intel:read ability | | 422 | (validation) | identifiers invalid or over limit | | 400 | Video lookup failed | Upstream lookup error | > **⚠️ Response shape: one vs many identifiers** > > With one identifier, data is a flat video object. With multiple, data is { videos: [...], errors: [...] }. ### Video Search {#video-search} AI-powered YouTube video search. Charges 2 team credits per request. Rate limit: 60 requests/minute (intel). **Rate limit:** 60 requests per minute #### Request ``` POST https://subscribr.ai/api/v1/intel/videos/search ``` **Authentication Required:** Yes (Bearer Token) **Required Abilities:** intel:read JSON body with natural-language query. **Request Body Fields:** | Field | Type | Required | Description | | --- | --- | --- | --- | | query | string | Yes | Search query. Max 255 characters. | | limit | integer | No | 1–20 results. Default 10. | **Example Request Body:** ```json { "query": "iPhone 15 review", "limit": 20 } ``` #### Response **Status Code:** 200 Array of matching YouTube videos with search metadata **Example Response:** ```json { "success": true, "data": { "videos": [ { "video_id": "abc123", "channel": { "channel_id": "UCBJycsmduvYEL83R_U4JriQ", "handle": "mkbhd", "title": "Marques Brownlee" }, "title": "iPhone 15 Review", "description": "Description (truncated to 300 chars)...", "published_at": "2026-01-01T00:00:00Z", "thumbnail_url": "https://i.ytimg.com/...", "view_count": 500000, "like_count": 15000, "comment_count": 1200, "duration": "PT12M", "format": "Review", "topic": "Smartphones", "angle": "Hands-on", "goals": "Inform", "outlier_score": 1.12 } ], "query": "iPhone 15 review", "total_results": 1, "message": "Videos found matching your criteria." } } ``` **Response Fields:** | Field | Type | Description | | --- | --- | --- | | success | boolean | Whether the request succeeded | | data.videos[].video_id | string | YouTube video ID | | data.videos[].channel | object | Channel summary with channel_id, handle, and title | | data.videos[].title | string | Video title | | data.videos[].description | string | Truncated description (max 300 chars) | | data.videos[].published_at | string | Publish timestamp | | data.videos[].thumbnail_url | string | Best available thumbnail URL | | data.videos[].view_count | integer | Total views | | data.videos[].like_count | integer | Total likes | | data.videos[].comment_count | integer | Total comments | | data.videos[].duration | string | ISO 8601 duration | | data.videos[].format | string\|null | Detected format (nullable) | | data.videos[].topic | string\|null | Detected topic (nullable) | | data.videos[].angle | string\|null | Detected angle (nullable) | | data.videos[].goals | string\|null | Detected goals (nullable) | | data.videos[].outlier_score | float\|null | Outlier score relative to channel baseline (nullable) | | data.query | string | The search query used | | data.total_results | integer | Total results found | | data.message | string | Search completion message | #### Error Responses | Status | Error code | Description | | --- | --- | --- | | 403 | Token does not have intel:read permission | Missing intel:read ability | | 422 | (validation) | Invalid query or limit | | 400 | Video search failed | Search service error | ### Bookmarks {#intel-bookmarks} Manage team Intel bookmarks. GET lists bookmarks (50 per page). POST creates a bookmark. DELETE removes by bookmark ID at `/intel/bookmarks/{bookmark}`. #### Request ``` GET/POST/DELETE https://subscribr.ai/api/v1/intel/bookmarks ``` **Authentication Required:** Yes (Bearer Token) **Required Abilities:** intel:read GET: optional page query. POST: JSON body below. DELETE: bookmark ID in path (not on /intel/bookmarks alone). **Parameters:** | Name | Type | Description | | --- | --- | --- | | page | integer | GET only — page number (default 1, 50 per page) | | bookmark | integer | DELETE only — bookmark ID at /intel/bookmarks/{bookmark} | **Request Body Fields:** | Field | Type | Required | Description | | --- | --- | --- | --- | | type | string | Yes | POST only. channel or video | | external_id | string | Yes | POST only. YouTube channel ID or video ID | | title | string | Yes | POST only. Max 255 characters | | url | string | Yes | POST only. Valid URL | | notes | string | No | POST only. Max 1000 characters | | tags | array | No | POST only. Tag strings max 50 chars; stored lowercase | **Example Request Body:** ```json { "type": "channel", "external_id": "UCBJycsmduvYEL83R_U4JriQ", "title": "Competitor: MKBHD", "url": "https://youtube.com/@mkbhd", "notes": "Strong product review format", "tags": [ "competitor", "tech" ] } ``` #### Response **GET** (HTTP 200) Bookmark array with root-level pagination ```json { "success": true, "data": [ { "id": 321, "type": "channel", "external_id": "UCBJycsmduvYEL83R_U4JriQ", "title": "Competitor: MKBHD", "url": "https://youtube.com/@mkbhd", "notes": "Strong product review format", "tags": [ "competitor", "tech" ] } ], "pagination": { "current_page": 1, "per_page": 50, "total": 1, "last_page": 1 } } ``` **POST** (HTTP 201) Created bookmark ```json { "success": true, "data": { "id": 321, "type": "channel", "external_id": "UCBJycsmduvYEL83R_U4JriQ", "title": "Competitor: MKBHD", "url": "https://youtube.com/@mkbhd", "notes": "Strong product review format", "tags": [ "competitor", "tech" ] } } ``` **DELETE** (HTTP 200) Bookmark deleted ```json { "success": true, "message": "Bookmark deleted successfully" } ``` **Response Fields:** | Field | Type | Description | | --- | --- | --- | | success | boolean | Request success | | data | array\|object | GET: bookmark array. POST: single bookmark | | pagination | object | GET only — current_page, per_page (50), total, last_page | | message | string | DELETE only | #### Error Responses | Status | Error code | Description | | --- | --- | --- | | 403 | Token does not have intel:write permission | POST/DELETE without intel:write | | 422 | (validation) | Invalid body or duplicate external_id | #### Notes - GET requires intel:read. POST and DELETE require intel:write. - POST does not consume Intel lookup credits. ## Channels {#channels} ### List Channels {#list-channels} List channels the authenticated user can access on the current team. Results are filtered in-memory then paginated. #### Request ``` GET https://subscribr.ai/api/v1/channels ``` **Authentication Required:** Yes (Bearer Token) **Required Abilities:** channels:read No request body required. **Parameters:** | Name | Type | Description | | --- | --- | --- | | search | string | Search by channel title or handle. | | per_page | integer | 1-100, default 20. | | page | integer | Page number (default 1). | #### Response **Status Code:** 200 Paginated list of channel objects with YouTube details **Example Response:** ```json { "success": true, "data": [ { "id": 123, "yt_handle": "mkbhd", "details": { "title": "Marques Brownlee", "custom_url": "mkbhd", "channel_id": "UCBJycsmduvYEL83R_U4JriQ", "subscriber_count": 19000000, "video_count": 1650, "view_count": 4200000000, "description": "Channel description", "country": "US", "default_language": "en", "thumbnails": { "default": { "url": "https://i.ytimg.com/..." } } }, "scripts_count": 42 } ], "pagination": { "current_page": 1, "per_page": 20, "total": 1, "last_page": 1 } } ``` **Response Fields:** | Field | Type | Description | | --- | --- | --- | | success | boolean | Request success | | data[].id | integer | Subscribr channel ID | | data[].yt_handle | string\|null | Stored YouTube handle | | data[].details.title | string\|null | Linked YouTube channel title | | data[].details.channel_id | string\|null | YouTube channel ID (UC…) | | data[].details.subscriber_count | integer\|null | Subscriber count when linked | | data[].scripts_count | integer | Scripts on this channel | | pagination.current_page | integer | Current page | | pagination.per_page | integer | Page size | | pagination.total | integer | Total accessible channels | | pagination.last_page | integer | Last page number | #### Error Responses | Status | Error code | Description | | --- | --- | --- | | 403 | Token does not have channels:read permission | Missing channels:read ability | ### Get Channel {#get-channel} Get detailed information about a specific channel including settings, default voice, and script counts. #### Request ``` GET https://subscribr.ai/api/v1/channels/{id} ``` **Authentication Required:** Yes (Bearer Token) **Required Abilities:** channels:read No request body required. **Parameters:** | Name | Type | Description | | --- | --- | --- | | id | integer | Subscribr channel ID. Unknown ID → 404. No access → 403. | #### Response **Status Code:** 200 Channel detail wrapped in { success, data } **Example Response:** ```json { "success": true, "data": { "id": 123, "yt_handle": "mkbhd", "details": { "title": "Marques Brownlee", "custom_url": "mkbhd", "channel_id": "UCBJycsmduvYEL83R_U4JriQ", "subscriber_count": 19000000, "video_count": 1650, "view_count": 4200000000, "description": "Channel description", "default_language": "en", "country": "US", "thumbnails": { "default": { "url": "https://i.ytimg.com/..." } } }, "settings": { "language": "English", "audience": "Tech enthusiasts" }, "voice": { "id": 77, "name": "Default Voice", "instructions": "Voice instructions", "voice": "neutral" }, "scripts_count": 42, "scripts_idea_count": 4, "scripts_active_count": 38 } } ``` **Response Fields:** | Field | Type | Description | | --- | --- | --- | | success | boolean | Request success | | data.id | integer | Subscribr channel ID | | data.yt_handle | string\|null | Stored handle | | data.details | object | YouTube metadata (title, custom_url, channel_id, counts, thumbnails, …) | | data.settings | object | setting_name → setting_value map | | data.voice | object\|null | Default voice: id, name, instructions, voice | | data.scripts_count | integer | All scripts | | data.scripts_idea_count | integer | Scripts in idea status | | data.scripts_active_count | integer | Scripts in active status | #### Error Responses | Status | Error code | Description | | --- | --- | --- | | 403 | You do not have access to this channel | User lacks channel access | | 404 | (not found) | Channel ID does not exist | ### List Channel Templates {#channel-templates} List script templates for a channel (system + custom). If none exist, default system templates are provisioned on first request. #### Request ``` GET https://subscribr.ai/api/v1/channels/{id}/templates ``` **Authentication Required:** Yes (Bearer Token) **Required Abilities:** channels:read No request body required. **Parameters:** | Name | Type | Description | | --- | --- | --- | | id | integer | Channel ID (path parameter). | #### Response **Status Code:** 200 Array of available templates with default template ID **Example Response:** ```json { "success": true, "data": { "default_template_id": 22, "templates": [ { "id": 22, "name": "Base Script", "category": "base", "description": "Standard outline", "is_active": true, "is_system": true } ] } } ``` **Response Fields:** | Field | Type | Description | | --- | --- | --- | | success | boolean | Request success | | data.default_template_id | integer\|null | Active template ID or null | | data.templates | array | Template objects | | templates[].id | integer | Template ID. | | templates[].name | string | Template name. | | templates[].category | string | Template category. | | templates[].description | string | Short description. | | templates[].is_active | boolean | Whether this is the active template. | | templates[].is_system | boolean | System template flag. | #### Notes - default_template_id may be null when no template is marked active. ### List Channel Voices {#channel-voices} List available voices for channel and the default voice. #### Request ``` GET https://subscribr.ai/api/v1/channels/{id}/voices ``` **Authentication Required:** Yes (Bearer Token) **Required Abilities:** channels:read No request body required. **Parameters:** | Name | Type | Description | | --- | --- | --- | | id | integer | Channel ID (path parameter). | #### Response **Status Code:** 200 Array of available voices with default voice ID **Example Response:** ```json { "success": true, "data": { "default_voice_id": 77, "voices": [ { "id": 77, "name": "Default Voice", "instructions": "Voice instructions", "voice": "neutral" } ] } } ``` **Response Fields:** | Field | Type | Description | | --- | --- | --- | | success | boolean | Request success | | data.default_voice_id | integer\|null | Default voice ID or null | | data.voices | array | Voice objects | | voices[].id | integer | Voice ID. | | voices[].name | string | Voice name. | | voices[].instructions | string | Voice guidelines. | | voices[].voice | string | Voice identifier. | ### Manage Competitors {#channel-competitors} List, add, delete competitors. GET: returns competitors array. POST: request with identifier. DELETE: no body. #### Request ``` GET/POST/DELETE https://subscribr.ai/api/v1/channels/{id}/competitors ``` **Authentication Required:** Yes (Bearer Token) **Required Abilities:** channels:read GET/DELETE: no body. POST: JSON with identifier. **Parameters:** | Name | Type | Description | | --- | --- | --- | | id | integer | Subscribr channel ID (path) | | competitor | string | DELETE only — YouTube channel ID (path) | **Request Body Fields:** | Field | Type | Required | Description | | --- | --- | --- | --- | | identifier | string | Yes | POST only. YouTube handle, channel ID, or URL. Max 255 chars. | **Example Request Body:** ```json { "identifier": "https://www.youtube.com/@competitor" } ``` #### Response **GET** (HTTP 200) Array of competitor channels ```json { "success": true, "data": [ { "channel_id": "UC123...", "title": "Competitor Channel", "custom_url": "competitor", "custom_url_with_at": "@competitor", "youtube_url": "https://youtube.com/@competitor", "thumbnails": { "default": { "url": "https://i.ytimg.com/..." } }, "subscriber_count": 500000, "video_count": 300, "view_count": 12000000 } ] } ``` **POST** (HTTP 201) Successfully added competitor ```json { "success": true, "data": { "channel_id": 123, "competitor": { "channel_id": "UC123...", "title": "Competitor Channel", "custom_url": "competitor", "custom_url_with_at": "@competitor", "youtube_url": "https://youtube.com/@competitor" } } } ``` **DELETE** (HTTP 200) Successfully removed competitor ```json { "success": true, "message": "Competitor removed." } ``` **Response Fields:** | Field | Type | Description | | --- | --- | --- | | channel_id | string | YouTube channel ID. | | title | string | Channel title. | | custom_url | string | Handle without @. | | custom_url_with_at | string | Handle with @. | | youtube_url | string | Full YouTube URL. | | thumbnails | object | Thumbnail images object. | | subscriber_count | integer | YouTube subscriber count. | | video_count | integer | Total video count. | | view_count | integer | Total view count. | #### Notes - GET requires channels:read. POST and DELETE require scripts:write. - DELETE path parameter competitor is the YouTube channel ID (UC…), not the Subscribr channel id. ## Ideas {#ideas} Create and manage video ideas within your channels. ### Create Idea {#create-idea} Create a new video idea in the ideas system. #### Request ``` POST https://subscribr.ai/api/v1/channels/{id}/ideas ``` **Authentication Required:** Yes (Bearer Token) **Required Abilities:** scripts:write JSON body with idea fields. No credits charged on create. **Request Body Fields:** | Field | Type | Required | Description | | --- | --- | --- | --- | | title | string | Yes | Max 255 characters | | topic | string | No | Max 5000 characters | | angle | string | No | Max 1000 characters | | suggested_length | integer | No | 200–20000 word target | | suggested_template_id | integer | No | Must exist in templates table | | thumbnail_concept | string | No | Max 1000 characters | | notes | string | No | Max 2000 characters | **Example Request Body:** ```json { "title": "Why Apple's AI Changes Everything", "topic": "Analysis of Apple Intelligence", "angle": "Reveal the hidden implication that most reviews miss.", "suggested_length": 1200, "thumbnail_concept": "Split-screen before/after with bold headline" } ``` #### Response **Status Code:** 201 The newly created idea **Example Response:** ```json { "success": true, "data": { "id": 456, "channel_id": 123, "title": "Why Apple's AI Changes Everything", "topic": "Analysis of Apple Intelligence", "angle": "Reveal the hidden implication that most reviews miss.", "suggested_length": 1200, "suggested_template_id": 22, "status": "new", "script_id": null, "source_type": "user_added", "thumbnail_concept": "Split-screen before/after with bold headline", "outlier_score": null, "notes": null } } ``` **Response Fields:** | Field | Type | Description | | --- | --- | --- | | success | boolean | Request success | | data.id | integer | Idea ID | | data.channel_id | integer | Channel ID | | data.title | string | Idea title | | data.topic | string\|null | Topic | | data.angle | string\|null | Angle | | data.suggested_length | integer\|null | Target word count | | data.status | string | Idea status | | data.script_id | integer\|null | Linked script when converted | | data.source_type | string | user_added, ai_generated, from_video, … | #### Error Responses | Status | Error code | Description | | --- | --- | --- | | 403 | You do not have access to this channel | No channel access | | 404 | (not found) | Channel not found | ### List Ideas {#list-ideas} List ideas for a channel with optional filters. #### Request ``` GET https://subscribr.ai/api/v1/channels/{id}/ideas ``` **Authentication Required:** Yes (Bearer Token) **Required Abilities:** scripts:read No request body required. **Parameters:** | Name | Type | Description | | --- | --- | --- | | id | integer | Channel ID (path parameter) | | status | string | Filter by status: new, considering, needs_script, rejected (deprecated banger is accepted as considering). Use scripted to return ideas with a non-null script_id (not a stored status value). | | source_type | string | Filter by source (user_added, ai_generated, from_video, from_channel, outlier, chat_saved) | | search | string | Search in title, topic, or angle | | per_page | integer | 1-100, default 20 | | page | integer | Page number (default 1) | #### Response **Status Code:** 200 Array of idea objects with pagination **Example Response:** ```json { "success": true, "data": [ { "id": 456, "channel_id": 123, "title": "Why Apple's AI Changes Everything", "topic": "Analysis of Apple Intelligence", "angle": "Reveal the hidden implication...", "suggested_length": 1200, "suggested_template_id": 22, "status": "new", "script_id": null, "source_type": "user_added", "thumbnail_concept": "Split-screen before/after with bold headline", "outlier_score": null, "notes": null } ], "pagination": { "current_page": 1, "per_page": 20, "total": 1, "last_page": 1 } } ``` **Response Fields:** | Field | Type | Description | | --- | --- | --- | | success | boolean | Request success | | data[].id | integer | Idea ID | | data[].channel_id | integer | Channel ID | | data[].title | string | Idea title | | data[].topic | string\|null | Idea topic (nullable) | | data[].status | string | Idea status | | data[].angle | string\|null | Angle | | data[].suggested_length | integer\|null | Target word count | | data[].status | string | Idea status | | data[].script_id | integer\|null | Linked script ID | | data[].source_type | string | Source type | | pagination.current_page | integer | Current page | | pagination.per_page | integer | Page size | | pagination.total | integer | Total results | | pagination.last_page | integer | Last page | #### Error Responses | Status | Error code | Description | | --- | --- | --- | | 403 | You do not have access to this channel | No channel access | ### Get Idea Details {#get-idea} Get a single idea by ID. #### Request ``` GET https://subscribr.ai/api/v1/ideas/{id} ``` **Authentication Required:** Yes (Bearer Token) **Required Abilities:** scripts:read No request body required. **Parameters:** | Name | Type | Description | | --- | --- | --- | | id | integer | Idea ID (path parameter) | #### Response **Status Code:** 200 Detailed idea information **Example Response:** ```json { "success": true, "data": { "id": 456, "channel_id": 123, "title": "Why Apple's AI Changes Everything", "topic": "Analysis of Apple Intelligence", "angle": "Reveal the hidden implication...", "suggested_length": 1200, "suggested_template_id": 22, "status": "new", "script_id": null, "source_type": "user_added", "thumbnail_concept": "Split-screen before/after", "outlier_score": null, "notes": null } } ``` **Response Fields:** | Field | Type | Description | | --- | --- | --- | | success | boolean | Request success | | data.id | integer | Idea ID | | data.channel_id | integer | Channel ID | | data.title | string | Idea title | | data.topic | string\|null | Topic | | data.angle | string\|null | Angle or hook | | data.suggested_length | integer\|null | Target word count | | data.suggested_template_id | integer\|null | Suggested template ID | | data.status | string | new, considering, needs_script, rejected | | data.script_id | integer\|null | Linked script when converted | | data.source_type | string | user_added, ai_generated, from_video, from_channel, outlier, chat_saved | | data.thumbnail_concept | string\|null | Thumbnail concept | | data.outlier_score | number\|null | Outlier score when set | | data.notes | string\|null | Internal notes | #### Error Responses | Status | Error code | Description | | --- | --- | --- | | 403 | You do not have access to this idea | No channel access | | 404 | (not found) | Idea not found | ### Write Idea {#write-idea} Convert an idea into a canvas script (synchronous). Checks credits for the idea's target length; actual generation credits apply when you call Generate Script. Rate limit: 5 requests/minute (script). #### Request ``` POST https://subscribr.ai/api/v1/ideas/{id}/write ``` **Authentication Required:** Yes (Bearer Token) **Required Abilities:** scripts:write No request body. Idea ID is the `{id}` path parameter. **Parameters:** | Name | Type | Description | | --- | --- | --- | | id | integer | Idea ID (path) | #### Response **Status Code:** 201 Script created from idea **Example Response:** ```json { "success": true, "data": { "script_id": 789, "script_number": 12, "thread_id": 456, "canvas_url": "https://subscribr.ai/chat/my-channel-thread/canvas/789" } } ``` **Response Fields:** | Field | Type | Description | | --- | --- | --- | | success | boolean | Request success | | data.script_id | integer | Created or existing script ID | | data.script_number | integer | Script number within channel | | data.thread_id | integer | 201 only — chat thread ID | | data.canvas_url | string | 201 only — browser canvas URL | | data.status | string | 200 idempotent response only | | message | string | 200 idempotent response only | #### Error Responses | Status | Error code | Description | | --- | --- | --- | | 402 | insufficient_credits | Not enough credits for target length | | 403 | subscription_paused | Team subscription paused | | 403 | You do not have access to this idea | No channel access | | 404 | (not found) | Idea not found | #### Notes - Returns 201 when a new script is created. - Returns 200 when the idea already has script_id (idempotent) — includes message and status, omits thread_id and canvas_url. ### Generate Ideas {#generate-ideas} Queue AI generation of new ideas for a channel library. Async — poll List Channel Ideas; new rows use source_type ai_generated when ready. #### Request ``` POST https://subscribr.ai/api/v1/channels/{id}/ideas/generate ``` **Authentication Required:** Yes (Bearer Token) **Required Abilities:** scripts:write Optional count. Path id is your Subscribr channel ID. **Parameters:** | Name | Type | Description | | --- | --- | --- | | id | integer | Subscribr channel ID (path) | **Request Body Fields:** | Field | Type | Required | Description | | --- | --- | --- | --- | | count | integer | No | 1–20 ideas. Default 10. | **Example Request Body:** ```json { "count": 10 } ``` #### Response **Status Code:** 202 Async generation started **Example Response:** ```json { "success": true, "message": "Idea generation started.", "data": { "channel_id": 123, "count": 10 } } ``` **Response Fields:** | Field | Type | Description | | --- | --- | --- | | success | boolean | Request success | | message | string | Status message | | data.channel_id | integer | Channel ID | | data.count | integer | Number of ideas to generate | #### Error Responses | Status | Error code | Description | | --- | --- | --- | | 403 | You do not have access to this channel | No channel access | | 404 | (not found) | Channel not found | #### Notes - Returns 202 immediately. There is no job ID — refresh ideas via GET /channels/{id}/ideas. ### Generate Ideas From Video {#generate-ideas-from-video} Queue ideas inspired by a long-form YouTube video. Provide video_url or video_id (one required). Shorts (< 3 min) are rejected. #### Request ``` POST https://subscribr.ai/api/v1/channels/{id}/ideas/generate-from-video ``` **Authentication Required:** Yes (Bearer Token) **Required Abilities:** scripts:write One of video_url or video_id is required. **Parameters:** | Name | Type | Description | | --- | --- | --- | | id | integer | Subscribr channel ID (path) | **Request Body Fields:** | Field | Type | Required | Description | | --- | --- | --- | --- | | video_url | string | No | YouTube watch URL. Required if video_id omitted. Max 500. | | video_id | string | No | 11-character video ID. Required if video_url omitted. | | count | integer | No | 1–20. Default 10. | **Example Request Body:** ```json { "video_url": "https://youtube.com/watch?v=dQw4w9WgXcQ", "count": 10 } ``` #### Response **Status Code:** 202 Async generation started **Example Response:** ```json { "success": true, "message": "Idea generation from video started.", "data": { "channel_id": 123, "count": 10 } } ``` **Response Fields:** | Field | Type | Description | | --- | --- | --- | | success | boolean | Request success | | message | string | Status message | | data.channel_id | integer | Channel ID | | data.count | integer | Number of ideas to generate | #### Error Responses | Status | Error code | Description | | --- | --- | --- | | 403 | You do not have access to this channel | No channel access | | 422 | Invalid YouTube video ID or URL. | Could not parse video ID | | 422 | YouTube Shorts are not supported | Video under 3 minutes | | 400 | Could not fetch video | Intel lookup failed | | 404 | Video fetched but not found in database | Transient indexing issue — retry | #### Notes - Returns 202. Poll List Channel Ideas; completed ideas use source_type from_video. - Video is fetched via Intel lookup before generation is queued. ### Generate Ideas From Channel {#generate-ideas-from-channel} Queue ideas inspired by top videos from another YouTube channel. The source channel must already exist in Subscribr (Intel lookup, competitor tracking, etc.). Async — poll List Channel Ideas. #### Request ``` POST https://subscribr.ai/api/v1/channels/{id}/ideas/generate-from-channel ``` **Authentication Required:** Yes (Bearer Token) **Required Abilities:** scripts:write One of channel_handle or channel_id is required. **Parameters:** | Name | Type | Description | | --- | --- | --- | | id | integer | Subscribr channel receiving ideas (path) | **Request Body Fields:** | Field | Type | Required | Description | | --- | --- | --- | --- | | channel_handle | string | No | e.g. @mkbhd. Required if channel_id omitted. Max 200. | | channel_id | string | No | YouTube UC channel ID. Required if channel_handle omitted. Max 50. | | count | integer | No | 1–20. Default 10. | **Example Request Body:** ```json { "channel_handle": "@mkbhd", "count": 10 } ``` #### Response **Status Code:** 202 Async generation started **Example Response:** ```json { "success": true, "message": "Idea generation from channel started.", "data": { "channel_id": 123, "source_channel_title": "Marques Brownlee", "count": 10 } } ``` **Response Fields:** | Field | Type | Description | | --- | --- | --- | | success | boolean | Request success | | message | string | Status message | | data.channel_id | integer | Your channel ID receiving the ideas | | data.source_channel_title | string | Title of the YouTube channel used as inspiration | | data.count | integer | Number of ideas to generate | #### Error Responses | Status | Error code | Description | | --- | --- | --- | | 403 | You do not have access to this channel | No access to destination channel | | 404 | YouTube channel not found | Source channel not indexed in Subscribr | #### Notes - Provide channel_handle or channel_id (YouTube UC id), not your Subscribr channel id. - Completed ideas use source_type from_channel. ### Change Idea Topic {#change-idea-topic} Queue async rewrite of an idea's title, angle, and thumbnail concept for a new topic while keeping the same hook structure. Poll Get Idea Details for updates. #### Request ``` POST https://subscribr.ai/api/v1/ideas/{id}/change-topic ``` **Authentication Required:** Yes (Bearer Token) **Required Abilities:** scripts:write JSON body with new topic direction. **Parameters:** | Name | Type | Description | | --- | --- | --- | | id | integer | Idea ID (path) | **Request Body Fields:** | Field | Type | Required | Description | | --- | --- | --- | --- | | topic | string | Yes | New topic direction. 3–5000 characters. | **Example Request Body:** ```json { "topic": "Instead of productivity apps, apply this to morning routines" } ``` #### Response **Status Code:** 202 Async topic change started **Example Response:** ```json { "success": true, "message": "Topic change started.", "data": { "idea_id": 456 } } ``` **Response Fields:** | Field | Type | Description | | --- | --- | --- | | success | boolean | Request success | | message | string | Status message | | data.idea_id | integer | ID of the idea being updated | #### Error Responses | Status | Error code | Description | | --- | --- | --- | | 403 | You do not have access to this idea | No channel access | | 404 | (not found) | Idea not found | ## Scripts {#scripts} Create and manage video scripts, generate outlines and content. ### Create Script {#create-script} Create a new canvas script (chat-first) within a channel. Scripts can be created with research URLs, topics, angles, and templates. Creating a script charges credits based on the target length (same as Canvas creation). #### Request ``` POST https://subscribr.ai/api/v1/channels/{id}/scripts ``` **Authentication Required:** Yes (Bearer Token) **Required Abilities:** scripts:write JSON body with script configuration. Credits for the target `length` are charged immediately on create. **Request Body Fields:** | Field | Type | Required | Description | | --- | --- | --- | --- | | title | string | Yes | Script title. Max 255 characters. | | topic | string | Yes | Concept or brief. Min 10, max 5000 characters. | | length | integer | Yes | Target word count. Min 200, max 20000. Determines credits charged on create. | | angle | string | No | Angle or hook. Max 1000 characters. | | language | string | No | Spoken/written language. Defaults to the channel language setting. Max 50 characters. | | template_id | integer | No | Channel template ID. Must belong to this channel. | | voice_id | integer | No | Channel voice profile ID. Must belong to this channel. Takes precedence over `voice` when both are sent. | | voice | string | No | Raw voice label when not using `voice_id`. Max 100 characters. | | prompt | string | No | Custom opening chat message for the script thread. Max 8000 characters. Auto-generated when omitted. | | research_urls | array | No | Up to 10 source URLs to ingest into the script thread research context. Each URL max 500 characters. | | research_texts | array | No | Up to 10 text snippets for research context. Each snippet max 5000 characters. | **Example Request Body:** ```json { "title": "Why Apple's AI Changes Everything", "topic": "Break down the key changes in Apple Intelligence and what they mean.", "length": 1200, "angle": "Explain the hidden implication most reviews miss", "voice_id": 55, "language": "English", "template_id": 22, "research_urls": [ "https://example.com/source-1" ], "research_texts": [ "Key bullet points" ], "prompt": "Custom starting prompt" } ``` #### Response **Status Code:** 201 The newly created script **Example Response:** ```json { "success": true, "data": { "script_id": 789, "script_number": 12, "thread_id": 456, "status": "active", "canvas_url": "https://subscribr.ai/chat/my-channel-thread/canvas/789" } } ``` **Response Fields:** | Field | Type | Description | | --- | --- | --- | | success | boolean | Request success | | data.script_id | integer | New script ID | | data.script_number | integer | Sequential script number within the channel | | data.thread_id | integer | Chat thread ID backing the canvas | | data.status | string | Script lifecycle status (`active` on create) | | data.canvas_url | string\|null | Browser URL to open the script canvas; null if the thread slug is not available yet | #### Error Responses | Status | Error code | Description | | --- | --- | --- | | 402 | (credits) | Insufficient credits for target length | | 403 | subscription paused | Team billing paused | | 403 | You do not have access to this channel | No channel access | | 422 | Invalid voice_id for this channel | voice_id not on channel | | 422 | Invalid template_id for this channel | template_id not on channel | ### List Channel Scripts {#list-scripts} List scripts for a specific channel with optional filtering. #### Request ``` GET https://subscribr.ai/api/v1/channels/{id}/scripts ``` **Authentication Required:** Yes (Bearer Token) **Required Abilities:** scripts:read No request body required. **Parameters:** | Name | Type | Description | | --- | --- | --- | | id | integer | Channel ID (path parameter) | | status | string | Filter by status (active, idea) | | format | string | Filter by format (Tutorial, Documentary, News, Interview, Compilation) | | search | string | Search in title/topic | | per_page | integer | Results per page. Min 1, max 100. Default 20. | | page | integer | Page number. Min 1. Default 1. | #### Response **Status Code:** 200 Array of script summary objects with Laravel-style pagination **Example Response:** ```json { "success": true, "data": [ { "id": 789, "channel_id": 123, "script_number": 12, "title": "Why Apple's AI Changes Everything", "topic": "Break down the key changes...", "angle": "Explain the hidden implication most reviews miss", "length": 1200, "status": "active", "format": "Tutorial", "language": "English", "voice": "neutral", "template_id": 22, "production_status": "scripting", "thread_id": 456, "canvas_url": "https://subscribr.ai/chat/my-channel-thread/canvas/789", "has_outline": true, "has_script": false } ], "pagination": { "current_page": 1, "per_page": 20, "total": 1, "last_page": 1 } } ``` **Response Fields:** | Field | Type | Description | | --- | --- | --- | | success | boolean | Request success | | data[].id | integer | Script ID | | data[].channel_id | integer | Owning channel ID | | data[].script_number | integer | Sequential script number within the channel | | data[].title | string | Script title | | data[].topic | string | Topic or brief | | data[].angle | string\|null | Angle or hook | | data[].length | integer | Target word count | | data[].format | string\|null | Content format label (e.g. Tutorial, Documentary) | | data[].language | string | Language | | data[].voice | string\|null | Resolved voice label | | data[].template_id | integer\|null | Template ID when set | | data[].status | string | Script lifecycle status: `idea` or `active` | | data[].production_status | string | Production pipeline stage (e.g. `planning`, `scripting`, `recording`, `published`) | | data[].thread_id | integer\|null | Chat thread ID | | data[].canvas_url | string\|null | Browser canvas URL when available | | data[].has_outline | boolean | True when non-empty outline content exists | | data[].has_script | boolean | True when non-empty script body exists | | pagination.current_page | integer | Current page index | | pagination.per_page | integer | Page size used for this response | | pagination.total | integer | Total scripts matching filters | | pagination.last_page | integer | Last available page number | #### Error Responses | Status | Error code | Description | | --- | --- | --- | | 403 | You do not have access to this channel | No channel access | | 404 | (not found) | Channel not found | ### Get Script {#get-script} Get detailed information about a specific script. #### Request ``` GET https://subscribr.ai/api/v1/scripts/{id} ``` **Authentication Required:** Yes (Bearer Token) **Required Abilities:** scripts:read No request body required. **Parameters:** | Name | Type | Description | | --- | --- | --- | | id | integer | Script ID (path parameter) | #### Response **Status Code:** 200 Detailed script information **Example Response:** ```json { "success": true, "data": { "id": 789, "channel_id": 123, "script_number": 12, "title": "Why Apple's AI Changes Everything", "topic": "Break down the key changes...", "angle": "Explain the hidden implication most reviews miss", "length": 1200, "format": "Tutorial", "language": "English", "voice": "neutral", "template_id": 22, "status": "active", "production_status": "scripting", "thread_id": 456, "canvas_url": "https://subscribr.ai/chat/my-channel-thread/canvas/789", "has_outline": true, "has_script": false, "notes": "Notes for collaborators", "hook": "Open with a surprising fact", "thumbnail": "Apple AI Shock" } } ``` **Response Fields:** | Field | Type | Description | | --- | --- | --- | | success | boolean | Request success | | data.id | integer | Script ID | | data.channel_id | integer | Owning channel ID | | data.script_number | integer | Sequential script number within the channel | | data.title | string | Script title | | data.topic | string | Topic or brief | | data.angle | string\|null | Angle or hook | | data.length | integer | Target word count | | data.format | string\|null | Content format label | | data.language | string | Language | | data.voice | string\|null | Resolved voice label | | data.template_id | integer\|null | Template ID when set | | data.status | string | Script lifecycle status: `idea` or `active` | | data.production_status | string | Production pipeline stage | | data.thread_id | integer\|null | Chat thread ID | | data.canvas_url | string\|null | Browser canvas URL when available | | data.has_outline | boolean | True when non-empty outline content exists | | data.has_script | boolean | True when non-empty script body exists | | data.notes | string\|null | Collaborator notes | | data.hook | string\|null | Hook line or opening angle text | | data.thumbnail | string\|null | Thumbnail concept or title text | #### Error Responses | Status | Error code | Description | | --- | --- | --- | | 403 | You do not have access to this script | No channel access | | 404 | (not found) | Script not found | ### Get Script Content {#get-script-content} Fetch raw outline and script markdown for a canvas script. Use Get Script for metadata only. #### Request ``` GET https://subscribr.ai/api/v1/scripts/{id}/content ``` **Authentication Required:** Yes (Bearer Token) **Required Abilities:** scripts:read No request body required. **Parameters:** | Name | Type | Description | | --- | --- | --- | | id | integer | Script ID (path parameter) | #### Response **Status Code:** 200 Script content with outline and script versions **Example Response:** ```json { "success": true, "data": { "script_id": 789, "outline": "# Outline\\n...", "script": "# Script\\n...", "outline_version": 3, "content_version": 7 } } ``` **Response Fields:** | Field | Type | Description | | --- | --- | --- | | success | boolean | Request success | | data.script_id | integer | Script ID | | data.outline | string | Outline markdown. Empty string when no outline has been written yet. | | data.script | string | Script body markdown. Empty string when no script has been written yet. | | data.outline_version | integer | Monotonic outline revision counter | | data.content_version | integer | Monotonic script body revision counter | #### Error Responses | Status | Error code | Description | | --- | --- | --- | | 403 | You do not have access to this script | No channel access | | 404 | (not found) | Script not found | ### Generate Outline {#generate-outline} Start outline generation for a canvas script. Does not charge script-generation credits by itself. Returns a UUID `run_id` to poll via Poll Generation Status. Script-generation credits are charged when you call Generate Script (once per script). #### Request ``` POST https://subscribr.ai/api/v1/scripts/{id}/outline/generate ``` **Authentication Required:** Yes (Bearer Token) **Required Abilities:** scripts:write No request body required. #### Response **Status Code:** 202 Async outline job queued (`{ success, data }` envelope) **Example Response:** ```json { "success": true, "data": { "run_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "status": "queued" } } ``` **Response Fields:** | Field | Type | Description | | --- | --- | --- | | success | boolean | Request success | | data.run_id | string (UUID) | Run ID — pass as `run_id` query param to Poll Generation Status | | data.status | string | Initial status (`queued`) | #### Error Responses | Status | Error code | Description | | --- | --- | --- | | 403 | You do not have access to this script | No channel access | ### Generate Script {#generate-script} Start full script generation for a canvas script. Requires an existing outline. Returns a UUID `run_id` for Poll Generation Status. #### Request ``` POST https://subscribr.ai/api/v1/scripts/{id}/script/generate ``` **Authentication Required:** Yes (Bearer Token) **Required Abilities:** scripts:write No request body required. #### Response **Status Code:** 202 Async script job queued (`{ success, data }` envelope) **Example Response:** ```json { "success": true, "data": { "run_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "status": "queued" } } ``` **Response Fields:** | Field | Type | Description | | --- | --- | --- | | success | boolean | Request success | | data.run_id | string (UUID) | Run ID — pass as `run_id` query param to Poll Generation Status | | data.status | string | Initial status (`queued`) | #### Error Responses | Status | Error code | Description | | --- | --- | --- | | 409 | (message) | When no outline exists: `{ "error": "Outline must be generated before script generation.", "outline_required": true }` | ### Humanize Script {#humanize-script} Rewrite the script to sound more natural and human. Uses the channel voice profile when available. Counts toward the per-script regeneration limit. #### Request ``` POST https://subscribr.ai/api/v1/scripts/{id}/script/humanize ``` **Authentication Required:** Yes (Bearer Token) **Required Abilities:** scripts:write No request body required. #### Response **Status Code:** 202 Async humanize job queued (`{ success, data }` envelope) **Example Response:** ```json { "success": true, "data": { "run_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "status": "queued" } } ``` **Response Fields:** | Field | Type | Description | | --- | --- | --- | | success | boolean | Request success | | data.run_id | string (UUID) | Run ID — pass as `run_id` query param to Poll Generation Status | | data.status | string | Initial status (`queued`) | #### Error Responses | Status | Error code | Description | | --- | --- | --- | | 409 | no_script_content | Script has no canvas body content to humanize yet | | 422 | regeneration_limit_reached | Per-script regeneration limit exhausted | #### Notes - Humanizing is free (no credits charged) but counts toward the per-script regeneration limit alongside Generate Script and Start Fresh. - Returns 422 with error=regeneration_limit_reached when the limit is exhausted. - Poll the result using the Poll Generation Status endpoint with the returned run_id. ### Poll Generation Status {#poll-generation} Poll canvas outline/script/humanize jobs using the UUID `run_id` from Generate Outline, Generate Script, or Humanize Script. This is separate from Agent Mode polling (integer run IDs). Returns outline/script markdown only when `status` is `completed`. #### Request ``` GET https://subscribr.ai/api/v1/scripts/{id}/generate/poll ``` **Authentication Required:** Yes (Bearer Token) **Required Abilities:** scripts:read Query parameters for polling **Parameters:** | Name | Type | Description | | --- | --- | --- | | id | integer | Script ID (path parameter) | | run_id | string (UUID) | Run UUID from Generate Outline, Generate Script, or Humanize Script | #### Response **Status Code:** 200 Generation status (`{ success, data }` envelope). If the run record is not found yet, returns `status: queued` with `content_type: null`. **Example Response:** ```json { "success": true, "data": { "run_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "status": "completed", "content_type": "script", "outline": "# Outline\n\n## Section 1\n...", "script": "# Script\n\nOpening hook...\n" } } ``` **Response Fields:** | Field | Type | Description | | --- | --- | --- | | success | boolean | Request success | | data.run_id | string (UUID) | Run ID from the poll query | | data.status | string | Status: `queued`, `running`, `completed`, `failed`, or `canceled` | | data.content_type | string\|null | Content type for the run (`outline` or `script`); null while queued before the run record exists | | data.outline | string\|null | Outline markdown (only when `status` is `completed`) | | data.script | string\|null | Script markdown (only when `status` is `completed`) | #### Error Responses | Status | Error code | Description | | --- | --- | --- | | 400 | Missing run_id | run_id query param required | | 403 | You do not have access to this script | No channel access | > **⚠️ Canvas vs Agent Mode** > > Canvas async jobs use this endpoint with a UUID `run_id`. Agent Mode uses Agent Mode — Poll Status with an integer `run_id` and a different response shape. ### Agent Mode — Generate {#script-agent-generate} Trigger an Agent Mode run that autonomously researches, outlines, and writes a complete script. The agent uses your channel's voice profile, templates, and research context to produce a production-ready draft. This is an asynchronous operation — use Agent Mode — Poll Status to track progress. Credits: included Kimi/MiniMax/GLM models use the same length-based tiers as Canvas script generation; BYOK Claude/GPT runs charge a flat 2-credit access fee. Optional deep_research adds +3 credits when not reused. #### Request ``` POST https://subscribr.ai/api/v1/scripts/{id}/agent/generate ``` **Authentication Required:** Yes (Bearer Token) **Required Abilities:** scripts:write Optional JSON body. Defaults to included Kimi K2.6 (kimi_k26) with no BYOK. **Request Body Fields:** | Field | Type | Required | Description | | --- | --- | --- | --- | | model | string | No | kimi_k26, minimax_m27, glm_51 (included). claude_* and gpt* require BYOK. | | research | string | No | auto, on, off, or deep_research (+3 credits when deep_research runs). Default auto. | | visual_cues | string | No | off or on. Default off. | **Example Request Body:** ```json { "model": "minimax_m27", "research": "deep_research", "visual_cues": "on" } ``` #### Response **Status Code:** 202 Agent run queued successfully (flat JSON — no `success` wrapper) **Example Response:** ```json { "run_id": 42, "status": "queued", "research": "auto", "visual_cues": "off", "model": "kimi_k26", "resolved_model": "kimi-k2.6", "poll_url": "https://subscribr.ai/api/v1/scripts/789/agent/runs/42", "cancel_url": "https://subscribr.ai/api/v1/scripts/789/agent/runs/42/cancel", "message": "Script agent run queued. Poll the poll_url for status updates." } ``` **Response Fields:** | Field | Type | Description | | --- | --- | --- | | run_id | integer | Agent run ID — use in Agent Mode — Poll Status and Cancel Run paths | | status | string | Initial status (`queued`) | | research | string | Resolved research mode for this run (`auto`, `on`, `off`, or `deep_research` when enabled) | | visual_cues | string | Resolved visual cues mode (`on` or `off`) | | model | string | Requested Agent Mode model key | | resolved_model | string | Provider-specific model id the runtime will call | | poll_url | string | Absolute URL for Agent Mode — Poll Status | | cancel_url | string | Absolute URL for Agent Mode — Cancel Run | | message | string | Human-readable queue confirmation | #### Error Responses | Status | Error code | Description | | --- | --- | --- | | 402 | insufficient_credits | Team does not have enough credits for an agent run | | 403 | subscription_paused | Team subscription is paused | | 409 | run_already_active | A run is already in progress for this script | | 422 | prerequisites_not_met | Script is missing required context (channel, topic, or length) | | 422 | script_agent_byok_required | The selected model requires a BYOK key that is not connected. Includes required_provider, model, resolved_model, settings_url, and reason_code. | | 429 | rate_limit_exceeded | Too many agent runs — wait before triggering another | | 503 | script_agent_factory_access_unavailable | The included-model service is temporarily unavailable; no credits were charged | | 503 | agent_run_queue_failed | The run could not be queued; any credits charged were refunded | > **⚠️ Response shape** > > Agent Mode — Generate returns a flat JSON object at the top level (no `success` wrapper). Poll and cancel endpoints use `{ success, data }` or flat cancel fields respectively. Claude and GPT runs require BYOK; Kimi, MiniMax, and GLM do not. ### Agent Mode — Poll Status {#script-agent-poll} Check the progress of an Agent Mode run. Returns the current status, active step, and elapsed time. When the run completes, the response includes a link to the script. Poll this endpoint at a reasonable interval (e.g. every 5–10 seconds). #### Request ``` GET https://subscribr.ai/api/v1/scripts/{id}/agent/runs/{run_id} ``` **Authentication Required:** Yes (Bearer Token) **Required Abilities:** scripts:read No request body required. **Parameters:** | Name | Type | Description | | --- | --- | --- | | id | integer | Script ID (path parameter) | | run_id | integer | Agent run ID returned from Agent Mode — Generate | #### Response **Status Code:** 200 Current run status **Example Response:** ```json { "success": true, "data": { "run_id": 42, "status": "running", "current_step": "generating_script", "current_step_label": "Writing script", "elapsed_seconds": 94, "executor": "claude_sdk", "auth_path": "byok" } } ``` **Response Fields:** | Field | Type | Description | | --- | --- | --- | | success | boolean | Request success | | data.run_id | integer | Agent run identifier (integer, from Agent Mode — Generate) | | data.status | string | Run status: `queued`, `pending`, `running`, `completed`, `completed_with_issues`, `failed`, `cancelled` | | data.current_step | string\|null | Machine-readable step key (e.g. `generating_script`, `deep_research`, `done`); null when not started | | data.current_step_label | string\|null | Human-readable label from the Agent Mode step catalog | | data.elapsed_seconds | integer\|null | Seconds since the run started (null while queued) | | data.executor | string\|null | Executor used once running (e.g. `claude_sdk`); omitted while queued | | data.auth_path | string\|null | Credential path (e.g. `byok`, `factory`); omitted while queued | | data.script_url | string\|null | API URL to GET the script (only when `status` is `completed`) | | data.error | string | User-facing error (when `status` is `failed` or `cancelled`) | | data.error_detail | string | Raw diagnostic error for logs/support | > **⚠️ Async Status** > > Use this endpoint to poll an active Agent Mode run until it completes, fails, or is cancelled. ### Agent Mode — Cancel Run {#script-agent-cancel} Cancel a queued or running Agent Mode run. A cancelled run never completes, so its credits are refunded automatically. #### Request ``` POST https://subscribr.ai/api/v1/scripts/{id}/agent/runs/{run_id}/cancel ``` **Authentication Required:** Yes (Bearer Token) **Required Abilities:** scripts:write No request body required. **Parameters:** | Name | Type | Description | | --- | --- | --- | | id | integer | Script ID (path parameter) | | run_id | integer | Agent run ID returned from Agent Mode — Generate | #### Response **Status Code:** 200 Run cancelled (flat JSON — no `success` wrapper) **Example Response:** ```json { "cancelled": true, "run_id": 42, "status": "cancelled", "refunded": true, "message": "Run cancelled. Credits were refunded." } ``` **Response Fields:** | Field | Type | Description | | --- | --- | --- | | cancelled | boolean | Whether the run was cancelled | | run_id | integer | Agent run identifier | | status | string | Run status after cancellation (cancelled) | | refunded | boolean | True when the per-run Agent Mode fee was refunded (full-mode runs). False if refund could not be completed. | | message | string | Human-readable cancellation summary | #### Error Responses | Status | Error code | Description | | --- | --- | --- | | 404 | run_not_found | No such run for this script | | 409 | run_not_active | The run is already completed, failed, or cancelled | ### Export Script {#export-script} Export the current canvas script body. Requires non-empty script content. #### Request ``` GET https://subscribr.ai/api/v1/scripts/{id}/export ``` **Authentication Required:** Yes (Bearer Token) **Required Abilities:** scripts:read Query parameters for export **Parameters:** | Name | Type | Description | | --- | --- | --- | | id | integer | Script ID (path parameter) | | format | string | Output format: `markdown` (raw markdown), `html` (converted HTML), or `text` (plain text with markdown markers stripped) | | include_headings | boolean | When false, strips markdown heading markers before export. Default true. | #### Response **Status Code:** 200 Exported script content (`{ success, data }` envelope) **Example Response:** ```json { "success": true, "data": { "script_id": 789, "title": "Why Apple's AI Changes Everything", "format": "markdown", "content": "# Script\\n...", "include_headings": true } } ``` **Response Fields:** | Field | Type | Description | | --- | --- | --- | | success | boolean | Request success | | data.script_id | integer | Script ID | | data.title | string | Script title at export time | | data.format | string | Format that was applied (`text`, `html`, or `markdown`) | | data.content | string | Full exported body in the requested format | | data.include_headings | boolean | Whether markdown headings were preserved in the export | #### Error Responses | Status | Error code | Description | | --- | --- | --- | | 400 | Script export failed | Returned when the script has no canvas content to export | ## Thumbnails {#thumbnails} ### Get Thumbnail Usage {#get-thumbnail-usage} Get current thumbnail generation quota and usage for your team. Returns up to three quota pools: the free pool (included with every plan), an optional add-on pool (monthly subscription), and an optional pack pool (one-time purchased credits). The total_remaining field provides the combined remaining generations across all pools. **Rate limit:** 60 requests per minute #### Request ``` GET https://subscribr.ai/api/v1/team/thumbnails/usage ``` **Authentication Required:** Yes (Bearer Token) **Required Abilities:** scripts:read, thumbnails:read No request body required. #### Response **Status Code:** 200 Thumbnail usage and quota details **Example Response:** ```json { "success": true, "data": { "eligible": true, "free_pool": { "limit": 5, "used": 2, "reserved": 1, "remaining": 2, "next_reset": "2026-04-01" }, "addon_pool": { "limit": 50, "used": 10, "reserved": 0, "remaining": 40, "next_reset": "2026-04-15" }, "pack_pool": { "limit": 100, "used": 5, "reserved": 0, "remaining": 95, "next_reset": null }, "total_remaining": 137 } } ``` **Response Fields:** | Field | Type | Description | | --- | --- | --- | | success | boolean | Request success | | data.eligible | boolean | Whether the team is eligible for thumbnail generation (requires active subscription) | | data.free_pool | object | Free pool included with every plan | | data.free_pool.limit | integer | Monthly free generation limit | | data.free_pool.used | integer | Completed generations this period | | data.free_pool.reserved | integer | In-progress generations (counted against remaining until completed or released) | | data.free_pool.remaining | integer | Available generations (limit minus used minus reserved) | | data.free_pool.next_reset | string\|null | Pool reset date (Y-m-d format) | | data.addon_pool | object\|null | Monthly add-on subscription pool (null if not subscribed). Same fields as free_pool. | | data.pack_pool | object\|null | One-time purchased credit pack (null if none purchased). Same fields as free_pool. next_reset is always null — pack credits do not expire. | | data.total_remaining | integer | Combined remaining across all active pools | ### Create Thumbnail Generation {#create-thumbnail-generation} Create an asynchronous thumbnail generation for a channel. Three main modes: (1) **Idea-based** — provide an idea_id to generate a final 2K thumbnail from an existing idea. (2) **Brainstorm** — provide a prompt (no idea_id) to generate concept sketches at 1K resolution. Brainstorm results are stored as concept variations on a new idea; to produce final 2K thumbnails, call this endpoint again with the returned idea_id. (3) **Clone** — provide a clone_strategy and reference_image_url to generate thumbnails that match an existing thumbnail's style. The generation runs in the background; poll the returned run_id for status or use callback_url for async notification. **Rate limit:** 12 requests per minute #### Request ``` POST https://subscribr.ai/api/v1/channels/{id}/thumbnails/generations ``` **Authentication Required:** Yes (Bearer Token) **Required Abilities:** scripts:write, thumbnails:write JSON body. Path id is your Subscribr channel ID. Requires active subscription (403 if ineligible). **Parameters:** | Name | Type | Description | | --- | --- | --- | | id | integer | Subscribr channel ID (path). | **Request Body Fields:** | Field | Type | Required | Description | | --- | --- | --- | --- | | idea_id | integer | No | Idea-based or improvement mode. Must belong to channel. | | prompt | string | No | Required for brainstorm/clone when idea_id omitted. Max 2000. | | topic | string | No | Brainstorm context. Max 1000. | | num_variations | integer | No | 1–8. Default 1 with idea_id, 4 without. Each variation reserves one quota unit when idea_id set. | | callback_url | string | No | HTTPS callback (not team webhooks). Max 2048. | | callback_secret | string | No | Signs callback body via X-Subscribr-Signature. Max 255. | | improvement_mode | boolean | No | Requires idea_id, feedback, reference_variation_url. | | feedback | string | No | Required when improvement_mode=true. Max 5000. | | reference_variation_url | string | No | HTTPS URL. Required when improvement_mode=true. | | clone_strategy | string | No | direct_reference or style_analysis | | reference_image_url | string | No | HTTPS reference image. Required when clone_strategy set. | **Example Request Body:** ```json { "idea_id": 42, "num_variations": 3, "callback_url": "https://partner.example.com/subscribr/thumbnail-callback", "callback_secret": "whsec_thumbnail_partner_secret" } ``` #### Response **Status Code:** 202 The generation has been queued. When using clone_strategy=style_analysis, the response shape differs — see the style_analysis note below. **Example Response:** ```json { "success": true, "data": { "run_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d", "run_ids": [ "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d" ], "status": "queued" } } ``` **Response Fields:** | Field | Type | Description | | --- | --- | --- | | data.run_id | string | Primary generation run ID (UUID). Use this to poll status via the Get Generation Status endpoint. | | data.run_ids | array | All run IDs for this request. When num_variations > 1 with an idea_id, each variation gets its own run ID that can be polled individually. | | data.status | string | Initial status (always "queued") | | data.idea_id | integer | Returned only for clone modes — the auto-created idea ID | #### Error Responses | Status | Error code | Description | | --- | --- | --- | | 403 | not eligible for thumbnail generation | No active subscription | | 404 | Idea not found in this channel | Invalid idea_id | | 429 | (quota) | Insufficient thumbnail quota | #### Notes **Clone with style_analysis:** When clone_strategy=style_analysis, the response has a different shape: { idea_id, status: "style_analysis_pending", message }. Style analysis runs asynchronously before thumbnail generation. Use a callback_url to be notified when analysis completes, then call this endpoint again with the returned idea_id to generate the final thumbnail. **Quota consumption:** Quota is reserved per generation run. When num_variations > 1 with an idea_id, each variation gets its own run_id and consumes one quota unit. Brainstorm mode (no idea_id) reserves one unit for the batch. Reserved quota is released if a run fails. ### Get Thumbnail Generation Status {#get-thumbnail-generation} Get the current status and results of a thumbnail generation. When the status is "completed", the output_urls array contains URLs to the generated thumbnail images. Poll this endpoint after creating a generation to track progress. **Rate limit:** 120 requests per minute #### Request ``` GET https://subscribr.ai/api/v1/channels/{id}/thumbnails/generations/{runId} ``` **Authentication Required:** Yes (Bearer Token) **Required Abilities:** scripts:read, thumbnails:read No request body required. **Parameters:** | Name | Type | Description | | --- | --- | --- | | id | integer | Channel ID (path parameter). | | runId | string | Generation run ID returned from the Create endpoint (path parameter). | #### Response **Status Code:** 200 Generation status and output **Example Response:** ```json { "success": true, "data": { "run_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d", "status": "completed", "idea_id": null, "output_urls": [ "https://subscribr.ai/storage/thumbnails/9b1deb4d_v1.png", "https://subscribr.ai/storage/thumbnails/9b1deb4d_v2.png" ], "created_at": "2026-02-27T10:30:00Z", "updated_at": "2026-02-27T10:30:45Z" } } ``` **Response Fields:** | Field | Type | Description | | --- | --- | --- | | data.run_id | string | Generation run identifier (UUID) | | data.status | string | Status: queued (processing), completed (output ready), or failed | | data.idea_id | integer\|null | Linked idea ID (present when using idea-based or clone modes) | | data.output_urls | array | Generated thumbnail image URLs (populated when status is "completed", empty otherwise) | | data.output_urls[] | string | URL to a generated thumbnail image | | data.created_at | string | ISO 8601 creation timestamp | | data.updated_at | string | ISO 8601 last update timestamp (indicates when status last changed) | ### List Thumbnail Generations {#list-thumbnail-generations} List past thumbnail generation runs for a channel. Returns a paginated list (15 per page) ordered by most recent first. Optionally filter by source_type to narrow results to a specific generation mode. **Rate limit:** 120 requests per minute #### Request ``` GET https://subscribr.ai/api/v1/channels/{id}/thumbnails/generations ``` **Authentication Required:** Yes (Bearer Token) **Required Abilities:** scripts:read, thumbnails:read Optional query parameters to filter and paginate results. **Parameters:** | Name | Type | Description | | --- | --- | --- | | id | integer | Channel ID (path parameter). | | source_type | string | Filter by ThumbnailUsageEvent source_type: api_generate (idea-based and brainstorm API runs), idea_improve, thumbnail_clone | | page | integer | Page number (default 1). | #### Response **Status Code:** 200 Paginated list of thumbnail generation runs **Example Response:** ```json { "success": true, "data": [ { "run_id": "9b1deb4d-3b7d-4bad-9bdd-2b0d7b3dcb6d", "status": "completed", "source_type": "api_generate", "idea_id": 42, "output_urls": [ "https://subscribr.ai/storage/thumbnails/9b1deb4d_v1.png" ], "created_at": "2026-02-27T10:30:00Z" } ], "meta": { "current_page": 1, "last_page": 3, "per_page": 15, "total": 42 } } ``` **Response Fields:** | Field | Type | Description | | --- | --- | --- | | data[] | array | List of generation run objects | | data[].run_id | string | Run identifier (UUID) — use with the Get Generation Status endpoint | | data[].status | string | Status: queued, completed, or failed | | data[].source_type | string | api_generate, idea_improve, or thumbnail_clone | | data[].idea_id | integer\|null | Linked idea ID (null for brainstorm runs without a pre-existing idea) | | data[].output_urls | array | Generated thumbnail URLs (empty for queued/failed runs) | | data[].created_at | string | ISO 8601 creation timestamp | | meta.current_page | integer | Current page number | | meta.last_page | integer | Total number of pages | | meta.per_page | integer | Items per page (15) | | meta.total | integer | Total matching generation runs | ## Webhooks {#webhooks} ### List Webhooks {#list-webhooks} Retrieve all webhooks configured for your team (not paginated). Returns summary fields including success_rate and last_delivery. #### Request ``` GET https://subscribr.ai/api/v1/webhooks ``` **Authentication Required:** Yes (Bearer Token) **Required Abilities:** webhooks:read No request body required. #### Response **Status Code:** 200 Webhook summaries wrapped in success/data **Example Response:** ```json { "success": true, "data": [ { "id": 12, "name": "Production notifications", "url": "https://example.com/webhooks/subscribr", "events": [ "script.generated", "idea.created" ], "active": true, "success_rate": 98.5, "last_delivery": null } ] } ``` **Response Fields:** | Field | Type | Description | | --- | --- | --- | | success | boolean | Request success | | data | array | Webhook summaries | | data[].id | integer | Webhook ID | | data[].name | string | Webhook label | | data[].url | string | HTTPS endpoint URL | | data[].events | array\|null | Subscribed events; null or empty receives all event types | | data[].active | boolean | Whether deliveries are enabled | | data[].success_rate | number | Delivery success percentage | | data[].last_delivery | object\|null | Most recent delivery summary or null | ### Create Webhook {#create-webhook} Create a new webhook. Team administrator required. URL must be HTTPS. **Team admin required:** Yes #### Request ``` POST https://subscribr.ai/api/v1/webhooks ``` **Authentication Required:** Yes (Bearer Token) **Required Abilities:** webhooks:write JSON body with webhook configuration. **Request Body Fields:** | Field | Type | Required | Description | | --- | --- | --- | --- | | name | string | Yes | Unique label per team. Max 255. | | url | string | Yes | Public HTTPS URL. Max 500. | | events | array | No | Optional. Omit to receive all event types. Allowed: idea.created, script.outline.generated, script.generated, script.export.requested, thumbnail.generated, webhook.test | | timeout | integer | No | Delivery timeout in seconds. 5–120. Default 30. | **Example Request Body:** ```json { "name": "Partner hook", "url": "https://example.com/webhooks/subscribr", "events": [ "script.generated", "idea.created" ] } ``` #### Response **Status Code:** 201 Created webhook (secret shown once) **Example Response:** ```json { "success": true, "data": { "id": 13, "name": "Partner hook", "url": "https://example.com/webhooks/subscribr", "events": [ "script.generated" ], "secret": "store-this-secret-once", "active": true, "timeout": 30 } } ``` **Response Fields:** | Field | Type | Description | | --- | --- | --- | | success | boolean | Request success | | data.id | integer | Webhook ID | | data.name | string | Webhook label | | data.url | string | Webhook URL | | data.events | array\|null | Subscribed events | | data.secret | string | HMAC secret (only on create) | | data.active | boolean | Active status | | data.timeout | integer | Delivery timeout seconds | #### Error Responses | Status | Error code | Description | | --- | --- | --- | | 403 | Only team administrators can create webhooks | Not team admin | | 422 | (validation) | Invalid URL, events, or duplicate name | ### Get Webhook {#get-webhook} Retrieve a webhook with recent delivery history (last 20). #### Request ``` GET https://subscribr.ai/api/v1/webhooks/{webhook} ``` **Authentication Required:** Yes (Bearer Token) **Required Abilities:** webhooks:read No request body required. **Parameters:** | Name | Type | Description | | --- | --- | --- | | webhook | integer | Webhook ID (path) | #### Response **Status Code:** 200 Webhook detail with deliveries **Example Response:** ```json { "success": true, "data": { "id": 12, "name": "Production notifications", "url": "https://example.com/webhooks/subscribr", "events": [ "script.generated", "idea.created" ], "active": true, "headers": null, "timeout": 30, "success_rate": 98.5, "deliveries": [ { "id": 901, "event_type": "script.generated", "status": "success", "response_code": 200 } ] } } ``` **Response Fields:** | Field | Type | Description | | --- | --- | --- | | success | boolean | Request success | | data.id | integer | Webhook ID | | data.name | string | Webhook label | | data.url | string | HTTPS endpoint | | data.events | array\|null | Subscribed events | | data.deliveries | array | Recent delivery attempts | ### Update Webhook {#update-webhook} Update webhook settings. Team administrator required. All body fields are optional. **Team admin required:** Yes #### Request ``` PUT https://subscribr.ai/api/v1/webhooks/{webhook} ``` **Authentication Required:** Yes (Bearer Token) **Required Abilities:** webhooks:write JSON body with fields to update. **Parameters:** | Name | Type | Description | | --- | --- | --- | | webhook | integer | Webhook ID (path) | **Request Body Fields:** | Field | Type | Required | Description | | --- | --- | --- | --- | | name | string | No | New label. Max 255. | | url | string | No | HTTPS URL. Max 500. | | events | array | No | Replace event list (min 1 when provided) | | active | boolean | No | Enable or disable deliveries | | timeout | integer | No | 5–120 seconds | **Example Request Body:** ```json { "active": false } ``` #### Response **Status Code:** 200 Updated webhook **Example Response:** ```json { "success": true, "data": { "id": 12, "name": "Production notifications", "url": "https://example.com/webhooks/subscribr", "events": [ "script.generated" ], "active": false, "timeout": 30 } } ``` **Response Fields:** | Field | Type | Description | | --- | --- | --- | | success | boolean | Request success | | data.id | integer | Webhook ID | | data.active | boolean | Active status | #### Error Responses | Status | Error code | Description | | --- | --- | --- | | 403 | Only team administrators can update webhooks | Not team admin | ### Delete Webhook {#delete-webhook} Soft-delete a webhook. Team administrator required. **Team admin required:** Yes #### Request ``` DELETE https://subscribr.ai/api/v1/webhooks/{webhook} ``` **Authentication Required:** Yes (Bearer Token) **Required Abilities:** webhooks:write No request body required. **Parameters:** | Name | Type | Description | | --- | --- | --- | | webhook | string | Webhook ID (path parameter) | #### Response **Status Code:** 200 Webhook soft-deleted **Example Response:** ```json { "success": true, "message": "Webhook deleted successfully" } ``` **Response Fields:** | Field | Type | Description | | --- | --- | --- | | success | boolean | Request success | | message | string | Confirmation message | ### Test Webhook {#test-webhook} Send a test webhook event to your configured URL. Useful for testing your webhook handler. #### Request ``` POST https://subscribr.ai/api/v1/webhooks/{webhook}/test ``` **Authentication Required:** Yes (Bearer Token) **Required Abilities:** webhooks:write No request body required. **Parameters:** | Name | Type | Description | | --- | --- | --- | | webhook | string | Webhook ID (path parameter) | #### Response **Status Code:** 200 Test webhook delivery status **Example Response:** ```json { "success": true, "data": { "success": true, "status_code": 200, "response_body": "{\"received\":true}", "error": null } } ``` **Response Fields:** | Field | Type | Description | | --- | --- | --- | | success | boolean | API request success | | data.success | boolean | Whether the test delivery succeeded | | data.status_code | integer\|null | HTTP status from your endpoint | | data.response_body | string\|null | Response body from your endpoint | | data.error | string\|null | Error message if delivery failed | ### Webhook Events {#webhook-events} Reference documentation for all webhook events that can be subscribed to. Webhooks allow you to receive real-time notifications about important events in your Subscribr account. Subscribe to the events you care about and your webhook endpoint will receive POST requests with a JSON body shaped as { event, timestamp, data }. Delivery headers include X-Subscribr-Event, X-Subscribr-Delivery, X-Subscribr-Timestamp, and X-Subscribr-Signature (HMAC sha256=… when a secret is configured). ### Webhook Security {#webhook-security} Learn how to secure and verify webhook requests from Subscribr. All webhook requests from Subscribr are signed with HMAC SHA-256. You should verify this signature to ensure the request is authentic and hasn't been tampered with. The signature is in the `X-Subscribr-Signature` header as `sha256=` followed by the hex digest of HMAC-SHA256 over the raw JSON body using your webhook secret. ```javascript const crypto = require("crypto"); function verifyWebhookSignature(body, signature, secret) { const hash = crypto .createHmac("sha256", secret) .update(body, "utf8") .digest("hex"); return crypto.timingSafeEqual( Buffer.from(hash), Buffer.from(signature) ); } // In your webhook handler: const signature = req.headers["x-subscribr-signature"]; const body = req.rawBody; // Raw request body as string const secret = process.env.WEBHOOK_SECRET; if (!verifyWebhookSignature(body, signature, secret)) { return res.status(401).json({ error: "Invalid signature" }); } // Process webhook... res.status(200).json({ success: true }); ```