Polling and the events feed
Lobbe works on a meeting in the background, so your tool finds out by asking. There are two ways: ask for one meeting’s status, or read the events feed, which says what changed across all your meetings.
Polling one meeting
Section titled “Polling one meeting”Ask for GET /recordings/{id}/status about once a
minute. Most meetings are done within a few minutes, and once a minute stays far inside the token’s
read budget (see Rate limits).
- Over the budget, the answer is
429 RATE_LIMITEDwithRetry-After: the seconds to wait. Wait that long, and don’t ask sooner. - Stop at
NAMINGorREADY: the first needs you, the second ends the wait. What to do at each is in The meeting lifecycle. FAILEDcan be temporary. Lobbe retries a failed step by itself after a short wait, so a poll can catch aFAILEDthat then moves on. Whenerror.retryableistrue, retry it and keep waiting: the retry never starts a second run while Lobbe’s own is pending, and409 NOT_FAILEDmeans the meeting has already recovered. Stop after a few retries. Whenerror.retryableisfalse, stop and reporterror.code. Retrying needsrecordings:write.
A loop that waits a minute between asks, honors Retry-After, retries a meeting that can recover
(up to three times), and stops on anything else, printing it (it needs jq):
retries=0while true; do code=$(curl -sS -o status.json -D headers.txt -w '%{http_code}' \ https://app.lobbe.ai/api/v1/recordings/MEETING_ID/status \ -H "Authorization: Bearer $LOBBE_TOKEN") || break wait=$(awk -F': *' 'tolower($1) == "retry-after" { print $2 + 0 }' headers.txt) if [ "$code" = 429 ] && [ -n "$wait" ]; then sleep "$wait"; continue; fi [ "$code" = 200 ] || { cat status.json; break; } status=$(jq -r .status status.json) echo "$status" case "$status" in NAMING | READY) break ;; esac if [ "$status" = FAILED ]; then if [ "$(jq -r .error.retryable status.json)" != true ] || [ "$retries" -ge 3 ]; then cat status.json break fi retries=$((retries + 1)) code=$(curl -sS -o retry.json -w '%{http_code}' -X POST \ https://app.lobbe.ai/api/v1/recordings/MEETING_ID/retry \ -H "Authorization: Bearer $LOBBE_TOKEN") || break # 200: the step runs again. 409 NOT_FAILED: it recovered by itself. Either way, keep waiting. [ "$code" = 200 ] || [ "$(jq -r .error.code retry.json 2>/dev/null)" = NOT_FAILED ] || { cat retry.json; break; } fi sleep 60doneThe events feed
Section titled “The events feed”Following more than a few meetings, read GET /events instead of
asking for each one’s status. It is one list of what happened to your meetings, oldest first:
{ "schemaVersion": 1, "events": [ { "id": "1042", "type": "recording.naming_required", "recordingId": "MEETING_ID", "seriesId": null, "occurredAt": "2026-10-19T09:14:03.120Z", "data": { "status": "NAMING" } } ], "nextCursor": "773.1042.1792300000000"}The event types
Section titled “The event types”type |
When it is sent | data |
|---|---|---|
recording.created |
Lobbe has the meeting in full: recorded in the app, uploaded, or sent by a token | status |
recording.naming_required |
The meeting is at NAMING; sent again each time naming opens again |
status |
recording.ready |
The meeting’s Lobbe was made for the first time | status |
recording.failed |
The meeting failed for good | status, error: { code, retryable } |
recording.updated |
A READY meeting changed, and data.change says how |
status, change |
recording.deleted |
The meeting was deleted, at any status | status |
data.change is one of regenerated (the Lobbe was made again), renamed, transcript_edited,
speakers_renamed, series_changed (moved into, out of or between series) and restored (a
deleted meeting is back, at whatever status it had).
- No event carries a meeting’s content: only ids, its status, an error code or the kind of change. To read the meeting, fetch its bundle.
- The steps in between send nothing:
QUEUED,TRANSCRIBINGandGENERATINGhave no event. Neither has a retry: the meeting’s next event follows. recording.failedis final: a failure Lobbe is still retrying by itself is never sent.- A regeneration is
recording.updatedwithregenerated, never a secondrecording.ready. - Changes to a series itself send nothing for its meetings: renaming it, changing how it titles meetings, or deleting it.
Whose meetings
Section titled “Whose meetings”- With
recordings:read, the feed has every meeting of the account, whoever sent it: the app, an upload, this token or another. It doesn’t have the meetings of a series the owner joined. - With
recordings:writeand norecordings:read, it has only the meetings this token sent. While sending meetings through a token isn’t on for the account, such a token gets403 SCOPE_DISABLED. seriesIdis the series the meeting was in when the change happened:nullwhen it was in none, or while series are switched off.
Reading it
Section titled “Reading it”- Start from now with
after=latest: no events, and a cursor to read on from. Withoutafter, the feed starts from the oldest event it still keeps, about 30 days back. - Keep
nextCursor. It is always there. Send it back asafteron the next read, exactly as you got it: never build or change one, and never use an event’sidas one. - Read page by page. A page has up to
limitevents (1–500, 100 by default). When a page comes back full, read the next one at once; otherwise wait about a minute. - Store the cursor after handling the page, never before.
A loop that does all four, and stops on any error, printing it:
cursor=$(cat lobbe-cursor 2>/dev/null || echo latest)while true; do body=$(curl -sS --fail-with-body "https://app.lobbe.ai/api/v1/events?after=$cursor&limit=100" \ -H "Authorization: Bearer $LOBBE_TOKEN") || { echo "$body"; break; } jq -c '.events[]' <<<"$body" # handle each event here cursor=$(jq -er .nextCursor <<<"$body") || { echo "$body"; break; } echo "$cursor" >lobbe-cursor [ "$(jq '.events | length' <<<"$body")" -eq 100 ] || sleep 60doneA consumer that runs for good waits Retry-After on a 429 instead of stopping, and recovers from a
410 as below. Reading the feed draws on the same read budget as status, bundles and the list.
At least once
Section titled “At least once”The feed gives you every event at least once, not exactly once. If your process stops after handling a page but before storing its cursor, the next read gives you those events again.
- Skip what you’ve handled, by event
id. It is the same on every read. - Don’t de-duplicate by meeting and type. One meeting can have several events of one type: naming
can open twice, and a meeting can be renamed many times. Each is its own event, with its own
id. - Follow the feed’s order, not
occurredAt. For one meeting, events come in the order its changes happened. An event can arrive a little after its change, but none is skipped.
30 days, then 410
Section titled “30 days, then 410”- Lobbe keeps events for 30 days.
- A cursor ages with the last event it read, or with the read itself once you’ve caught up. So a consumer that keeps reading never ages out, even on an account where nothing happens.
- A cursor older than that (one left unused for a month, say) answers
410 CURSOR_EXPIRED. So does a cursor from before a restore of Lobbe’s data.
To recover from a 410, or to start on an account that already has meetings:
- Take a new cursor with
after=latest. - Catch up. With
recordings:read, page throughGET /recordings/readyand add the finished meetings you don’t have yet. The list holdsREADYmeetings only, so a meeting that failed for good or was deleted in the gap isn’t in it. For a meeting you hold that the list no longer has, ask its status:404means it was deleted; otherwise the status says where it is now (a regeneration in progress, say). For a meeting you were still waiting on, its status says whether it failed. A token withrecordings:writealone has no list: ask the status of each meeting it sent that you are still waiting on. - Read on from the new cursor.
In that order, a meeting that changes while you catch up shows up in the feed too. You may meet it twice, but you never miss it.