CoDNA Request access
Contents

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 as https://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 when CODNA_PUBLIC_URL is 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: recording can be fetched;
    • failed: with error, 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. id is CoDNA's, for a member of the workspace; a guest has none.
  • recording is null when a ready meeting recorded nothing, as when nobody turned their microphone on.
    • mix is everyone together, for playing back. It becomes the page's original.
    • tracks are one per person and stretch of their speech: someone who left and came back has a track for each stay.
    • path is relative to the meeting: files/mix.ogg is fetched from /v1/meetings/{id}/files/mix.ogg.
    • speaker is the name to transcribe the track under. Two people with the same name are told apart by the service ("Nino Beridze (2)").
    • offset is 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 check CODNA_MEET_TOKEN; starting one is refused with the same words.
  • Other 4xx: the service's answer about this meeting. CoDNA shows error and 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 longer Retry-After, for up to a day since the service last answered.
  • 404 for a meeting CoDNA is following: the service lost it, and its page says so.

What CoDNA does with it

  1. 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.
  2. While the call is on, a worker asks the service how it stands, and the page shows who is in the call.
  3. 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.
  4. 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.
  5. 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/instance says "meetings": true when a meetings service is set up.
  • POST /api/meetings with title starts one. It answers the recording, its page included, and a join_url for the person who started it. 503 when 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}/meeting with title: rename it, for whoever started it. The page gains a version with the new title and nothing else changed.
  • POST /api/documents/{id}/meeting/invite with recipients, 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 a meeting.invite notification for each person in them. It answers who was invited, who was skipped and why, and how many people were told.
  • 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 where page_url leads.
  • The page's recording carries meeting: its status, participants, on while it can be joined, guest_url while it is on, and can_end.

Writing a service

The least a service has to do:

  1. Check the bearer token.
  2. POST /v1/meetings: make a room, answer an id and a guest link.
  3. POST /v1/meetings/{id}/join: answer a link into the room under the person's name.
  4. Record each person's audio on its own track, and everyone together.
  5. GET /v1/meetings/{id}: say how it stands, and list the files once it is ready.
  6. GET /v1/meetings/{id}/files/{name}: serve them.
  7. 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.