Skip to content

Pull meetings into your own tools

A token with recordings:read (Meetings, Read) can copy every finished meeting out of Lobbe: into a folder of Markdown notes, a wiki, a search index, or the inbox an agent works through. This page walks through that pull, then keeps it up to date.

  1. Page through the finished meetings with GET /recordings/ready: newest first, up to limit (1–100, 20 by default) per page. Send each page’s nextCursor back as cursor, exactly as you got it; it is null on the last page.
  2. For each meeting you don’t have yet, fetch the whole meeting from its bundleUrl: JSON, or one Markdown document with ?format=markdown.
  3. Keep it under its id. The id never changes.
{
"schemaVersion": 1,
"items": [
{
"id": "MEETING_ID",
"title": "Weekly product sync",
"kind": "MEETING",
"createdAt": "2026-10-19T08:31:12.000Z",
"recordedAt": "2026-10-19T07:00:00.000Z",
"durationSeconds": 2520,
"url": "https://app.lobbe.ai/m/MEETING_ID",
"bundleUrl": "https://app.lobbe.ai/api/v1/recordings/MEETING_ID/bundle",
"series": null,
"displayTitle": "Weekly product sync"
}
],
"nextCursor": "1792398672000.MEETING_ID"
}

Three things to know about the list:

  • Walk all of it, every time. Don’t stop at the first meeting you already have. The list is in the order Lobbe received the meetings, and a meeting joins it only once it is finished: a long meeting sent at 10:00 can finish after a short one sent at 10:05, and then sits below it.
  • The cursor holds its place. It continues after the last meeting of the page, so a meeting that leaves the list between two pages never makes you miss the next one.
  • It has your own meetings only. A series you joined isn’t in it: walk GET /recordings/ready?seriesId=SERIES_ID for each of those (see Series).

The list can’t tell you that a meeting you copied has changed (its Lobbe made again, a new title, an edited transcript, renamed speakers, another series) or was deleted. The events feed can.

Instead of walking the whole list again and again, follow GET /events (the events feed). It covers the same meetings as the list without seriesId, your own, at every status.

  1. Take a cursor first, with GET /events?after=latest, and keep it.
  2. Then walk the list once, as above, for everything that is already there.
  3. Then read the feed from that cursor. Anything that changed during the walk is in it, so you never miss a meeting. You may meet one twice, which is harmless.

What to do with each event:

Event What to do
recording.ready Fetch its bundle
recording.updated Fetch its bundle again: data.change says what changed
recording.deleted Delete your copy
recording.created, recording.naming_required, recording.failed Nothing: the meeting isn’t finished
  • Fetch when data.status is READY. A deleted meeting that is restored comes back as recording.updated with restored, at the status it had: fetch it if that is READY.
  • A bundle can answer 409 NOT_READY when the meeting is being made again by the time you fetch it, and 404 when it was deleted since. Skip it: a later event says what happened.
  • Fetching twice is harmless. The feed gives each event at least once: see At least once.
  • A cursor left unused for 30 days answers 410 CURSOR_EXPIRED: start again from step 1. The walk skips the meetings you already have, so one that changed while you were away keeps its old copy. To be sure, empty your copy and fetch everything again.

This script copies every finished meeting into ./meetings, one Markdown file per meeting, then follows the feed for good. It needs bash, curl and jq. Run it again after a stop and it goes on from the cursor it stored, even when the stop came during its first walk; an answer it doesn’t expect stops it, printed.

