Documentation
Voice Engine
The Voice Engine turns a workspace’s voices, a purpose-built engine, and a brief into a draft the author can edit, then scores that draft against the same material. Profiles hold the voice. Engines decide which voices apply. Facts, phrase rules, feeds, and X lists supply the evidence and the constraints.
How the Voice Engine works
Every tenant shares the same structure. The words inside it belong to the workspace. There is no built-in list of companies, people, competitors, or industry terms. A profile with no published version is absent from drafting and checking.
Command CenterThe Voice section at /voice is the editor: explorer, Inbox, profiles, engines, facts, and the prompt composer. Humans publish versions here.
WriteDraft from a brief and run a voice check beside the composer. A failed check warns. Saving still works. Scheduling past a failure is an explicit acknowledgement.
HTTP APIEvery capability on this page is a route under /api/v1/voice. The product UI and an agent key call the same handlers. Nothing here schedules or publishes a post.
WorkerIngestion, transcription, stance briefs, intake, archive parsing, feed polls, X list sync, and X compliance run as jobs. The API enqueues them and reads the result.
Concepts
Profiles
A profile is a brand, a personal voice, a persona, or a lens. A workspace has one brand profile. Each publishing human has one personal profile. Personas are many: a person, an institution, or a group. Influence is align (“stay on message”) or emulate (“sound like them”). Align contributes beliefs, claim policy, pillars, audience, and attributed examples. Emulate also contributes style, playbooks, and lexicon terms the persona prefers. Neither permits verbatim reuse.
Sections are a fixed taxonomy: charter, style, beliefs, audience, playbooks, pillars, lexicon, claim policy, negative examples, aligned examples, and network. Prose sections store a body. Lexicon, examples, and network store rows. A lens may hold only the topic sections: beliefs, audience, pillars, lexicon, playbooks, claim policy, both example tables, and network. Charter and style stay on the personal voice.
Edits land on a working copy. Publishing snapshots that copy, the identity card, and the persona influence into an immutable version. Drafting and checking read the latest published version. Restoring a version copies it back into the working draft and leaves history intact.
Engines
An engine is a named recipe for one purpose: which author (self or a personal profile), which lens, which personas at which influence, whether the brand applies, engine instructions, routing keywords and tags, platforms, prompt blocks, example pins, and fact settings. General is built in. Its id is general, it has no config of its own, and it uses the workspace’s published personal voice, personas, and brand.
Phrasing precedence is voice memory, then personal, then emulated personas, then aligned personas, then brand. When the engine has a lens, the lens outranks the core personal voice. Claim policy and lexicon use the stricter rule. Members can lock a persona to a specific published version. Otherwise the latest publish is used.
Prompt
Block mode sends an ordered plan: identity, style, beliefs, examples, facts, phrase rules, platform playbook, and the engine’s instructions. Three blocks stay on and cannot be budgeted: rules.core, guardrails.phrases, and platform. Raw mode replaces that plan with a template of up to 20,000 characters. Slots look like {{charter}} and are filled from the published profiles at draft time. A detached slot is literal text the author pasted over a slot; the editor warns when that profile is republished. The floor stays outside the template: fenced third-party text is data, the output contract stays fixed, and the phrase-rule pass still runs.
Facts, rules, and memory
A fact is a claim with tags, a citation, and an optional expiry, attached to a brand or persona profile. A person who creates one approves it immediately. An agent key can only propose. Drafts see approved facts that have not expired. An engine chooses tags, pins up to 24 facts, and caps how many are offered (default 5, at most 12).
A phrase rule bans a phrase and optionally supplies up to three replacements. Scope is the author’s drafts, one engine, or the whole workspace. People create active rules. Agents propose, and a person accepts. Matching ignores case, treats hyphens and whitespace runs as equal, and respects word boundaries. The post-generation pass rewrites a hit when a replacement exists, and the voice check flags whatever remains.
Voice memory is a short list of preferred and avoided spans learned when a caption is saved in Write. The assembler reads it. This API does not write it. GET /api/v1/voice returns the same object the context embeds.
Objects
ProfileThe durable voice: sections, identity, and published versions.
- Types: brand, personal, persona, lens
- Persona kinds: person, institution, group
- Working copy until a human publishes
- Markdown export that round-trips with replace
EngineA versioned recipe that picks voices and a prompt for one purpose.
- Up to 24 per owner, 12 persona members
- Block mode or a raw slot template
- Playground trials compare draft and published
- General stays available with id general
FactAn approved claim a draft may use as evidence.
- Status: proposed, approved, retired
- Expiry removes it from retrieval until renewed
- 500 characters, 8 tags, citation required in spirit
- 2,000 facts per workspace
Phrase ruleA live ban that applies on the next draft without a publish.
- Scopes: author, engine, workspace
- Up to 3 replacements, 120-character phrase
- Word-bounded, case-insensitive matching
- 500 rules per workspace
Source and feedMaterial a person reviews before it becomes voice.
- Link, upload, or paste, then a stance brief
- RSS, podcast, or YouTube channel
- 12 feeds per profile, poll at least hourly
- New items arrive in the Inbox
X listA workspace’s own X app, used as an engine input.
- Bearer token, optional OAuth app, optional user token
- Estimate shown before the first attach
- Circle, radar digest, or a group persona
- Daily read cap, 7-day post cache, compliance purge
Choose a path
Browse by capability
Voice memorySpans learned from caption edits. The assembler reads them; this route does not write them.
Context and promptThe published voices, the prompt plan, and a preview that does not call a model.
Drafts and checksTurn a brief into an editable draft, then score that draft against the same engine.
ProfilesBrand, personal, persona, and lens voices: working sections, identity, versions, and Markdown.
EnginesNamed recipes that choose an author, a lens, personas, brand, and a prompt.
Phrase rulesLive “never use” and “use instead” rules that apply on the next draft without a publish.
Brand factsApproved claims with a source and an optional expiry, offered to engines as evidence.
Sources and briefsLinks, uploads, and pastes, then the stance brief a person reviews before anything merges.
FeedsRSS, podcast, and YouTube subscriptions. New items become sources in the Inbox.
X listsBring-your-own X credentials, a read estimate, then circle, radar, and circle voice.
Library and collectionExplorer, search, collections, the interview, and archive imports.
Authentication
Send a Clerk session cookie from the signed-in app, or an agent key:
Authorization: Bearer stories_…
Agent keys match stories_ plus 32 URL-safe characters. A key carries scopes. read covers context, plans, lists, and checks that are marked Read. write covers creates and edits. A human session has every scope. Routes marked Human reject every agent key with privileged_human_required, including publish, engine create and delete, phrase-rule edits, fact approval, archive import, and X credentials.
Browser calls from the app must be same-origin. Responses send Cache-Control: private, no-store. The API rate limit is 120 requests per actor per minute. Unauthenticated calls are limited to 60 per IP per minute.
Errors
Failures return JSON. The message is safe to show. Provider response bodies are not copied through.
| Status | Code | When |
|---|
| 400 | invalid_input | A required field is missing or a value fails validation. The message says which rule. |
| 401 | unauthorized | No session, or the agent key is invalid, revoked, or belongs to a deleted workspace. |
| 403 | forbidden | The key is missing the read or write scope the route requires. |
| 403 | privileged_human_required | The route is human-only and the caller is an agent key. |
| 404 | not_found | The id is missing, or a lens is hidden from this human. |
| 409 | conflict | The brief was already reviewed, or this engine already follows that X list. |
| 400 | not_configured | An X call needs a bearer token that has not been saved. |
| 400 | invalid_credentials | X rejected the stored credentials. The token is not echoed. |
| 402 | out_of_credit | The X app is out of credit. Sync pauses until it can read again. |
| 429 | rate_limited | The actor or X is over a cap. Retry-After is set, in seconds. |
| 500 | llm_failed | The model call failed. The public message does not include the provider error. No draft is created. |
| 500 | internal_error | Anything else. The message is generic. |
Shape: { "code": "invalid_input", "message": "A brief needs a subject." }
Limits
| Limit | Value |
|---|
| API requests | 120 per actor per minute |
| Drafts | 30 per actor per hour |
| Voice checks | 120 per actor per hour |
| Playground trials | 20 per actor per hour, one or two variants each |
| Briefs, intake, source writes, X identity reads | 30 per actor per hour |
| Engines per owner | 24 |
| Persona members per engine | 12 |
| Facts per workspace | 2,000 |
| Phrase rules per workspace | 500 |
| Feeds per profile | 12, polled between every hour and every 7 days |
| X daily reads | Default 2,000, configurable from 10 to 100,000 |
| X list members | Default cap 500, maximum 5,000 |
| Radar posts kept | 100 reads per sync, stored 7 days |
| Markdown import | 64 files, 2 MB |
Background jobs
These run on the worker. The HTTP routes above enqueue them and return the job id. Poll the resource, or watch the Inbox, for the result.
| Kind | What it does |
|---|
voice_ingest | Fetch a link or file, classify it, and store text. Moves the source through pending, fetching, transcribing, ready, needs_text, or failed. |
voice_transcribe | Speech-to-text for audio and video, under per-source and per-workspace caps. |
voice_brief | Propose section items, examples, lexicon rows, and facts from a transcript. A person merges or rejects. |
voice_intake | Draft working sections from a site, sample posts, and leader sources. |
voice_archive_import | Parse an X archive or LinkedIn export into a reviewable report. |
voice_feed_poll | Read a due RSS, podcast, or YouTube feed and create a source per new item, at most 10. |
voice_x_list_sync | Refresh an attached list: circle members, radar posts, and the optional group persona. |
voice_x_compliance | Delete stored posts that X no longer serves. Runs on the compliance schedule, within 24 hours of a deletion on X. |
The reference below is every route under /api/v1/voice.
Voice memory
Spans learned from caption edits. The assembler reads them; this route does not write them.
GETRead voice memory
/api/v1/voice
The preferred and avoided spans Write learned from caption edits, plus a short context note. The same object is embedded in the assembled context.
Read A signed-in session, or any agent key.
200. VoiceMemoryDTO: preferred and avoided string lists (at most 18 each), context, and updatedAt.
Context and prompt
The published voices, the prompt plan, and a preview that does not call a model.
GETAssemble voice context
/api/v1/voice/context
Published profiles for one engine, voice memory, and the precedence order the drafter and the checker both use. Omit engineId, or pass general, for the built-in General engine.
Read A signed-in session, or any agent key.
Query parameters
engineIdstringOptional
An engine id, or general. Absent means General.
200. VoiceContextDTO: precedence, personal, personas, brand, optional lens, engine, memory, hasProfiles, generatedAt.
- A profile with no published version is absent. Missing sections are omitted rather than filled with defaults.
- Phrasing precedence is voice memory, then personal, then emulated personas, then aligned personas, then brand. On a lens engine the lens outranks the core personal voice.
GETRead the prompt plan
/api/v1/voice/plan
The deterministic system prompt for one engine and platform: every block, its provenance, character budget, and the rendered text. No model call.
Read A signed-in session, or any agent key.
Query parameters
engineIdstringOptional
Engine id. Absent or general uses General.
platform"x" | "linkedin"Optional
Platform playbook to include. Anything other than linkedin is treated as x.
themestringOptional
Stand-in for a brief subject, up to 500 characters, so example retrieval can be previewed.
draft"1"Optional
When 1, preview the engine’s unpublished working settings.
200. plan (PromptPlan), system (rendered string), hasProfiles, candidates (ranked aligned examples), defaultTemplate (raw mode’s starting template).
- Locked blocks rules.core, guardrails.phrases, and platform stay on in block mode and cannot be budgeted.
- Raw mode still keeps a fixed floor: fenced third-party text, the output contract, and the post-generation phrase pass.
POSTPreview unsaved prompt settings
/api/v1/voice/plan
Validate a config the way a save would, then return the prompt plan for it. Nothing is stored. Read access is enough.
Read A signed-in session, or any agent key.
Body
engineIdstringRequired
A saved engine. general is rejected.
configVoiceEngineConfigRequired
Unsaved settings. Unknown keys are dropped.
platform"x" | "linkedin"Optional
Defaults to x.
themestringOptional
Up to 500 characters.
200. The same object as GET /plan.
Drafts and checks
Turn a brief into an editable draft, then score that draft against the same engine.
POSTDraft from a brief
/api/v1/voice/drafts
Create an editable draft post from a content brief. The copy lands in Drafts with generation metadata. This route never schedules or publishes.
Write A signed-in session, or an agent key with the write scope.
Body
subject{ kind, text, url? }Required
kind is link, transcript, or idea. text is required, up to 2,000 characters.
claimsBriefClaim[]Required
At least one evidence claim. Each has text, source, and an optional factId. At most 20.
anglestringOptional
Optional point of view, up to 1,000 characters.
platforms("x" | "linkedin")[]Required
At least one platform.
constraints{ maxPosts?, thread? }Optional
Thread shape and length hints passed to the model.
engineIdstringOptional
Engine to draft with. Absent or general uses General.
mediaAssetIduuidOptional
Media to attach, such as brand-approved media offered with a fact.
201. post (the draft), notes (editorial notes), generation (model, prompt hash, profile versions, example ids, engine, phrase-rule hits, fact ids).
- Limited to 30 drafts per actor per hour. A 429 includes Retry-After.
- Active phrase rules are applied after generation. Hits are recorded on generation.phraseRuleHits.
- A failed model call returns llm_failed and does not create a post.
POSTRun a voice check
/api/v1/voice/check
Score the current copy of a draft on six rubric dimensions and run deterministic triggers (banned phrases, avoid terms, unverified handles, register). A failed check warns. It does not block save. Scheduling past it is a separate human acknowledgement in Write.
Write A signed-in session, or an agent key with the write scope.
Body
postIduuidRequired
The draft to score.
engineIdstringOptional
Engine to check against. Absent uses the engine stored on the draft, then General.
201. VoiceCheckDTO: id, postId, engine, optional engineNote, rawPrompt, passed, scores per platform, triggers, model, profileVersionIds, createdAt.
- A check fails when any trigger fires or any dimension scores below 3.
- Dimensions are register, argument, grounding, platformFit, restraint, and novelty, each an integer from 1 to 5.
- Limited to 120 checks per actor per hour.
Profiles
Brand, personal, persona, and lens voices: working sections, identity, versions, and Markdown.
GETList profiles
/api/v1/voice/profiles
Every voice profile this caller can see. Lenses are visible to their owner and to agent keys. Other types are workspace-wide.
Read A signed-in session, or any agent key.
200. { workspaceId, profiles: VoiceProfileSummary[] }.
POSTCreate a profile
/api/v1/voice/profiles
Create a brand, persona, or personal voice. A workspace has one brand profile and one personal profile per publishing human. Lenses are created from an engine, not here.
Write A signed-in session, or an agent key with the write scope.
Body
type"brand" | "persona" | "personal"Required
Profile type.
displayNamestringRequired
Name shown in the explorer.
relationshipNotestringOptional
Why this persona is in the workspace. Institutions and groups may omit it.
influence"align" | "emulate"Optional
Personas only. align stays on message. emulate also picks up cadence. Defaults to align.
personaKind"person" | "institution" | "group"Optional
Personas only. Defaults to person.
201. VoiceProfileDTO, including empty working sections.
GETGet the personal or brand profile
/api/v1/voice/profiles/by-type/{type}
The workspace’s single personal or brand profile, for the type-based Voice routes. Personas are many; list them with GET /profiles.
Read A signed-in session, or any agent key.
Path parameters
type"personal" | "brand"Required
Any other value is invalid_input.
200. VoiceProfileDTO. 404 when that type has not been created yet.
GETGet a profile
/api/v1/voice/profiles/{id}
Working sections, identity card, influence, and the current published version.
Read A signed-in session, or any agent key.
Path parameters
iduuidRequired
Profile id.
200. VoiceProfileDTO.
PATCHUpdate profile metadata
/api/v1/voice/profiles/{id}
Rename a profile or change a persona’s relationship note, influence, or kind. Section text is saved with the section route.
Write A signed-in session, or an agent key with the write scope.
Path parameters
iduuidRequired
Profile id.
Body
displayNamestringOptional
New name.
relationshipNotestringOptional
Persona relationship note.
influence"align" | "emulate"Optional
Persona influence. Takes effect on the next publish.
personaKind"person" | "institution" | "group"Optional
What the persona represents.
200. VoiceProfileDTO.
DELETEDelete a profile
/api/v1/voice/profiles/{id}
Remove a profile from the workspace. Published history goes with it.
Write A signed-in session, or an agent key with the write scope.
Path parameters
iduuidRequired
Profile id.
200. { ok: true }.
PUTSave a section
/api/v1/voice/profiles/{id}/sections/{section}
Replace one working section. Drafting keeps using the published version until a person publishes.
Write A signed-in session, or an agent key with the write scope.
Path parameters
iduuidRequired
Profile id.
sectionVoiceSectionRequired
charter, style, beliefs, audience, playbooks, pillars, lexicon, claim_policy, negative_examples, aligned_examples, or network. A lens may only hold beliefs, audience, pillars, lexicon, playbooks, claim_policy, negative_examples, aligned_examples, and network.
Body
bodystringOptional
Prose for a non-tabular section.
rowsVoiceRow[]Optional
Table rows. Lexicon uses prefer, avoid, why. Examples use text, source, tag, attribution, platform. Network uses handle, platform, name, relationship, note.
unmappedUnmappedChunk[]Optional
Import leftovers kept beside the section.
200. The saved VoiceSectionDTO.
POSTPublish a profile version
/api/v1/voice/profiles/{id}/publish
Snapshot the working sections, identity, and persona influence into an immutable version. Agents can edit the working copy. Only a person can publish.
Human A signed-in human session. An agent key receives 403 privileged_human_required. Also requires the write scope when the caller is an agent key, which this route rejects before that check.
Path parameters
iduuidRequired
Profile id.
Body
changelogstringOptional
What changed, stored on the version.
201. VoiceVersionDTO: id, number, publishedAt, changelog, profileId, publishedBy, sections, influence, identity.
GETList profile versions
/api/v1/voice/profiles/{id}/versions
Immutable publishes, newest last in the array the service returns.
Read A signed-in session, or any agent key.
Path parameters
iduuidRequired
Profile id.
200. { versions: VoiceVersionRef[] } with id, number, publishedAt, changelog.
GETGet a profile version
/api/v1/voice/profiles/{id}/versions/{versionId}
One published snapshot, including sections and the identity card as they were.
Read A signed-in session, or any agent key.
Path parameters
iduuidRequired
Profile id.
versionIduuidRequired
Version id.
200. VoiceVersionDTO.
POSTRestore a profile version
/api/v1/voice/profiles/{id}/versions/{versionId}/restore
Copy a published snapshot into the working draft. History is unchanged until someone publishes again.
Write A signed-in session, or an agent key with the write scope.
Path parameters
iduuidRequired
Profile id.
versionIduuidRequired
Version to copy.
200. VoiceProfileDTO with the restored working sections.
GETExport a profile as Markdown
/api/v1/voice/profiles/{id}/export
One Markdown file per filled section. Importing those files back with replace: true restores the working draft.
Read A signed-in session, or any agent key.
Path parameters
iduuidRequired
Profile id.
200. { files: [{ name, content }] }.
POSTImport a Markdown bundle
/api/v1/voice/profiles/{id}/import
Map headings and tables onto working sections. The import never publishes. With replace, the bundle overwrites those sections so an export round-trips.
Write A signed-in session, or an agent key with the write scope.
Path parameters
iduuidRequired
Profile id.
Body
files{ name, content }[]Required
At most 64 files and 2 MB.
replacebooleanOptional
When true, imported sections replace the working copy instead of merging.
200. Import result: mapped sections and any unmapped chunks.
PUTSave the identity card
/api/v1/voice/profiles/{id}/identity
Replace the working identity card: role, organization, industry, location, bio, website, expertise, interests, handles, and a one-paragraph network summary. Drafting reads it after publish.
Write A signed-in session, or an agent key with the write scope.
Path parameters
iduuidRequired
Profile id.
Body
identityVoiceIdentityRequired
Text fields are at most 120 characters (bio 500, website 200, network 300). Lists hold at most 20 items. At most 10 handles.
200. { identity, profile }.
POSTPropose identity from X
/api/v1/voice/profiles/{id}/identity/from-x
Read the workspace’s connected X account and return a proposed identity card. Nothing is saved until PUT /identity.
Human A signed-in human session. An agent key receives 403 privileged_human_required.
Path parameters
iduuidRequired
Profile id. A lens is rejected; it takes identity from the personal voice.
200. { handle, proposal: VoiceIdentity, changes: field names that differ from the working card }.
- Requires a connected X account in Settings. Limited to 30 calls per actor per hour.
- A lens returns invalid_input.
GETSuggest posted examples
/api/v1/voice/profiles/{id}/posted
Posted copy ranked by official metrics that is not yet an aligned example on this profile.
Read A signed-in session, or any agent key.
Path parameters
iduuidRequired
Profile id.
Query parameters
limitnumberOptional
How many suggestions. Defaults to 10.
200. { suggestions: PostedSuggestion[] } with id, postId, platform, text, publishedAt, and likes.
POSTAdd posted examples
/api/v1/voice/profiles/{id}/posted
Copy the picked posts onto the working aligned-examples section.
Write A signed-in session, or an agent key with the write scope.
Path parameters
iduuidRequired
Profile id.
Body
idsstring[]Required
Suggestion ids from GET /posted.
200. The updated profile section state from the collector.
Engines
Named recipes that choose an author, a lens, personas, brand, and a prompt.
GETList engines
/api/v1/voice/engines
General first, then the caller’s engines. Agent keys see every engine in the workspace.
Read A signed-in session, or any agent key.
200. { engines: VoiceEngineSummary[] }. General has id general and builtin true.
POSTCreate an engine
/api/v1/voice/engines
An engine belongs to the person who creates it. Names cannot be General. At most 24 engines per owner.
Human A signed-in human session. An agent key receives 403 privileged_human_required.
Body
namestringRequired
Up to 60 characters. A URL-safe slug is derived from it.
descriptionstringOptional
Up to 300 characters.
configVoiceEngineConfigOptional
Author, members, brand, instructions, routing, platforms, blocks, examples, facts, and prompt mode. Defaults are used for anything omitted.
withLensbooleanOptional
When true, also create the topic lens.
201. VoiceEngineDTO: summary plus config and publishedConfig.
- VoiceEngineConfig.author is self or a personal profile id. members hold at most 12 personas, each align, emulate, or off. instructions are at most 2,000 characters. routing keywords and tags are at most 20 items of 40 characters.
GETGet an engine
/api/v1/voice/engines/{id}
Working config and the last published config.
Read A signed-in session, or any agent key.
Path parameters
idstringRequired
Engine id. general is the built-in engine.
200. VoiceEngineDTO.
PATCHSave engine settings
/api/v1/voice/engines/{id}
Update the working draft. An agent key may edit settings. Only a person can rename or publish.
Write A signed-in session, or an agent key with the write scope.
Body
namestringOptional
Human callers only.
descriptionstringOptional
Up to 300 characters.
configVoiceEngineConfigOptional
Full settings object. Block budgets are 200 to 12,000 characters. Raw templates are at most 20,000 characters and may contain {{slot}} placeholders.
200. VoiceEngineDTO.
DELETEDelete an engine
/api/v1/voice/engines/{id}
Remove an engine the caller owns. General cannot be deleted.
Human A signed-in human session. An agent key receives 403 privileged_human_required.
200. { ok: true }.
POSTAdd a lens
/api/v1/voice/engines/{id}/lens
Create the topic lens for this engine. The call is idempotent when the lens already exists. The circle for an X list is stored on this lens.
Human A signed-in human session. An agent key receives 403 privileged_human_required.
201. VoiceEngineDTO including the lens summary.
POSTPublish an engine
/api/v1/voice/engines/{id}/publish
Snapshot the working config. Drafts use the latest published version unless a member is locked to an older profile version.
Human A signed-in human session. An agent key receives 403 privileged_human_required.
Body
changelogstringOptional
What changed.
201. The published version.
GETList engine versions
/api/v1/voice/engines/{id}/versions
Published configs for this engine, including the changelog and the settings snapshot for each version.
Read A signed-in session, or any agent key.
200. { versions } with id, number, publishedAt, changelog, and config.
POSTRestore an engine version
/api/v1/voice/engines/{id}/versions/{versionId}/restore
Copy a version’s settings into the working draft. It does not rewrite history.
Write A signed-in session, or an agent key with the write scope.
Path parameters
versionIduuidRequired
Version id.
200. VoiceEngineDTO.
GETList playground trials
/api/v1/voice/engines/{id}/trials
The engine’s Playground log, newest first. Trials are samples. They are not posts.
Read A signed-in session, or any agent key.
200. { trials: VoiceTrialDTO[] }.
POSTRun a playground trial
/api/v1/voice/engines/{id}/trials
Draft a sample brief with one or two variants of the engine and voice-check each. Variants are draft, published, or a version id. No post is created.
Write A signed-in session, or an agent key with the write scope.
Body
briefContentBriefRequired
Same shape as POST /drafts, without engineId and mediaAssetId.
variantsstring[]Optional
One or two of draft, published, or a version uuid. Defaults to [draft].
201. The trial run: each variant’s copy, editorial notes, and voice-check result.
- Limited to 20 trials per actor per hour. Each variant spends a model draft and a check.
Phrase rules
Live “never use” and “use instead” rules that apply on the next draft without a publish.
GETList phrase rules
/api/v1/voice/phrase-rules
Rules this caller can see, plus suggestions built from phrases they keep deleting.
Read A signed-in session, or any agent key.
200. { rules: PhraseRuleDTO[], suggestions: PhraseRuleSuggestion[] }.
POSTCreate a phrase rule
/api/v1/voice/phrase-rules
A person creates an active rule. An agent key can only propose one, and a person accepts it before it applies. Matching is case-insensitive and word-bounded.
Write A signed-in session, or an agent key with the write scope.
Body
phrasestringRequired
Up to 120 characters.
alternativesstring[]Optional
Up to 3 replacements, each at most 80 characters.
reasonstringOptional
Up to 200 characters.
avoidSimilarbooleanOptional
Also flag close variants.
scope"author" | "engine" | "workspace"Optional
All my drafts, this engine only, or everyone in the workspace.
engineIduuidOptional
Required when scope is engine.
createdFrom"manual" | "post" | "check" | "trial" | "memory"Optional
Where the rule was born. Defaults to manual.
201. PhraseRuleDTO.
- At most 500 rules per workspace.
PATCHUpdate a phrase rule
/api/v1/voice/phrase-rules/{id}
Edit the phrase, replacements, scope, or status. Status may become active or disabled. proposed is only set by an agent create.
Human A signed-in human session. An agent key receives 403 privileged_human_required.
Body
phrasestringOptional
Banned phrase.
alternativesstring[]Optional
Replacements.
reasonstringOptional
Why it is banned.
avoidSimilarbooleanOptional
Match close variants.
scope"author" | "engine" | "workspace"Optional
Who the rule applies to.
engineIduuidOptional
Engine when scope is engine.
status"active" | "disabled"Optional
Turn the rule on or off.
200. PhraseRuleDTO.
DELETEDelete a phrase rule
/api/v1/voice/phrase-rules/{id}
Remove the rule. Past drafts are unchanged.
Human A signed-in human session. An agent key receives 403 privileged_human_required.
200. { ok: true }.
POSTAccept a proposed rule
/api/v1/voice/phrase-rules/{id}/accept
Activate a rule an agent proposed. It starts applying on the next draft.
Human A signed-in human session. An agent key receives 403 privileged_human_required.
200. PhraseRuleDTO with status active.
POSTMove a rule into a lexicon
/api/v1/voice/phrase-rules/{id}/move-to-lexicon
Copy the rule onto a profile’s working lexicon and disable the live rule. Publish the profile for drafting to pick up the lexicon row.
Human A signed-in human session. An agent key receives 403 privileged_human_required.
Body
profileIduuidRequired
Profile whose lexicon receives the row.
200. The disabled rule and the lexicon write.
POSTDismiss a phrase suggestion
/api/v1/voice/phrase-rules/suggestions/dismiss
Stop suggesting a rule for a phrase the author keeps removing.
Write A signed-in session, or an agent key with the write scope.
Body
phrasestringRequired
The suggested phrase, truncated at 200 characters.
200. { ok: true }.
Brand facts
Approved claims with a source and an optional expiry, offered to engines as evidence.
GETList facts
/api/v1/voice/facts
Facts on brand and persona profiles, plus counts for the explorer. At most 500 rows.
Read A signed-in session, or any agent key.
Query parameters
profileIduuidOptional
Limit to one profile.
status"proposed" | "approved" | "retired" | "expired"Optional
expired means approved and past expiresAt.
tagstringOptional
One tag.
200. { facts: VoiceFactDTO[], counts: { approved, proposed, expired } }.
POSTAdd a fact
/api/v1/voice/facts
A person’s fact is approved on save. An agent key can only propose one. Drafts use a fact after it is approved and before it expires.
Write A signed-in session, or an agent key with the write scope.
Body
profileIduuidRequired
A brand or persona profile.
textstringRequired
The claim, up to 500 characters.
tagsstring[] | stringOptional
Up to 8 tags, 40 characters each. A string is split on commas.
citationstringOptional
Where the claim came from, up to 300 characters.
expiresAtstring | nullOptional
A date (2027-01-31) or an ISO timestamp. Empty means no expiry.
sourceIduuid | nullOptional
Voice source this fact was taken from.
201. VoiceFactDTO.
- At most 2,000 facts per workspace.
GETGet a fact
/api/v1/voice/facts/{id}
One fact, including whether an approved fact is past its expiry.
Read A signed-in session, or any agent key.
200. VoiceFactDTO.
PATCHEdit a fact
/api/v1/voice/facts/{id}
Change text, tags, citation, or expiry. Approved facts reach drafts as written, so this is human-only.
Human A signed-in human session. An agent key receives 403 privileged_human_required.
Body
textstringOptional
Replacement claim.
tagsstring[] | stringOptional
Replacement tags.
citationstringOptional
Replacement citation.
expiresAtstring | nullOptional
New expiry, or null to clear it.
200. VoiceFactDTO.
DELETEDelete a fact
/api/v1/voice/facts/{id}
Remove the fact from the library. Drafts stop seeing it immediately, and the row is not kept for the record.
Human A signed-in human session. An agent key receives 403 privileged_human_required.
200. { ok: true }.
POSTApprove a fact
/api/v1/voice/facts/{id}/approve
Approve a proposal, reinstate a retired fact, or renew an expired one.
Human A signed-in human session. An agent key receives 403 privileged_human_required.
Body
expiresAtstring | nullOptional
Optional new expiry applied as part of the approval.
200. VoiceFactDTO with status approved.
POSTRetire a fact
/api/v1/voice/facts/{id}/retire
Keep the fact in the library and stop every draft from using it.
Human A signed-in human session. An agent key receives 403 privileged_human_required.
200. VoiceFactDTO with status retired.
GETSuggest facts for a brief
/api/v1/voice/facts/suggest
Approved, current facts that match the engine and the brief text, plus brand-approved media tagged like them. Nothing is added to a brief until the author picks it.
Read A signed-in session, or any agent key.
Query parameters
engineIdstringOptional
Engine whose fact settings (tags, pins, max) apply. Absent uses General.
qstringOptional
Subject and angle text, up to 1,000 characters.
200. { facts: RetrievedFact[], media: ApprovedMediaDTO[] }.
- An engine offers at most max facts (default 5, hard cap 12) after pins. Pins are at most 24.
Sources and briefs
Links, uploads, and pastes, then the stance brief a person reviews before anything merges.
GETList sources
/api/v1/voice/sources
Sources in the library, optionally filtered by profile, tag, or collection.
Read A signed-in session, or any agent key.
Query parameters
profileIduuidOptional
One profile.
tagstringOptional
One tag.
collectionIduuidOptional
One collection.
200. { sources: VoiceSourceDTO[] }.
POSTAdd a source
/api/v1/voice/sources
Register a link, an uploaded file (media created with purpose voice_source), or pasted text. Links and files enqueue voice_ingest. Pasted text can go straight to a brief.
Write A signed-in session, or an agent key with the write scope.
Body
profileIduuidRequired
Profile this source informs.
titlestringOptional
Library title.
originUrlstringOptional
http(s) link. url is accepted as an alias.
mediaAssetIduuid | nullOptional
Uploaded file.
transcriptstringOptional
Pasted text. When present, ingestion can skip the fetch.
speakerstringOptional
Who is speaking.
sourceDatestringOptional
When the source was published.
tagsstring[]Optional
Up to 12 tags, 32 characters each.
collectionIduuid | nullOptional
Library collection.
metadataobjectOptional
Optional title, author, siteName, canonicalUrl, publishedAt, language, durationSeconds, pages, platform.
201. VoiceSourceDTO plus job when ingestion was queued.
- Status moves pending → fetching → transcribing → ready, or needs_text, or failed. After a brief it becomes briefed, then merged.
- Kinds are link, post, article, video, podcast, document, audio, and paste.
- Limited to 30 source writes per actor per hour.
GETGet a source
/api/v1/voice/sources/{id}
Transcript, ingest status, metadata, and tags.
Read A signed-in session, or any agent key.
200. VoiceSourceDTO.
PATCHUpdate a source
/api/v1/voice/sources/{id}
Edit title, link, speaker, date, transcript, tags, collection, or metadata. profileId cannot be changed here.
Write A signed-in session, or an agent key with the write scope.
Body
titlestringOptional
Library title.
originUrlstringOptional
Replacement link.
transcriptstringOptional
Replacement text.
speakerstringOptional
Speaker.
sourceDatestringOptional
Source date.
tagsstring[]Optional
Replacement tags.
collectionIduuid | nullOptional
Move between collections.
200. VoiceSourceDTO.
DELETEDelete a source
/api/v1/voice/sources/{id}
Remove the source. Items already merged into a profile are marked unsourced rather than deleted from the section.
Write A signed-in session, or an agent key with the write scope.
200. { ok: true }.
POSTRetry ingestion
/api/v1/voice/sources/{id}/retry
Reset a failed or stuck source and enqueue voice_ingest again.
Write A signed-in session, or an agent key with the write scope.
202. The reset source plus the job.
POSTRequest a stance brief
/api/v1/voice/sources/{id}/analyze
Queue analysis for a source that already has text. If a pending brief exists, that brief is returned instead of a second job.
Write A signed-in session, or an agent key with the write scope.
202. { job } or { brief, deduped: true }.
- 400 invalid_input when the source has no transcript yet.
GETList stance briefs
/api/v1/voice/briefs
Proposals waiting for review, optionally for one source.
Read A signed-in session, or any agent key.
Query parameters
sourceIduuidOptional
Limit to one source.
200. { briefs: StanceBriefDTO[] }. reviewStatus is pending, merged, or rejected.
POSTQueue a stance brief
/api/v1/voice/briefs
Enqueue analysis of a source. The worker transcribes attached media first when speech-to-text is available, then proposes section items with confidence and citations. Nothing auto-publishes.
Write A signed-in session, or an agent key with the write scope.
Body
sourceIduuidRequired
Source to analyze.
202. { job, hint } or { brief, deduped: true } when a pending brief already exists.
- Limited to 30 briefs per actor per hour.
GETGet a stance brief
/api/v1/voice/briefs/{id}
The proposal: section items, example rows, lexicon rows, and fact candidates.
Read A signed-in session, or any agent key.
200. StanceBriefDTO.
POSTMerge a stance brief
/api/v1/voice/briefs/{id}/merge
Write the accepted items into the working draft, and accepted facts into the library. A person’s facts are approved. An agent’s facts stay proposed. Publishing the profile is still a separate human step.
Write A signed-in session, or an agent key with the write scope.
Body
items{ section, text }[]Optional
Section paragraphs to merge.
examples{ text, attribution?, platform?, polarity? }[]Optional
polarity is aligned or negative.
lexicon{ prefer?, avoid?, why? }[]Optional
Lexicon rows.
facts{ text?, tags?, citation? }[]Optional
Fact candidates to keep.
200. The brief with reviewStatus merged.
- 409 conflict when the brief is already merged or rejected.
POSTReject a stance brief
/api/v1/voice/briefs/{id}/reject
Discard the proposal. The source stays in the library.
Write A signed-in session, or an agent key with the write scope.
200. The brief with reviewStatus rejected.
POSTRun brand intake
/api/v1/voice/intake
Enqueue a worker run that drafts working sections from a site, sample posts, and leader sources. The result is a working draft, never a publish.
Write A signed-in session, or an agent key with the write scope.
Body
profileIduuidRequired
Profile to fill.
siteUrlstringOptional
Public site to read.
postUrlsstring[]Optional
Sample post links.
leaderSourceIdsuuid[]Optional
Existing sources to treat as leaders.
202. { job: { id, status } }. Retries with the same material share an idempotency key.
- Limited to 30 intake runs per actor per hour.
Feeds
RSS, podcast, and YouTube subscriptions. New items become sources in the Inbox.
GETList feeds
/api/v1/voice/feeds
RSS, podcast, and YouTube subscriptions. X lists are attached from an engine and are not created here.
Read A signed-in session, or any agent key.
Query parameters
profileIduuidOptional
Limit to one profile.
200. { feeds: VoiceFeedDTO[] }.
POSTSubscribe to a feed
/api/v1/voice/feeds
Poll an RSS feed, a podcast, or a YouTube channel. New items become sources. A person reviews the brief before anything merges.
Write A signed-in session, or an agent key with the write scope.
Body
profileIduuidRequired
Profile that owns the subscription.
kind"rss" | "podcast" | "youtube"Required
x_list is rejected. Attach lists from the engine.
urlstringRequired
http(s) URL. A missing scheme is treated as https. YouTube must be a channel URL.
labelstringOptional
Up to 120 characters.
intervalMinutesintegerOptional
From 60 minutes to 7 days. Default is 360.
201. VoiceFeedDTO.
- At most 12 feeds per profile. Each poll creates at most 10 sources.
GETGet a feed
/api/v1/voice/feeds/{id}
One subscription, including the last error.
Read A signed-in session, or any agent key.
200. VoiceFeedDTO.
PATCHUpdate a feed
/api/v1/voice/feeds/{id}
Pause a feed, change how often it is checked, or rename it.
Write A signed-in session, or an agent key with the write scope.
Body
enabledbooleanOptional
Whether the worker polls it.
intervalMinutesintegerOptional
60 to 10,080.
labelstringOptional
Display label.
200. VoiceFeedDTO.
DELETEUnsubscribe
/api/v1/voice/feeds/{id}
Remove the subscription. Sources the feed already created stay in the library.
Write A signed-in session, or an agent key with the write scope.
200. { ok: true }.
POSTPoll a feed now
/api/v1/voice/feeds/{id}/poll
Check the feed immediately. New items become sources and show in the Inbox as new from the feed.
Write A signed-in session, or an agent key with the write scope.
200. { feedId, created, skipped, error }.
X lists
Bring-your-own X credentials, a read estimate, then circle, radar, and circle voice.
GETRead X credential status
/api/v1/voice/x-credentials
Whether a bearer token, OAuth client, and user list-reader token are stored, plus today’s read count. The secrets themselves are never returned.
Read A signed-in session, or any agent key.
200. XCredentialStatus.
PUTSave X credentials
/api/v1/voice/x-credentials
Store the workspace’s own X app credentials. Tokens are encrypted at rest. Omit a secret field to leave the stored value in place. Send an empty string to clear one.
Human A signed-in human session. An agent key receives 403 privileged_human_required.
Body
bearerstringOptional
App bearer token, up to 2,000 characters. Required on the first save.
clientIdstringOptional
OAuth client id for private lists, up to 200 characters.
clientSecretstringOptional
OAuth client secret, up to 2,000 characters.
readerTokenstringOptional
User token that can list the account’s own lists.
dailyReadCapintegerOptional
From 10 to 100,000. Default 2,000.
200. XCredentialStatus after the save.
DELETERemove X credentials
/api/v1/voice/x-credentials
Delete the keys and pause every X list sync. The next sync reports that credentials were removed.
Human A signed-in human session. An agent key receives 403 privileged_human_required.
200. XCredentialStatus with the secrets cleared.
POSTTest the X connection
/api/v1/voice/x-credentials/test
One lookup with the stored bearer token. A success stamps verifiedAt. A failure stores lastError and returns the X error code.
Human A signed-in human session. An agent key receives 403 privileged_human_required.
200. XCredentialStatus.
- 400 not_configured when no bearer token is stored.
- 401 or 403 from X becomes invalid_credentials. 429 becomes rate_limited. 402 becomes out_of_credit. The X response body is never copied into the error.
GETList owned X lists
/api/v1/voice/x/lists
Lists the connected X account owns, when a user reader token is stored. Public lists are attached by URL without this call.
Human A signed-in human session. An agent key receives 403 privileged_human_required.
200. { lists: [{ id, name, private }], note }. note explains a missing token or an X error. The HTTP status stays 200 in those cases.
POSTEstimate X list reads
/api/v1/voice/x-lists/estimate
A cap-based estimate. This does not call X. Show it before the first attach and require the caller to confirm it.
Read A signed-in session, or any agent key.
Body
urlstringOptional
https://x.com/i/lists/123 or a bare numeric id. listId is accepted too.
circlebooleanOptional
Sync members into the lens network. Default true.
radarbooleanOptional
Cache recent posts for a digest. Default false. Costs 100 post reads.
circleVoicebooleanOptional
Also build a group persona from the members. Default false.
maxMembersintegerOptional
Member cap from 1 to 5,000. Default 500. Circle and circle voice each count this many reads.
200. { lookupReads, memberReads, postReads, total }.
- Radar posts are kept for 7 days. A compliance job deletes stored posts that disappear from X.
GETList attached X lists
/api/v1/voice/x-lists
Lists attached to one engine, including the members currently stored.
Read A signed-in session, or any agent key.
Query parameters
engineIduuidRequired
Engine whose lists to return.
200. { lists: XListFeedDTO[] }.
POSTAttach an X list
/api/v1/voice/x-lists
Follow a public or private list after the caller confirms the read estimate. The engine needs a lens. The workspace needs a bearer token.
Human A signed-in human session. An agent key receives 403 privileged_human_required.
Body
engineIduuidRequired
Engine that owns the list.
urlstringRequired
List URL or numeric id.
circlebooleanOptional
Write members into the lens network. Default true.
radarbooleanOptional
Keep a short-lived post cache for digests.
circleVoicebooleanOptional
Create a group persona from the circle.
maxMembersintegerOptional
1 to 5,000. Default 500.
acknowledgetrueRequired
Must be true. Confirms the caller saw the estimate.
201. XListFeedDTO. The first sync runs on the worker interval (24 hours) or via POST /sync.
- 409 conflict when this engine already follows that list.
- At least one of circle, radar, or circleVoice must be on.
POSTSync an X list now
/api/v1/voice/x-lists/{id}/sync
Run one sync: members into the lens network, a radar digest when enabled, and the circle voice when enabled. Reads count against the daily cap.
Human A signed-in human session. An agent key receives 403 privileged_human_required.
Path parameters
iduuidRequired
Attached list id, not the X list id.
200. { feedId, members, posts, digest, reads, error }. error is empty on success.
- When the daily cap is reached, sync pauses until the next day.
- Revoked keys return an error that credentials were removed and sync is paused.
Library and collection
Explorer, search, collections, the interview, and archive imports.
GETLoad the Command Center
/api/v1/voice/explorer
Profiles, engines, source counts, collections, tags, phrase-rule counts, fact counts, and the Inbox.
Read A signed-in session, or any agent key.
200. VoiceExplorerDTO.
- Inbox kinds: source_processing, source_needs_you, brief_review, unmapped_chunks, phrase_rule_proposed, import_ready, import_failed, fact_proposed, fact_expired, feed_item.
GETSearch the library
/api/v1/voice/search
Voices by name, sources by title, link, speaker, or tag, and collections by name.
Read A signed-in session, or any agent key.
Query parameters
qstringRequired
Search text.
200. { profiles, sources, collections }.
GETList collections
/api/v1/voice/collections
Named groups of sources. At most 100 per workspace.
Read A signed-in session, or any agent key.
200. { collections: [{ id, name, sourceCount, createdAt, updatedAt }] }.
POSTCreate a collection
/api/v1/voice/collections
Name a source collection. Names are at most 60 characters and unique in the workspace.
Write A signed-in session, or an agent key with the write scope.
Body
namestringRequired
Collection name.
201. The collection.
PATCHRename a collection
/api/v1/voice/collections/{id}
Change the collection name. Sources stay put.
Write A signed-in session, or an agent key with the write scope.
Path parameters
iduuidRequired
Collection id.
Body
namestringRequired
New name.
200. The collection.
DELETEDelete a collection
/api/v1/voice/collections/{id}
Remove the collection. Its sources stay in the library, ungrouped.
Write A signed-in session, or an agent key with the write scope.
Path parameters
iduuidRequired
Collection id.
200. { ok: true }.
POSTApply the voice interview
/api/v1/voice/interview
Write guided-interview answers onto a voice’s working copy. The lens part can create an engine, which only a person may do.
Write A signed-in session, or an agent key with the write scope.
Body
profileIduuidRequired
Personal voice or lens to fill.
answersRecord<string, string>Optional
Question id to answer. Each answer is at most 2,000 characters.
skipstring[]Optional
Section ids, or identity, the person unchecked in the preview.
lens{ engineId?, createEngine? }Optional
Where the lens answers go: an engine you own, or a new engine named after the topic.
200. { profile, sections, identityFields, engine? }.
GETList archive imports
/api/v1/voice/imports
X archive and LinkedIn export imports this caller started.
Read A signed-in session, or any agent key.
200. { imports } with kind unknown, x_archive, or linkedin_export, and status queued, running, ready, applied, or failed.
POSTStart an archive import
/api/v1/voice/imports
Import an uploaded X archive or LinkedIn export. Create the media asset with purpose voice_archive first. A person reviews the report before anything reaches a profile.
Human A signed-in human session. An agent key receives 403 privileged_human_required.
Body
profileIduuidRequired
Personal voice or lens the import is for.
mediaAssetIduuidRequired
The uploaded archive.
201. The import row. The worker parses it as voice_archive_import.
- The report can hold up to 40 post samples, 20 liked posts, and 2,000 network rows.
GETGet an archive import
/api/v1/voice/imports/{id}
The report: identity candidates, sample posts, liked posts, and network rows, once status is ready.
Read A signed-in session, or any agent key.
200. The import and its report.
DELETEDiscard an import
/api/v1/voice/imports/{id}
Delete the import and its report. Profiles keep whatever was already applied.
Human A signed-in human session. An agent key receives 403 privileged_human_required.
200. { ok: true }.
POSTApply an archive import
/api/v1/voice/imports/{id}/apply
Write the chosen identity fields, samples, likes, and network rows onto the working copy. With asSource, the chosen posts also become a pasted source and a stance brief is queued.
Human A signed-in human session. An agent key receives 403 privileged_human_required.
Body
identityFieldsstring[]Optional
Identity card fields to copy. Unknown names are rejected.
sampleIdsstring[]Optional
Post sample ids from the report, shaped like x:abc.
likedIdsstring[]Optional
Liked-post ids.
networkIdsstring[]Optional
Network row ids.
networkSummarybooleanOptional
Fold the circle summary into the identity card.
targetProfileIduuid | nullOptional
Apply to this profile instead of the import’s profile.
asSourcebooleanOptional
Also create a pasted source and queue a brief.
200. What was applied, including sourceId when a source was created.