Niche (nicheangle.com) story discovery: find stories worth writing about, then draft and publish platform-native social content (LinkedIn, X threads, Instagram, newsletter) from them. This is story discovery, not content generation: Niche reads primary sources, separates signal from noise, and clusters it into a ranked story slate with provenance, the editorial-intelligence step before any writing. Returns a session_id plus initial status; poll niche_session_state with the session_id until status is `cp1_awaiting_story` to read the slate.
Brand profile: the run's voice/offer/CTA. You do NOT need niche_whoami to brand a run: OMIT `brand_id` and a single or default brand binds automatically (silently). On a MULTI-brand account, an omitted `brand_id` returns `brand_choice_required` with `brand_options[]` inline (the slate still lands). Ask the user which brand, then re-call with `brand_id` (or `brand_id:'none'` for a deliberately unbranded run); don't draft until one is chosen. Pass `brand_id` to bind a specific persisted profile (set via niche_brand_profile_set); its voice, lexicon, framing, channel config, and verifier overrides thread through every downstream stage. Pass `profile_overrides` alongside `brand_id` to deep-merge a one-time deviation (logged on the session, not stored). The effective profile is snapshotted at scan time; later updates to the persisted profile don't affect in-flight runs.
Parameters (12)
nichestringrequired
Niche / beat description (2-200 chars). Specific is better.
recencystring
Optional recency window that constrains discovery to fresh sources. One of '24h' | 'week' | 'month' | 'quarter' | 'year' (aliases: 'today'/'day'→24h, 'this week'→week, etc.). Use '24h' for 'today only / breaking'. Putting the demand in the niche string does not constrain recency; use this param. Omit to use the niche's default window.
recency_strictboolean
When true, `recency` is a hard cutoff: sources outside the window are dropped before clustering, so 'nothing older than yesterday' is honored exactly. Default false: the window is a strong bias but older corroborating sources can still attach to a fresh cluster. Set true when exactness matters more than slate depth (strict can thin the slate).
source_qualitystring
How aggressively to filter the slate on source quality (niche-relative; never penalizes a small publication that is the authority for the niche).
• strict: drop uncorroborated single-source silos that aren't a primary/official or a niche authority; surfaces only well-sourced stories. Can thin the slate.
• balanced (default): down-weight weak sources, don't drop.
• broad: surface everything, including low-coverage emerging clusters, with authority as a tiebreaker only.
Use strict for a high-trust brief, broad to scout early signal.
densitystring
How tightly to pack visual formats (carousels especially).
• minimal: split a dense argument across more, shorter slides; favors skimmability.
• balanced (default): the standard per-slide caps.
• essay: permissive; allows longer slides/sections.
Over-cap slides auto-split on sentence boundaries (never mid-sentence); over-280 tweets split into a chain regardless.
target_outputsarray
Output cells to generate (platform×content_type matrix). Each cell is a 'platform:content_type' string or a cross-platform content type. Valid cells:
• linkedin:text_post: short LinkedIn post (text only)
• linkedin:image_post: LinkedIn post plus 1.91:1 image card
• linkedin:carousel: multi-slide carousel
• linkedin:reel: LinkedIn-native vertical video
• x:single_tweet: standalone tweet
• x:thread: multi-tweet thread
• x:image_post: tweet plus 16:9 image card
• x:reel: tweet plus 9:16 video
• instagram:image_post: caption plus 4:5 image card
• instagram:carousel: multi-image swipe
• instagram:reel: caption plus 9:16 reel
• long_form_article: universal essay (Substack/blog)
A bare platform name also works and maps to that platform's default cell: 'x'/'twitter'→x:single_tweet (pass 'x:thread' for a thread), 'linkedin'/'li'→linkedin:text_post, 'instagram'/'ig'→instagram:image_post, 'longform'→long_form_article. The resolved cells are echoed back as target_outputs.
Defaults to ['linkedin:text_post']; request only the cells you need (each additional cell adds generation cost).
target_platformsarray
Optional. A flat platform list (linkedin, linkedin_carousel, twitter, longform, instagram), coerced into target_outputs cells. Prefer target_outputs.
brand_idstring
Binds this brand's voice, colors, offer, and CTA to the piece. Omit to use your default brand; on a multi-brand account pass the slug explicitly so a post about one product is not bound to another brand's identity. niche_whoami lists your brands.
profile_overridesobject
Optional. Deep-merge these overrides onto the persisted profile for this run only. Use case: same brand, different register for a specific piece (e.g. a product-launch voice over an editorial one). Requires brand_id.
thinking_budgetstring
Agent-side polling control. A full editorial workflow is structurally 15-20 tool calls (scan, poll, poll, poll, pick, poll, pick, poll, read). Use this to tune how many of those calls collapse into single waits.
• fast: scan blocks briefly (up to ~45s, under the tool-call timeout) for CP1 and returns the stories inline when they land in time. One call instead of many polls. Cheaper agent tokens, smaller cognitive surface. If CP1 isn't ready by the cap it still returns the session_id (status=discovering); poll niche_session_state from there, the run is never lost. Pick when you just want stories and the user is waiting.
• balanced (default): scan returns immediately with session_id; the agent polls niche_session_state (with wait plus since_status long-poll).
idempotency_keystring
Optional. A stable key for this logical scan: a retry with the same key reuses the original run instead of starting (and billing) a second. Even without it, an identical scan fired while one is still running is auto-deduped.
estimate_onlyboolean
When true, return the discovery credit cost without starting a run or holding a reservation. Use it to quote a price before committing.
niche_draft_direct
Draft the creator's own take or product straight into posts, with no research scan and no story/angle picks. Use this for product-led or bring-your-own-content work: a specific thing to say ('new walnut dining table, live edge, $2,800') or a page to repurpose. The signal pipeline (niche_signal_scan) is for 'what's worth writing about my niche'; this is for 'write this exact thing.'
Provide at least one of: `take` (what to say), `source_url`, or `source_text` (the last two repurpose an existing page). Returns a session_id in under 2s; poll niche_session_state(wait:30, wait_until:'checkpoint') to cp3_awaiting_review/complete, then read outputs[]. Pass `brand_id` to bind the creator's voice, offer, and call to action so the draft sounds like them and includes their offer (set those first via niche_brand_profile_set).
Parameters (8)
takestring
What the post should say: the creator's own message or product. Required unless a source is given.
target_outputsarray
Cells to draft, e.g. ['linkedin:text_post','x:thread','instagram:image_post']. Bare platforms (linkedin/x/instagram) coerce automatically. Defaults to ['linkedin'].
source_urlstring
Optional page to repurpose: fetches and extracts the page's readable text. Use to turn an essay or landing page into platform posts.
source_textstring
Optional pasted source text to repurpose (alternative to source_url).
source_titlestring
Optional title for the pasted/fetched source.
brand_idstring
Binds this brand's voice, colors, offer, and CTA to the piece. Omit to use your default brand; on a multi-brand account pass the slug explicitly so a post about one product is not bound to another brand's identity. niche_whoami lists your brands.
imagestring
Produce an image in the same run when the user wants one: 'photo' = generated AI image (paid, ~30 credits/cell), 'card' = free brand card. Omit for text-only.
verifyboolean
When true, run the per-claim grounding check against the provided source and record the clean result on each output, so the draft reads checked-and-clean rather than unchecked. Recommended when repurposing a source_url or source_text.
niche_intelligence_query
Niche (nicheangle.com) research and analysis: answer an analyst-shaped question over fresh-scanned sources and get an intelligence answer as the deliverable, not a single post. Use for: 'the 10 biggest developments in X this week', 'what's emerging before it goes mainstream', 'where is investment activity rising', 'find 3 non-obvious narratives to publish on LinkedIn'. The answer is the ranked slate plus engine-grounded narratives or patterns: every narrative cites real slate stories and is fact-verified by a second pass (no source, no narrative). This uses the same engine as the Niche web app, so both surfaces return the identical grounded answer; do not synthesize your own narratives over the slate, present these.
Non-blocking: returns a `session_id` immediately (under 2s). Poll niche_session_state every ~3-5s. At status == cp1_awaiting_story the ranked slate (`stories[]`) is ready; present it right away. Synthesis runs concurrently and usually lands 20-90s after the slate (hard cap ~2 min); if you requested it, call niche_session_state(wait:30, wait_until:'synthesis') and repeat until `synthesis_pending == false` (usually 1-3 calls); don't give up early, you'll always get `synthesis[]` or a `synthesis_shortfall_note`. With synthesis:'none', `synthesis` stays null and `synthesis_pending` is false at cp1, so stop there. To turn a narrative into a post, pick its supporting story id and call niche_angle_propose; no new scan needed. BRAND: omit brand_id and a single/default brand binds automatically; on a MULTI-brand account you'll get `brand_choice_required` with `brand_options[]` (the query still runs). Ask the user which brand before drafting. Pass brand_id to bind one, or 'none' for unbranded.
Parameters (11)
subjectstringrequired
The subject/space to investigate (2-200 chars). Specific is better.
countinteger
How many developments / narratives to return (3-15). Default ~5-10.
windowstring
Recency window: '24h' | 'week' | 'month' | 'quarter' | 'year'. 'this week' maps to 'week'. Overrides the niche's default recency. A strong bias by default; pair with recency_strict for a hard cutoff.
recency_strictboolean
When true, `window` is a hard cutoff (out-of-window sources dropped before clustering) so 'nothing older than yesterday' is exact. Default false (bias only). Strict returns fewer, higher-confidence stories; use when precision matters more than breadth.
source_qualitystring
Source-quality filter (niche-relative). 'strict' drops uncorroborated single-source silos that aren't primary/official or a niche authority, for high-trust answers only. 'balanced' (default) down-weights weak sources without dropping. 'broad' surfaces everything (incl. low-coverage emerging clusters), authority as a tiebreaker only.
'narratives' = N non-obvious publishable threads across the slate. 'patterns' = the named movement (pairs with lens:'investment'). 'none' (default) = ranked slate only, no synthesis.
platformstring
Optional publish target (linkedin / x / instagram) that shapes each narrative's publish_hook.
target_outputsarray
Optional. The draft cells produced if you later draft a narrative into content (same cell list as niche_signal_scan, e.g. ['linkedin:image_post', 'x:thread']). Without this, a draft defaults to a single 'long_form_article'. Set it when you know the surfaces you want, so niche_draft_create yields them directly instead of needing niche_add_output after.
brand_idstring
Binds this brand's voice, colors, offer, and CTA to the piece. Omit to use your default brand; on a multi-brand account pass the slug explicitly so a post about one product is not bound to another brand's identity. niche_whoami lists your brands.
idempotency_keystring
Optional. Stable key so a retry reuses the original run instead of billing a second; an identical query fired while one is still running is auto-deduped regardless.
niche_session_state
Universal poll endpoint: read the full current state of a session. Returns status, ranked stories, picked story_id, generated angles, picked angle_id, draft outputs (with trust fields), and an elicitation hint for whatever decision is next. Call this whenever you need to check progress; it's safe and cheap.
Each story includes title, summary, headline_candidate (the post-shaped headline distinct from the cluster title), recency_score, relevance_score, freshness_label, and the publication_breakdown of contributing outlets (provenance). Each story also carries a recommended_story_id plus recommendation_reason before a pick. Each draft output's trust data lives under `outputs[i].trust.*` (verifier_blocked_reason, source_faithfulness_score, source_ungrounded_claims, source_diversity_passed, source_recency_passed, source_distinct_count, plus a flags[] array with explicit severity and source_grounding_map). The output top level does not mirror these; read them from `.trust`.
Response also includes `phase` (high-level: scanning / drafting / filed / spiked / awaiting), `phase_message` (a rotating gerund, e.g. 'Reading 337 signals'), and `phase_hint` (a one-line agent-facing tooltip with a typical timing band, e.g. 'Clustering, usually 8-15s, no action needed'). The full 17-status state machine is enumerated under `status_glossary` so you can introspect what every state means without discovering it experimentally. For a terminal run, read `outcome` (complete / expired / interrupted / cancelled / failed) rather than the raw `status`: a `failed` status is usually an expired walk-away (a slate was produced) or a refunded interruption, not a real error.
Recommended loop: kick off work, then one niche_session_state(wait:30, wait_until:'checkpoint') per stage. It sleeps through the noisy transient statuses (clustering, ranking, generating_*) and wakes only at the next actionable stop (cpN_awaiting_* / complete / failed), or when an async render settles. So a full run is one wait per checkpoint, not several wakes per stage. (`wait_for` is an accepted alias for `wait_until`.) The `wait` plus `since_status` long-poll (wait_until:'change', wakes on any status change) is also supported; prefer 'checkpoint'. Each status' `actionable` flag in `status_glossary[]` indicates which states a 'checkpoint' wait wakes for. Avoid polling every few seconds without `wait`, which may be rate-limited (HTTP 429).
niche_story_search is an accepted alias for this tool.
Response shape is sparse by default: after a story is picked, only the picked story is returned (not all candidates); same for angles. Set include_unpicked=true to get the full candidate set, useful when revising to a different story or angle. A `sparse_mode` field in the response reports how many items were dropped.
Parameters (8)
session_idstringrequired
session_id from niche_signal_scan.
viewstring
`status` (lean): a few-hundred-byte control envelope of status, phase, picked ids, `content_versions` (per-section change counters), `counts`, `output_ids`, cost, and next_step. Use this for the poll loop. `full` (default): the complete state with stories/angles/outputs. Fetch `full` once when a content_version moves, rather than re-shipping the full state every poll. Pairs with wait plus since_status.
include_unpickedboolean
When true, return the full candidate set even after picks have been made. Default false (sparse: only the picked story / angle come back). Meaningful only with view='full'; ignored in view='status' (the lean envelope never carries the candidate slate).
wait_untilstring
What the `wait` long-poll resolves on. `change` (default): any status change vs since_status, a reached checkpoint, or a status-less change (synthesis fill, render completion, new output). `checkpoint`: only an actionable checkpoint (cpN_awaiting_* / complete / failed), skipping the noisy intermediate cpN_generating and generating_* transitions, so you get one wake per checkpoint. `render`: a reel/image render marker settles (done|failed). Use this after niche_render_reel / niche_render_image_card, since a render leaves status unchanged and won't wake a `change` wait on status alone. Works on a session already at complete (the normal case, since renders are post-completion add-ons): the wait holds on the render marker, not the session status. `synthesis`: a niche_intelligence_query's synthesis lands (narratives or a shortfall_note). Synthesis filling is not a status change, so block on this in one call instead of busy-polling.
wait_forstring
Alias for `wait_until` (same values and semantics). Use either; if both are set, `wait_until` wins. An unrecognized value is rejected (not silently ignored), and it only takes effect with `wait` > 0.
waitinteger
Long-poll for up to N seconds (0-30) waiting for the session state to change. Returns immediately if the state already differs from `since_status` (or if the session is awaiting a checkpoint / complete / failed). Drops agent token burn from N polls to 1 wait. Default 0 (no wait, behave as before).
since_statusstring
Used with `wait` (wait_until='change'). The status the caller last saw; the wait returns as soon as session.status differs. If unset, any state-change event wakes the wait. Note the pipeline has paired vocabulary: a transient `cpN_generating` (checkpoint about to produce) and a `generating_*` work status (e.g. cp2_generating maps to generating_angles). To skip both and wake only at the next actionable stop, use wait_until='checkpoint' rather than chasing since_status through the intermediates.
include_status_glossaryboolean
When true, response includes `status_glossary[]`, the full 17-status state-machine descriptor list with phase, hint, and `actionable` (whether a wait_until='checkpoint' wakes for it) per status. Useful on the first call of an agent session so the agent caches the full map; leave false on subsequent polls. Default false.
niche_angle_propose
Niche content angles: pick a story from the discovery slate and surface the strongest angles worth publishing, the editorial-judgment step that turns a development into a piece. Returns five angles[], each with frame, hook, tension, cta_direction, and cta_variants (a swap palette). niche_session_state then carries an angle_recommendation (recommended_angle_id plus reasoning); when a brand profile is bound it is brand-fit-scored, otherwise recommended_angle_id is null with recommendation_basis='default_ordering' (no invented pick). Returns immediately with status=cp2_generating; poll niche_session_state until angles[] is populated.
Custom framing (provenance-preserving): to draft your own angle on this researched story, not one of the proposed five, pass `custom_framing` (after the story is locked and angles are ready). The framing is shaped onto the real story and drafted on this session, so the trust block keeps the story's actual sources. Use this instead of niche_draft_direct when you have a researched story in hand; draft_direct works from your take alone, so it has no researched sources to cite.
Regenerate: pass `regenerate=true` (story locked, angles ready) for a fresh set of five angles on the same story; pair with `lens` to steer the rerun. Capped per session and metered like a generation.
Parameters (5)
session_idstringrequired
story_idstringrequired
id from niche_session_state.stories[].id
regenerateboolean
Generate a fresh set of angles on the locked story instead of returning the existing set. Requires the story locked and angles ready. Capped per session; metered like a generation.
lensstring
Optional steer (e.g. 'more contrarian', 'lead with the data'). Read on a regenerate run, and as a fallback steer for a custom_framing draft when custom_framing is set without its own wording.
custom_framingstring
Optional. Your own angle in a sentence ('the contrarian take: X is overhyped because Y'). When set, drafts that angle on the real story (provenance preserved) instead of returning the proposed five. Requires the story locked and angles ready (cp2_awaiting_angle).
niche_draft_create
Niche draft content: lock the chosen angle and turn it into platform-native social drafts (LinkedIn post, X thread, Instagram, newsletter/long-form), ready to review and publish, the downstream payoff once the story and angle are decided. Returns immediately with status=cp3_generating; poll niche_session_state until outputs[] is populated. Each output carries its trust data nested under `outputs[i].trust`: `source_faithfulness_score`, `source_ungrounded_claims[]`, `source_diversity_passed`, `source_recency_passed`, `overall_severity`, a severity-tagged `flags[]`, and `verifier_blocked_reason`/`verifier_blocked_claim` when a fabrication was refused. (No top-level `verifier_audit` field; read `outputs[i].trust`.)
BRAND: voice/offer/CTA come from the run's bound brand. If this account has MULTIPLE brands and the run isn't bound to one, this returns `brand_choice_required` with `brand_options[]` and generates NOTHING. STOP, ask the user which brand, then re-call with `brand_id` (or `brand_id:'none'` to draft deliberately unbranded). A single/default brand binds automatically.
Parameters (4)
session_idstringrequired
angle_idstringrequired
id from niche_angle_propose.angles[].id
brand_idstring
Bind this draft to a brand (its voice/offer/CTA). Usually unnecessary (the run inherits the scan's brand). Pass it to answer a `brand_choice_required` on a multi-brand account, or `'none'` to draft deliberately unbranded.
estimate_onlyboolean
When true, return the per-platform content credit cost without locking the angle or generating. Quote a price before committing.
niche_draft_revise
Applies the values you pass to a specific output. Accepts any subset of the output's fields: caption, hashtags, or partial script updates (hook / body / cta / hook_tweet / body_tweets / title / subtitle / pull_quote / cover_slide / slides / cta_slide / alt_text / card_headline, where card_headline rewords the image card's header). Pass `apply_hook_variant_index` to splice an existing hook_variants[N] into the live hook in one move without rewriting the rest. If you pass no editable field (or values identical to the current draft) it changes nothing and returns `status:'no_change'` naming the params that edit content. Angle and story changes still go back through niche_angle_propose; they invalidate the verifier trust block and need fresh generation.
Response includes a `diff[]` array listing every field that changed (`{field, before, after}`) so agents can show users the delta rather than the full new payload.
Parameters (7)
output_idstringrequired
regenerate_hooksinteger
Generate this many fresh alternate opening lines for the output and return them as hook_variants for you to present. The body stays put. After the user picks one, splice it with apply_hook_variant_index. Metered like a short generation.
captionstring
Replace the full caption (legacy field; same effect as setting `script.body` for LinkedIn, `script.caption` for Instagram).
Partial updates to the output.script JSON. Shallow-merged: keys present here replace the matching fields, keys absent are preserved. Available fields depend on platform: linkedin {hook, body, cta, structure}; twitter {hook_tweet, body_tweets[], landing_tweet, single_tweet}; longform {title, subtitle, body, pull_quote}; instagram {hook, caption, alt_text}; linkedin_carousel {cover_slide, slides[], cta_slide}.
apply_hook_variant_indexinteger
Splice an existing hook_variants[N] into the live hook. 0-indexed. Cheaper than rewriting the caption by hand. Errors if the index is out of bounds.
slide_patchesarray
linkedin_carousel only. Two modes, one per call (do not mix). In-place edit: {index, headline?, body?} patches a slide's text without resending the whole slides[] array, and re-renders just that slide. Structural op: {op, index, ...} reorders the deck, where op is 'move' ({op:'move', index, to_index}), 'insert' ({op:'insert', index, headline?, body?}; index may equal the slide count to append), or 'delete' ({op:'delete', index}). Structural ops keep the slide text and the rendered images in lockstep, preserving the cover and closing slides; an inserted slide is rendered automatically. Out-of-bounds index errors; passing slide_patches on a non-carousel output errors.
niche_draft_publish
Publish a single output to its platform. Defaults to dry_run=true: returns the would-publish payload plus any verifier blocks without actually filing. Set dry_run=false and provide an idempotency_key to commit. The commit is the only irreversible action in the workflow; the agent should present the dry-run preview to the human and only commit on explicit go-ahead. For any piece with a rendered image, reel, or card, show the human the actual pixels (the preview_url / image_url) first: never let publish be the first time a human sees the final visual. Verifier-blocked outputs refuse to publish even with dry_run=false; clear the block on the row first. When the target social isn't linked, returns status='not_connected' plus a `connect_url` instead of publishing; send the user to connect once, then retry.
Scheduling: pass `scheduled_for` (an ISO-8601 datetime in the future) to file the post for later instead of publishing now; it dispatches automatically through the same publish path. Pass `cancel_scheduled=true` to clear a pending schedule for this platform.
Prefer an exact cell string (e.g. 'x:thread', 'x:single_tweet') over a bare family name. A bare family that matches more than one draft on the session returns status='ambiguous_platform' with the candidate cells rather than guessing. Every publish and dry-run response echoes `resolved_cell` so you can confirm which artifact shipped.
Parameters (8)
session_idstringrequired
platformstringrequired
Accepts either:
• cell string: the platform×content_type the run produced (linkedin:text_post, linkedin:image_post, linkedin:carousel, x:single_tweet, x:thread, x:image_post, instagram:image_post, instagram:carousel), preferred for new code
• family name: linkedin, linkedin_carousel, twitter, instagram, which coerces to the registered default cell for that family (e.g. linkedin maps to linkedin:text_post)
Reels (linkedin:reel / x:reel / instagram:reel) and long_form_article have no in-app publish path: reels get a download URL, long-form ships as Markdown for paste-to-Substack.
dry_runboolean
idempotency_keystring
Required when dry_run=false. Same key returns the prior result without re-publishing.
acknowledge_surfacedboolean
Required true to commit when the dry-run surfaced claims/flags worth a human look (the dry-run's next_step names them). Affirms the agent showed the human the surfaced items and got go-ahead. Ignored when nothing was surfaced. Default false.
acknowledge_publish_costboolean
Required true to commit an X post that carries a link (X charges per link-post, so the dry-run returns a `publish_cost` in credits). Affirms the agent showed the human the cost and got go-ahead. Ignored when the publish is free (plain X / LinkedIn / Instagram). Default false.
scheduled_forstring
ISO-8601 datetime to publish this output later (must be in the future). Files a pending scheduled post and returns status='scheduled' instead of publishing now. Requires the platform's social account connected; long-form has no scheduled path.
cancel_scheduledboolean
Clear a pending scheduled post for this platform on this run. Idempotent: returns a clean message when nothing is scheduled.
niche_brand_profile_set
Set or update the persisted brand profile for a brand. The profile is a structured JSON document applied across every pipeline stage: voice rules, banned terms, canonical vocabulary, framing allowlist, channel config, compliance disclosures, and verifier overrides. Use it to persist a profile derived from a repo or docs so future runs inherit the rules, or to update voice rules and banned terms before the next run.
Required sections: `identity` and `voice` (a profile with no voice falls back to generic drafts). A re-set that omits voice is accepted with a default voice stub rather than rejected.
Validation: tiered lints (error / warn / info). 'error' lints reject the set; 'warn' lints accept with a note. The response includes `lints[]`. `conflicts[]` lists fields locked by the brand kit, which takes precedence; those profile values are not stored.
brand_id is unique per user; re-setting the same brand_id replaces the prior profile.
Parameters (3)
brand_idstringrequired
Unique-per-user identifier for this brand (e.g. 'acme', 'acme-blog'). Stored as a string; lowercase kebab-case recommended.
profileobjectrequired
The brand profile JSON. Schema sections (each drives a pipeline stage): identity (who the brand is), audience (who it's for), voice (register/rhythm/lexicon rules the copy obeys), lexicon (canonical_terms plus banned_terms the verifier enforces), framing (frame slugs from Niche's editorial taxonomy; pass `allowed` and/or `blocked` as lists of slugs. The set is closed: an unrecognized frame rejects the set and the error returns the valid slugs, so nothing is silently dropped), structure (default article shapes), offers (what the creator sells: {product, what_it_is, proof_point?, cta, link}; lands signal-led posts on the offer), verifier_overrides (truthfulness thresholds), channels (per-platform config), source_quality (trusted/blocked domains), compliance (required disclosures), metadata. Fill the load-bearing ones (identity / voice / lexicon / framing / verifier_overrides) for a strong profile; the rest are optional. A malformed set returns a `template` of the full schema with inline field docs in the error response, so no separate discovery call is needed.
schema_versionstring
Profile schema version. Defaults to '1.0'.
niche_list_sessions
Enumerate the user's recent sessions. Returns id, niche_input, status, `outcome`, target_platforms, picked story/angle ids, and created/updated_at for each. Use this when the session_id has been lost (across agent invocations, hours of work, etc.) or to find an in-flight session to resume. Returns newest-first.
Judge a terminal run by `outcome`, not raw `status`: a `failed` status is usually a walk-away, not an error. outcome ∈ {complete, expired (a slate was produced but nobody picked; re-open and choose), interrupted (a restart ended it, credits refunded; just re-run), cancelled (stopped on purpose), failed (a real error; see error_message), running}. (`status_filter` still matches the raw status value.)
Parameters (6)
limitinteger
Max sessions to return (default 25, max 100).
offsetinteger
Skip this many sessions before returning, for paging through history. Default 0.
status_filterstring
Optional status filter, e.g. 'cp1_awaiting_story', 'cp3_awaiting_review', 'complete', 'failed'. Omit for all.
niche_containsstring
Optional case-insensitive substring filter on the niche input, to find sessions on a topic (e.g. 'walnut').
brand_idstring
Optional filter to sessions tied to one brand profile slot.
include_outputsboolean
When true, also returns `recent_outputs`: the account's produced posts/images/reels across all sessions, newest first, each with its session_id, cell, a reachable asset_url, and publish state. Use it to locate a past asset (e.g. an image made on a prior run). Default false.
niche_brand_kit_ingest
Auto-populate the user's BrandKit (palette / fonts / tagline / logo / wordmark / boilerplate / voice notes) from files, a URL, or pasted text. Additive by default: fills empty fields, leaves populated ones alone. Idempotent: re-running the same inputs doesn't double-write.
Overwrite rule: if the target brand kit already has an identity (a tagline/boilerplate/voice for a different brand), do not silently overwrite it. First ask the user whether to replace it. If the account supports multiple brand profiles, prefer creating a separate brand instead: pass a new `brand_id` slug plus `brand_name` rather than clobbering the existing one. Only pass `replace=true` once the user has confirmed they want this brand re-learned from the new source.
Use when the agent has brand assets in scope (a working directory with logos / press-kit / brand-guide PDFs, the user's portfolio or Substack URL, pasted boilerplate copy) and wants to populate Niche's BrandKit so future signal_scan and content generation inherit the brand context. Agent-side equivalent of the Niche web app brand-kit ingest surface, same backend engine.
Async, then poll: a URL or multi-file ingest runs in the background, so this call returns fast with {ingest_id, status:'ingesting'}. Then poll niche_brand_kit_ingest_status(ingest_id) until status is 'done'; that response carries the populated BrandKit, the ingest report (detected[] / skipped[] / errors[]), and a diff[] of changed fields. (Loop: ingest, then poll status until done/failed; same pattern as niche_signal_scan to niche_session_state.) Do not re-call ingest while one is running; a duplicate of the same inputs attaches to the in-flight job. URL ingest also fills voice primitives when the page has post-shaped text (Substack/blog/X). If a URL is slow or thin to scrape, the visual fields may land before the voice pass completes; when the report flags this, paste the page's About/homepage copy via `text=` to complete the brand voice.
Parameters (5)
filesarray
Files to ingest (logos, brand-guide PDFs, screenshots, color swatches, headshots). Each entry: {name, mime_type, data_base64}. The engine classifies each by image content and routes to logo/wordmark/headshot/brand-guide/color-swatch slots.
textstring
Optional URL (homepage, Substack, portfolio, LinkedIn) or a paste of brand text (tagline, boilerplate, voice notes). URL takes precedence when both look URL-shaped.
brand_idstring
Which brand slot to ingest into. Omit for the default brand. Pass a slug (e.g. 'acme') to target or create a separate brand kit; do this when the default kit already belongs to another brand, so you don't merge two brands into one. If the account isn't entitled to additional brands, the call returns a clear error explaining what's needed.
brand_namestring
Optional display name when creating a new brand_id slot (e.g. 'Acme Co').
replaceboolean
Default false (additive: fill empty fields only). Set true only after the user confirms they want an already-populated brand re-learned from this source; it overwrites the detected identity fields. Do not set true to silently clobber a different brand's kit; create a new brand_id instead.
niche_brand_kit_ingest_status
Poll the result of an async niche_brand_kit_ingest. Pass the `ingest_id` it returned. status: 'ingesting' (keep polling) | 'done' (response carries the full kit plus detected[]/skipped[]/errors[]/diff[]/left_unchanged_because_populated[]) | 'failed' (error plus failed_step; the kit was not written). The marker expires 30 min after the ingest starts.
Parameters (1)
ingest_idstringrequired
The ingest_id returned by niche_brand_kit_ingest.
niche_voice_profile_ingest
Extract voice primitives (register / sentence rhythm / lexicon preferences / punctuation habits) from post-shaped text and persist onto the user's VoiceProfile. The voice primitives thread into content generation so generated copy matches the user's actual writing voice.
Two input shapes: pass `posts` (list of pre-collected text snippets, ≥80 chars each) or pass `url` (the server scrapes post-shaped snippets from the page: Substack / Medium / blog / X profile). Inline posts win when both are given. Inline post-shaped snippets need to be the user's own writing, not press articles or marketing copy.
Returns the extracted primitives + a diff of what changed on the stored VoiceProfile.
Parameters (5)
postsarray
Post-shaped text snippets the user wrote. ≥80 chars each; 3-10 snippets is the sweet spot for primitives extraction.
urlstring
URL to scrape post-shaped text from. Substack / Medium / blog homepages work best; X / LinkedIn profile pages are supported but yield less per-snippet text.
archetypestring
Optional label for how the profile was acquired; tags the VoiceProfile.archetype field. Defaults to 'agent_ingest'.
overwriteboolean
Whether to replace an existing VoiceProfile. Default false: first run wins, subsequent runs are no-ops unless explicitly opted in, so an automated re-run does not overwrite a hand-curated voice.
brand_idstring
Which brand's voice to ingest into. Voice is brand-scoped: omit for the default brand, or pass a brand_id (from niche_brand_profile_get) to set up that brand's voice without touching another brand's. A brand with no voice of its own falls back to the default voice at generation time.
niche_brand_profile_get
Read back the persisted brand profile without modifying it. Use it to confirm a profile landed as expected, to check the active profile a run is bound to, or to inspect the current shape before a partial update.
Pass `brand_id` to read one profile; omit it to list all (brand_id, updated_at) summaries. Returns the full profile JSON, schema_version, created/updated timestamps, and a brand_kit_sync_status indicating whether the profile's mirrored fields are in sync with the brand kit.
Parameters (1)
brand_idstring
Brand identifier to read. Omit to list all.
niche_brand_kit_guided_setup
Return a structured question chain the agent walks the user through to populate the BrandKit, VoiceProfile, and (optionally) BrandProfile. Each entry carries an `intent` describing what the answer is for (so the agent paraphrases in its own voice based on the conversation it's already having) plus a `prompt_hint` fallback for agents that relay tools verbatim.
Use this when the agent is helping a new user set up Niche and wants a predictable, brand-aware Q&A sequence instead of improvising. Tiered by impact:
- Tier 0 (primary): URL ingest, fills 70-90% in one ask.
- Tier 1 (gap_fill): only the fields URL ingest didn't fill.
- Tier 2 (discipline): opt-in guardrails (topics off, banned terms, competitor stance).
- Tier 3 (no_url): full fallback for users without a site.
Per-question `applies_to_field` tells the agent where the answer writes (via niche_brand_kit_update or niche_brand_profile_set). `is_already_set` is computed from the user's current BrandKit and BrandProfile state so the agent skips questions already answered.
Returns the full chain in one call; the agent inspects state, decides flow, and asks in any order (or skips entirely if the user volunteered the answer earlier in the conversation).
Parameters (2)
brand_idstring
Optional. If set, also reads the persisted BrandProfile for that brand_id so questions whose answers live in the profile (banned_terms, framing.allowed, etc.) get their is_already_set computed against profile state too.
include_filledboolean
If true, return all questions including those already answered. Default false: the agent only sees the gaps.
niche_brand_kit_update
Set specific BrandKit fields by name. The write path for the structured fields (tagline / boilerplate / voice_notes / forbidden_phrases / signature_phrases / endcard preferences / video voice preference / colors / fonts) without going through the ingest engine. Use after niche_brand_kit_ingest fills the easy stuff, or to commit values the user answered through niche_brand_kit_guided_setup.
Only fields you pass are touched; fields you omit stay at their current value. Lists replace the current value (they do not append). Response includes a diff[] of fields that changed and the full updated kit.
Archiving: pass archive=true to archive a brand (soft and reversible; it disappears from every list but is not deleted), or archive=false to restore one. The default/active brand can't be archived (promote another to default first). A brand with published history won't archive unless you also pass acknowledge=true. Use archive_scope='test' to archive every scratch brand at once (never the default). There is no hard delete here; archive is the removal verb agents have.
Parameters (27)
brand_idstring
Which brand slot to update. Omit for default; pass a slug to target a specific brand kit (for multi-brand accounts).
archiveboolean
true archives the brand (soft, reversible; hidden from every list, not deleted); false restores it. Refuses the default/active brand and refuses a brand with published history unless acknowledge=true.
archive_scopestring
With archive=true, 'test' archives every scratch brand in one call (never the default). Returns the list of brands archived; each is reversible with archive=false.
acknowledgeboolean
Required (true) to archive a brand that has published history, confirming the user means to remove it from the roster even though it shipped work.
brand_namestring
Display name when creating a new brand_id slot.
namestring
Rename an existing brand's display name (its brand_id is unchanged).
taglinestring
Brand tagline (5-12 words).
boilerplatestring
Brand boilerplate (one paragraph, ≤350 chars).
voice_notesstring | array
Voice direction in plain prose. A string or a list of strings (the guided-setup text_list form); a list is joined server-side.
external_urlstring
Canonical brand URL (homepage).
cta_textstring
One-line call to action appended to generated posts, e.g. 'DM to commission'.
forbidden_phrasesarray
Phrases never to use in generated copy. Max 16; trimmed beyond.
signature_phrasesarray
Phrases the brand uses. Max 16.
primary_bgstring
Primary background color (#rrggbb).
secondary_bgstring
text_primarystring
text_secondarystring
accentstring
headline_font_familystring
body_font_familystring
logo_urlstring
wordmark_urlstring
headshot_urlstring
endcard_mark_preferencestring
endcard_outro_textstring
endcard_templatestring
video_voice_preferencestring
niche_render_image_card
Render a visual onto a post at CP3, or edit an existing image.
scope: 'full' (default) renders a new visual and requires `background`. 'recomposite' re-composites new text, color, or size over the retained background at no charge. 'restore' reverts to the prior image from history, at no charge. 'reframe' produces a per-platform aspect variant from the retained background, at no charge.
background (required for scope='full'):
• 'photo': a generated AI/photographic image with the headline composited over it. ~30 credits, ~30-90s, asynchronous: returns status='rendering_image_card'; poll niche_session_state (image_render.status: rendering, then done with static_urls on the output, or failed with credits refunded). A repeat call while a render is in flight is a no-op.
• 'design': the INFOGRAPHIC, a generated editorial graphic that DRAWS the argument (a ranked bar chart, a two-column diagram, a stat, a before/after, a pull-quote), on-brand and legible, with vetted icons. The designer art-directs the treatment from the brand PALETTE (leads light for data, dark for narrative; accent as a spice), and honors a look steer in design_concept ('brighten it up', 'a ranked bar chart', 'navy and gold'). ~30 credits, asynchronous (poll as above).
• 'brand_color': a flat brand card with the headline on the brand's solid color with logo and wordmark. No generation, no credits, synchronous.
• 'svg': you author the card exactly as SVG markup (pass `svg`); the server rasterizes it to the cell's dimensions. Free, instant, deterministic. The right choice for data, labels, charts, and comparisons (where generated images fail at layout), and the only visual that works from a network-locked sandbox (SVG is text). The SVG owns the whole canvas; use brand colors and fonts from niche_whoami. Static shapes, paths, and text only (no scripts, external references, or foreignObject).
`headline` sets the bold header (defaults to the post's card_headline; auto-fits, not truncated). Idempotent: a prior render is replaced. Errors: render_not_ready before CP3; render_not_configured when image generation is unavailable; render_card_unavailable when background='brand_color' but the piece has no card-bearing platform.
Parameters (16)
session_idstringrequired
Session UUID that's reached cp3_awaiting_review or complete.
scopestring
'full' (default): render a new visual (background required). 'recomposite': free text edit on the existing image, with new headline/subhead/color/size composited over the retained background, pixels otherwise identical, synchronous, 0 credits. Use this for wording iteration; it avoids paying a re-render to change words. 'restore': bring back the prior image from history, free. 'reframe': free per-platform aspect variant that re-composites an existing card's retained background at the `cell`'s canvas size (x 16:9, instagram 4:5, linkedin 1.91:1), same text and grounding, synchronous, 0 credits. Requires `cell` (the target aspect).
backgroundstring
Required for scope='full' (ignored otherwise): what's behind the text. 'photo' = a generated AI/photographic image (the actual picture; ~30 credits, ~30-90s, async). 'design' = a generated editorial design graphic that draws the argument (concept diagram / stat / pull-quote / comparison / method / abstract), on-brand and legible, no photo, no clichés, ~30 credits, async. 'brand_color' = a free, instant flat brand card (no generation). 'svg' = a free, instant card you author exactly as SVG markup (pass `svg`), rasterized at the cell's size; best for data, labels, and charts, and the only visual that works from a network-locked sandbox. No default: choose deliberately.
svgstring
Required when background='svg': the card's SVG markup as a string (<svg ...>...</svg>). You author the exact layout; the server rasterizes it to the cell's pixel dimensions. Use brand colors and fonts from niche_whoami to stay on-brand. Bundled fonts (set font-family to any by name; an unknown family or the generics 'sans-serif'/'serif'/'monospace' fall back to a real font so text always draws): sans (Inter, Geist, Open Sans, Montserrat, Lato, Poppins, DejaVu Sans), serif (DejaVu Serif, Lora, Playfair Display), mono (DejaVu Sans Mono, JetBrains Mono). Allowed: static shapes, paths, text, gradients, internal (#id) and inline data: references. Rejected with a named error: scripts, event handlers, foreignObject, external/file references, and DOCTYPE/ENTITY declarations. Max 512KB.
design_conceptstring
Optional, background='design' only: free-text art direction for the design graphic, the layout/shape and concept (e.g. 'a 2-column comparison', 'an abstract composition, no literal imagery', 'a concept diagram of intended vs actual'). Omit to let the designer pick the shape that best carries the argument.
design_colorstring
Optional, background='design': color control for the design card. Free text. Sets the card BACKGROUND when the phrase names a background or the card overall ('cream background', 'navy', 'on a green card'), or the ACCENT when it names one ('blue accent', '#0a3d62'); the rest stays on the brand's palette (or the default style when the brand has no kit). Omit to use the brand's palette.
subheadstring
The smaller line under the header. On scope='full' it sets the subhead in the single render; on scope='recomposite' it edits it for no charge. Omit to keep the current one; pass '' to clear it.
text_colorstring
Text color as a name ('blue') or hex ('#ec4899'). Applies on scope='full' (set the color in the render) and scope='recomposite' (re-color for no charge). On background='brand_color' it colors the card text; omit to auto-pick a legible color from the background.
background_colorstring
The background colour of a SOLID brand card (background='brand_color') as a name ('cream'/'navy'), a hex, or a brand keyword ('primary'/'accent'/'secondary'). Applies on a full brand_color render AND on scope='recomposite' (free, persists, so the colour does not snap back to the brand default on a later edit). (A generated-photo card recolours its text, not its photo background.)
font_sizestring
Headline size. A relative word ('bigger'/'smaller'/'reset') steps from the current size and compounds; an absolute value (a number like 80, '80px', or '120%') sets it directly. Applies on scope='full' and scope='recomposite'. The response's font_changed/font_at_limit report whether it actually moved.
text_positionstring
Where the overlay sits: 'top', 'center', or 'bottom'. On scope='full' it places the text in the render; on scope='recomposite' it moves the text on the existing image for no charge. Omit to keep the position the card was rendered at.
needs_legible_textboolean
scope='full', background='photo' only. Set true when in-image text is genuinely the subject of the scene, which routes the render to a text-capable image generator. Defaults false (an atmospheric, text-free background, the usual choice).
headlinestring
The bold header words; works for both backgrounds. Defaults to the post's `card_headline` (the short, sized-for-the-box line). Pass this to force exact text, e.g. a brand name leading it ('Acme drew a line'). It auto-fits the box and is never truncated.
art_directionstring
Optional free-text direction for a generated photo background (applies only when background='photo'). State the visual concept and any negatives, such as subjects or styles to avoid. Without it the background is generated from the story alone and tends toward category clichés (e.g. a robot for 'AI'); use this to steer toward a specific concept or an abstract, non-literal composition. The no-in-image-text rule still applies.
cellstring
Optional. Render at the canvas size for this cell:
• linkedin:image_post: 1200×627 (1.91:1 landscape)
• x:image_post: 1200×675 (16:9 landscape)
• instagram:image_post: 1080×1350 (4:5 portrait)
• linkedin:carousel / instagram:carousel: 1080×1080 (cover slide)
When omitted: 1080×1080 universal-square asset stored under platform='image_card' (shared across all image cells).
estimate_onlyboolean
If true, return {credit_cost} without rendering or spending. Use to learn the cost before committing.
niche_render_reel
Render a 9:16 vertical reel for a session at CP3. A per-beat script (typically 4-7 beats) is composited into one video: stills, voiceover, motion, caption overlays, and an endcard (~30-120s). Reels are delivered as a downloadable file for the user to publish.
scope: 'full' (default) renders a new reel. 'recomposite' re-renders presentation only (captions on/off, caption_sync_mode, endcard text) from the retained footage at no charge. 'endcard' is the same, limited to the endcard. 'beat:N' re-renders a single beat's still (optional beat_direction), charged for that one image; everything else is reused.
Idempotent on the video output: a prior render is replaced.
Options: captions_enabled (default true) overlays per-beat captions; music_enabled (default false) mixes background music under the voiceover; tone shapes the script (default | punchy | direct | reflective), with reel_direction for free-form direction beyond those words; length or target_duration_sec set the runtime target; music_direction shapes the music; voice picks the voiceover voice; endcard_mark chooses the endcard brand mark; needs_legible_text forces a text-capable still generator; caption_sync_mode (default 'phrase') reveals captions phrase-by-phrase in sync with the voiceover at no extra cost, and is ignored when captions_enabled is false.
Errors: 503 if reel rendering is unavailable; 400 before CP3; 422 if the script is unusable. Cost: ~350 credits.
Parameters (20)
session_idstringrequired
Session UUID that's reached cp3_awaiting_review or complete.
scopestring
'full' (default): render a new reel (script plus stills plus voiceover, ~350 credits). 'recomposite': re-render presentation from the retained footage (captions on/off, caption_sync_mode, endcard_text/endcard_outro), at no charge, async. 'endcard': the same, limited to the endcard (endcard_text/endcard_outro/endcard_mark; use 'recomposite' to change captions). 'beat:N': re-render shot N's still (1-based, optional beat_direction and motion), ~15 credits; everything else is reused. Script changes (tone, voiceover words, music) require scope='full'. Partial scopes require a reel whose footage was retained, otherwise the response indicates scope='full' is needed.
beat_directionstring
scope='beat:N' only: optional plain-language steer for the re-rolled shot ('warmer light, no people'). Omit for a fresh take on the same brief.
captions_enabledboolean
Overlay caption text on each beat. Default true. With scope='recomposite': omit to keep the reel's current setting.
endcard_textstring
scope='recomposite'/'endcard' only: the big endcard words (replaces the brand stamp text). Omit to keep the current endcard.
endcard_outrostring
scope='recomposite'/'endcard' only: the smaller endcard line under the mark. Omit to keep the current one; pass '' to clear it.
music_enabledboolean
Mix background music behind the voiceover. Default false.
tonestring
Script tone hint (scope='full'). Default 'default'. For direction beyond these four, use reel_direction.
reel_directionstring
scope='full': free-form creative direction applied alongside tone, for steers the four tone words do not cover (pacing, narrative shape, mood), e.g. 'courtroom-drama narration, build to a reveal'. The free-form sibling of tone.
lengthstring
scope='full': total runtime target the script is budgeted to hit. 'short' ~10-15s, 'standard' ~15-22s, 'long' ~25-40s. For an exact number of seconds, use target_duration_sec instead.
target_duration_secinteger
scope='full': exact total runtime target in seconds (8 to 45). Wins over `length`. The script budgets each beat to land near this total; the delivered duration can vary by a second or two.
music_directionstring
scope='full': free-form style, genre, and tempo for the background music, e.g. 'calm lo-fi, no vocals'. Supplying it turns music on for this render. Music is generated audio, so a free re-render or shot re-roll cannot change it.
voicestring
scope='full': voiceover voice for this render, overriding the brand kit's saved preference just this once. Use 'auto', 'male', or 'female', or a short hint like 'a warm female narrator'.
endcard_markstring
Which brand mark stamps the endcard for this render, overriding the brand kit's saved preference just this once (e.g. end on the logo, not the name). Honored on scope='full', 'recomposite', and 'endcard'. Falls back to the brand name text when the chosen asset is not set on the kit.
endcard_colorstring
The endcard background color, a name or hex ('navy', '#0a3d62'), overriding the brand color on the closing frame just this once. The endcard text re-resolves to stay legible on it. Honored on scope='full', 'recomposite', and 'endcard'; ignored on 'beat:N'.
motionstring
scope='beat:N': the Ken Burns motion for the re-rolled shot ('zoom-in', 'zoom-out', 'static', or a plain steer like 'slow push-out'). Overrides that shot's existing motion. Omit to keep the current motion.
needs_legible_textboolean
scope='full': force the text-capable still generator for every shot, for a reel whose stills carry readable words. Default false lets each shot pick the best generator on its own.
caption_sync_modestring
How captions track the voiceover. 'beat' (default): each beat's caption shows for that beat's spoken window. 'phrase': the caption reveals phrase-by-phrase in lockstep with the spoken words (caption text is taken from the spoken VO). Ignored when captions_enabled is false. No extra cost.
cellstring
Optional. Cell to tag the rendered Output row (linkedin:reel / x:reel / instagram:reel). All reels are 9:16 1080×1920 universal, so cell only affects bookkeeping, not pixels. When omitted: stored under platform='video' (shared across all reel cells).
estimate_onlyboolean
If true, return {credit_cost} without rendering or spending. Use to learn the cost before committing.
niche_session_revert
Revert a session back to an earlier checkpoint. Use when the user (or you) decided the picked story / angle isn't the right one and you want to re-pick without starting a new scan.
to='story': cancels current generation, returns to CP1_AWAITING_STORY with the same ranked stories list. Clears selected_story_id and selected_angle_id.
to='angle': returns to CP2_AWAITING_ANGLE with the same angles list. Clears selected_angle_id only.
Idempotent: reverting a session already at the target checkpoint returns the current state unchanged.
Safety: reverting a finished run (cp3/complete) discards its paid-for drafts. The first call returns status='confirm_discard' with the count instead of reverting; pass acknowledge_discard=true to actually proceed. Credits: the discarded drafts are not refunded (the generation already ran), and re-picking re-runs and re-charges content generation. Revert to change direction, not to reclaim spend.
Parameters (3)
session_idstringrequired
tostringrequired
acknowledge_discardboolean
Required to revert a cp3/complete session; confirms you accept discarding its finished, paid-for drafts.
niche_session_cancel
Cancel a running session. Marks status=failed with an error_message ('cancelled by user' or the caller's reason), and stops the in-flight orchestrator if any.
Used when you want to free a slot under the concurrent-session cap without deleting the session (history and audit trail preserved). Use the REST DELETE endpoint if you want a hard archive cleanup that also wipes outputs.
Idempotent: cancelling a completed / already-failed session returns its current state unchanged.
Credits: the unused hold is released; work already committed (e.g. the discovery for a story you picked) stays charged, so cancelling does not refund committed work. A pre-CP1 cancel (nothing committed yet) is effectively free. A full refund applies only to an actual run failure.
Parameters (2)
session_idstringrequired
reasonstring
Optional. Surfaced as session.error_message.
niche_session_export
Export a session as a structured calendar artifact preserving session_id and per-story story_id traceability. Use after a niche_signal_scan when you want a metadata-rich content backlog instead of running individual pieces end-to-end. Outputs a standard editorial-calendar shape suitable for content-backlog and planning workflows.
Two formats:
• markdown: human-readable and agent-citable. Session metadata at top (session_id, niche, scan timestamp, and brand_profile_active state). Then a card per story with title, headline_candidate, summary, recency_score, publication_breakdown, source_breakdown, and empty slots for the user to fill (Frame, Hook, Article-shape, Ship Order). Followed by a 'recommended ship order' section and cross-cutting notes.
• json: structured shape ready to pipe to other tools or load into a notebook. Same data, machine-shaped.
Preserves story_id and session_id traceability so you can come back in N weeks and re-run niche_angle_propose / niche_draft_create against the same stories with the same brand profile bound. The artifact is the entry point to a calendar-builder workflow.
Parameters (2)
session_idstringrequired
formatstring
Output shape. Default markdown.
niche_add_output
Add an output cell to a session that's already reached CP3. Use this when the user picked a small initial cell set, previewed the drafts, and now wants another surface (e.g. the session started with linkedin:text_post and the user wants to add instagram:carousel too).
Text-only cells (linkedin:text_post, x:thread, long_form_article, etc.): generates text via the matching generator if it hasn't run yet, then creates the Output row. This call blocks synchronously ~20-30s when it must run a new generator family (no status to poll); it returns fast when that family already generated. Idempotent: if the cell is already on the session, returns the existing Output unchanged.
Asset cells (linkedin:image_post, x:reel, instagram:image_post, etc.): creates the text Output row; image-bearing cells auto-attach a free branded card (static_urls populated without an explicit render, swappable anytime), while video/reel assets are never auto-rendered. The response's next_step says exactly what to call for more: niche_render_image_card (cell plus an explicit background: 'photo' or 'brand_color') or niche_render_reel (cell). The response's copy_lineage says whether this cell drafted fresh text or shares its generator family's existing copy.
Remove: pass `remove_cell` to delete a produced cell the user no longer wants on this run. Idempotent: a clean message when the cell isn't present.
Errors: 400 if session is pre-CP3; 422 if cell is invalid.
Parameters (4)
session_idstringrequired
cellstring
Cell to add. Must be one of the valid cells (see niche_signal_scan's target_outputs for the list).
remove_cellstring
Cell to remove from this run (deletes the produced output). Idempotent: a no-op message when the cell isn't present. Pass this instead of `cell` to remove rather than add.
estimate_onlyboolean
When true, return the credit cost of adding this cell without creating a row or generating. Returns 0 when the cell's platform family already generated (text is reused).
niche_reuse_asset
Copy an image that already exists on one output onto another cell, instant and free (no regeneration, no credits). Use this when the user wants 'the same image' on a second surface ('use the LinkedIn image on X', 'same picture on the newsletter') instead of niche_render_image_card (which generates a new image and costs credits).
Both cells must already exist on the session (add the target via niche_add_output first if needed) and the source must have a rendered image. Copies the source's static_urls onto the target so it publishes with that image. Idempotent: source==target is a no-op.
Parameters (3)
session_idstringrequired
from_cellstringrequired
Cell that has the image (e.g. 'linkedin:image_post').
to_cellstringrequired
Cell to copy the image onto (e.g. 'x:image_post').
niche_attach_image
Upload or attach a user-supplied or externally-designed image (bring-your-own asset) to a post: the creator's own visual (a product shot, their actual work, a card designed elsewhere) instead of an AI-generated image (niche_render_image_card photo, paid) or a flat brand card. Free, with no image-generation spend. For a visual-product maker the real piece is the sale.
Input modes, in order of preference: (1) `upload_ref`, the FAST path for an agent that built the asset itself and can run a shell: POST the raw file to `/asset/upload` (multipart/form-data, your bearer token) to get back an `upload_ref`, then pass it here. The bytes travel over HTTP and never round-trip through the model as base64, so it's effectively instant for a real graphic. (2) `image_url`, a fetchable https URL (the server fetches + stores it; for an asset that already lives on the web). (3) `image: {mime_type, data_base64}`, inline base64, fine for small images only. (4) `image_chunk`, the no-shell FALLBACK: upload bounded chunks of base64. It still re-types the bytes through the model (slow), so use it only when the agent has no shell to curl with. Split the file's bytes into ~32-48KB pieces, base64 EACH independently, send in order, each with a `sha256` of that piece's raw bytes so the server catches a mis-transcribed chunk and has you resend just that one (this is what makes the slow path reliable). Omit upload_id on the first chunk; the response returns one to pass on the rest. Set `final:true` on the last chunk (optionally with `total_sha256`); that call assembles, validates, and attaches.
The cell's output must already exist (use niche_add_output first if needed). Sets it as the post's image; publishes with the caption. A dimension_note warns if the image's aspect won't fit the cell. Undo-able (the prior image is kept in history).
Parameters (6)
session_idstringrequired
cellstringrequired
Post cell to attach onto (e.g. 'linkedin:image_post', 'x:image_post', 'instagram:image_post').
upload_refstring
FAST bring-your-own path. First POST the raw file to /asset/upload (multipart/form-data field 'file', Authorization: Bearer <token>); the response returns an upload_ref. Pass it here. The bytes go over HTTP, not through the model, so it's instant for a real graphic. Preferred whenever the agent can run a shell.
image_urlstring
A fetchable image URL (https). For an asset that already lives on the web: the server fetches and stores it, so you don't inline a large base64 payload. One of upload_ref / image_url / image / image_chunk is required.
imageobject
Inline image as base64: {mime_type, data_base64} (plus optional name). Max 8MB, but large inline payloads are unreliable over MCP, so prefer upload_ref or image_url. One of upload_ref / image_url / image / image_chunk is required.
image_chunkobject
Chunked upload for bring-your-own bytes you hold locally (no URL). Split the file's BYTES into ~32-48KB pieces, base64-encode EACH piece, send in order. Always include a per-chunk `sha256` (hex of that piece's raw bytes): the server verifies it and rejects a mis-transcribed chunk so you resend just that one, which is what makes this path reliable (valid base64 can still decode to wrong bytes and silently corrupt the image). Omit upload_id on the first chunk (the response returns one); set final:true on the last, optionally with `total_sha256` of the whole file.
niche_whoami
List the full tool catalog and orient the agent, in one read-only call. Returns every registered tool name and the live tool count, the account (plan and credit balance), a capability map (tools grouped into bands: discover, decide, draft, render, publish, brand, session, plus the recommended flow), and the brand state (whether a brand profile, kit, or voice will personalize output). Call it first to discover what's available, plan a session, and check credits before spending.
Parameters (3)
include_testboolean
Include scratch and experimental brand slots in available_brands. Default false so only real brands are listed and an agent does not bind a throwaway by accident.
include_all_brandsboolean
List every brand on the account in available_brands. Default false: only the active brand is returned (with other_brand_count) so a single orienting call does not enumerate the whole roster. Set true when you genuinely need to pick among multiple brands.
include_brand_assetsboolean
Include usable brand asset URLs (logo, wordmark) in brand_palette. Default false: the palette returns colours and fonts plus logo_available / wordmark_available booleans, and the renderer fetches the actual asset when a render needs it. Set true only if you need the asset URL directly.
Find stories worth writing about. Niche is an editorial intelligence
platform for individuals (and the agents working for them), exposed as a live
MCP server. When
an agent needs to know what's worth writing (emerging narratives, the signals
that matter, the strongest angle), it calls Niche.
Most AI tools help you write. Niche helps you decide what's worth writing. It
reads primary-source signal, discovers the stories before they're obvious, ranks
the strongest angle, and turns that signal into publishable content, grounded in
real sources and in your voice. The authority is earned, not faked: you sound
like someone who did the research, because Niche did.
Niche is not an AI writer, a social scheduler, a content calendar, or an SEO
tool. Those help after the story exists. Niche finds the story.
The category: editorial intelligence, for individuals
Editorial intelligence has always been a newsroom discipline: the systematic
work of deciding what's worth covering before everyone else does. Niche brings it
to the people the newsrooms left out: individual creators, analysts, and the
agents working for them.
Editorial intelligence, for individuals, not newsrooms. Systematically identifying, evaluating, and prioritizing the stories worth publishing.
Signal-to-story. The workflow that turns raw signal into a narrative people care about: signal → story → angle → draft. Story discovery, not content generation.
Signal discovery. Separating meaningful developments from background noise, and finding what matters early, from primary sources.
When you (or your agent) think "what should I write about in X," "what's emerging
in this space," or "how do I become credible in this field," that's Niche.
Connect
Niche is a remote MCP server with OAuth 2.1 (and PAT bearer for headless agents).
Endpoint:https://api.nicheangle.com/mcp
Claude Desktop / Claude Code
Add it as a custom connector by URL, or via config:
Sign in when prompted (OAuth). For headless/scripted use, issue a Personal Access
Token (PAT) at nicheangle.com/settings/api-keys
(niche_sk_…) and pass it as a bearer token.
Codex (CLI / IDE)
Codex connects to remote MCP servers over streamable HTTP. The PAT path is the
most reliable (no OAuth round-trip). It takes two steps, and bearer_token_env_var
wants the name of an env var, not the token itself:
toml
# 1) put the SECRET in your shell, not in the config file:# export NICHE_API_KEY="niche_sk_…"# 2) ~/.codex/config.toml holds the NAME of that env var, never the token:[mcp_servers.niche]url = "https://api.nicheangle.com/mcp"bearer_token_env_var = "NICHE_API_KEY"# the env var NAME, not the niche_sk_… value
Set export NICHE_API_KEY="niche_sk_…" in your shell, restart Codex (clients
cache credentials and the tool list at connect, so re-add or restart after changing
either), then run codex mcp list to confirm. (OAuth alternative: drop
bearer_token_env_var and run codex mcp login niche.) This requires a recent
Codex, since older versions only do local stdio servers.
Or add the bare URL via the client's MCP settings to use OAuth. Discovery metadata
lives at /.well-known so compliant clients can self-configure.
After adding or updating credentials, restart the client or open a new thread.
MCP clients cache the connection (auth and the tools/list) at connect time, so a
token change or a new tool won't show up until you reconnect.
Four background modes: photo (a generated image, steerable via art direction to skip the clichés), design (a generated editorial graphic that draws the argument, such as a concept diagram, stat, pull-quote, or comparison; on-brand and legible, no photo), svg (you author the exact card as SVG markup and it renders to size; free and deterministic, ideal for data, labels, and charts, and the one visual that works from any environment), or brand_color (a free flat brand card). Plus free per-platform aspect reframes from the retained background.
niche_attach_image brings your own visual onto a post (a card designed elsewhere, a product shot), stored server-side, free. Provide it as a hosted image_url, a quick file upload that returns a reference, or chunked bytes, whichever your environment supports.
niche_whoami: the full tool catalog (every tool name and the live count) plus account, plan, credit balance, the capability map (tools grouped by band and the recommended flow), and brand state. One read-only call to discover everything available and orient before a run. (If your client only surfaces a few Niche tools, it cached an old list. Reconnect, or call this to enumerate the rest.)
Every tool response carries a trust block: verifier audit, source-faithfulness
score, ungrounded-claim list, and source-diversity and recency checks. Provenance is
the product.
Recent additions (1.4.0). The tools gained finer control without new surface
area. Read your resolved brand palette and connection state before a render or a
publish. Schedule a post for later. Regenerate a fresh angle set. Reorder, insert, or
delete carousel slides. Set a reel's length, motion, music direction, voice, and
endcard mark. Place and size image-card text. Bring your own image and attach it to a
post. Quote the credit cost of any step before spending. Every capability is reachable
from both the agent tools and the Niche web app.
Grounding: earned, not generated
The narratives Niche returns are produced engine-side and fact-checked against
their sources, so the agent presents a verified result instead of free-styling
one. "No source, no narrative." That's the difference between earned authority and
confident nonsense.