{"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":"Returns the credit balance of the user the request identifies. Check this before starting external work. 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":["agentId","siteId"],"description":"Send messages, or the deprecated message.","properties":{"messages":{"type":"array","items":{"type":"object"},"description":"Conversation as UI message objects ({ id, role, parts }); parts may carry file attachments"},"message":{"type":"string","deprecated":true,"description":"Deprecated single user message; used only when messages is empty"},"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"},"threadKey":{"type":"string","description":"Optional caller-chosen key that maps to one thread; threadId wins when both are sent"},"timezone":{"type":"string","description":"Optional IANA timezone of the user, e.g. 'America/Los_Angeles'"},"userId":{"type":"string","deprecated":true,"description":"Deprecated and ignored; the user comes from the token"}}}}}},"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":"Fetches one of the caller's conversations by threadId, or lists the caller's conversations, optionally filtered by agentId.","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: 50)","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"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 sent by the frontend.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["eventName","source","eventData"],"properties":{"eventName":{"type":"string","description":"Name of the analytics event"},"siteId":{"type":"string","nullable":true,"description":"Site ID associated with the event; defaults to the caller's site"},"source":{"type":"string","enum":["frontend"],"description":"Source of the event"},"eventData":{"type":"object","description":"Event-specific data"},"threadId":{"type":"string","description":"Optional thread ID for conversation-related events"}}}}}},"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"}}}}}}}}},"/api/v1/tools":{"get":{"summary":"List available tools","description":"Returns metadata for every registered tool whose feature flag is enabled for the caller's context.","security":[{"BearerAuth":[]}],"responses":{"200":{"description":"Tool list: { tools: ToolMetadata[] }"},"403":{"description":"No authenticated user context"},"500":{"description":"Server error"}}}},"/api/v1/tools/{toolName}":{"get":{"summary":"Get one tool and its input schema","description":"Returns metadata and the full input schema for a single registered, enabled tool.","security":[{"BearerAuth":[]}],"parameters":[{"name":"toolName","in":"path","required":true,"schema":{"type":"string"},"description":"Tool name as registered in ToolRegistry"}],"responses":{"200":{"description":"Tool metadata: { tool: ToolMetadata }"},"403":{"description":"No authenticated user context"},"404":{"description":"Tool not found or not enabled"},"500":{"description":"Server error"}}}},"/api/v1/tools/{toolName}/execute":{"post":{"summary":"Execute a tool directly","description":"Runs a tool with the given parameters without an agent or LLM turn; streams SSE when the tool streams and the client accepts text/event-stream.","security":[{"BearerAuth":[]}],"parameters":[{"name":"toolName","in":"path","required":true,"schema":{"type":"string"},"description":"Tool name as registered in ToolRegistry"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["parameters"],"properties":{"parameters":{"type":"object","description":"Tool-specific parameters"},"threadId":{"type":"string","description":"Existing conversation ID; wins over threadKey"},"threadKey":{"type":"string","description":"Caller key converted server-side to a deterministic threadId"},"agentId":{"type":"string","description":"Agent context for product-specific routing"}}}}}},"responses":{"200":{"description":"Tool result (JSON, or SSE for streaming tools)"},"400":{"description":"Invalid tool name, body or headers"},"401":{"description":"Unauthorized"},"403":{"description":"Tool not enabled, or agent not allowed for this site"},"404":{"description":"Tool or thread not found"},"413":{"description":"Request body too large"},"500":{"description":"Tool execution failed"},"503":{"description":"Entitlement flags could not be read"}}}},"/api/v1/actions":{"get":{"summary":"List user-triggered actions","description":"Returns the action handlers available for deterministic button-click actions in tool cards.","security":[{"BearerAuth":[]}],"responses":{"200":{"description":"Action list: { actions }"},"401":{"description":"Unauthorized"},"500":{"description":"Server error"}}},"post":{"summary":"Execute an action without persistence","description":"Runs an action handler directly, bypassing the LLM, and does not write to conversation history.","security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["handler","params"],"properties":{"handler":{"type":"string","description":"Handler identifier, e.g. 'everyaction/save-query'"},"params":{"type":"object","description":"Handler-specific parameters"},"threadId":{"type":"string","description":"Conversation the action belongs to; must be owned by the caller"}}}}}},"responses":{"200":{"description":"Action succeeded: { success, data }"},"400":{"description":"Missing handler, params or required handler parameters, or any other error the handler threw (message in error)"},"401":{"description":"Unauthorized"},"404":{"description":"Handler or conversation not found"},"500":{"description":"The handler returned success: false, or an unexpected server error"},"503":{"description":"Entitlement flags could not be read"}}}},"/api/v1/actions/{threadKeyOrId}":{"post":{"summary":"Execute an action and persist it to the conversation","description":"Runs an action handler, appends a synthetic message to the conversation history and returns it with the result.","security":[{"BearerAuth":[]}],"parameters":[{"name":"threadKeyOrId","in":"path","required":true,"schema":{"type":"string"},"description":"Conversation thread ID"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["handler","params"],"properties":{"handler":{"type":"string","description":"Handler identifier, e.g. 'everyaction/save-query'"},"params":{"type":"object","description":"Handler-specific parameters"}}}}}},"responses":{"200":{"description":"Action succeeded: { success, data, message }"},"400":{"description":"Missing handler, params or required handler parameters, or any other error the handler threw (message in error)"},"401":{"description":"Unauthorized"},"404":{"description":"Handler or conversation not found"},"500":{"description":"The handler returned success: false, or an unexpected server error"},"503":{"description":"Entitlement flags could not be read"}}}},"/api/v1/config/{toolName}":{"parameters":[{"name":"toolName","in":"path","required":true,"schema":{"type":"string"},"description":"Tool name; letters, digits, _ and -"},{"name":"siteId","in":"query","required":false,"schema":{"type":"string"},"description":"Target scope: site. Without it the scope is global; global and site scopes are staff-only"},{"name":"entityId","in":"query","required":false,"schema":{"type":"string"},"description":"Target scope: entity (with siteId); the entity's admins or staff. GET still checks it for Auth0 callers; only its lookup uses the token's entity"},{"name":"userId","in":"query","required":false,"schema":{"type":"string"},"description":"Target scope: user (with siteId); that user, an admin of the same site, or staff"}],"get":{"summary":"Get a tool configuration","description":"Returns the tool configuration resolved through the user, entity, site and global cascade, starting at the scope named by the query parameters. Non-staff callers name their own user scope (siteId and userId) or, as admins, their entity scope.","security":[{"BearerAuth":[]}],"responses":{"200":{"description":"Resolved configuration object"},"400":{"description":"Invalid tool name"},"401":{"description":"Unauthorized"},"403":{"description":"Caller may not access the requested scope"},"404":{"description":"Tool configuration not found"},"500":{"description":"Server error"}}},"post":{"summary":"Create a tool configuration","description":"Stores a tool configuration at the scope named by the query parameters.","security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"The configuration object itself; validated against the tool schema when one is registered"}}}},"responses":{"201":{"description":"Configuration created: { success, message, config }"},"400":{"description":"Invalid tool name, JSON or configuration"},"401":{"description":"Unauthorized"},"403":{"description":"Caller may not write the requested scope"},"415":{"description":"Content-Type is not application/json"},"500":{"description":"Server error"}}},"put":{"summary":"Update a tool configuration","description":"Stores the tool configuration at the scope named by the query parameters, even when that scope had no row, provided some level of the user, entity, site and global cascade holds one.","security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"The configuration object itself; validated against the tool schema when one is registered"}}}},"responses":{"200":{"description":"Configuration updated: { success, message, config }"},"400":{"description":"Invalid tool name, JSON or configuration"},"401":{"description":"Unauthorized"},"403":{"description":"Caller may not write the requested scope"},"404":{"description":"No level of the cascade holds a configuration"},"415":{"description":"Content-Type is not application/json"},"500":{"description":"Server error"}}},"delete":{"summary":"Delete a tool configuration","description":"Removes the tool configuration at the exact scope named by the query parameters. Answers 200 when some cascade level holds a configuration, even if that exact scope had none.","security":[{"BearerAuth":[]}],"responses":{"200":{"description":"Configuration deleted: { success, message }"},"400":{"description":"Invalid tool name"},"401":{"description":"Unauthorized"},"403":{"description":"Caller may not write the requested scope"},"404":{"description":"No level of the cascade holds a configuration"},"500":{"description":"Server error"}}}},"/api/v1/agents":{"get":{"summary":"List registered agents","description":"Returns registered agents with public metadata and their tools; internal fields are never returned.","security":[{"BearerAuth":[]}],"parameters":[{"name":"include","in":"query","required":false,"schema":{"type":"string"},"description":"Comma-separated extras; 'suggestions' adds initialSuggestions"}],"responses":{"200":{"description":"Agent list: { agents }"},"403":{"description":"No authenticated user context"},"500":{"description":"Server error"}}}},"/api/v1/org-data":{"get":{"summary":"Get organization data","description":"Returns Snowflake organization data for the caller's entity, taken from the token only.","security":[{"BearerAuth":[]}],"parameters":[{"name":"agentId","in":"query","required":false,"schema":{"type":"string"},"description":"Data source: 'fundraising-coach-ea' reads EveryAction, anything else NFG"},{"name":"snowflakeDemoMode","in":"query","required":false,"schema":{"type":"string"},"description":"'true' turns on demo data (NFG only)"},{"name":"snowflakeDemoDataset","in":"query","required":false,"schema":{"type":"string"},"description":"Demo dataset name (default 'default')"}],"responses":{"200":{"description":"Organization data: { success, data, timestamp, requestId, responseTime }"},"403":{"description":"Token carries no entity (entity_missing)"},"500":{"description":"Failed to fetch organization data"}}}},"/api/v1/upgrade/eligibility":{"get":{"summary":"Check self-service upgrade eligibility","description":"Evaluates whether the caller may self-serve the upgrade, so the widget can render the upgrade call to action on open.","security":[{"BearerAuth":[]}],"responses":{"200":{"description":"Eligibility result: { eligible, reason?, reasonCode?, pricing?, planDetails? }"},"400":{"description":"Missing entityId in user context"},"401":{"description":"Unauthorized"},"500":{"description":"Server error"}}}},"/api/v1/thinking-narration":{"post":{"summary":"Generate a thinking narration phrase","description":"Generates the next short status phrase describing what the agent is doing; returns message null when the thinkingNarration flag is off.","security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"userMessage":{"type":"string","description":"User message, cut to 200 characters"},"toolName":{"type":"string","description":"Tool currently running"},"elapsed":{"type":"number","description":"Seconds the operation has been running"},"previousMessages":{"type":"array","items":{"type":"string"},"description":"Earlier narration phrases; the last 5 are kept"}}}}}},"responses":{"200":{"description":"Narration: { message: string | null }"},"403":{"description":"No authenticated user context"},"500":{"description":"Failed to generate narration"}}}},"/api/v1/conversations/{threadKeyOrId}/metadata/clear":{"post":{"summary":"Clear conversation metadata","description":"Clears all non-platform metadata (resetting initialization) or the listed keys; an unknown thread is an idempotent success.","security":[{"BearerAuth":[]}],"parameters":[{"name":"threadKeyOrId","in":"path","required":true,"schema":{"type":"string"},"description":"Thread ID (UUID) or a threadKey that resolves to one"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"all":{"type":"boolean","description":"True clears all non-platform metadata"},"keys":{"type":"array","items":{"type":"string"},"description":"Metadata keys to clear; required unless all is true"}}}}}},"responses":{"200":{"description":"Metadata cleared, or nothing to clear"},"400":{"description":"Neither all nor keys, a platform key, or no site in the token"},"401":{"description":"Unauthorized"},"404":{"description":"Conversation not owned by the caller"},"500":{"description":"Server error"}}}},"/api/v1/conversations/{threadKeyOrId}/metadata/clear/{key}":{"post":{"summary":"Clear one conversation metadata key","description":"Clears a single metadata key named in the path; an unknown thread or key is a success.","security":[{"BearerAuth":[]}],"parameters":[{"name":"threadKeyOrId","in":"path","required":true,"schema":{"type":"string"},"description":"Thread ID (UUID) or a threadKey that resolves to one"},{"name":"key","in":"path","required":true,"schema":{"type":"string"},"description":"Metadata key to clear, e.g. 'organizationProgramsTool'"}],"responses":{"200":{"description":"Key cleared, or nothing to clear"},"400":{"description":"Platform key, or no site in the token"},"401":{"description":"Unauthorized"},"404":{"description":"Conversation not owned by the caller"},"500":{"description":"Server error"}}}},"/api/v1/resources/{integration}/{resource}":{"parameters":[{"name":"integration","in":"path","required":true,"schema":{"type":"string"},"description":"Integration, e.g. 'everyaction'"},{"name":"resource","in":"path","required":true,"schema":{"type":"string"},"description":"Resource, e.g. 'activist-codes'"}],"get":{"summary":"Fetch an integration resource","description":"Proxies a widget request to the handler registered for {integration}/{resource}; all query parameters are passed to it.","security":[{"BearerAuth":[]}],"responses":{"200":{"description":"Handler-defined result"},"400":{"description":"Invalid or missing handler input"},"401":{"description":"Unauthorized"},"403":{"description":"Refused by the handler"},"404":{"description":"No handler registered"},"500":{"description":"Failed to fetch resource"}}},"post":{"summary":"Trigger an integration resource action","description":"Passes the JSON body to the handler registered for {integration}/{resource}; only handlers that implement handle accept POST.","security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"Handler-specific body"}}}},"responses":{"200":{"description":"Handler-defined result"},"400":{"description":"Body is not valid JSON, or invalid handler input"},"401":{"description":"Unauthorized"},"403":{"description":"Refused by the handler"},"404":{"description":"No handler registered"},"405":{"description":"Handler does not support POST"},"500":{"description":"Failed to handle resource"}}}},"/api/v1/resources/{integration}/{resource}/{id}":{"get":{"summary":"Fetch one integration resource by ID","description":"Proxies to the handler registered for {integration}/{resource}/:id, passing the id with all query parameters.","security":[{"BearerAuth":[]}],"parameters":[{"name":"integration","in":"path","required":true,"schema":{"type":"string"},"description":"Integration, e.g. 'everyaction'"},{"name":"resource","in":"path","required":true,"schema":{"type":"string"},"description":"Resource, e.g. 'themes'"},{"name":"id","in":"path","required":true,"schema":{"type":"string"},"description":"Resource ID"}],"responses":{"200":{"description":"Handler-defined result"},"400":{"description":"Invalid or missing handler input"},"401":{"description":"Unauthorized"},"403":{"description":"Refused by the handler"},"404":{"description":"No handler registered"},"500":{"description":"Failed to fetch resource"}}}},"/api/v1/uploads/presigned-url":{"post":{"summary":"Get a presigned S3 upload URL","description":"Returns a presigned PUT URL (valid 300 seconds) for uploading a file straight to S3; needs the file-uploads flag.","security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["fileName","fileType","fileSize"],"properties":{"fileName":{"type":"string","description":"Original file name"},"fileType":{"type":"string","description":"MIME type"},"fileSize":{"type":"number","description":"Size in bytes"},"threadId":{"type":"string","description":"Conversation UUID; threadId or threadKey is required"},"threadKey":{"type":"string","description":"Resolved to a threadId when threadId is absent"},"agentId":{"type":"string","description":"Agent whose allowed file extensions apply"}}}}}},"responses":{"200":{"description":"Upload URL: { uploadUrl, fileKey, expiresIn }"},"400":{"description":"Missing or invalid fields, file type or size"},"401":{"description":"Unauthorized"},"403":{"description":"File uploads are not enabled"},"500":{"description":"Server error"}}},"get":{"summary":"Describe the presigned-url endpoint (documentation only)","description":"Returns a JSON description of the POST on this path (threadId or threadKey required) and the file limits; it creates no URL.","security":[{"BearerAuth":[]}],"responses":{"200":{"description":"Endpoint description"}}}},"/api/v1/uploads/download-url":{"post":{"summary":"Get a presigned S3 download URL","description":"Returns a presigned GET URL (valid 300 seconds) for a file the caller uploaded; needs the file-uploads flag.","security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["fileKey"],"properties":{"fileKey":{"type":"string","description":"S3 object key returned by presigned-url"}}}}}},"responses":{"200":{"description":"Download URL: { downloadUrl, fileKey, expiresIn }"},"400":{"description":"Missing or invalid fileKey"},"401":{"description":"Unauthorized"},"403":{"description":"File uploads not enabled, or the file belongs to another user"},"500":{"description":"Server error"}}},"get":{"summary":"Describe the download-url endpoint (documentation only)","description":"Returns a JSON description of the POST on this path; it creates no URL.","security":[{"BearerAuth":[]}],"responses":{"200":{"description":"Endpoint description"}}}},"/api/v1/uploads/ensure-text-extraction":{"post":{"summary":"Run text extraction for uploaded files","description":"Extracts text synchronously: a single fileKey returns the text inline, a files array (up to 5) reports failures.","security":[{"BearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"fileKey":{"type":"string","description":"Single-file shape: S3 key owned by the caller"},"fileName":{"type":"string","description":"Single-file shape: defaults to the last fileKey segment"},"files":{"type":"array","items":{"type":"object"},"description":"Batch shape: up to 5 { fileKey, fileName } entries"}}}}}},"responses":{"200":{"description":"Done, or failed per file: { ok: true, fileKey?, text? }; single-file failure { ok: false, fileKey, error, text: \"\" }; batch failures { details, failures }"},"400":{"description":"Invalid body, file entry or too many files"},"401":{"description":"Unauthorized"},"403":{"description":"File uploads not enabled, or a file belongs to another user"},"500":{"description":"Unexpected server error"}}}},"/api/v1/uploads/feature-flag":{"get":{"summary":"Check whether file uploads are enabled","description":"Resolves the file-uploads feature flag for the caller's site and entity so the widget can show the attach button.","security":[{"BearerAuth":[]}],"responses":{"200":{"description":"Flag state: { enabled: boolean }"},"401":{"description":"Unauthorized"}}}}},"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":"Bongentic user JWT from POST /api/v1/auth/token. 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"}}}}}}}