{"openapi":"3.1.0","info":{"title":"Dispatch","description":"Turns a raw development log (commit subjects + merged PR titles) into customer-facing\nrelease-notes Markdown. **Drafting only** — distribution and human approval stay with the\ncaller, and request/response bodies are never persisted (logs carry counts, not content).\n\n- **Auth:** `Authorization: Bearer <api key>` on every `/v1/*` call.\n- **Voice:** your account's brand-voice floor is composed beneath your `voice` server-side —\n  pass persona/tone only. Read it at `GET /v1/voice`.\n- **Errors:** RFC 7807 `application/problem+json`; request content is never echoed back.\n- **Agent quickstart:** `GET /llms.txt`.\n","version":"1.6.2"},"servers":[{"url":"https://dispatch.majr.app"}],"paths":{"/":{"get":{"tags":["discovery"],"summary":"Service card: what this service is + where its contracts live","operationId":"getServiceCard","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Getservicecard"}}}}}}},"/catalog.json":{"get":{"tags":["discovery"],"summary":"Machine-readable catalog of every MAJR Agent Service","operationId":"getCatalog","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"additionalProperties":true,"type":"object","title":"Response Getcatalog"}}}}}}},"/v1/draft-dispatch":{"post":{"tags":["drafting"],"summary":"Draft customer-facing release notes from a raw development log","operationId":"draftDispatch","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DraftRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DraftResponse"}}}},"400":{"description":"Requested model is not in the allowlist","content":{"application/problem+json":{"schema":{"properties":{"type":{"type":"string","title":"Type","default":"about:blank"},"title":{"type":"string","title":"Title"},"status":{"type":"integer","title":"Status"},"detail":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Detail"}},"type":"object","required":["title","status"],"title":"Problem","description":"RFC 7807 error body, served as application/problem+json."}}}},"401":{"description":"Missing or invalid bearer token","content":{"application/problem+json":{"schema":{"properties":{"type":{"type":"string","title":"Type","default":"about:blank"},"title":{"type":"string","title":"Title"},"status":{"type":"integer","title":"Status"},"detail":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Detail"}},"type":"object","required":["title","status"],"title":"Problem","description":"RFC 7807 error body, served as application/problem+json."}}}},"403":{"description":"API key is valid but not scoped for this service — use a key scoped for `required_scope` (do not rotate); `granted_scopes` lists where this key works","content":{"application/problem+json":{"schema":{"properties":{"type":{"type":"string","title":"Type","default":"about:blank"},"title":{"type":"string","title":"Title"},"status":{"type":"integer","title":"Status"},"detail":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Detail"},"code":{"type":"string","title":"Code","default":"insufficient_service_scope"},"required_scope":{"type":"string","title":"Required Scope"},"granted_scopes":{"items":{"type":"string"},"type":"array","title":"Granted Scopes"}},"type":"object","required":["title","status","required_scope","granted_scopes"],"title":"InsufficientScopeProblem","description":"The 403 shape (per-service key scopes): valid key, not scoped for THIS service. The\nextension members make remediation deterministic for agents — `code` to dispatch on without\nparsing English, `required_scope` = the service refusing, `granted_scopes` = where the key\ndoes work. Emitted with `WWW-Authenticate: Bearer error=\"insufficient_scope\", scope=\"...\"`.\nDistinct from 401 (rotate the key) and 503 (retry): the fix is a key scoped for this\nservice, never rotation."}}}},"408":{"description":"Request body was not received within the read deadline (app/request_limits.py)","content":{"application/problem+json":{"schema":{"properties":{"type":{"type":"string","title":"Type","default":"about:blank"},"title":{"type":"string","title":"Title"},"status":{"type":"integer","title":"Status"},"detail":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Detail"}},"type":"object","required":["title","status"],"title":"Problem","description":"RFC 7807 error body, served as application/problem+json."}}}},"413":{"description":"Request body exceeds the size limit (app/request_limits.py)","content":{"application/problem+json":{"schema":{"properties":{"type":{"type":"string","title":"Type","default":"about:blank"},"title":{"type":"string","title":"Title"},"status":{"type":"integer","title":"Status"},"detail":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Detail"}},"type":"object","required":["title","status"],"title":"Problem","description":"RFC 7807 error body, served as application/problem+json."}}}},"422":{"description":"Request validation failed (input values are never echoed back)","content":{"application/problem+json":{"schema":{"$defs":{"ValidationErrorDetail":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Loc"},"msg":{"type":"string","title":"Msg"},"type":{"type":"string","title":"Type"}},"type":"object","required":["loc","msg","type"],"title":"ValidationErrorDetail"}},"properties":{"type":{"type":"string","title":"Type","default":"about:blank"},"title":{"type":"string","title":"Title"},"status":{"type":"integer","title":"Status"},"detail":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Detail"},"errors":{"items":{"$ref":"#/$defs/ValidationErrorDetail"},"type":"array","title":"Errors"}},"type":"object","required":["title","status","errors"],"title":"ValidationProblem","description":"The 422 shape: Problem plus the pydantic error locations — with input VALUES stripped\n(never echoed back; the change log can reveal unreleased features)."}}}},"429":{"description":"Back off for `Retry-After` seconds; the key is valid and needs no rotation. Either the account reached its draft ceiling for the current window (app/spend_cap.py; no `code`), or — `code: free_tier_quota` — the key is over the platform's free-tier quota: `reason` (`account_quota` | `fleet_quota`) and `resets_at` say why, and until when","headers":{"Retry-After":{"description":"Seconds to wait before retrying — for the free-tier quota, until `resets_at`.","schema":{"type":"integer"}}},"content":{"application/problem+json":{"schema":{"properties":{"type":{"type":"string","title":"Type","default":"about:blank"},"title":{"type":"string","title":"Title"},"status":{"type":"integer","title":"Status"},"detail":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Detail"},"code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Code"},"reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reason"},"resets_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Resets At"}},"type":"object","required":["title","status"],"title":"QuotaProblem","description":"The 429 shape. The platform's free-tier meter (`code: free_tier_quota`) adds `reason` —\n`account_quota` (this builder's calls for the month are used) or `fleet_quota` (every\nself-serve key is paused for the day) — and `resets_at` (ISO-8601 UTC), with Retry-After\ncounting down to it. This service's own draft ceiling (app/spend_cap.py) is a plain Problem\non the same status: no `code`, Retry-After only. Either way the key is valid: never rotate."}}}},"502":{"description":"Upstream model error — fail loud; a half draft is never returned","content":{"application/problem+json":{"schema":{"properties":{"type":{"type":"string","title":"Type","default":"about:blank"},"title":{"type":"string","title":"Title"},"status":{"type":"integer","title":"Status"},"detail":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Detail"}},"type":"object","required":["title","status"],"title":"Problem","description":"RFC 7807 error body, served as application/problem+json."}}}},"503":{"description":"Auth not configured / auth or usage store unavailable","content":{"application/problem+json":{"schema":{"properties":{"type":{"type":"string","title":"Type","default":"about:blank"},"title":{"type":"string","title":"Title"},"status":{"type":"integer","title":"Status"},"detail":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Detail"}},"type":"object","required":["title","status"],"title":"Problem","description":"RFC 7807 error body, served as application/problem+json."}}}}},"security":[{"HTTPBearer":[]}]}},"/v1/voice":{"get":{"tags":["voice"],"summary":"Your account's brand-voice floor (the text composed beneath every draft)","operationId":"getVoice","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VoiceResponse"}}}},"401":{"description":"Missing or invalid bearer token","content":{"application/problem+json":{"schema":{"properties":{"type":{"type":"string","title":"Type","default":"about:blank"},"title":{"type":"string","title":"Title"},"status":{"type":"integer","title":"Status"},"detail":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Detail"}},"type":"object","required":["title","status"],"title":"Problem","description":"RFC 7807 error body, served as application/problem+json."}}}},"403":{"description":"API key is valid but not scoped for this service — use a key scoped for `required_scope` (do not rotate); `granted_scopes` lists where this key works","content":{"application/problem+json":{"schema":{"properties":{"type":{"type":"string","title":"Type","default":"about:blank"},"title":{"type":"string","title":"Title"},"status":{"type":"integer","title":"Status"},"detail":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Detail"},"code":{"type":"string","title":"Code","default":"insufficient_service_scope"},"required_scope":{"type":"string","title":"Required Scope"},"granted_scopes":{"items":{"type":"string"},"type":"array","title":"Granted Scopes"}},"type":"object","required":["title","status","required_scope","granted_scopes"],"title":"InsufficientScopeProblem","description":"The 403 shape (per-service key scopes): valid key, not scoped for THIS service. The\nextension members make remediation deterministic for agents — `code` to dispatch on without\nparsing English, `required_scope` = the service refusing, `granted_scopes` = where the key\ndoes work. Emitted with `WWW-Authenticate: Bearer error=\"insufficient_scope\", scope=\"...\"`.\nDistinct from 401 (rotate the key) and 503 (retry): the fix is a key scoped for this\nservice, never rotation."}}}},"429":{"description":"Back off for `Retry-After` seconds; the key is valid and needs no rotation. Either the account reached its draft ceiling for the current window (app/spend_cap.py; no `code`), or — `code: free_tier_quota` — the key is over the platform's free-tier quota: `reason` (`account_quota` | `fleet_quota`) and `resets_at` say why, and until when","headers":{"Retry-After":{"description":"Seconds to wait before retrying — for the free-tier quota, until `resets_at`.","schema":{"type":"integer"}}},"content":{"application/problem+json":{"schema":{"properties":{"type":{"type":"string","title":"Type","default":"about:blank"},"title":{"type":"string","title":"Title"},"status":{"type":"integer","title":"Status"},"detail":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Detail"},"code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Code"},"reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reason"},"resets_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Resets At"}},"type":"object","required":["title","status"],"title":"QuotaProblem","description":"The 429 shape. The platform's free-tier meter (`code: free_tier_quota`) adds `reason` —\n`account_quota` (this builder's calls for the month are used) or `fleet_quota` (every\nself-serve key is paused for the day) — and `resets_at` (ISO-8601 UTC), with Retry-After\ncounting down to it. This service's own draft ceiling (app/spend_cap.py) is a plain Problem\non the same status: no `code`, Retry-After only. Either way the key is valid: never rotate."}}}},"503":{"description":"Auth not configured / auth or usage store unavailable","content":{"application/problem+json":{"schema":{"properties":{"type":{"type":"string","title":"Type","default":"about:blank"},"title":{"type":"string","title":"Title"},"status":{"type":"integer","title":"Status"},"detail":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Detail"}},"type":"object","required":["title","status"],"title":"Problem","description":"RFC 7807 error body, served as application/problem+json."}}}}},"security":[{"HTTPBearer":[]}]}},"/v1/usage":{"get":{"tags":["usage"],"summary":"Your usage: month-to-date and lifetime drafts + token counts","operationId":"getUsage","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UsageResponse"}}}},"401":{"description":"Missing or invalid bearer token","content":{"application/problem+json":{"schema":{"properties":{"type":{"type":"string","title":"Type","default":"about:blank"},"title":{"type":"string","title":"Title"},"status":{"type":"integer","title":"Status"},"detail":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Detail"}},"type":"object","required":["title","status"],"title":"Problem","description":"RFC 7807 error body, served as application/problem+json."}}}},"403":{"description":"API key is valid but not scoped for this service — use a key scoped for `required_scope` (do not rotate); `granted_scopes` lists where this key works","content":{"application/problem+json":{"schema":{"properties":{"type":{"type":"string","title":"Type","default":"about:blank"},"title":{"type":"string","title":"Title"},"status":{"type":"integer","title":"Status"},"detail":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Detail"},"code":{"type":"string","title":"Code","default":"insufficient_service_scope"},"required_scope":{"type":"string","title":"Required Scope"},"granted_scopes":{"items":{"type":"string"},"type":"array","title":"Granted Scopes"}},"type":"object","required":["title","status","required_scope","granted_scopes"],"title":"InsufficientScopeProblem","description":"The 403 shape (per-service key scopes): valid key, not scoped for THIS service. The\nextension members make remediation deterministic for agents — `code` to dispatch on without\nparsing English, `required_scope` = the service refusing, `granted_scopes` = where the key\ndoes work. Emitted with `WWW-Authenticate: Bearer error=\"insufficient_scope\", scope=\"...\"`.\nDistinct from 401 (rotate the key) and 503 (retry): the fix is a key scoped for this\nservice, never rotation."}}}},"429":{"description":"Back off for `Retry-After` seconds; the key is valid and needs no rotation. Either the account reached its draft ceiling for the current window (app/spend_cap.py; no `code`), or — `code: free_tier_quota` — the key is over the platform's free-tier quota: `reason` (`account_quota` | `fleet_quota`) and `resets_at` say why, and until when","headers":{"Retry-After":{"description":"Seconds to wait before retrying — for the free-tier quota, until `resets_at`.","schema":{"type":"integer"}}},"content":{"application/problem+json":{"schema":{"properties":{"type":{"type":"string","title":"Type","default":"about:blank"},"title":{"type":"string","title":"Title"},"status":{"type":"integer","title":"Status"},"detail":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Detail"},"code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Code"},"reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reason"},"resets_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Resets At"}},"type":"object","required":["title","status"],"title":"QuotaProblem","description":"The 429 shape. The platform's free-tier meter (`code: free_tier_quota`) adds `reason` —\n`account_quota` (this builder's calls for the month are used) or `fleet_quota` (every\nself-serve key is paused for the day) — and `resets_at` (ISO-8601 UTC), with Retry-After\ncounting down to it. This service's own draft ceiling (app/spend_cap.py) is a plain Problem\non the same status: no `code`, Retry-After only. Either way the key is valid: never rotate."}}}},"503":{"description":"Auth not configured / auth or usage store unavailable","content":{"application/problem+json":{"schema":{"properties":{"type":{"type":"string","title":"Type","default":"about:blank"},"title":{"type":"string","title":"Title"},"status":{"type":"integer","title":"Status"},"detail":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Detail"}},"type":"object","required":["title","status"],"title":"Problem","description":"RFC 7807 error body, served as application/problem+json."}}}}},"security":[{"HTTPBearer":[]}]}}},"components":{"schemas":{"DraftRequest":{"properties":{"changes":{"type":"string","maxLength":100000,"minLength":1,"title":"Changes","description":"Free text: commit subjects + merged PR titles","examples":["- add CSV export to reports\n- fix crash when login token expires"]},"product":{"anyOf":[{"type":"string","maxLength":500},{"type":"null"}],"title":"Product","description":"One-line 'Name: what it does'","examples":["Goblin: return tracking that chases refunds for you"]},"voice":{"anyOf":[{"type":"string","maxLength":4000},{"type":"null"}],"title":"Voice","description":"Persona/tone guidance. Your account's brand-voice floor is composed beneath this server-side — you style on top of the floor, you can't replace it.","examples":["Warm, plain-spoken, no hype"]},"model":{"anyOf":[{"type":"string","maxLength":128},{"type":"null"}],"title":"Model","description":"Optional model override; must be in the service allowlist, else 400","examples":["claude-opus-4-8"]},"render_target":{"anyOf":[{"type":"string","enum":["embedded_fragment","standalone_document"]},{"type":"null"}],"title":"Render Target","description":"Consuming surface's relationship to document structure. 'embedded_fragment' (default): the caller owns the page chrome, so the body carries no headings (e.g. Goblin's /whats-new). 'standalone_document': the notes are the whole document and may use section headings (e.g. Dossier's Sparkle updater, a GitHub release).","examples":["embedded_fragment"]}},"type":"object","required":["changes"],"title":"DraftRequest"},"DraftResponse":{"properties":{"markdown":{"type":"string","title":"Markdown","description":"Trimmed release-notes Markdown; empty when no_news is true"},"no_news":{"type":"boolean","title":"No News","description":"True when nothing user-facing shipped (sentinel collapsed server-side)"}},"type":"object","required":["markdown","no_news"],"title":"DraftResponse"},"ServiceUsage":{"properties":{"month":{"additionalProperties":{"anyOf":[{"type":"integer"},{"type":"number"}]},"type":"object","title":"Month"},"lifetime":{"additionalProperties":{"anyOf":[{"type":"integer"},{"type":"number"}]},"type":"object","title":"Lifetime"}},"type":"object","required":["month","lifetime"],"title":"ServiceUsage","description":"Per-service totals: `events` plus that service's own quantity sums (bytes, seconds, ...)."},"UsagePeriod":{"properties":{"drafts":{"type":"integer","title":"Drafts"},"input_tokens":{"type":"integer","title":"Input Tokens"},"output_tokens":{"type":"integer","title":"Output Tokens"}},"type":"object","required":["drafts","input_tokens","output_tokens"],"title":"UsagePeriod"},"UsageResponse":{"properties":{"account":{"type":"string","title":"Account"},"product":{"type":"string","title":"Product"},"tenant":{"type":"string","title":"Tenant","description":"DEPRECATED alias of `product` (pre-accounts contract); will be removed in v2","deprecated":true},"month":{"$ref":"#/components/schemas/UsagePeriod","description":"UTC calendar month-to-date (drafting)"},"lifetime":{"$ref":"#/components/schemas/UsagePeriod"},"services":{"additionalProperties":{"$ref":"#/components/schemas/ServiceUsage"},"type":"object","title":"Services","description":"Cross-service breakdown for this product — every platform service that metered usage for it (only services with recorded events appear). Drafting appears as 'dispatch' with the same numbers as month/lifetime."}},"type":"object","required":["account","product","tenant","month","lifetime"],"title":"UsageResponse"},"VoiceResponse":{"properties":{"account":{"type":"string","title":"Account"},"voice_floor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Voice Floor","description":"Your account's brand-voice floor — composed beneath every draft's `voice` server-side. null = no floor configured (drafts use your voice as-is)."},"voice_floor_sha":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Voice Floor Sha","description":"sha256[:12] of the floor — matches the `floor_sha` on draft logs; cheap drift check against your canonical voice asset."}},"type":"object","required":["account","voice_floor","voice_floor_sha"],"title":"VoiceResponse"}},"securitySchemes":{"HTTPBearer":{"type":"http","description":"Service API key (`rn_...`), minted per product on the platform Control Plane.","scheme":"bearer"}}},"tags":[{"name":"drafting","description":"The capability: dev log in, release-notes Markdown out."},{"name":"voice","description":"Your account's brand-voice floor (composed beneath every draft)."},{"name":"usage","description":"Your metered usage (drafts + token counts)."},{"name":"discovery","description":"Who this service is and where its contracts live."}]}