{"openapi":"3.1.0","info":{"title":"LetBe.ai Public API","version":"1.0.0","summary":"Run hosted AI scenarios (flows) from your own code.","description":"LetBe.ai hosts AI scenarios you build visually in the portal. Publish a scenario, then drive a whole conversation with one endpoint: POST /api/v1/chat/next. Every response returns an opaque `state` string; send it back on the next call. Audio and file inputs use the multipart variants of the same endpoint.\n\n**Authentication.** Every `/api/v1` endpoint except this document takes an account-owned API key: `Authorization: Bearer lb_...`. Keys are created self-serve on the portal's Developer Access page (https://portal.letbe.ai/developer). Apps where each end user connects their own LetBe.ai account use OAuth 2.0 authorization code with PKCE (S256); the token exchange returns an API key scoped to the user's grant. Server metadata: https://letbe.ai/.well-known/oauth-authorization-server.\n\n**Errors.** Every error is JSON: `{ \"code\": string, \"message\": string, \"hint\"?: string, \"docs\"?: string }`. `code` is stable and safe to branch on; `message` is for humans. Out-of-funds is HTTP 402 with `walletLimit: true`. Unknown routes are HTTP 404 with `code: not_found`.\n\n**Rate limits.** Upload and delete endpoints are throttled per client IP and answer with the IETF `RateLimit` and `RateLimit-Policy` headers on every response; when exhausted they return HTTP 429 with `Retry-After` (seconds). Other endpoints are limited by the account's prepaid wallet rather than by request rate. Abusive traffic may also be answered at the edge with 429 or 403; always honor `Retry-After` when present.\n\n**Billing.** Usage is billed to the wallet of the account that owns the API key, per model and processing type. 1 USD = 1,000 credits. New accounts start with free credits, so an agent can complete the whole onboarding without human contact.","termsOfService":"https://letbe.ai/terms","contact":{"name":"LetBe.ai support","email":"support@letbe.ai","url":"https://letbe.ai/contact"}},"externalDocs":{"description":"Developer documentation","url":"https://letbe.ai/docs/"},"servers":[{"url":"https://letbe.ai/api","description":"Production"}],"tags":[{"name":"Flows","description":"Run published scenarios step by step."},{"name":"Knowledge","description":"Manage the retrieval collections and files a scenario answers from."},{"name":"OAuth","description":"Connect an end user's LetBe.ai account to your app."},{"name":"Meta","description":"Discovery and health."}],"security":[{"ApiKeyAuth":[]}],"paths":{"/v1/openapi.json":{"get":{"tags":["Meta"],"operationId":"getOpenApiSpec","summary":"This OpenAPI description","description":"Returns this document. No authentication. Also published statically at https://letbe.ai/openapi.json.","security":[],"responses":{"200":{"description":"The OpenAPI 3.1 document.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/v1/ping":{"get":{"tags":["Meta"],"operationId":"ping","summary":"Verify an API key","description":"Cheapest call that exercises authentication. Returns `{ data: { ok: true } }` when the key is valid. Deliberately echoes no account identifiers.","responses":{"200":{"description":"The key is valid.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean","const":true}}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/v1/chat/next":{"post":{"tags":["Flows"],"operationId":"runFlowStep","summary":"Run the next step of a published scenario","description":"Drives a conversation with a published scenario (flow). Call it with an empty `state` to start; every response carries a new opaque `state` to send back on the following call together with the user's next `userInput`. `expectingUserInput` tells you whether the scenario is waiting for input, `expectingInputType` what kind, and `done` whether the run has finished. Alternatively pass `conversationId` and let the platform keep the state for you.\n\nHTTP 201 is returned on every successful step (not only the first).","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlowStepRequest"}}}},"responses":{"201":{"$ref":"#/components/responses/FlowStep"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/WalletLimit"},"404":{"$ref":"#/components/responses/FlowNotFound"}}}},"/v1/chat/next/audio":{"post":{"tags":["Flows"],"operationId":"runFlowStepWithAudio","summary":"Run the next step with spoken input","description":"Same contract as `runFlowStep`, but the user's input is an audio file (multipart field `audio`). The platform transcribes it and continues the flow in one call, so a scenario step that expects speech needs no separate speech-to-text request. Use `windowSeconds` (1-120, default 60) to tell the platform how long the clip is when you stream fixed windows.","requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/FlowStepAudioRequest"}}}},"responses":{"201":{"$ref":"#/components/responses/FlowStep"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/WalletLimit"},"404":{"$ref":"#/components/responses/FlowNotFound"},"413":{"$ref":"#/components/responses/PayloadTooLarge"}}}},"/v1/chat/next/file":{"post":{"tags":["Flows","Knowledge"],"operationId":"runFlowStepWithFile","summary":"Run the next step with a document attached","description":"Same contract as `runFlowStep`, with a document in the multipart field `file`. The scenario decides what happens to it: a RAG Ingest block validates it and queues it for retrieval indexing (poll `getRagFile` for status), an OCR block extracts its text immediately. There is deliberately no standalone upload endpoint, so every upload is billed and attributed to a scenario run.\n\nThrottled per client IP (see `RateLimit` headers).","requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/FlowStepFileRequest"}}}},"responses":{"201":{"$ref":"#/components/responses/FlowStep"},"400":{"$ref":"#/components/responses/BadRequest"},"401":{"$ref":"#/components/responses/Unauthorized"},"402":{"$ref":"#/components/responses/WalletLimit"},"404":{"$ref":"#/components/responses/FlowNotFound"},"413":{"$ref":"#/components/responses/PayloadTooLarge"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/rag/collections":{"get":{"tags":["Knowledge"],"operationId":"listRagCollections","summary":"List retrieval collections","description":"Returns the retrieval collections owned by the API key's account. A collection groups the documents a scenario's RAG Search block answers from.","responses":{"200":{"description":"Collections owned by this account.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/RagCollection"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"}}}},"/v1/rag/collections/{id}":{"delete":{"tags":["Knowledge"],"operationId":"deleteRagCollection","summary":"Delete a retrieval collection","description":"Deletes a collection and every file and chunk in it. Only collections owned by the API key's account are visible; others answer 404. Throttled per client IP (see `RateLimit` headers).","parameters":[{"$ref":"#/components/parameters/CollectionId"}],"responses":{"204":{"description":"Deleted."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/v1/rag/collections/{id}/files":{"get":{"tags":["Knowledge"],"operationId":"listRagCollectionFiles","summary":"List files in a collection","description":"Returns every file in the collection with its ingestion status.","parameters":[{"$ref":"#/components/parameters/CollectionId"}],"responses":{"200":{"description":"Files in the collection.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/RagFile"}}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}}},"/v1/rag/files/{id}":{"get":{"tags":["Knowledge"],"operationId":"getRagFile","summary":"Get a file's ingestion status","description":"Poll this after `runFlowStepWithFile` until `status` is `ready` (or `failed`, with `errorCode` and `errorMessage`). New files start as `pending`.","parameters":[{"$ref":"#/components/parameters/FileId"}],"responses":{"200":{"description":"The file and its status.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"$ref":"#/components/schemas/RagFile"}}}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"}}},"delete":{"tags":["Knowledge"],"operationId":"deleteRagFile","summary":"Delete a file from its collection","description":"Removes the file and its chunks. Throttled per client IP (see `RateLimit` headers).","parameters":[{"$ref":"#/components/parameters/FileId"}],"responses":{"204":{"description":"Deleted."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/TooManyRequests"}}}},"/oauth/scopes":{"get":{"tags":["OAuth"],"operationId":"listOAuthScopes","summary":"List OAuth scopes","description":"The scopes an app may request when a user connects their LetBe.ai account. Scopes marked `public: false` are reserved for first-party apps.","security":[],"responses":{"200":{"description":"Scope definitions.","content":{"application/json":{"schema":{"type":"object","required":["data"],"properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/OAuthScope"}}}}}}}}}},"/oauth/token":{"post":{"tags":["OAuth"],"operationId":"exchangeOAuthCode","summary":"Exchange an authorization code for an API key","description":"Second leg of the OAuth 2.0 authorization code flow. Send the user to `https://portal.letbe.ai/oauth/authorize` with `client_id`, `redirect_uri`, `scope`, `state`, `code_challenge` and `code_challenge_method=S256`; the portal redirects back with `code`. Exchange it here within 5 minutes. Public clients prove possession with `code_verifier` (PKCE); confidential clients may send `client_secret` instead.\n\nThe credential issued is a LetBe.ai API key limited to the granted scopes. It is returned both as the standard `access_token` (`token_type: Bearer`) and, for existing integrations, inside `data.apiKey`. Use it as `Authorization: Bearer ...` on every `/api/v1` call. The user can revoke it at any time from the portal.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthTokenRequest"}},"application/x-www-form-urlencoded":{"schema":{"$ref":"#/components/schemas/OAuthTokenRequest"}}}},"responses":{"200":{"description":"The code was valid; an API key was issued.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OAuthTokenResponse"}}}},"400":{"description":"Unsupported grant type, bad or expired code, PKCE mismatch, or unknown client.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"The account has reached its limit of connected apps or API keys.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"components":{"securitySchemes":{"ApiKeyAuth":{"type":"http","scheme":"bearer","bearerFormat":"lb_ API key","description":"Account-owned API key created on https://portal.letbe.ai/developer, or issued by the OAuth token exchange. Keys start with `lb_`."},"OAuth2":{"type":"oauth2","description":"Authorization code with PKCE (S256 only). The token exchange returns an API key that is then used as a Bearer token.","flows":{"authorizationCode":{"authorizationUrl":"https://portal.letbe.ai/oauth/authorize","tokenUrl":"https://letbe.ai/api/oauth/token","scopes":{"api_key:create":"Receive an app-specific API key after user authorization.","flows:run":"Execute published flow endpoints with the issued API key.","wallet:read":"Read the remaining wallet balance for display and preflight checks."}}}}},"parameters":{"CollectionId":{"name":"id","in":"path","required":true,"description":"Collection id (UUID).","schema":{"type":"string","format":"uuid"}},"FileId":{"name":"id","in":"path","required":true,"description":"File id (UUID).","schema":{"type":"string","format":"uuid"}}},"headers":{"RateLimit":{"description":"IETF RateLimit header (draft-7): `limit=<n>, remaining=<n>, reset=<seconds>`.","schema":{"type":"string"}},"RateLimit-Policy":{"description":"IETF RateLimit-Policy header (draft-7): `<limit>;w=<window seconds>`.","schema":{"type":"string"}},"Retry-After":{"description":"Seconds to wait before retrying.","schema":{"type":"integer","minimum":0}}},"responses":{"FlowStep":{"description":"The step ran. Returned with HTTP 201 on every successful step.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FlowStepResponse"}}}},"BadRequest":{"description":"Missing or invalid input, or the scenario step failed. `code` identifies the cause (for example `v1_1` when `flowId` is missing, `v1_img` for a bad image, `invalid_json` for an unparseable body).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"Unauthorized":{"description":"Missing, malformed (`api2`), or invalid/expired (`api3`) API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"WalletLimit":{"description":"The account's prepaid wallet cannot cover this step. Top up at https://portal.letbe.ai/wallet and retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/WalletLimitError"}}}},"FlowNotFound":{"description":"No published flow with that id, and none owned by this account (`v1_2`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"NotFound":{"description":"Nothing with that id is visible to this account.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"PayloadTooLarge":{"description":"The upload exceeds the size limit (audio 15 MB, documents 25 MB by default).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"TooManyRequests":{"description":"Per-IP throttle exhausted (`rl_limit`). Wait `Retry-After` seconds.","headers":{"Retry-After":{"$ref":"#/components/headers/Retry-After"},"RateLimit":{"$ref":"#/components/headers/RateLimit"},"RateLimit-Policy":{"$ref":"#/components/headers/RateLimit-Policy"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"schemas":{"Error":{"type":"object","description":"Every error response uses this envelope.","required":["code","message"],"properties":{"code":{"type":"string","description":"Stable machine-readable identifier. Branch on this, not on `message`."},"message":{"type":"string","description":"Human-readable explanation."},"hint":{"type":"string","description":"What to do next, when the platform can tell."},"docs":{"type":"string","format":"uri","description":"Where the API is described."}},"examples":[{"code":"api1","message":"API key required. Include Authorization header: \"Bearer <api-key>\""},{"code":"not_found","message":"No API route matches GET /api/v1/nope","hint":"Check the path and method against the OpenAPI description. Public endpoints live under /api/v1.","docs":"https://letbe.ai/openapi.json"}]},"WalletLimitError":{"type":"object","required":["walletLimit","message"],"properties":{"walletLimit":{"type":"boolean","const":true},"message":{"type":"string"},"ctaPath":{"type":"string","description":"Portal path where the account owner can add funds.","examples":["/wallet"]},"ctaLabel":{"type":"string","examples":["Add funds"]}}},"FlowStepRequest":{"type":"object","required":["flowId"],"properties":{"flowId":{"type":"string","description":"Id of a published scenario, or of a private scenario owned by the API key's account."},"state":{"type":"string","description":"Opaque state string from the previous response. Omit or send empty to start a new run."},"userInput":{"type":"string","description":"The user's input for this step, when the scenario expects one."},"conversationId":{"type":"string","description":"Server-managed memory: send the id from a previous response instead of `state` and the platform keeps the conversation. A stale id starts a fresh conversation."},"vars":{"type":"object","description":"Client variables the scenario can read (string values).","additionalProperties":{"type":"string"}},"images":{"type":"array","maxItems":4,"description":"Up to 4 pictures of what the caller is looking at, as `data:image/(jpeg|png|webp);base64,...` URLs of at most 1.5 MB each. Passed to the AI Model block when the model can see; never stored in state.","items":{"type":"string","pattern":"^data:image/(jpeg|png|webp);base64,"}}}},"FlowStepAudioRequest":{"type":"object","required":["flowId","audio"],"properties":{"flowId":{"type":"string"},"audio":{"type":"string","format":"binary","description":"Audio clip in any common container (webm is the default when no content type is sent). Max 15 MB by default."},"state":{"type":"string","description":"Opaque state string from the previous response."},"userInput":{"type":"string","description":"Optional text context passed alongside the transcript."},"windowSeconds":{"type":"integer","minimum":1,"maximum":120,"default":60,"description":"Length of the clip in seconds when streaming fixed windows."},"conversationId":{"type":"string"},"vars":{"type":"string","description":"JSON object of client variables, serialized as a string (multipart)."}}},"FlowStepFileRequest":{"type":"object","required":["flowId","file"],"properties":{"flowId":{"type":"string"},"file":{"type":"string","format":"binary","description":"Document for a RAG Ingest block (PDF, DOCX, TXT, MD, HTML) or the input an OCR block is configured to accept. Max 25 MB by default."},"state":{"type":"string"},"userInput":{"type":"string"},"conversationId":{"type":"string"},"vars":{"type":"string","description":"JSON object of client variables, serialized as a string (multipart)."}}},"FlowMessage":{"type":"object","required":["role","content"],"properties":{"role":{"type":"string","description":"`assistant`, `user`, or `system`."},"content":{"type":"string"}},"additionalProperties":true},"FlowStepResponse":{"type":"object","required":["data"],"properties":{"data":{"type":"object","required":["messages","state","expectingUserInput","done","creditsUsed"],"properties":{"messages":{"type":"array","items":{"$ref":"#/components/schemas/FlowMessage"},"description":"Messages produced by this step, in order."},"state":{"type":"string","description":"Opaque state. Send it back on the next call."},"stateToken":{"type":"string","description":"Deprecated alias of `state`."},"conversationId":{"type":["string","null"],"description":"Server-managed conversation id, when the platform is keeping state for you."},"expectingUserInput":{"type":"boolean","description":"True when the scenario is waiting for the user's next input."},"expectingInputType":{"type":["string","null"],"enum":["text","audio","file",null],"description":"What kind of input is expected next."},"done":{"type":"boolean","description":"True when the run has finished; the state cannot be continued."},"creditSnap":{"type":["object","null"],"description":"Snapshot of the account's remaining credits.","additionalProperties":true},"creditsUsed":{"type":"number","description":"What this step cost, in credits (1,000 credits = 1 USD)."}}}}},"RagCollection":{"type":"object","required":["id","name"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"embeddingModel":{"type":"string"},"chunkSizeDefault":{"type":"integer"},"chunkOverlapDefault":{"type":"integer"},"fileCount":{"type":"integer"},"totalSizeBytes":{"type":"integer"},"chunkCount":{"type":"integer"},"createdAt":{"type":"string","format":"date-time"}}},"RagFile":{"type":"object","required":["id","collectionId","name","status"],"properties":{"id":{"type":"string","format":"uuid"},"collectionId":{"type":"string","format":"uuid"},"name":{"type":"string"},"ext":{"type":"string"},"sizeBytes":{"type":"integer"},"status":{"type":"string","enum":["pending","processing","ready","failed"],"description":"Ingestion status."},"errorCode":{"type":["string","null"]},"errorMessage":{"type":["string","null"]},"chunkCount":{"type":"integer"},"createdAt":{"type":"string","format":"date-time"},"processedAt":{"type":["string","null"],"format":"date-time"}}},"OAuthScope":{"type":"object","required":["scope","label","description","public","risk"],"properties":{"scope":{"type":"string"},"label":{"type":"string"},"description":{"type":"string"},"public":{"type":"boolean","description":"False for scopes reserved to first-party apps."},"risk":{"type":"string","enum":["low","medium","high"]}}},"OAuthTokenRequest":{"type":"object","required":["grant_type","client_id","redirect_uri","code"],"properties":{"grant_type":{"type":"string","enum":["authorization_code"]},"client_id":{"type":"string"},"redirect_uri":{"type":"string","format":"uri","description":"Must match the redirect_uri used in the authorization request."},"code":{"type":"string"},"code_verifier":{"type":"string","description":"PKCE verifier (required for public clients)."},"client_secret":{"type":"string","description":"Confidential clients only."}}},"OAuthTokenResponse":{"type":"object","required":["access_token","token_type","scope","data"],"properties":{"access_token":{"type":"string","description":"The issued LetBe.ai API key (`lb_...`)."},"token_type":{"type":"string","const":"Bearer"},"scope":{"type":"string","description":"Space-separated granted scopes."},"data":{"type":"object","description":"Legacy envelope kept for existing integrations.","required":["apiKey","token_type","scope"],"properties":{"apiKey":{"type":"string"},"token_type":{"type":"string"},"tokenType":{"type":"string"},"scope":{"type":"string"},"apiKeyId":{"type":"string"},"client":{"type":"object","additionalProperties":true}}}}}}}}