{"openapi":"3.0.3","info":{"title":"Brella API","version":"2026-08-22","summary":"The public API of Brella, a commercial insurance brokerage in Houston, Texas.","description":"The public and admin API behind brellapartners.com and Brella OS.\n\n## Public endpoints\n\nEverything tagged **Public** or **System** is open — no key, no account, no\nsign-up. `GET /api/blogs`, `GET /api/status`, `GET /api/health` and\n`GET /api/openapi.json` are safe to call right now. `POST /api/quotes` and\n`POST /api/analysis` submit a real enquiry to the brokerage, so treat them as\nyou would a contact form: send them when a person means to.\n\n## Authentication\n\nEndpoints tagged **Admin** belong to Brella OS, the workspace Brella runs its\nown book on. They need a bearer token from `POST /api/admin/login`, and an\naccount is issued to an agency, not signed up for. Tokens are HMAC-signed and\nlast 12 hours.\n\n## Versioning\n\nThe current version is `2026-08-22`, returned on every response in\nthe `X-Brella-API-Version` header. A version is a DATE, because what a caller\nneeds to pin is the behaviour on the day it integrated.\n\nBreaking changes get a new version date. Additive changes — a new field, a new\nendpoint, a new optional parameter — do not, so clients must ignore fields they\ndo not recognise.\n\nWhen a version is being retired, every response from it carries the RFC 8594\n`Deprecation` header with the date the decision was taken, and a `Sunset` header\nwith the date it stops working. Neither header is present on a version that is\nnot deprecated — which is why its absence is the signal that a version is live.\n\n## Rate limits\n\nPublic endpoints allow 120 requests per 60\nseconds per IP. Every response carries `RateLimit-Limit`, `RateLimit-Remaining`\nand `RateLimit-Reset` (and the combined `RateLimit` field), so a client can pace\nitself without guessing. Exceeding the limit returns `429` with `Retry-After`.\n\n## Errors\n\nEvery failure returns the same JSON object: `ok: false`, a machine-readable\n`code`, and a human-readable `error` sentence. Branch on `code`; show `error`.\nThe sentence gets reworded, the code does not.","contact":{"name":"Brella","email":"hello@brellapartners.com","url":"https://www.brellapartners.com/developers"},"license":{"name":"Proprietary","url":"https://www.brellapartners.com/terms"}},"externalDocs":{"description":"Developer documentation","url":"https://www.brellapartners.com/developers"},"servers":[{"url":"https://www.brellapartners.com","description":"Production"},{"url":"/","description":"Same origin (this host)"}],"tags":[{"name":"Public","description":"Open endpoints, no authentication"},{"name":"Admin","description":"Bearer-token protected (Brella OS)"},{"name":"System","description":"Health, status and machine-readable description"}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"Token from POST /api/admin/login"}},"schemas":{"Error":{"type":"object","required":["ok","error"],"description":"A failed request. `code` is stable and meant to be branched on; `error` is a sentence for a person and may be reworded at any time.","properties":{"ok":{"type":"boolean","enum":[false],"example":false},"code":{"type":"string","description":"Machine-readable reason.","enum":["bad_request","unauthorized","forbidden","not_found","conflict","payload_too_large","unprocessable","rate_limited","not_implemented","service_unavailable","server_error","request_failed"],"example":"bad_request"},"error":{"type":"string","example":"Invalid request body."}}},"Health":{"type":"object","required":["ok","database"],"properties":{"ok":{"type":"boolean","example":true},"version":{"type":"string","example":"funnel-1"},"database":{"type":"string","enum":["connected","not-configured"],"example":"connected"}}},"ServiceStatus":{"type":"object","required":["name","state","latencyMs","detail"],"properties":{"name":{"type":"string","example":"Database"},"state":{"type":"string","enum":["operational","degraded","down"],"example":"operational"},"latencyMs":{"type":"integer","nullable":true,"example":42},"detail":{"type":"string","example":"Reachable."}}},"HealthReport":{"type":"object","required":["ok","checkedAt","services","history"],"description":"The per-service board behind brellapartners.com/status. `ok` is false when any service is down; a missing third-party key reports `degraded`, not `down`.","properties":{"ok":{"type":"boolean","example":true},"checkedAt":{"type":"string","format":"date-time"},"services":{"type":"array","items":{"$ref":"#/components/schemas/ServiceStatus"}},"history":{"type":"array","description":"Recent snapshots, newest first.","items":{"type":"object","required":["checkedAt","ok"],"properties":{"checkedAt":{"type":"string","format":"date-time"},"ok":{"type":"boolean"}}}}}},"LoginInput":{"type":"object","required":["email","password"],"properties":{"email":{"type":"string","format":"email","example":"owner@example.com"},"password":{"type":"string","format":"password"}}},"LoginResult":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean","example":true},"token":{"type":"string","description":"Bearer token, valid 12 hours."}}},"SessionResult":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean","example":true}}},"Blog":{"type":"object","required":["id","slug","title"],"properties":{"id":{"type":"string"},"slug":{"type":"string","example":"why-we-built-brella-os"},"title":{"type":"string"},"excerpt":{"type":"string"},"author":{"type":"string"},"author_designation":{"type":"string"},"tag":{"type":"string","enum":["Insights","Product","Company","Guides","News"]},"template":{"type":"string","enum":["single","double","triple"]},"images":{"type":"array","items":{"type":"string","format":"uri"}},"body":{"type":"string","description":"Markdown."},"status":{"type":"string","enum":["draft","published"]},"view_count":{"type":"integer"},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"BlogInput":{"type":"object","required":["title","slug","body"],"properties":{"title":{"type":"string","example":"Why we built Brella OS"},"slug":{"type":"string","example":"why-we-built-brella-os"},"excerpt":{"type":"string"},"author":{"type":"string","example":"Jane Smith"},"author_designation":{"type":"string","example":"Head of Risk, Brella Partners"},"tag":{"type":"string","enum":["Insights","Product","Company","Guides","News"]},"template":{"type":"string","enum":["single","double","triple"]},"images":{"type":"array","items":{"type":"string","format":"uri"}},"body":{"type":"string","example":"Markdown body…"},"status":{"type":"string","enum":["draft","published"]}}},"QuoteInput":{"type":"object","required":["email"],"description":"A request for commercial insurance quotes. Reaches the brokerage as a lead.","properties":{"business_name":{"type":"string","example":"Acme Logistics LLC"},"industry":{"type":"string","example":"Transportation & Distribution"},"email":{"type":"string","format":"email","example":"owner@acme.com"},"first_name":{"type":"string"},"last_name":{"type":"string"},"phone":{"type":"string"}}},"AnalysisInput":{"type":"object","required":["name","email"],"description":"A request for a review of an existing insurance program.","properties":{"name":{"type":"string"},"company":{"type":"string"},"email":{"type":"string","format":"email","example":"owner@acme.com"},"phone":{"type":"string"},"revenue":{"type":"string"},"concern":{"type":"string"}}},"Accepted":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean","example":true},"id":{"type":"string","description":"The stored record id."},"persisted":{"type":"boolean","example":true}}},"Ok":{"type":"object","required":["ok"],"properties":{"ok":{"type":"boolean","example":true}}},"QuoteList":{"type":"object","required":["ok","quotes"],"properties":{"ok":{"type":"boolean","example":true},"quotes":{"type":"array","items":{"type":"object","additionalProperties":true}}}},"BlobUploadToken":{"type":"object","description":"The Vercel Blob client-token payload. Consumed by `@vercel/blob/client`'s `upload()`.","additionalProperties":true}}},"paths":{"/api/health":{"get":{"operationId":"getHealth","tags":["System"],"summary":"Liveness and database reachability","description":"A cheap liveness probe. Returns immediately and never touches a table — use `GET /api/status` for the per-service board.","responses":{"200":{"description":"The service is up.","headers":{"RateLimit-Limit":{"description":"Requests permitted per window.","schema":{"type":"integer","example":120}},"RateLimit-Remaining":{"description":"Requests left in the current window.","schema":{"type":"integer","example":119}},"RateLimit-Reset":{"description":"Seconds until the window resets.","schema":{"type":"integer","example":60}},"X-Brella-API-Version":{"description":"The API version that served this response.","schema":{"type":"string","example":"2026-08-22"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Health"}}}},"429":{"description":"Rate limit exceeded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/status":{"get":{"operationId":"getStatus","tags":["System"],"summary":"Per-service health board and recent history","description":"The live data behind brellapartners.com/status: every critical flow, integration and table checked for real on each call. Recorded to a history table at most once every 50 minutes, so polling it is cheap.","responses":{"200":{"description":"The current board.","headers":{"RateLimit-Limit":{"description":"Requests permitted per window.","schema":{"type":"integer","example":120}},"RateLimit-Remaining":{"description":"Requests left in the current window.","schema":{"type":"integer","example":119}},"RateLimit-Reset":{"description":"Seconds until the window resets.","schema":{"type":"integer","example":60}},"X-Brella-API-Version":{"description":"The API version that served this response.","schema":{"type":"string","example":"2026-08-22"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthReport"}}}},"429":{"description":"Rate limit exceeded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/openapi.json":{"get":{"operationId":"getOpenApiDocument","tags":["System"],"summary":"This document","description":"The OpenAPI 3.0 description of this API, as JSON. No authentication.","responses":{"200":{"description":"The OpenAPI document.","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}},"/api/quotes":{"post":{"operationId":"submitQuoteRequest","tags":["Public"],"summary":"Submit a request for commercial insurance quotes","description":"Files a new-business enquiry with the brokerage and links it to the customer book. A person reads it and replies within one business day, so send it when somebody actually wants a quote.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuoteInput"}}}},"responses":{"200":{"description":"The enquiry was filed.","headers":{"RateLimit-Limit":{"description":"Requests permitted per window.","schema":{"type":"integer","example":120}},"RateLimit-Remaining":{"description":"Requests left in the current window.","schema":{"type":"integer","example":119}},"RateLimit-Reset":{"description":"Seconds until the window resets.","schema":{"type":"integer","example":60}},"X-Brella-API-Version":{"description":"The API version that served this response.","schema":{"type":"string","example":"2026-08-22"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Accepted"}}}},"400":{"description":"The payload failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"The enquiry could not be stored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"No database is configured.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/analysis":{"post":{"operationId":"submitAnalysisRequest","tags":["Public"],"summary":"Request a review of an existing insurance program","description":"For a business that already carries coverage and wants it read for gaps. Same destination and same reply expectation as a quote request.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalysisInput"}}}},"responses":{"200":{"description":"The request was filed.","headers":{"RateLimit-Limit":{"description":"Requests permitted per window.","schema":{"type":"integer","example":120}},"RateLimit-Remaining":{"description":"Requests left in the current window.","schema":{"type":"integer","example":119}},"RateLimit-Reset":{"description":"Seconds until the window resets.","schema":{"type":"integer","example":60}},"X-Brella-API-Version":{"description":"The API version that served this response.","schema":{"type":"string","example":"2026-08-22"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Accepted"}}}},"400":{"description":"The payload failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"The request could not be stored.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"No database is configured.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/blogs":{"get":{"operationId":"listPublishedPosts","tags":["Public"],"summary":"List published blog posts","description":"Every published post, newest first, with its full markdown body. Drafts are never returned here. Returns an empty array — not an error — when no database is configured.","responses":{"200":{"description":"The published posts.","headers":{"RateLimit-Limit":{"description":"Requests permitted per window.","schema":{"type":"integer","example":120}},"RateLimit-Remaining":{"description":"Requests left in the current window.","schema":{"type":"integer","example":119}},"RateLimit-Reset":{"description":"Seconds until the window resets.","schema":{"type":"integer","example":60}},"X-Brella-API-Version":{"description":"The API version that served this response.","schema":{"type":"string","example":"2026-08-22"}}},"content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Blog"}}}}},"429":{"description":"Rate limit exceeded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/blogs/{slug}":{"get":{"operationId":"getPostBySlug","tags":["Public"],"summary":"Get one published blog post","description":"The post at that slug, including its markdown body. Drafts return 404.","parameters":[{"name":"slug","in":"path","required":true,"description":"The post slug, as it appears in the URL.","schema":{"type":"string","example":"why-we-built-brella-os"}}],"responses":{"200":{"description":"The post.","headers":{"RateLimit-Limit":{"description":"Requests permitted per window.","schema":{"type":"integer","example":120}},"RateLimit-Remaining":{"description":"Requests left in the current window.","schema":{"type":"integer","example":119}},"RateLimit-Reset":{"description":"Seconds until the window resets.","schema":{"type":"integer","example":60}},"X-Brella-API-Version":{"description":"The API version that served this response.","schema":{"type":"string","example":"2026-08-22"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Blog"}}}},"404":{"description":"No published post at that slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/blogs/{slug}/view":{"post":{"operationId":"recordPostView","tags":["Public"],"summary":"Increment a post view count","description":"Called by the site when a post is opened. Idempotency is not attempted — the count is a readership signal, not an analytic.","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","example":"why-we-built-brella-os"}}],"responses":{"200":{"description":"Recorded.","headers":{"RateLimit-Limit":{"description":"Requests permitted per window.","schema":{"type":"integer","example":120}},"RateLimit-Remaining":{"description":"Requests left in the current window.","schema":{"type":"integer","example":119}},"RateLimit-Reset":{"description":"Seconds until the window resets.","schema":{"type":"integer","example":60}},"X-Brella-API-Version":{"description":"The API version that served this response.","schema":{"type":"string","example":"2026-08-22"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Ok"}}}},"404":{"description":"No published post at that slug.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limit exceeded.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/admin/login":{"post":{"operationId":"createAdminSession","tags":["Admin"],"summary":"Exchange credentials for a bearer token","description":"Accounts are provisioned to an agency by Brella; there is no public sign-up. The token carries the caller’s role and organisation and lasts 12 hours.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginInput"}}}},"responses":{"200":{"description":"Signed in.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginResult"}}}},"401":{"description":"Wrong email or password.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"No database is configured, so no account can be checked.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/admin/session":{"get":{"operationId":"getAdminSession","tags":["Admin"],"summary":"Check whether a token is still valid","description":"Cheap validity check for a stored token. Does not extend its lifetime.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"The token is valid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SessionResult"}}}},"401":{"description":"Missing, malformed or expired token.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/admin/quotes":{"get":{"operationId":"listQuoteLeads","tags":["Admin"],"summary":"List the quote leads for the caller’s agency","description":"The marketing-site enquiries belonging to the caller’s own organisation. Scoped server-side from the token — an organisation cannot be requested.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"The leads.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuoteList"}}}},"401":{"description":"Not authorised.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Authorised, but not for this organisation’s book.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/admin/blogs":{"get":{"operationId":"listAllPosts","tags":["Admin"],"summary":"List every post, drafts included","description":"The editorial view of the blog. Restricted to the Brella Partners organisation.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Every post.","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Blog"}}}}},"401":{"description":"Not authorised.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Authorised, but not for the blog.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"createPost","tags":["Admin"],"summary":"Create a post","description":"Creates a post. A slug that already exists is rejected rather than merged.","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BlogInput"}}}},"responses":{"200":{"description":"Created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Blog"}}}},"400":{"description":"The payload failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not authorised.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Authorised, but not for the blog.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"That slug is already taken.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/admin/blogs/{id}":{"put":{"operationId":"updatePost","tags":["Admin"],"summary":"Update a post","description":"Replaces the post’s fields. Publishing is a `status` change like any other.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BlogInput"}}}},"responses":{"200":{"description":"Updated.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Blog"}}}},"400":{"description":"The payload failed validation.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Not authorised.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Authorised, but not for the blog.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No post with that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"operationId":"deletePost","tags":["Admin"],"summary":"Delete a post","description":"Permanent. There is no trash and no undo.","security":[{"bearerAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Ok"}}}},"401":{"description":"Not authorised.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Authorised, but not for the blog.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No post with that id.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/admin/upload":{"post":{"operationId":"createBlobUploadToken","tags":["Admin"],"summary":"Mint a Vercel Blob client upload token","description":"Not called directly. The browser SDK (`@vercel/blob/client`’s `upload()`) posts here for a short-lived token, then uploads the file straight to Blob storage — so the file never passes through this API.","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"The token payload.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BlobUploadToken"}}}},"401":{"description":"Not authorised.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Authorised, but not for uploads.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"501":{"description":"Blob storage is not configured on this deployment.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}}}