Skip to content

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.

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_LIMITED with Retry-After: the seconds to wait. Wait that long, and don’t ask sooner.
  • Stop at NAMING or READY: the first needs you, the second ends the wait. What to do at each is in The meeting lifecycle.
  • FAILED can be temporary. Lobbe retries a failed step by itself after a short wait, so a poll can catch a FAILED that then moves on. When error.retryable is true, retry it and keep waiting: the retry never starts a second run while Lobbe’s own is pending, and 409 NOT_FAILED means the meeting has already recovered. Stop after a few retries. When error.retryable is false, stop and report error.code. Retrying needs recordings: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):

Terminal window
retries=0
while 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 60
done

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"
}
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, TRANSCRIBING and GENERATING have no event. Neither has a retry: the meeting’s next event follows.
  • recording.failed is final: a failure Lobbe is still retrying by itself is never sent.
  • A regeneration is recording.updated with regenerated, never a second recording.ready.
  • Changes to a series itself send nothing for its meetings: renaming it, changing how it titles meetings, or deleting it.
  • 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:write and no recordings:read, it has only the meetings this token sent. While sending meetings through a token isn’t on for the account, such a token gets 403 SCOPE_DISABLED.
  • seriesId is the series the meeting was in when the change happened: null when it was in none, or while series are switched off.
  1. Start from now with after=latest: no events, and a cursor to read on from. Without after, the feed starts from the oldest event it still keeps, about 30 days back.
  2. Keep nextCursor. It is always there. Send it back as after on the next read, exactly as you got it: never build or change one, and never use an event’s id as one.
  3. Read page by page. A page has up to limit events (1–500, 100 by default). When a page comes back full, read the next one at once; otherwise wait about a minute.
  4. Store the cursor after handling the page, never before.

A loop that does all four, and stops on any error, printing it:

Terminal window
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 60
done

A 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.

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.
  • 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:

  1. Take a new cursor with after=latest.
  2. Catch up. With recordings:read, page through GET /recordings/ready and add the finished meetings you don’t have yet. The list holds READY meetings 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: 404 means 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 with recordings:write alone has no list: ask the status of each meeting it sent that you are still waiting on.
  3. 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.