# Open Mic
> Voice Q&A you can drop into any site. Visitors record or type a question; you
> answer by voice; the pair is spliced into a short podcast-style episode with a
> transcript, published on your own domain.
## Install
Paste one tag. No other attribute is required — the defaults render a working,
themed widget.
## Optional attributes
data-mode="inline|modal|bubble" how the ask widget appears (default: inline)
data-archive="list|grid|latest|featured"
render published episodes instead of the ask form
data-limit="10" how many episodes to show
data-episode="" embed one published episode's player
data-view="full" on an episode embed, render the whole
episode rather than the player
data-episode-base="/questions" where YOUR site keeps episode pages, so a
click in an embedded archive goes there
## Recommended layout
Three pages on YOUR site plus a link from wherever you already have one. This is
the shape the first production install arrived at after trying the alternatives,
and each piece of it is a mistake somebody already made.
/ask/
/questions/
/questions//
data-view="full" is what makes that third page worth landing on. Without it the
embed is a title and a play button, while the same episode on the space's own
host carries who asked, what the answer gets into, its themes and both
transcripts — so the journey the archive encourages ends somewhere thinner than
it started. With it, the two pages hold the same content.
It does NOT make that content yours for search: everything inside a frame is
noindexed, deliberately, or it would compete with the page embedding it. A
verified CNAME is what makes an episode page genuinely yours.
On the home page: your own button linking to /ask/, and optionally
data-archive="featured" is the home-page one. It shows the episodes the host
picked, in the order they put them in, chosen on the console's Featured page —
up to six. Until they pick any it shows their most recent, so the tag works from
the day it is pasted and starts obeying them the day they choose. Use
data-archive="list" data-limit="3" instead if you want strictly-most-recent and
never the host's selection.
Keep your own header, footer, nav, fonts and colours on all of these. Only the
widget comes from us; the page is yours.
### Why it is shaped like that
* **Give asking a page, not a modal.** data-mode="modal" exists and is the
wrong choice for this: a recording form is tall — record, email, consent, a
photo, a disclaimer — and an overlay fights the viewport and has no obvious
way out. Use inline on a page of its own.
* **Use your own button to reach it.** The modal launcher styles itself from
your SPACE's accent, so it will not match a button in your own stylesheet.
A plain link you style yourself always matches.
* **data-episode-base is not optional on an archive.** Without it, clicking an
episode navigates the IFRAME: your page's URL never changes, and the episode
cannot be linked, shared or bookmarked. It looks like it worked and quietly
is not a page. With it, the click lands on your own /questions//.
* **Link to your own pages, never to .openmic.dev.** Sending a visitor
to our hostname is the opposite of an embed.
* **Do not try to style the widget from your CSS.** It is a cross-origin
iframe and your rules cannot reach inside it, which is deliberate — it also
means your global `button` and `box-sizing` rules cannot break it. Change
how it looks in the console's Branding step instead: colours, fonts, radius
and copy all come from there.
* **Set the space's accent to match your primary button**, not to your
brand's loudest colour. The accent paints the widget's buttons and play
control, so matching what a visitor already sees on your site is what makes
it read as part of the page.
## Host page requirements
The widget records audio from a cross-origin iframe. Two headers on YOUR page
can block it, and both fail silently unless you set them.
Name YOUR SPACE'S host in them, not openmic.dev — that is the origin the iframe
is actually loaded from, and it becomes your own domain the moment you verify
one. The console's Install page shows the exact hostname to use.
Permissions-Policy: microphone=(self "https://")
Content-Security-Policy: frame-src https://;
connect-src https://openmic.dev
Measured browser behaviour, so you can predict failures:
* Chromium enforces Permissions-Policy AND exposes document.featurePolicy, so
a blocked microphone is detectable before the visitor taps record.
* Safari ignores the Permissions-Policy response header entirely and exposes
no featurePolicy API. A failure there is almost always a real user denial.
* A policy denial, a missing allow attribute, and a user clicking "Block" all
raise the identical NotAllowedError. The error name alone never tells you why.
## API
Everything the console can do, for a server or an agent.
curl https://openmic.dev/v1/space \
-H "Authorization: Bearer sk_..."
* Full reference: https://openmic.dev/docs/api/
* OpenAPI 3.1: https://openmic.dev/v1/openapi.json
* MCP server: https://openmic.dev/mcp — also at https://mcp.openmic.dev
Streamable HTTP. Speaks both the stateless 2026-07-28 revision and the
older initialize-based ones, so any client era connects.
If you are an agent, prefer the MCP server: the same operations arrive as
tools, with their scopes and their consequences in the descriptions.
Read endpoints today: /v1/space, /v1/questions, /v1/questions/,
/v1/episodes, /v1/episodes/, /v1/themes.
## Two ways to authenticate
A secret key, or an OAuth authorization made by a person. Both work on the REST
API and on the MCP server, and they answer different questions — "which space
is this server allowed to touch" versus "which person is this client acting
for".
**A secret key.** Create one in the console under Keys. Shown once, scoped
(read < write < admin), belongs to exactly one space, revocable. Right for a
server with nobody at a keyboard. This is NOT the pk_ key in your embed snippet
— that one is public, only tells a browser where to point the widget, and does
not authenticate here.
**OAuth, so the client acts as a person.** The MCP server answers an
unauthenticated tools/call with 401 and a WWW-Authenticate header naming its
RFC 9728 document, which is the entry point:
https://mcp.openmic.dev/.well-known/oauth-protected-resource
https://openmic.dev/.well-known/oauth-authorization-server
Authorization code with PKCE (S256 only), scopes openmic:read, openmic:write,
openmic:admin. The person signs in to Open Mic, picks ONE space, and the token
carries no more than their own membership allows — an editor cannot grant admin
however much the client asked for. It can be withdrawn from the same Keys page,
by an owner or by whoever approved it.
initialize, tools/list, ping and server/discover need no credential, so a
client can see what is on offer before deciding to authorize. Only
tools/call challenges — which means a client will connect successfully and
be sent through the browser on its first actual tool call.
**Register yourself.** RFC 7591 dynamic client registration is open at
https://openmic.dev/o/register/, advertised in the metadata above, so a client
nobody has met can get a client_id without asking anyone. Ten an hour per
address.
A registration is a name, not access. It reaches nothing until a person signs
in and approves it, for one space, and they can withdraw it again from the same
Keys page.
Every error, including 404 and 405, has one shape. Branch on `code`:
{"error": {"code": "invalid_credential", "message": "That key is not valid."}}
Lists take limit (max 100) and offset and return `total`. Out-of-range values
are clamped rather than refused, so a paging loop cannot be broken by asking
for too much.
No response carries an asker's email address. Names and locations appear only
where the asker consented to them being shown.
## Notes
* The recorder needs a secure context (https, or localhost).
* iOS Safari records audio/mp4; Chrome records audio/webm;codecs=opus. Both are
transcoded server-side to MP3, so you do not need to care.
* Published episodes are also served on your own domain via CNAME, which is
what makes their transcripts count as your SEO rather than ours.
* Episodes can be filed under themes — a curated per-space vocabulary, drafted
with the rest of the metadata and confirmed by the host. Each theme is a page
on the space's own host: /themes/ lists them, /themes// holds the
episodes filed under one, and both are in the sitemap.
* Themes are a hosted-site feature and the embed deliberately renders no theme
links. A chip inside a frame either loads a whole site page into somebody's
widget or navigates their visitor off their own site. If you want theme
browsing on YOUR pages, link to the space's /themes// URLs yourself, or
send visitors to the hosted archive.