{"openapi":"3.0.0","info":{"title":"Bonterra API","version":"1.0.0","description":"API for Fundraising Coach, conversations, analytics, feedback, and credits. For the external credit workflow, see /api/credits-guide."},"servers":[{"url":"/","description":"Same origin as this documentation; paths already include /api/v1"}],"paths":{"/api/v1/credits/balance":{"get":{"summary":"Get a user credit balance","description":"Check this before starting external work. A trusted backend supplies siteId, entityId, and userId with X-Token-Exchange-Secret. A Bearer JWT instead uses the identity in the token. Treat allocationLevel=global-default as missing provisioning; do not start external work. For unlimited allocations, remaining is -1 and isUnlimited is true.","security":[{"TokenExchangeSecret":[]},{"BearerAuth":[]}],"parameters":[{"name":"siteId","in":"query","schema":{"type":"string"},"description":"Required with the shared secret; ignored for JWT callers."},{"name":"entityId","in":"query","schema":{"type":"string"},"description":"Required with the shared secret; ignored for JWT callers."},{"name":"userId","in":"query","schema":{"type":"string"},"description":"Required with the shared secret; ignored for JWT callers."},{"name":"windowId","in":"query","schema":{"type":"string","pattern":"^\\d{4}-(0[1-9]|1[0-2])$"},"description":"UTC month, YYYY-MM; defaults to the current month."},{"name":"details","in":"query","schema":{"type":"boolean","default":false},"description":"Set true to include the allocation hierarchy."}],"responses":{"200":{"description":"Resolved balance for the wallet","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditBalance"}}}},"400":{"description":"Invalid or missing query parameters"},"403":{"description":"Invalid or missing authentication"},"500":{"description":"Server error"}}}},"/api/v1/credits/allocation":{"put":{"summary":"Provision or replace a monthly credit allocation","description":"Upsert an allocation for the given UTC month. Provide siteId and entityId for an entity allocation; omit entityId for a site allocation, or add userId for a user allocation. Each new month starts at zero consumption and must be provisioned separately. allocation=-1 means unlimited. This operation changes quota, so use a trusted backend or a Bonterra staff JWT.","security":[{"TokenExchangeSecret":[]},{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["siteId","allocation"],"properties":{"siteId":{"type":"string"},"entityId":{"type":"string"},"userId":{"type":"string","description":"Requires entityId when present."},"allocation":{"type":"integer","minimum":-1,"description":"-1 means unlimited; otherwise use a non-negative integer."},"windowId":{"type":"string","pattern":"^\\d{4}-(0[1-9]|1[0-2])$","description":"UTC month, YYYY-MM; defaults to current month."}},"example":{"siteId":"example-site","entityId":"example-entity","allocation":100,"windowId":"2026-09"}}}}},"responses":{"200":{"description":"Allocation created or replaced","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreditAllocationUpdate"}}}},"400":{"description":"Invalid allocation or request body"},"403":{"description":"Invalid shared secret or non-staff JWT"},"500":{"description":"Server error"}}}},"/api/v1/credits/consumption/entity":{"get":{"summary":"List non-zero credit consumption for an entity","description":"Returns one page of users with non-zero consumption for an entity and month. Use nextCursor until absent. Months before the first full UTC month after index rollout may omit users; historical entity reports are not supported. Requires a trusted backend secret or Bonterra staff JWT.","security":[{"TokenExchangeSecret":[]},{"BearerAuth":[]}],"parameters":[{"name":"siteId","in":"query","required":true,"schema":{"type":"string"}},{"name":"entityId","in":"query","required":true,"schema":{"type":"string"}},{"name":"windowId","in":"query","required":false,"schema":{"type":"string","pattern":"^\\d{4}-(0[1-9]|1[0-2])$"},"description":"UTC month, YYYY-MM; defaults to current month."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":100}},{"name":"cursor","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Entity consumption page","content":{"application/json":{"schema":{"type":"object","required":["siteId","entityId","windowId","usage"],"properties":{"siteId":{"type":"string"},"entityId":{"type":"string"},"windowId":{"type":"string"},"usage":{"type":"array","items":{"type":"object","required":["userId","consumed"],"properties":{"userId":{"type":"string"},"consumed":{"type":"number"}}}},"nextCursor":{"type":"string"}}}}}},"400":{"description":"Invalid query or cursor"},"403":{"description":"Unauthorized"},"500":{"description":"Server error"}}}},"/api/v1/credits/deductions":{"post":{"summary":"Create an external credit deduction","description":"Call from a trusted backend only, after completed external work. Check GET /api/v1/credits/balance before starting work and do not start when exhausted or not explicitly provisioned. Once work succeeds, submit the deduction even if another operation has since exhausted the wallet: a new charge returns 201, exhausted=true, and remaining may be negative. Use a stable eventId per completed work item; on timeout or HTTP 500, 502, 503, or 504 retry the exact same request with the same eventId. A replay returns 200 and does not charge twice. Never put participant records, prompts, or generated content in metadata.","security":[{"TokenExchangeSecret":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["eventId","caller","siteId","entityId","userId","credits","actionType","pricingVersion","occurredAt"],"properties":{"eventId":{"type":"string","maxLength":200,"pattern":"^[a-zA-Z0-9][a-zA-Z0-9._:@/-]{0,199}$","description":"Stable ID for one completed work item. Reuse unchanged for retries."},"caller":{"type":"string","maxLength":100,"description":"Stable backend service name; idempotency is scoped to caller + eventId."},"siteId":{"type":"string"},"entityId":{"type":"string"},"userId":{"type":"string"},"credits":{"type":"integer","minimum":1,"maximum":100000},"actionType":{"type":"string","maxLength":100,"description":"Caller-owned action name."},"pricingVersion":{"type":"string","maxLength":100,"description":"Caller-owned price version."},"occurredAt":{"type":"string","format":"date-time","description":"When work occurred. New deductions must be no more than five minutes in the future and no earlier than the first day of the previous UTC month; an identical replay remains valid later."},"metadata":{"type":"object","additionalProperties":true,"description":"Optional safe labels only; maximum 4096 bytes as JSON."}},"example":{"eventId":"job-123","caller":"apricot","siteId":"example-site","entityId":"example-entity","userId":"example-user","credits":10,"actionType":"report","pricingVersion":"v1","occurredAt":"2026-09-14T12:00:00Z"}}}}},"responses":{"200":{"description":"Identical request already applied; no second charge","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalCreditDeductionResult"}}}},"201":{"description":"New deduction recorded, including when the wallet became overdrawn after work began","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExternalCreditDeductionResult"}}}},"400":{"description":"Invalid request or occurrence time; fix the request rather than retrying unchanged"},"403":{"description":"Invalid or missing token exchange secret"},"404":{"description":"No explicit credit allocation exists; provision the wallet"},"409":{"description":"The caller + eventId was already used with different request data; fix the caller"},"500":{"description":"Server error; retry the same request and eventId"},"502":{"description":"Gateway error; retry the same request and eventId"},"503":{"description":"Temporarily unavailable; retry the same request and eventId"},"504":{"description":"Gateway timeout; retry the same request and eventId"}}}},"/api/v1/agent/chat":{"post":{"summary":"Stream chat with an AI agent","description":"Initiates a streaming chat conversation with a specified AI agent. Creates a new conversation or continues an existing one.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["message","agentId","siteId"],"properties":{"message":{"type":"string","description":"The user message to send to the agent"},"agentId":{"type":"string","description":"The ID of the agent to chat with"},"siteId":{"type":"string","description":"The site ID for the conversation"},"threadId":{"type":"string","description":"Optional thread ID for continuing an existing conversation"},"userId":{"type":"string","description":"Optional user ID for the conversation"}}}}}},"parameters":[{"name":"X-Bonterra-User-Id","in":"header","description":"DEPRECATED: User ID for authentication and conversation ownership. Use JWT authentication instead.","required":false,"deprecated":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Streaming response from the agent","headers":{"X-Thread-Id":{"description":"Thread ID for new conversations","schema":{"type":"string"}}},"content":{"text/plain":{"schema":{"type":"string","description":"Streaming text response from the agent"}}}},"400":{"description":"Bad request - missing required parameters or unknown agent","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Conversation not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/v1/agent/suggest":{"post":{"summary":"Get suggestions from an AI agent","description":"Generates conversation suggestions from a specified AI agent, either initial suggestions or contextual suggestions based on conversation history.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["agentId","siteId"],"properties":{"agentId":{"type":"string","description":"The ID of the agent to get suggestions from"},"siteId":{"type":"string","description":"The site ID for the suggestions"},"threadId":{"type":"string","description":"Optional thread ID for contextual suggestions based on conversation history"},"userId":{"type":"string","description":"Optional user ID for the suggestions"}}}}}},"parameters":[{"name":"X-Bonterra-User-Id","in":"header","description":"DEPRECATED: User ID for authentication. Use JWT authentication instead.","required":false,"deprecated":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response with suggestions","content":{"application/json":{"schema":{"type":"object","properties":{"suggestions":{"type":"array","items":{"type":"string"},"description":"Array of suggested conversation starters or follow-up questions"}}}}}},"400":{"description":"Bad request - missing required parameters or unknown agent","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/v1/conversations":{"get":{"summary":"List conversations or get a specific conversation","description":"Retrieves conversations based on filters. Can fetch a specific conversation by threadId or list conversations filtered by siteId, agentId, and/or userId.","parameters":[{"name":"threadId","in":"query","description":"Specific conversation thread ID to retrieve","required":false,"schema":{"type":"string"}},{"name":"siteId","in":"query","description":"Filter conversations by site ID","required":false,"schema":{"type":"string"}},{"name":"agentId","in":"query","description":"Filter conversations by agent ID","required":false,"schema":{"type":"string"}},{"name":"limit","in":"query","description":"Maximum number of conversations to return (default: 10)","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":10}},{"name":"nextToken","in":"query","description":"Pagination token for retrieving the next page of results","required":false,"schema":{"type":"string"}},{"name":"X-Bonterra-User-Id","in":"header","description":"DEPRECATED: User ID for authentication and filtering conversations by user. Use JWT authentication instead.","deprecated":true,"required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Successful response","content":{"application/json":{"schema":{"oneOf":[{"type":"object","description":"Single conversation response (when threadId is provided)","properties":{"threadId":{"type":"string"},"agentId":{"type":"string"},"siteId":{"type":"string"},"userId":{"type":"string"},"created":{"type":"string","format":"date-time"},"updated":{"type":"string","format":"date-time"},"messages":{"type":"array","items":{"type":"object","properties":{"role":{"type":"string","enum":["user","assistant","system"]},"content":{"type":"string"}}}}}},{"type":"object","description":"List of conversations response","properties":{"conversations":{"type":"array","items":{"type":"object","properties":{"threadId":{"type":"string"},"agentId":{"type":"string"},"siteId":{"type":"string"},"userId":{"type":"string"},"created":{"type":"string","format":"date-time"},"updated":{"type":"string","format":"date-time"}}}},"nextToken":{"type":"string","description":"Token for retrieving the next page of results"}}}]}}}},"400":{"description":"Bad request - missing required filters","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"Conversation not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"string"}}}}}}}}},"/api/v1/analytics":{"post":{"summary":"Log analytics events","description":"Records analytics events from frontend or backend sources. Supports both modern and legacy event formats.","requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"type":"object","description":"Modern event format","required":["eventName","siteId","source","eventData"],"properties":{"eventName":{"type":"string","description":"Name of the analytics event"},"siteId":{"type":"string","nullable":true,"description":"Site ID associated with the event"},"source":{"type":"string","enum":["frontend","backend","agent"],"description":"Source of the event"},"eventData":{"type":"object","description":"Event-specific data"},"threadId":{"type":"string","description":"Optional thread ID for conversation-related events"}}},{"type":"object","description":"Legacy event format","required":["event","properties"],"properties":{"event":{"type":"string","description":"Name of the analytics event"},"sessionId":{"type":"string","description":"Session/thread ID"},"properties":{"type":"object","description":"Event properties"},"metadata":{"type":"object","description":"Additional metadata"}}}]}}}},"responses":{"200":{"description":"Event logged successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"}}}}}},"400":{"description":"Bad request - invalid event format","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"get":{"summary":"Analytics endpoint health check","description":"Simple health check for the analytics endpoint","responses":{"200":{"description":"Analytics endpoint is healthy","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["analytics-endpoint-ok"]}}}}}}}}},"/api/v1/auth/token":{"post":{"summary":"Exchange user context for JWT token","description":"Customer backend endpoint to exchange user context for a signed JWT token. This enables direct widget access to Bongentic APIs with proper authentication and tenant isolation.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["user_context"],"properties":{"user_context":{"type":"object","required":["user_id","entity_id","site_id","roles"],"properties":{"user_id":{"type":"string","description":"Unique user identifier","example":"user-123"},"entity_id":{"type":"string","description":"Organization/tenant identifier","example":"org-456"},"site_id":{"type":"string","description":"Product identifier","enum":["npohub","everyaction","nfg"],"example":"npohub"},"roles":{"type":"array","items":{"type":"string","enum":["user","admin","bonterra-staff"]},"minItems":1,"description":"User roles for authorization","example":["admin"]},"optional_claims":{"type":"object","description":"Product-specific optional claims","properties":{"admin_id":{"type":"integer","description":"NFG admin identifier","example":789},"department":{"type":"string","description":"User department","example":"marketing"}}}}}}}}}},"parameters":[{"name":"X-Token-Exchange-Secret","in":"header","description":"Token exchange secret for customer backend authentication","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"JWT token created successfully","content":{"application/json":{"schema":{"type":"object","properties":{"access_token":{"type":"string","description":"Signed JWT token for API access","example":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."},"expires_in":{"type":"integer","description":"Token expiration time in seconds","example":900},"token_type":{"type":"string","description":"Token type","example":"Bearer"}}}}}},"400":{"description":"Bad request - Invalid user context","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","example":"Missing required fields: user_id, entity_id, site_id"}}}}}},"403":{"description":"Forbidden - Invalid API secret","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","example":"Invalid or missing API secret"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string","example":"Internal server error"}}}}}}}}},"/api/v1/health":{"get":{"summary":"Check the health status of the API","description":"Simple health check endpoint","responses":{"200":{"description":"API is healthy","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["ok"]}}}}}}}}},"/api/v1/chat-feedback":{"post":{"summary":"Submit user feedback on agent messages","description":"Records user feedback (positive/negative) for specific messages in a conversation","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["messageIndex","feedbackType"],"properties":{"messageIndex":{"type":"integer","description":"Index of the message being rated"},"feedbackType":{"type":"string","enum":["positive","negative"],"description":"Type of feedback"},"feedbackText":{"type":"string","description":"Optional additional feedback text"},"messageContent":{"type":"string","description":"Optional content of the message being rated"},"siteId":{"type":"string","description":"The site ID"},"threadId":{"type":"string","description":"Optional thread ID"}}}}}},"responses":{"200":{"description":"Feedback received successfully","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/v1/report-error":{"post":{"summary":"Report errors from the frontend","description":"Records error reports from the frontend for monitoring and debugging","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["errorMessage"],"properties":{"errorMessage":{"type":"string","description":"Error message"},"siteId":{"type":"string","description":"The site ID"},"threadId":{"type":"string","description":"Optional thread ID"},"failedMessageIndex":{"type":"integer","description":"Optional index of the message that failed"}}}}}},"responses":{"200":{"description":"Error report received successfully","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}}},"components":{"securitySchemes":{"TokenExchangeSecret":{"type":"apiKey","in":"header","name":"X-Token-Exchange-Secret","description":"Server-to-server secret. Keep it on the backend; never expose it in a browser."},"BearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"JWT","description":"Credit reads accept a user JWT; allocation writes and entity reports require a Bonterra staff JWT. External deductions do not accept JWTs."}},"schemas":{"CreditBalance":{"type":"object","required":["siteId","entityId","userId","allocation","allocationLevel","consumed","remaining","isUnlimited","exhausted","percentUsed","windowId","windowResetDate"],"properties":{"siteId":{"type":"string"},"entityId":{"type":"string"},"userId":{"type":"string"},"allocation":{"type":"number","description":"-1 represents unlimited credits."},"allocationLevel":{"type":"string","enum":["user","entity","site","global-default","none"]},"consumed":{"type":"number"},"remaining":{"type":"number","description":"-1 for unlimited credits; can be negative after concurrent completed work."},"isUnlimited":{"type":"boolean"},"exhausted":{"type":"boolean"},"percentUsed":{"type":"number"},"windowId":{"type":"string","description":"UTC month, YYYY-MM."},"windowResetDate":{"type":"string","format":"date","description":"First UTC day of the next month, YYYY-MM-DD."},"hierarchy":{"type":"object","description":"Present only when details=true.","properties":{"user":{"type":"object","nullable":true,"additionalProperties":true},"entity":{"type":"object","nullable":true,"additionalProperties":true},"site":{"type":"object","nullable":true,"additionalProperties":true}}}}},"CreditAllocationUpdate":{"type":"object","required":["updated"],"properties":{"updated":{"type":"object","required":["level","siteId","allocation","windowId"],"properties":{"level":{"type":"string","enum":["site","entity","user"]},"siteId":{"type":"string"},"entityId":{"type":"string"},"userId":{"type":"string"},"allocation":{"type":"integer"},"windowId":{"type":"string","description":"UTC month, YYYY-MM."}}}}},"ExternalCreditDeductionResult":{"type":"object","required":["deductionId","credits","windowId","consumed","remaining","exhausted","isUnlimited","duplicate"],"properties":{"deductionId":{"type":"string","description":"The eventId from the request."},"credits":{"type":"integer"},"windowId":{"type":"string","description":"UTC month charged, YYYY-MM."},"consumed":{"type":"number"},"remaining":{"type":"number","description":"May be negative when multiple completed jobs overdraw the wallet."},"exhausted":{"type":"boolean"},"isUnlimited":{"type":"boolean"},"duplicate":{"type":"boolean","description":"True for an identical replay (HTTP 200); false for a new charge (HTTP 201)."}},"example":{"deductionId":"job-123","credits":10,"windowId":"2026-09","consumed":105,"remaining":-5,"exhausted":true,"isUnlimited":false,"duplicate":false}},"Error":{"type":"object","properties":{"error":{"type":"string"},"details":{"type":"string"}}},"Message":{"type":"object","properties":{"role":{"type":"string","enum":["user","assistant","system"]},"content":{"type":"string"}}},"Conversation":{"type":"object","properties":{"threadId":{"type":"string"},"agentId":{"type":"string"},"siteId":{"type":"string"},"userId":{"type":"string"},"created":{"type":"string","format":"date-time"},"updated":{"type":"string","format":"date-time"},"messages":{"type":"array","items":{"$ref":"#/components/schemas/Message"}}}}}}}