Every API here is available over the APIs.io API and to AI agents over MCP.
{
"openapi": "3.1.0",
"info": {
"title": "Bike4Mind API",
"version": "21.0.0",
"description": "Programmatic access to the Bike4Mind API. Schemas are generated from the same Zod definitions that validate requests at runtime, so this spec cannot drift from the implementation - with one documented exception: a `pollResult` schema (see the `x-poll-result` extension) describes a handoff response's outcome on a plain, non-contract endpoint, so it is hand-maintained against that handler rather than runtime-validated, and can drift if the handler changes without a matching schema update.\n\n## Getting started\n1. Create a key in the web app under **Profile > API Keys** (pick a scope preset).\n2. Make a first call: `GET /api/v1/me` with `Authorization: Bearer b4m_live_<key>`. It needs only the `me:read` scope, spends no credits, and returns your identity, plan tier and credit balance.\n3. Read the rest of this page for the behaviour shared across endpoints: rate limits, credits, errors and async jobs.\n\nEndpoints are grouped by tag: **AI** (chat, completions, embeddings, agent runs, tools, quest polling), **Images**, **Audio**, **Videos**, **Music**, **Transcriptions**, **Sessions**, **Artifacts**, **Files**, **Projects**, **Agents**, **Data Lakes**, **Skills** and **Account**.\n\n## Authentication\nSend an API key as `Authorization: Bearer b4m_live_<key>` (canonical), `x-api-key: b4m_live_<key>` (legacy), or `Authorization: ApiKey b4m_live_<key>`. A JWT access token is also accepted in the `Authorization: Bearer` header.\n\n## Scopes\nAPI keys carry `resource:action` scopes. The documented scopes are:\n- `notebooks:read`\n- `notebooks:write`\n- `files:read`\n- `files:write`\n- `ai:generate`\n- `ai:decide`\n- `ai:chat`\n- `projects:read`\n- `projects:write`\n- `agents:read`\n- `agents:write`\n- `me:read`\n- `marketing-reports:read`\n- `marketing-reports:write`\n- `hearth:read`\n- `hearth:write`\n- `datalake:read`\n- `datalake:write`\n- `datalake:share`\n- `datalake:query`\n- `overwatch:read`\n- `skills:read`\n- `skills:write`\n- `skills:review`\n- `bob:read`\n- `bob:run`\n- `slack:post`\n\nPer-operation required scopes are published via the `x-required-scopes` extension. Semantics are OR: a key needs ANY ONE of an operation's listed scopes, not all of them. Operations with no `x-required-scopes` enforce no scope (e.g. the JWT-only tools endpoint).\n\n## Rate limits\nEach API key has a per-minute and a per-day request ceiling, by default 60 requests/minute and 1000 requests/day (a key can be minted with its own ceilings). Responses from rate-limited operations carry the current state of both windows (the streaming completions endpoint sends them only on an API-key `429`):\n- `X-RateLimit-Limit-Minute`\n- `X-RateLimit-Remaining-Minute`\n- `X-RateLimit-Reset-Minute`\n- `X-RateLimit-Limit-Day`\n- `X-RateLimit-Remaining-Day`\n- `X-RateLimit-Reset-Day`\n\nReset values are Unix epoch seconds. Exceeding a ceiling returns `429` with a `Retry-After` header; wait that long before retrying. This includes `POST /api/ai/v1/completions`, which rate-limits before it opens its event stream. `GET /api/v1/me`, `GET /api/v1/credits`, `GET /api/v1/models`, `GET /api/v1/video-models`, `GET /api/v1/music-models`, the `GET` list endpoints for `/api/v1/sessions`, `/api/v1/video-generations` and `/api/v1/music-generations`, and the poll endpoints listed under Async jobs are exempt from the per-day ceiling: a poll consumes no daily slot, and only the per-minute limit applies. A request refused at the route scope gate (`403`) or for a bad key (`401`) never reaches the limiter and carries no rate-limit headers.\n\n## Credits\nGeneration endpoints spend credits from the balance shown at `credits.balance` on `GET /api/v1/me` (the caller's personal ledger). A key without `me:read` reads the same number from `GET /api/v1/credits`, which accepts `ai:chat`, `ai:generate` and `ai:decide` too. A synchronous call that cannot be paid for fails with `422` and `errorCode: \"insufficient_credits\"`. On a queued job the same code arrives on the polled result instead (see Async jobs), so check both places.\n\n## Errors\nError bodies use one envelope, the `ErrorResponse` schema: `{ \"error\": string, \"request_id\"?: string }`, plus an `errorCode` on failures a client is expected to branch on (`insufficient_credits`, `spend_cap_exceeded`, `provider_not_configured`, ...). A deprecated `name` field may also appear until its sunset date; do not rely on it. Branch on the HTTP status and `errorCode`, never on the `error` prose. A few operations declare a bespoke error shape for a specific status (e.g. the `500` of the tools endpoint and the `413` of text-to-speech); each operation lists the statuses it can return and the body for each.\n\nThe shared statuses: a malformed JSON body is `400`, a body that fails schema validation is `422`, a missing or invalid credential is `401`, a valid key without the required scope is `403`, an unknown resource is `404` and an exceeded rate limit is `429`. A `401` carrying `errorCode: \"provider_rejected\"` means the upstream provider refused the provider key the request used - the platform key by default, or your own stored provider key when you configured one - so your API key itself was accepted.\n\n## Async jobs\nWork that is not provably fast is queued and polled rather than held open:\n- **Chat** (`POST /api/chat`) and **image generation/editing** (`POST /api/v1/image-generations`, `POST /api/v1/image-edits`) return a quest. Poll `GET /api/v1/quests/{id}` until `status` is `done` or `stopped`; chat text arrives in `reply`, generated images in `images`, edited images in `files[].url`. Check for failure before reading results: a job that ends `done` with `type: \"error\"` failed (the reason is in `reply`), and a job that ends `stopped` also failed even when `type` is not `\"error\"` (its `reply` carries an explanation, not an answer). The outcome schema of each of these quest handoffs is linked from its response via the `x-poll-result` extension. Chat also accepts `wait: true` to block until the reply is ready.\n- **Agent runs** (`POST /api/v1/agent-executions`) return `202`. Poll `GET /api/v1/agent-executions/{id}` until `status` is `completed`, `failed` or `aborted`.\n- **Video generation** (`POST /api/v1/video-generations`) returns `202`. Poll `GET /api/v1/video-generations/{id}` until `state` is `succeeded`, `failed`, `blocked` or `cancelled`.\n- **Music generation** (`POST /api/v1/music-generations`) returns `202`. Poll `GET /api/v1/music-generations/{id}` until `state` is `succeeded`, `failed`, `blocked` or `cancelled`.\n- **File uploads** (`POST /api/v1/files`) return a presigned `upload_url`; after the `PUT`, poll `GET /api/v1/files/{id}` until `moderation_status` is `clean` before passing the id elsewhere.\n- **Audio** endpoints are synchronous and return the result directly.\n- **Transcriptions** (`POST /api/v1/transcriptions`) are synchronous too: after the upload, one call returns the text. Long audio can outlast the request timeout and fail with a `504`; see that endpoint for the limit.\n\nPoll with a backoff of a few seconds: each poll counts against the per-minute limit.\n\n## Versioning\nNew endpoints live under `/api/v1`. Published endpoints are forward-only: a live URL is never renamed or removed, and a breaking change ships as a new path while the old one keeps working. Nothing published is removed silently - a deprecated field or endpoint is marked `deprecated` in this spec with a sunset date in its description, and keeps working until that date.\n\n## CORS\nThe spec (`/api/v1/openapi.json`) is served publicly with permissive CORS. The API endpoints themselves are called server-to-server with a secret key and are not intended for browser CORS use.\n\n## Correlation\nEvery response carries an `X-Request-ID` header (echoed as `request_id`/`requestId` in bodies and events) for log correlation.",
"contact": {
"name": "Bike4Mind",
"url": "https://your-deployment.example.com"
},
"license": {
"name": "Proprietary"
}
},
"servers": [
{
"url": "https://your-deployment.example.com",
"description": "Production"
},
{
"url": "https://staging.your-deployment.example.com",
"description": "Staging"
},
{
"url": "http://localhost:3000",
"description": "Local dev"
}
],
"components": {
"securitySchemes": {
"bearerAuth": {
"type": "http",
"scheme": "bearer",
"description": "Canonical auth. Send `Authorization: Bearer b4m_live_<key>`. A JWT access token is also accepted in this header; a token beginning with `b4m_` is treated as an API key, anything else as a JWT."
},
"apiKeyAuth": {
"type": "apiKey",
"in": "header",
"name": "x-api-key",
"description": "Legacy auth. Send `x-api-key: b4m_live_<key>`. Equivalent to `bearerAuth` for API keys."
},
"jwtAuth": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT",
"description": "JWT access token only. Send `Authorization: Bearer <access_token>`. API keys are NOT accepted here."
}
},
"schemas": {
"ErrorResponse": {
"type": "object",
"properties": {
"error": {
"type": "string",
"description": "Human-readable error message."
},
"request_id": {
"type": "string",
"description": "Correlation id, mirrors the X-Request-ID header."
},
"name": {
"type": "string",
"deprecated": true,
"description": "Deprecated, sunset 2026-12-01. The `name` of whatever the server threw - usually one of our own error classes (`NotFoundError`, `UnprocessableEntityError`, ...), but on an unhandled 500 it can be a library or driver class name instead. Unstable by nature: these are internal names we rename at will, and the set is open. Documented only so the wire and this spec agree for the deprecation window; it is removed on the sunset date. Branch on the HTTP status, not on this."
}
},
"required": [
"error"
],
"example": {
"error": "Missing or invalid toolName",
"request_id": "abc-123"
}
},
"sendChatMessage200PollResult": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"status": {
"type": "string",
"enum": [
"pending",
"stopped",
"running",
"done"
]
},
"type": {
"type": "string",
"enum": [
"message",
"oob",
"error",
"system",
"voice_transcript"
]
},
"errorCode": {
"type": "string",
"enum": [
"insufficient_credits",
"spend_cap_exceeded"
]
},
"reply": {
"type": [
"string",
"null"
]
},
"replies": {
"type": "array",
"items": {
"type": "string"
}
},
"fallbackInfo": {
"type": "object",
"properties": {
"primaryModel": {
"type": "string"
},
"primaryModelName": {
"type": "string"
},
"fallbackModel": {
"type": "string"
},
"fallbackModelName": {
"type": "string"
},
"reason": {
"type": "string"
}
},
"required": [
"primaryModel",
"primaryModelName",
"fallbackModel",
"fallbackModelName"
]
}
},
"required": [
"id"
],
"description": "Outcome fields of the quest polled at `GET /api/v1/quests/{id}` after this ACK. A finished turn that failed is `status: \"done\"` with `type: \"error\"` and the failure text in `reply`, so a caller reading `reply` alone cannot tell a failure from an answer - check `type` first, and also treat a terminal `status: \"stopped\"` (a missing session, a user-cancelled turn) as failure even though it never sets `type`. `errorCode` is an optional refinement of `type: \"error\"`, present only for a classified billing failure; credit exhaustion arrives here as `insufficient_credits`, the same vocabulary the synchronous 422s on `/api/ai/music`, `/api/ai/sound-effects` and `/api/ai/tts` use. `QUEST_ERROR_CODES` publishes a second member, `spend_cap_exceeded`, but no current throw site on this endpoint raises it as a quest errorCode: its only one, the embed chat route's pre-flight 422, fires outside the process try/catch that would classify it onto the quest. Most `type: \"error\"` turns - an abort, a provider timeout or overload - have NO `errorCode`; its absence does not mean success, only that the failure is unclassified. A recovered stuck quest that still has renderable content is not a failure at all: it keeps `type: \"message\"` even though it did not finish, so a caller gets the content rather than an error. `fallbackInfo` is present only when the requested model failed and another model produced the reply: `fallbackModel` answered, `primaryModel` is what was asked for, and `reason` is the error from the first failed attempt on the requested model. The poll body carries further fields not modelled here, including `images`, `files`, `toolPayloads`, `promptMeta`, and the attachment report (`attachmentNotices`/`attachmentDelivery`) - only the outcome subset is modelled here.",
"example": {
"id": "664f1c2b9a1e4d0012ab34cd",
"status": "done",
"type": "error",
"errorCode": "insufficient_credits",
"reply": "You're out of credits. This request needs about 12 credits, but only 3 are available."
}
},
"generateImage200PollResult": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"status": {
"type": "string",
"enum": [
"pending",
"stopped",
"running",
"done"
]
},
"type": {
"type": "string",
"enum": [
"message",
"oob",
"error",
"system",
"voice_transcript"
]
},
"errorCode": {
"type": "string",
"enum": [
"insufficient_credits",
"spend_cap_exceeded"
]
},
"reply": {
"type": [
"string",
"null"
]
},
"replies": {
"type": "array",
"items": {
"type": "string"
}
},
"fallbackInfo": {
"type": "object",
"properties": {
"primaryModel": {
"type": "string"
},
"primaryModelName": {
"type": "string"
},
"fallbackModel": {
"type": "string"
},
"fallbackModelName": {
"type": "string"
},
"reason": {
"type": "string"
}
},
"required": [
"primaryModel",
"primaryModelName",
"fallbackModel",
"fallbackModelName"
]
},
"sessionId": {
"type": "string"
},
"images": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"id",
"sessionId"
],
"description": "The quest polled at `GET /api/v1/quests/{id}` after this ACK. The render is finished once `status` is `\"done\"`: on success `images` holds the rendered images; a render that FAILED is `type: \"error\"` with the reason in `reply`, never a 4xx. That covers credit exhaustion (tagged `errorCode: \"insufficient_credits\"`, the same vocabulary as the synchronous 422s on the audio endpoints), a provider error, a reference image that is missing, not an image, or still in moderation, and the combined FLUX.2 input-image limit after attachments are resolved. Check `type` before reading `images`; `errorCode` is absent on unclassified failures, so its absence does not mean success.",
"example": {
"id": "664f1c2b9a1e4d0012ab34cd",
"sessionId": "664f1c2b9a1e4d0012ab34aa",
"status": "done",
"type": "message",
"images": [
"3f6c1a52-9d1e-4b7a-8c2f-5e0d4a9b1c7e.png"
]
}
},
"editImage200PollResult": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"status": {
"type": "string",
"enum": [
"pending",
"stopped",
"running",
"done"
]
},
"type": {
"type": "string",
"enum": [
"message",
"oob",
"error",
"system",
"voice_transcript"
]
},
"errorCode": {
"type": "string",
"enum": [
"insufficient_credits",
"spend_cap_exceeded"
]
},
"reply": {
"type": [
"string",
"null"
]
},
"replies": {
"type": "array",
"items": {
"type": "string"
}
},
"fallbackInfo": {
"type": "object",
"properties": {
"primaryModel": {
"type": "string"
},
"primaryModelName": {
"type": "string"
},
"fallbackModel": {
"type": "string"
},
"fallbackModelName": {
"type": "string"
},
"reason": {
"type": "string"
}
},
"required": [
"primaryModel",
"primaryModelName",
"fallbackModel",
"fallbackModelName"
]
},
"sessionId": {
"type": "string"
},
"images": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"id",
"sessionId"
],
"description": "The quest polled at `GET /api/v1/quests/{id}` after this ACK. The render is finished once `status` is `\"done\"`: on success `images` holds the rendered images; a render that FAILED is `type: \"error\"` with the reason in `reply`, never a 4xx. That covers credit exhaustion (tagged `errorCode: \"insufficient_credits\"`, the same vocabulary as the synchronous 422s on the audio endpoints), a provider error, a reference image that is missing, not an image, or still in moderation, and the combined FLUX.2 input-image limit after attachments are resolved. Check `type` before reading `images`; `errorCode` is absent on unclassified failures, so its absence does not mean success.",
"example": {
"id": "664f1c2b9a1e4d0012ab34cd",
"sessionId": "664f1c2b9a1e4d0012ab34aa",
"status": "done",
"type": "message",
"images": [
"3f6c1a52-9d1e-4b7a-8c2f-5e0d4a9b1c7e.png"
]
}
},
"sendChatMessageResponse200": {
"anyOf": [
{
"type": "object",
"properties": {
"id": {
"type": "string"
},
"status": {
"type": "string"
},
"message_received": {
"type": "boolean"
},
"timestamp": {
"type": "string"
},
"model": {
"type": "string"
},
"sessionId": {
"type": "string"
},
"message": {
"type": "string"
},
"type": {
"type": "string",
"enum": [
"message",
"oob",
"error",
"system",
"voice_transcript"
]
},
"errorCode": {
"type": "string",
"enum": [
"insufficient_credits",
"spend_cap_exceeded"
]
},
"tools": {
"type": "object",
"properties": {
"toolMode": {
"type": "string",
"enum": [
"fast",
"smart"
]
},
"autoSelectedTools": {
"type": "array",
"items": {
"type": "string"
}
},
"effectiveTools": {
"type": "array",
"items": {
"type": "string"
}
},
"unrecognizedTools": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"effectiveTools"
]
},
"tracking_info": {
"type": "object",
"properties": {
"quest_id": {
"type": "string"
},
"check_status_url": {
"type": "string"
},
"poll_url": {
"type": "string"
}
},
"required": [
"quest_id",
"check_status_url"
]
},
"response": {
"type": [
"string",
"null"
]
},
"responses": {
"type": "array",
"items": {
"type": "string"
}
},
"toolPayloads": {
"type": "array",
"items": {
"type": "object",
"properties": {
"type": {
"type": "string"
},
"payload": {}
},
"required": [
"type"
]
}
},
"createdAt": {
"anyOf": [
{
"type": "string",
"format": "date-time"
},
{
"type": "string",
"format": "date-time"
}
]
},
"promptDetails": {
"type": "array",
"items": {
"type": "object",
"properties": {
"source": {
"type": "string",
"enum": [
"hardcoded",
"admin",
"user",
"project",
"session",
"org",
"caller"
]
},
"name": {
"type": "string"
},
"tokenCount": {
"type": "number"
},
"wasIncluded": {
"type": "boolean"
},
"exclusionReason": {
"type": "string",
"enum": [
"duplicate",
"disabled",
"token_limit"
]
}
},
"required": [
"source",
"name",
"tokenCount",
"wasIncluded"
]
}
},
"promptText": {
"type": "object",
"properties": {
"blocks": {
"type": "array",
"items": {
"type": "object",
"properties": {
"source": {
"type": "string",
"enum": [
"hardcoded",
"admin",
"user",
"project",
"session",
"org",
"caller"
]
},
"name": {
"type": "string"
},
"text": {
"type": "string"
},
"redacted": {
"type": "boolean"
},
"placement": {
"type": "string",
"enum": [
"system",
"user_turn",
"mixed"
]
}
},
"required": [
"source",
"name",
"redacted",
"placement"
]
}
},
"sizeCapped": {
"type": "boolean"
}
},
"required": [
"blocks",
"sizeCapped"
]
},
"performance": {
"type": "object",
"properties": {
"total_ms": {
"type": "number"
},
"phases": {
"type": "object",
"additionalProperties": {
"type": "number"
}
},
"pipeline_phases": {
"type": "object",
"additionalProperties": {
"type": "number"
}
}
},
"required": [
"total_ms",
"phases"
]
}
},
"required": [
"id",
"status",
"message_received",
"timestamp",
"model",
"type",
"tracking_info",
"response",
"responses",
"toolPayloads",
"createdAt",
"performance"
]
},
{
"type": "object",
"properties": {
"id": {
"type": "string"
},
"status": {
"type": "string"
},
"message_received": {
"type": "boolean"
},
"timestamp": {
"type": "string"
},
"model": {
"type": "string"
},
"sessionId": {
"type": "string"
},
"message": {
"type": "string"
},
"type": {
"type": "string",
"enum": [
"message",
"oob",
"error",
"system",
"voice_transcript"
]
},
"errorCode": {
"type": "string",
"enum": [
"insufficient_credits",
"spend_cap_exceeded"
]
},
"tools": {
"type": "object",
"properties": {
"toolMode": {
"type": "string",
"enum": [
"fast",
"smart"
]
},
"autoSelectedTools": {
"type": "array",
"items": {
"type": "string"
}
},
"effectiveTools": {
"type": "array",
"items": {
"type": "string"
}
},
"unrecognizedTools": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"effectiveTools"
]
},
"tracking_info": {
"type": "object",
"properties": {
"quest_id": {
"type": "string"
},
"check_status_url": {
"type": "string"
},
"poll_url": {
"type": "string"
}
},
"required": [
"quest_id",
"check_status_url"
]
}
},
"required": [
"id",
"status",
"message_received",
"timestamp",
"model",
"tracking_info"
]
}
]
},
"ScopeForbiddenResponse": {
"type": "object",
"properties": {
"error": {
"type": "string"
},
"request_id": {
"type": "string"
},
"name": {
"type": "string",
"deprecated": true,
"description": "Deprecated, sunset 2026-12-01. The `name` of whatever the server threw - usually one of our own error classes (`NotFoundError`, `UnprocessableEntityError`, ...), but on an unhandled 500 it can be a library or driver class name instead. Unstable by nature: these are internal names we rename at will, and the set is open. Documented only so the wire and this spec agree for the deprecation window; it is removed on the sunset date. Branch on the HTTP status, not on this."
},
"required_scopes": {
"type": "array",
"items": {
"type": "string"
},
"description": "API key scopes this route accepts. Holding any one of them satisfies it."
},
"also_required_scopes": {
"type": "array",
"items": {
"type": "string"
},
"description": "API key scopes this route also requires. Every one must be held, on top of `required_scopes`."
}
},
"required": [
"error"
]
},
"sendChatMessageResponse422": {
"type": "object",
"properties": {
"error": {
"type": "string"
},
"request_id": {
"type": "string"
},
"name": {
"type": "string",
"deprecated": true,
"description": "Deprecated, sunset 2026-12-01. The `name` of whatever the server threw - usually
# --- truncated at 32 KB (1699 KB total) ---
# Full source: https://artifacts.apis.io/bike4mind-com/openapi/bike4mind-com-openapi.json