{
  "openapi": "3.1.0",
  "info": {
    "title": "meetbot API",
    "version": "0.0.1",
    "summary": "HTTP API for the meetbot bot orchestrator.",
    "description": "The meetbot orchestrator's public HTTP API. Drop a meetbot bot into a\nGoogle Meet, Microsoft Teams, or Zoom meeting, get back a structured\nrecording manifest. Migrating from Recall.ai? The webhook events and\nfailure-code taxonomy are deliberately compatible \u2014 re-route, re-test,\nre-bill at $0.30/hr flat.\n\nSpec mirror: https://meetbot.dev/openapi.yaml (YAML),\nhttps://meetbot.dev/openapi.json (JSON).\nPostman collection: https://meetbot.dev/meetbot.postman_collection.json.\n",
    "termsOfService": "https://meetbot.dev/legal/terms",
    "contact": {
      "name": "meetbot support",
      "email": "hello@meetbot.dev",
      "url": "https://meetbot.dev"
    },
    "license": {
      "name": "MIT",
      "identifier": "MIT"
    }
  },
  "servers": [
    {
      "url": "https://api.meetbot.dev",
      "description": "Production"
    },
    {
      "url": "http://localhost:3010",
      "description": "Local orchestrator (default `bun src/server.ts` port)"
    }
  ],
  "tags": [
    {
      "name": "bots",
      "description": "Create, fetch, cancel, and control meeting-bot jobs."
    },
    {
      "name": "recordings",
      "description": "Manifest + media artifacts produced by completed jobs."
    },
    {
      "name": "calendar",
      "description": "Calendar-invite intake (used by the meetbot-email worker)."
    },
    {
      "name": "ingest",
      "description": "Bot \u2192 orchestrator lifecycle ingest. Documented for completeness; called by meetbot bot containers, not by customers."
    },
    {
      "name": "cli-auth",
      "description": "Device-flow auth endpoints used by the `meetbot` CLI."
    },
    {
      "name": "webhooks",
      "description": "Outbound webhook events the orchestrator sends to consumer-supplied URLs."
    },
    {
      "name": "admin",
      "description": "Operator endpoints, gated by a shared admin secret."
    }
  ],
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "paths": {
    "/api/v1/jobs": {
      "post": {
        "tags": ["bots"],
        "operationId": "createJob",
        "summary": "Dispatch a meeting bot",
        "description": "Schedule a meetbot bot to join a meeting URL. If `joinAt` is in\nthe past, the bot dispatches immediately; otherwise the\norchestrator sleeps the dispatch task until that wall-clock time.\n\nThe job's `externalId` is your idempotency key \u2014 POSTing twice\nwith the same `externalId` for the same consumer returns the\nexisting job rather than spinning up a duplicate bot.\n",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/DispatchBotInput"
              },
              "examples": {
                "minimal": {
                  "summary": "Minimum viable dispatch",
                  "value": {
                    "externalId": "lesson-2026-05-09-pavel",
                    "meetingUrl": "https://meet.google.com/abc-defg-hij"
                  }
                },
                "fullyConfigured": {
                  "summary": "Scheduled join with custom auto-leave + webhooks",
                  "value": {
                    "externalId": "q2-board-meeting-acme",
                    "meetingUrl": "https://meet.google.com/xyz-abcd-efg",
                    "platform": "meet",
                    "joinAt": "2026-05-15T14:00:00Z",
                    "displayName": "Acme Notetaker",
                    "webhooks": {
                      "onStatusChange": "https://api.acme.example/meetbot/status",
                      "onFinalize": "https://api.acme.example/meetbot/finalize"
                    },
                    "metadata": {
                      "customer_id": "cust_4711",
                      "workspace": "acme-prod"
                    },
                    "autoLeave": {
                      "afterEntryDelaySeconds": 300,
                      "afterMaxSeconds": 7200,
                      "onBotDetected": true
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Job created and queued for dispatch.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Job"
                },
                "example": {
                  "id": "5c3e9d8a-7c4e-4d11-b2cb-9d6e0c5e1234",
                  "externalId": "lesson-2026-05-09-pavel",
                  "status": "scheduled",
                  "meetingUrl": "https://meet.google.com/abc-defg-hij",
                  "platform": "meet",
                  "joinAt": "2026-05-09T13:00:00.000Z",
                  "attempts": 0,
                  "createdAt": "2026-05-09T12:59:59.124Z",
                  "startedAt": null,
                  "finalizedAt": null,
                  "manifestUri": null,
                  "failureCode": null,
                  "failureDetail": null,
                  "metadata": {},
                  "autoLeave": {
                    "afterEntryDelaySeconds": 600,
                    "afterSilenceSeconds": 1200,
                    "afterSoloSeconds": 60,
                    "afterMaxSeconds": 14400,
                    "onBotDetected": false
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidInput"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        },
        "callbacks": {
          "onStatusChange": {
            "{$request.body#/webhooks/onStatusChange}": {
              "post": {
                "summary": "Lifecycle status transition",
                "requestBody": {
                  "content": {
                    "application/json": {
                      "schema": {
                        "$ref": "#/components/schemas/JobStatusChangedEvent"
                      }
                    }
                  }
                },
                "responses": {
                  "2XX": {
                    "description": "Consumer ack \u2014 orchestrator stops retrying."
                  }
                }
              }
            }
          },
          "onFinalize": {
            "{$request.body#/webhooks/onFinalize}": {
              "post": {
                "summary": "Job finalized (manifest written)",
                "requestBody": {
                  "content": {
                    "application/json": {
                      "schema": {
                        "$ref": "#/components/schemas/JobFinalizedEvent"
                      }
                    }
                  }
                },
                "responses": {
                  "2XX": {
                    "description": "Consumer ack \u2014 orchestrator stops retrying."
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/jobs/{id}": {
      "parameters": [
        {
          "$ref": "#/components/parameters/JobId"
        }
      ],
      "get": {
        "tags": ["bots"],
        "operationId": "getJob",
        "summary": "Fetch a job",
        "description": "Returns the current state of a job. Scoped to the calling consumer.",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "200": {
            "description": "Job found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Job"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "delete": {
        "tags": ["bots"],
        "operationId": "cancelJob",
        "summary": "Cancel a job",
        "description": "Marks the job as `cancelled`. If the bot has already joined,\nthe orchestrator signals it to leave on its next heartbeat;\nif it hasn't dispatched yet, the queued dispatch is skipped.\n",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "204": {
            "description": "Cancelled. No body."
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/v1/jobs/{id}/request-recording-permission": {
      "parameters": [
        {
          "$ref": "#/components/parameters/JobId"
        }
      ],
      "post": {
        "tags": ["bots"],
        "operationId": "requestRecordingPermission",
        "summary": "Ask the host for explicit recording consent",
        "description": "Queues a bot-control verb that asks the meeting host to grant\nexplicit recording permission. The orchestrator returns\nimmediately with the queued command id. The actual host\ndecision (granted / denied / timeout) lands on the consumer's\nwebhook later as `bot.recording_permission_response`.\n\nIdempotent: re-issuing the verb while a previous one is still\nundispatched returns the existing command id.\n",
        "security": [
          {
            "bearerAuth": []
          }
        ],
        "responses": {
          "202": {
            "description": "Verb queued (or matched an undispatched verb).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CommandQueuedResponse"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "description": "Job is in a terminal/teardown state \u2014 there's no one to ask.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/ingest": {
      "post": {
        "tags": ["ingest"],
        "operationId": "postIngest",
        "summary": "Bot \u2192 orchestrator lifecycle ingest",
        "description": "Internal endpoint used by meetbot bot containers to report\nlifecycle events back to the orchestrator. Authenticated via\nper-job HMAC token (NOT a bearer API key). Documented here for\ncompleteness \u2014 customers don't call this directly.\n",
        "security": [
          {
            "bearerHmacAuth": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/IngestEvent"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Event accepted; state machine advanced.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "additionalProperties": true,
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidInput"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/api/v1/intake/calendar-invite": {
      "post": {
        "tags": ["calendar"],
        "operationId": "intakeCalendarInvite",
        "summary": "Receive a parsed calendar invite",
        "description": "Used by the meetbot-email Cloudflare Worker to forward parsed\ncalendar invites (after extracting the meeting URL from the\nICS attachment) to the orchestrator. Authenticated by a\nshared secret in `x-meetbot-intake-secret`.\n",
        "security": [
          {
            "intakeSecret": []
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CalendarInvite"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Existing job matched (idempotent dedupe).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IntakeResult"
                }
              }
            }
          },
          "201": {
            "description": "New job scheduled.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IntakeResult"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidInput"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          }
        }
      }
    },
    "/webhooks/stripe": {
      "post": {
        "tags": ["admin"],
        "operationId": "stripeWebhook",
        "summary": "Stripe webhook receiver (operator-only)",
        "description": "Receives Stripe events for subscription + invoice lifecycle.\nVerified via the `Stripe-Signature` header. Customers don't\ncall this directly \u2014 Stripe does. Documented for operators\nwiring up the Stripe webhook destination.\n",
        "security": [],
        "parameters": [
          {
            "name": "Stripe-Signature",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Event accepted (or deduped).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "deduped": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing or invalid signature."
          },
          "500": {
            "$ref": "#/components/responses/InternalError"
          },
          "503": {
            "description": "STRIPE_WEBHOOK_SECRET not configured on this orchestrator."
          }
        }
      }
    },
    "/api/v1/admin/host": {
      "get": {
        "tags": ["admin"],
        "operationId": "getAdminHost",
        "summary": "Host metrics + bot counts (operator-only)",
        "description": "Returns CPU/RAM/load/disk pulled from the local Netdata agent,\nplus active-job and scheduled-job counts. Auth via shared\nsecret in `x-meetbot-admin-secret`.\n",
        "security": [
          {
            "adminSecret": []
          }
        ],
        "responses": {
          "200": {
            "description": "Host metrics snapshot.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HostMetrics"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/v1/cli-auth/init": {
      "post": {
        "tags": ["cli-auth"],
        "operationId": "cliAuthInit",
        "summary": "Mint a device-flow nonce",
        "description": "First step of the `meetbot login` device flow. The CLI calls\nthis to get a nonce + a `verifyUrl` to open in the user's\nbrowser.\n",
        "security": [],
        "responses": {
          "201": {
            "description": "Nonce minted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["nonce", "verifyUrl", "expiresAt"],
                  "properties": {
                    "nonce": {
                      "type": "string"
                    },
                    "verifyUrl": {
                      "type": "string",
                      "format": "uri"
                    },
                    "expiresAt": {
                      "type": "string",
                      "format": "date-time"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/cli-auth/poll/{nonce}": {
      "parameters": [
        {
          "name": "nonce",
          "in": "path",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "tags": ["cli-auth"],
        "operationId": "cliAuthPoll",
        "summary": "Poll for device-flow completion",
        "description": "The CLI polls until `status` flips from `pending` to `complete`.\nReturns `expired` (HTTP 410) past TTL or for unknown nonces.\n",
        "security": [],
        "responses": {
          "200": {
            "description": "Pending or complete.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "required": ["status"],
                      "properties": {
                        "status": {
                          "type": "string",
                          "enum": ["pending"]
                        }
                      }
                    },
                    {
                      "type": "object",
                      "required": ["status", "apiKey"],
                      "properties": {
                        "status": {
                          "type": "string",
                          "enum": ["complete"]
                        },
                        "apiKey": {
                          "type": "string"
                        },
                        "email": {
                          "type": "string",
                          "format": "email"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "410": {
            "description": "Nonce expired or unknown.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": ["expired"]
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/cli-auth/complete": {
      "post": {
        "tags": ["cli-auth"],
        "operationId": "cliAuthComplete",
        "summary": "Finalize device-flow auth",
        "description": "Called by the authenticated web page once the user clicks\n\"authorize the CLI on this device\". Web mints a personal API\nkey and posts it here.\n",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["nonce", "apiKey"],
                "properties": {
                  "nonce": {
                    "type": "string"
                  },
                  "apiKey": {
                    "type": "string"
                  },
                  "email": {
                    "type": "string",
                    "format": "email"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Completed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/InvalidInput"
          },
          "410": {
            "description": "Nonce expired or unknown."
          }
        }
      }
    },
    "/healthz": {
      "get": {
        "tags": ["admin"],
        "operationId": "healthz",
        "summary": "Liveness probe",
        "description": "Returns \"ok\" when the orchestrator process is up.",
        "security": [],
        "responses": {
          "200": {
            "description": "Alive.",
            "content": {
              "text/plain": {
                "schema": {
                  "type": "string",
                  "example": "ok\n"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "meetbot API key (mb_live_\u2026 / mb_test_\u2026)",
        "description": "Send your meetbot API key as a Bearer token. Keys are scoped per\nconsumer; revoked keys 401 immediately. Issue + rotate keys in\nthe dashboard or via the admin API.\n"
      },
      "bearerHmacAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "Per-job HMAC-SHA256 of `<jobId>:<attempt>` keyed by\nMEETBOT_INGEST_SIGNING_KEY, hex-encoded. Used by bot\ncontainers; never issued to customers.\n"
      },
      "intakeSecret": {
        "type": "apiKey",
        "in": "header",
        "name": "x-meetbot-intake-secret",
        "description": "Shared secret between the orchestrator and the meetbot-email Cloudflare Worker."
      },
      "adminSecret": {
        "type": "apiKey",
        "in": "header",
        "name": "x-meetbot-admin-secret",
        "description": "Operator shared secret for /api/v1/admin/* endpoints."
      }
    },
    "parameters": {
      "JobId": {
        "name": "id",
        "in": "path",
        "required": true,
        "description": "UUIDv4 of the job, returned by `POST /api/v1/jobs`.",
        "schema": {
          "type": "string",
          "format": "uuid",
          "example": "5c3e9d8a-7c4e-4d11-b2cb-9d6e0c5e1234"
        }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing or invalid auth credentials.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "unauthorized"
            }
          }
        }
      },
      "NotFound": {
        "description": "Resource not found (or not visible to this consumer).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "not_found"
            }
          }
        }
      },
      "InvalidInput": {
        "description": "Request body failed schema validation.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "invalid_input",
              "issues": [
                {
                  "path": ["meetingUrl"],
                  "message": "Invalid url",
                  "code": "invalid_string"
                }
              ]
            }
          }
        }
      },
      "InternalError": {
        "description": "Unhandled server error. The orchestrator captured a Sentry event; include the response timestamp when reporting.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "error": "internal_server_error"
            }
          }
        }
      }
    },
    "schemas": {
      "Platform": {
        "type": "string",
        "enum": ["meet", "teams", "zoom-web", "zoom-sdk"],
        "description": "- `meet`     \u2014 Google Meet\n- `teams`    \u2014 Microsoft Teams\n- `zoom-web` \u2014 Zoom Web client (default for Zoom URLs today)\n- `zoom-sdk` \u2014 Zoom SDK (M2; not yet in production)\n"
      },
      "JobStatus": {
        "type": "string",
        "enum": [
          "pending",
          "scheduled",
          "dispatching",
          "joining",
          "entering_lobby",
          "in_meeting",
          "recording_started",
          "recording_paused",
          "recording_stopped",
          "permission_requested",
          "finalizing",
          "post_processing",
          "media_uploading",
          "completed",
          "failed",
          "cancelled"
        ],
        "description": "Granular bot lifecycle. Mirrors Attendee's 16-state taxonomy so a\nconsumer's automation logic (\"page when admitted\") can branch on\n`entering_lobby` \u2192 `in_meeting` as separate observable transitions.\n"
      },
      "FailureCode": {
        "type": "string",
        "enum": [
          "waiting_for_host",
          "meeting_not_found",
          "host_denied_admission",
          "removed_from_meeting",
          "meeting_ended",
          "internal_error",
          "dispatch_timeout",
          "network_loss",
          "recording_disabled",
          "lobby_timeout",
          "auth_failure",
          "quota_exceeded"
        ],
        "description": "Structured failure taxonomy. Recall.ai-compatible \u2014 switching\nfrom Recall to meetbot only requires routing on the same set of\ncodes. `null` on non-failed jobs.\n"
      },
      "AutoLeaveConfig": {
        "type": "object",
        "description": "Per-bot auto-leave knobs. Each fires INDEPENDENTLY in the bot's\nadapter loop \u2014 the bot leaves the moment ANY of them trips.\nMaps 1:1 onto Recall's `automatic_leave` config so a customer\nmigrating off Recall can keep their existing settings.\n",
        "properties": {
          "afterEntryDelaySeconds": {
            "type": "integer",
            "minimum": 1,
            "maximum": 86400,
            "default": 600,
            "description": "Leave if not admitted within N seconds of clicking Join."
          },
          "afterSilenceSeconds": {
            "type": "integer",
            "minimum": 1,
            "maximum": 86400,
            "default": 1200,
            "description": "Leave if no audio activity for N seconds."
          },
          "afterSoloSeconds": {
            "type": "integer",
            "minimum": 1,
            "maximum": 86400,
            "default": 60,
            "description": "Leave if the bot is the only participant for N seconds."
          },
          "afterMaxSeconds": {
            "type": "integer",
            "minimum": 1,
            "maximum": 86400,
            "default": 14400,
            "description": "Hard ceiling on meeting duration."
          },
          "onBotDetected": {
            "type": "boolean",
            "default": false,
            "description": "When TRUE, scan the participant roster for known competing-\nnotetaker display names (Otter, Fireflies, Read, \u2026) and leave\nimmediately if one is present.\n"
          }
        }
      },
      "DispatchBotInput": {
        "type": "object",
        "required": ["externalId", "meetingUrl"],
        "properties": {
          "externalId": {
            "type": "string",
            "minLength": 1,
            "description": "Your idempotency key for this job. POSTing twice with the\nsame value returns the existing job rather than dispatching\na second bot.\n",
            "example": "lesson-2026-05-09-pavel"
          },
          "meetingUrl": {
            "type": "string",
            "format": "uri",
            "description": "Meet/Teams/Zoom join URL.",
            "example": "https://meet.google.com/abc-defg-hij"
          },
          "platform": {
            "$ref": "#/components/schemas/Platform"
          },
          "joinAt": {
            "type": "string",
            "format": "date-time",
            "description": "Wall-clock time the bot should attempt to join. Omit for \"join now\"."
          },
          "displayName": {
            "type": "string",
            "description": "Display name shown to other participants.",
            "example": "Acme Notetaker"
          },
          "webhooks": {
            "type": "object",
            "properties": {
              "onStatusChange": {
                "type": "string",
                "format": "uri"
              },
              "onFinalize": {
                "type": "string",
                "format": "uri"
              }
            }
          },
          "metadata": {
            "type": "object",
            "additionalProperties": true,
            "description": "Free-form metadata echoed back in webhooks."
          },
          "autoLeave": {
            "$ref": "#/components/schemas/AutoLeaveConfig"
          }
        }
      },
      "Job": {
        "type": "object",
        "required": [
          "id",
          "externalId",
          "status",
          "meetingUrl",
          "platform",
          "joinAt",
          "attempts",
          "createdAt",
          "startedAt",
          "finalizedAt",
          "manifestUri",
          "failureCode",
          "failureDetail",
          "metadata",
          "autoLeave"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "externalId": {
            "type": "string"
          },
          "status": {
            "$ref": "#/components/schemas/JobStatus"
          },
          "meetingUrl": {
            "type": "string",
            "format": "uri"
          },
          "platform": {
            "$ref": "#/components/schemas/Platform"
          },
          "joinAt": {
            "type": "string",
            "format": "date-time"
          },
          "attempts": {
            "type": "integer",
            "minimum": 0
          },
          "createdAt": {
            "type": "string",
            "format": "date-time"
          },
          "startedAt": {
            "type": ["string", "null"],
            "format": "date-time"
          },
          "finalizedAt": {
            "type": ["string", "null"],
            "format": "date-time"
          },
          "manifestUri": {
            "type": ["string", "null"],
            "format": "uri",
            "description": "URI to the recording manifest (S3/R2/etc.) once finalized; `null` until then."
          },
          "failureCode": {
            "oneOf": [
              {
                "$ref": "#/components/schemas/FailureCode"
              },
              {
                "type": "null"
              }
            ],
            "description": "Structured failure code; `null` on non-failed jobs."
          },
          "failureDetail": {
            "type": ["string", "null"],
            "description": "Human-readable failure detail; `null` on non-failed jobs."
          },
          "metadata": {
            "type": "object",
            "additionalProperties": true
          },
          "autoLeave": {
            "$ref": "#/components/schemas/AutoLeaveConfig"
          }
        }
      },
      "CommandQueuedResponse": {
        "type": "object",
        "required": ["status", "commandId"],
        "properties": {
          "status": {
            "type": "string",
            "enum": ["queued"]
          },
          "commandId": {
            "type": "string",
            "format": "uuid"
          }
        }
      },
      "CalendarInvite": {
        "type": "object",
        "required": ["organizerEmail", "meetingUrl", "platform", "joinAt"],
        "properties": {
          "organizerEmail": {
            "type": "string",
            "format": "email"
          },
          "subject": {
            "type": "string",
            "default": "(no subject)"
          },
          "meetingUrl": {
            "type": "string",
            "format": "uri"
          },
          "platform": {
            "type": "string",
            "enum": ["meet", "teams", "zoom-web"]
          },
          "joinAt": {
            "type": "string",
            "format": "date-time"
          },
          "icalUid": {
            "type": ["string", "null"]
          }
        }
      },
      "IntakeResult": {
        "type": "object",
        "required": ["kind", "jobId"],
        "properties": {
          "kind": {
            "type": "string",
            "enum": ["scheduled", "deduped", "free_tier_exhausted"]
          },
          "jobId": {
            "type": "string",
            "format": "uuid"
          },
          "consumerId": {
            "type": "string"
          }
        }
      },
      "IngestEvent": {
        "type": "object",
        "description": "Discriminated union on `kind`. The orchestrator validates with\nZod; see `apps/orchestrator/src/domain/ingest-event.ts` for the\ncomplete authoritative schema (kept in sync with this spec).\n",
        "required": ["kind", "jobId", "attempt", "emittedAt"],
        "properties": {
          "kind": {
            "type": "string",
            "enum": [
              "lifecycle.joined",
              "lifecycle.entering_lobby",
              "lifecycle.in_meeting",
              "lifecycle.recording_started",
              "lifecycle.recording_paused",
              "lifecycle.recording_stopped",
              "lifecycle.permission_requested",
              "lifecycle.permission_response",
              "lifecycle.competing_bot_detected",
              "lifecycle.post_processing",
              "lifecycle.media_uploading",
              "lifecycle.exited",
              "manifest.finalized"
            ]
          },
          "jobId": {
            "type": "string",
            "format": "uuid"
          },
          "attempt": {
            "type": "integer",
            "minimum": 0
          },
          "emittedAt": {
            "type": "string",
            "format": "date-time"
          },
          "data": {
            "type": "object",
            "additionalProperties": true
          }
        }
      },
      "HostMetrics": {
        "type": "object",
        "properties": {
          "cpu": {
            "type": ["object", "null"],
            "properties": {
              "user": {
                "type": "number"
              },
              "system": {
                "type": "number"
              },
              "iowait": {
                "type": "number"
              },
              "idle": {
                "type": "number"
              }
            }
          },
          "ram": {
            "type": ["object", "null"],
            "properties": {
              "used": {
                "type": "number"
              },
              "free": {
                "type": "number"
              },
              "cached": {
                "type": "number"
              },
              "buffers": {
                "type": "number"
              },
              "totalMB": {
                "type": "number"
              }
            }
          },
          "load": {
            "type": ["object", "null"],
            "properties": {
              "one": {
                "type": "number"
              },
              "five": {
                "type": "number"
              },
              "fifteen": {
                "type": "number"
              }
            }
          },
          "disk": {
            "type": ["object", "null"],
            "properties": {
              "usedPct": {
                "type": "number"
              },
              "freeGB": {
                "type": "number"
              }
            }
          },
          "bots": {
            "type": "object",
            "properties": {
              "running": {
                "type": "integer"
              },
              "scheduled": {
                "type": "integer"
              }
            }
          },
          "uptimeSec": {
            "type": ["integer", "null"]
          },
          "ts": {
            "type": "integer"
          }
        }
      },
      "WebhookEnvelope": {
        "type": "object",
        "required": ["deliveryId", "event", "emittedAt", "attempt", "data"],
        "properties": {
          "deliveryId": {
            "type": "string",
            "description": "Stable id; consumers should dedupe on this."
          },
          "event": {
            "type": "string"
          },
          "emittedAt": {
            "type": "string",
            "format": "date-time"
          },
          "attempt": {
            "type": "integer",
            "minimum": 1,
            "description": "Number of delivery attempts including this one."
          },
          "version": {
            "type": "integer",
            "enum": [1, 2],
            "description": "Payload schema version. v1 omits the field; v2 always sets\nit. The orchestrator emits `version: 2` today; SDKs should\nbranch on the `x-meetbot-webhook-version` header or this\nfield when decoding.\n"
          },
          "data": {
            "type": "object",
            "additionalProperties": true
          }
        }
      },
      "JobStatusChangedEvent": {
        "allOf": [
          {
            "$ref": "#/components/schemas/WebhookEnvelope"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "object",
                "required": ["event", "jobId", "from", "to", "job"],
                "properties": {
                  "event": {
                    "type": "string",
                    "enum": ["job.status_changed"]
                  },
                  "jobId": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "from": {
                    "$ref": "#/components/schemas/JobStatus"
                  },
                  "to": {
                    "$ref": "#/components/schemas/JobStatus"
                  },
                  "job": {
                    "$ref": "#/components/schemas/Job"
                  },
                  "failure_code": {
                    "oneOf": [
                      {
                        "$ref": "#/components/schemas/FailureCode"
                      },
                      {
                        "type": "null"
                      }
                    ]
                  },
                  "failure_detail": {
                    "type": ["string", "null"]
                  },
                  "failure_reason": {
                    "type": ["string", "null"],
                    "deprecated": true,
                    "description": "v1 mirror of failure_detail."
                  }
                }
              }
            }
          }
        ]
      },
      "JobFinalizedEvent": {
        "allOf": [
          {
            "$ref": "#/components/schemas/WebhookEnvelope"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "object",
                "required": ["event", "jobId", "manifestUri", "job"],
                "properties": {
                  "event": {
                    "type": "string",
                    "enum": ["job.finalized"]
                  },
                  "jobId": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "manifestUri": {
                    "type": "string",
                    "format": "uri"
                  },
                  "job": {
                    "$ref": "#/components/schemas/Job"
                  }
                }
              }
            }
          }
        ]
      },
      "JobFailedEvent": {
        "allOf": [
          {
            "$ref": "#/components/schemas/WebhookEnvelope"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "object",
                "required": ["event", "jobId", "status", "failure_code", "failure_detail", "job"],
                "properties": {
                  "event": {
                    "type": "string",
                    "enum": ["job.failed"]
                  },
                  "jobId": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "status": {
                    "type": "string",
                    "enum": ["failed"]
                  },
                  "failure_code": {
                    "$ref": "#/components/schemas/FailureCode"
                  },
                  "failure_detail": {
                    "type": "string"
                  },
                  "failure_reason": {
                    "type": "string",
                    "deprecated": true
                  },
                  "error": {
                    "type": "string",
                    "deprecated": true,
                    "description": "v1 free-text error string; use failure_detail."
                  },
                  "job": {
                    "$ref": "#/components/schemas/Job"
                  }
                }
              }
            }
          }
        ]
      },
      "BotRecordingPermissionResponseEvent": {
        "allOf": [
          {
            "$ref": "#/components/schemas/WebhookEnvelope"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "object",
                "required": ["event", "jobId", "granted", "job"],
                "properties": {
                  "event": {
                    "type": "string",
                    "enum": ["bot.recording_permission_response"]
                  },
                  "jobId": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "granted": {
                    "type": "boolean"
                  },
                  "detail": {
                    "type": "string"
                  },
                  "job": {
                    "$ref": "#/components/schemas/Job"
                  }
                }
              }
            }
          }
        ]
      },
      "BotCompetingBotDetectedEvent": {
        "allOf": [
          {
            "$ref": "#/components/schemas/WebhookEnvelope"
          },
          {
            "type": "object",
            "properties": {
              "data": {
                "type": "object",
                "required": ["event", "jobId", "detectedBotName", "leftMeeting", "job"],
                "properties": {
                  "event": {
                    "type": "string",
                    "enum": ["bot.competing_bot_detected"]
                  },
                  "jobId": {
                    "type": "string",
                    "format": "uuid"
                  },
                  "detectedBotName": {
                    "type": "string"
                  },
                  "leftMeeting": {
                    "type": "boolean"
                  },
                  "job": {
                    "$ref": "#/components/schemas/Job"
                  }
                }
              }
            }
          }
        ]
      },
      "Error": {
        "type": "object",
        "required": ["error"],
        "properties": {
          "error": {
            "type": "string",
            "description": "Stable machine-readable error code (snake_case)."
          },
          "message": {
            "type": "string",
            "description": "Optional human-readable detail."
          },
          "issues": {
            "type": "array",
            "description": "Zod validation issues; only present on 400s from schema-validated endpoints.",
            "items": {
              "type": "object",
              "additionalProperties": true
            }
          }
        }
      }
    }
  },
  "webhooks": {
    "jobStatusChanged": {
      "post": {
        "tags": ["webhooks"],
        "summary": "job.status_changed",
        "description": "Fired on every status transition. v2 payloads carry failure_code + failure_detail when transitioning into \"failed\".",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/JobStatusChangedEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Consumer ack."
          }
        }
      }
    },
    "jobFinalized": {
      "post": {
        "tags": ["webhooks"],
        "summary": "job.finalized",
        "description": "Fired once per successful job, when the manifest is written. `manifestUri` points to the structured recording bundle.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/JobFinalizedEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Consumer ack."
          }
        }
      }
    },
    "jobFailed": {
      "post": {
        "tags": ["webhooks"],
        "summary": "job.failed",
        "description": "Fired once when the job moves into terminal \"failed\". Always carries a structured `failure_code` in v2.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/JobFailedEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Consumer ack."
          }
        }
      }
    },
    "botRecordingPermissionResponse": {
      "post": {
        "tags": ["webhooks"],
        "summary": "bot.recording_permission_response",
        "description": "Fired when the host responds (or times out) to a request_recording_permission verb.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BotRecordingPermissionResponseEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Consumer ack."
          }
        }
      }
    },
    "botCompetingBotDetected": {
      "post": {
        "tags": ["webhooks"],
        "summary": "bot.competing_bot_detected",
        "description": "Fired when the bot spots a competing notetaker (Otter, Fireflies, Read, \u2026) in the participant roster. `leftMeeting` is `true` when the job had `autoLeave.onBotDetected = true`.",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/BotCompetingBotDetectedEvent"
              }
            }
          }
        },
        "responses": {
          "2XX": {
            "description": "Consumer ack."
          }
        }
      }
    }
  }
}
