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.
Walk the list, fetch each meeting
Section titled “Walk the list, fetch each meeting”- Page through the finished meetings with
GET /recordings/ready: newest first, up tolimit(1–100, 20 by default) per page. Send each page’snextCursorback ascursor, exactly as you got it; it isnullon the last page. - For each meeting you don’t have yet, fetch the whole meeting from its
bundleUrl: JSON, or one Markdown document with?format=markdown. - 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_IDfor 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.
Keep up with the feed
Section titled “Keep up with the feed”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.
- Take a cursor first, with
GET /events?after=latest, and keep it. - Then walk the list once, as above, for everything that is already there.
- 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.statusisREADY. A deleted meeting that is restored comes back asrecording.updatedwithrestored, at the status it had: fetch it if that isREADY. - A bundle can answer
409 NOT_READYwhen the meeting is being made again by the time you fetch it, and404when 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.
A script that does all of it
Section titled “A script that does all of it”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/v1mkdir -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/.cursorfi
# 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 60done- After a
410 CURSOR_EXPIRED: deletemeetings/.cursorandmeetings/.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 leavespage.json,events.txtandheaders.txtin the directory you run it from, andmeeting.tmpafter a fetch that stopped it. - JSON instead of Markdown: drop
?format=markdownand name the files.json. - Rate limits: the list, the bundles and the feed share the token’s
readbudget: its size is in Rate limits. A large first pull can use it up;getthen waits the secondsRetry-Aftergives. - One token, one job: this token needs
recordings:readonly. It never spends minutes.
What a bundle holds
Section titled “What a bundle holds”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.
Read it leniently
Section titled “Read it leniently”- The bundle is stable within v1.
schemaVersionstays1, 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
kindyou don’t know. - Key your copy on
recording.id, and file it byrecordedAt, when the meeting happened.