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.

Ran on 10 November 2026Language GoJob Live transcript

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/keys

The 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 connection

Printed 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 same in Node.js · Python

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.