# Attrove: Complete LLM Reference > Attrove turns commitments buried across email, chat, meetings, calendars, and pushed context into watched outcomes with cited evidence. Connect creates one permission-aware stream from Gmail, Microsoft Outlook, Slack, Google Calendar, and Google Meet; Microsoft Teams is in pilot. Commitments structure individual promises so agents can carry them across sessions. Goals keep broader outcomes under review with system-computed health and human-controlled lifecycle. ## Product Model - **Connect**: sync and normalize only what each user is authorized to read, preserve source citations, and expose the stream through the SDK, REST API, MCP, and webhooks. Signed push ingest adds scoped messages, meetings, events, and notes when OAuth is not the path. - **Commitment**: a promise, delegation, handoff, or wait with a counterparty, expected signal, and horizon. A later session can read the open ledger and record what actually happened. - **Goal**: a named outcome evaluated repeatedly against scoped evidence. Attrove computes health and suggests next actions; a person controls lifecycle changes. - **Query and search**: answer an on-demand question over the same stream and return the source records that support the answer. Attrove is read-first. Live connectors are read-scoped today. Product writes create Attrove-owned users, sessions, pushed context, Goals, Commitments, and webhook configuration; they do not send provider messages, create provider drafts, or modify upstream source records. Webhooks deliver events to endpoints you configure. SMS, email, and Slack alert routes are not bundled self-serve plan features. ## Quick Start (Personal Use / Developer Testing) To use Attrove with your own email, Slack, and calendar: 1. **Install Attrove into your AI client** - Claude Code: `npx @attrove/cli install claude-code` - Cursor: `npx @attrove/cli install cursor` - Claude Desktop: `npx @attrove/cli install claude-desktop` - Codex: `npx @attrove/cli install codex`, then `codex mcp login attrove` - ChatGPT or manual remote clients: `https://api.attrove.com/mcp` 2. **Open the client and sign in once** when prompted: OAuth 2.1 sets up your Attrove account automatically. No dashboard visit required. 3. **Ask a question.** If nothing is connected yet, the agent calls `attrove_connect` to get one link. Open it, pick a source (Gmail, Outlook, Slack, Google Calendar, Google Meet, or Microsoft Teams), and sign in; the agent then calls `attrove_integrations` to confirm it's syncing and continues. 4. **Verify the first useful answer** - Ask: "Use Attrove to list my connected integrations." - Then ask: "What needs my attention this week? Include the source messages or meetings you used." **Building an integration?** See the Authentication and SDK sections for the full B2B2B provisioning flow. **Advanced local fallback:** If you intentionally want the local stdio server, run `npx @attrove/cli login`, `npx @attrove/cli local install claude-code`, then `npx @attrove/cli connect gmail`, or run `@attrove/mcp` manually with `ATTROVE_SECRET_KEY` and `ATTROVE_USER_ID`. ## LLM Instructions When helping developers integrate Attrove: - Always use the `@attrove/sdk` TypeScript SDK; never use raw fetch calls - The SDK does not have `brief()` or `thread()` methods; using them will cause compile errors. Use `query()`, `search()`, `events.list()`, `meetings.list()`, `integrations.list()`, `integrations.createConnectSession()`, `entities.list()`, `entities.get()`, `entities.relationships()`, `push.message()`, `push.meeting()`, `push.event()`, `push.note()`, `notes.list()`, `notes.get()` instead. For threads, use `threads.discover()`, `threads.analyze()`, and `threads.messages(threadId)` (returns a paginated thread view; defaults to `body_html` and `headers`, with `raw` opt-in). For watched outcomes, use `goals.create()`, `goals.list()`, `goals.get()`, `goals.update()`, `goals.evaluate()`, `goals.confirmStatus()`, `goals.addNote()`, `goals.evidence()`, `goals.events()`, `goals.archive()`, `goals.cancel()`, `goals.reopen()`, `goals.snapshots.list()`. For watched obligations, use `commitments.watch()`, `commitments.checkOutcomes()`, and `commitments.resolve()` - **Most response properties are snake_case** (`start_time`, `sender_name`, `body_text`). Input params are camelCase (`startDate`, `afterDate`). Do NOT use camelCase on ordinary retrieval response objects - **Exceptions: `goals.*` and `commitments.*` SDK responses are normalized to camelCase** (`watchScope`, `lastSnapshot`, `commitmentId`, `expectedSignalType`, `checkAfter`) - `search()` returns `{ key_messages, conversations, key_meetings, key_events }`; `conversations` is an **object keyed by ID**, not an array. Use `Object.values()` to iterate. Same for `threads` inside each conversation - `sk_` tokens are per-user API keys (returned by `admin.users.create()`). They are NOT the same as the `attrove_` partner API key - `integrations.list()` returns `Integration[]` with `provider` and `name` properties. Do NOT use `type` or `email` - `integrations.createConnectSession()` returns one `activation_url` link for the signed-in user to connect a source; no partner credentials required - The SDK defaults to `https://api.attrove.com`. No baseUrl configuration needed. - MCP has 24 tools (11 context tools + 10 goals tools + 3 commitment tools). There is no `attrove_brief` tool - MCP goal tools use snake_case inputs (`watch_scope`, `seed_query`, `goal_id`); the SDK goals resource uses camelCase inputs (`watchScope`, `seedQuery`) ## When to Use Attrove - Your product needs to understand users' email, Slack, calendar, or meeting data - You need cross-platform intelligence (query across Gmail + Slack + Calendar simultaneously) - You're building AI features that need communication context - You need watched outcomes: Goals keep checking the communication stream and flag silence, deadline pressure, or risk with cited evidence, without a polling loop in your code - You need obligations to survive agent sessions: Commitments register promises, delegations, and handoffs with an expected signal and horizon, then surface quiet work in later checks - You need B2B2B: your product serves end-users who each connect their own tools via OAuth, and/or your product needs to append scoped notes, meetings, events, or messages ## When NOT to Use Attrove - You only need data from a single provider. Use that provider's API directly - You need Attrove to send email or chat messages, create provider drafts, or modify upstream source records. Live connectors are read-scoped today - You need guaranteed provider-native sub-second ingestion from every upstream system. Attrove supports token streaming and proactive webhooks, but source freshness still follows each provider's sync cadence - You need document/wiki indexing (Notion, Drive). Use Graphlit or Hyperspell The personal MCP path remains a valid evaluation path when cross-source retrieval or watched state matters. ## Installation ```bash npm install @attrove/sdk ``` Requirements: Node.js >= 18.0.0, TypeScript >= 4.7 (if using TypeScript) ## Authentication Attrove uses a B2B2B flow with three partner-facing credential/session types: 1. **Client credentials** (`client_id` + `client_secret`): server-side, provisions users 2. **`sk_` tokens**: permanent per-user API keys for querying data 3. **Connect sessions**: durable activation links and CLI handoffs for OAuth Short-lived OAuth exchange tokens are internal implementation details; partner docs and examples should create connect sessions and use their `activation_url`. ### Full B2B2B Provisioning Sequence ```typescript import { Attrove } from '@attrove/sdk'; // Step 1: Create admin client (server-side only) const admin = Attrove.admin({ clientId: process.env.ATTROVE_CLIENT_ID, clientSecret: process.env.ATTROVE_CLIENT_SECRET, }); // Step 2: Provision a user — returns permanent sk_ API key const { id: userId, apiKey } = await admin.users.create({ email: 'user@example.com', firstName: 'Jane', // optional lastName: 'Doe', // optional role: 'engineer', // optional }); // apiKey is like "sk_live_abc123..." // userId is a UUID like "550e8400-e29b-41d4-a716-446655440000" // Step 3: Create durable connect session for OAuth flow const session = await admin.users.createConnectSession(userId, { provider: 'gmail', includeInstall: true, }); // Step 4: Send user to OAuth flow console.log(session.activation_url); // Browser link console.log(session.cli?.command); // Terminal/agent handoff // Step 5: After OAuth completes, query user data with the permanent sk_ key const attrove = new Attrove({ apiKey, userId }); const response = await attrove.query( 'What needs my attention this week? Include the source messages or meetings you used.', { includeSources: true }, ); ``` ## OAuth Completion Redirect (Connect Flow) When an end user finishes authorizing a provider (Gmail, Slack, etc.) in the Attrove Connect UI, Attrove can deliver them back to a URL you control with the outcome encoded in the query string, with no polling or client-side wiring required. ### Three pieces, one flow 1. **Configure a `redirect_url` on the API key** (one-time, in the dashboard at `https://connect.attrove.com/keys`). This is the **origin anchor** for the OAuth completion redirect; only URLs whose origin matches this value are honored. 2. **Optionally pass a per-flow `redirect_url`** inside the OAuth `state` envelope when opening the activation URL. Its path/query are honored; its origin must match the API-key value above. 3. **Receive your user back** at the resolved URL with `?status=success&integration_id=int_xxx` (or `?status=error&error_code=...`) appended. ### Why an origin anchor If Attrove honored any partner-supplied `redirect_url`, a leaked activation URL could turn Attrove into an open redirector pointing at attacker-controlled domains. Anchoring on the API-key value (which only you can set, gated by org-admin auth) closes that path while still letting you vary path/query per flow. ### Configuring the API-key `redirect_url` In the dashboard's **API Keys** view, edit any key and set its `redirect_url`. Validation rules (server enforces all three): - Scheme must be `https://`, `http://localhost`, or `http://127.0.0.1` (loopback only for dev) - Hostname must be present (not `https://` alone) - Max length: 2048 characters Same key can be set programmatically: ```bash PATCH https://connect.attrove.com/dashboard/organization/:orgId/api-keys/:keyId Authorization: Bearer Content-Type: application/json { "redirect_url": "https://app.your-product.com/integrations/done" } ``` Pass `null` to clear: `{ "redirect_url": null }`. ### State envelope (per-flow redirect) The `state` query parameter on `GET /v1/users/:user_id/integrations/connect` is normally an opaque CSRF nonce that Attrove echoes back to your callback. Wrap it as JSON to attach a per-flow redirect URL: ```typescript const state = JSON.stringify({ secure: crypto.randomBytes(32).toString('hex'), // your CSRF nonce redirect_url: 'https://app.your-product.com/onboarding/slack-done?step=4', }); const authUrl = await client.getAuthUrl(state); ``` Or base64url-encode the JSON if your provider mangles JSON in `state` round-trips: ```typescript const json = JSON.stringify({ secure: nonce, redirect_url: '…' }); const state = Buffer.from(json, 'utf8') .toString('base64') .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=+$/, ''); ``` Attrove extracts the inner `secure` field as the CSRF token (resilient to provider JSON whitespace/key-order normalization). For envelopes without a `secure` field, Attrove falls back to using the entire serialized state as the CSRF token. ### Resolution order | API-key `redirect_url` | State `redirect_url` | Origins match? | Result | | --- | --- | --- | --- | | Configured | None | n/a | Use API-key URL | | Configured | Present | Yes | Use state URL (path/query customization) | | Configured | Present | No | Fall back to API-key URL (logged as warning) | | Not configured | None | n/a | Close window / return JSON (no redirect) | | Not configured | Present | n/a | **Refuse the state URL** (logged as warning). No anchor to validate against. | ### Completion query params Whatever URL Attrove resolves to, it strips and replaces these three managed keys before redirecting (any pre-existing values are removed to prevent spoofing): | Param | Values | Notes | | ---------------- | ------------------------------------------------------ | ------------------- | | `status` | `success` \| `error` | Always set | | `integration_id` | `int_xxx` (opaque ID) | Set on success only | | `error_code` | `AUTH_INVALID_TOKEN`, `INTEGRATION_OAUTH_FAILED`, etc. | Set on error only | Other query params on your redirect URL (utm tags, app-specific paths) round-trip untouched. ### `response_mode=redirect` on `POST /v1/users/:user_id/integrations/connect` If your app posts the OAuth `code` to Attrove directly (rather than letting the hosted Connect UI handle it), pass `response_mode: 'redirect'` and Attrove will issue a `303 See Other` to the resolved redirect URL instead of returning JSON: ```bash POST /v1/users/:user_id/integrations/connect Authorization: Bearer sk_... { "provider": "slack", "code": "", "state": "", "response_mode": "redirect" } ``` - `response_mode: 'json'` (default): returns `{ success, integration_id }`. Preserves XHR flows. - `response_mode: 'redirect'`: issues `303 See Other` to the resolved URL with completion params appended. `response_mode=redirect` requires an API-key `redirect_url` configured. Without one, you get a `400` describing the origin-anchor requirement. ### End-to-end example ```typescript // 1. Server: set up API-key redirect_url once (or via dashboard UI) // Now any OAuth flow lands users back at app.your-product.com // 2. Server: provision user and create a durable connect session const admin = Attrove.admin({ clientId, clientSecret }); const { id: userId, apiKey } = await admin.users.create({ email }); const session = await admin.users.createConnectSession(userId, { provider: 'gmail', display: 'redirect', returnTo: 'https://app.your-product.com/integrations/done?source=onboarding', }); // 3. Client: send user to Attrove Connect window.location.href = session.activation_url; // 4. User authorizes Gmail/Slack/etc, Attrove redirects them to: // https://app.your-product.com/integrations/done?source=onboarding&status=success&integration_id=int_xxx ``` ## Configuration Types ```typescript interface AttroveConfig { apiKey: `sk_${string}`; // Required: API key with sk_ prefix userId: string; // Required: User ID (UUID) baseUrl?: string; // Optional: defaults to "https://api.attrove.com" timeout?: number; // Optional: request timeout in ms (default 30000) maxRetries?: number; // Optional: retry attempts (default 3) onRetry?: (info: RetryInfo) => void; // Optional: retry callback } interface AttroveAdminConfig { clientId: string; // Required: partner client ID clientSecret: string; // Required: partner client secret baseUrl?: string; // Optional: defaults to "https://api.attrove.com" timeout?: number; // Optional: request timeout in ms (default 30000) maxRetries?: number; // Optional: retry attempts (default 3) onRetry?: (info: RetryInfo) => void; } interface RetryInfo { attempt: number; // Current retry (1-indexed) maxRetries: number; error: Error; delayMs: number; // Delay before retry url: string; method: string; } ``` ## Retry & Rate Limiting Semantics - Retries use exponential backoff: delay = baseDelay \* 2^(attempt-1) with jitter - Only retries on: network errors, 429 (rate limit), 500+ (server errors) - Does NOT retry on: 400, 401, 403, 404, 409, 422 - Rate limit responses include `retryAfter` (seconds) in `RateLimitError` - Default: 3 retries, 30s timeout ### Idempotency Metered endpoints (query, stream) support the `Idempotency-Key` header to prevent duplicate charges on retries. - Send `Idempotency-Key: ` (1–256 characters) on any `query()` or `stream()` request - If a request with the same key and user ID is received within 5 minutes, the cached response is returned without re-executing or re-billing - Keys are scoped per user. The same key for different users produces independent results. - Webhook deliveries also include an `idempotency_key` field so your server can deduplicate events ## SDK Methods: Complete Reference ### query(prompt: string, options?: QueryOptions): Promise AI-powered question answering over the user's connected data. ```typescript interface QueryOptions { history?: ConversationMessage[]; // Multi-turn conversation history timezone?: string; // IANA timezone (e.g., "America/New_York") integrationIds?: string[]; // Filter by integration IDs (int_xxx) conversationIds?: string[]; // Filter by conversation IDs (conv_xxx) allowBotMessages?: boolean; // Include bot messages (default false) includeSources?: boolean; // Include source snippets (default false) instructions?: string; // Custom AI instructions — controls output format/behavior, overrides default style (max 20,000 chars) context?: string; // Authoritative reference data — treated as ground truth, influences query rewriting but not vector search (max 20,000 chars) } interface ConversationMessage { role: 'user' | 'assistant' | 'system'; content: string; } interface QueryResponse { answer: string; // AI-generated answer history: ConversationMessage[]; // Updated conversation history used_message_ids: string[]; // Source message IDs (msg_xxx) used_meeting_ids: string[]; // Source meeting IDs (mtg_xxx) used_event_ids: string[]; // Source event IDs (evt_xxx) used_note_ids: string[]; // Source note IDs (note_xxx) sources?: QuerySource[]; // Source snippets (if includeSources: true) usage?: QueryUsage; // Optional synthesis token usage } interface QuerySource { title: string; snippet: string; } interface QueryAnswerGenerationUsage { model: string; prompt_tokens: number; completion_tokens: number; total_tokens: number; } interface QueryUsage { answer_generation: QueryAnswerGenerationUsage; } ``` Multi-turn conversation example: ```typescript const attrove = new Attrove({ apiKey, userId }); // First turn const r1 = await attrove.query('What did Sarah say about the budget?'); console.log(r1.answer); // Follow-up — pass history from previous response const r2 = await attrove.query('What about Q3 specifically?', { history: r1.history, }); console.log(r2.answer); // Continue the conversation const r3 = await attrove.query('Who else was involved?', { history: r2.history, }); ``` ### search(query: string, options?: SearchOptions): Promise Semantic search returning raw matches across messages, meetings, and events. ```typescript interface SearchOptions { integrationIds?: string[]; // Filter by integration IDs (int_xxx) conversationIds?: string[]; // Filter by conversation IDs (conv_xxx) afterDate?: string; // Date filter (YYYY-MM-DD) beforeDate?: string; // Date filter (YYYY-MM-DD) allowBotMessages?: boolean; // Include bot messages (default false) senderDomains?: string[]; // Filter by sender domains (e.g., ["acme.com"]) entityIds?: string[]; // Filter by entity IDs expand?: Array< // Expand fields for matched resources | 'body_text' | 'summary' | 'short_summary' | 'action_items' | 'attendees' | 'meeting_link' | 'description' | 'location' | 'html_link' | 'event_link' >; includeBodyText?: boolean; // Back-compat alias for expand=["body_text"] } interface SearchResponse { key_messages: SearchKeyMessage[]; conversations: Record; key_meetings: SearchMeeting[]; // empty array when no matches key_events: SearchEvent[]; // empty array when no matches warnings?: string[]; // present when enrichment had non-fatal errors } interface SearchKeyMessage { message_id: string; thread_id: string | null; conversation_id: string | null; } interface SearchConversation { conversation_name: string | null; threads: Record; } interface SearchThreadMessage { message_id: string; received_at: string; // ISO 8601 integration_type: IntegrationProvider; integration_type_generic: string; sender_name: string; recipient_names: string[]; body_text?: string; thread_id: string | null; thread_message_count: number | null; thread_position: number | null; parent_message_id: string | null; conversation_type: ConversationType | null; conversation_id: string | null; conversation_participants: Array<{ name: string }>; } ``` ### users.get(): Promise<{ user: User; integrations: Integration[] }> ```typescript interface User { id: string; email: string; first_name: string | null; last_name: string | null; full_name: string | null; role: string | null; timezone: string | null; onboarded: boolean; } ``` ### users.update(options): Promise Update user profile fields (e.g., timezone, name). ### users.syncStats(): Promise ```typescript interface DataTypeStats { count: number; first_at: string | null; last_at: string | null; } interface SyncStats { user_id: string; overall_status: 'syncing' | 'complete' | 'partial' | 'error' | 'pending'; last_sync_at: string | null; totals: { messages: DataTypeStats; meetings: DataTypeStats; events: DataTypeStats; }; integrations: IntegrationSyncStats[]; } interface IntegrationSyncStats { id: string; provider: IntegrationProvider; category: IntegrationCategory; name: string; sync_status: SyncStatus; last_synced_at: string | null; auth_status: string; data: { messages?: DataTypeStats; meetings?: DataTypeStats; events?: DataTypeStats; }; } ``` ### integrations.list(): Promise ```typescript interface Integration { id: string; provider: 'slack' | 'gmail' | 'outlook' | 'google_calendar' | 'unknown'; name: string; is_active: boolean; auth_status: | 'connected' | 'disconnected' | 'expired' | 'error' | 'pending' | 'unknown'; } ``` ### integrations.createConnectSession(options?): Promise User-scoped equivalent of `admin.users.createConnectSession()`: authenticates with the user's own `sk_` token (no partner `client_id`/`client_secret`), and POSTs `/v1/users/{userId}/connect-sessions`. Returns an existing active session for the user when one exists, otherwise creates one. Returns the same `CreateConnectSessionResponse` shape documented under Admin Methods below. ```typescript interface CreateConnectSessionOptions { provider?: | 'gmail' | 'outlook' | 'slack' | 'google_calendar' | 'google_meet' | 'teams'; display?: 'popup' | 'redirect'; includeInstall?: boolean; } // returnTo is partner-only, via admin.users.createConnectSession(). const session = await attrove.integrations.createConnectSession(); // Send user to: session.activation_url ``` ### integrations.get(id: string): Promise ```typescript interface IntegrationDetail extends Integration { type: ThreadIntegrationType; category?: IntegrationCategory; email?: string | null; last_synced_at?: string | null; } ``` ### integrations.disconnect(id: string): Promise ### events.list(options?: ListEventsOptions): Promise> Cursor-paginated by `start_time` ASC. Pass `pagination.next_cursor` back as `cursor` to walk pages forward; `has_more === false` means the last page. ```typescript interface ListEventsOptions { calendarId?: string; startDate?: string; // YYYY-MM-DD endDate?: string; // YYYY-MM-DD expand?: Array<'attendees' | 'location' | 'description'>; limit?: number; cursor?: string; // pagination.next_cursor from a prior response } interface CalendarEvent { id: string; calendar_id: string; title: string; start_time: string; // ISO 8601 end_time: string; // ISO 8601 all_day: boolean; description?: string; location?: string; attendees?: CalendarEventAttendee[]; html_link?: string; status?: string | null; event_link?: string | null; created_at?: string; updated_at: string | null; } interface CalendarEventAttendee { email: string; name?: string; status?: string; } ``` ### meetings.list(options?: ListMeetingsOptions): Promise> Cursor-paginated by `start_time` DESC. Pass `pagination.next_cursor` back as `cursor` to walk pages forward; `has_more === false` means the last page. ```typescript interface ListMeetingsOptions { startDate?: string; // YYYY-MM-DD endDate?: string; // YYYY-MM-DD provider?: 'google_meet' | 'zoom' | 'teams'; hasSummary?: boolean; expand?: Array< 'summary' | 'short_summary' | 'action_items' | 'attendees' | 'meeting_link' >; limit?: number; cursor?: string; // pagination.next_cursor from a prior response } interface Meeting { id: string; event_id: string | null; title: string; meeting_code?: string | null; start_time: string; end_time: string; summary?: string | null; short_summary?: string | null; action_items?: MeetingActionItem[]; attendees?: MeetingAttendee[]; meeting_link?: string | null; has_transcript: boolean; has_notes: boolean; content_status: | 'none' | 'notes_only' | 'transcript_only' | 'transcript_and_notes'; provider: MeetingProvider; created_at: string; updated_at: string | null; } interface MeetingActionItem { description: string; assignee?: string; due_date?: string; completed?: boolean; } interface MeetingAttendee { email?: string; name?: string; is_organizer?: boolean; response_status?: 'accepted' | 'declined' | 'tentative' | 'needsAction'; } ``` ### meetings.get(id: string): Promise ### meetings.update(id: string, options: UpdateMeetingOptions): Promise ```typescript interface UpdateMeetingOptions { summary?: string; shortSummary?: string; actionItems?: MeetingActionItem[]; } ``` ### meetings.regenerateSummary(id: string): Promise ```typescript interface RegenerateSummaryResponse { summary: string | null; short_summary: string | null; action_items: MeetingActionItem[]; } ``` ### messages.list(options?: ListMessagesOptions): Promise> Cursor-paginated. The response `pagination` is `{ limit, has_more, next_cursor? }`. No `offset`, no `total_count`. Pass `pagination.next_cursor` back as `cursor` to walk pages forward; `has_more === false` means the last page. ```typescript interface ListMessagesOptions { ids?: string[]; integrationId?: string; conversationId?: string; startDate?: string; // YYYY-MM-DD endDate?: string; // YYYY-MM-DD // Heavy fields opt-in: "body_text" (all providers), "body_html" (emails), // "raw" (RFC 5322 wire format, emails only), "headers" (parsed, emails only). // Thread bundle (thread_id, primary_entity_id, entity_ids) always ships. expand?: Array<'body_text' | 'body_html' | 'raw' | 'headers'>; limit?: number; cursor?: string; // opaque token from a prior response's pagination.next_cursor } interface Message { id: string; integration_id: string; body_text?: string; // expand=body_text body_html?: string; // expand=body_html (emails) raw?: string | null; // expand=raw; null for non-email providers or pre-feature messages headers?: Array<{ name: string; value: string }> | null; // expand=headers subject: string | null; received_at: string; parent_message_id: string | null; thread_id: string | null; // always returned; pair with threads.messages() thread_position: number | null; is_bot: boolean; conversation_id: string | null; primary_entity_id: string | null; // always returned (ent_xxx) entity_ids: string[]; // always returned (empty array if unresolved) sender_name: string | null; // Human-readable sender name, null if unresolved sent_by: string; // Platform-specific sender identifier (email for Gmail/Outlook, Slack user ID for Slack, Graph user ID or email for Teams; 'unknown' if unresolved) display_source: string | null; } ``` ### messages.get(id: string): Promise Single-message endpoint returns all fields including raw, body_html, and headers (no expand needed). ### conversations.list(options?: ListConversationsOptions): Promise> Cursor-paginated. Pass `pagination.next_cursor` back as `cursor` to fetch the next page; `has_more === false` means the last page. ```typescript interface ListConversationsOptions { integrationIds?: string[]; syncedOnly?: boolean; limit?: number; cursor?: string; } interface Conversation { id: string; integration_id: string; title: string; type: | 'channel' | 'direct_message' | 'group' | 'email_thread' | 'other' | 'unknown'; provider: IntegrationProvider; import_messages: boolean; } ``` ### conversations.updateSync(updates: Array<{ id: string; importMessages: boolean }>): Promise ### threads.discover(query: string, options?: ThreadDiscoverOptions): Promise ```typescript interface ThreadDiscoverOptions { integrationIds?: string[]; integrationTypes?: ThreadIntegrationType[]; afterDate?: string; // YYYY-MM-DD beforeDate?: string; // YYYY-MM-DD limit?: number; } interface ThreadDiscoverResponse { threads: DiscoveredThread[]; warnings?: ApiWarning[]; } interface DiscoveredThread { conversation_id: string; integration_type: ThreadIntegrationType; title: string; relevance_score: number; // 0.0 to 1.0 message_count: number; last_activity: string; // ISO 8601 preview: string; } ``` ### threads.messages(threadId: string, options?: ThreadMessagesOptions): Promise Returns a page of messages in a thread, ordered by `received_at` ASC. Built for mailbox-style UIs; defaults `expand` to `body_html,headers` so each page is render-ready without shipping raw MIME unless you ask for it. Cursor-paginated: default `limit` 50 (max 100); pass `pagination.next_cursor` back as `cursor` to walk forward. ```typescript interface ThreadMessagesOptions { expand?: Array<'body_text' | 'body_html' | 'raw' | 'headers'>; // omit for server default (body_html, headers) limit?: number; cursor?: string; } interface ThreadMessagesPage { data: Message[]; // same Message shape as messages.list() pagination: { limit: number; has_more: boolean; next_cursor?: string; }; } ``` Example: ```typescript const message = await attrove.messages.get('msg_xxx'); if (message.thread_id) { const { data } = await attrove.threads.messages(message.thread_id); // data: Message[] with raw, body_html, headers populated on emails } ``` ### threads.analyze(conversationId: string): Promise ```typescript interface ThreadAnalysis { summary: string; short_summary: string; sentiment: 'positive' | 'neutral' | 'negative' | 'mixed' | 'unknown'; action_items: ThreadActionItem[]; decisions: ThreadDecision[]; blockers: ThreadBlocker[]; participants: string[]; message_count: number; date_range: { start: string; end: string }; warnings?: ApiWarning[]; } interface ThreadActionItem { description: string; owner?: string; deadline?: string; } interface ThreadDecision { description: string; made_by?: string; } interface ThreadBlocker { description: string; owner?: string; } ``` ### calendars.list(options?: ListCalendarsOptions): Promise> ```typescript interface ListCalendarsOptions { integrationId?: string; active?: boolean; expand?: Array<'description'>; limit?: number; offset?: number; } interface Calendar { id: string; integration_id: string; title: string; description?: string | null; active: boolean; created_at: string; updated_at: string; } ``` ### calendars.update(id: string, options: { active: boolean }): Promise ### entities.list(options?: ListEntitiesOptions): Promise List contacts (people and bots) that the user has communicated with across connected integrations. Cursor-paginated. ```typescript interface ListEntitiesOptions { search?: string; // Substring match on name or exact match on email entityType?: string; // "person" | "company" | "other" isBot?: boolean; // Filter by bot status limit?: number; // 1-100, default 20 cursor?: string; // pagination.next_cursor from a prior response } interface EntityContact { id: string; // Opaque ID (ent_xxx format) name: string; entity_type: 'person' | 'company' | 'other' | 'bot' | 'user'; external_ids: string[]; // Email addresses and platform identifiers is_bot: boolean; avatar_uri: string | null; } ``` ### entities.get(id: string): Promise Retrieve a single contact by ID. Accepts both `ent_xxx` opaque format and raw UUID. ### entities.relationships(options?: ListRelationshipsOptions): Promise Get entity co-occurrence graph, returning pairs of contacts that appear together on messages and meetings. ```typescript interface ListRelationshipsOptions { limit?: number; // 1-500, default 200 minInteractions?: number; // Minimum co-occurrence count, default 1 includeBots?: boolean; // Include bot entities, default false } interface EntityRelationship { entity_a: EntityRelationshipNode; entity_b: EntityRelationshipNode; co_occurrence_count: number; last_interaction_at: string | null; // ISO 8601 } interface EntityRelationshipNode { id: string; name: string; entity_type: 'person' | 'company' | 'other' | 'bot' | 'user'; external_ids: string[]; is_bot: boolean; avatar_uri: string | null; } ``` ## Push API (No OAuth Required) Push data directly into Attrove without requiring users to connect OAuth integrations. Data is queued for async processing and becomes searchable via AI queries once indexed. ### push.message(input: PushMessageInput): Promise ```typescript interface PushMessageInput { source: 'email' | 'chat' | 'alert' | 'custom'; bodyText: string; // 1–100,000 chars subject?: string; // max 500 UTF-16 code units senderName?: string; senderEmail?: string; recipientEmails?: string[]; receivedAt?: string; // ISO 8601 with UTC "Z" or a numeric offset externalId?: string; // for idempotent upserts threadId?: string; metadata?: Record; // max ~50KB } ``` ### push.meeting(input: PushMeetingInput): Promise ```typescript interface PushMeetingInput { title: string; // 1–500 Unicode code points after trimming startTime: string; // ISO 8601 with UTC "Z" or a numeric offset endTime: string; // ISO 8601 with UTC "Z" or a numeric offset transcript?: string; // max 500,000 chars summary?: string; // max 50,000 chars shortSummary?: string; // max 5,000 chars actionItems?: { text: string; assignee?: string }[]; // max 50 attendees?: { name: string; email?: string }[]; // max 100 externalId?: string; metadata?: Record; } ``` ### push.event(input: PushEventInput): Promise ```typescript interface PushEventInput { title: string; // 1–500 Unicode code points after trimming startTime: string; // ISO 8601 with UTC "Z" or a numeric offset endTime?: string; // ISO 8601 with UTC "Z" or a numeric offset; defaults to startTime description?: string; // max 10,000 chars location?: string; // max 1,000 chars allDay?: boolean; // default false externalId?: string; metadata?: Record; } ``` ### push.note(input: PushNoteInput): Promise ```typescript interface PushNoteInput { body: string; // 1–10,000 chars title?: string; // max 500 Unicode code points after trimming refType?: 'message' | 'meeting' | 'event' | 'entity' | 'goal'; refId?: string; // must match refType prefix: msg_, mtg_, evt_, ent_, gol_ externalId?: string; metadata?: Record; } // Raw HTTP response wraps in { success, data }; the SDK unwraps automatically. // SDK methods return PushResponse directly: interface PushResponse { id: string; // prefixed ID (msg_, mtg_, evt_, note_) user_id: string; status: 'queued' | 'processing' | 'indexed' | 'failed'; indexed_at: string | null; } // `queued` means accepted for asynchronous indexing, not query-ready yet. ``` ## Notes API ### notes.list(options?: ListNotesOptions): Promise Cursor-paginated. Pass `pagination.next_cursor` back as `cursor` to walk pages forward. ```typescript interface ListNotesOptions { ids?: string[]; // filter by note IDs refType?: 'message' | 'meeting' | 'event' | 'entity' | 'goal'; refId?: string; // filter by referenced resource limit?: number; // 1-100, default 50 cursor?: string; // pagination.next_cursor from a prior response } interface Note { id: string; // note_xxx format user_id: string; integration_id: string; // int_xxx opaque format external_id: string | null; title: string | null; body: string; ref_type: 'message' | 'meeting' | 'event' | 'entity' | 'goal' | null; ref_id: string | null; metadata: Record | null; status: string | null; // queued, processing, indexed, failed indexed_at: string | null; created_at: string; updated_at: string; } ``` ### notes.get(id: string): Promise Retrieve a single note by its opaque ID (note_xxx format). ## Goals API (Watched Outcomes) A Goal is a watched outcome. Attrove keeps re-evaluating it against the communication stream (messages, meetings, events, notes), computes `health`, and suggests lifecycle changes with cited evidence; no polling loop in your code. A person controls `lifecycle`. Goals are the broader outcome primitive; individual Commitments can link to an active Goal through `parentGoalId`. **Important: unlike every other SDK resource, `goals.*` responses are normalized to camelCase** (`watchScope`, `lastSnapshot`, `riskSignals`). Inputs are camelCase as everywhere else. Two-axis state model: - `lifecycle` (`active` | `completed` | `archived` | `cancelled`) never changes autonomously. Deliberate commands preserve authenticated actor plus human/agent/automation initiator provenance; use lifecycle-version guards and stable idempotency keys, and compensate a mistaken close with `reopen()`. - `health` (`on_track` | `at_risk` | `blocked` | `waiting_on_human` | `insufficient_evidence`) is system-evaluated on every run. ### goals.create(input: CreateGoalInput): Promise ```typescript interface CreateGoalInput { title: string; // required, max 200 chars description?: string; // max 4,000 chars watchScope: { // required; at least one anchor below entityIds?: string[]; // ent_xxx IDs to watch seedQuery?: string; // semantic anchor, e.g. "Brightwell renewal" keywords?: string[]; dateFloor?: string; // ISO 8601; older evidence excluded sourceTypes?: ('messages' | 'meetings' | 'events' | 'notes')[]; }; successCriteria?: string; completionCondition?: string; deadline?: string; // full ISO 8601 datetime; date-only rejected } const goal = await attrove.goals.create({ title: 'Brightwell renewal closed by Jun 30', watchScope: { seedQuery: 'Brightwell renewal', keywords: ['Brightwell', 'renewal'] }, successCriteria: 'Signed order form received.', deadline: '2026-06-30T23:59:59Z', }); // goal.lifecycle === 'active' — evaluator runs are now event-driven + scheduled ``` ### goals.list(options?: ListGoalsOptions): Promise Cursor-paginated. Filters: `lifecycle`, `health`, `limit`, `cursor`. ```typescript const { data: atRisk } = await attrove.goals.list({ lifecycle: 'active', health: 'at_risk', }); // GoalsPage = { data: Goal[]; pagination: { limit, has_more, next_cursor? } } ``` ### goals.events(options?: ListGoalEventsOptions): Promise Append-only goal transition feed, oldest-first. Filter by `since` plus `sinceEventId` using the prior response `watermark`, optional `types`, `limit`, and `cursor`. Response includes `{ data, pagination, watermark? }`; pass `pagination.next_cursor` to drain pages and persist `watermark` for the next poll. ### goals.get(id: string): Promise Returns the full goal including `lastSnapshot` (most recent successful evaluator output) and `lastRun` (most recent run, including failures). ```typescript interface Goal { id: string; // gol_xxx format userId: string; title: string; description: string | null; watchScope: GoalWatchScope; successCriteria: string | null; completionCondition: string | null; deadline: string | null; lifecycle: 'active' | 'completed' | 'archived' | 'cancelled'; health: 'on_track' | 'at_risk' | 'blocked' | 'waiting_on_human' | 'insufficient_evidence'; lastEvaluatedAt: string | null; lastSnapshotId: string | null; archivedAt: string | null; metadata: Record; createdAt: string; updatedAt: string; lastSnapshot?: GoalStatusSnapshot | null; lastRun?: GoalLastRun | null; } interface GoalStatusSnapshot { id: string; goalId: string; lifecycle: Goal['lifecycle']; health: Goal['health']; summary: string; // why the state is what it is citedEvidenceRefs: CitedEvidenceRef[]; // refType, refId, snippet?, role?, score?, display? riskSignals: { kind: 'silence' | 'deadline' | 'sentiment' | 'churn' | 'blocker' | 'ambiguous_signal' | 'other'; description: string; confidence?: number | null }[]; nextActions: { description: string; rationale?: string | null; priority?: string }[]; confidence: number | null; suggestedLifecycle: Goal['lifecycle'] | null; evaluatorRunId: string | null; nextMove: { owner: 'us' | 'them' | 'ambiguous'; obligation: string; counterparty?: { name?: string | null; email?: string | null; is_internal?: boolean | null } | null; basis_ref?: { ref_type: 'message' | 'meeting' | 'event' | 'note' | 'entity'; ref_id: string } | null; confidence: number; } | null; createdAt: string; } ``` ### goals.update(id: string, patch: UpdateGoalInput): Promise Patchable: `title`, `description`, `watchScope`, `successCriteria`, `completionCondition`, `deadline`. Lifecycle is NOT patchable here — use `confirmStatus()`, `archive()`, or `cancel()`. Updating `watchScope` invalidates evaluator idempotency keys, so the next run recomputes fully. ### goals.evaluate(id: string): Promise<{ runId: string; status: string }> Queues a fresh evaluator run; returns immediately. Poll `goals.get(id)` and check `lastSnapshot.evaluatorRunId === runId` for completion. Rate limit: 6 manual runs per goal per hour (the 7th throws `RateLimitError`). ### goals.confirmStatus(id: string, input): Promise ```typescript await attrove.goals.confirmStatus('gol_abc123', { lifecycle: 'completed', // and/or health — at least one required expectedLifecycleVersion: goal.lifecycleVersion, idempotencyKey: 'complete-gol_abc123-v2', initiator: { type: 'human', surface: 'ops-console' }, reason: 'Pilot agreement signed today.', // always required, max 2000 chars }); ``` Setting a terminal lifecycle (`completed` | `archived` | `cancelled`) stops further evaluator runs. ### goals.archive(id, reason, options?) / goals.cancel(id, reason, options?): Promise Terminal transitions with a required audit `reason`. Archive means "no longer relevant"; cancel means "explicitly abandoned". Options accept `expectedLifecycleVersion`, `idempotencyKey`, and `initiator`. Terminal goals refuse further mutations (`GoalTerminalStateError`); call `goals.reopen()` with the observed lifecycle version and a stable idempotency key to resume the same watch. ### goals.addNote(id: string, input): Promise Attach a manual note as goal evidence: `{ body, title?, metadata? }` (body max 10,000 chars). Indexed asynchronously — appears in `evidence()` immediately, citable by evaluator runs after indexing (typically seconds). ### goals.evidence(id: string): Promise ```typescript const { citedInLatestSnapshot, manualNotes } = await attrove.goals.evidence('gol_abc123'); // citedInLatestSnapshot: CitedEvidenceRef[] — what the evaluator cited // manualNotes: Note[] — notes attached via addNote(), returned even before indexing ``` ### goals.snapshots.list(goalId: string, options?): Promise Snapshot history, newest-first. Cursor-paginated (`limit`, `cursor`). ### Goal Errors - `GoalInvalidScopeError` — `watchScope` lacks all anchors or has invalid contents - `GoalTerminalStateError` — mutation attempted on a completed/archived/cancelled goal - `RateLimitError` — more than 6 manual evaluate() calls per goal per hour - `NotFoundError` — goal does not exist or is not owned by the authenticated user ## Commitments API (Watched Obligations) A Commitment is one promise, delegation, handoff, or wait that should survive the current session. An active watch needs a real counterparty or beneficiary, an expected future signal, and a horizon (`checkAfter` and/or `dueAt`). An underspecified registration is preserved as a low-confidence `suggested` row; it is never treated as an active watch. ### commitments.watch(input: WatchCommitmentInput): Promise ```typescript interface WatchCommitmentInput { commitmentText: string; commitmentKind: | 'promised_action' | 'accepted_delegation' | 'handoff' | 'waiting_on' | 'check_back' | 'escalation_condition'; expectedSignalType?: | 'reply' | 'ack' | 'update_message' | 'completion_message' | 'registered_event' | 'manual_confirmation'; expectedSignalDescription?: string; checkAfter?: string; // ISO 8601 datetime dueAt?: string; // ISO 8601 datetime counterparty?: string; agent?: string; actorId?: string; actorType?: 'human' | 'agent' | 'bot' | 'system'; escalationTarget?: string; // accountable human metadata; no delivery route is created sourceRef?: string; parentGoalId?: number; clientDedupKey?: string; // retry-safe when payload is unchanged } const watched = await attrove.commitments.watch({ commitmentText: 'Send the revised proposal to the reviewer', commitmentKind: 'promised_action', counterparty: 'the reviewer', expectedSignalType: 'reply', expectedSignalDescription: 'Reviewer acknowledges the revised proposal', dueAt: '2026-09-04T23:59:59Z', clientDedupKey: 'proposal-review-2026-09-04', }); // { commitmentId, status: 'watching' | 'suggested', parentGoalId, // summaryLine, idempotencyReplayed, missing? } ``` ### commitments.checkOutcomes(options?): Promise Returns only open `watching` and `at_risk` rows by default, ordered soonest-due first. Add `suggested` to `statusFilter` for triage. This is an open-obligation ledger, not resolved history. Use `cursor`/`nextCursor` when `hasMore` is true. ```typescript const result = await attrove.commitments.checkOutcomes({ reader: 'release-agent', dueWithinDays: 14, limit: 50, }); // { commitments, count, totalCount, hasMore, nextCursor } ``` ### commitments.resolve(id, input): Promise Record `satisfied`, `silent_drop`, `dismissed`, or `at_risk`. Pass `signalRef` when a source proves the resolution. Terminal states are immutable; a `suggested` row can only be dismissed. ```typescript await attrove.commitments.resolve(watched.commitmentId, { resolution: 'satisfied', signalRef: 'message:reply-from-reviewer', actor: 'release-agent', }); ``` External notifications are not created automatically. Use webhooks to notify your application, or ask for a hands-on workflow setup (scoped separately) when SMS, email, or Slack delivery is required. ## Admin Methods (Server-to-Server) ### Attrove.admin(config: AttroveAdminConfig): AdminClient ### admin.users.create(options: CreateUserOptions): Promise ```typescript interface CreateUserOptions { email: string; // Required firstName?: string; lastName?: string; role?: string; } interface CreateUserResponse { id: string; // User UUID apiKey: string; // sk_ prefixed API key } ``` ### admin.users.createConnectSession(userId: string, options?): Promise ```typescript // `install` and `cli` are a discriminated union: both are present (when // `includeInstall: true` is passed) or both are null. They are never // independently nullable. interface CreateConnectSessionResponseBase { session_id: string; // durable session UUID user_id: string; activation_url: string; // send this to the user expires_at: string; // ISO 8601 organization_name: string; partner_name: string | null; connected_providers: string[]; recommended_provider: string | null; } type CreateConnectSessionResponse = CreateConnectSessionResponseBase & ( | { install: { claude_code: { client: 'claude_code'; recommended_scope: 'project' | 'user'; command: string; config_path: string; config: Record; }; cursor: { client: 'cursor'; install_url: string; deeplink_url: string; config: Record; }; claude_desktop: { client: 'claude_desktop'; config_path: string; config: Record; }; chatgpt: { client: 'chatgpt'; beta: boolean; url: string; note: string; }; codex: { client: 'codex'; config_path: string; // ~/.codex/config.toml config_toml: string; // [mcp_servers.attrove] block to merge in login_command: string; // codex mcp login attrove note: string; }; }; cli: { command: string; // npx @attrove/cli connect --session ... json_command: string; install_claude_code: string; install_cursor: string; install_claude_desktop: string; }; } | { install: null; cli: null } ); ``` ## Streaming API For real-time streaming of query responses via WebSocket: ```typescript const result = await attrove.stream('What happened in the meeting?', { onChunk: (chunk: string) => process.stdout.write(chunk), onState: (state: StreamState) => console.log('State:', state), onEnd: (reason: StreamEndReason) => console.log('Done:', reason), }); console.log('Full answer:', result.answer); ``` ```typescript type StreamState = | 'selecting_messages' | 'streaming' | 'completed' | 'cancelled' | 'error'; type StreamEndReason = 'completed' | 'cancelled' | 'error'; type StreamFrame = | { type: 'chunk'; message_id: string; content: string } | { type: 'end'; message_id: string; reason: StreamEndReason; used_message_ids?: string[]; used_meeting_ids?: string[]; used_event_ids?: string[]; } | { type: 'error'; message_id: string; error: string } | { type: 'state'; message_id: string; state: StreamState } | { type: 'message_ids'; message_id: string; used_message_ids: string[]; used_meeting_ids?: string[]; used_event_ids?: string[]; } | { type: 'stream_start'; message_id: string }; ``` ## Error Handling: Complete Hierarchy ```typescript import { AttroveError, // Base class for all SDK errors AuthenticationError, // 401 — invalid/expired token NotFoundError, // 404 — resource not found RateLimitError, // 429 — rate limited (check retryAfter) ValidationError, // 400/422 — invalid input NetworkError, // Connection/timeout errors isAttroveError, // Type guard } from '@attrove/sdk'; // Error properties class AttroveError extends Error { code: ErrorCode; statusCode?: number; details?: ErrorDetails; } class RateLimitError extends AttroveError { retryAfter?: number; // Seconds until rate limit resets } // Error codes const ErrorCodes = { AUTH_MISSING_TOKEN: 'AUTH_MISSING_TOKEN', AUTH_INVALID_TOKEN: 'AUTH_INVALID_TOKEN', AUTH_EXPIRED_TOKEN: 'AUTH_EXPIRED_TOKEN', AUTH_USER_MISMATCH: 'AUTH_USER_MISMATCH', AUTH_INSUFFICIENT_PERMISSIONS: 'AUTH_INSUFFICIENT_PERMISSIONS', RESOURCE_NOT_FOUND: 'RESOURCE_NOT_FOUND', RESOURCE_ACCESS_DENIED: 'RESOURCE_ACCESS_DENIED', RESOURCE_ALREADY_EXISTS: 'RESOURCE_ALREADY_EXISTS', RESOURCE_DELETED: 'RESOURCE_DELETED', VALIDATION_INVALID_ID: 'VALIDATION_INVALID_ID', VALIDATION_REQUIRED_FIELD: 'VALIDATION_REQUIRED_FIELD', VALIDATION_INVALID_FORMAT: 'VALIDATION_INVALID_FORMAT', VALIDATION_OUT_OF_RANGE: 'VALIDATION_OUT_OF_RANGE', INTEGRATION_OAUTH_FAILED: 'INTEGRATION_OAUTH_FAILED', INTEGRATION_EMAIL_EXISTS: 'INTEGRATION_EMAIL_EXISTS', INTEGRATION_TOKEN_EXPIRED: 'INTEGRATION_TOKEN_EXPIRED', INTEGRATION_SYNC_FAILED: 'INTEGRATION_SYNC_FAILED', INTEGRATION_NOT_CONNECTED: 'INTEGRATION_NOT_CONNECTED', RATE_LIMIT_EXCEEDED: 'RATE_LIMIT_EXCEEDED', INTERNAL_ERROR: 'INTERNAL_ERROR', SERVICE_UNAVAILABLE: 'SERVICE_UNAVAILABLE', REQUEST_TIMEOUT: 'REQUEST_TIMEOUT', }; ``` ## Response Wrappers ```typescript interface SuccessResponse { success: true; data: T; } interface PaginatedResponse { success: true; data: T[]; pagination: { limit: number; offset: number; has_more: boolean; total_count?: number; }; } // Cursor-based pagination — used by endpoints migrated to the unified list // primitive (entities, notes, conversations, threads/messages, messages). // Walk pages forward by passing `pagination.next_cursor` back as `?cursor=`. // There is no `offset` and no `total_count` — use `has_more` to detect the last page. interface CursorPaginatedResponse { success: true; data: T[]; pagination: | { limit: number; has_more: true; next_cursor: string } | { limit: number; has_more: false }; } ``` ## MCP Server Attrove provides an MCP server for AI assistants (Codex, Claude Desktop, Cursor, ChatGPT, Claude Code). **Hosted remote MCP (recommended)**: use `npx @attrove/cli install codex`, `npx @attrove/cli install claude-code`, `npx @attrove/cli install cursor`, `npx @attrove/cli install claude-desktop`, or point remote-capable clients to `https://api.attrove.com/mcp`. Auth is automatic via OAuth 2.1. For Codex, run `codex mcp login attrove` after install; raw hosted tools include `attrove_notes`, `attrove_create_goal`, and `attrove_list_goals`. If you only see `mcp__codex_apps__attrove` tools, that is the OpenAI Apps connector. **Advanced local stdio fallback**: use this only if you explicitly want local credential-backed MCP: ```bash npx @attrove/cli login npx @attrove/cli local install claude-code ``` **Manual stdio config**: ```json { "mcpServers": { "attrove": { "command": "npx", "args": ["-y", "@attrove/mcp@latest"], "env": { "ATTROVE_SECRET_KEY": "sk_...", "ATTROVE_USER_ID": "user-uuid" } } } } ``` ### MCP Tool Schemas #### attrove_query Ask questions and get AI-generated answers with sources. ```json { "name": "attrove_query", "inputSchema": { "type": "object", "properties": { "query": { "type": "string", "description": "The question to ask about the user's context" }, "integration_ids": { "type": "array", "items": { "type": "string" }, "description": "Filter to specific integration IDs (int_xxx)" }, "include_sources": { "type": "boolean", "description": "Include source snippets in the response", "default": false }, "instructions": { "type": "string", "description": "Custom instructions for the AI. Controls output format, filtering, and behavior. Max 20,000 chars." }, "context": { "type": "string", "description": "Authoritative reference data for answer generation. Treated as ground truth. Influences query rewriting but not vector search. Max 20,000 chars." } }, "required": ["query"] } } ``` #### attrove_search Semantic search across messages, meetings, and calendar events. ```json { "name": "attrove_search", "inputSchema": { "type": "object", "properties": { "query": { "type": "string", "description": "Semantic search query" }, "after_date": { "type": "string", "description": "Only results after this date (YYYY-MM-DD)" }, "before_date": { "type": "string", "description": "Only results before this date (YYYY-MM-DD)" }, "sender_domains": { "type": "array", "items": { "type": "string" }, "description": "Filter by sender domains" }, "include_body_text": { "type": "boolean", "description": "Include message body text preview for message results", "default": true } }, "required": ["query"] } } ``` #### attrove_integrations List connected services and their status. No input parameters. #### attrove_connect Create one link the user opens to connect a source (Gmail, Outlook, Slack, Google Calendar, Google Meet, or Microsoft Teams) to their Attrove account. Use it when `attrove_integrations` shows no sources, when a query comes back empty because nothing is connected yet, or when the user asks to add a source. Show the user the returned link. After they finish, call `attrove_integrations` to confirm the source is syncing, then continue. The link is tied to the authenticated user, expires after 7 days, and is safe to return again on later calls: repeated calls hand back the same active link. ```json { "name": "attrove_connect", "inputSchema": { "type": "object", "properties": { "provider": { "type": "string", "enum": [ "gmail", "outlook", "slack", "google_calendar", "google_meet", "teams" ], "description": "Preselect a source. Omit to open a source picker." } }, "required": [] } } ``` #### attrove_events Calendar events with attendees. ```json { "name": "attrove_events", "inputSchema": { "type": "object", "properties": { "start_date": { "type": "string", "description": "Start of date range (YYYY-MM-DD)" }, "end_date": { "type": "string", "description": "End of date range (YYYY-MM-DD)" }, "limit": { "type": "number", "description": "Max events (default 25, max 100)" } }, "required": [] } } ``` #### attrove_meetings Meetings with AI summaries and action items. ```json { "name": "attrove_meetings", "inputSchema": { "type": "object", "properties": { "start_date": { "type": "string", "description": "Start of date range (YYYY-MM-DD)" }, "end_date": { "type": "string", "description": "End of date range (YYYY-MM-DD)" }, "provider": { "type": "string", "enum": ["google_meet", "zoom", "teams"], "description": "Filter by provider" }, "limit": { "type": "number", "description": "Max meetings (default 10, max 50)" } }, "required": [] } } ``` #### attrove_notes List notes with optional filtering. ```json { "name": "attrove_notes", "inputSchema": { "type": "object", "properties": { "ref_type": { "type": "string", "enum": ["message", "meeting", "event", "entity", "goal"], "description": "Filter by reference type" }, "ref_id": { "type": "string", "description": "Filter by referenced resource ID" }, "limit": { "type": "number", "description": "Max notes (default 20, max 100)" } }, "required": [] } } ``` #### attrove_push_note Save a note to user context. ```json { "name": "attrove_push_note", "inputSchema": { "type": "object", "properties": { "body": { "type": "string", "description": "Note content (1-10,000 chars)" }, "title": { "type": "string", "description": "Optional note title (max 500 Unicode code points after trimming)" }, "ref_type": { "type": "string", "enum": ["message", "meeting", "event", "entity", "goal"], "description": "Type of referenced resource" }, "ref_id": { "type": "string", "description": "Opaque ID of referenced resource" } }, "required": ["body"] } } ``` #### attrove_push_meeting Save a meeting from another meeting MCP or a user-shared transcript. ```json { "name": "attrove_push_meeting", "inputSchema": { "type": "object", "properties": { "title": { "type": "string", "description": "Meeting title (required, max 500 Unicode code points after trimming)" }, "start_time": { "type": "string", "description": "ISO 8601 datetime with UTC \"Z\" or numeric offset (required)" }, "end_time": { "type": "string", "description": "ISO 8601 datetime with UTC \"Z\" or numeric offset (required, must be >= start_time)" }, "transcript": { "type": "string", "description": "Full meeting transcript" }, "summary": { "type": "string", "description": "Detailed summary" }, "short_summary": { "type": "string", "description": "1-3 sentence summary" }, "attendees": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string" }, "email": { "type": "string" } }, "required": ["name"] } }, "action_items": { "type": "array", "items": { "type": "object", "properties": { "text": { "type": "string" }, "assignee": { "type": "string" } }, "required": ["text"] } }, "external_id": { "type": "string", "description": "Dedup key from the source meeting system" } }, "required": ["title", "start_time", "end_time"] } } ``` #### attrove_delete_meeting Reversibly archive a pushed meeting by `id` or `external_id`. This is a soft archive: the meeting is hidden from meeting/search/query results and can be restored by re-pushing the same `external_id`. ```json { "name": "attrove_delete_meeting", "inputSchema": { "type": "object", "properties": { "id": { "type": "string", "description": "Opaque meeting ID (mtg_xxx)" }, "external_id": { "type": "string", "description": "External ID used when the meeting was pushed" } } } } ``` #### attrove_delete_note Reversibly archive a note by `id` or `external_id`. This is a soft archive: the note is hidden from notes/search/query results and can be restored by re-pushing the same `external_id`. ```json { "name": "attrove_delete_note", "inputSchema": { "type": "object", "properties": { "id": { "type": "string", "description": "Opaque note ID (note_xxx)" }, "external_id": { "type": "string", "description": "External ID used when the note was pushed" } } } } ``` #### attrove_create_goal Create a watched outcome. Use when the user wants to start tracking a deal, project, follow-up, or deadline-sensitive decision. ```json { "name": "attrove_create_goal", "inputSchema": { "type": "object", "properties": { "title": { "type": "string", "description": "Goal title (required, max 200 characters)" }, "description": { "type": "string", "description": "Optional goal description (max 4,000 characters)" }, "watch_scope": { "type": "object", "description": "Must include at least one of entity_ids, seed_query, or keywords", "properties": { "entity_ids": { "type": "array", "items": { "type": "string" } }, "seed_query": { "type": "string" }, "keywords": { "type": "array", "items": { "type": "string" } }, "date_floor": { "type": "string", "description": "Optional ISO 8601 datetime floor; older evidence excluded" }, "source_types": { "type": "array", "items": { "type": "string", "enum": ["messages", "meetings", "events", "notes"] } } } }, "success_criteria": { "type": "string", "description": "Optional success criteria" }, "completion_condition": { "type": "string", "description": "Optional explicit completion condition" }, "deadline": { "type": "string", "description": "Optional ISO 8601 datetime deadline. Date-only strings are not accepted" } }, "required": ["title", "watch_scope"] } } ``` #### attrove_list_goals List goals, optionally filtered by lifecycle, health, whose move is next (`next_move_owner`), or acknowledgment state. Rows include `next_move` (owner: `us` | `them` | `ambiguous`, obligation, evidence basis, confidence) from the latest evaluation, so one call builds a ball-in-court board — e.g. `next_move_owner: "us"` + `lifecycle: "active"` is "everything waiting on our side". Pass `pagination.next_cursor` back as `cursor` for further pages. ```json { "name": "attrove_list_goals", "inputSchema": { "type": "object", "properties": { "lifecycle": { "type": "string", "enum": ["active", "completed", "archived", "cancelled"] }, "health": { "type": "string", "enum": ["on_track", "at_risk", "blocked", "waiting_on_human", "insufficient_evidence"] }, "next_move_owner": { "type": "string", "enum": ["us", "them", "ambiguous", "none"], "description": "Whose move is next on the latest evaluation; \"none\" = no next move on the latest snapshot" }, "attention_state": { "type": "string", "enum": ["acknowledged", "normal"], "description": "\"acknowledged\" = an active snooze is suppressing silence escalation" }, "limit": { "type": "number", "description": "Max goals to return (default 20, max 100)" }, "cursor": { "type": "string", "description": "Opaque pagination cursor from pagination.next_cursor" } }, "required": [] } } ``` #### attrove_get_goal_status Full goal record with the latest snapshot. Returns `last_snapshot.cited_evidence_refs` (evaluator-cited evidence); manual notes are not returned by this tool. ```json { "name": "attrove_get_goal_status", "inputSchema": { "type": "object", "properties": { "goal_id": { "type": "string", "description": "Goal ID in gol_xxx format" } }, "required": ["goal_id"] } } ``` #### attrove_evaluate_goal Queue a fresh evaluator run (async — returns a run_id immediately). Check completion via attrove_get_goal_status: when `last_snapshot.evaluator_run_id` equals the returned run_id, the run finished. Rate limit: 6 manual runs per goal per hour. ```json { "name": "attrove_evaluate_goal", "inputSchema": { "type": "object", "properties": { "goal_id": { "type": "string", "description": "Goal ID in gol_xxx format" } }, "required": ["goal_id"] } } ``` #### attrove_add_goal_note Attach a manual note as goal evidence. Indexed asynchronously (typically within seconds), not immediately. ```json { "name": "attrove_add_goal_note", "inputSchema": { "type": "object", "properties": { "goal_id": { "type": "string", "description": "Goal ID in gol_xxx format" }, "body": { "type": "string", "description": "Note body (required, max 10,000 characters)" }, "title": { "type": "string", "description": "Optional note title" }, "metadata": { "type": "object", "description": "Optional free-form metadata (e.g., source, recordedBy)" } }, "required": ["goal_id", "body"] } } ``` #### attrove_confirm_goal_status Human confirm or override of goal status, with a required reason. Provide lifecycle or health (or both). Lifecycle is the only path for completing, archiving, or cancelling a goal. ```json { "name": "attrove_confirm_goal_status", "inputSchema": { "type": "object", "properties": { "goal_id": { "type": "string", "description": "Goal ID in gol_xxx format" }, "lifecycle": { "type": "string", "enum": ["completed", "archived", "cancelled"], "description": "Target lifecycle. Required if health is omitted. Terminal values stop evaluation" }, "health": { "type": "string", "enum": ["on_track", "at_risk", "blocked", "waiting_on_human", "insufficient_evidence"], "description": "Target health. Required if lifecycle is omitted" }, "reason": { "type": "string", "description": "Required reason for the manual status change (max 2000 chars)" } }, "required": ["goal_id", "reason"] } } ``` #### attrove_goal_events Tail goal lifecycle transitions (completed, archived, cancelled, and the other goals.* events) as a pollable, append-only feed, oldest-first. Complements attrove_list_goals: that reports each goal's current state and health; this reports the discrete transitions over time. Pass `since` plus `since_event_id` from the prior response watermark to poll incrementally; pass `pagination.next_cursor` as cursor to drain additional pages. ```json { "name": "attrove_goal_events", "inputSchema": { "type": "object", "properties": { "since": { "type": "string", "description": "ISO 8601 datetime lower bound (e.g. \"2026-05-01T00:00:00Z\"). Pair with since_event_id for a durable composite watermark" }, "since_event_id": { "type": "number", "description": "Event id tie-breaker paired with since. Pass the prior response watermark.event_id so same-timestamp rows are not skipped" }, "types": { "type": "array", "items": { "type": "string", "enum": ["goals.created", "goals.status_changed", "goals.risk_detected", "goals.evidence_added", "goals.completed", "goals.archived", "goals.cancelled", "goals.status_overridden", "goals.evaluation_failed", "goals.acknowledged", "goals.acknowledgment_resolved", "goals.next_move_changed", "goals.reopened"] }, "description": "Event types to include. Defaults to lifecycle boundary types (goals.completed, goals.archived, goals.cancelled, goals.reopened) when omitted" }, "limit": { "type": "number", "description": "Max events to return (default 50, max 100)" }, "cursor": { "type": "string", "description": "Opaque pagination cursor from pagination.next_cursor. Pass it unchanged to drain additional pages for the same since window" } }, "required": [] } } ``` #### attrove_acknowledge_goal Acknowledge that a goal's silence is expected: suppress the silence-driven health escalation until a horizon, or until real activity arrives first — whichever comes first. Requires an active goal with a configured silence_condition. Does NOT suppress non-silence risks (deadline, blocker, churn, sentiment) and does NOT change lifecycle or complete the goal. ```json { "name": "attrove_acknowledge_goal", "inputSchema": { "type": "object", "properties": { "goal_id": { "type": "string", "description": "Goal ID in gol_xxx format" }, "until": { "type": "string", "description": "ISO 8601 timestamp; horizon after which silence re-raises. Must be in the future and within 90 days" }, "reason": { "type": "string", "description": "Optional human-provided reason (max 2000 chars)" }, "channel": { "type": "string", "description": "Optional provenance, e.g. \"mcp\", \"slack\" (max 100 chars)" } }, "required": ["goal_id", "until"] } } ``` #### attrove_clear_goal_acknowledgment Clear the active acknowledgment on a goal, resuming normal silence monitoring immediately. Idempotent: succeeds even if there is no active acknowledgment. ```json { "name": "attrove_clear_goal_acknowledgment", "inputSchema": { "type": "object", "properties": { "goal_id": { "type": "string", "description": "Goal ID in gol_xxx format" }, "reason": { "type": "string", "description": "Optional reason for clearing (max 2000 chars)" } }, "required": ["goal_id"] } } ``` #### attrove_draft_goal_follow_up Draft the next move on a quiet goal as a ready-to-copy follow-up email, grounded in the goal's real thread evidence. Adapts to whose move it is: chase the counterparty when they owe the next step, or draft the user's reply when the user does. Read-only: it does NOT send, create a provider draft, or change the goal. Returns recipient, subject, plain-text body, threading refs, and a review status a human should check before sending. ```json { "name": "attrove_draft_goal_follow_up", "inputSchema": { "type": "object", "properties": { "goal_id": { "type": "string", "description": "Goal ID in gol_xxx format" }, "directive": { "type": "string", "description": "Optional freeform steer for the draft, e.g. \"push for a call this week\" (max 2000 chars)" } }, "required": ["goal_id"] } } ``` #### attrove_watch_commitment Register a commitment — a promise, delegation, or handoff that will remain unresolved after this session — so it survives agent statelessness and surfaces as at risk in later outcome checks if the expected signal stays quiet. A registration becomes an active watch only with an expected signal type AND a check_after/due_at horizon; underspecified registrations are preserved as low-confidence suggestions that never become active watches (the response lists what was missing). Pass client_dedup_key to make retries safe: the same key with the same payload returns the original commitment, while the same key with a different payload is rejected. No external alert route is created by this tool. ```json { "name": "attrove_watch_commitment", "inputSchema": { "type": "object", "properties": { "commitment_text": { "type": "string", "description": "What was promised or delegated, as one self-contained sentence a human can act on later (max 4000 chars)" }, "commitment_kind": { "type": "string", "enum": ["promised_action", "accepted_delegation", "handoff", "waiting_on", "check_back", "escalation_condition"], "description": "Shape of the obligation" }, "expected_signal_type": { "type": "string", "enum": ["reply", "ack", "update_message", "completion_message", "registered_event", "manual_confirmation"], "description": "What future signal would prove the commitment landed. Required for an active watch" }, "expected_signal_description": { "type": "string", "description": "Free-text description of the expected signal — who/what/where" }, "check_after": { "type": "string", "description": "ISO 8601 datetime with timezone after which the commitment should be checked. At least one of check_after/due_at is required for an active watch" }, "due_at": { "type": "string", "description": "ISO 8601 datetime with timezone the commitment is due by; a watching commitment past its horizon is flagged at_risk" }, "counterparty": { "type": "string", "description": "The other party to the obligation — who it is owed to or waited on" }, "agent": { "type": "string", "description": "Agent identity (name/slug) for attribution. Defaults to the ATTROVE_AGENT_ID environment variable when set" }, "escalation_target": { "type": "string", "description": "Accountable human metadata for follow-up. Defaults to the authenticated user; no external delivery route is created" }, "source_ref": { "type": "string", "description": "Provenance pointer: message id, PR URL, ticket id" }, "parent_goal_id": { "type": "integer", "description": "Optional active Attrove goal id to link this commitment to" }, "client_dedup_key": { "type": "string", "description": "Optional caller-chosen key for retry-safe registration. Reuse only with the exact same payload" } }, "required": ["commitment_text", "commitment_kind"] } } ``` #### attrove_check_my_outcomes Read back OPEN commitments (watching / at_risk, optionally suggested) for the authenticated user. Call at the START of a working session to re-hydrate obligations registered in earlier sessions. Results are ordered soonest-due first, each with a one-line summary. This is a to-do ledger, not memory: it never returns resolved history. When has_more is true, pass next_cursor as cursor to drain the next page. ```json { "name": "attrove_check_my_outcomes", "inputSchema": { "type": "object", "properties": { "status_filter": { "type": "array", "items": { "type": "string", "enum": ["watching", "at_risk", "suggested"] }, "description": "Statuses to include. Defaults to [\"watching\",\"at_risk\"]. \"suggested\" adds low-confidence entries awaiting triage" }, "agent": { "type": "string", "description": "Your agent identity for the re-hydration record. Defaults to ATTROVE_AGENT_ID when set" }, "due_within_days": { "type": "integer", "description": "Only commitments due within this many days (1-365)" }, "limit": { "type": "integer", "description": "Max commitments to return (default 50, max 100)" }, "cursor": { "type": "string", "description": "Opaque cursor returned as next_cursor by a previous call" } }, "required": [] } } ``` #### attrove_resolve_commitment Record what actually happened to a watched commitment: "satisfied" (the expected signal arrived), "silent_drop" (it never came and the obligation is dead), "dismissed" (it was not a real commitment), or "at_risk" (flag for attention without closing). Terminal states are immutable, and a suggested entry can only be dismissed. Every resolution appends who/when/which-signal to the commitment's append-only observation log. ```json { "name": "attrove_resolve_commitment", "inputSchema": { "type": "object", "properties": { "commitment_id": { "type": "string", "description": "Commitment id (UUID) from attrove_watch_commitment or attrove_check_my_outcomes" }, "resolution": { "type": "string", "enum": ["satisfied", "silent_drop", "dismissed", "at_risk"], "description": "What actually happened" }, "note": { "type": "string", "description": "Why — recorded verbatim in the observation log (max 2000 chars)" }, "signal_ref": { "type": "string", "description": "Pointer to the satisfying/refuting signal (message id, PR URL, ticket id)" }, "agent": { "type": "string", "description": "Your agent identity for attribution. Defaults to ATTROVE_AGENT_ID when set" } }, "required": ["commitment_id", "resolution"] } } ``` ## Supported Integrations Live: Gmail, Microsoft Outlook, Slack, Google Calendar, and Google Meet. Microsoft Teams is in pilot. Push ingest: messages, meetings, events, and notes. ## Enums Reference ```typescript type IntegrationProvider = "slack" | "gmail" | "outlook" | "google_calendar" | "google_meet" | "teams" | "unknown"; type AuthStatus = "connected" | "disconnected" | "expired" | "error" | "pending" | "unknown"; type SyncStatus = "syncing" | "complete" | "partial" | "error" | "pending" | "paused" | "unknown"; type ConversationType = "channel" | "direct_message" | "group" | "email_thread" | "other" | "unknown"; type MeetingProvider = "google_meet" | "zoom" | "teams" | "manual_meetings" | "unknown"; type ThreadIntegrationType = "slack" | "gmail" | "outlook" | "google_calendar" | "google_meet" | "zoom" | "teams" | "teams_chat" | "teams_meet" | "teams_calendar" | "unknown"; type IntegrationCategory = "email" | "chat" | "calendar" | "meeting" | (string & {}); type EntityType = "person" | "company" | "other" | "bot" | "user"; type GoalLifecycle = "active" | "completed" | "archived" | "cancelled"; type GoalHealth = "on_track" | "at_risk" | "blocked" | "waiting_on_human" | "insufficient_evidence"; type GoalRiskSignalKind = "silence" | "deadline" | "sentiment" | "churn" | "blocker" | "ambiguous_signal" | "other"; type CommitmentKind = "promised_action" | "accepted_delegation" | "handoff" | "waiting_on" | "check_back" | "escalation_condition"; type CommitmentExpectedSignalType = "reply" | "ack" | "update_message" | "completion_message" | "registered_event" | "manual_confirmation"; type CommitmentStatus = "suggested" | "watching" | "at_risk" | "satisfied" | "silent_drop" | "escalated" | "dismissed"; type CommitmentResolution = "satisfied" | "silent_drop" | "dismissed" | "at_risk"; ``` ## Framework Integration Examples ### OpenAI Agents SDK ```typescript import { Attrove } from '@attrove/sdk'; import OpenAI from 'openai'; const attrove = new Attrove({ apiKey: sk_key, userId }); const openai = new OpenAI(); const tools = [ { type: 'function' as const, function: { name: 'query_communication', description: 'Query user email, Slack, and calendar data', parameters: { type: 'object', properties: { question: { type: 'string' } }, required: ['question'], }, }, }, ]; const response = await openai.chat.completions.create({ model: 'gpt-4o', messages: [{ role: 'user', content: 'Summarize my meetings this week' }], tools, }); // Handle tool call if (response.choices[0].message.tool_calls) { const question = JSON.parse( response.choices[0].message.tool_calls[0].function.arguments, ).question; const result = await attrove.query(question); // Feed result.answer back to the model } ``` ### LangChain ```typescript import { Attrove } from '@attrove/sdk'; import { tool } from '@langchain/core/tools'; import { z } from 'zod'; const attrove = new Attrove({ apiKey: sk_key, userId }); const attroveQuery = tool( async ({ question }) => { const result = await attrove.query(question); return result.answer; }, { name: 'attrove_query', description: 'Query user email, Slack, calendar, and meeting data with natural language', schema: z.object({ question: z.string() }), }, ); ``` ### Codex / Claude Desktop / Cursor (MCP, zero code) ```json { "mcpServers": { "attrove": { "command": "npx", "args": ["-y", "@attrove/mcp@latest"], "env": { "ATTROVE_SECRET_KEY": "sk_...", "ATTROVE_USER_ID": "user-uuid" } } } } ``` ### OpenClaw (Skill-Based) OpenClaw agents discover Attrove via SKILL.md. Configure the agent with environment variables: ```yaml # OpenClaw agent config skills: - name: attrove source: npm:@attrove/mcp@latest env: ATTROVE_SECRET_KEY: sk_... ATTROVE_USER_ID: user-uuid ``` The agent reads the SKILL.md trigger phrases and automatically invokes Attrove tools when the user asks about email, Slack, calendar, or meeting data. ## A2A Agent Card Attrove publishes an A2A Agent Card for agent discovery and direct agent-to-agent invocation: - Canonical card: `https://attrove.com/.well-known/agent-card.json` - Legacy alias: `https://attrove.com/.well-known/agent.json` - HTTP+JSON endpoint: `https://api.attrove.com/a2a/v1` Current A2A support is scoped to the core synchronous path: - `POST /a2a/v1/message:send` - `GET /a2a/v1/tasks` - `GET /a2a/v1/tasks/:id` - `POST /a2a/v1/tasks/:id:cancel` Auth matches Attrove's existing agent-facing surfaces: - OAuth Bearer token for user-self access - `sk_` Bearer token plus `X-Attrove-User-Id` for server-scoped end-user access This first A2A surface is deliberately conservative. It supports synchronous query-style interactions only, and returned tasks are short-lived retrieval records rather than durable workflow runs. A2A streaming, A2A push notifications, and extended authenticated agent cards are not enabled yet. ## Webhooks & Event Subscriptions Attrove delivers real-time event notifications via webhooks. Partners create subscription endpoints, choose which event types to receive, and get signed payloads as data flows through the platform. ### Subscription Management (REST API) All endpoints are authenticated with `Bearer ` + `X-Auth-Type: partner`. - `POST /v1/webhooks`: create a subscription endpoint (URL, event types, optional user ID filter) - `GET /v1/webhooks`: list all subscription endpoints - `GET /v1/webhooks/:id`: get subscription details - `PATCH /v1/webhooks/:id`: update endpoint URL, event types, user scope, or pause/resume via `is_active` - `DELETE /v1/webhooks/:id`: remove a subscription - `POST /v1/webhooks/:id/test`: send a synthetic `webhook.test` event to validate connectivity - `POST /v1/webhooks/:id/rotate-secret`: rotate the HMAC signing secret - `GET /v1/webhooks/:id/deliveries`: inspect delivery history with status filtering - `GET /v1/users/:user_id/capabilities`: snapshot of a user's active capabilities (source of truth for reconciliation against `capability.granted` / `capability.revoked` deltas) ### Subscribable Event Types - `messages.new`: newly synced messages are indexed and query-ready - `sync.completed`: integration sync cycle finished with summary metrics - `integration.status_changed`: connected integration status transition (e.g. active → error) - `capability.granted`: a partner-facing capability (`email`, `chat`, `calendar`, `meet`) became available via a new integration; one event per capability - `capability.revoked`: a previously-granted capability was lost because its integration was disconnected via the partner API - `webhook.test`: synthetic test event for endpoint validation - `meetings.new`: meeting records indexed and queryable - `events.new`: calendar events indexed and queryable - `events.starting_soon`: proactive notification before a calendar event starts (configurable: 5/10/15/30/60 min) - `notes.new`: pushed notes indexed and queryable - `goals.created`: a new goal started watching - `goals.status_changed`: a goal's lifecycle or health changed after an evaluator run - `goals.risk_detected`: an evaluator run surfaced new risk signals (silence, deadline pressure, churn, blocker) - `goals.evidence_added`: new evidence was cited for a goal - `goals.completed`: a goal was confirmed complete - `goals.archived`: a goal was archived - `goals.cancelled`: a goal was cancelled - `goals.status_overridden`: a human manually overrode lifecycle or health - `goals.evaluation_failed`: an evaluator run failed (delivery lets you alert on watch gaps) Reserved (not yet subscribable): `meetings.summary_ready`. ### Delivery Guarantees - HMAC-SHA256 signature on every delivery (`webhook-id`, `webhook-timestamp`, `webhook-signature`) - Automatic retries with exponential backoff (up to 18 attempts over 24 hours) - Dead letter queue for failed deliveries with replay/dismiss - Circuit breaker: auto-pauses endpoint after 50 consecutive failures - Each delivery includes an `idempotency_key` for server-side deduplication - Payloads conform to CloudEvents 1.0 specification ### Write Operations Summary Attrove is read-first: live connectors are read-scoped, and these writes affect Attrove-owned state rather than upstream mail, chat, calendar, or meeting records. Attrove does not send provider messages or create provider drafts. Beyond webhook configuration, Attrove exposes these write endpoints: - `POST /v1/users`: provision a new user - `POST /v1/users/:user_id/connect-sessions`: create a durable activation session - `PATCH /v1/users/settings`: update sync preferences - `POST /v1/users/:user_id/messages`, `/meetings`, `/events`, `/notes`: push messages, meetings, events, and notes - `PATCH /v1/users/:user_id/calendars/:id`: update calendar sync preferences - Goal endpoints create and manage Attrove-owned watched outcomes; lifecycle changes remain explicit and human-controlled - Commitment endpoints register, check, and resolve Attrove-owned obligations across sessions Webhooks deliver signed events to endpoints you configure. Self-serve plans do not bundle SMS, email, or Slack alert routes. ## Pricing Self-serve Connect pricing follows published allocations and hard caps. Hands-on workflow setup is scoped separately. - **Starter — $0/month**: 5 connected users and 500 AI queries/month included; hard caps of 10 users and 1,000 queries. No workspace contexts or seats. No card and no usage overage billing. - **Growth — $99/month**: 25 users, 5,000 queries/month, 1 workspace context, and 50 workspace seats included. Hard caps: 50 users, 10,000 queries, and 5 workspace contexts. Overages: $5/additional user/month, $0.01/additional query, and $1/additional workspace seat. - **Scale — $499/month**: 100 users, 50,000 queries/month, 5 workspace contexts, and 500 workspace seats included. Hard caps: 200 users, 100,000 queries, and 20 workspace contexts. Overages: $3/additional user/month, $0.008/additional query, and $1/additional workspace seat. - **Enterprise — custom**: no published fixed hard caps or overage rates; connected-user, AI-query, workspace, and billing terms are configured by agreement. Definitions: a connected user is someone whose communication stream Attrove reads; dashboard logins are not metered. An AI query is a user or agent question over the communication stream; webhook events, workflow updates, and indexing do not count. A workspace context is a Slack or Teams workspace-level connection, and a workspace seat is a member covered by it. Core sync, indexing, webhook delivery, signal updates, and Goals are not billed as separate usage units. Plans do not bundle SMS, email, or Slack alert routes. Machine-readable copy: https://attrove.com/pricing.md ## Machine-Readable API Spec OpenAPI 3.1 specification: https://api.attrove.com/openapi.json ## Links - SDK: https://www.npmjs.com/package/@attrove/sdk - MCP: https://www.npmjs.com/package/@attrove/mcp - Examples: https://github.com/attrove/examples - Documentation: https://attrove.com/docs/quickstart/ - API reference: https://attrove.com/docs/api-reference/ - Pricing: https://attrove.com/pricing/ (machine-readable: https://attrove.com/pricing.md) - Goals (watched outcomes): https://attrove.com/goals/ - Security: https://attrove.com/security/ (SOC 2 Type I attestation, token model, webhook signing, revocation) - Dashboard: https://connect.attrove.com ## Optional - Quickstart: https://github.com/attrove/examples/tree/main/quickstart (B2B2B provisioning flow) - Goal monitor: https://github.com/attrove/examples/tree/main/goal-monitor (watched outcomes — create, evaluate, inspect evidence, poll goal events, ~150 lines) - Goal watch webhook: https://github.com/attrove/examples/tree/main/goal-watch-webhook (webhook receiver — verify signatures, print catch cards for goals.risk_detected / goals.next_move_changed, acknowledge back, ~520 lines) - Meeting prep agent: https://github.com/attrove/examples/tree/main/meeting-prep-agent (~160 lines) - Daily rundown: https://github.com/attrove/examples/tree/main/daily-rundown (scheduled digest, ~230 lines) - Search agent: https://github.com/attrove/examples/tree/main/search-agent (ad-hoc Q&A, ~65 lines) - MCP demo: https://github.com/attrove/examples/tree/main/mcp-demo (zero-code Codex/Claude/Cursor/ChatGPT setup) - Support: support@attrove.com