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.
What the token can do
Section titled “What the token can do”- Send meetings:
POST /recordings. Each one spends the meeting’s minutes, so give the token a daily cap. - Follow the meetings it sent: their status,
GET /recordings/{id}/status, and their events inGET /events, where it sees only the meetings it sent. - Act on the meetings it sent: skip naming and retry a failed meeting. See The meeting lifecycle.
- Check itself with
GET /whoami, which answers every token.
What it can’t
Section titled “What it can’t”- 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 needrecordings:query: each answers403 TOKEN_SCOPE. bundleUrlin its answers is alwaysnull.
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.
The flow
Section titled “The flow”- Send the recording, with a new
Idempotency-Keyfor each file (kept for every retry of it: see Idempotency and retries). AddrecordedAt, when the meeting happened, if the source knows it, and atitle. KeepidandstatusUrlfrom the202. - Wait about a minute between status reads, or read the feed: see Polling and the events feed.
- At
NAMING, sendnaming.urlto the person, in the same Slack thread for example, and go on waiting. Or skip naming:POST …/namingwith{ "action": "skip" }, or send the meeting withnaming=skipin the first place. - At
READY, send the personurl: the meeting, and its Lobbe, in the app. - At
FAILED, retry iferror.retryableistrue, and go on waiting: Lobbe retries a failed step by itself too, so aFAILEDcan be temporary, and your retry never starts a second run (409 NOT_FAILEDmeans the meeting has already recovered). After a few retries, or whenerror.retryableisfalse, tell the person.
In n8n
Section titled “In n8n”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.
With curl
Section titled “With curl”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):
key=$(uuidgen) # one key for this file, kept for every retry of itbody=$(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=0while [ -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 60doneWith 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.