{
  "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
              }
            }
          }
        }
      }
    }
  }
}
