The Podcast API: four entities, four endpoint groups, seven webhook events. Everything an episode says carries a claim marker, and every marker names who made it.
A live session is a record in waiting. The API treats them as one pipeline: a session ends, converts into an episode, and its markers carry over unchanged.
This is a design draft. The API is not live yet.
Base path /v1/podcast, bearer-token auth, cursor pagination, JSON bodies, ISO 8601 UTC timestamps. This holds if the core API uses the same conventions. Check it against the UI & API documentation before implementation and correct this page where they differ.
podcast:read lists and reads published content. podcast:write lets a listener raise a hand, tag a claim or set a reminder. podcast:host creates, edits and publishes episodes and sessions. podcast:moderate runs the queue and the floor during a live session.
tag is one of fact, heuristic, assumption, caution. Anything else returns invalid_tag.
| Field | Type | Rule |
|---|---|---|
| id | string | Prefix ep_. Immutable. |
| title | string | Up to 140 characters. |
| format | enum | long_take_audio, rebuttal, replay. |
| source_long_take_id | string, null | Required for long_take_audio. For rebuttal it points at the piece being answered. |
| session_id | string, null | Set only when format is replay. |
| audio_url | string | Signed URL, expires after one hour. |
| duration_s | integer | Read-only after upload processing. |
| transcript_status | enum | pending, ready, failed. |
| audit_status | enum | not_requested, pending, complete, failed. |
| status | enum | draft, published, withdrawn. |
| published_at | timestamp, null | Null until published. |
| Field | Type | Rule |
|---|---|---|
| id | string | Prefix se_. |
| title | string | Up to 140 characters. |
| kind | enum | moderated_debate, open_table, live_audit. |
| starts_at | timestamp | Must be in the future when created. |
| planned_min | integer | 15 to 120. |
| host_id | string | Account id of the host. |
| seats | integer | Listener capacity. Speakers are counted separately. |
| turn_seconds | integer | Defaults to 90. |
| objection_min | integer | Minutes reserved for the strongest objection. Defaults to 10. |
| recording_consent | object | Map of speaker id to pending, granted, declined. |
| status | enum | See the lifecycle below. |
| episode_id | string, null | Set when the session converts. |
A session cannot move to live until every speaker's consent is granted. A session that ends without full consent stays ended and never converts.
| Field | Type | Rule |
|---|---|---|
| id | string | Prefix mk_. |
| episode_id | string | Markers made live attach to the session first and move to the episode on conversion. |
| span_id | string | Prefix sp_. Server-assigned. Markers that cover the same stretch of audio (50% overlap or more) share one span. |
| t_start_s | integer | Seconds from the start of the audio. |
| t_end_s | integer, null | Optional. Must be greater than t_start_s. |
| tag | enum | Closed set of four. |
| text | string | The claim as spoken, up to 280 characters. |
| source | enum | author, auditor, listener. Set by the server, never by the client. |
| agreement | enum | single, agreed, disputed, resolved. Computed per span from author and auditor markers only. Listener markers never change it. |
| resolution | object, null | Set by the host on a disputed span: decision (accepted_auditor, kept_author, noted), note, resolved_at. Both original markers stay visible. |
| Field | Type | Rule |
|---|---|---|
| id | string | Prefix qe_. |
| session_id | string | Foreign key to session. |
| user_id | string | One open entry per user per session. |
| claim_text | string | What the person wants to say, up to 280 characters. |
| kind | enum | turn or objection. Objections feed the final slot. |
| status | enum | waiting, speaking, done, withdrawn. |
On one span, the same tag from both means agreed. Different tags mean disputed. One source means single. Listener markers never move this field.
Each span reports how many distinct accounts chose each tag. One account has one vote per span and can change it. Distributions stay hidden until enough accounts have voted, so a handful of people never looks like a crowd.
When at least 10 accounts have voted and 60% or more chose a tag different from the agreed one, the span gets listener_signal: "divergent" and the host is notified. Both numbers are starting values. They hold for a mid-sized audience and need calibrating against real episodes, since a small niche audience could clear 10 easily.
If more than 30% of a span's votes arrive within five minutes from accounts under seven days old, the span gets listener_signal: "suspect_burst". Its votes drop out of the distribution until a moderator clears it. These thresholds are also starting values, and they depend on the platform exposing account age.
The host resolves it by accepting the Auditor's tag, keeping their own, or adding a note. The resolution is recorded next to the two markers and never replaces them.
Same signing, retry and delivery rules as the core webhooks. Only the event types below are new.
| Event | Fires when | Payload |
|---|---|---|
| episode.published | A draft becomes public. | episode |
| episode.audit_completed | The Argument Auditor finishes a run. | episode, counts per tag, disputed count |
| marker.disagreement_flagged | Two sources tag the same span differently. | both markers |
| marker.listener_divergence | A span first meets the divergence rule, or is flagged suspect_burst. | span, distribution, signal |
| session.started | A host starts a session. | session |
| session.ended | A session ends, with or without consent to publish. | session, convertible flag |
| session.replay_ready | Conversion finishes and the draft episode exists. | session, episode |
Every error response is JSON with a machine-readable code and a human-readable message, for example { "code": "consent_required", "message": "Speaker u_34 has not granted consent." }. Clients should branch on code, never on message.
| Code | HTTP | Meaning |
|---|---|---|
| invalid_tag | 400 | Tag is outside the closed set. |
| consent_required | 403 | A speaker has not granted recording consent. |
| not_host | 403 | Caller lacks podcast:host on this resource. |
| session_not_live | 409 | Queue and floor actions only work while live. |
| not_disputed | 409 | Only a span marked disputed can be resolved. |
| queue_entry_exists | 409 | The user already has an open entry. |
| transcript_not_ready | 422 | Audit and publishing need a ready transcript. |
| rate_limited | 429 | Retry after the interval in Retry-After. |
These examples use Node 18 or later with ES modules ("type": "module" in package.json, or .mjs files) and nothing else to install. An official package would wrap the same calls. Its name is not decided, so none is used here.
NAHMEAN_API_URL is the API origin without a trailing slash. NAHMEAN_TOKEN is a bearer token with the scopes the call needs: podcast:write to join, raise a hand and tag, podcast:host to resolve.
ONE PIPELINE from live room to published record. The API never lets a claim lose its source on the way.
Keeping listeners out of agreement protects the field from brigading, but it assumes the author and the Auditor are the reliable pair. They can agree and both be wrong, and an author who copies the Auditor's classifications would produce agreement that means nothing.
The thresholds (10 voters, 60% divergence, a 30% burst share, seven-day account age) are guesses until real episodes exist. If they are set too low, the divergence signal fires constantly and hosts learn to ignore it. If they are set too high, the signal never fires.
The stronger claim is that the rule should be tested the way Nah Mean tests arguments: run it on the first ten episodes, compare listener divergence against the disputes hosts resolve, and publish what it missed.
| Version | Date | Change |
|---|---|---|
| v4.0 draft 3 | Oct 8, 2026 | Added the Node.js quickstart (client, join and queue, tag, resolve, webhooks), the shared error body shape, and span_id in marker examples and a response example for the queue endpoint. |
| v4.0 draft 2 | Oct 8, 2026 | Settled the agreement rule: author and Auditor only, listener distribution with a threshold, divergence and burst signals, host resolution. Added span_id, resolution, the resolve endpoint, one webhook event and the not_disputed error. Corrected the group count in the page intro. |
| v4.0 draft | Oct 8, 2026 | Added the Podcast section: episodes, sessions, claim markers, queue, six webhook events. |