Skip to content

A submit-only flow (n8n)

Some tools only need to hand meetings to Lobbe. An n8n workflow, say, picks up a recording, a file someone shares in Slack or the recording of an online meeting, and sends it. The person then reads the Lobbe in the app; the workflow never has to.

Give such a tool a token with Send meetings alone: recordings:write, without recordings:read. If it ever leaks, it can spend minutes, but it can’t read any meeting’s transcript or Lobbe.

Like every token, it works only while Developer mode is on. While it is off, every request answers 403 DEVELOPER_MODE_OFF until it is turned back on: see Tokens and scopes. A workflow that runs for months should tell the person when it meets that code, not retry it in a loop.

  • Any other meeting: one recorded in the app, or sent by another token, answers 404, as if it didn’t exist.
  • The transcript and the Lobbe of any meeting, its own included: the status of a meeting it sent carries the title, and nothing more. The bundle and the list of finished meetings need recordings:read, and search and Ask need recordings:query: each answers 403 TOKEN_SCOPE.
  • bundleUrl in its answers is always null.

So the workflow passes on links, never the meeting itself: naming.url while the meeting waits for names, then url once it is ready. Neither is a public link: each opens the meeting in the app, and only for someone signed in who can already see it.

  1. Send the recording, with a new Idempotency-Key for each file (kept for every retry of it: see Idempotency and retries). Add recordedAt, when the meeting happened, if the source knows it, and a title. Keep id and statusUrl from the 202.
  2. Wait about a minute between status reads, or read the feed: see Polling and the events feed.
  3. At NAMING, send naming.url to the person, in the same Slack thread for example, and go on waiting. Or skip naming: POST …/naming with { "action": "skip" }, or send the meeting with naming=skip in the first place.
  4. At READY, send the person url: the meeting, and its Lobbe, in the app.
  5. At FAILED, retry if error.retryable is true, and go on waiting: Lobbe retries a failed step by itself too, so a FAILED can be temporary, and your retry never starts a second run (409 NOT_FAILED means the meeting has already recovered). After a few retries, or when error.retryable is false, tell the person.

An HTTP Request node sends the meeting:

HTTP Request node
Method: POST
URL: https://app.lobbe.ai/api/v1/recordings
Credential (Header Auth): Authorization = Bearer <your token>
Body: Form-Data, field audio = the meeting's file
Then poll the statusUrl it answers with, about once a minute.

The file goes in the audio field with its type. A 400 VALIDATION that says Unsupported audio type means it went without one: see Upload limits. Treat the token like a password: see Keep it safe.

The n8n recipe sets this node up step by step, with the token in a Header Auth credential rather than in the node, and adds the loop that waits for the meeting.

The same flow from a shell. It sends the meeting, prints the naming link once if the meeting waits for names, retries a failure that can recover (up to three times), and stops at READY, at a failure it can’t retry, or on any error, printing it (it needs jq and uuidgen):

Terminal window
key=$(uuidgen) # one key for this file, kept for every retry of it
body=$(curl -sS --fail-with-body https://app.lobbe.ai/api/v1/recordings \
-H "Authorization: Bearer $LOBBE_TOKEN" \
-H "Idempotency-Key: $key" \
-F "audio=@meeting.m4a;type=audio/mp4" \
-F "title=Weekly product sync" \
-F "recordedAt=2026-10-19T10:30:00+03:30")
status_url=$(jq -r '.recording.statusUrl // empty' <<<"$body")
[ -n "$status_url" ] || echo "$body"
told=""
retries=0
while [ -n "$status_url" ]; do
body=$(curl -sS --fail-with-body "$status_url" \
-H "Authorization: Bearer $LOBBE_TOKEN") || { echo "$body"; break; }
status=$(jq -r .status <<<"$body")
if [ "$status" = NAMING ] && [ -z "$told" ]; then
echo "Name the speakers: $(jq -r .naming.url <<<"$body")"
told=yes
fi
if [ "$status" = READY ]; then
echo "The Lobbe is ready: $(jq -r .url <<<"$body")"
break
fi
if [ "$status" = FAILED ]; then
if [ "$(jq -r .error.retryable <<<"$body")" != true ] || [ "$retries" -ge 3 ]; then
echo "It failed: $(jq -c .error <<<"$body")"
break
fi
retries=$((retries + 1))
# 409 NOT_FAILED: it recovered by itself in the meantime. Keep waiting either way.
body=$(curl -sS --fail-with-body -X POST "${status_url%/status}/retry" \
-H "Authorization: Bearer $LOBBE_TOKEN") ||
[ "$(jq -r .error.code <<<"$body" 2>/dev/null)" = NOT_FAILED ] || { echo "$body"; break; }
fi
sleep 60
done

With Name speakers set to Ask, a meeting can wait for its names for hours. Reading its status once a minute all that time stays far inside the token’s budget.