Docs How it works
Meetings
People start an online meeting from their workspace, hold it as a video call, and find it in raw
afterwards: the recording, and a transcript that names who said what. CoDNA runs no calls itself. A
meetings service at CODNA_MEET_URL holds them, on whatever media server it likes, records each
person's audio on its own track, and hands the recording back once the call is over. CoDNA files it as
a recording and sends the tracks to the transcription service together, so the
transcript's speakers are the people in the call rather than "Speaker 1" and "Speaker 2". The
reasoning is adr/0018.
This page is the contract between CoDNA and a meetings service. Like transcription's, it is small on purpose, so that anyone can put their own media server behind it. The hosted service at codna.me is one implementation of it, on LiveKit.
Settings
CODNA_MEET_URL: the service's base URL, such ashttps://meet.example.com. CoDNA appends the paths below to it. Unset, there is no Meeting button.CODNA_MEET_TOKEN: the bearer token CoDNA sends. One per instance: the service knows which instance is calling by its token.
Version 1
Every path starts with /v1. Every request carries Authorization: Bearer <token>.
Within v1, either side may add optional fields, and both ignore fields they do not know. Anything
else is a /v2, and a service keeps answering /v1 until the cores calling it have moved.
Start a meeting
POST /v1/meetings
Content-Type: application/json
Idempotency-Key: 0192a4c6-6f1e-7c3a-9e55-3c1d2f0a9b11
{
"title": "Weekly sync",
"workspace": "Acme",
"host": { "id": "0192a4c6-…", "name": "Nino Beridze" },
"notice": "Recordings on this workspace are kept to improve speech recognition for Georgian.",
"guests": true,
"page_url": "https://kb.acme.example/meetings/0192a4c6-6f1e-7c3a-9e55-3c1d2f0a9b11?invite"
}
title: what the person starting it called it.workspace: the workspace's name as people know it, shown in the call:CODNA_ORG_NAME, or the host CoDNA is served at when that is unset.host: CoDNA's id for the person starting it, and their name.notice: optional words everyone joining is shown, beside the service's own "this meeting is recorded". CoDNA sends the transcription service's notice while that service keeps recordings.guests: whether anyone with a link may join, typing their own name. CoDNA always asks for one.page_url: optional, the meeting's page in CoDNA, with the picker for inviting people from the workspace open on arrival: an absolute http(s) URL of at most 2000 characters. It lasts however the page is moved. CoDNA sends it whenCODNA_PUBLIC_URLis an http(s) URL. A service may show it to members in the call (not to guests), as "Invite people from"; one that does not ignores it. Idempotency-Key: CoDNA's id for the meeting. The same key answers the same meeting, so a start that was not heard back from can be sent again without starting two.
201 Created
{
"id": "m_7HxK2",
"status": "waiting",
"guest_url": "https://meet.example.com/j/k3Jd9",
"created_at": "2026-10-19T18:00:00Z"
}
The meeting's id is the service's, opaque to CoDNA, at most 200 characters. guest_url is the link
anyone can open to join, present when guests was true; it lasts as long as the meeting.
Join
POST /v1/meetings/{id}/join
Content-Type: application/json
{ "person": { "id": "0192a4c6-…", "name": "Nino Beridze" } }
200 OK
{ "url": "https://meet.example.com/j/t_8sKq…", "expires_at": "2026-10-19T18:10:00Z" }
CoDNA sends the person's browser to url, in a new tab. It is good once, for a few minutes, and opens
the call under their name. CoDNA asks for one each time someone presses Join, never ahead of time. A
meeting that has ended answers 409.
Who may join is CoDNA's decision, not the service's: whoever can read the meeting's page in CoDNA.
Ask how a meeting stands
GET /v1/meetings/{id}
{
"id": "m_7HxK2",
"title": "Weekly sync",
"status": "ready",
"started_at": "2026-10-19T18:00:41Z",
"ended_at": "2026-10-19T18:47:02Z",
"duration": 2781.2,
"participants": [
{ "id": "0192a4c6-…", "name": "Nino Beridze", "guest": false, "joined_at": "…", "left_at": "…" },
{ "name": "Levan (guest)", "guest": true, "joined_at": "…", "left_at": "…" }
],
"recording": {
"mix": { "path": "files/mix.ogg", "content_type": "audio/ogg", "bytes": 21345678, "duration": 2781.2 },
"tracks": [
{ "path": "files/t1.ogg", "content_type": "audio/ogg", "bytes": 8123456,
"speaker": "Nino Beridze", "offset": 0.0, "duration": 2779.9 },
{ "path": "files/t2.ogg", "content_type": "audio/ogg", "bytes": 6234567,
"speaker": "Levan (guest)", "offset": 63.4, "duration": 1201.0 }
]
}
}
status:waiting: nobody has joined yet;live: someone is in the call;ended: the call is over, and the recording is being put together;ready:recordingcan be fetched;failed: witherror, in words for the person who started it.
- A meeting ends when everyone has left and two minutes pass, when someone ends it for everyone, or after six hours.
participants: everyone who was in the call.idis CoDNA's, for a member of the workspace; a guest has none.recordingis null when a ready meeting recorded nothing, as when nobody turned their microphone on.mixis everyone together, for playing back. It becomes the page's original.tracksare one per person and stretch of their speech: someone who left and came back has a track for each stay.pathis relative to the meeting:files/mix.oggis fetched from/v1/meetings/{id}/files/mix.ogg.speakeris the name to transcribe the track under. Two people with the same name are told apart by the service ("Nino Beridze (2)").offsetis where the track starts, in seconds from the start of the mix.
There is no callback. CoDNA asks every half minute while a meeting is waiting, live or ended, and every two minutes for one nobody has joined in a quarter of an hour. When someone ends a meeting from its page, CoDNA asks at once.
Fetch a file
GET /v1/meetings/{id}/files/{name}
The bytes, with the content_type the meeting gave, and their length. CoDNA streams them to disk and
tells audio from the bytes themselves: a mix that is not audio or video ends the meeting.
End it for everyone
POST /v1/meetings/{id}/end
202. Whoever started the meeting can end it from its page in CoDNA, as from the call itself.
Let go of it
DELETE /v1/meetings/{id}
CoDNA calls this once it has fetched the files, before transcribing them, or once it has given up on
the meeting. The service then deletes the recording. 204, or 404 for one it no longer has. A meeting
that is ready and not let go of may be deleted by the service a week later.
Errors
An error is a status and, where there is something to say, a body: { "error": "<words for people>" }.
401: the token is wrong. A meeting being followed ends, saying whoever runs CoDNA needs to checkCODNA_MEET_TOKEN; starting one is refused with the same words.- Other
4xx: the service's answer about this meeting. CoDNA showserrorand does not ask again. 429,5xx, and a connection that fails: try again later. Starting a meeting tells the person to try again in a moment. A meeting being followed is asked again, backing off from ten seconds doubling to ten minutes, honouring a longerRetry-After, for up to a day since the service last answered.404for a meeting CoDNA is following: the service lost it, and its page says so.
What CoDNA does with it
- New → Start a meeting starts one at once, named for when it started in the person's own time
("Meeting, 19 Oct 15:30"). CoDNA files a raw page for it, opens the call in a new tab for the person
who started it, and takes this tab to the page. The page says the meeting is in progress and
carries, for whoever can read it, Join the call and the guest link to copy. Whoever started it
can rename it and end it for everyone.
- Invite people (for whoever may share the page) picks people and groups from the workspace. Each is shared the page to read, so they join under their own names, and each person is told once: an item in their inbox, and a banner in any CoDNA page they have open, "Nino invites you to Weekly sync", with Join and Dismiss, for as long as the meeting is on. Sharing the page by hand lets someone join too, without the invitation.
- What happens to the recording is said once, before anyone joins: the meetings service shows the transcription service's notice on its camera check, which members and guests alike pass. The page does not repeat it.
- While the call is on, a worker asks the service how it stands, and the page shows who is in the call.
- Once it is
ready, the worker fetches the mix as the page's original and each track beside it, then lets go of the meeting at the service. The page's next version says the meeting is being transcribed, and the mix plays from the page. - The tracks go to the transcription service as one job (Transcription), each under its speaker's name, when the service's policy says it takes tracks. Otherwise, or when a meeting has more than 50 tracks, the mix is sent alone, and speakers are told apart by ear.
- The transcript becomes the page's next version, with real names:
---
title: Weekly sync
original_filename: weekly-sync.ogg
mime_type: audio/ogg
source: meeting
transcription: done
meeting_started: "2026-10-19T18:00:41Z"
meeting_ended: "2026-10-19T18:47:02Z"
participants: [Nino Beridze, Levan (guest)]
length: "46:21"
length_seconds: 2781
language: ka
engine: gemini-3.5-transcribe
speakers: [Nino Beridze, Levan (guest)]
kept_for_training: true
---
**Nino Beridze** (0:00) Let's start with the budget.
**Levan (guest)** (1:03) I sent the numbers on Friday.
The tracks are kept until the transcript arrives, so a transcript that fails can be asked for again with the speakers' names; then they are dropped, and the mix stays as the original. A meeting that recorded nothing, failed at the service, or was lost there gets a version saying so. Without a transcription service, the mix is kept and the page says it was not transcribed.
REST
GET /api/instancesays"meetings": truewhen a meetings service is set up.POST /api/meetingswithtitlestarts one. It answers therecording, its page included, and ajoin_urlfor the person who started it.503when the service cannot be reached.POST /api/documents/{id}/meeting/join: a link into the call of the meeting a page holds, for whoever can read the page.POST /api/documents/{id}/meeting/end: end it for everyone, for whoever started it.PATCH /api/documents/{id}/meetingwithtitle: rename it, for whoever started it. The page gains a version with the new title and nothing else changed.POST /api/documents/{id}/meeting/invitewithrecipients, people (by email) and groups (group:<slug>) as sharing names them: shares the page with each to read, without the usual "shared with you", and raises ameeting.invitenotification for each person in them. It answers who wasinvited, who wasskippedand why, and how many people weretold.GET /api/me/meeting-invites: the caller's unanswered invitations to meetings still on, for the banner. Marking the notification read answers one. A meeting that ends answers all of its own.GET /api/meetings/{id}: the page of the meeting with that id, for whoever can read it, which is wherepage_urlleads.- The page's
recordingcarriesmeeting: itsstatus,participants,onwhile it can be joined,guest_urlwhile it is on, andcan_end.
Writing a service
The least a service has to do:
- Check the bearer token.
POST /v1/meetings: make a room, answer an id and a guest link.POST /v1/meetings/{id}/join: answer a link into the room under the person's name.- Record each person's audio on its own track, and everyone together.
GET /v1/meetings/{id}: say how it stands, and list the files once it is ready.GET /v1/meetings/{id}/files/{name}: serve them.DELETE /v1/meetings/{id}: delete them.
POST …/end may do nothing but end the room. A service that cannot record separate tracks can list
none: the meeting is transcribed from its mix.