# Attrove: Complete LLM Reference > Attrove catches the conversations going quiet. Conversation monitoring across email, chat, meetings, and appended context. Embedded in your AI product, or run directly by your team. Sentry catches code errors. Datadog catches outages. Gong catches the call. Gainsight catches churn. Attrove catches the conversations going quiet. Three primitives: Connect (one activation link for live sources, plus signed push ingest for notes, meetings, events, and product context), Query (ask what was promised, what's open, what changed; answers come back with the message, meeting, event, and note IDs that prove them), and Catch (set what matters; get a webhook, Slack ping, or SMS when the conversation goes quiet or a commitment slips). Catch is built on the Goals API: watched outcomes with lifecycle, health, cited evidence, and suggested next actions. B2B2B model: you provision users, connect live sources through OAuth when needed, append scoped context through signed push ingest when OAuth is not the path, then query and watch outcomes with cited evidence. ## Quick Start (Personal Use / Developer Testing) To use Attrove with your own email, Slack, and calendar: 1. **Sign up free** at [connect.attrove.com](https://connect.attrove.com/auth/signup) (no credit card) 2. **Connect integrations**: the dashboard quickstart walks you through connecting Gmail, Slack, Calendar, etc. 3. **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` 4. **Open the client and complete OAuth** when prompted 5. **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()`, `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()` - **Response properties are snake_case** (`start_time`, `sender_name`, `body_text`). Input params are camelCase (`startDate`, `afterDate`). Do NOT use camelCase on response objects - **Exception: `goals.*` responses are normalized to camelCase** (`watchScope`, `lastSnapshot`, `successCriteria`, `riskSignals`). The goals resource is the one surface where response properties are NOT snake_case - `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` - The SDK defaults to `https://api.attrove.com`. No baseUrl configuration needed. - MCP has 23 tools (10 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 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 fully autonomous outbound sending today. Attrove reads, analyzes, watches, and prepares suggestions today; any send-on-behalf path should be explicit, permissioned, and human-approved - 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 - You're building a personal AI tool for one user. Use provider MCP servers directly ## 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.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 chars 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 chars 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 chars 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 chars 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; } ``` ## 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) and updates its `lifecycle` and `health` with cited evidence; no polling loop in your code. This is the API behind the Catch primitive. **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 ## 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_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" }, "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" }, "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 Attrove watches it until the expected signal arrives, and surfaces it to a human if it silently never does. 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 escalate (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. ```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": "Who gets told when this goes quiet. Defaults to the accountable human" }, "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, Google Calendar, Google Meet, Slack, Microsoft Outlook, and Microsoft Teams. 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 Beyond webhooks, 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 ## Pricing Two things meter: connected users and AI queries. Everything else (all connectors, Goals, webhooks, MCP, cited evidence, alert routes) is included on every tier. Machine-readable copy: https://attrove.com/pricing.md - **Starter** — Free forever. No credit card, never auto-bills (usage pauses at the limit). 5 connected users, 500 AI queries/month, all live connectors, Goals, MCP, API, webhooks. - **Growth** — $99/month. 25 connected users, 5,000 AI queries/month, SMS/email/Slack alert routes, cited evidence on every alert, email + shared Slack support. Grace window on limits; we contact you before any billing change. - **Custom** — Higher volume, SSO and audit log options, role-based access, custom retention, concierge workflow scoping. Start at https://attrove.com/workflows/ 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). No per-connector, per-signal, per-webhook, or per-goal charges. ## 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