API reference
Drive ChannelForge from your own code: create projects, start and cancel jobs, read the library — and hear about finished work the moment it happens. JSON over HTTPS, on the Pro plan and above.
Authentication
Create a key in Settings → API & webhooks. A key belongs to one workspace and carries scopes: read (every GET), write (projects, channels, schedules), jobs (start and cancel work — it spends credits) and publish (post to connected accounts). It is shown once; we keep only its hash. Send it as a bearer token:
curl https://forge.storeswiki.com/api/v1/me -H "Authorization: Bearer fk_live_…"Errors and limits
Every error has the same shape, {"error": {"code": "…", "message": "…"}}, and the message is written to be shown to a person. Each key may make 120 requests a minute.
| Status | Code | When |
|---|---|---|
| 400 | invalid | the body or a parameter is wrong — the message says which |
| 401 | unauthenticated | no key, or a revoked or expired one |
| 402 | credits | not enough credits for the job |
| 403 | plan / scope | the plan does not include it, or the key lacks the scope |
| 404 | not_found | nothing with that id in this workspace |
| 409 | limit | a plan limit, e.g. jobs already queued |
| 429 | rate_limited | more than 120 requests a minute for this key — see Retry-After |
/api/v1/mescope: readThe workspace the key belongs to, its plan and credits, and the key itself.
{ "workspace": { "id": "ws_…", "slug": "northwind", "name": "Northwind Media", "plan": "pro", "credits": 3160, "state": "active" },
"key": { "id": "key_…", "name": "Zapier", "scopes": ["read", "jobs"] } }/api/v1/projectsscope: readThe workspace’s projects, newest first (up to 200).
{ "data": [ { "id": "prj_…", "name": "Engineering explainers", "brief": "…", "language": "en", "state": "active", "created_at": "…" } ] }/api/v1/projectsscope: writeCreate a project from a brief. Niche research starts at once unless you send research: false (about 3 credits).
name2–80 charactersbrief30–4,000 characters — what you want; it steers every proposallanguageBCP 47 primary tag, default "en"researchboolean, default true
curl -X POST https://forge.storeswiki.com/api/v1/projects \
-H "Authorization: Bearer fk_live_…" -H "Content-Type: application/json" \
-d '{"name":"Lost cities","brief":"Reconstructions of ancient cities at their peak, told in 60 seconds with maps."}'201 { "id": "prj_…", "job_id": "job_…" }/api/v1/channelsscope: readEvery channel that is not archived: its stage, kit and brand state, schedule and platforms.
{ "data": [ { "id": "ch_…", "project_id": "prj_…", "key": "loadpath", "name": "Load Path", "state": "live", "kit_state": "built",
"brand_state": "built", "schedule_enabled": true, "post_times": ["09:00","15:00","21:00"], "timezone": "Europe/Rome", … } ] }/api/v1/episodesscope: readThe library, newest first, a page at a time.
channel_idonly this channelstategenerating · ready · held · failed · published · archivedlimit1–100, default 25cursornext_cursor from the previous page
{ "data": [ { "id": "ep_…", "channel_id": "ch_…", "title": "Why the Golden Gate sways", "state": "ready", "duration_s": 148, "score": 8, … } ],
"next_cursor": "ep_…" }/api/v1/episodes/{id}scope: readOne episode with its posts, and signed links to the video and thumbnail that work for 15 minutes.
{ "id": "ep_…", "title": "…", "state": "published", "posts": [ { "platform": "youtube", "state": "posted", "url": "…", "views": 18400 } ],
"video_url": "https://…", "thumb_url": "https://…", "urls_expire_in": 900 }/api/v1/jobsscope: readThe latest 50 jobs: kind, state, phase and progress, error, credits charged.
{ "data": [ { "id": "job_…", "kind": "episode.generate", "state": "running", "phase": "…", "progress": 0.55, … } ] }/api/v1/jobsscope: jobs (publish for episode.publish)Start work. The credits a job is expected to use are held at once and settled when it ends; a job that fails because of us is not charged.
kindone of the job kinds belowchannel_id / project_id / episode_idwhat the job works onparamskind-specific options (below)idempotency_keyup to 100 characters: sending the same key again returns the same job instead of a second one
curl -X POST https://forge.storeswiki.com/api/v1/jobs \
-H "Authorization: Bearer fk_live_…" -H "Content-Type: application/json" \
-d '{"kind":"episode.generate","channel_id":"ch_…","params":{"quality":"standard"},"idempotency_key":"nightly-2026-10-19"}'202 { "id": "job_…", "state": "queued", "credits_reserved": 190 }/api/v1/jobs/{id}scope: readOne job: state, phase, progress, attempts, error and its kind, the result, credits held and charged.
{ "id": "job_…", "kind": "episode.generate", "state": "succeeded", "attempts": 1, "result": { "episodes": [ { "episode_id": "ep_…" } ] }, … }/api/v1/jobs/{id}scope: jobsCancel. A queued job stops at once and its credits are released; a running one stops at its next checkpoint and pays for what it used.
202 { "id": "job_…", "state": "cancelled", "cancel_requested": false }Job kinds
| kind | needs | what it does |
|---|---|---|
| niches.suggest | project_id | research and score niche proposals · params.notes |
| channel.draft | project_id | draft a channel from a proposal · params.niche_id |
| topics.seed | channel_id | plan more topics · params.n (4–30) |
| kit.create | channel_id | build the channel’s renderer kit on a GPU · params.repair, params.notes |
| brand.create | channel_id | logos, banners, covers and BRAND.md, rendered with the kit |
| episode.generate | channel_id | an original episode, checked by the critic · params.quality (standard · premium), params.topic_key |
| episode.publish | episode_id | post to a connected account · params.platform (youtube · facebook · instagram · linkedin) |
| analytics.pull | channel_id | refresh views, likes and comments from the platforms |
Webhooks
Add an HTTPS endpoint in Settings → API & webhooks and pick its events: job.succeeded, job.failed, episode.ready, post.published, post.failed. Each delivery is a POST of {"id", "type", "created", "data"} with X-Forge-Event and a signature you should check before trusting it:
X-Forge-Signature: t=1792360000,v1=<hex HMAC-SHA256(signing secret, "<t>.<raw body>")>import { createHmac, timingSafeEqual } from 'node:crypto';
export function verify(rawBody: string, header: string, secret: string, toleranceS = 300): boolean {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > toleranceS) return false; // too old: a replay
const want = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
return want.length === parts.v1?.length && timingSafeEqual(Buffer.from(want), Buffer.from(parts.v1));
}Answer with any 2xx within 10 seconds. A failed delivery is tried three times; an endpoint that keeps failing (20 in a row) is switched off and shows why in your settings. We only deliver to public HTTPS addresses and never follow redirects.
Questions or a missing endpoint? Write to support@storeswiki.com. Related: security and system status.