# Keep in sync with: packages/agent-skill/SKILL.md (source of truth), llms-full.txt, scripts/examples-AGENTS.md, scripts/examples-root-README.md # Synced sections: authentication, SDK methods, MCP tools, error handling. This file adds Optional/Links sections specific to llms.txt. # Attrove > 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()` - Every `Message` includes `thread_id`, `primary_entity_id`, and `entity_ids` by default. Opt into heavy fields via `expand`: `body_text`, `body_html`, `raw` (RFC 5322, emails only), `headers` (parsed, emails only) - **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 ## 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 ## 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`. Flow: create user → receive `sk_` key → create a durable connect session when live OAuth is needed → user authorizes Gmail/Slack via OAuth or you push scoped context via `push.*` → query their data with `sk_` key. ## OAuth Completion Redirect After a user finishes OAuth, Attrove can redirect them back to a URL you control with `?status=success&integration_id=int_xxx` (or `?status=error&error_code=...`) appended, without closing the popup or forcing client-side polling. **Setup:** 1. **Configure `redirect_url` on the API key** (dashboard → API Keys → edit). This is the **origin anchor**: a per-flow URL is only honored when its origin matches the configured value, so a stolen activation URL can't redirect users to attacker domains. Allowed schemes: `https://`, `http://localhost`, `http://127.0.0.1`. Max 2048 chars. 2. **Optionally vary the path/query per flow** by wrapping the OAuth `state` parameter as a JSON envelope: `{"secure":"","redirect_url":"https://app.your-product.com/integrations/done"}` (raw JSON or base64url-encoded). The inner `secure` field is the CSRF token; `redirect_url` must share an origin with the API-key value. 3. **Receive the user back** at the resolved URL with `status`, `integration_id`, and `error_code` query params (any pre-existing values for these keys are stripped to prevent spoofing). `POST /v1/users/:user_id/integrations/connect` supports `response_mode: 'redirect'`, returning `303 See Other` to the resolved URL instead of JSON. Requires `redirect_url` configured on the API key. See `llms-full.txt` for the full reference (resolution table, end-to-end example). ## SDK ```bash npm install @attrove/sdk ``` ### Provision a user (server-side) ```typescript import { Attrove } from '@attrove/sdk'; const admin = Attrove.admin({ clientId: process.env.ATTROVE_CLIENT_ID, clientSecret: process.env.ATTROVE_CLIENT_SECRET, }); const { id: userId, apiKey } = await admin.users.create({ email: 'user@example.com', }); const session = await admin.users.createConnectSession(userId, { includeInstall: true, }); // Send user to: session.activation_url // Terminal/agent handoff: session.cli?.command ``` ### Query user data ```typescript const attrove = new Attrove({ apiKey: process.env.ATTROVE_SECRET_KEY!, userId: process.env.ATTROVE_USER_ID!, }); const response = await attrove.query( 'What needs my attention this week? Include the source messages or meetings you used.', { includeSources: true }, ); console.log(response.answer); console.log(response.used_message_ids); // source message IDs (msg_xxx) console.log(response.used_meeting_ids); // source meeting IDs (mtg_xxx) console.log(response.used_event_ids); // source event IDs (evt_xxx) ``` ### Search messages ```typescript const results = await attrove.search('project deadline', { afterDate: '2026-01-01', senderDomains: ['acme.com'], includeBodyText: true, }); // results.conversations is Record — NOT an array for (const convo of Object.values(results.conversations)) { for (const msgs of Object.values(convo.threads)) { // threads is also a Record for (const msg of msgs) { console.log(msg.sender_name, msg.body_text); // snake_case properties } } } ``` ### Other methods ```typescript const integrations = await attrove.integrations.list(); // connected services const { data: events } = await attrove.events.list({ // calendar events startDate: new Date().toISOString().split('T')[0], endDate: tomorrow.toISOString().split('T')[0], expand: ['attendees'], }); const { data: meetings } = await attrove.meetings.list({ // past meetings with AI summaries expand: ['short_summary', 'action_items'], limit: 5, }); const { data: contacts } = await attrove.entities.list(); // people the user communicates with const { data: graph } = await attrove.entities.relationships(); // co-occurrence network const { data: notes } = await attrove.notes.list(); // user notes const note = await attrove.notes.get('note_xxx'); // single note by ID const goal = await attrove.goals.create({ // outcome monitoring title: 'ACME renewal', watchScope: { seedQuery: 'ACME renewal', keywords: ['ACME'] }, }); const { data: goals } = await attrove.goals.list({ lifecycle: 'active' }); ``` ### Push data (no OAuth required) ```typescript // Push a message const msg = await attrove.push.message({ source: 'email', bodyText: 'The Q4 report is ready.', senderEmail: 'alice@acme.com', externalId: 'email-12345', }); // Push a meeting const mtg = await attrove.push.meeting({ title: 'Sprint Planning', startTime: '2026-01-15T14:00:00Z', endTime: '2026-01-15T14:45:00Z', summary: 'Discussed roadmap priorities...', }); // Push a note const note = await attrove.push.note({ body: 'Decision: chose Redis for caching.', title: 'Architecture Decision', refType: 'goal', refId: 'gol_xxx', }); ``` ### Watch outcomes with Goals 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. ```typescript // Create a watched outcome const goal = await attrove.goals.create({ title: 'Brightwell renewal closed by Jun 30', watchScope: { seedQuery: 'Brightwell renewal', keywords: ['Brightwell', 'renewal'], sourceTypes: ['messages', 'meetings', 'notes'], }, successCriteria: 'Signed order form received.', deadline: '2026-06-30T23:59:59Z', }); // List goals that need attention const { data: atRisk } = await attrove.goals.list({ lifecycle: 'active', health: 'at_risk', }); // Read the latest snapshot — why the state is what it is const g = await attrove.goals.get(goal.id); console.log(g.lastSnapshot?.summary); console.log(g.lastSnapshot?.riskSignals); // e.g. [{ kind: 'silence', description: '...' }] // Fetch the evidence behind the state const { citedInLatestSnapshot, manualNotes } = await attrove.goals.evidence(goal.id); // Other methods: update(), evaluate() (queue a fresh run, 6/goal/hour), // confirmStatus() (human confirm/override with reason), addNote(), // archive(reason, { expectedLifecycleVersion?, idempotencyKey?, initiator? }), // cancel(reason, { expectedLifecycleVersion?, idempotencyKey?, initiator? }), // reopen({ expectedLifecycle, // expectedLifecycleVersion, reasonCode, reason, idempotencyKey }), // snapshots.list(goalId) ``` Notes for agents: - `watchScope` requires at least one anchor: `entityIds`, `seedQuery`, or `keywords` - `deadline` must be a full ISO 8601 datetime; date-only strings are rejected - `lifecycle` is one of `active | completed | archived | cancelled`; it never changes autonomously. Deliberate commands preserve actor/initiator provenance; pass the observed lifecycle version and a stable idempotency key, and compensate a close with `reopen()`. - `health` is system-evaluated: `on_track | at_risk | blocked | waiting_on_human | insufficient_evidence` - Risk signal kinds: `silence | deadline | sentiment | churn | blocker | ambiguous_signal | other` ### Response types (snake_case; do not use camelCase) ```typescript // query() → QueryResponse { answer: string; history: ConversationMessage[]; used_message_ids: string[]; used_meeting_ids: string[]; used_event_ids: string[]; used_note_ids: string[]; sources?: { title: string; snippet: string }[] } // search() → SearchResponse { key_messages: SearchKeyMessage[]; // key message refs conversations: Record; // keyed by thread ID }>; 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 } // SearchThreadMessage fields: // message_id, sender_name, body_text?, received_at, integration_type, recipient_names[] // integrations.list() → Integration[] // Integration fields: // id, provider (e.g. 'gmail', 'slack', 'outlook', 'google_calendar'), name, is_active, auth_status // NOTE: use `provider` not `type`, use `name` not `email` // CursorPage pagination is discriminated: has_more=true includes next_cursor. type CursorPage = { data: T[]; pagination: | { limit: number; has_more: true; next_cursor: string } | { limit: number; has_more: false }; }; // events.list() → CursorPage // CalendarEvent fields: // id, title, start_time, end_time, all_day (boolean), description?, location?, // attendees?: { email: string; name?: string; status?: string }[] // meetings.list() → CursorPage // Meeting fields: // id, title, start_time, end_time, summary?, short_summary?, provider?, // action_items?: { description: string; assignee?: string }[], // attendees?: { email?: string; name?: string }[] // entities.list() → CursorPage // EntityContact fields: // id (ent_xxx), name, entity_type ("person" | "company" | "other" | "bot" | "user"), external_ids: string[], // is_bot: boolean, avatar_uri: string | null // entities.relationships() → RelationshipsPage { data: EntityRelationship[]; pagination: { limit: number; offset: number; has_more: boolean; total_count?: number } } // EntityRelationship fields: // entity_a: { id, name, entity_type, external_ids, is_bot, avatar_uri }, entity_b: { id, name, entity_type, external_ids, is_bot, avatar_uri }, // co_occurrence_count: number, last_interaction_at: string | null // push.message() / push.meeting() / push.event() / push.note() → PushResponse // SDK unwraps the envelope — returns { id, user_id, status, indexed_at } directly { id: string; user_id: string; status: 'queued' | 'processing' | 'indexed' | 'failed'; indexed_at: string | null } // notes.list() → CursorPage // Note fields: // id, body, title?, ref_type? ("message" | "meeting" | "event" | "entity" | "goal"), ref_id?, status?, indexed_at?, created_at, updated_at // goals.list() → GoalsPage (cursor pagination) { data: Goal[]; pagination: { limit: number; has_more: boolean; next_cursor?: string } } // Goal fields (camelCase — goals responses are normalized, unlike other resources): // id (gol_xxx), title, description, watchScope, successCriteria, completionCondition, // deadline, lifecycle ('active' | 'completed' | 'archived' | 'cancelled'), // health ('on_track' | 'at_risk' | 'blocked' | 'waiting_on_human' | 'insufficient_evidence'), // lastEvaluatedAt, lastSnapshot?, lastRun?, createdAt, updatedAt // GoalStatusSnapshot fields (goals.get() lastSnapshot / goals.snapshots.list()): // id, goalId, lifecycle, health, summary, citedEvidenceRefs[], riskSignals[], // nextActions[], confidence, suggestedLifecycle, nextMove, createdAt // goals.evidence() → { citedInLatestSnapshot: CitedEvidenceRef[]; manualNotes: Note[] } // goals.events() → { data: GoalEvent[]; pagination: CursorPage metadata; watermark?: { eventId, occurredAt } } // Pass watermark back as since/sinceEventId; pass pagination.next_cursor as cursor to drain pages. ``` ### Error handling ```typescript import { AuthenticationError, RateLimitError, isAttroveError, } from '@attrove/sdk'; try { await attrove.query('...'); } catch (err) { if (err instanceof AuthenticationError) { /* invalid sk_ token (401) */ } if (err instanceof RateLimitError) { /* retry after err.retryAfter seconds (429) */ } if (isAttroveError(err)) { /* other API error */ } } ``` ## 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" } } } } ``` 23 MCP tools available: - `attrove_query`: ask questions, get AI-generated answers with sources - `attrove_search`: semantic search across messages, meetings, and calendar events - `attrove_integrations`: list connected services - `attrove_events`: calendar events with attendees - `attrove_meetings`: meetings with AI summaries and action items - `attrove_notes`: list notes with filtering - `attrove_push_note`: save a note to user context - `attrove_push_meeting`: save a meeting from another meeting MCP (Otter, Read.ai, Fireflies) or a user-shared transcript (Granola, voice memo, manual notes) - `attrove_delete_meeting`: reversibly archive a pushed meeting by `id` or `external_id` - `attrove_delete_note`: reversibly archive a note by `id` or `external_id` - `attrove_create_goal`: create a watched outcome (title + watch scope; optional success criteria and deadline) - `attrove_list_goals`: list goals filtered by lifecycle, health, whose move is next (`next_move_owner`: us | them | ambiguous | none), or acknowledgment state; rows include `next_move` from the latest evaluation and cursor pagination — one call builds a ball-in-court board - `attrove_get_goal_status`: full goal record with latest snapshot, risk signals, and evidence refs - `attrove_evaluate_goal`: queue a fresh evaluation run for a goal - `attrove_add_goal_note`: attach a manual note as goal evidence - `attrove_confirm_goal_status`: human confirm or override of lifecycle/health, with a reason - `attrove_goal_events`: poll goal lifecycle transitions with cursor metadata - `attrove_acknowledge_goal`: acknowledge that a goal's silence is expected; suppress silence escalation until a horizon or until real activity arrives, whichever comes first - `attrove_clear_goal_acknowledgment`: clear an active acknowledgment and resume silence monitoring immediately - `attrove_draft_goal_follow_up`: draft an evidence-grounded follow-up email for a quiet goal (read-only; nothing is sent) - `attrove_watch_commitment`: register a promise, delegation, or handoff (counterparty + expected signal + check-after/due time) so it is watched until the signal arrives and escalated to a human if it silently never does; underspecified registrations land as suggestions that never escalate - `attrove_check_my_outcomes`: read back open commitments (watching/at_risk, optionally suggested) at session start, soonest-due first with cursor pagination; never returns resolved history - `attrove_resolve_commitment`: record what actually happened (satisfied / silent_drop / dismissed / at_risk), citing the satisfying signal ## A2A Agent Card Attrove publishes an A2A Agent Card for agent discovery: - 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 intentionally narrow and honest: `POST /a2a/v1/message:send`, `GET /a2a/v1/tasks`, `GET /a2a/v1/tasks/:id`, and `POST /a2a/v1/tasks/:id:cancel` over Bearer auth. A2A streaming and A2A push notifications are not enabled yet. Returned tasks are short-lived retrieval records, not durable workflow runs. ## Webhooks & Event Subscriptions Real-time event delivery via webhook subscriptions. Manage endpoints with full CRUD: - `POST /v1/webhooks`: create subscription (URL + event types + optional user filter) - `GET /v1/webhooks`: list subscriptions - `PATCH /v1/webhooks/:id`: update, pause/resume - `DELETE /v1/webhooks/:id`: remove subscription - `POST /v1/webhooks/:id/test`: test endpoint connectivity - `GET /v1/webhooks/:id/deliveries`: inspect delivery history **Event types:** `messages.new`, `sync.completed`, `integration.status_changed`, `capability.granted`, `capability.revoked`, `meetings.new`, `events.new`, `events.starting_soon`, `notes.new`, `webhook.test`, plus goal events: `goals.created`, `goals.status_changed`, `goals.risk_detected`, `goals.evidence_added`, `goals.completed`, `goals.archived`, `goals.cancelled`, `goals.status_overridden`, `goals.evaluation_failed`. Deliveries are HMAC-SHA256 signed via `webhook-id`, `webhook-timestamp`, and `webhook-signature`, auto-retried (18 attempts / 24h), and include idempotency keys. CloudEvents 1.0 format. ## Supported Integrations Live: Gmail, Google Calendar, Google Meet, Slack, Microsoft Outlook, and Microsoft Teams. Push ingest: messages, meetings, events, and notes. ## 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 until upgrade or next cycle) - 5 connected users, 500 AI queries/month - All live connectors, Goals, MCP, API, and webhooks - Docs, SDK, examples, community support ### Growth — $99/month - Card required, cancel anytime. Grace window on limits; we contact you before any billing change - 25 connected users, 5,000 AI queries/month - Everything in Starter, plus SMS/email/Slack alert routes and cited evidence on every alert - Email and shared Slack support ### Custom — talk to the founder - Higher user and query volume, SSO and audit log options, role-based access, custom retention - Concierge workflow scoping, dedicated support - Start at https://attrove.com/workflows/ (send one missed workflow; we scope the signal before you pick a plan) 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