Developers · Go
Live meeting transcript in Go, each line with its speaker
A Go program that prints each line as it is said, with the speaker, with what it printed when it was run.
What it does
This program reads the socket until its context runs out, printing each transcript line as hours:minutes:seconds, the speaker, and the words.
It needs a key and a meeting code in MEETFUL_KEY and MEETING. Two curl requests make both. MEETFUL_URL is optional and defaults to https://meetful.com.
Go 1.22 or later. The socket programs use github.com/coder/websocket (go get github.com/coder/websocket@v1.8.12); the transcript program uses only the standard library.
The code
// Print every line said in a Meetful meeting, with its speaker, as it is said. go get github.com/coder/websocket
// MEETFUL_KEY=tmk_… MEETING=abc-defg-hij LISTEN_FOR=60 go run .
package main
import (
"context"
"fmt"
"log"
"os"
"strconv"
"strings"
"time"
"github.com/coder/websocket"
"github.com/coder/websocket/wsjson"
)
type message struct {
Type string
Meeting string
Why string
Line struct {
At int64
Speaker string
Text string
}
}
func main() {
base := os.Getenv("MEETFUL_URL")
if base == "" {
base = "https://meetful.com"
}
secs, _ := strconv.Atoi(os.Getenv("LISTEN_FOR"))
if secs == 0 {
secs = 60
}
ctx, cancel := context.WithTimeout(context.Background(), time.Duration(secs)*time.Second)
defer cancel()
conn, _, err := websocket.Dial(ctx, "ws"+strings.TrimPrefix(base, "http"), nil)
if err != nil {
log.Fatal(err)
}
defer conn.Close(websocket.StatusNormalClosure, "done")
conn.SetReadLimit(4 << 20)
hello := map[string]string{"type": "agent-hello", "key": os.Getenv("MEETFUL_KEY"), "meeting": os.Getenv("MEETING")}
if err := wsjson.Write(ctx, conn, hello); err != nil {
log.Fatal(err)
}
for {
var m message
if err := wsjson.Read(ctx, conn, &m); err != nil {
if ctx.Err() != nil {
return // listened for as long as we were asked to; the closed socket is expected
}
log.Fatal(err)
}
switch m.Type {
case "agent-ready":
fmt.Printf("listening in %s…\n", m.Meeting)
case "transcript":
// The speaker is whose microphone the words came from, not a guess.
at := time.UnixMilli(m.Line.At).UTC().Format("15:04:05")
fmt.Printf("%s %s: %s\n", at, m.Line.Speaker, m.Line.Text)
case "agent-refused":
log.Fatal("Refused: " + m.Why)
}
}
}Run it
MEETFUL_KEY=tmk_… MEETING=abc-defg-hij go run .
What it printed
listening in gik-fhvv-dcu…
14:17:16 Dana: Can we move the launch to the third of March?
14:17:17 Sam: Yes, if the docs are ready by the twentieth.
14:17:17 Dana: Then Sam owns the docs.
Run on 10 November 2026 against a Meetful server built from the same code as meetful.com, with two people — Dana and Sam — in the meeting saying three lines. Nothing above was edited.
How it works
The context made with WithTimeout bounds the whole conversation. When it expires, the library closes the socket under the waiting Read, so the error that comes back is about a closed connection, not the deadline; the program asks ctx.Err() whether time is up and, if so, ends quietly.
time.UnixMilli turns the line's at into a time.Time. Every line carries the name of the microphone it came from, so the speaker field needs no cleaning up.
What can go wrong
Refused: no such key — mint one with POST /api/keysThe key is wrong, or was never minted on this server. A key from one Meetful server does not work on another.
failed to read JSON message: failed to get reader: use of closed network connectionPrinted at the very end if you test the error with errors.Is(err, context.DeadlineExceeded): the first version of this program did, and this is what it printed. Check ctx.Err() instead.
Next
In Go: join a meeting · speak · get the transcript
The whole API · Every socket message and webhook · All guides
Try Meetful
Meetings that take their own notes
Free video meetings in the browser: every line of the transcript under the name of who said it, and the summary and action items when you hang up. Nothing to install — guests just open the link.