Skip to content

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.

StatusCodeWhen
400invalidthe body or a parameter is wrong — the message says which
401unauthenticatedno key, or a revoked or expired one
402creditsnot enough credits for the job
403plan / scopethe plan does not include it, or the key lacks the scope
404not_foundnothing with that id in this workspace
409limita plan limit, e.g. jobs already queued
429rate_limitedmore than 120 requests a minute for this key — see Retry-After
GET/api/v1/mescope: read

The 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"] } }
GET/api/v1/projectsscope: read

The workspace’s projects, newest first (up to 200).

{ "data": [ { "id": "prj_…", "name": "Engineering explainers", "brief": "…", "language": "en", "state": "active", "created_at": "…" } ] }
POST/api/v1/projectsscope: write

Create a project from a brief. Niche research starts at once unless you send research: false (about 3 credits).

  • name2–80 characters
  • brief30–4,000 characters — what you want; it steers every proposal
  • languageBCP 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_…" }
GET/api/v1/channelsscope: read

Every 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", … } ] }
GET/api/v1/episodesscope: read

The library, newest first, a page at a time.

  • channel_idonly this channel
  • stategenerating · ready · held · failed · published · archived
  • limit1–100, default 25
  • cursornext_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_…" }
GET/api/v1/episodes/{id}scope: read

One 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 }
GET/api/v1/jobsscope: read

The latest 50 jobs: kind, state, phase and progress, error, credits charged.

{ "data": [ { "id": "job_…", "kind": "episode.generate", "state": "running", "phase": "…", "progress": 0.55, … } ] }
POST/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 below
  • channel_id / project_id / episode_idwhat the job works on
  • paramskind-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 }
GET/api/v1/jobs/{id}scope: read

One 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_…" } ] }, … }
DELETE/api/v1/jobs/{id}scope: jobs

Cancel. 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

kindneedswhat it does
niches.suggestproject_idresearch and score niche proposals · params.notes
channel.draftproject_iddraft a channel from a proposal · params.niche_id
topics.seedchannel_idplan more topics · params.n (4–30)
kit.createchannel_idbuild the channel’s renderer kit on a GPU · params.repair, params.notes
brand.createchannel_idlogos, banners, covers and BRAND.md, rendered with the kit
episode.generatechannel_idan original episode, checked by the critic · params.quality (standard · premium), params.topic_key
episode.publishepisode_idpost to a connected account · params.platform (youtube · facebook · instagram · linkedin)
analytics.pullchannel_idrefresh 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.