# GhostMind > An OpenAI-compatible AI gateway for workspaces, with beta-gated Action APIs. Source reviewed 2026-09-26 at 08bbcca; deployment status is not verified. The full public method/path inventory is in /api-reference/overview/ on the docs site. Key/workspace/invitation management, connect/capture, and the teach/compile/verify/publish journey are not public /v1 APIs in this revision. OpenAPI lacks Bearer security metadata and some response schemas; generated clients need explicit authorization and contract checks. Enabled/healthy is a projection, not proof of a successful provider call. Rate-limit coverage is partial and rate-limit headers are not guaranteed. GhostMind routes requests through your connected AI provider accounts (ChatGPT, Google Savio) with workspace isolation, session pooling, usage metering, and quota enforcement. ## Base URL https://ghostmind.optdmsa.com All endpoints listed below include their full path (e.g. `/v1/chat/completions`). Append the path to the base URL: `https://ghostmind.optdmsa.com/v1/chat/completions`. ## Authentication Bearer token: `Authorization: Bearer gmk_live_...` API keys start with `gmk_live_` and are scoped to a workspace. ## Agent Quick Start 1. Call `GET /v1/capabilities` first — discover what is enabled for your workspace. 2. Call `GET /v1/models` — list available models. 3. Do NOT assume a feature is enabled. Check `enabled: true` in the capabilities manifest. 4. Distinguish `supported` (platform-level) from `enabled` (effective for your workspace). 5. Preserve the `X-GhostMind-Conversation-Id` response header to continue conversations. 6. Inspect canonical error codes in responses. Do not invent endpoints. 7. Read the OpenAPI spec for exact request/response schemas. ## Capabilities - Chat completions (streaming + non-streaming) - Conversation continuity with sticky binding - Projects with file context and memory policies - File attachments and asset management - Speech generation (TTS) via Google Savio - Audio transcription (speech-to-text) - Usage tracking - Capability discovery manifest - Connected account inspection ## Error Format Structured application errors use this envelope; framework validation may use a detail array, throttling may return a string-valued error, and proxies may return non-JSON. Check HTTP status/content type and types before nested access: ```json { "error": { "code": "ERROR_CODE", "message": "Human-readable message", "type": "error_type", "request_id": "req_abc123", "retryable": false } } ``` Some errors may be wrapped in `{"detail": {"error": {...}}}`. Always check for `error` at both top level and under `detail`. Error codes may arrive in UPPER_SNAKE_CASE or lowercase_snake_case — compare case-insensitively. `retryable` may be `true`, `false`, or `null`. Every response includes an `X-Request-Id` header for correlation. ## Endpoints ### Chat - POST /v1/chat/completions — Chat completions (OpenAI-compatible) - GET /v1/models — List models ### Conversations - POST /v1/conversations — Create a conversation - GET /v1/conversations — List conversations - GET /v1/conversations/{id} — Get conversation - GET /v1/conversations/{id}/messages — Message history - PATCH /v1/conversations/{id} — Update title/pinned - DELETE /v1/conversations/{id} — Soft-delete - POST /v1/conversations/{id}/sync — Sync with ChatGPT - POST /v1/conversations/{id}/archive — Archive - POST /v1/conversations/{id}/restore — Restore - POST /v1/conversations/{id}/share — Share with user - POST /v1/conversations/{id}/transfer — Transfer ownership - POST /v1/conversations/{id}/attachments — Upload attachment ### Projects - POST /v1/projects — Create project - GET /v1/projects — List projects - GET /v1/projects/{id} — Get project detail - PATCH /v1/projects/{id} — Update project - DELETE /v1/projects/{id} — Delete project - POST /v1/projects/{id}/files — Upload file (max 10MB). Account-tier gated: can return 403 `UPSTREAM_ACCOUNT_NOT_AUTHORIZED` — check `capabilities.project_files` first; conversation attachments are the fallback - GET /v1/projects/{id}/files — List files - DELETE /v1/projects/{id}/files/{source_id} — Delete file ### Assets - GET /v1/messages/{id} — Poll message status + assets - GET /v1/assets/{id} — Poll asset status - GET /v1/assets/{id}/download — Download asset - POST /v1/assets/{id}/retry — Retry failed asset ### Audio - POST /v1/audio/transcriptions — Transcribe audio - POST /v1/audio/speech — Sync TTS - POST /v1/audio/generations — Create async TTS job - GET /v1/audio/generations — List TTS jobs - GET /v1/audio/generations/{id} — Get TTS job status - GET /v1/audio/generations/{id}/content — Download audio - POST /v1/audio/generations/{id}/cancel — Cancel TTS job - POST /v1/audio/generations/{id}/retry — Retry failed TTS job - GET /v1/audio/providers — List TTS providers - GET /v1/audio/providers/{provider}/voices — List voices - GET /v1/audio/providers/{provider}/capabilities — Provider capabilities ### Additional conversation resources - GET /v1/conversations/{id}/participants — List participants - PATCH /v1/conversations/{id}/participants/{user_id} — Update participant - DELETE /v1/conversations/{id}/participants/{user_id} — Remove participant - GET /v1/conversations/{id}/assets — List conversation assets ### Actions (beta-gated execution) - GET /v1/actions — Catalog (actions:read) - GET /v1/actions/{slug} — Details (actions:read) - POST /v1/actions/{slug}/assets — Upload file for a `file` input (actions:run) - GET /v1/actions/{slug}/assets — List uploaded assets (actions:read) - POST /v1/actions/{slug}/runs — Submit execution (actions:run), HTTP 202 - GET /v1/actions/{slug}/runs — History (actions:read) - GET /v1/actions/{slug}/runs/{run_id} — State/result (actions:read) - GET /v1/actions/{slug}/runs/{run_id}/events — Events (actions:read) - POST /v1/actions/{slug}/runs/{run_id}/cancel — Cancel (actions:run) Run body: input object, optional connection_id UUID, optional idempotency_key. The public Action details currently omit full input/output schemas; obtain the input contract from the publisher. HTTP202 is acceptance, not business success. Inspect status/verdict/output; needs_review is not success. Cancellation does not undo effects already applied. Audio idempotency uses Idempotency-Key header; Actions use a body field. Do not assume chat has the same guarantee. File inputs: when an Action declares a `file` input, POST the bytes to /v1/actions/{slug}/assets first (multipart `file` field, ≤20 MiB default), then pass {"asset_id": "..."} as that input's value. Assets are workspace+action scoped, expire (24h default), checksum-verified, and consumed once — reuse fails with ASSET_INPUT_INVALID. ### Usage - GET /v1/usage?period=24h|7d|30d — Aggregated request/token/duration counters for the calling API key only (scope=api_key). No cost/quota internals. Use for self-service consumption monitoring. ### Discovery - GET /v1/capabilities — Machine-readable capability manifest - GET /v1/connections — List connected accounts (read-only) ## Routing Headers These headers are NOT in the OpenAPI spec (they are runtime routing hints). Pass them as request headers: - X-GhostMind-Conversation-Id — Continue a conversation (value from response header of previous chat request) - X-GhostMind-Upstream-Project — Upstream project SLUG for project-scoped chat (the `slug` field returned by POST /v1/projects, e.g. "research-assistant"). THIS is the header to use with projects created via POST /v1/projects. - X-GhostMind-Project — INTERNAL GhostMind project slug (admin-managed internal projects, not the same entity as /v1/projects). Using an upstream slug here returns 422 wrong_project_header. - X-GhostMind-Upstream-Account — Specific account label - X-GhostMind-Route-Profile — Routing profile slug - X-Request-Id — Correlation ID (echoed back) ## Projects in Chat — correct pattern Projects created via POST /v1/projects are UPSTREAM projects. Two correct ways to chat inside one: 1. Preferred: POST /v1/conversations {"upstream_project_id": ""} → then chat with X-GhostMind-Conversation-Id from the response. 2. Direct: POST /v1/chat/completions with X-GhostMind-Upstream-Project: (slug, not UUID). Do NOT pass a /v1/projects id or slug in X-GhostMind-Project — that header addresses internal projects and returns 422 wrong_project_header for upstream slugs. ## General-space conversations A chat request without any conversation/project routing stays in the general space of the selected account. GhostMind does NOT create an upstream project per request (GHOSTMIND_AUTO_CREATE_UPSTREAM_PROJECTS defaults to false). To organize conversations under a project, create one explicitly. ## Conversation Continuation 1. Send first message: `POST /v1/chat/completions` (no conversation header) 2. Extract `X-GhostMind-Conversation-Id` from the **response header** 3. Send follow-up: `POST /v1/chat/completions` with `X-GhostMind-Conversation-Id: {id}` header ## File Attachments 1. Upload: `POST /v1/conversations/{id}/attachments` (multipart form, `file` field) 2. Response returns `file_id` and `asset_id` 3. Reference in chat: add `attachment_file_ids: ["{file_id}"]` to the chat request body ## Multimodal Input (Images & Files) Send images or documents to ChatGPT through the attachments workflow: 1. Create or use an existing conversation 2. Upload the file: `POST /v1/conversations/{id}/attachments` (multipart) 3. Send a chat request with `attachment_file_ids: ["file_..."]` in the body 4. ChatGPT receives the file and can reference its content in the response Supported input types: PNG, JPEG, GIF, WebP, PDF, TXT, CSV, DOCX, XLSX, PPTX, ZIP. Images require width/height metadata (returned by the upload endpoint). ## Generated Artifacts (Images & Files) When ChatGPT generates an image (DALL-E) or a file (code interpreter): 1. The response includes an `assets` array with structured metadata 2. Each asset has: `id`, `asset_type`, `file_name`, `mime_type`, `size_bytes`, `status`, `download_url` 3. `status` transitions: `discovered` → `downloading` → `validating` → `ready` (or `failed`) 4. `download_url` is set only when `status = "ready"`: `GET /v1/assets/{id}/download` 5. Poll `GET /v1/assets/{id}` for readiness if status is not terminal CRITICAL for agents: - Do NOT parse `data:image/...;base64,...` from message content — use the `assets` array - Do NOT attempt to download from upstream URLs — use `GET /v1/assets/{id}/download` - The message `content` may contain a text placeholder like `[Image generated]` - The actual binary is served through GhostMind's authenticated download endpoint - Assets are tenant-scoped: the API key must belong to the same workspace ## Streaming Set `stream: true` for SSE streaming. Format: `data: {json}\n\n` terminated by `data: [DONE]`. ### Streaming Error Contract Provider/session errors during streaming are emitted as structured `response.failed` lifecycle events — NOT as assistant content text. Error event format: ``` data: {"choices":[{"delta":{},"finish_reason":"error"}],"lifecycle_event":{"type":"response.failed","error":{"code":"ACCOUNT_REAUTH_REQUIRED","message":"...","retryable":false}}} ``` Machine-readable error codes in streaming: - `ACCOUNT_REAUTH_REQUIRED` (403, not retryable) — session expired, reconnect - `PROVIDER_RATE_LIMITED` (429, retryable) — upstream rate-limited - `PROVIDER_UNAVAILABLE` (502/503, retryable) — upstream error - `PROVIDER_TIMEOUT` (504, retryable) — upstream timeout - `INVALID_REQUEST` (400, not retryable) — bad request CRITICAL: Stop reading the stream on `response.failed`. Never interpret error text as assistant content. Check error.retryable but reconcile possible external effects before repeating a write. A transient error is not proof that nothing executed. IMAGE_GENERATION_UNAVAILABLE (503, not retryable) is a feature failure, not proof that the whole account session is dead. ### Lifecycle Events (SSE) SSE chunks may include a `lifecycle_event` field alongside the standard OpenAI delta format. These events signal asset processing state: - `asset.discovered` — an image/file was found in the upstream response - `asset.processing` — ingestion started (downloading from upstream) - `asset.ready` — asset downloaded, validated, stored, available for download - `asset.failed` — ingestion failed (check `error_code`/`error_message`) - `response.partially_completed` — text done, assets still processing - `response.completed` — all assets in terminal state (before [DONE]) When you receive `asset.discovered`, start polling `GET /v1/assets/{id}`. Do NOT embed base64 in your client — use the structured `assets` array. ## Async Audio Jobs 1. POST /v1/audio/generations → 200 with job ID and status 2. Poll GET /v1/audio/generations/{id} until status=completed 3. GET /v1/audio/generations/{id}/content to download Expect ~1–2 minutes of provider-side generation time per clip (browser automation upstream — this is normal). Job output EXPIRES 24 hours after creation — download promptly. Note: The TTS response_format field is accepted but the provider may coerce to its native output format (WAV). Check the downloaded file's Content-Type header for the actual format. ## Latency expectations - Chat: ~7–45 s total is normal (upstream web session, not a raw API). Streaming first token can take ~20 s — set generous client timeouts. - Transcription: typically < 2 s. - Speech: ~1–2 min per clip — always use the async jobs endpoints, never sync speech in a latency-sensitive path. ## Actions scopes /v1/actions* requires your API key to carry actions:read / actions:run scopes — keys are issued with an explicit scope list, and a key without them gets 403 "Missing scope". Request the scopes from your workspace admin when the key is created. ## OpenAI Compatibility The `/v1/chat/completions` endpoint accepts OpenAI SDK parameters: - `temperature`, `top_p`, `max_tokens` — accepted for SDK compatibility, but ignored (ChatGPT does not expose these controls) - `tools`, `tool_choice`, `n>1`, `logprobs` — rejected with 400 UNSUPPORTED_PARAMETER ## OpenAPI Public spec (data plane only, no admin routes): https://ghostmind.optdmsa.com/openapi-public.json Swagger UI: https://ghostmind.optdmsa.com/docs-public ReDoc: https://ghostmind.optdmsa.com/redoc ## Docs https://docs.ghostmind.optdmsa.com