#!/usr/bin/env bash
# Copy every finished meeting into ./meetings as Markdown, then keep up with the feed.
api=https://app.lobbe.ai/api/v1
mkdir -p meetings
# GET $1 into the file $2 and print the HTTP status (000: no answer). Waits Retry-After on a 429.
get() {
local code wait
while true; do
rm -f "$2" headers.txt
code=$(curl -sS -o "$2" -D headers.txt -w '%{http_code}' "$1" \
-H "Authorization: Bearer $LOBBE_TOKEN") || code=000
wait=$(awk -F': *' 'tolower($1) == "retry-after" { print $2 + 0 }' headers.txt 2>/dev/null)
if [ "$code" = 429 ] && [ -n "$wait" ]; then sleep "$wait"; continue; fi
echo "$code"
return
done
}
# One meeting's Markdown. 404 (deleted) and 409 (being made again) are skipped:
# a later event says what happened. Anything else stops the script.
fetch() {
case "$(get "$api/recordings/$1/bundle?format=markdown" meeting.tmp)" in
200) mv meeting.tmp "meetings/$1.md" ;;
404 | 409) rm -f meeting.tmp ;;
*) cat meeting.tmp 2>/dev/null; exit 1 ;;
esac
}
# The first run: a cursor from now, THEN the whole list, so nothing that changes meanwhile is missed.
# A run stopped during the walk keeps that first cursor, so the feed still covers what changed since.
if [ ! -s meetings/.cursor ]; then
if [ ! -s meetings/.start-cursor ]; then
[ "$(get "$api/events?after=latest" page.json)" = 200 ] || { cat page.json 2>/dev/null; exit 1; }
jq -er .nextCursor page.json >next.cursor && mv next.cursor meetings/.start-cursor || exit 1
fi
cursor=""
while true; do
[ "$(get "$api/recordings/ready?limit=100${cursor:+&cursor=$cursor}" page.json)" = 200 ] ||
{ cat page.json 2>/dev/null; exit 1; }
for id in $(jq -r '.items[].id' page.json); do
[ -f "meetings/$id.md" ] || fetch "$id"
done
cursor=$(jq -r '.nextCursor // empty' page.json)
[ -n "$cursor" ] || break
done
mv meetings/.start-cursor meetings/.cursor
fi
# Then the feed, page by page, storing the cursor only once a page is handled.
while true; do
[ "$(get "$api/events?after=$(cat meetings/.cursor)&limit=100" page.json)" = 200 ] ||
{ cat page.json 2>/dev/null; exit 1; }
jq -r '.events[] | "\(.type) \(.recordingId) \(.data.status)"' page.json >events.txt
while read -r type id status; do
if [ "$type" = recording.deleted ]; then rm -f "meetings/$id.md"
elif [ "$status" = READY ]; then fetch "$id"
fi
done <events.txt
jq -er .nextCursor page.json >next.cursor && mv next.cursor meetings/.cursor || exit 1
[ "$(jq '.events | length' page.json)" -eq 100 ] || sleep 60
done
  • After a 410 CURSOR_EXPIRED: delete meetings/.cursor and meetings/.start-cursor, and run it again. It takes a new cursor and walks the list again, fetching only the meetings it doesn’t have.
  • Its own files: the two cursors live in meetings/, beside the copies. It also leaves page.json, events.txt and headers.txt in the directory you run it from, and meeting.tmp after a fetch that stopped it.
  • JSON instead of Markdown: drop ?format=markdown and name the files .json.
  • Rate limits: the list, the bundles and the feed share the token’s read budget: its size is in Rate limits. A large first pull can use it up; get then waits the seconds Retry-After gives.
  • One token, one job: this token needs recordings:read only. It never spends minutes.

GET /recordings/{id}/bundle answers one finished meeting. As JSON it has four parts:

Part What it holds
recording The meeting: its id, its titles (title, displayTitle, subtitle), kind, source, when it happened (recordedAt) and when Lobbe received it (createdAt), its length, its url in the app, its series, and access: owner, or member for a series you joined
transcript What was said: the language, the people (speakers, with the names given to them), the turns (segments, each with its speaker, times and text), the whole text, and the transcript as Markdown
lobbe The Lobbe: its template (type), its language, its content as sections, and the whole Lobbe as Markdown
stats The meeting’s numbers, counted from the transcript: speaking time, turns, questions, per person

The Lobbe’s sections come in reading order, and each one’s kind says what fields it has: summary, decision_list, action_item_list and so on. The reference lists every kind’s fields.

With ?format=markdown, the same meeting is one text/markdown document: the title as a heading, the meeting’s details (when, how long, the series, who spoke), the Lobbe’s sections, then the transcript. It is made to drop into a note or a prompt as it is. Its reach and its errors are the JSON bundle’s: 404 for a meeting you can’t read, 409 NOT_READY for one that isn’t finished.

  • The bundle is stable within v1. schemaVersion stays 1, and changes within v1 only add: a field is never removed or renamed.
  • So ignore what you don’t know. A field you don’t recognize may appear in any part: skip it, never fail on it. The same goes for a Lobbe section whose kind you don’t know.
  • Key your copy on recording.id, and file it by recordedAt, when the meeting happened.