{"openapi": "3.1.0", "info": {"title": "over:heard Partner API", "version": "1.0.0", "description": "Brand-reputation stories, advisories, Brand Notes and delegations over REST. Partner plan only; keys are scoped, hashed at rest and apply exactly the UI's permission rules. The same operations are exposed as MCP tools (v1_*) \u2014 see https://overheard.co/developers/mcp."}, "servers": [{"url": "https://overheard.co"}], "paths": {"/v1/stories": {"get": {"operationId": "list_stories", "summary": "list stories", "description": "List stories (mentions) of one scoped brand.", "tags": ["v1"], "security": [{"ApiKeyAuth": []}, {"BearerAuth": []}], "responses": {"200": {"description": "Success", "content": {"application/json": {"schema": {"type": "object", "properties": {"brand": {"type": "string"}, "stories": {"type": "array", "items": {"type": "object", "properties": {"event_uri": {"type": "string"}, "title": {"type": "string"}, "severity": {"type": "string"}, "detected_at": {"type": "string"}}}}}}, "example": {"brand": "acme.com", "stories": [{"event_uri": "ev_1a2b3c4d5e6f", "title": "Example story headline", "severity": "high", "detected_at": "2026-07-20T09:15:00Z"}]}}}}, "401": {"description": "Missing, invalid or revoked API key (state is read per request \u2014 revocation cuts at the next call).", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "example": {"error": "unknown or revoked API key"}}}}, "403": {"description": "Authenticated but refused: plan without API access, brand outside this key's scope, or a capability the key can never hold.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "example": {"error": "outside this key's brand scope"}}}}, "404": {"description": "Not found \u2014 unknown/foreign brands answer 404, never confirming existence (anti-IDOR).", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "example": {"error": "not found"}}}}, "429": {"description": "Rate limit exceeded (120/min per key, fixed 60s window). Honour Retry-After.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "example": {"error": "rate limit exceeded"}}}}}, "parameters": [{"name": "brand", "in": "query", "required": true, "schema": {"type": "string", "minLength": 1, "description": "brand host, e.g. danone.com"}, "description": "brand host, e.g. danone.com"}]}}, "/v1/story": {"get": {"operationId": "get_story", "summary": "get story", "description": "Read one story of a scoped brand (advisory fields follow the same paywall as the UI).", "tags": ["v1"], "security": [{"ApiKeyAuth": []}, {"BearerAuth": []}], "responses": {"200": {"description": "Success", "content": {"application/json": {"schema": {"type": "object", "properties": {"brand": {"type": "string"}, "story": {"type": "object", "properties": {"event_uri": {"type": "string"}, "title": {"type": "string"}, "severity": {"type": "string"}, "detected_at": {"type": "string"}, "topics": {"type": "array", "items": {"type": "string"}}, "advisory": {"type": "object", "properties": {"assessment": {"type": "string"}, "urgency": {"type": "string"}, "executive_summary": {"type": "array", "items": {"type": "object", "properties": {"text": {"type": "string"}, "cites": {"type": "array", "items": {"type": "string"}}}}}, "schema": {"type": "string"}, "sources": {"type": "array", "items": {"type": "object", "properties": {"id": {"type": "string"}, "url": {"type": "string"}, "publisher": {"type": "string"}, "title": {"type": "string"}, "published_at": {"type": "string"}, "type": {"type": "string"}}}}, "summary_basis": {"type": "array", "items": {"type": "string"}}, "locked": {"type": "boolean"}}}}}}}, "example": {"brand": "acme.com", "story": {"event_uri": "ev_1a2b3c4d5e6f", "title": "Example story headline", "severity": "high", "detected_at": "2026-07-20T09:15:00Z", "topics": ["cyber-databreach"], "advisory": {"assessment": "threat", "urgency": "high", "executive_summary": [{"text": "What happened and why it matters.", "cites": ["c01"]}, {"text": "What we recommend.", "cites": []}], "schema": "v4", "sources": [{"id": "s01", "url": "https://news.example/a", "publisher": "Reuters", "title": "Example coverage", "published_at": "2026-07-20", "type": "news"}], "summary_basis": ["c01"], "locked": true}}}}}}, "401": {"description": "Missing, invalid or revoked API key (state is read per request \u2014 revocation cuts at the next call).", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "example": {"error": "unknown or revoked API key"}}}}, "403": {"description": "Authenticated but refused: plan without API access, brand outside this key's scope, or a capability the key can never hold.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "example": {"error": "outside this key's brand scope"}}}}, "404": {"description": "Not found \u2014 unknown/foreign brands answer 404, never confirming existence (anti-IDOR).", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "example": {"error": "not found"}}}}, "429": {"description": "Rate limit exceeded (120/min per key, fixed 60s window). Honour Retry-After.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "example": {"error": "rate limit exceeded"}}}}}, "parameters": [{"name": "brand", "in": "query", "required": true, "schema": {"type": "string", "minLength": 1, "description": "brand host"}, "description": "brand host"}, {"name": "event_uri", "in": "query", "required": true, "schema": {"type": "string", "minLength": 1, "description": "story id"}, "description": "story id"}]}}, "/v1/brand-note": {"post": {"operationId": "post_brand_note", "summary": "post brand note", "description": "Publish the official Brand Note on a story \u2014 gated: verified or delegated brands only. Never deletes: an empty or missing text is refused (use delete_brand_note).", "tags": ["v1"], "security": [{"ApiKeyAuth": []}, {"BearerAuth": []}], "responses": {"200": {"description": "Success", "content": {"application/json": {"schema": {"type": "object", "properties": {"ok": {"type": "boolean"}, "brand": {"type": "string"}, "event_uri": {"type": "string"}}}, "example": {"ok": true, "brand": "acme.com", "event_uri": "ev_1a2b3c4d5e6f"}}}}, "401": {"description": "Missing, invalid or revoked API key (state is read per request \u2014 revocation cuts at the next call).", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "example": {"error": "unknown or revoked API key"}}}}, "403": {"description": "Authenticated but refused: plan without API access, brand outside this key's scope, or a capability the key can never hold.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "example": {"error": "outside this key's brand scope"}}}}, "404": {"description": "Not found \u2014 unknown/foreign brands answer 404, never confirming existence (anti-IDOR).", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "example": {"error": "not found"}}}}, "429": {"description": "Rate limit exceeded (120/min per key, fixed 60s window). Honour Retry-After.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "example": {"error": "rate limit exceeded"}}}}}, "parameters": [{"name": "Idempotency-Key", "in": "header", "required": false, "schema": {"type": "string"}, "description": "A retried POST with the same key returns the ORIGINAL stored result (never re-executes)."}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "additionalProperties": false, "properties": {"brand": {"type": "string", "minLength": 1, "description": "brand host"}, "event_uri": {"type": "string", "minLength": 1, "description": "story id"}, "text": {"type": "string", "minLength": 1, "maxLength": 600, "description": "note (600 chars max)"}}, "required": ["brand", "event_uri", "text"]}}}}}, "delete": {"operationId": "delete_brand_note", "summary": "delete brand note", "description": "Remove the official Brand Note from a story \u2014 the ONLY way to delete it, and an explicit intent. Same gate as publishing.", "tags": ["v1"], "security": [{"ApiKeyAuth": []}, {"BearerAuth": []}], "responses": {"200": {"description": "Success", "content": {"application/json": {"schema": {"type": "object", "properties": {"ok": {"type": "boolean"}, "brand": {"type": "string"}, "event_uri": {"type": "string"}, "deleted": {"type": "boolean"}}}, "example": {"ok": true, "brand": "acme.com", "event_uri": "ev_1a2b3c4d5e6f", "deleted": true}}}}, "401": {"description": "Missing, invalid or revoked API key (state is read per request \u2014 revocation cuts at the next call).", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "example": {"error": "unknown or revoked API key"}}}}, "403": {"description": "Authenticated but refused: plan without API access, brand outside this key's scope, or a capability the key can never hold.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "example": {"error": "outside this key's brand scope"}}}}, "404": {"description": "Not found \u2014 unknown/foreign brands answer 404, never confirming existence (anti-IDOR).", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "example": {"error": "not found"}}}}, "429": {"description": "Rate limit exceeded (120/min per key, fixed 60s window). Honour Retry-After.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "example": {"error": "rate limit exceeded"}}}}}, "parameters": [{"name": "brand", "in": "query", "required": true, "schema": {"type": "string", "minLength": 1, "description": "brand host"}, "description": "brand host"}, {"name": "event_uri", "in": "query", "required": true, "schema": {"type": "string", "minLength": 1, "description": "story id"}, "description": "story id"}]}}, "/v1/advisory": {"post": {"operationId": "trigger_advisory", "summary": "trigger advisory", "description": "Request the full advisory for a story. Consumes the same per-brand quota and bills exactly like the UI (15 EUR beyond the included).", "tags": ["v1"], "security": [{"ApiKeyAuth": []}, {"BearerAuth": []}], "responses": {"200": {"description": "Success", "content": {"application/json": {"schema": {"type": "object", "properties": {"ok": {"type": "boolean"}, "counted": {"type": "boolean"}, "billed_eur": {"type": "number"}, "note": {"type": "string"}}}, "example": {"ok": true, "counted": true, "billed_eur": 0, "note": "identical quota + billing as the UI path"}}}}, "401": {"description": "Missing, invalid or revoked API key (state is read per request \u2014 revocation cuts at the next call).", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "example": {"error": "unknown or revoked API key"}}}}, "403": {"description": "Authenticated but refused: plan without API access, brand outside this key's scope, or a capability the key can never hold.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "example": {"error": "outside this key's brand scope"}}}}, "404": {"description": "Not found \u2014 unknown/foreign brands answer 404, never confirming existence (anti-IDOR).", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "example": {"error": "not found"}}}}, "429": {"description": "Rate limit exceeded (120/min per key, fixed 60s window). Honour Retry-After.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "example": {"error": "rate limit exceeded"}}}}}, "parameters": [{"name": "Idempotency-Key", "in": "header", "required": false, "schema": {"type": "string"}, "description": "A retried POST with the same key returns the ORIGINAL stored result (never re-executes)."}], "requestBody": {"required": true, "content": {"application/json": {"schema": {"type": "object", "additionalProperties": false, "properties": {"brand": {"type": "string", "minLength": 1, "description": "brand host"}, "event_uri": {"type": "string", "minLength": 1, "description": "story id"}}, "required": ["brand", "event_uri"]}}}}}}, "/v1/delegations": {"get": {"operationId": "list_delegations", "summary": "list delegations", "description": "Delegation state of the brands in this key's scope.", "tags": ["v1"], "security": [{"ApiKeyAuth": []}, {"BearerAuth": []}], "responses": {"200": {"description": "Success", "content": {"application/json": {"schema": {"type": "object", "properties": {"delegations": {"type": "array", "items": {"type": "object", "properties": {"brand_key": {"type": "string"}, "status": {"type": "string"}, "client": {"type": "string"}}}}}}, "example": {"delegations": [{"brand_key": "acme.com", "status": "active", "client": "ck_client1"}]}}}}, "401": {"description": "Missing, invalid or revoked API key (state is read per request \u2014 revocation cuts at the next call).", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "example": {"error": "unknown or revoked API key"}}}}, "403": {"description": "Authenticated but refused: plan without API access, brand outside this key's scope, or a capability the key can never hold.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "example": {"error": "outside this key's brand scope"}}}}, "404": {"description": "Not found \u2014 unknown/foreign brands answer 404, never confirming existence (anti-IDOR).", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "example": {"error": "not found"}}}}, "429": {"description": "Rate limit exceeded (120/min per key, fixed 60s window). Honour Retry-After.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "example": {"error": "rate limit exceeded"}}}}}, "parameters": []}, "post": {"operationId": "grant_delegation", "summary": "grant delegation", "description": "Always 403: delegation is an explicit client-side act \u2014 a key can never self-delegate.", "tags": ["v1"], "security": [{"ApiKeyAuth": []}, {"BearerAuth": []}], "responses": {"401": {"description": "Missing, invalid or revoked API key (state is read per request \u2014 revocation cuts at the next call).", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "example": {"error": "unknown or revoked API key"}}}}, "403": {"description": "Authenticated but refused: plan without API access, brand outside this key's scope, or a capability the key can never hold.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "example": {"error": "outside this key's brand scope"}}}}, "404": {"description": "Not found \u2014 unknown/foreign brands answer 404, never confirming existence (anti-IDOR).", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "example": {"error": "not found"}}}}, "429": {"description": "Rate limit exceeded (120/min per key, fixed 60s window). Honour Retry-After.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "example": {"error": "rate limit exceeded"}}}}}, "parameters": [{"name": "Idempotency-Key", "in": "header", "required": false, "schema": {"type": "string"}, "description": "A retried POST with the same key returns the ORIGINAL stored result (never re-executes)."}]}}, "/v1/api-keys": {"post": {"operationId": "create_api_key", "summary": "create api key", "description": "Always 403: API keys are managed by Owners/Admins in the app \u2014 a key can never mint keys.", "tags": ["v1"], "security": [{"ApiKeyAuth": []}, {"BearerAuth": []}], "responses": {"401": {"description": "Missing, invalid or revoked API key (state is read per request \u2014 revocation cuts at the next call).", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "example": {"error": "unknown or revoked API key"}}}}, "403": {"description": "Authenticated but refused: plan without API access, brand outside this key's scope, or a capability the key can never hold.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "example": {"error": "outside this key's brand scope"}}}}, "404": {"description": "Not found \u2014 unknown/foreign brands answer 404, never confirming existence (anti-IDOR).", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "example": {"error": "not found"}}}}, "429": {"description": "Rate limit exceeded (120/min per key, fixed 60s window). Honour Retry-After.", "content": {"application/json": {"schema": {"$ref": "#/components/schemas/Error"}, "example": {"error": "rate limit exceeded"}}}}}, "parameters": [{"name": "Idempotency-Key", "in": "header", "required": false, "schema": {"type": "string"}, "description": "A retried POST with the same key returns the ORIGINAL stored result (never re-executes)."}]}}}, "components": {"schemas": {"Error": {"type": "object", "properties": {"error": {"type": "string"}}}}, "securitySchemes": {"ApiKeyAuth": {"type": "apiKey", "in": "header", "name": "x-api-key", "description": "Partner API key (ok_\u2026) created in the portal (API keys \u2014 Owner/Admin). Scoped to brands, hashed at rest; revocation applies on the key's next call."}, "BearerAuth": {"type": "http", "scheme": "bearer", "description": "The same ok_\u2026 key sent as Authorization: Bearer ok_\u2026"}}}, "security": [{"ApiKeyAuth": []}]}