> ## Agent Instructions > Outlit is the product that monitors customers and completes approved customer work. The customer context interfaces are the API, CLI, MCP server, and skills. Canonical references on this site: /api-reference/introduction for API concepts and authentication, /openapi.json for the public API contract, /cli/overview for the CLI, /ai-integrations/mcp for MCP. Prefer the `outlit` CLI or MCP tools for agent tasks; cite page sources when answering. # Customer workflows Source: https://docs.outlit.ai/ai-integrations/customer-workflows Evidence-first workflows for churn, renewals, expansion, and customer collaboration using the Outlit CLI or MCP tools. These recipes are written for agents and automation scripts. The CLI, MCP, and `POST /api/tools/call` expose **corresponding capabilities** — but not identical names or shapes. `outlit sql` maps to `outlit_query`, `customers owner set` maps to `outlit_assign_customer_owner`, and kebab-case flags map to camelCase schema fields (`--billing-status` → `billingStatus`). MCP/API readers should follow tool names and input schemas, not CLI spellings. **Evidence rules that apply to every recipe:** cite what you retrieved, check `contentPage.completeness` before treating source content as complete, and treat missing fields, empty results, and absent permalinks as *unavailable evidence* — never as negative evidence. ## Recipe: weekly churn-risk review Find paying customers that went quiet, then rank by revenue exposure. ```bash theme={null} # 1. Paying customers with no activity in 30 days, highest MRR first outlit customers list --billing-status PAYING --no-activity-in 30d --order-by mrr_cents --json # 2. Full profile for each account worth a look outlit customers get acme.com --include users,revenue,recentTimeline --json # 3. What changed? Recent activity across all channels outlit customers timeline acme.com --timeframe 90d --json # 4. Active facts — current findings to check for risk outlit facts list acme.com --status ACTIVE --json ``` **Then:** open the sources behind facts (`outlit sources get --source-type CALL --source-id `) and check `outlit attention list --customer-id ` for review items Outlit already prepared. ## Recipe: renewal preparation Assemble the account's current state and evidence before a renewal conversation. ```bash theme={null} # Bounded relationship summary: categorized statements with source labels outlit customers relationship acme.com --json # Billing position outlit customers get acme.com --include revenue --json # Renewal-relevant evidence outlit search "renewal" --customer acme.com --json outlit facts list acme.com --status ACTIVE --json # Who owns the account, and who else can see it outlit customers get acme.com --json ``` `customers relationship` returns categorized current statements — not raw fact IDs or source quotes. Use `facts list` / `sources get` when you need the underlying records. ## Recipe: expansion candidates Accounts that are healthy enough to expand. ```bash theme={null} # Active paying customers, sorted by MRR outlit customers list --billing-status PAYING --has-activity-in 7d --order-by mrr_cents --json # Feature adoption for one account (1-53 weeks, optional weekly breakdown) outlit customers features acme.com --weeks 12 --weekly --json # Seat-by-seat engagement inside the account outlit users list --customer-id '' --journey-stage ENGAGED --json ``` An `unavailable` state means a source did not report; it does not mean usage is zero. ## Recipe: investigate attention items Read the review queue Outlit's agents produced. ```bash theme={null} outlit attention list --json outlit attention list --status resolved --customer-id '' --json outlit attention get '' --json ``` Items can include a `preparedActionUrl` for review in the app. The public surface is **read-only** — no resolve, send, or act tool exists; review happens in the product. ## Recipe: hand a customer to a teammate Exact-ID writes; discover the IDs first. ```bash theme={null} # 1. Find the member's user ID (requires workspace_members:read) outlit ws-users list --search alex --json # 2. Assign ownership (requires customer_access:manage) outlit customers owner set '' --target-user-id user_123 --json # Or share without transferring ownership outlit customers grant '' --target-user-id user_123 --role VIEWER --json ``` Write commands accept **exact identifiers only**: a customer UUID and a `user_*` workspace-member ID. The CLI does not fuzzy-match names, emails, or domains for mutations. Discovering members requires `workspace_members:read`; changing customer access requires `customer_access:manage`. ## Recipe: merge a duplicate customer Two-phase and auditable — preview first, then execute with the returned token. ```bash theme={null} # 1. Preview the merge (no mutation yet) outlit customers merge '' '' --json # 2. Execute — requires BOTH the preview token and a stable request ID outlit customers merge '' '' --execute --preview-token '' --request-id merge-acme-001 --json # 3. Check progress (status reads never re-execute) outlit customers merge-status '' --json ``` Reuse the same `--request-id` for an identical retry — it makes the execution idempotent. A different request ID on the same pair is a new execution attempt. ## Recipe: SQL analytics Four read-only views: `activity`, `customers`, `users`, `revenue`. Check the schema before querying: ```bash theme={null} outlit schema customers --json outlit sql "SELECT billing_status, sum(mrr_cents)/100 AS mrr FROM customers WHERE mrr_status = 'calculated' GROUP BY 1" --json outlit sql "SELECT event_name, count(*) FROM activity GROUP BY 1 ORDER BY 2 DESC LIMIT 10" --json ``` SELECT only. `properties`/`traits` columns are JSON strings — inspect with `outlit schema ` before filtering nested values. `mrr_cents` and `lifetime_revenue_cents` are in the billing provider's units: cents for USD, EUR, GBP, and CAD, so `/100` gives the amount in those currencies. Other currencies follow the provider's amount rules (see [Stripe's currency docs](https://docs.stripe.com/currencies)). The `customers` view doesn't carry a currency, so the sum above is one amount only when every customer bills in the same currency. The `revenue` view has a `currency` column on each snapshot; group by it to keep currencies apart. ## Recipe: check source coverage before trusting empty answers `integrations status` reports **configuration readiness** (`not_connected`, `awaiting_auth`, `setup_required`, `ready`, `requires_intervention`) — `ready` proves the connection is configured, not that data has synced or is fresh. Use it to rule out a broken setup, then enumerate actual records: ```bash theme={null} outlit integrations status stripe --json # setup/config readiness only outlit sources list --customer acme.com --json # what records actually exist ``` If you can't confirm a source ever synced, leave the absence inconclusive — report the coverage gap instead of a customer-health conclusion. Sync/backfill freshness is visible in the app's integration settings, not in this command. ## Customer identity touchpoints | Task | Command | Identity argument | | - | - | - | | Inspect resolved identifiers | `outlit customers identity ` | Exact UUID only | | Review merge suggestions | `outlit identity suggestions list` | Optional `--customer-id`, `--status`, `--confidence` filters | | Reject a suggestion | `outlit identity suggestions reject ` | Exact suggestion ID (positional) | | Merge duplicate customers | `outlit customers merge ` | Exact UUIDs, two-phase (see recipe above) | Reads (`customers get`, `relationship`, `timeline`, `facts`, `credits`, `features`) accept a domain, UUID, or exact name. **Writes accept exact IDs.** When in doubt, resolve the UUID with `customers get` first, then feed it to the write command. Auth, permission, identity, and empty-result failure modes Full flag and schema detail for every command # MCP integration Source: https://docs.outlit.ai/ai-integrations/mcp Connect remote MCP clients to Outlit with your workspace URL, OAuth, or scoped API keys. Outlit's MCP server lets MCP-compatible clients query customer data, manage customer ownership and access, and retrieve revenue metrics, timelines, facts, search results, and SQL analytics over a remote MCP connection. The Outlit CLI is not required for MCP setup. Use the workspace MCP URL directly in your MCP client. ## Authentication model Outlit's remote MCP endpoint supports both OAuth and scoped Outlit API keys. Both authentication methods expose the same public tool catalog; Outlit applies the permissions of the caller: * OAuth calls use the signed-in member's current workspace permissions and customer access. * API-key calls use the key's current workspace and explicit grants. For most interactive MCP clients, OAuth is the simplest setup. In practice that means: * you add the workspace MCP URL to your client * you do not paste API keys or bearer headers into config files * the client opens an OAuth sign-in flow and stores the resulting session Headless clients that cannot complete OAuth may send an Outlit API key as their bearer credential. A tool remains unavailable unless the member permissions or API-key grants authorize its underlying command. ## Connect your MCP client Open [**Settings → CLI & MCP**](https://app.outlit.ai/settings/workspace/mcp) and copy the workspace-specific MCP URL for the workspace you want to connect. It looks like: ```text theme={null} https://mcp.outlit.ai/w//mcp ``` Use your client's remote MCP / connector / server settings and add the Outlit URL directly. For clients that accept raw JSON config, the shape is typically: ```json theme={null} { "mcpServers": { "outlit": { "url": "https://mcp.outlit.ai/w//mcp" } } } ``` No `Authorization` header is needed for OAuth-based customer-intelligence tools. After you add the server, your client should prompt you to sign in to Outlit and authorize access for that workspace. Ask your client: ```text theme={null} Show me the available Outlit analytics views. ``` For coding agents that run local commands, see [Agent skills](/ai-integrations/skills). ## Available tools ### Customers and people | Tool | What it does | | - | - | | `outlit_list_customers` | Filter customers by billing status, activity recency, revenue, and exact trait values | | `outlit_list_users` | Filter users by journey stage, activity, customer, and exact trait values | | `outlit_list_workspace_users` | Browse internal workspace users such as CSMs, managers, account owners, and admins | | `outlit_get_customer` | Get a complete customer profile with users, revenue, timeline, and metrics | | `outlit_get_customer_relationship` | Get the bounded relationship summary with categorized statements and source labels | | `outlit_get_customer_credits` | Read imported credit pools, remaining credits, burn rate, and runway | ### Identity | Tool | What it does | | - | - | | `outlit_get_customer_identity` | Read a customer's identity coverage and possible-split records | | `outlit_list_identity_merge_suggestions` | Browse saved identity merge suggestions and history | | `outlit_reject_identity_merge_suggestion` | Reject one saved suggestion by exact ID | | `outlit_merge_customers` | Preview then execute a two-phase customer merge by exact UUIDs | | `outlit_get_customer_merge_status` | Track a merge operation by its operation ID | ### Attention and access | Tool | What it does | | - | - | | `outlit_list_attention_items` | List authorized open or resolved attention items with priority and a prepared-action URL | | `outlit_get_attention_item` | Retrieve one attention item by exact UUID | | `outlit_assign_customer_owner` | Assign a customer's primary owner | | `outlit_grant_customer_access` | Share a customer with a workspace member as Viewer or Editor | | `outlit_update_customer_access` | Change an existing collaborator between Viewer and Editor | | `outlit_revoke_customer_access` | Remove a collaborator's explicit access to a customer | ### Activity and analysis | Tool | What it does | | - | - | | `outlit_get_timeline` | Get the activity timeline for a customer, filtered by channel and event type | | `outlit_list_facts` | Retrieve structured customer facts with status, source type, public fact type and category, occurrence time filters, pagination, and source provenance (`sourceMetadata` includes `sourceType`, `sourceId`, `sourceOccurredAt`, `sourceQuote`, and `permalink`) | | `outlit_get_fact` | Retrieve one exact fact by ID, optionally expanding best-effort related data like evidence | | `outlit_get_source` | Retrieve one exact source record by generic `sourceType` and `sourceId` | | `outlit_list_sources` | List concrete source records such as calls, emails, calendar events, support tickets, Slack conversations, or opportunities | | `outlit_search_customer_context` | Search across customer facts, emails, calls, calendar events, support tickets, Slack conversations, and CRM opportunities using natural language. Returns grouped artifact-level matches for `source` and `fact` results, with `sourceTypes` and time-range filters | | `outlit_query` | Run read-only SQL queries against your customer data | | `outlit_schema` | Get view schemas to help write accurate SQL queries | ### Features | Tool | What it does | | - | - | | `outlit_list_features` | Read configured Features, historical evidence, sources, and ready, partial, or unavailable candidate discovery | | `outlit_create_feature` | Create one Feature from an exact product-event rule | | `outlit_archive_feature` | Archive a non-final Feature using its current opaque revision | | `outlit_get_customer_features` | Read exact Feature usage for one authorized customer | ### Feedback | Tool | What it does | | - | - | | `outlit_submit_feedback` | Submit product feedback — bugs, ideas, or agent friction — to the Outlit team | `outlit_submit_feedback` takes `body` (required, at most 20,000 characters), optional `area` (at most 100 characters), and optional `context` (a JSON object of at most 16 KiB for diagnostics — never secrets or unnecessary customer data). It returns `{ id, createdAt }`. Submissions are authenticated and rate limited under a dedicated quota that does not consume paid plan API-call quota. Feedback is product intake, not a support ticket, and does not guarantee a reply. MCP registers the entire public catalog, including customer ownership/access, Features, and the safe destination, integration setup/status, activation, and workspace-settings capabilities described in [Workspace configuration](/ai-integrations/platform-actions). Feature creation and archive still require the underlying workspace permission and feature availability. See [Agents and AI overview](/ai-integrations/overview#read-vs-write-controls) for the public tool boundaries. # Agents and AI overview Source: https://docs.outlit.ai/ai-integrations/overview How external agents and developers consume Outlit customer context: CLI, MCP, the tool gateway API, skills, and Pi. Outlit is the product that monitors customers and completes approved customer work — its own opinionated agents produce the churn and renewal attention items your team reviews (including billing-risk evidence), and surface expansion insights inside customer context. The CLI, MCP server, and API let your coding agents and applications read authorized customer context and perform supported workspace actions. ## Choose your interface | Surface | Best for | Auth | Reference | | - | - | - | - | | `outlit` CLI | Coding agents, scripts, terminal work | `ok_` API key (`--api-key`, `OUTLIT_API_KEY`, or stored credentials) | [CLI](/cli/overview) | | Remote MCP | MCP clients (Cursor, VS Code, Claude, custom clients) | OAuth (member permissions) or scoped API key (key grants) | [MCP](/ai-integrations/mcp) | | Tool gateway API | HTTP integrations, serverless, non-JS runtimes | `Authorization: Bearer ok_...` | [Tools](/api-reference/tools) | | Agent skills | Teaching agents workflows on top of CLI/MCP | Inherits the CLI/MCP credential | [Skills](/ai-integrations/skills) | | `@outlit/pi` | Pi agents with embedded Outlit tools | `OUTLIT_API_KEY` | [Pi](/ai-integrations/pi) | | Tracking SDKs | Emitting events into Outlit (not querying) | `pk_` public key in the URL path | [Tracking](/tracking/quickstart) | CLI, MCP, and the gateway share **one underlying tool catalog and authorization model** — but names and input shapes differ per surface. The CLI wraps tools in kebab-case commands/flags (`outlit customers list --billing-status PAYING`), while MCP and the API use tool names and camelCase schema fields (`outlit_list_customers`, `billingStatus`). Pick whichever host fits; capabilities and permissions are the same. ## How an agent answers a customer question This is the evidence-first pattern the interfaces are designed around: `outlit schema` lists analytics views and columns. `outlit customers list` / `outlit users list` filter by billing status, journey stage, activity recency, and traits. `outlit customers get --include users,revenue,recentTimeline` returns a bounded profile. `outlit customers timeline` filters activity by channel and event type. `outlit facts list` returns structured facts with provenance (`sourceType`, `sourceId`, `sourceOccurredAt`, `sourceQuote`, `permalink` when available). `outlit search ""` returns grouped artifact-level `source` and `fact` results — not raw vector chunks. `outlit sources get --source-type CALL --source-id ` returns the exact record. When a source offers `contentPage`, check `contentPage.completeness` before treating the content as complete — partial pages mean more content exists beyond what was returned. Cite the sources and facts you actually retrieved. An absent permalink or empty result set is **unavailable evidence**, not negative evidence — say so instead of inferring. ## Read vs. write controls Most of the catalog is read-only customer intelligence. Writes are bounded and each requires an explicit grant: | Write family | Examples | Notes | | - | - | - | | Customer collaboration | `customers owner set`, `customers grant`, `customers revoke` | Requires `customer_access:manage`; writes need **exact UUIDs** — the CLI never fuzzy-matches names, domains, or emails for mutations | | Destinations | `destinations create/update/enable/disable/archive` | Configuration is masked in reads; secrets are never returned | | Features | `features create`, `features archive` | Create maps one product event to a Feature; archive requires the current opaque revision | | Workspace config | `activation update`, `settings update` | Narrow, enumerated settings — not arbitrary config | | Identity resolution | `customers merge`, `identity merge` | Exact IDs only; merges are auditable and reportable via `merge-status` | | Product feedback | `feedback`, `outlit_submit_feedback` | One-way product intake for bugs, ideas, and agent friction; authenticated with a dedicated rate limit, no paid quota, and no support-ticket or reply guarantee | | Attention items | — | **Read-only.** `attention list`/`get` expose reviewable items and a `preparedActionUrl`; there is no public resolve/send/act tool | Outlit manages its monitoring rules and schedules. Public tools do not configure agents or signals, store or reveal credentials, disconnect integrations, or send arbitrary notifications. ## Authorization model * **API keys are independent workspace principals.** A key's grants are its own; they do not inherit the creator's human permissions or customer access. * **OAuth (MCP) runs as the signed-in member**, so that member's workspace permissions apply directly. * **Read presets ≠ write presets.** Read-only keys get discovery but not mutation; `workspace_members:read` and `customer_access:manage` are separate grants, and the discover-then-mutate flow needs both. * **CLI output is script-friendly.** See [Output behavior](/cli/configuration#output-behavior) for the exact JSON-mode rules. ## Distinguish the MCP servers * **Product MCP** — `https://mcp.outlit.ai/w//mcp`: customer intelligence tools described here. * **Docs search MCP** — the documentation-site search endpoint Mintlify hosts for this docs site. It searches these docs only; it has no customer data and no product tools. Do not point customer-intelligence workloads at the docs endpoint, and do not expect product tools there. ## Next steps Copy-paste task recipes: churn review, renewal prep, expansion, evidence trails Auth, permissions, empty results, identity, JSON, MCP, and SDK delivery issues The shared HTTP contract behind CLI and MCP Every command, flag, and output schema # Pi agents Source: https://docs.outlit.ai/ai-integrations/pi Build Pi agents with Outlit customer intelligence tools, skills, and examples. `@outlit/pi` registers Outlit customer intelligence tools inside [Pi](https://github.com/mariozechner/pi-coding-agent). Use it when you want a Pi agent to query complete customer profiles, timelines, facts, sources, and customer-context search results through Outlit's public tool gateway. ## Install ```bash theme={null} pi install npm:@outlit/pi ``` Set an Outlit API key before starting Pi: ```bash theme={null} export OUTLIT_API_KEY=ok_your_api_key pi ``` ## Default tools The default Pi extension exposes the customer intelligence tools that are read-only: | Tool | What it does | | - | - | | `outlit_list_customers` | Browse customers by billing status, activity, revenue, traits, or name | | `outlit_list_users` | Browse contacts by journey stage, activity, customer, traits, email, or name | | `outlit_get_customer` | Retrieve one complete customer profile | | `outlit_get_timeline` | Retrieve customer activity across product, billing, support, and conversation channels | | `outlit_list_facts` | List structured facts with status, source type, date, and provenance filters | | `outlit_get_fact` | Retrieve one exact fact | | `outlit_get_source` | Retrieve one exact source record behind a fact or search result | | `outlit_list_sources` | List concrete source records for a customer | | `outlit_search_customer_context` | Search grouped source and fact results with natural language | SQL tools are available through `@outlit/tools`, but they are not enabled by default in `@outlit/pi`. Use them only for agents that should run read-only SQL. ## Minimal extension If you only need the Outlit tools and the bundled `outlit` Pi skill, the package default is enough: ```ts theme={null} import { createOutlitPiExtension } from "@outlit/pi" export default createOutlitPiExtension() ``` The extension reads `OUTLIT_API_KEY` from the environment and uses `https://app.outlit.ai` by default. ## Custom toolsets Pass `toolNames` when you want a narrower or broader toolset: ```ts theme={null} import { createOutlitPiExtension, piToolNames } from "@outlit/pi" export default createOutlitPiExtension({ toolNames: piToolNames, }) ``` Use the default policy for nine read-only customer tools. Use `analyticalToolNames` when the agent also needs `outlit_schema` and read-only `outlit_query` (11 tools total). `piToolNames` is broader: it includes SQL, customer relationship and Attention reads, customer ownership and access actions, Feature tools, workspace configuration, and `outlit_submit_feedback` for product feedback intake. Choose the smallest set your Pi agent needs. ## Growth agent examples The [`examples/pi-agents`](https://github.com/OutlitAI/outlit-sdk/tree/main/examples/pi-agents) package shows four Pi agents built on `@outlit/pi` for revenue and retention work. It includes: * Outlit customer intelligence, SQL, and schema tools registered in Pi * the generic `outlit` skill from `@outlit/pi` * a shared growth-agent skill with evidence-gathering playbooks * a local usage-decay churn pretriage helper with a `churn.json` config * slash commands and editable prompt templates for usage decay, friction-to-churn, activation failure, and expansion readiness Try it locally from the SDK repo: ```bash theme={null} cd examples/pi-agents npm install export OUTLIT_API_KEY=ok_your_api_key pi install -l . pi ``` Then ask: ```text theme={null} /outlit-usage-decay-watchtower paying customers over $500 MRR ``` or: ```text theme={null} Which paying customers are active enough to be expansion candidates but blocked by plan limits? ``` See the HTTP route and shared `@outlit/tools` contracts Install Outlit guidance for coding agents # Workspace configuration Source: https://docs.outlit.ai/ai-integrations/platform-actions Manage safe destinations, activation, workspace settings, and Features through the tool gateway. Use `POST /api/tools/call` or the CLI to manage delivery destinations, the activation event, selected workspace settings, Features, and integration setup. Each action requires the matching API-key grant or member permission. ## What you can configure | Area | Tools | | - | - | | Destinations | List, get, create, update, enable, disable, and archive | | Activation | Get, preview, and update the ordinary customer activation event | | Workspace settings | Get and update the workspace timezone | | Features | List, create, and archive product capabilities observed through exact event rules | | Integrations | List integrations, complete supported setup steps, and check readiness | Destination results mask configuration values. Integration setup accepts provider-specific credential and configuration shapes and does not return submitted credentials. See [Agents and AI overview](/ai-integrations/overview#read-vs-write-controls) for public tool boundaries. For example, use these CLI commands to perform the same actions: ```bash theme={null} outlit destinations create --type slack --channel-id C0123456789 --label "#customer-ops" --default --json outlit activation preview --event activated --json outlit settings update --default-timezone America/Los_Angeles --json outlit features list --json outlit features create --source metric_source_v1_0123456789abcdef0123456789abcdef --event report_exported --key reports_exported --name "Reports exported" --json outlit features archive value_feature_v1_0123456789abcdef0123456789abcdef --revision value_feature_revision_v1_fedcba9876543210fedcba9876543210 --json ``` Use the generated [OpenAPI specification](/openapi.json) for the exact request, output, and stable gateway error schemas. # Agent skills Source: https://docs.outlit.ai/ai-integrations/skills Give coding agents better guidance for customer queries and Outlit SDK work. Agent skills teach coding agents how to work with Outlit. The `outlit` skill covers customer intelligence workflows. The optional `outlit-sdk` skill covers tracking and instrumentation work. Skills are open source at [github.com/OutlitAI/outlit-agent-skills](https://github.com/OutlitAI/outlit-agent-skills). Use skills when the agent runs local commands or needs workflow guidance. Use [MCP integration](/ai-integrations/mcp) for remote MCP clients, and use [Pi agents](/ai-integrations/pi) when you are packaging a Pi extension with Outlit tools, prompts, or custom skills. ## Install Install the `outlit` skill for detected local agents: ```bash theme={null} outlit setup --yes ``` To choose agents manually or add the optional `outlit-sdk` skill, run `outlit setup skills`. For a specific agent, use `outlit setup `; [Set up coding agents](/cli/ai-agents#supported-agents) lists the supported names. ```bash theme={null} npx -y skills add https://github.com/OutlitAI/outlit-agent-skills -g ``` To select a skill and agent explicitly: ```bash theme={null} npx -y skills add https://github.com/OutlitAI/outlit-agent-skills --skill outlit --agent claude-code --agent opencode -y -g ``` ## Verify ```bash theme={null} outlit doctor ``` Look for a per-agent check such as: ```text theme={null} ✓ Claude Code: Outlit skill installed ``` ## Available skills | Skill | What it teaches | | - | - | | **outlit** | Customer queries, timelines, search, facts, SQL, and when to use CLI vs MCP | | **outlit-sdk** | Tracking integration work: detection, consent, authentication, billing-integration guidance, and event instrumentation | # Troubleshoot agents and the API Source: https://docs.outlit.ai/ai-integrations/troubleshooting Auth, permissions, identity, JSON output, MCP, and SDK delivery failure modes for the CLI, MCP, and API. ## Authentication | Symptom | Cause | Fix | | - | - | - | | `not_authenticated` / `auth_required` | No key found | Provide a key via `--api-key`, `OUTLIT_API_KEY`, or stored credentials (`outlit auth login`) — precedence is flag → env → stored config | | `invalid_key_format` | Key doesn't start with `ok_` | Re-copy the key from [**Settings → API keys**](https://app.outlit.ai/settings/workspace/api-keys) | | `invalid_key` / `validation_failed` | Server rejected the key | Verify with `outlit auth status`; rotate the key if revoked | | `401` on the tool gateway | Missing/expired bearer | `POST /api/validate-api-key` confirms whether the key authenticates at all | | MCP client never prompts to sign in | OAuth flow didn't start | Re-add the workspace URL `https://mcp.outlit.ai/w//mcp`; headless clients must send an API key as bearer instead | | `unsupported_core_version` | Workspace runs an older Outlit server | Upgrade the CLI; if the workspace is still behind, the capability isn't available there yet | ## Permissions and authorization * **`TOOL_CALL_FORBIDDEN` with `retryable: false`** — the key or member lacks the underlying grant. Add the grant on the key (or use an OAuth session with permission); retrying the identical call won't help. * **`ws-users list` works but `owner set`/`grant` fails** — discovery needs `workspace_members:read`; collaboration writes need `customer_access:manage`. They are separate grants and the full flow needs both. * **A key can't see what its creator sees** — API keys are independent principals and do not inherit the creator's permissions or customer access. * **Writes reject names/domains** — mutations take exact UUIDs (`customers owner set --target-user-id user_…`). Resolve IDs with `customers get` / `ws-users list` first. ## Empty or partial results | Symptom | Check | | - | - | | Customer has no activity | `outlit integrations status ` reports configuration readiness only — `ready` doesn't prove data synced. Enumerate real records with `sources list`; check sync/backfill freshness in the app's integration settings. Never report "inactive" for a source you can't confirm synced | | `customers features` empty | The response distinguishes *unavailable source evidence* from *no matches* — check which one applies | | `customers credits` empty | Same: unavailable/stale observations are distinct from a zero balance | | Search returns nothing | Try `sources list --customer ` for deterministic enumeration; search is artifact-level (`source` + `fact` groups), not raw chunks | | Fact has no permalink | Normal — cite `sourceType`/`sourceId`/`sourceOccurredAt`/`sourceQuote` instead | | `contentPage` looks truncated | Check `contentPage.completeness`; partial means more content exists — don't summarize as if complete | ## CLI output and scripting * **Expected JSON but got a table** — pass `--json` explicitly, or ensure one auto-trigger: piped stdout, `CI`/`GITHUB_ACTIONS` set, or `TERM=dumb`. * **`outlit` in a cron/CI job exits non-zero** — JSON errors go to stderr as `{"error": …, "code": …}`; all failures exit `1`. See the [error-code table](/cli/configuration#error-codes). * **`doctor` reports a missing skill** — run `outlit setup ` (e.g. `openclaw`, `claude-code`, `codex`, `gemini`, `droid`, `opencode`, `pi`, `skills`). ## MCP setup * **Wrong URL shape** — it is workspace-scoped: `https://mcp.outlit.ai/w//mcp`, copied from Settings → CLI & MCP. A bare `mcp.outlit.ai` URL is not valid. * **Tools visible but calls forbidden** — OAuth runs as the signed-in member; API-key calls use the key's grants. The catalog is identical either way; authorization is not. * **This is not the docs search MCP** — the docs site hosts a separate search-only MCP endpoint. It has no customer tools; use the workspace URL above for product data. ## Tracking and ingest | Symptom | Cause | Fix | | - | - | - | | `403` from `/api/i/v1/…/events` | Domain not enabled | Add the origin in Settings → Website Visitors → Enabled Domains (public key auth is by design, the allowlist is the boundary) | | Some events land, some don't | Partial success | Responses can be `207` per-event — inspect per-event errors instead of assuming all-or-nothing | | Events >100 per request | Batch cap | Split payloads; the schema caps `events` at 100 | | Server events lost on exit | Queue not flushed | Call `await outlit.flush()` / `shutdown()` — `@outlit/node` batches (default 10s flush) | | Returning visitors not tracked | Persisted opt-out | `disableTracking()` writes `outlit_consent` (localStorage + cookie, 1 year). Re-enable with `enableTracking()` | | `data-*` attr ignored | Not in the loader's set | The script tag reads only `data-public-key`, `data-api-host`, `data-track-pageviews`, `data-track-forms`, `data-auto-track`, `data-auto-identify`, `data-track-calendar-embeds` — `trackEngagement`/`idleTimeout` need the [npm package](/tracking/browser/npm) | | `identify()` not linking | Missing identity | Requires `email` or `userId`; Outlit server SDK `track()` additionally accepts `fingerprint`/`customerId` (customer-only events resolve later when identified) | ## Identity mismatches * **Duplicate customers appearing** — check `outlit identity suggestions list`; reject wrong pairs with `suggestions reject `, merge real duplicates with `customers merge` (preview first). * **User events attributed to the wrong account** — Outlit Browser SDK and Outlit server SDK `identify()` accept `customerId` for explicit account attribution; Outlit server SDK `track()` does too. * **Personal-email domains** — `user@gmail.com` resolves to an individual customer keyed on the full address, not a `gmail.com` company; expect separate customers per personal email. Working workflows to compare against Connection and auth detail # Submit feedback Source: https://docs.outlit.ai/api-reference/feedback openapi.json POST /api/feedback Submit product feedback to the Outlit team through the public API. ## Authentication Pass an Outlit API key in the `Authorization` header as `Bearer ok_...`. Feedback attribution comes from the authenticated caller — the request body cannot set a source, user, or organization. Submissions are rate limited under a dedicated per-organization feedback quota. They do not consume paid plan API-call quota. The request body as a whole is capped at 256 KiB (`413` above that), comfortably above the per-field limits below. Feedback is product intake — bug reports, feature ideas, and agent friction — not a support ticket, and it does not guarantee a reply. ## Request body | Field | Type | Required | Description | | - | - | - | - | | `body` | string | Yes | Feedback text: trimmed, non-empty, at most 20,000 characters | | `area` | string | No | Product area label such as `cli`, `mcp`, or `integrations`: trimmed, non-empty, at most 100 characters | | `context` | object | No | JSON object with diagnostic context, at most 16 KiB serialized. Never include secrets or unnecessary customer data | ## Example ```bash theme={null} curl -X POST https://app.outlit.ai/api/feedback \ -H "Authorization: Bearer ok_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "body": "The customers merge preview did not list the losing record traits", "area": "customers", "context": { "surface": "cli", "command": "customers merge" } }' ``` ## Response `201 Created`: ```json theme={null} { "id": "10000000-0000-4000-8000-000000000004", "createdAt": "2026-09-28T12:00:00.000Z" } ``` The same intake is available as the `outlit_submit_feedback` tool through [`POST /api/tools/call`](/api-reference/tools) and the `outlit feedback` CLI command. # Ingest events Source: https://docs.outlit.ai/api-reference/ingest openapi.json POST /api/i/v1/{publicKey}/events Send tracking events to Outlit. Use your organization's `pk_` public key in the URL path. Ingest requests do not use a bearer API key. ## Request body ```json theme={null} { "visitorId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "source": "client", "events": [ { "type": "pageview", "url": "https://example.com/pricing", "path": "/pricing", "title": "Pricing - Example", "referrer": "https://google.com", "timestamp": 1699999999999 } ] } ``` For browser (`client`) tracking, send a UUID `visitorId`; server events can omit it when an event provides `email`, `userId`, `fingerprint`, or `customerId` for identity or account attribution. `source` defaults to `client` and also accepts `server` or `integration`. Send 1–100 events per request. When a browser user is logged in, `userIdentity` can provide `email`, a system-owned `userId`, and user `traits` for immediate resolution. `customerIdentity` can provide a system-owned `customerId` and `customerTraits` for account attribution across the whole batch. ## Event types All events share these common fields: | Field | Type | Required | Description | | - | - | - | - | | `type` | string | Yes | Event type (see below) | | `timestamp` | number | No | Unix timestamp in milliseconds. Defaults to server time. | | `url` | string | Yes | Full page URL | | `path` | string | Yes | URL path (e.g., `/pricing`) | | `referrer` | string | No | Referrer URL | | `utm` | object | No | UTM parameters (see below) | ### UTM parameters ```json theme={null} { "utm": { "source": "google", "medium": "cpc", "campaign": "spring_2024", "term": "pricing software", "content": "header_cta" } } ``` ### Pageview event Track a page view. ```json theme={null} { "type": "pageview", "url": "https://example.com/pricing?plan=pro", "path": "/pricing", "title": "Pricing - Example", "referrer": "https://google.com/search?q=example", "utm": { "source": "google", "medium": "cpc", "campaign": "spring_2024" }, "timestamp": 1699999999999 } ``` Set `type` to `pageview`. `title` is the page title when available. ### Custom event Track any custom event. ```json theme={null} { "type": "custom", "url": "https://example.com/pricing", "path": "/pricing", "eventName": "button_clicked", "properties": { "buttonId": "cta-hero", "page": "/pricing", "variant": "A" }, "timestamp": 1699999999999 } ``` Set `type` to `custom` and provide an `eventName`. The optional `properties` map accepts string, number, boolean, and null values. ### Form event Track a form submission. ```json theme={null} { "type": "form", "url": "https://example.com/contact", "path": "/contact", "formId": "contact-form", "formFields": { "email": "jane@example.com", "name": "Jane Doe", "company": "Acme Inc", "message": "I'd like to learn more..." }, "timestamp": 1699999999999 } ``` Set `type` to `form`. `formId` is the form's ID or name; `formFields` holds field names and values. Do not send passwords, payment details, or national identifiers. Outlit filters `formFields` using a field denylist and sensitive-value checks; these safeguards do not replace filtering at the source. ### Identify event Identify the visitor with their email/userId. ```json theme={null} { "type": "identify", "url": "https://example.com/signup", "path": "/signup", "email": "jane@example.com", "userId": "usr_12345", "customerId": "cust_123", "traits": { "name": "Jane Doe", "company": "Acme Inc", "plan": "enterprise", "role": "admin" }, "customerTraits": { "plan": "enterprise", "seats": 50 }, "timestamp": 1699999999999 } ``` Set `type` to `identify` and provide at least one of `email` or your system-owned `userId`. `traits` updates the user profile. Send your system-owned `customerId` to link that external account or workspace to the resolved contact; `customerTraits` updates its profile. ### Engagement event Track active time on a page. Automatically sent by the Outlit Browser SDK. ```json theme={null} { "type": "engagement", "url": "https://example.com/features", "path": "/features", "activeTimeMs": 45000, "totalTimeMs": 60000, "sessionId": "b2c3d4e5-f6a7-8901-bcde-f12345678901", "timestamp": 1699999999999 } ``` Set `type` to `engagement`. Send `activeTimeMs` for visible-tab interaction time and `totalTimeMs` for total wall-clock time on the page. `sessionId` is a UUID that groups engagement events and resets after 30 minutes of inactivity or when the tab closes. ### Calendar event Track calendar booking from embedded widgets (Cal.com, Calendly). ```json theme={null} { "type": "calendar", "url": "https://example.com/demo", "path": "/demo", "provider": "cal.com", "eventType": "30 Minute Demo", "startTime": "2024-03-15T10:00:00Z", "endTime": "2024-03-15T10:30:00Z", "duration": 30, "isRecurring": false, "timestamp": 1699999999999 } ``` Set `type` to `calendar` and `provider` to `cal.com`, `calendly`, or `unknown`. `eventType` names the meeting; `startTime` and `endTime` use ISO 8601, `duration` is in minutes, and `isRecurring` indicates a recurring meeting. `inviteeEmail` and `inviteeName` are available only through server-side webhooks. ## Response ### Success response ```json theme={null} { "success": true, "processed": 3 } ``` `processed` counts the successfully processed events. Inspect `errors` when `success` is false. ### Partial success response (HTTP 207) When some events succeed and others fail: ```json theme={null} { "success": false, "processed": 2, "errors": [ { "index": 2, "message": "Invalid event type" } ] } ``` ### Error response ```json theme={null} { "success": false, "processed": 0, "errors": [ { "index": -1, "message": "Invalid or inactive tracking configuration" } ] } ``` ## Examples ### cURL - browser events ```bash theme={null} curl -X POST "https://app.outlit.ai/api/i/v1/pk_your_key/events" \ -H "Content-Type: application/json" \ -d '{ "visitorId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "source": "client", "events": [ { "type": "pageview", "url": "https://example.com/pricing", "path": "/pricing", "title": "Pricing", "timestamp": 1699999999999 } ] }' ``` ### cURL - server events ```bash theme={null} curl -X POST "https://app.outlit.ai/api/i/v1/pk_your_key/events" \ -H "Content-Type: application/json" \ -d '{ "source": "server", "events": [ { "type": "identify", "url": "server://jane@acme.com", "path": "/", "email": "jane@acme.com", "userId": "usr_123", "customerId": "cust_123", "traits": { "name": "Jane Doe", "plan": "pro" }, "customerTraits": { "plan": "pro" } }, { "type": "custom", "url": "server://jane@acme.com", "path": "/", "eventName": "subscription_created", "email": "jane@acme.com", "userId": "usr_123", "customerId": "cust_123", "properties": { "plan": "pro", "amount": 99 } } ] }' ``` ### JavaScript (fetch) ```javascript theme={null} const response = await fetch( 'https://app.outlit.ai/api/i/v1/pk_your_key/events', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ visitorId: 'visitor-123', source: 'client', events: [ { type: 'pageview', url: window.location.href, path: window.location.pathname, title: document.title, timestamp: Date.now() } ] }) } ) const result = await response.json() console.log(result) ``` ### Python ```python theme={null} import requests import time response = requests.post( 'https://app.outlit.ai/api/i/v1/pk_your_key/events', json={ 'source': 'server', 'events': [ { 'type': 'identify', 'url': 'server://jane@example.com', 'path': '/', 'email': 'jane@example.com', 'customerId': 'cust_123', 'traits': {'name': 'Jane Doe'}, 'customerTraits': {'plan': 'enterprise'}, 'timestamp': int(time.time() * 1000) } ] } ) print(response.json()) ``` ## Server-side events For server-side tracking, the `visitorId` field can be omitted. Identity is resolved from the event data, including customer-only attribution when `customerId` is provided: * **Identify events**: Use `email` and/or `userId` fields, plus optional customer context * **Custom events**: Include top-level `email`, `userId`, `fingerprint`, and/or `customerId` fields ```json theme={null} { "source": "server", "events": [ { "type": "custom", "url": "server://user@example.com", "path": "/", "eventName": "payment_received", "email": "user@example.com", "userId": "usr_123", "customerId": "cust_123", "properties": { "amount": 99, "currency": "USD" } } ] } ``` Server events require identity or account attribution. Custom events need at least one of `email`, `userId`, `fingerprint`, or `customerId`; identify events need `email` or `userId`. ## Batching best practices For high-volume tracking: 1. **Batch events** - Send multiple events per request (up to 100) 2. **Use background queue** - Don't block user actions 3. **Retry with backoff** - Handle temporary failures 4. **Flush on shutdown** - Send remaining events before exit ```javascript theme={null} // Example batching implementation const eventQueue = [] const FLUSH_INTERVAL = 5000 const MAX_BATCH_SIZE = 100 function track(event) { eventQueue.push({ ...event, timestamp: Date.now() }) if (eventQueue.length >= MAX_BATCH_SIZE) { flush() } } async function flush() { if (eventQueue.length === 0) return const events = eventQueue.splice(0, MAX_BATCH_SIZE) try { await fetch('/api/i/v1/pk_xxx/events', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ visitorId: getVisitorId(), source: 'client', events }) }) } catch (error) { // Re-queue failed events eventQueue.unshift(...events) } } setInterval(flush, FLUSH_INTERVAL) window.addEventListener('beforeunload', flush) ``` # Integration tools Source: https://docs.outlit.ai/api-reference/integrations POST https://app.outlit.ai/api/tools/call Negotiate setup, configure providers, and read canonical integration readiness. Integration setup and status calls use `POST /api/tools/call`. There are no public provider-credential or disconnect REST routes. Send an Outlit bearer API key in the `Authorization` header; setup calls for supported personal integrations use `integrations:connect_own`, while workspace or admin setup uses `integrations:manage`. The Outlit server determines which authorization route applies to the setup request. ## Available tools | Tool | Purpose | | - | - | | `outlit_get_integration_capabilities` | Negotiate setup without sending credentials; `preferredSetupVersion: 1` selects the preferred flow. | | `outlit_setup_integration` | Perform one bounded credential, configuration, or safe-handoff setup transition. | | `outlit_get_integration_status` | Return canonical configuration readiness for all providers or one provider. | | `outlit_begin_integration_setup` | Compatibility tool for starting an older browser handoff. | | `outlit_get_integration_setup_status` | Compatibility tool for polling an authentication session owned by the current actor. | Older clients can continue using their existing integration reads. New clients should use `outlit_get_integration_status` and `outlit_setup_integration`. The generated [OpenAPI specification](/openapi.json) and `@outlit/tools` `publicToolContracts` contain the exact input and output schemas. ## Preferred setup negotiation Call `outlit_get_integration_capabilities` with the provider first. Do not include credentials in this request. When the result contains `preferredSetupVersion: 1`, call `outlit_setup_integration` with the provider and, when requested, its generated `connectionMode`, `credentials`, or `configuration` shape. ```bash theme={null} curl -X POST https://app.outlit.ai/api/tools/call \ -H "Authorization: Bearer ${OUTLIT_API_KEY}" \ -H "Content-Type: application/json" \ -d '{ "tool": "outlit_get_integration_capabilities", "input": { "provider": "hubspot" } }' ``` `outlit_setup_integration` returns the provider, its canonical status, an optional `next` transition, and an optional stable error. Supported next transitions are: * `browser_handoff` for authentication, recovery, or provider-controlled external setup * `crm_mapping` with the exact recommendation that can be confirmed and returned * `mixpanel_mapping` with a preview used to choose and confirm an exact mapping On `CREDENTIAL_REQUIRED`, collect the provider's declared credential shape through a secret-safe channel and retry once. Never log, serialize into errors, or return submitted credentials. Browser handoffs must remain on the configured Outlit origin and must not carry credential material in the URL. Only authentication handoffs with a session ID use the compatibility status poll; recovery and external-setup handoffs are terminal responses. If `preferredSetupVersion` is absent or not `1`, use the browser compatibility tools without sending credentials or provider configuration. ## Canonical readiness `outlit_get_integration_status` returns exactly these configuration-readiness states: | State | Meaning | | - | - | | `not_connected` | No usable provider connection exists. | | `awaiting_auth` | Authentication must be completed. | | `setup_required` | Provider configuration or mapping remains. | | `ready` | Required authentication and configuration are complete. | | `requires_intervention` | Setup needs human attention or repair. | `ready` is not evidence that sync, backfill, or data ingestion is complete. Consumers must not compose the compatibility session status, raw provider state, or sync metadata into a different readiness contract. # API overview Source: https://docs.outlit.ai/api-reference/introduction Direct API access for custom integrations. ## Base URL Outlit exposes two API surfaces: | Surface | Base URL | Authentication | | - | - | - | | Platform API | `https://app.outlit.ai` | Bearer API key | | Ingest API | `https://app.outlit.ai/api/i/v1` | Public key in the URL path | [Download the canonical OpenAPI spec](/openapi.json) for schema-driven clients, crawlers, and API discovery. ## Authentication ### Platform API Platform API routes use an Outlit API key in the `Authorization` header: ```http theme={null} Authorization: Bearer ok_... ``` Use the public tool gateway for supported customer and workspace actions and the validation route for API-key checks: ```http theme={null} POST /api/tools/call POST /api/validate-api-key ``` The public tool gateway publishes the full catalog: customer intelligence, destinations, integration setup and status, activation configuration, workspace settings, and read-only SQL. CLI and agent integrations can expose selected subsets of these tools. [Workspace configuration](/ai-integrations/platform-actions) explains destination and workspace-setting tools that are also available through the CLI. ### Ingest API The Ingest API uses your **public key** in the URL path. No additional authentication headers are required for ingestion. ``` POST /api/i/v1/{publicKey}/events ``` Your public key is **not secret** and is safe to include in client-side code. It's designed to be visible in your website's source code. ## Limits ### Ingest API The verified contract limit is **100 events per request** — larger batches are rejected by schema validation. Send events in batches of 100 or fewer. Handle responses by status class, not by a promise of specific headers: * `4xx` other than `429` (validation `400`, auth `401`/`403`, invalid public key or domain) — **do not retry**; these are permanent until the request or credentials change * `207` (ingest partial success) — inspect the body's `processed` count and per-event errors; correct the failed events before retrying them, and never resubmit the batch blindly * network failures, `429`, and `5xx` — retryable; use bounded exponential backoff Per-key rate-limit headers are not part of the documented contract; don't build on their presence. ### Platform API Capability calls are authenticated with your Outlit API key and may count toward your plan's API-call allowance. Plan-limit failures surface through the tool gateway's error envelope (see below) rather than through ingest-style tracking responses. ## Error responses Ingest API errors use the tracking response format: ```json theme={null} { "success": false, "message": "Error description", "errors": [ { "path": ["events", 0, "type"], "message": "Invalid event type" } ] } ``` The tool gateway uses one stable redacted error envelope: ```json theme={null} { "code": "TOOL_CALL_FORBIDDEN", "message": "Capability not authorized", "retryable": false, "requestId": "request_123" } ``` ### HTTP status codes | Code | Description | | - | - | | 200 | Success | | 207 | Some ingest events succeeded; inspect per-event errors | | 400 | Invalid request body or parameters | | 401 | Invalid or missing API key | | 402 | Plan or API-call allowance exceeded, where enforced | | 403 | Invalid public key, unauthorized domain, or plan connection limit | | 429 | Platform limit applied (where enforced) — retry with backoff | | 500 | Internal server error | | 503 | Service unavailable; retry with backoff | ## CORS The Ingest API supports CORS for browser requests. Allowed origins are configured in your tracking settings [**Settings → Website Visitors → Enabled Domains**](https://app.outlit.ai/settings/workspace/website-visitors). ``` Access-Control-Allow-Origin: https://your-domain.com Access-Control-Allow-Methods: POST, OPTIONS Access-Control-Allow-Headers: Content-Type ``` ## Available endpoints Check whether a key can authenticate API requests. Read customer context or perform an authorized action. Set up a provider and check configuration readiness. Send browser or server tracking events. # Call a tool Source: https://docs.outlit.ai/api-reference/tools openapi.json POST /api/tools/call Call customer intelligence tools through the public tool gateway. ## Authentication Pass an Outlit API key in the `Authorization` header as `Bearer ok_...`. ## Request body ```json theme={null} { "tool": "outlit_list_customers", "input": { "limit": 10, "billingStatus": "PAYING" } } ``` `tool` is a public tool name. The table below lists the main tools; [Integration tools](/api-reference/integrations) also documents two compatibility handoff tools. `input` is the tool-specific object, validated against the shared `@outlit/tools` contract before the tool runs. ## Supported tools ### Customers and people | Tool | Purpose | | - | - | | `outlit_list_customers` | Browse and filter customers by billing status, activity recency, revenue, traits, or name | | `outlit_list_users` | Browse and filter users by journey stage, activity recency, customer, traits, email, or name | | `outlit_get_customer` | Get full details for one customer, with optional related sections | | `outlit_get_customer_relationship` | Bounded relationship read model: categorized statements with source labels (no raw fact IDs or quotes) | | `outlit_get_customer_credits` | Read imported credit pools, remaining credits, burn rate, and runway for one customer | ### Identity | Tool | Purpose | | - | - | | `outlit_get_customer_identity` | Read a customer's identity coverage and bounded possible-split records | | `outlit_list_identity_merge_suggestions` | Browse saved identity merge suggestions and history | | `outlit_reject_identity_merge_suggestion` | Reject one saved suggestion by exact ID | | `outlit_merge_customers` | Preview then execute a two-phase customer merge by exact UUIDs | | `outlit_get_customer_merge_status` | Track a merge operation by its operation ID | ### Attention | Tool | Purpose | | - | - | | `outlit_list_attention_items` | List authorized open or resolved attention items (bounded fields, `preparedActionUrl`) | | `outlit_get_attention_item` | Retrieve one attention item by exact UUID | ### Access | Tool | Purpose | | - | - | | `outlit_list_workspace_users` | Browse internal workspace users such as CSMs, managers, account owners, and admins | | `outlit_assign_customer_owner` | Assign an active workspace member as the customer's primary owner | | `outlit_grant_customer_access` | Share a customer with an active workspace member as a Viewer or Editor | | `outlit_update_customer_access` | Change an existing customer collaborator between Viewer and Editor | | `outlit_revoke_customer_access` | Remove a collaborator's explicit access to a customer | ### Timeline and sources | Tool | Purpose | | - | - | | `outlit_get_timeline` | Get chronological customer activity across product, billing, support, and conversation channels | | `outlit_list_facts` | Browse stored customer facts and evidence | | `outlit_get_fact` | Retrieve one exact customer fact | | `outlit_get_source` | Retrieve one exact source record behind a fact or search result | | `outlit_list_sources` | List concrete source records such as calls, emails, calendar events, support tickets, Slack conversations, or opportunities | | `outlit_search_customer_context` | Search customer context semantically | ### Analytics | Tool | Purpose | | - | - | | `outlit_query` | Run SQL against customer intelligence views | | `outlit_schema` | Inspect available analytics views | ### Features | Tool | Purpose | | - | - | | `outlit_list_features` | Read configured Features, exact historical usage, eligible sources, and ready, partial, or unavailable candidate discovery | | `outlit_create_feature` | Create or idempotently return one Feature observed by one event rule | | `outlit_archive_feature` | Archive a Feature using its opaque id and current revision when another active feature remains | | `outlit_get_customer_features` | Read exact historical Feature usage for one authorized customer | ### Configuration | Tool | Purpose | | - | - | | `outlit_list_destinations` | List destinations with masked configuration | | `outlit_get_destination` | Read one destination with masked configuration | | `outlit_create_destination` | Create a delivery destination | | `outlit_update_destination` | Update a destination | | `outlit_enable_destination` | Enable a destination | | `outlit_disable_destination` | Disable a destination | | `outlit_archive_destination` | Archive a destination | | `outlit_get_integration_capabilities` | Check supported setup steps without sending credentials | | `outlit_setup_integration` | Complete a supported credential, mapping, or browser handoff step | | `outlit_get_integration_status` | Read whether an integration is configured and ready | | `outlit_begin_integration_setup` | Start an older browser handoff without sending credentials | | `outlit_get_integration_setup_status` | Check the status of an older authentication handoff | | `outlit_get_customer_activation` | Read the configured activation event | | `outlit_preview_customer_activation` | Preview an event against past activity without changing activation | | `outlit_update_customer_activation` | Set the ordinary activation event | | `outlit_get_workspace_settings` | Read supported workspace settings | | `outlit_update_workspace_settings` | Update a supported workspace setting | ### Feedback | Tool | Purpose | | - | - | | `outlit_submit_feedback` | Submit product feedback — bugs, ideas, or agent friction — to the Outlit team | `outlit_submit_feedback` accepts `body` (required, trimmed, non-empty, at most 20,000 characters), optional `area` (trimmed, non-empty, at most 100 characters), and optional `context` (a JSON object of at most 16 KiB serialized, for diagnostics such as the surface or client version — never secrets or unnecessary customer data). It returns the recorded feedback `id` and `createdAt` timestamp. Submissions require authentication and are rate limited under a dedicated quota; they do not consume paid plan API-call quota. Feedback is product intake, not a support ticket, and does not guarantee a reply. For schema-driven clients, `@outlit/tools` publishes tool input schemas as `publicToolContracts`. ## TypeScript client Use `@outlit/tools` when you want the same tool names and input contracts that power the CLI, Pi package, and public tool gateway: ```bash theme={null} npm install @outlit/tools ``` ```typescript theme={null} import { createOutlitClient, publicToolContracts } from "@outlit/tools" const outlit = createOutlitClient({ apiKey: process.env.OUTLIT_API_KEY!, }) const result = await outlit.callTool( publicToolContracts.outlit_search_customer_context.toolName, { query: "What pricing concerns came up recently?", customer: "acme.com", sourceTypes: ["CALL", "EMAIL", "SUPPORT_TICKET", "OPPORTUNITY"], topK: 5, }, ) ``` The package also exports `publicToolContracts`, `publicToolNames`, server-defined `consumerToolPolicies`, `defaultToolNames`, `analyticalToolNames`, `piToolNames`, `cliToolNames`, `allPublicToolNames`, `publicOpenApiTransports`, `toolGatewayTransport`, `apiKeyValidationTransport`, `toolGatewayErrorCodes`, `toolGatewayErrorSchema`, `ingestTransport`, and enum lists such as `customerSourceTypes`. `OPPORTUNITY` is the canonical CRM opportunity source type; `CRM` and `CRM_OPPORTUNITY` are accepted input aliases. ## Examples ### List paying customers ```bash theme={null} curl -X POST https://app.outlit.ai/api/tools/call \ -H "Authorization: Bearer ok_your_api_key" \ -H "Content-Type: application/json" \ -d '{ "tool": "outlit_list_customers", "input": { "billingStatus": "PAYING", "limit": 10 } }' ``` ### Get customer details ```json theme={null} { "tool": "outlit_get_customer", "input": { "customer": "acme.com", "include": ["users", "revenue", "recentTimeline"], "timeframe": "30d" } } ``` ### Search customer context ```json theme={null} { "tool": "outlit_search_customer_context", "input": { "customer": "acme.com", "query": "What renewal concerns came up recently?", "sourceTypes": ["EMAIL", "CALL", "SUPPORT_TICKET", "OPPORTUNITY"], "after": "2026-01-01T00:00:00Z", "topK": 5 } } ``` Omit `customer` to search across all customers. The top-level `customer` is the customer you scoped to, or `null` for a search across customers. Each `source` and `fact` result also carries a `customer` (`id`, `name`, `domain`) naming the customer it belongs to. A result's `customer` is `null` when it can't be attributed to exactly one customer you can read. Servers that predate the field omit it, so treat a missing `customer` like `null`. ```json theme={null} { "tool": "outlit_search_customer_context", "input": { "query": "Who raised security concerns?", "topK": 5 } } ``` Example response with two source results: ```json theme={null} { "customer": null, "results": [ { "kind": "source", "score": 0.91, "sourceType": "EMAIL", "sourceId": "email_123", "customer": { "id": "cust_123", "name": "Acme", "domain": "acme.com" }, "occurredAt": "2026-08-12T14:05:00Z", "title": "Security review before renewal", "permalink": null, "matchCount": 1, "matches": [{ "text": "Can you send your SOC 2 Type II report before we renew?" }] }, { "kind": "source", "score": 0.87, "sourceType": "CALL", "sourceId": "call_456", "customer": { "id": "cust_789", "name": "Globex", "domain": "globex.com" }, "occurredAt": "2026-08-20T16:00:00Z", "title": "Q3 business review", "permalink": null, "matchCount": 2, "matches": [{ "text": "Their CISO asked about data residency and SSO enforcement." }] } ] } ``` ### List active contact transition facts ```json theme={null} { "tool": "outlit_list_facts", "input": { "customer": "acme.com", "status": ["ACTIVE"], "sourceTypes": ["CALL", "OPPORTUNITY"], "factTypes": ["CONTACT_DEPARTURE", "CONTACT_POSITION_CHANGE", "CONTACT_DISENGAGEMENT"], "after": "2026-01-01T00:00:00Z", "limit": 20 } } ``` ### Open one source record ```json theme={null} { "tool": "outlit_get_source", "input": { "sourceType": "OPPORTUNITY", "sourceId": "opp_123" } } ``` ### Read exact source content Exact lookup can include a `contentPage` with bounded content segments. The default page size is 10 segments, with a maximum of 100. Pass the returned `contentPage.page.nextCursor` with the same source type and ID when `contentPage.page.hasMore` is true. Check `contentPage.completeness` before treating the content as complete. ```json theme={null} { "tool": "outlit_get_source", "input": { "sourceType": "CALL", "sourceId": "call_123", "limit": 10 } } ``` Example response with complete content: ```json theme={null} { "sourceType": "CALL", "sourceId": "call_123", "occurredAt": "2026-09-01T12:00:00Z", "title": "Renewal discussion", "summary": "The customer asked for a renewal proposal.", "permalink": null, "provider": "gong", "customer": { "id": "cust_123", "name": "Acme", "domain": "acme.com" }, "participants": [], "record": {}, "contentPage": { "sourceType": "CALL", "sourceId": "call_123", "contentKind": "call_transcript", "segments": [{ "id": "segment_1", "text": "Please send the renewal proposal." }], "page": { "returned": 1, "hasMore": false, "nextCursor": null }, "completeness": { "status": "complete", "omittedFields": [] } } } ``` ### Follow person profile evidence When a fact or search result references a `PERSON_PROFILE` source, use its source ID for exact lookup. Replace the example ID with the ID from that reference. ```json theme={null} { "tool": "outlit_get_source", "input": { "sourceType": "PERSON_PROFILE", "sourceId": "snapshot_123" } } ``` ### Read customer features ```json theme={null} { "tool": "outlit_get_customer_features", "input": { "customer": "acme.com", "weeks": 12 } } ``` Feature usage is factual historical evidence. A feature can report complete or partial counts, no matches in the requested window, or unavailable source evidence. Unavailable evidence is not converted to zero. Archive is the available self-service lifecycle action. Creating a Feature also creates the supporting weekly event-count and active-days metrics inside Outlit; those metrics are not separate public resources. ### Submit product feedback ```json theme={null} { "tool": "outlit_submit_feedback", "input": { "body": "The merge preview did not list the losing record's traits.", "area": "customers", "context": { "surface": "cli", "command": "customers merge" } } } ``` Returns `{ "id": "10000000-0000-4000-8000-000000000004", "createdAt": "2026-09-28T12:00:00.000Z" }`. See [Feedback](#feedback) for input limits. Feedback is product intake, not a support ticket. ## Response The response is the selected tool's JSON result. List tools return paginated collections; exact lookup tools return the matched record or an error when the record cannot be found. Search returns grouped artifact-level `source` and `fact` results, not raw vector chunks. Fact results include provenance fields such as `sourceType`, `sourceId`, `sourceOccurredAt`, `sourceQuote`, and `permalink` when available. Money amounts such as `currentMrr`, `lifetimeRevenue`, and `accountImportance.arrCents` are in the billing provider's units for the currency they were billed in. For USD, EUR, GBP, and CAD these are cents: divide by 100 (for example, 1234500 is $12,345 in USD). Other currencies follow the provider's amount rules (see [Stripe's currency docs](https://docs.stripe.com/currencies)), so don't assume a divisor for them. Attention items return the currency alongside `arrCents`; customer results don't name it. ARR is 12 × `currentMrr` in the same units. The example below is for a customer billed in USD, so `"currentMrr": 25000` is $250 MRR. Example list response: ```json theme={null} { "items": [ { "id": "cus_123", "name": "Acme", "domain": "acme.com", "billingStatus": "PAYING", "activatedAt": "2026-01-15T09:30:00.000Z", "firstSeenAt": "2025-12-02T17:05:00.000Z", "lastActivityAt": "2026-04-10T18:22:11.000Z", "contactCount": 42, "currentMrr": 25000, "mrrCalculationStatus": "calculated", "owner": null, "daysSinceActivity": 3 } ], "pagination": { "hasMore": true, "nextCursor": "cursor_abc", "total": 128 } } ``` ## Error responses Invalid JSON, unknown tool names, or invalid tool inputs return `400`: ```json theme={null} { "code": "INVALID_TOOL_INPUT", "message": "Invalid tool input", "retryable": false, "requestId": "req_123" } ``` Invalid credentials return `401`. Plan API-call limits can return `402` or `429` with a stable billing `code`. # Validate an API key Source: https://docs.outlit.ai/api-reference/validation openapi.json POST /api/validate-api-key Verify that an Outlit API key can authenticate Platform API requests. ## Authentication Pass an Outlit API key in the `Authorization` header as `Bearer ok_...`. ## Request body No request body is required when validating the API key from the `Authorization` header. ## Example ```bash theme={null} curl -X POST https://app.outlit.ai/api/validate-api-key \ -H "Authorization: Bearer ok_your_api_key" ``` ## Success response ```json theme={null} { "valid": true, "organizationId": "org_123", "createdById": "user_123", "authorization": { "grants": ["customer_intelligence:read"] } } ``` `authorization.grants` lists the key's current capabilities. Only offer commands whose required authority appears in this list. `createdById` is null when the creator is unavailable. ## Error responses Invalid or disabled keys return `401`: ```json theme={null} { "valid": false, "error": "Invalid credentials" } ``` If the credential store is temporarily unavailable, the endpoint returns `503` with the same failure envelope. Retry that response; it does not mean the key is invalid or revoked. # Set up coding agents Source: https://docs.outlit.ai/cli/ai-agents Install Outlit skills for Claude Code, Codex, Gemini CLI, Droid, OpenCode, Pi, OpenClaw, and other coding agents. The `outlit onboard` command is the first command for coding agents. It bootstraps CLI auth, installs the `outlit` skill for the selected agent, checks integration setup readiness, and prints next actions. `outlit setup` remains available when you only want to install skills. It does not configure MCP clients. If you want Cursor, VS Code, Claude, or another MCP client, use the direct [MCP integration](/ai-integrations/mcp) guide instead. ## Supported agents Run `onboard` for the agent you use, or run `outlit setup --yes` to detect installed agents automatically when you only need skills. | Agent | Onboard command | | - | - | | Claude Code | `outlit onboard --agent claude-code --json` | | Codex | `outlit onboard --agent codex --json` | | Gemini CLI | `outlit onboard --agent gemini --json` | | Droid | `outlit onboard --agent droid --json` | | OpenCode | `outlit onboard --agent opencode --json` | | Pi | `outlit onboard --agent pi --json` | | OpenClaw | `outlit onboard --agent openclaw --json` | | Manual / multi-agent | `outlit setup skills` | ## Onboarding ```bash theme={null} outlit onboard --agent codex --json ``` If no API key is available, `onboard` starts the browser approval flow outside CI and stores the approved CLI key. It then validates the key, installs the Outlit skill, reads canonical integration status, and returns next commands such as `outlit integrations setup --json`. `onboard` does not connect third-party integrations. Run `outlit integrations setup --json` and check `outlit integrations status --json` afterward. See [Integration setup (CLI)](/cli/integrations) for credentials, browser handoffs, CRM mapping, and headless mode. ## Automatic setup Detect installed coding agents and install `outlit` for all of them: ```bash theme={null} outlit setup --yes ``` Without `--yes`, the CLI lists detected agents before installing. ### JSON output ```json theme={null} { "detected": ["claude-code", "opencode"], "configured": ["claude-code", "opencode"], "failed": [], "runner": "npx" } ``` ## Interactive skills installer Use this when you want to pick agents manually or optionally add `outlit-sdk`: ```bash theme={null} outlit setup skills ``` This opens the interactive `skills add` flow scoped to `github.com/OutlitAI/outlit-agent-skills`. ## Diagnostics The `doctor` command checks your CLI auth plus coding-agent skill installs: ```bash theme={null} outlit doctor ``` ### Checks performed | Check | What it verifies | | - | - | | CLI version | Whether you're running the latest version | | API key | Presence and format of your API key | | API validation | Live test call to verify the key works | | Integrations | Whether Outlit can read integration status | | Per-agent checks | Whether detected coding agents already have the `outlit` skill | ### Example output ``` Outlit Doctor ✓ CLI version: v1.4.1 (latest) ✓ API key: Found (ok_ab...1234) via config ✓ API validation: Key is valid ! Claude Code: Installed, but Outlit skill not found ✓ Codex: Outlit skill installed Everything works, 1 suggestion(s). ``` ### JSON output ```bash theme={null} outlit doctor --json ``` ```json theme={null} { "ok": true, "checks": [ { "name": "CLI version", "status": "pass", "message": "v1.4.1 (latest)" }, { "name": "API key", "status": "pass", "message": "Found (ok_ab...1234) via config" }, { "name": "API validation", "status": "pass", "message": "Key is valid" }, { "name": "Claude Code", "status": "warn", "message": "Installed, but Outlit skill not found" } ] } ``` Filter for failures: ```bash theme={null} outlit doctor --json | jq '.checks[] | select(.status == "fail")' ``` ### Exit code `doctor` exits with code `1` if any check fails, which makes it useful in CI: ```bash theme={null} outlit doctor || echo "Setup issues detected" ``` # Command reference Source: https://docs.outlit.ai/cli/commands Complete reference for all Outlit CLI data commands. Most commands in this reference share these common flags: | Flag | Description | | - | - | | `--api-key ` | Override the active API key for this request | | `--json` | Force JSON output (auto-enabled when piped) | | `--help` | Show help for the command | ## Activation Inspect, preview against past events, and explicitly configure the exact ordinary product event Outlit uses for activation. The first matching event activates eligible contacts and their resolved company. Outlit records activation for each eligible person and resolved company only once. Later matching events leave those timestamps unchanged. Applications and connected analytics sources keep sending ordinary product events. They do not send a synthetic activation event or change lifecycle state directly. ### Get ```bash theme={null} outlit activation get --json ``` The command returns the configured `eventName`. ### Preview Preview one exact event name against historical product events: ```bash theme={null} outlit activation preview \ --event integration_connected \ --lookback-days 30 \ --example-limit 10 \ --json ``` Preview is always read-only. It never saves the event setting, changes `activatedAt`, or records activation. The response reports separate contact and company totals: matching people and companies, those already activated, and those that would activate if this event were configured. `evaluatedContactOccurrenceCount` counts matching contact-event occurrences, while `evaluatedEventCount` counts matching events attributed to resolved companies. Contact identities are not returned as preview examples. Both sides are bounded independently for query safety. If `contactTruncated` is `true`, treat contact totals as lower bounds. If `customerTruncated` is `true`, treat company totals as lower bounds. The legacy `truncated` flag can also mean that `exampleLimit` omitted company examples, so it does not by itself make aggregate totals a lower bound. | Flag | Required | Description | | - | - | - | | `--event` | Yes | Exact ordinary product event name, from 1–191 characters | | `--lookback-days` | No | Historical lookback from 1–90 (server default: 30) | | `--example-limit` | No | Customer examples from 1–20 (server default: 10) | ### Update After reviewing a preview, explicitly replace the configured event name: ```bash theme={null} outlit activation update \ --event integration_connected \ --json ``` Saving the event name changes future matching only. It does not scan historical events or backfill activation. ### Disable To stop future activation matching: ```bash theme={null} outlit activation disable --json ``` Disabling preserves existing contact and company activation timestamps. All four commands return the public tool result in `--json` and piped output. Local validation errors use the CLI's `missing_input` or `invalid_input` shape; gateway failures retain Outlit's stable `code`, `message`, `retryable`, and `requestId` envelope. ## Customers list List and filter customers with pagination. ```text theme={null} outlit customers list [flags] ``` ### Flags | Flag | Type | Default | Description | | - | - | - | - | | `--billing-status` | string | — | Filter by billing status: `NONE`, `TRIALING`, `PAYING`, `PAST_DUE`, `CHURNED` | | `--mrr-above` | number | — | MRR floor, in `currentMrr`'s units: cents for USD, EUR, GBP, and CAD (e.g. `10000` for \$100/mo in USD) | | `--mrr-below` | number | — | MRR ceiling, in the same units as `--mrr-above` | | `--no-activity-in` | string | — | Inactive for period: `7d`, `14d`, `30d`, `90d` | | `--has-activity-in` | string | — | Active within period: `7d`, `14d`, `30d`, `90d` | | `--trait` | string | — | Exact trait filters as comma-separated `key=value` pairs (for example `segment=enterprise,active=true`) | | `--activated-since` | string | — | Company activation at or after an ISO-8601 datetime; explicit offsets are accepted | | `--search` | string | — | Search by name or domain | | `--order-by` | string | `last_activity_at` | Sort field | | `--order-direction` | string | `desc` | Sort direction: `asc`, `desc` | | `--limit` | number | `20` | Results per page (1–100) | | `--cursor` | string | — | Pagination cursor from a previous response | ### Examples ```bash theme={null} # Paying customers inactive for 30 days outlit customers list --billing-status PAYING --no-activity-in 30d # High-MRR customers sorted by revenue outlit customers list --mrr-above 50000 --order-by mrr_cents --order-direction desc # Search for a customer outlit customers list --search "acme" # Filter by exact customer traits outlit customers list --trait "segment=enterprise,active=true" # Companies activated since July 1 outlit customers list --activated-since 2026-07-01T00:00:00Z # Pipe to jq outlit customers list --json | jq '.items[] | {domain, activatedAt}' ``` ### JSON response ```json theme={null} { "items": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Acme Corp", "domain": "acme.com", "billingStatus": "PAYING", "currentMrr": 500000, "mrrCalculationStatus": "calculated", "contactCount": 42, "firstSeenAt": "2025-01-02T10:00:00Z", "lastActivityAt": "2025-02-10T15:30:00Z", "activatedAt": "2025-01-20T09:15:00Z", "owner": null, "daysSinceActivity": 3 } ], "pagination": { "hasMore": true, "nextCursor": "cursor_abc123", "total": 150 } } ``` `currentMrr` is an integer in the billing provider's units for the customer's currency, which the response doesn't name. For USD, EUR, GBP, and CAD this is cents: `500000` is \$5,000 MRR for a customer billed in USD. Other currencies follow the provider's amount rules (see [Stripe's currency docs](https://docs.stripe.com/currencies)). ARR is 12 × `currentMrr` in the same units. `currentMrr` is `null` unless `mrrCalculationStatus` is `calculated`. ### Pagination Use `--cursor` to page through results: ```bash theme={null} # First page outlit customers list --limit 50 --json > page1.json # Next page CURSOR=$(jq -r '.pagination.nextCursor' page1.json) outlit customers list --limit 50 --cursor "$CURSOR" --json ``` In interactive mode, the CLI shows a hint after the table: ``` Showing 20 of 150 total. Next page: --cursor cursor_abc123 ``` ## Customers get Get a detailed customer profile with optional sections. ```text theme={null} outlit customers get [flags] ``` ### Arguments | Argument | Required | Description | | - | - | - | | `customer` | Yes | Customer domain (e.g. `acme.com`), UUID, or name | ### Flags | Flag | Type | Default | Description | | - | - | - | - | | `--include` | string | — | Comma-separated sections: `users`, `revenue`, `recentTimeline`, `behaviorMetrics`, `enrichment` | | `--timeframe` | string | `30d` | Lookback window for included sections: `7d`, `14d`, `30d`, `90d` | ### Examples ```bash theme={null} # Basic profile outlit customers get acme.com # Profile with people, revenue, and recent activity outlit customers get acme.com --include users,revenue,recentTimeline --timeframe 90d # By UUID outlit customers get 550e8400-e29b-41d4-a716-446655440000 ``` ### JSON response ```json theme={null} { "customer": { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Acme Corp", "domain": "acme.com", "billingStatus": "PAYING", "activatedAt": "2025-01-20T09:15:00Z", "firstSeenAt": "2025-01-02T10:00:00Z", "lastActivityAt": "2025-02-10T15:30:00Z", "owner": null }, "users": [ { "id": "user-uuid", "name": "Jane Doe", "email": "jane@acme.com", "journeyStage": "ENGAGED", "lastActivityAt": "2025-02-10T15:30:00Z", "relationshipRole": "Champion", "jobTitle": "VP of Product", "department": "Product", "employmentStartDate": "2023-04-10" } ], "revenue": { "currentMrr": 500000, "mrrCalculationStatus": "calculated", "lifetimeRevenue": 2400000, "activeSubscriptions": 1 }, "recentTimeline": [ { "type": "PAGE_VIEW", "title": "Pricing page viewed", "contact": "jane@acme.com", "occurredAt": "2025-02-10T15:30:00Z" } ] } ``` `revenue.currentMrr` and `revenue.lifetimeRevenue` are in the billing provider's units, like `currentMrr` in `customers list`: for a customer billed in USD, `500000` is $5,000 MRR and `2400000` is $24,000 of lifetime revenue. `customer.activatedAt` is an ISO-8601 timestamp when Outlit has recorded company activation, and `null` otherwise. Contact journey stages remain separate under `users`. ## Customers relationship Get the relationship summary used for customer detail, without adding it to the `customers get` response or replacing the chronological activity timeline. ```text theme={null} outlit customers relationship ``` `` accepts a customer domain, UUID, or exact name. The relationship returns the stable customer identity and a bounded `relationship` section (summary plus up to eight categorized current statements). Each statement carries an ISO `observedAt` timestamp when the authorized source supports one and bounded `sourceLabels`. `updatedAt` is the compiled summary timestamp when available, and otherwise `null`. The response never includes raw quotes, internal memory-card data, unrestricted fact IDs, or unbounded source records. ```bash theme={null} outlit customers relationship acme.com --json ``` ### JSON response ```json theme={null} { "customer": { "id": "550e8400-e29b-41d4-a716-446655440000", "name": "Acme Corp", "domain": "acme.com" }, "relationship": { "summary": "Renewal needs attention", "items": [ { "category": "Renewal", "statement": "The renewal is under active review.", "observedAt": "2026-08-01T10:30:00.000Z", "sourceLabels": ["CRM opportunity"] } ], "updatedAt": "2026-08-13T00:00:00.000Z" } } ``` ## Customer identity and merging Inspect a customer's identity coverage and possible split records using its exact customer ID: ```bash theme={null} outlit customers identity "" --json ``` Candidate records are leads, not proof of common identity. An empty or incomplete search does not prove the account has zero usage. This read never creates a merge suggestion or combines customer activity. Saved suggestions and review history are a separate read. List returns complete details; use `--suggestion-id` to retrieve one exact record in the same collection response, or `--customer-id` to scope the list. Follow `nextCursor` using `--cursor`. ```bash theme={null} outlit identity suggestions list --customer-id "" --limit 20 --json outlit identity suggestions list --suggestion-id "" --json outlit identity suggestions reject "" --review-notes "Different companies" --json ``` List also accepts `--status suggested|processing|merged|rejected` and `--confidence HIGH|MEDIUM|LOW`. Rejection requires identity review authority and does not authorize merging. It returns only `{ "suggestionId": "…", "status": "REJECTED" }`; read the suggestion list again for updated details. The merge command previews by default. Both positional arguments are exact IDs; the first is the surviving record. Without `--suggestion-id`, both records must be `COMPANY` customers. A pair involving an `INDIVIDUAL` requires an eligible saved suggestion for that exact pair. An optional `--suggestion-id` links an existing suggestion to the exact pair, and `--review-notes` records the review explanation. ```bash theme={null} outlit customers merge "" "" --json ``` Merge execution is dangerous and has no supported undo. It moves customer data and access relationships into the survivor. Execute only when authorized and certain that the records represent the same customer; two company records must represent the same company. Similar names, related domains, shared participants, and parent/subsidiary relationships do not establish that identity. If uncertain, do not execute. Execution requires an explicit `--execute`, the reviewed preview's token, a stable request ID, and dedicated merge authority. A read-only credential can preview but cannot execute. A merge API key must include an explicitly selected `customer_identity:merge` grant; ordinary full-access and personal CLI presets exclude it. Use the same pair, token, request ID, and review inputs when retrying an execution. ```bash theme={null} outlit customers merge "" "" --execute --preview-token "" --request-id "" --json outlit customers merge-status "" --json ``` A saved suggestion's `latestJob.operationId`, when present, can be passed to `customers merge-status`. A null value means that job has no public operation ID attached. A queued or running merge has not finished. Reading status does not run the merge again, including after a partial failure. The workspace must support these commands, and the API key needs the required permission. ## Customers credits Read imported credit pools and burn summaries for one authorized customer. ```text theme={null} outlit customers credits ``` `` accepts a customer domain, UUID, or exact name. The command returns the same JSON schema as `outlit_get_customer_credits`: the resolved customer, overall availability status, credit pools, and a `truncated` marker. Each credit pool keeps its provider source, observation time, staleness, granted and remaining amounts, reset date, and burn summary. Missing or stale provider observations remain distinct from a zero balance. ```bash theme={null} outlit customers credits acme.com --json ``` ## Customers features Read event-based and imported metered Feature usage for one authorized customer. ```text theme={null} outlit customers features [--weeks 1-53] [--weekly] ``` `` accepts a customer domain, UUID, or exact name. The default window is 12 weeks. The response is a flat `features` array. Every item has a `kind` discriminator. An `event` item reports historical evidence from its configured Outlit event rule. A `metered` item reports provider-defined quantities imported from Autumn and may link to an event Feature. These values use different units and must not be added together. The response also lists source status so a missing provider connection, unavailable source, and an available source with no matches remain distinct. Top-level `events` is `available` or `unavailable`; if the event source cannot be read, imported metered Features can still be returned. Complete, partial, and unavailable evidence remain distinct; unavailable evidence is never rendered as zero usage. Add `--weekly` to include ordered weekly buckets. Event Features report `eventCount` and `activeDays`. Metered Features report their provider quantity and observed days. ```bash theme={null} outlit customers features acme.com --json outlit customers features acme.com --weeks 6 --json outlit customers features acme.com --weeks 53 --weekly --json ``` ## Features Configure the small workspace taxonomy of product capabilities whose recurring use represents customer value. ```text theme={null} outlit features list [flags] outlit features create [flags] outlit features archive --revision ``` `features list` returns eligible product-event sources, the selected source, active features with exact historical evidence, and candidate discovery. Candidate discovery is a discriminated result: `ready` and `partial` contain `items` and `truncated`, while `unavailable` contains a reason. Outlit auto-selects a source only when exactly one is eligible. | Workspace flag | Default | Description | | - | - | - | | `--source` | auto-select when unambiguous | Exact source key returned by the workspace command | | `--weeks` | `12` | Historical evidence window from 1 to 53 weeks | | `--candidate-limit` | `100` | Maximum event candidates from 1 to 100 | Create one feature from one exact event rule: ```bash theme={null} outlit features create \ --source metric_source_v1_0123456789abcdef0123456789abcdef \ --event report_exported \ --key reports_exported \ --name "Reports exported" \ --json ``` Use `--property-filters` with a JSON array of generated exact or exists filter objects when the event alone is too broad. Event names are preserved exactly; use the candidate returned by the workspace command. Archive requires both opaque values returned by Outlit: ```bash theme={null} outlit features archive \ value_feature_v1_0123456789abcdef0123456789abcdef \ --revision value_feature_revision_v1_fedcba9876543210fedcba9876543210 \ --json ``` Creating a Feature atomically creates its internal weekly event-count and active-days metrics. Those metrics are implementation details rather than separate CLI resources. Outlit rejects stale revisions and prevents archiving the final active Feature. Historical usage remains available. Archive is the available self-service lifecycle action. ## Attention Inspect the authorized, read-only Attention collection. Attention is separate from customer relationship and from the chronological customer timeline. ```text theme={null} outlit attention list [flags] outlit attention get [flags] ``` `attention list` defaults to open items. It accepts optional `--customer-id`, `--limit` (1–100; server default 25), and an opaque `--cursor` returned with the same filters. Use `--status resolved` for resolved items. Each returned item includes its current priority; the command does not filter by priority. ```bash theme={null} outlit attention list --status resolved --customer-id 550e8400-e29b-41d4-a716-446655440000 --json outlit attention get 550e8400-e29b-41d4-a716-446655440000 --json ``` List rows contain bounded customer identity, current priority, evidence count, lifecycle times, and account importance. Detail adds the current assessment, timeline, evidence summaries, and latest visible update. `accountImportance.arrCents` is calculated by Outlit as 12 × the stored monthly revenue; clients must not multiply it again. It is in the billing provider's units for `accountImportance.currency`: cents for USD, EUR, GBP, and CAD (`1200000` is \$12,000 ARR in USD), and other currencies follow the provider's amount rules. `arrShareWithinCurrency` and `arrPercentileWithinCurrency` compare only customers in the same currency; neither implies FX conversion or an all-currency organization total. Both responses may contain a current reviewable `preparedActionUrl` only. They never include action IDs, action lifecycle or execution state, email drafts, recipients, evidence IDs, source URLs, internal responsibility-agent configuration, or billing-recovery details. The commands are read-only: there is no resolve, reopen, send, or review operation. ### JSON response ```json theme={null} { "items": [ { "id": "550e8400-e29b-41d4-a716-446655440000", "customer": { "id": "4ad7ee3b-a3ff-46a8-902b-2b8eae6be671", "name": "Acme Corp", "domain": "acme.com", "ownerName": "Ada Lovelace" }, "title": "Reporting usage collapsed", "priority": "HIGH", "evidenceCount": 2, "lifecycle": { "status": "open", "customerVisibleAt": "2026-08-01T00:00:00.000Z", "resolvedAt": null, "reopenedAt": null, "createdAt": "2026-08-01T00:00:00.000Z", "updatedAt": "2026-08-02T00:00:00.000Z", "materialUpdateAt": null }, "accountImportance": { "arrCents": 1200000, "currency": "USD", "arrShareWithinCurrency": 0.12, "arrPercentileWithinCurrency": 95, "segments": ["Enterprise"] }, "preparedActionUrl": null } ], "pagination": { "hasMore": false, "nextCursor": null, "total": 1 } } ``` ## Customer ownership and access Assign a primary owner or manage a workspace member's explicit Viewer or Editor access. These write commands require exact IDs and never resolve customer names, domains, or user emails on your behalf. For API keys, workspace-member discovery and customer mutations are independent capabilities. `outlit ws-users list` requires `workspace_members:read`; setting an owner, granting or changing access, and revoking access require `customer_access:manage`. The complete discover-then-mutate flow needs both grants, while scripts and agents that already know exact IDs may use `customer_access:manage` alone. API keys are independent workspace principals and do not inherit their creator's human permissions or customer access. **Read only** includes discovery but not mutation, **Full workspace access** includes both grants, and **Custom** includes only the grants you select; there is no separate customer-collaboration preset. First discover the customer UUID. Use JSON output because the interactive table is optimized for scanning rather than copying identifiers: ```bash theme={null} outlit customers list --search "acme" --json ``` Copy the exact `items[].id` for the intended customer. Then discover the target workspace user: ```bash theme={null} outlit ws-users list --search "alex@example.com" --json ``` Without `--has-owned-customers`, this command lists all active workspace members that resolve to local Outlit users, including members who own no customers. Use `--has-owned-customers` only when you intentionally want to restrict discovery to current owners. Copy the exact workspace-user `items[].id` as `--target-user-id`. Review multiple matches before writing; the ownership and access commands intentionally do not perform fuzzy name matching. ### Set owner ```bash theme={null} outlit customers owner set \ 10000000-0000-4000-8000-000000000000 \ --target-user-id user_123 \ --json ``` The target must be an active workspace member. When ownership changes, the former owner keeps Editor access. ### Grant or change access ```bash theme={null} outlit customers grant \ 10000000-0000-4000-8000-000000000000 \ --target-user-id user_123 \ --role VIEWER \ --json ``` Run the same `grant` command again with the other role to change an existing collaborator between Viewer and Editor. ### Revoke access ```bash theme={null} outlit customers revoke \ 10000000-0000-4000-8000-000000000000 \ --target-user-id user_123 \ --json ``` `--role` accepts only `VIEWER` or `EDITOR`; `OWNER` is represented by the separate primary-owner assignment. Malformed customer IDs, missing or oversized target-user IDs, and invalid roles fail locally without sending a request. Permission, conflict, and other gateway failures retain Outlit's structured `code`, `message`, `retryable`, and `requestId` fields in JSON output. ## Customers timeline View the activity timeline for a customer, filtered by channel and event type. ```text theme={null} outlit customers timeline [flags] ``` ### Arguments | Argument | Required | Description | | - | - | - | | `customer` | Yes | Customer domain or UUID | ### Flags | Flag | Type | Default | Description | | - | - | - | - | | `--timeframe` | string | `30d` | Lookback window: `7d`, `14d`, `30d`, `90d`, `all` | | `--start-date` | string | — | Start datetime (ISO 8601, for example `2025-01-01T00:00:00Z`) — overrides `--timeframe` | | `--end-date` | string | — | End datetime (ISO 8601, for example `2025-03-01T23:59:59.999Z`) — overrides `--timeframe` | | `--channels` | string | — | Comma-separated canonical channels: `PRODUCT`, `COMMUNICATION`, `MEETING`, `CRM`, `BILLING`, `SUPPORT`, `IDENTITY`, `DOCUMENT`, `SYSTEM` | | `--event-types` | string | — | Comma-separated types: `PAGE_VIEW`, `MEETING`, `SUPPORT_TICKET`, etc. | | `--limit` | number | `20` | Results per page (1–100) | | `--cursor` | string | — | Pagination cursor | When `--start-date` or `--end-date` is provided, `--timeframe` is ignored. The CLI also accepts and normalizes these legacy aliases: `SDK` → `PRODUCT`, `EMAIL` → `COMMUNICATION`, `SLACK` → `COMMUNICATION`, `CALL` → `MEETING`, `CALENDAR` → `MEETING`, and `INTERNAL` → `SYSTEM`. ### Examples ```bash theme={null} # Last 30 days (default) outlit customers timeline acme.com # Last 90 days, communication and meeting activity outlit customers timeline acme.com --timeframe 90d --channels COMMUNICATION,MEETING # Custom date range outlit customers timeline acme.com --start-date 2025-01-01T00:00:00Z --end-date 2025-03-01T23:59:59.999Z # Page views and meetings only outlit customers timeline acme.com --event-types PAGE_VIEW,MEETING --limit 50 ``` ### JSON response ```json theme={null} { "events": [ { "id": "event-uuid", "eventType": "PAGE_VIEW", "eventName": "pricing_page_viewed", "channel": "PRODUCT", "title": "Pricing page viewed", "summary": "Jane viewed the pricing page", "occurredAt": "2025-02-10T15:30:00Z", "contact": "jane@acme.com", "sourceType": null, "sourceId": null, "metadata": { "url": "https://acme.com/pricing", "title": "Pricing Page" } } ], "pagination": { "hasMore": false, "nextCursor": null, "effectiveLimit": 20 } } ``` Older CLI and tool versions may omit `pagination.effectiveLimit`, per-event `unreadable`/`unreadableReason`, and the `product` weekly usage aggregate (`channels:"PRODUCT"`). Check for their presence before using them. ## Users list List and filter users across your customer base. ```text theme={null} outlit users list [flags] ``` ### Flags | Flag | Type | Default | Description | | - | - | - | - | | `--journey-stage` | string | — | Filter by stage: `DISCOVERED`, `SIGNED_UP`, `ACTIVATED`, `ENGAGED`, `INACTIVE` | | `--customer-id` | string | — | Filter to users of a specific customer (UUID) | | `--no-activity-in` | string | — | Inactive for period: `7d`, `14d`, `30d`, `90d` | | `--has-activity-in` | string | — | Active within period: `7d`, `14d`, `30d`, `90d` | | `--trait` | string | — | Exact trait filters as comma-separated `key=value` pairs (for example `role=admin,onboarded=true`) | | `--search` | string | — | Search by name or email | | `--order-by` | string | `last_activity_at` | Sort field | | `--order-direction` | string | `desc` | Sort direction: `asc`, `desc` | | `--limit` | number | `20` | Results per page (1–100) | | `--cursor` | string | — | Pagination cursor | ### Examples ```bash theme={null} # Find engaged users outlit users list --journey-stage ENGAGED # Inactive users from a specific customer outlit users list --customer-id 550e8400-... --no-activity-in 30d # Filter by exact user traits outlit users list --trait "role=admin,onboarded=true" # Search by name outlit users list --search "alice" --order-by last_activity_at ``` ### JSON response ```json theme={null} { "items": [ { "id": "user-uuid", "email": "jane@acme.com", "name": "Jane Doe", "customerId": "customer-uuid", "customerName": "Acme Corp", "customerDomain": "acme.com", "journeyStage": "ENGAGED", "lastActivityAt": "2025-02-10T15:30:00Z", "daysSinceActivity": 3, "relationshipRole": "Champion", "jobTitle": "VP of Product", "department": "Product", "employmentStartDate": "2023-04-10" } ], "pagination": { "hasMore": true, "nextCursor": "cursor_abc", "total": 500 } } ``` ## Facts Retrieve AI-generated facts and insights for a customer. ```text theme={null} outlit facts list [flags] outlit facts get --fact-id [flags] ``` ### `facts list` arguments | Argument | Required | Description | | - | - | - | | `customer` | Yes | Customer domain or UUID | ### `facts list` flags | Flag | Type | Default | Description | | - | - | - | - | | `--status` | string | — | Comma-separated fact status filter: `ACTIVE`, `ACKNOWLEDGED`, `RESOLVED`, `SNOOZED`, `CANDIDATE` | | `--source-types` | string | — | Comma-separated generic source type filter: `EMAIL`, `CALL`, `CALENDAR_EVENT`, `SUPPORT_TICKET`, `OPPORTUNITY`, `SLACK`, `PERSON_PROFILE`. Case-insensitive. Aliases: `CRM`, `CRM_OPPORTUNITY` | | `--fact-types` | string | — | Comma-separated public fact type filter, including `CHURN_RISK`, `PRODUCT_USAGE`, `CONTACT_DEPARTURE`, `CONTACT_POSITION_CHANGE`, and `CONTACT_DISENGAGEMENT` | | `--fact-categories` | string | — | Comma-separated fact category filter: `MEMORY`, `RELATIONSHIP`, `CUSTOM` | | `--after` | string | — | Lower bound on fact occurrence time (ISO 8601 datetime) | | `--before` | string | — | Upper bound on fact occurrence time (ISO 8601 datetime) | | `--limit` | number | `20` | Results per page (1–100) | | `--cursor` | string | — | Pagination cursor | ### `facts get` flags | Flag | Type | Default | Description | | - | - | - | - | | `--fact-id` | string | — | Exact fact identifier | | `--include` | string | — | Comma-separated best-effort expansions. `evidence` requests evidence details when available | ### Examples ```bash theme={null} # All facts for a customer outlit facts list acme.com # Filter by status and source type outlit facts list acme.com --status ACTIVE --source-types CALL,OPPORTUNITY --after 2025-01-01T00:00:00Z # Filter customer-memory facts by public fact type outlit facts list acme.com --fact-types CHURN_RISK,PRODUCT_USAGE # Pipe for processing outlit facts list acme.com --json | jq '.facts[] | select(.severity == "HIGH")' # Fetch one exact fact with best-effort evidence expansion outlit facts get --fact-id fact_123 --include evidence ``` ### JSON response ```json theme={null} { "customer": { "id": "cust_123", "name": "Acme", "domain": "acme.com" }, "facts": [ { "id": "fact-uuid", "assertion": "Budget concerns raised", "reasoning": "The customer explicitly deferred the purchase because of budget timing.", "factType": "BUDGET", "factCategory": "MEMORY", "status": "ACTIVE", "confidence": 0.92, "priority": "HIGH", "severity": "HIGH", "occurredAt": "2025-02-10T14:00:00Z", "detectedAt": "2025-02-10T15:30:00Z", "validFrom": "2025-02-10T14:00:00Z", "validUntil": null, "linkedSourceRef": { "sourceType": "CALL", "sourceId": "call_123", "occurredAt": "2025-02-10T14:00:00Z", "permalink": "https://granola.ai/notes/call_123" }, "sourceMetadata": { "sourceType": "CALL", "sourceId": "call_123", "sourceOccurredAt": "2025-02-10T14:00:00Z", "sourceQuote": "Budget is blocked until next quarter.", "permalink": "https://granola.ai/notes/call_123" }, "evidenceCount": 1, "metricName": null, "metricValue": null, "metricPreviousValue": null, "metricDelta": null, "metricUnit": null, "periodStart": null, "periodEnd": null, "changePointDate": null, "occurrenceCount": 1, "lastDetectedAt": "2025-02-10T15:30:00Z" } ], "pagination": { "hasMore": false, "nextCursor": null } } ``` `facts get` returns the same canonical fact shape as `facts list`. Passing `--include evidence` requests a best-effort evidence expansion; unsupported include values are ignored. ## Search Semantic search across customer context. Results are grouped at the artifact level, so matching sources and facts are returned directly instead of raw chunks. ```text theme={null} outlit search [flags] ``` ### Arguments | Argument | Required | Description | | - | - | - | | `query` | Yes | Natural language search query | ### Flags | Flag | Type | Default | Description | | - | - | - | - | | `--customer` | string | — | Scope to a specific customer (domain or UUID) | | `--top-k` | number | `20` | Maximum results to return (1–50) | | `--after` | string | — | Filter to events after this ISO 8601 datetime | | `--before` | string | — | Filter to events before this ISO 8601 datetime | | `--source-types` | string | — | Comma-separated generic source type filter (`EMAIL`, `CALL`, `CALENDAR_EVENT`, `SUPPORT_TICKET`, `OPPORTUNITY`, `SLACK`, `PERSON_PROFILE`). Case-insensitive. Aliases: `CRM`, `CRM_OPPORTUNITY` | ### Examples ```bash theme={null} # Organization-wide search outlit search "pricing objections" # Scoped to a customer outlit search "budget concerns" --customer acme.com # Time-bounded outlit search "expansion opportunities" --after 2025-01-01T00:00:00Z --before 2025-03-31T23:59:59Z # Filter by source type outlit search "renewal opportunities" --source-types CALL,OPPORTUNITY # More results (max 50) outlit search "churn risk" --top-k 50 --json ``` ### JSON response ```json theme={null} { "customer": { "id": "cust_123", "name": "Acme", "domain": "acme.com" }, "results": [ { "kind": "source", "score": 0.92, "matchCount": 2, "sourceType": "CALL", "sourceId": "call_456", "customer": { "id": "cust_123", "name": "Acme", "domain": "acme.com" }, "occurredAt": "2025-02-10T15:30:00Z", "title": "Q1 planning meeting", "permalink": "https://granola.ai/notes/call_456", "matches": [ { "text": "Customer mentioned pricing concerns in Q1 planning meeting" } ] } ] } ``` Search returns both matching `source` and `fact` artifacts when both are relevant. Multiple chunk hits from the same source are grouped into one source result. The top-level `customer` is the `--customer` you scoped to, or `null` for an organization-wide search. Each result's `customer` names the customer it belongs to, so an organization-wide search tells you whose call or fact matched. It is `null` when the result can't be attributed to exactly one customer you can read, and older servers omit it; treat a missing `customer` like `null`. ## Sources List concrete source records deterministically or fetch one exact source by generic `sourceType` and `sourceId`. ### List sources List enumerated calls, emails, calendar events, support tickets, Slack conversations, or opportunities. Use this command when you need deterministic source records; use [`outlit search`](#search) for semantic ranking. Results are paginated. ```text theme={null} outlit sources list [flags] ``` ### Flags | Flag | Type | Default | Description | | - | - | - | - | | `--source-type` | string | — | Generic source type: `EMAIL`, `CALL`, `CALENDAR_EVENT`, `SUPPORT_TICKET`, `OPPORTUNITY`, `SLACK`, `PERSON_PROFILE`. Case-insensitive. Aliases: `CRM`, `CRM_OPPORTUNITY` | | `--customer` | string | — | Customer UUID, domain, or name to scope source listing | | `--participant` | string | — | Participant email or name to filter source records | | `--provider` | string | — | Provider identifier such as `gmail`, `gong`, or `google-calendar` | | `--has-transcript` | boolean | `false` | Only list call sources with a finalized transcript; when combined with `--source-type`, that type must be `CALL` | | `--after` | string | — | Filter to sources occurring after this ISO 8601 UTC datetime | | `--before` | string | — | Filter to sources occurring before this ISO 8601 UTC datetime | | `--limit` | number | `50` | Results per page (1–100) | | `--cursor` | string | — | Pagination cursor from a previous response (`pagination.nextCursor`) | Source-type aliases are normalized to their canonical values before the request. With `--json`, the command returns the paginated `items` and `pagination` response. In interactive output, rows show `TYPE`, `OCCURRED`, `TITLE`, and `SOURCE ID`; when more results are available, the next cursor is printed for the following `--cursor` request. ### Examples ```bash theme={null} outlit sources list --customer acme.com --source-type CALL outlit sources list --participant alice@acme.com --source-type CALENDAR_EVENT --json outlit sources list --source-type SLACK --customer acme.com --limit 25 --json outlit sources list --after 2026-01-01T00:00:00Z --limit 25 ``` ### Get a source ```text theme={null} outlit sources get --source-type --source-id [--limit ] [--cursor ] ``` ### Flags | Flag | Type | Default | Description | | - | - | - | - | | `--source-type` | string | — | Source type: `EMAIL`, `CALL`, `CALENDAR_EVENT`, `SUPPORT_TICKET`, `OPPORTUNITY`, `SLACK`, `PERSON_PROFILE`. Case-insensitive. Aliases: `CRM`, `CRM_OPPORTUNITY` | | `--source-id` | string | — | Exact source identifier | | `--limit` | number | `10` | Exact content segments per page (1–100). | | `--cursor` | string | — | Opaque cursor returned by a previous exact lookup for the same source. | ### Examples ```bash theme={null} outlit sources get --source-type CALL --source-id call_123 --limit 10 --json outlit sources get --source-type CALL --source-id call_123 --cursor "$CURSOR" --json outlit sources get --source-type SLACK --source-id thread_123 --limit 50 outlit sources get --source-type SLACK --source-id thread_123 --cursor "$CURSOR" outlit sources get --source-type OPPORTUNITY --source-id opp_123 outlit sources get --source-type SUPPORT_TICKET --source-id ticket_456 --json ``` `EXTRACTED` is accepted by the exact-source tool API but is currently rejected by `outlit sources get`. Use one of the source types listed above in the CLI. When a response includes `contentPage`, read `contentPage.segments` and check `contentPage.completeness` for partial or unavailable content. If `contentPage.page.hasMore` is true, pass `contentPage.page.nextCursor` as `--cursor` with the same source type and ID. Use `--json` to inspect these fields. When a fact or search result references a `PERSON_PROFILE` source, pass that source ID to exact lookup to inspect its evidence: ```bash theme={null} outlit sources get --source-type PERSON_PROFILE --source-id "$SOURCE_ID" --json ``` ## Destinations Inspect and manage configured delivery destinations (for example Slack channels) with masked configuration only. ```text theme={null} outlit destinations list [flags] outlit destinations get [flags] outlit destinations create [flags] outlit destinations update [flags] outlit destinations enable [flags] outlit destinations disable [flags] outlit destinations archive [flags] ``` ### Examples ```bash theme={null} outlit destinations list --json outlit destinations get 10000000-0000-4000-8000-000000000003 --json outlit destinations create --type slack --channel-id C0123456789 --label "#customer-ops" --default --json outlit destinations update 10000000-0000-4000-8000-000000000003 --type slack --default --json outlit destinations enable 10000000-0000-4000-8000-000000000003 --json outlit destinations disable 10000000-0000-4000-8000-000000000003 --json outlit destinations archive 10000000-0000-4000-8000-000000000003 --json ``` Destination creation and update are Slack-channel backed. Use `--default` on `destinations create` or `destinations update` to make a destination the default fallback. `destinations update` patches only the fields you pass. Raw destination secrets and unmasked provider configuration are not returned. ## Settings Read and update the workspace timezone. Configure delivery and reports in the Outlit app. ```text theme={null} outlit settings get [flags] outlit settings update --default-timezone [flags] ``` ### Examples ```bash theme={null} outlit settings get --json outlit settings update --default-timezone America/Los_Angeles --json outlit destinations list --json ``` The public workspace settings capability returns and updates the default IANA timezone. Default delivery destination selection is handled by `outlit destinations create --default` or `outlit destinations update --default`. ## SQL Execute read-only SQL queries against Outlit analytics views. ```text theme={null} outlit sql [flags] outlit sql --query-file [flags] ``` ### Arguments | Argument | Required | Description | | - | - | - | | `query` | Yes\* | SQL query string (\*or use `--query-file`) | ### Flags | Flag | Type | Default | Description | | - | - | - | - | | `--query-file` | string | — | Path to a `.sql` file (takes precedence over positional arg) | | `--limit` | number | `1000` | Maximum rows to return | ### Examples ```bash theme={null} # Inline query outlit sql "SELECT event_type, COUNT(*) as cnt FROM activity GROUP BY 1 ORDER BY cnt DESC" # From a file outlit sql --query-file ./queries/monthly-activity.sql # Pipe to jq outlit sql "SELECT domain, mrr_cents FROM customers ORDER BY mrr_cents DESC LIMIT 10" | jq ``` ### JSON response ```json theme={null} { "success": true, "data": [ { "event_type": "PAGE_VIEW", "cnt": 12500 }, { "event_type": "MEETING", "cnt": 340 } ], "metadata": { "rowsReturned": 2, "executionTimeMs": 18.4, "truncated": false } } ``` ### Available views Use `outlit schema` to discover all views and their columns. Common views: | View | Description | | - | - | | `activity` | Event stream (page views, meetings, emails, etc.) | | `customers` | Denormalized customer attributes and metrics | | `users` | Denormalized user attributes | | `revenue` | Monthly recurring revenue snapshots | ## Schema Discover analytics views and their column definitions. ```bash theme={null} outlit schema [view] ``` ### Arguments | Argument | Required | Description | | - | - | - | | `view` | No | Specific view name — omit to list all views | ### Examples ```bash theme={null} # List all views outlit schema # Describe a specific view outlit schema activity # JSON output outlit schema customers --json ``` The public `customers` analytics view exposes company activation as `activated_at Nullable(DateTime64(3))`. Contact journey stages remain separate on users. ### JSON response ```json theme={null} { "name": "customers", "description": "One row per customer with current account and revenue attributes.", "columns": [ { "name": "domain", "type": "Nullable(String)", "description": "Customer domain when known." }, { "name": "mrr_cents", "type": "Nullable(Int64)", "description": "Current monthly recurring revenue in cents." } ], "exampleQueries": [ "SELECT domain, mrr_cents FROM customers ORDER BY mrr_cents DESC LIMIT 10" ] } ``` ## Feedback Submit product feedback to the Outlit team: bug reports, feature ideas, or friction you hit while working with Outlit. Feedback is product intake — it is not a support ticket and does not guarantee a reply. ```text theme={null} outlit feedback --body [flags] ``` ### Flags | Flag | Type | Default | Description | | - | - | - | - | | `--body` | string | — | Feedback text (required unless `--body-file`); trimmed, non-empty, at most 20,000 characters | | `--body-file` | string | — | Read the feedback body from a UTF-8 text file | | `--area` | string | — | Optional product area label such as `cli`, `mcp`, or `integrations`; trimmed, non-empty, at most 100 characters | | `--context` | string | — | Optional JSON object with diagnostic context; at most 16 KiB serialized | | `--context-file` | string | — | Read the JSON context object from a file instead of `--context` | `--body` and `--body-file` are mutually exclusive, as are `--context` and `--context-file`. Use `context` for diagnostics such as the surface, command, or client version — never for secrets or unnecessary customer data. Submitting feedback requires an authenticated API key and is rate limited separately from customer-intelligence reads. It does not consume paid usage quota. ### Examples ```bash theme={null} # Report a bug outlit feedback --body 'The customers merge preview did not list the losing record traits' --area customers # Suggest a feature outlit feedback --body 'Add a way to export attention items' --area attention --json # Include diagnostic context outlit feedback \ --body 'The SQL tool rejected a valid LIMIT clause' \ --context '{"surface":"cli","command":"sql"}' \ --json # Long-form feedback from files outlit feedback --body-file report.md --context-file diagnostics.json ``` ### JSON response ```json theme={null} { "id": "10000000-0000-4000-8000-000000000004", "createdAt": "2026-09-28T12:00:00.000Z" } ``` # Configuration Source: https://docs.outlit.ai/cli/configuration Environment variables, output behavior, and file paths for the Outlit CLI. ## Environment variables | Variable | Description | Default | | - | - | - | | `OUTLIT_API_KEY` | API key for authentication (overridden by `--api-key` flag) | — | | `OUTLIT_NO_UPDATE_NOTIFIER` | Disable automatic interactive CLI update notices when set | — | | `XDG_CONFIG_HOME` | Override the config directory (Unix) | `~/.config` | | `CI` | Triggers JSON output mode when equal to exactly `"true"` or `"1"` | — | | `GITHUB_ACTIONS` | Triggers JSON output mode when set | — | | `TERM` | When set to `dumb`, switches output to JSON | — | ## Config directory The CLI stores configuration in a platform-specific directory: | Platform | Path | | - | - | | macOS / Linux | `~/.config/outlit/` (or `$XDG_CONFIG_HOME/outlit/`) | | Windows | `%APPDATA%\outlit\` | ### Files | File | Purpose | Created by | | - | - | - | | `credentials.json` | Stored API key | `outlit auth login` | | `update-check.json` | Cached latest CLI version and installer hint | Automatic update check | The credentials file has `0600` permissions (readable by owner only) and contains: ```json theme={null} { "apiKey": "ok_your_api_key_here" } ``` ## Update checks In interactive terminals, the CLI periodically checks npm for the latest `@outlit/cli` version and caches the result in `update-check.json`. Cached notices are printed to stderr and never affect JSON output. ```bash theme={null} OUTLIT_NO_UPDATE_NOTIFIER=1 outlit customers list ``` Use `outlit upgrade` to update with the same package manager that installed the CLI when it can be inferred. ## Shell completions Generate completion scripts for your shell: ```bash theme={null} outlit completions bash >> ~/.bash_completion source ~/.bash_completion ``` ```bash theme={null} outlit completions zsh >> ~/.zshrc exec zsh ``` ```bash theme={null} outlit completions fish > ~/.config/fish/completions/outlit.fish ``` ## Output behavior ### TTY mode (interactive) When running in an interactive terminal, the CLI outputs: * **Tables** with Unicode box-drawing characters for list commands * **Spinners** on stderr during API calls * **Colored output** with green checkmarks for success * **Pagination hints** showing total count and next cursor Example: ``` ┌──────────────────────────┬────────────┬─────────┬──────────┬────────────┐ │ Name │ Domain │ Billing │ MRR │ Last Active│ ├──────────────────────────┼────────────┼─────────┼──────────┼────────────┤ │ Acme Corp │ acme.com │ PAYING │ $5000.00 │ 2d ago │ │ Globex Inc │ globex.com │ TRIALING│ $0.00 │ 5h ago │ └──────────────────────────┴────────────┴─────────┴──────────┴────────────┘ Showing 20 of 150 total. Next page: --cursor cursor_abc123 ``` ### JSON mode When JSON mode is active, all output is pretty-printed JSON on stdout: ```json theme={null} { "items": [ { "id": "cus_123", "name": "Acme Corp", "domain": "acme.com" } ], "pagination": { "hasMore": true, "nextCursor": "cursor_abc123", "total": 150 } } ``` Errors are also JSON, written to stderr: ```json theme={null} { "error": "Not authenticated. Run `outlit auth login` or pass --api-key.", "code": "not_authenticated" } ``` ### When JSON mode activates JSON mode activates automatically in any of these conditions: | Condition | Example | | - | - | | `--json` flag | `outlit customers list --json` | | stdout is piped | `outlit customers list \| jq` | | `CI=true` or `CI=1` | GitHub Actions, CircleCI, etc. | | `GITHUB_ACTIONS` is set | GitHub Actions | | `TERM=dumb` | Terminals without ANSI support | This means AI agents (Claude Code, Cursor, etc.) automatically get JSON output when they run CLI commands as subprocesses — no extra configuration needed. ## Error handling All errors exit with code `1`. The error format depends on the output mode: **TTY:** ``` Error: Not authenticated. Run `outlit auth login` or pass --api-key. ``` **JSON (stderr):** ```json theme={null} { "error": "Not authenticated. Run `outlit auth login` or pass --api-key.", "code": "not_authenticated" } ``` ### Error codes | Code | Description | | - | - | | `not_authenticated` | No API key found in any source | | `auth_required` | Client creation failed (authentication error) | | `auth_request_failed` | Browser/device auth request failed | | `auth_timeout` | Timed out waiting for browser authentication | | `invalid_key_format` | API key doesn't match expected format (`ok_` prefix) | | `invalid_key` | API key rejected by the server | | `validation_failed` | Key failed server verification during `auth login` | | `missing_key` | `--key` flag required but not provided (non-interactive `auth login`) | | `missing_input` | Required argument not provided | | `invalid_input` | Argument failed validation | | `api_error` | API request failed (includes HTTP status) | | `api_unavailable` | API unreachable (network/DNS failure) | | `unsupported_core_version` | The workspace does not support this command yet | | `file_error` | Failed reading or writing a file | | `write_error` | Failed to write a config file | | `unlink_error` | Failed to delete credentials file during `auth logout` | | `unknown_agent` | `--agent` value isn't a supported agent alias | | `unknown_provider` | Provider isn't a supported integration | | `unknown_shell` | Shell isn't supported by `completions` | | `agent_required` | Command requires `--agent` (non-interactive) | | `runner_not_found` | Agent runtime binary not found on PATH | | `skills_install_error` | Skill installation failed | | `update_check_failed` | Version update check failed | | `upgrade_failed` | Self-upgrade failed | Commands that call Outlit can return the server’s `code`, `message`, `retryable`, and `requestId` fields. See [Error responses](/api-reference/tools#error-responses). ## Data formatting The CLI formats values for readability in TTY mode: | Type | Raw value | Displayed as | | - | - | - | | MRR | `500000` | `$5000.00` | | Date | `2025-02-10T15:30:00Z` | `2d ago` | | Long names | `Very Long Customer Name Inc` | `Very Long Custo...` | | Missing values | `null` / `undefined` | `--` | The MRR column always divides `currentMrr` by 100 and shows `$`, because the list result doesn't name the customer's currency. That is right for customers billed in USD. For EUR, GBP, and CAD the amount is right but the symbol isn't. Other currencies follow the billing provider's amount rules (see [Stripe's currency docs](https://docs.stripe.com/currencies)), so the amount can be wrong too: JPY has no minor unit, so `500000` is ¥500,000, not `$5000.00`. Use `--json` for the raw `currentMrr`. ### Relative date format | Time elapsed | Display | | - | - | | Under 1 minute | `just now` | | Under 1 hour | `5m ago` | | Under 1 day | `3h ago` | | Under 30 days | `7d ago` | | Under 1 year | `2mo ago` | | 1 year or more | `1y ago` | ## Scripting examples ### Export all customers ```bash theme={null} #!/bin/bash LIMIT=100 CURSOR="" while true; do if [ -z "$CURSOR" ]; then PAGE=$(outlit customers list --limit $LIMIT --json) else PAGE=$(outlit customers list --limit $LIMIT --cursor "$CURSOR" --json) fi jq -r '.items[].domain' <<< "$PAGE" >> all_domains.txt HAS_MORE=$(jq -r '.pagination.hasMore' <<< "$PAGE") [ "$HAS_MORE" = "false" ] && break CURSOR=$(jq -r '.pagination.nextCursor' <<< "$PAGE") done ``` ### Export to CSV ```bash theme={null} outlit customers list --limit 100 --json \ | jq -r '.items[] | [.domain, .billingStatus, .currentMrr] | @csv' ``` ### CI health check ```bash theme={null} # Validate API key and agent setup in CI outlit doctor --json | jq -e '.ok' > /dev/null || exit 1 ``` ### Filter inactive users ```bash theme={null} outlit users list --journey-stage INACTIVE --no-activity-in 30d --json \ | jq '.items[] | {email, customer: .customerId, lastSeen: .lastActivityAt}' ``` # Integration setup (CLI) Source: https://docs.outlit.ai/cli/integrations Set up integrations and inspect their configuration readiness from the CLI. The CLI exposes two integration commands: `outlit integrations setup` and `outlit integrations status`. Both use the public tool gateway. For personal connections, the API key needs `integrations:connect_own`. Workspace setup needs `integrations:manage`. Outlit determines which permission applies to each provider and request. ## Integration setup ```bash theme={null} outlit integrations setup hubspot outlit integrations setup hubspot --json ``` Setup checks the provider capabilities without sending credentials. If the response includes `preferredSetupVersion: 1`, the CLI uses `outlit_setup_integration`. Older server versions use a browser handoff; the CLI does not send credentials or provider settings to that flow. Interactive setup can: * Prompt for a provider credential only after the Outlit server returns `CREDENTIAL_REQUIRED`. Secret prompts disable terminal echo. * Open a validated browser handoff on the configured Outlit origin. Authentication handoffs are polled for at most five minutes. Recovery and external setup open in the browser; the CLI does not wait for them to finish. * Display a CRM recommendation and submit that exact mapping after confirmation. * Display a Mixpanel preview and submit the exact mapping selected by the user. If setup repeats a credential, authentication, or mapping step, the CLI stops with `SETUP_STALLED`. If it cannot open a validated handoff URL, it prints the URL so you can open it yourself. Setup has no provider-secret flags. Never place provider credentials directly in command-line arguments, where process listings and shell history can expose them. ### Automation and JSON mode Use `--config-stdin` to opt into reading exactly one strict JSON object from stdin. Its shape is provider-specific. For example, a Fireflies API key is supplied only through stdin: ```bash theme={null} printf '%s\n' '{"credentials":{"apiKey":""}}' \ | outlit integrations setup fireflies --config-stdin --json ``` An explicit HubSpot CRM mapping is likewise supplied as the generated CRM mapping shape: ```bash theme={null} printf '%s\n' '{"configuration":{"kind":"crm_mapping","mappings":[{"pipelineId":"default","pipelineName":"Sales","mappings":[{"outlitStage":"Won","crmStages":[{"id":"closedwon","name":"Closed Won"}]}]}],"confirm":true}}' \ | outlit integrations setup hubspot --config-stdin --json ``` The provider remains the positional CLI argument and is rejected inside the stdin document. Unknown top-level fields are also rejected. `--config-stdin` is explicit; ordinary piped stdin is not read. JSON mode never prompts, opens a browser, or polls. It returns the raw setup result for handoffs and configuration previews. If credentials are required but were not supplied through `--config-stdin`, the command exits nonzero with Outlit's stable `CREDENTIAL_REQUIRED` error. For a CRM recommendation, `--accept-recommended` submits the exact recommendation from the current response. It conflicts with a custom CRM mapping supplied through stdin. A Mixpanel mapping must be supplied as an exact, confirmed configuration in headless mode. Expected provider credentials are never returned in command output. Handoff URLs must match the configured Outlit origin, use HTTPS except for loopback development, and contain no URL credentials, fragments, or secret-bearing query parameters. ## Integration status ```bash theme={null} outlit integrations status outlit integrations status hubspot --json ``` `status` calls `outlit_get_integration_status` and returns one of five configuration-readiness states: | State | Meaning | | - | - | | `not_connected` | No usable provider connection exists. | | `awaiting_auth` | Authentication must be completed. | | `setup_required` | Provider configuration or mapping remains. | | `ready` | Required authentication and configuration are complete. | | `requires_intervention` | Setup needs human attention or repair. | `ready` describes configuration readiness only. It does not promise that a sync or backfill has finished, that customer data is present, or that provider data is current. The status command does not expose browser-session, raw provider-state, sync, or action metadata. `gmail` is accepted as a CLI alias for `google-mail`. # CLI overview Source: https://docs.outlit.ai/cli/overview Access customer context from the terminal and install Outlit skills for coding agents. The Outlit CLI gives you direct access to structured customer context from the terminal. List customers, inspect activity timelines, search conversations and facts, run SQL queries, and install Outlit skills for coding agents without leaving your workflow. ## Installation Fastest path: ```bash theme={null} curl -fsSL https://outlit.ai/install.sh | bash ``` Alternative install methods: ```bash theme={null} npm install -g @outlit/cli ``` ```bash theme={null} brew install outlitai/tap/outlit ``` Download the latest release for your platform from [GitHub Releases](https://github.com/OutlitAI/outlit-sdk/releases). Extract the archive and move the `outlit` binary to a directory in your `PATH`. Verify the installation: ```bash theme={null} outlit --version ``` ## Quick start Go to [**Settings → API keys**](https://app.outlit.ai/settings/workspace/api-keys) in the Outlit app. Create an API key, copy it, and keep it safe. Treat your API key like a password. It grants whatever capabilities its configured grants allow — potentially including changing who can access customers — to anyone holding it. You can revoke keys at any time from the Outlit app. ```bash theme={null} outlit auth login ``` The CLI will prompt you to enter your API key. It validates the key against the API and stores it securely at `~/.config/outlit/credentials.json` with `0600` permissions. You can also pass the key directly: ```bash theme={null} outlit auth login --key ok_your_api_key_here ``` ```bash theme={null} # List paying customers outlit customers list --billing-status PAYING # Get a customer profile outlit customers get acme.com --include users,revenue,recentTimeline # Search with natural language outlit search "pricing objections last quarter" # Run SQL outlit sql "SELECT event_type, COUNT(*) FROM activity GROUP BY 1" ``` Install the `outlit` skill for detected coding agents with one command: ```bash theme={null} outlit setup --yes ``` Or target a specific coding agent: ```bash theme={null} outlit setup claude-code outlit setup codex outlit setup gemini outlit setup droid outlit setup opencode outlit setup pi outlit setup openclaw ``` For MCP clients like Cursor or VS Code, use the direct [MCP integration](/ai-integrations/mcp) guide instead. ## Authentication The CLI resolves your API key from the following sources, in order: | Priority | Source | How to set | | - | - | - | | 1 | `--api-key` flag | `outlit customers list --api-key ok_...` | | 2 | `OUTLIT_API_KEY` env var | `export OUTLIT_API_KEY=ok_...` | | 3 | Config file | `outlit auth login` | ### Auth commands ```bash theme={null} outlit auth login # Store API key (interactive or --key) outlit auth logout # Remove stored credentials outlit auth signup # Open sign-up page in browser outlit auth status # Check authentication state and validate key outlit auth whoami # Validate key and print masked key + source ``` `auth status` makes a live API call to validate your key: ```bash theme={null} $ outlit auth status ✓ Authenticated Key: ok_ab...1234 Source: config ``` `auth whoami` validates your key and outputs a single line in TTY mode — useful for scripting: ```console theme={null} $ outlit auth whoami ok_ab...1234 (config) ``` ## Available commands | Command | Description | | - | - | | [`activation`](/cli/commands#activation) | Read, preview against past events, update, or disable the exact product event used for contact and company activation | | [`customers list`](/cli/commands#customers-list) | Filter and list customers by billing status, MRR, activity, and traits | | [`customers get`](/cli/commands#customers-get) | Get a detailed customer profile with optional sections (users, revenue, etc.) | | [`customers features`](/cli/commands#customers-features) | Read event-based and imported metered Feature usage for one authorized customer | | [`customers credits`](/cli/commands#customers-credits) | Read imported credit pools and burn summaries for one authorized customer | | [`customers timeline`](/cli/commands#customers-timeline) | View activity timeline filtered by channel and event type | | [`features`](/cli/commands#features) | Read, create, and archive workspace Features | | [`users list`](/cli/commands#users-list) | Filter and list users by journey stage, activity, customer, and traits | | [`facts`](/cli/commands#facts) | Retrieve AI-generated facts and insights for a customer | | [`search`](/cli/commands#search) | Natural language search across customer context | | [`sources get`](/cli/commands#sources) | Retrieve one exact source record behind a fact or search result | | [`sql`](/cli/commands#sql) | Run SQL queries against analytics views | | [`schema`](/cli/commands#schema) | Discover analytics views and columns | | [`feedback`](/cli/commands#feedback) | Submit product feedback (bugs, ideas, agent friction) to the Outlit team | | [`onboard`](/cli/ai-agents#onboarding) | Bootstrap auth, install an agent skill, and inspect integration readiness | | [`integrations setup`](/cli/integrations#integration-setup) | Connect a provider from the terminal | | [`integrations status`](/cli/integrations#integration-status) | Show whether each integration is configured | | [`destinations list`](/cli/commands#destinations) | List configured delivery destinations with masked configuration | | [`setup`](/cli/ai-agents) | Install Outlit skills for coding agents | | [`doctor`](/cli/ai-agents#diagnostics) | Run environment diagnostics | | [`completions`](/cli/configuration#shell-completions) | Generate shell completion scripts | | `upgrade` | Upgrade the CLI with the package manager that installed it | ## Updates In interactive terminals, the CLI checks for newer published versions using a local cache so normal commands stay fast. When an update is available, it prints a short notice to stderr and continues. ```bash theme={null} outlit upgrade ``` `outlit upgrade` tries to use the same package manager that installed the CLI, such as Bun, npm, pnpm, or Yarn. Set `OUTLIT_NO_UPDATE_NOTIFIER=1` to suppress automatic notices. ## Output modes The CLI has two output modes: **interactive** (tables, spinners, colors) and **JSON** (machine-readable). JSON mode activates automatically when: * stdout is piped (`outlit customers list | jq`) * `--json` flag is passed * Running in CI (`CI=true`, `CI=1`, or `GITHUB_ACTIONS` is set) * Terminal is dumb (`TERM=dumb`) ```bash theme={null} # Interactive table outlit customers list # JSON output (explicit) outlit customers list --json # JSON output (automatic via pipe) outlit customers list | jq '.items[].domain' ``` ## Next steps Full reference for all data commands Install Outlit skills for Claude Code, Codex, Gemini CLI, Droid, OpenCode, Pi, OpenClaw, and more Manage destinations and selected workspace settings through API and CLI Environment variables and config paths # Customer context graph Source: https://docs.outlit.ai/concepts/customer-context-graph See how connected sources become customer profiles, evidence, and attention items. Outlit connects customer data from billing, product usage, CRM, conversations, and support. It links people to customers and keeps a timeline of what happened and where each piece of evidence came from. Your team can review that context in **Customers** and **Attention**; authorized agents can query it through the CLI, MCP, and API. ## From sources to customer work
Scroll sideways or use arrow keys. Connected SDKs, integrations, and conversations feed a graph of visitors, contacts, accounts, relationships, activity, and facts; customer profiles and developer interfaces use that context. Connected SDKs, integrations, and conversations feed a graph of visitors, contacts, accounts, relationships, activity, and facts; customer profiles and developer interfaces use that context. Connected SDKs, integrations, and conversations feed a graph of visitors, contacts, accounts, relationships, activity, and facts; customer profiles and developer interfaces use that context. Connected SDKs, integrations, and conversations feed a graph of visitors, contacts, accounts, relationships, activity, and facts; customer profiles and developer interfaces use that context.
The diagram shows how connected sources feed customer profiles. Outlit uses those profiles and their evidence when it prepares churn cases and renewal briefs. | Source | What it adds | | - | - | | Outlit Browser SDK and Outlit server SDKs | Website visits, identified people, and product events you instrument | | PostHog or Mixpanel integrations | Product activity and usage trends imported into Outlit | | Stripe | Subscription state, billing status, and revenue | | Salesforce, HubSpot, or Attio | Account records, contacts, owners, deals, and renewal dates | | Gmail, Slack, Granola, Gong, Fireflies, or Google Calendar | Conversations and meetings | | Pylon | Support tickets and escalations | | Clerk or Supabase Auth | User identity and sign-in activity | Connect sources in the [Outlit app](/integrations/overview). Churn monitoring needs billing and product activity for useful coverage; the Outlit Browser SDK adds the landing page, referrer, UTM data, and anonymous website visits before signup. Identifying the browser visitor links that history to later customer activity. Check **Responsibilities → Churn** for your workspace's actual monitoring status. ## How records become a profile [Identity resolution](/concepts/identity-resolution) links an anonymous visitor to a known person after they identify. A company email domain, explicit `customerId`, or connected account records can link that person to a customer. Ambiguous matches wait for review rather than being merged automatically. A customer's **Coverage** tab shows which sources support its profile. The activity timeline records events in time order. Each source can also support a **fact**, such as declining usage, a support escalation, a competitor mention, or a change in a key contact. Public fact results include `occurredAt`, `status`, and available provenance fields such as `sourceType`, `sourceId`, `sourceOccurredAt`, `sourceQuote`, and `permalink`. Read the source time and status before relying on an older finding. ## Customer journey and billing A person's product journey can be **Discovered**, **Signed Up**, **Activated**, **Engaged**, or **Inactive**. A customer's billing status can be **None**, **Trialing**, **Paying**, **Past due**, or **Churned**. These describe different parts of the relationship. Billing comes from a verified integration such as Stripe. You choose an ordinary product event for activation. The first matching event records activation once for each eligible resolved person and company, independently; later matches leave the recorded timestamp unchanged. SDKs and analytics integrations send ordinary events, and Outlit derives activation from the configured event. See [Customer journey](/concepts/customer-journey) for the exact stages and setup. ## Where to use the context * **Customers** shows profiles, people, coverage, timelines, and facts. * **Attention** shows churn and renewal situations that need a person, with evidence and an approach when available. * The [CLI](/cli/overview), [MCP](/ai-integrations/mcp), and [API](/api-reference/tools) let authorized agents and applications query customer context. Their access follows the same customer and workspace permissions. ## Next steps Review profiles and source coverage. Understand activation, engagement, and billing. See how visits and events link to people. Add the sources Outlit uses for monitoring. # Customer journey Source: https://docs.outlit.ai/concepts/customer-journey How Outlit separates contact journeys, company activation, and account billing in the customer context graph. Outlit tracks separate dimensions of your customer relationships as part of the [customer context graph](/concepts/customer-context-graph): **contact journey** (how engaged each person is), **company activation** (whether an account reached its configured value milestone), and **account billing** (the commercial relationship). The Outlit Browser SDK can preserve the earlier website path — landing page, referrer, UTM data, and anonymous activity — before browser identification links it to a contact. Product events and billing then show what happened through conversion and the later lifecycle. A contact can be highly engaged while their company has not activated or paid, and a company can remain activated while a specific contact becomes inactive. ## Three parts of the customer journey Outlit tracks contacts and accounts separately: ### Contact journey (per person) Contacts progress from **Discovered → Signed Up → Activated → Engaged**. With no qualifying activity they become **Inactive**; when activity again meets the engagement threshold, they return to **Engaged**. ### Account billing (per company) Account billing can be **None**, **Trialing**, **Paying**, **Past due**, or **Churned**. A churned account returns to **Paying** if it re-subscribes. ### Company activation (per company) A company activates once when the configured exact ordinary product event first occurs for that resolved company. The same event activates eligible contacts. Outlit writes each person's or company's activation timestamp only once; later matches leave it unchanged. Use `outlit activation preview --event ` to evaluate historical exact-event matches without changing configuration or recording activation. Its separate contact and company counts show how many people and companies already activated and how many would activate, without returning contact identities. Use `outlit activation update --event ` only after review. Disabling future matching preserves existing contact and company activation timestamps. Company activation remains an account milestone, while a contact's `ACTIVATED` value remains a per-person journey stage. They share one configured event but are recorded independently when the event resolves the corresponding identity. ## Contact journey stages Each person (contact) progresses through stages based on their product engagement: | Stage | Meaning | How it is set | | - | - | - | | **Discovered** | Email known, hasn't signed up yet | Auto-detected by the Outlit Browser SDK when email is provided without userId | | **Signed Up** | Created an account | Auto-detected by the Outlit Browser SDK when both email and userId are provided | | **Activated** | Completed onboarding or key milestone | Derived by Outlit from your selected ordinary activation event | | **Engaged** | Actively using the product | Auto-inferred from product activity via PostHog or Outlit SDKs | | **Inactive** | No activity for extended period | Auto-inferred when no qualifying activity is detected for the configured inactivity period | Contact stages track **product engagement**, not billing. A contact can be Engaged even if their company hasn't paid, or Inactive even if their company is actively paying. ### How stages progress Contacts move forward through Discovered, Signed Up, Activated, and Engaged. The exception is the Engaged ↔ Inactive cycle: a contact returns to Engaged when activity again meets the configured threshold. ## How stages are detected ### Discovered vs signed up The difference is determined by the identifiers you provide: When you identify a visitor with **only an email** (no userId), they're marked as Discovered: ```typescript theme={null} outlit.identify({ email: 'jane@example.com', traits: { source: 'newsletter' } }) ``` Typical triggers: newsletter signup, contact form submission, lead magnet download. When you identify a visitor with **both email and userId**, they're marked as Signed Up: ```typescript theme={null} outlit.identify({ email: 'jane@example.com', userId: 'usr_12345', traits: { plan: 'free' } }) ``` Typical triggers: account creation, first login, OAuth authentication. If a Discovered contact later provides a userId (e.g., they sign up after submitting a lead form), they automatically advance to Signed Up. ### Automatic engagement and inactivity Outlit derives Engaged and Inactive from tracked product activity. Activity signals can come from the Outlit Browser SDK, Outlit server SDKs, and connected product integrations. Engagement is based on consistent activity in a configured rolling window; inactivity is based on no qualifying activity for the configured inactivity period. ## How contact activation works Identify the user and send the same ordinary activation event consistently from the Outlit Browser SDK, an Outlit server SDK, or a verified product integration. ### Track the selected activation event Choose an ordinary event that represents the product's meaningful value moment, then track it after the action succeeds: ```typescript theme={null} outlit.track('onboarding_completed', { flow: 'self_serve', completedSteps: 3 }) ``` Configure `onboarding_completed` (or your equivalent event) as the activation event in Outlit. Outlit derives Activated from that ordinary event. It also derives Engaged and Inactive from the broader stream of tracked product activity. ## Account billing statuses Each account (company) has a billing status that's separate from individual contact journeys: | Status | Meaning | How it is set | | - | - | - | | **None** | Never had a subscription | Default | | **Trialing** | Active trial period | Verified billing integration | | **Paying** | Active paid subscription | Verified billing integration | | **Past due** | Payment failing or overdue | Verified billing integration | | **Churned** | Had subscription, now cancelled | Verified billing integration | ## Billing from Stripe If you've connected Stripe to Outlit, billing status is handled automatically: Stripe subscription states map onto the billing statuses above — trialing maps to Trialing, active to Paying, delinquent states to Past due, and cancellation to Churned. When a subscription status changes in Stripe, Outlit updates the matching account's billing status. Individual contact journey stages are not affected. Stripe integration links accounts through synced customer and contact identity. Company-domain accounts and personal-email customers are handled differently, so billing status should be read as account context, not as a contact journey stage. ## Track meaningful events **Send product events.** Discovered and Signed Up are auto-detected from identity. Activated is derived by Outlit from the customer-selected ordinary activation event. Engaged and Inactive are derived from tracked product activity captured by the Outlit Browser SDK, Outlit server SDKs, and verified product integrations. Verified billing integrations provide billing status. **Configure one ordinary activation event.** Continue tracking ordinary product events or ingesting them through connected analytics sources. Select the exact event name with the activation CLI; do not add a synthetic activation event or client-side lifecycle state. **Track activation at the right time.** Send the selected ordinary activation event when the action is confirmed complete, not when the user clicks a button. Wait for the backend to confirm success before tracking. **Include useful properties.** Properties help you analyze stage transitions. Include context like which onboarding flow was completed, time-to-activate, and which steps were skipped. ## Frequently asked questions Contact journey tracks individual people's product engagement (Discovered through Inactive). Account billing tracks the commercial relationship (None through Churned). They're independent — Jane can be Engaged while her company is on a free plan, or Inactive while her company is actively paying. Contact activation is a per-person journey stage, while company activation is a separate account milestone exposed as `activatedAt`. Outlit derives both from the same configured exact ordinary product event and independently activates whichever contact or company identity the event resolves. None are set directly. Discovered and Signed Up are derived from identity, Activated is derived from your selected ordinary activation event, and Engaged and Inactive are derived from tracked product activity. Billing statuses sync from verified billing integrations. Contacts progress forward through Discovered, Signed Up, Activated, and Engaged. The only cycle is Engaged ↔ Inactive, which is driven by activity and inactivity. ## Next steps How Outlit connects anonymous visitors to known contacts Track ordinary product events from your backend Set up browser tracking Direct API for identify and product events # Identity resolution Source: https://docs.outlit.ai/concepts/identity-resolution How Outlit connects anonymous visitors, known contacts, and customer accounts. Outlit links anonymous website visitors, known contacts, and customer accounts in the [customer context graph](/concepts/customer-context-graph). The Outlit Browser SDK records the first landing page, referrer, UTM data, and anonymous browsing; identifying that same browser visitor with an email connects their earlier history to a contact. An email-only call from an Outlit server SDK does not identify an otherwise anonymous browser visitor. ## Identify from forms By default, Outlit **automatically identifies visitors** when they submit a form containing an email field—no code required. ### How it works When a form is submitted, Outlit: 1. **Detects email** by looking for: * Field names: `email`, `e-mail`, `user_email`, `emailAddress` * Input type: `type="email"` * Any field with a valid email value (fallback) 2. **Extracts name** by looking for: * Full name: `name`, `full_name`, `fullname`, `your_name` * First name: `first_name`, `fname`, `firstname`, `given_name` * Last name: `last_name`, `lname`, `surname`, `family_name` 3. **Calls `identify()` automatically**: ```typescript theme={null} // This happens behind the scenes when a form with email is submitted identify({ email: 'jane@acme.com', traits: { name: 'Jane Doe', // If full name found firstName: 'Jane', // If first name found lastName: 'Doe' // If last name found } }) ``` The form submission event is still captured separately. Auto-identify adds an additional `identify` event when an email is found. ### Disabling auto-identify If you prefer to handle identification manually: ```html theme={null} ``` ```tsx theme={null} {children} ``` ```typescript theme={null} outlit.init({ publicKey: 'pk_xxx', autoIdentify: false }) ``` ## How identity resolution works For example, two anonymous visits with different visitor IDs both identify as `jane@acme.com`. Outlit links both histories to the same contact. A server event for `usr_456` that identifies with the same email joins that contact's customer profile too. ### Identity priority When multiple identifiers exist, Outlit uses this priority order: | Priority | Identifier type | Example | Source | | - | - | - | - | | 1 (Highest) | Email | `jane@acme.com` | Identify call, form submission | | 2 | External User ID | `usr_12345` | Auth provider (Supabase, Auth0) | | 3 | Fingerprint | `device_abc123` | Outlit server SDK (device identifier) | | 4 (Lowest) | Anonymous ID | `abc-def-123` | Browser visitor ID | ### Resolution process When you call `identify()`: 1. **Search existing contacts** by provided identifiers 2. **Match found?** Link the anonymous visitor to existing contact 3. **No match?** Create a new contact and, when the evidence supports it, a customer profile Identifying a person does not request a merge of separate customer records. Review customer merge suggestions through **Identity**. When `customerId` is provided with an identify call, Outlit also links that contact to your system-owned account/workspace identifier. That is how earlier `customerId`-only server events become part of the same customer profile once a user identifies. Outlit treats organizational domains differently from common personal email providers. An address like `jane@acme.com` can establish company account context, while `jane@gmail.com`, academic addresses, and other personal/common-provider emails are resolved as personal contacts unless another source links them to an account. ## Identifier types ### Email (primary) Email is the most reliable identifier because it's unique per person, verified through login/signup, and works across devices. ```typescript theme={null} outlit.identify({ email: 'jane@acme.com', // Primary identifier traits: { name: 'Jane' } }) ``` ### External user ID Your application's internal user ID (from Supabase, Auth0, Firebase, etc.): ```typescript theme={null} outlit.identify({ userId: 'usr_12345', // Your auth system's ID email: 'jane@acme.com', traits: { authProvider: 'auth0' } }) ``` ### Customer ID Your system-owned customer, account, or workspace ID. Use this when product usage or billing is account-scoped: ```typescript theme={null} outlit.identify({ email: 'jane@acme.com', userId: 'usr_12345', customerId: 'cust_123', // Your app's account/workspace/customer ID customerTraits: { plan: 'enterprise' } }) ``` Server-side `track()` calls can also use `customerId` without a user identity: ```typescript theme={null} outlit.track({ customerId: 'cust_123', // Your app's account/workspace/customer ID eventName: 'account_synced' }) ``` Those events are attributable immediately to the account/workspace and can later link to the contact resolved from email when `identify({ email, customerId })` is sent. ### Anonymous visitor ID Generated for browser visitors once tracking is enabled: ```typescript theme={null} // Generated and stored once tracking is enabled (null before then) const visitorId = outlit.getVisitorId() // "a1b2c3d4-e5f6-7890-abcd-ef1234567890" ``` This ID is stored in localStorage and cookies, persists across sessions (same browser), and gets linked to contact after identify. With the default `autoTrack: "auto"` mode, nothing is stored in regions that require opt-in consent (EEA, UK, Switzerland, or unknown) until `enableTracking()` runs. The visitor ID cookie is set on your root domain, so `example.com` and `app.example.com` share one ID. Install the Browser SDK on both your marketing site and your app, and identify users after login with `setUser()` or `identify()`. Their earlier marketing-site visits then link to the contact. Sites on different root domains don't share an ID. ### Fingerprint (device ID) For native apps without browser storage (desktop, mobile, CLI), use a fingerprint—a stable device identifier you generate and manage: ```rust theme={null} // Outlit Rust SDK client.track_by_fingerprint("app_opened", fingerprint("device_abc123")) .send() .await?; // Later, link to user client.identify(email("user@example.com")) .fingerprint("device_abc123") .send() .await?; ``` See [Device Tracking](/concepts/website-visitors#device-tracking-non-browser) for implementation details. ## How identifiers connect activity Outlit links histories when they share an exact identifier, such as the same email or user ID. When two customer records might match but the evidence is ambiguous, Outlit presents a merge suggestion for review instead of combining them automatically. ### Linking examples User visits on phone (visitorId: A), then laptop (visitorId: B), identifies with same email on both. **Result:** Both visitor histories link to one contact. User identifies as `jane@old-company.com`, later updates to `jane@new-company.com` while logged into same account (userId). **Result:** The shared user ID can link activity to the known contact; review conflicting records before any customer merge. User signs up with email, later links Google OAuth which has different email. **Result:** The shared user ID can connect the activity to the same contact. Earlier visitor events keep their original timestamps when they join a known contact’s timeline. Merging two customer records is a separate reviewed action; see [the merge workflow](/ai-integrations/customer-workflows#recipe-merge-a-duplicate-customer). ## Browser vs server identity **Anonymous visitors are supported** * visitorId generated once tracking is enabled * Events stored until identification * Later linked via identify() or setUser() ```typescript theme={null} // Works without identity outlit.track('page_viewed', { page: '/pricing' }) // Later, when they sign up outlit.identify({ email: 'user@example.com' }) // Or for SPAs, use setUser after authentication outlit.setUser({ email: 'user@example.com', userId: 'usr_123' }) ``` **Identity or account attribution required** * User-scoped events use `email`, `userId`, or `fingerprint` * Account-scoped events can use `customerId` only * `customerId` links account/workspace activity to contacts when identify uses the same ID * For device/app tracking, use `fingerprint` ([learn more](/concepts/website-visitors#device-tracking-non-browser)) ```typescript theme={null} // User-scoped event outlit.track({ email: 'user@example.com', eventName: 'subscription_created' }) // Account/workspace-scoped event outlit.track({ customerId: 'cust_123', // Your app's account/workspace/customer ID eventName: 'workspace_synced' }) ``` ## Best practices ### 1. Identify early Call `identify()` as soon as you have user information: ```typescript theme={null} // After login function onLogin(user: { email: string; id: string; plan: string }) { outlit.identify({ email: user.email, userId: user.id, traits: { plan: user.plan } }) } // After form submission function onFormSubmit(data: { email: string }) { outlit.identify({ email: data.email, traits: { source: 'contact_form' } }) } ``` ### 2. Always send both identifiers When you have both email and userId, send both: ```typescript theme={null} // Good - creates strong identity link outlit.identify({ email: user.email, userId: user.id, traits: { plan: user.plan } }) // Less good - single identifier outlit.identify({ email: user.email, traits: { plan: user.plan } }) ``` ## Review identity across connected sources Tracking identifiers are one part of customer identity. Outlit also links records from connected billing, CRM, and other sources to the customer profile. Open the customer's **Coverage** tab to inspect the sources behind that account and investigate missing or unexpected matches. An **Identity** review page is available in some workspaces for inspecting identifiers and merge suggestions. Ask your Outlit contact about access. Identity resolution continues to operate when this review page is unavailable. For programmatic diagnosis, use `outlit customers identity ` and `outlit identity suggestions list`. Customer merges require exact IDs and a reviewed preview before execution. Follow the [merge workflow](/ai-integrations/customer-workflows#recipe-merge-a-duplicate-customer) rather than merging based on a similar name alone. ## Debugging **Common identity issues:** * **Events not linking to user:** Ensure `identify()` is called after authentication and that email/userId matches what you expect. * **Duplicate customer profiles:** Inspect their identifiers and source coverage. Different emails, missing shared identifiers, and personal-email accounts can produce separate records. Review any merge suggestion before combining records; contact Outlit when the correct match is unclear. * **Anonymous history not appearing:** The `identify()` call must carry the same browser/device identifier used for the earlier activity. Check that tracking was enabled and the same public key was used, then allow for background processing. ## Next steps Deep dive into the anonymous phase Direct API for custom integrations # Website visitors Source: https://docs.outlit.ai/concepts/website-visitors What Outlit captures when someone visits your website, from the first anonymous pageview through identification. ## Website activity Most visitors browse your site anonymously before signing up or logging in. The Outlit Browser SDK captures where they landed, how they arrived, and what they viewed. When the browser visitor identifies, Outlit connects that earlier history to their contact so the path from discovery to conversion joins the later [customer context graph](/concepts/customer-context-graph). Tracked visitors appear on the **Visitors** surface in the app. ## The anonymous phase On a first visit, the Outlit Browser SDK assigns a visitor ID and records pageviews, the landing URL, the browser referrer, and any UTM parameters—once tracking is enabled. With the default `autoTrack: "auto"` mode, tracking starts automatically where the visitor's region doesn't require opt-in consent; in the EEA, UK, and Switzerland—or when the region can't be determined—nothing is stored (no visitor ID, cookie, or localStorage key) until your site calls `enableTracking()` (see [consent management](/tracking/browser/script#consent-management)). When the browser visitor later identifies with an email, Outlit links that earlier activity to their profile while keeping its original timestamps. An email-only call from an Outlit server SDK cannot link an otherwise anonymous browser visit.
Scroll sideways or use arrow keys. A first visit creates anonymous pageview history with a visitor ID. After the visitor submits an email, the Outlit Browser SDK identifies them and Outlit links earlier activity to the contact. A first visit creates anonymous pageview history with a visitor ID. After the visitor submits an email, the Outlit Browser SDK identifies them and Outlit links earlier activity to the contact. A first visit creates anonymous pageview history with a visitor ID. After the visitor submits an email, the Outlit Browser SDK identifies them and Outlit links earlier activity to the contact. A first visit creates anonymous pageview history with a visitor ID. After the visitor submits an email, the Outlit Browser SDK identifies them and Outlit links earlier activity to the contact.
## Captured data ### Automatic attribution On first visit, Outlit captures: | Data | Source | Example | | - | - | - | | UTM Source | URL params | `utm_source=google` | | UTM Medium | URL params | `utm_medium=cpc` | | UTM Campaign | URL params | `utm_campaign=spring_2024` | | First Referrer | Browser `document.referrer` | `https://google.com` | | First Landing Page | URL | `/pricing?plan=pro` | ### Events stored All events are stored for anonymous visitors and will be linked when they identify: ```typescript theme={null} // All these are tracked for anonymous visitors outlit.track('pricing_viewed', { plan: 'enterprise' }) outlit.track('demo_video_played', { duration: 120 }) outlit.track('feature_explored', { feature: 'integrations' }) ``` ### Form data Form submissions are captured with automatic sanitization: ```json theme={null} { "type": "form", "formId": "contact-form", "formFields": { "email": "jane@acme.com", "name": "Jane", "company": "Acme Inc" } } ``` Sensitive fields (password, credit\_card, SSN, etc.) are automatically removed from form data. ## Link a visit to a person When `identify()` is called, the anonymous history is linked: ```typescript theme={null} // This triggers identity resolution outlit.identify({ email: 'jane@acme.com', traits: { name: 'Jane Doe' } }) ``` Here's what happens: 1. **Find or create contact** with the provided email 2. **Link the anonymous visitor** to the contact 3. **Historical events are processed**: * All previous events are added to the customer timeline * Original timestamps are preserved * The pre-identification journey is now visible on the profile ### Timeline after resolution ``` Jane Doe (jane@acme.com) - Customer Timeline ├── Mar 1, 10:00am - First visited /pricing (anonymous) ├── Mar 1, 10:05am - Viewed enterprise plan (anonymous) ├── Mar 1, 10:10am - Watched demo video (anonymous) ├── Mar 3, 2:00pm - Returned, viewed /features (anonymous) ├── Mar 3, 2:15pm - Submitted contact form ← IDENTIFIED ├── Mar 3, 2:20pm - Viewed /signup └── Mar 3, 2:25pm - Created account ``` ## Visits across devices Anonymous tracking is per-browser. The same person on different devices has different visitor IDs until identified. Once someone calls `identify()` with the same email on multiple devices, their visit histories link to the same contact timeline. **Edge cases**: If visitors clear cookies, use incognito mode, or share browsers, they may generate new visitor IDs. Call `identify()` immediately after login—even for returning users—to ensure continuity and accurate tracking. ## Device tracking (non-browser) For native apps without browser storage (desktop apps, mobile apps, CLI tools), use `fingerprint` instead of `visitorId`. A fingerprint is a device identifier that you generate and manage. ### When to use fingerprint | Environment | Use | | - | - | | Desktop apps (Tauri, Electron) | `fingerprint` | | Mobile apps (React Native) | `fingerprint` | | CLI tools | `fingerprint` | | Browser/Webview | `visitorId` (automatic) | ### How it differs from `visitorId` | | visitorId | fingerprint | | - | - | - | | Generated by | SDK (once tracking is enabled) | You (manual) | | Stored in | localStorage + cookies | Your app (you decide) | | SDKs | Outlit Browser SDK | Outlit Rust SDK, Outlit Node.js SDK | ### Generating a fingerprint Use any persistent, unique identifier for the device or installation: * **Device UUID** - Hardware identifier * **Installation ID** - Generated on first app launch, stored in app data * **Keychain/Secure storage** - For sensitive apps The fingerprint must be: * Consistent across app sessions * Unique per device/installation * Stored persistently by your app ### Example: tracking before sign-in ```rust theme={null} use outlit::{Outlit, fingerprint, email}; // Get or generate device fingerprint (store in app data) let device_id = get_or_create_device_id(); // Track anonymously client.track_by_fingerprint("feature_used", fingerprint(&device_id)) .property("feature", "export") .send() .await?; // Later, when user signs in client.identify(email("user@example.com")) .fingerprint(&device_id) .send() .await?; // ^ All previous fingerprint events are now linked to this user ``` ```typescript theme={null} import { Outlit } from '@outlit/node' // Get or generate device fingerprint const deviceId = await getOrCreateDeviceId() // Track anonymously outlit.track({ fingerprint: deviceId, eventName: 'feature_used', properties: { feature: 'export' } }) // Later, when user signs in outlit.identify({ email: 'user@example.com', fingerprint: deviceId }) // ^ All previous fingerprint events are now linked to this user ``` Events tracked with only a fingerprint are stored but not visible in customer timelines until linked via `identify()`. Once linked, they appear with their original timestamps. ## Privacy considerations Ask your Outlit contact about the retention and deletion policies that apply to your workspace. A visitor ID is a pseudonymous identifier; successful identification can link the associated activity to a known customer. With the default `autoTrack: "auto"` mode, tracking stays off—and nothing is stored—for visitors in regions that require opt-in consent (EEA, UK, Switzerland, or unknown) until your consent tool calls `enableTracking()`; see [consent management](/tracking/browser/script#consent-management) for the setup. `disableTracking()` persists an opt-out for visitors who decline, and `enableTracking()` resumes collection when appropriate. To wait for `enableTracking()` instead — unless a previous opt-in is stored — initialize with `autoTrack: false`. ## Next steps How visitors fit into the unified data model How anonymous visitors become known contacts Detailed browser integration guide # Connect your data Source: https://docs.outlit.ai/getting-started/connect-data Link the systems that hold your customer data so Outlit can start monitoring. Outlit monitors what it can see. Every source you connect — billing, product usage, CRM, conversations, support — sharpens customer profiles and widens what monitoring can catch. You connect sources once from **Integrations** in the app, and Outlit keeps them in sync. The [Outlit Browser SDK](/tracking/quickstart) adds the website visits that precede those customer records. ## Before you start * **Workspace access.** You have signed in at [app.outlit.ai](https://app.outlit.ai) to a workspace provisioned with Outlit. * **Who can connect what.** Admins can connect any source. Members can connect their own accounts where the source is per-person (Gmail, Granola personal keys) — see [roles and access](/product/workspace-access). * **Admin rights at the source.** Some connections need privileges on the other side: Salesforce requires a Salesforce admin, and API-key sources need a key you can create in that tool. ## Connect a source In the app sidebar, open **Integrations** — or finish the connect step of onboarding, which shows the same catalog grouped by what each source adds: revenue, customer records, conversations, and product usage. Click **Connect** on a card. Depending on the source you'll either authorize Outlit in a popup (OAuth — Stripe, Salesforce, HubSpot, Attio, Slack, Google, Gong) or paste a key you create in that tool (PostHog, Pylon, Fireflies, Granola, Autumn, Supabase Auth, Clerk). Mixpanel uses a service-account username and secret, project ID, and region. See the [integrations guides](/integrations/overview) for exactly what each one asks for. Some sources need one more step before their data counts: CRM connections ask you to map the pipeline — renewal stages, won/lost outcomes, and the renewal-date field; Gong and Pylon need a webhook; Slack asks which channels to include; and Mixpanel needs its account-property mapping confirmed. The app flags these on the source and in onboarding's **Setup** step. First-sync time varies by source and how much history it holds. The **Attention** page tells you where you stand — see below. ## How you know it's working The Attention page reflects your data state until the first item arrives: | Attention empty state | What it means | What to do | | - | - | - | | **Connect a data source** | Nothing connected yet | Connect your first source | | **Finish integration setup** | A source is connected but has a required step left (mapping, webhook, channel pick) | Open **Integrations** and complete the flagged step | | **An integration needs attention** | A connection broke (expired auth, revoked token, provider error) | Review the flagged integration | | **Your data is synchronizing** | Connected, first import still running | Check back; large CRMs and email history take longer | Once imports land, customers appear in **Customers**, and the first attention items start arriving as monitoring sees enough signal to say something. ## Which sources matter most Monitoring is only as good as its inputs. For churn and renewal coverage, these do the most work: | Source | Powers | | - | - | | **Stripe** | Billing status (trialing, paying, past due, churned), MRR, subscription changes — required for a customer to enter churn-monitoring scope | | **PostHog, Mixpanel, or Outlit SDKs** | Product activity per account — feeds engagement, inactivity, and usage-drop signals | | **Outlit Browser SDK** | First landing page, referrer and UTM data, and anonymous website activity linked after browser identification | | **Salesforce / HubSpot / Attio** | Accounts, contacts, deals — the source for renewal cycles and deal-linked outcomes | | **Gmail / Slack / Granola / Gong / Fireflies** | Conversations, meetings, and calls — stakeholder context and risk language | | **Pylon** | Support tickets — volume spikes and escalations | If you can only connect two things first, connect **Stripe** and a **product-usage source** — they cover the inputs churn monitoring checks. Whether monitoring is actually active depends on coverage *and* your workspace's responsibilities being enabled: open **Responsibilities → Churn** and the status card shows readiness plus anything still missing. Add a CRM to get renewals, conversations for evidence quality, and the Outlit Browser SDK for the journey from first visit to conversion. ## Troubleshooting Monitoring needs eligible accounts (paying or past-due customers for churn) plus enough signal to justify an item. Check **Responsibilities → Churn** for the monitoring status card — it lists reduced-coverage reasons like a missing billing source, missing product source, or missing notification destination. Open **Integrations** and review the flagged card — usually re-authorization or a missing webhook. See the [integrations overview](/integrations/overview) for per-source specifics. Gmail connects per teammate — each person syncs their own mailbox, so shared coverage grows as teammates connect. Most other sources connect once for the whole workspace. ## Next steps What lands when monitoring finds something Per-source connect steps and requirements # Review your first attention item Source: https://docs.outlit.ai/getting-started/first-attention-item What an attention item is, what's inside it, and how to act on it. **Attention** holds customer situations that need your decision: churn risks and upcoming renewals. Each item shows Outlit’s assessment, supporting evidence, and a proposed approach when one is available. Attention items in the Outlit app ## Reading an item Each card shows, at a glance: * **Customer** — the account the item is about, with its logo. * **Kind** — *Churn risk* or *Renewal*, plus a priority badge (**Urgent**, **High**, or **Normal**). * **What happened** — a plain-language title, like "Usage dropped after the champion left" or "Renews in 30 days." * **Account context** — annual revenue and revenue percentile where billing data exists; renewal items show the renewal date, marked **Overdue** when past. * **Approach progress** — if work is underway, the next step and how many steps are complete. * **Owner** — the teammate responsible for the account, when one is set. Opening an item takes you to its full story: the assessment, the supporting evidence — with a link back to the source record where one exists — and any approaches Outlit proposes. ## Acting on it For a churn case, the detail page lets you: 1. **Read the assessment** — what changed, why it matters, and the evidence behind its findings. Evidence shows its source type and when it was observed, and links out to the actual record where a link exists (a Slack thread, a ticket, a usage series). 2. **Choose an approach** — where Outlit proposes one: a primary approach, sometimes with alternatives. Choosing an approach sets the plan; you can also pick a different one or take no approach. An item with no proposals yet says so — the assessment still stands on its own. 3. **Review outbound work** — if the plan includes an email follow-up, you get the drafted email to edit and approve before anything sends. Approvals are covered in [Actions and approvals](/product/actions-and-approvals). 4. **Resolve or reopen** — **Mark resolved** when the situation is handled; **Reopen** brings a resolved item back if circumstances change. Attention is a queue, not a feed. Resolving an item moves it to the **Resolved** view (`?status=resolved`), which you can revisit anytime — it's an archive, not a delete. ## Review a renewal item A renewal item shows its date and priority on the card. Open it to read the approved brief, including adoption, stakeholders, risks, opportunities, and source evidence. If Outlit proposes an approach, choose it or an alternative. Review any prepared email before approving it. Resolving the attention item records the handoff as complete; record the cycle outcome separately when the renewal closes. See [Work a renewal](/guides/renewals) for the full cycle. ## If the queue looks empty Check three things before concluding monitoring is quiet: * **The Open/Resolved toggle** — the queue defaults to **Open**; resolved items live under the Resolved view. * **Filters and search** — an active filter or search produces a "No items match these filters" state, which looks different from a genuinely empty queue. * **Your access** — members see items on customers they own or that are shared with them. A teammate may see items you don't. With those ruled out, an empty queue means monitoring is active and nothing needs a person right now — or the empty state itself is telling you what's missing (connect a source, finish a setup step, wait for the first sync). See [Connect your data](/getting-started/connect-data) for what each message means. ## Next steps Filters, priorities, lifecycle, and the evidence model What the Churn and Renewals responsibilities watch for # Outlit for customer success Source: https://docs.outlit.ai/getting-started/overview What Outlit does, how your workspace gets access, and the path to your first useful result. Outlit continuously monitors your customers, finds churn risks and growth opportunities, and prepares the next step for your team to review — with the evidence behind every call. It is built for the part of your book of business nobody can watch closely enough: the long tail of accounts that renew, expand, or quietly drift away while your team focuses on strategic names. ## How Outlit works 1. **Connect** — You link the systems that already hold your customer data: billing (Stripe), product usage (PostHog, Mixpanel, or Outlit SDKs), CRM (Salesforce, HubSpot, Attio), conversations (Gmail, Slack, Granola, Gong, and more), and support (Pylon). 2. **Resolve** — Outlit reconciles people, accounts, domains, and billing records into customer profiles, keeping provenance on what they're built from. 3. **Monitor** — Outlit's responsibilities watch eligible customers continuously: Churn looks for retention risk, Renewals prepares upcoming renewals. 4. **Review** — Anything that needs a person lands in **Attention**: the risk, the evidence, and — where Outlit has one — a proposed approach. 5. **Approve** — You steer. Choosing an approach or approving an email draft is a human decision; Outlit executes only what you approve, like sending the reviewed Gmail follow-up. ## Getting access Your Outlit team provisions the workspace during onboarding. After you receive an invitation, sign in to your company's workspace at [app.outlit.ai](https://app.outlit.ai). Then: * A **workspace admin** can invite teammates from **Settings → Members**, or during onboarding. * Each teammate connects their own work accounts where relevant (for example, Gmail connects per person, not once per company). If you joined an existing workspace, [check its connected sources](/getting-started/connect-data). Connect your own Gmail if your team uses email as customer evidence. ## Where things live | Area | What it's for | | - | - | | **Attention** | Your queue: churn cases and renewal items that need a person, sorted by what's recommended | | **Customers** | The accounts Outlit has resolved, with profiles, people, and source coverage | | **Responsibilities** | What Outlit is doing for you: **Churn** monitoring status, cases, and watches; **Renewals** portfolio and briefs | | **Integrations** | Connect and maintain your data sources | | **Settings** | Members, Slack **Destinations**, **Website Visitors** tracking, **API keys**, **CLI & MCP**, billing | The [Responsibilities guide](/product/responsibilities) explains monitoring status and workspace availability. ## Next steps Link billing, product, CRM, and conversation sources What an item contains and how to act on it What the Churn and Renewals responsibilities actually do Admin vs member permissions, invites, personal connections # Catch churn early Source: https://docs.outlit.ai/guides/churn End-to-end: from connected sources to a resolved churn case, with a worked example. This guide walks the full loop: the data churn monitoring needs, what fires a case, and how a risk goes from detected to resolved. The example uses a fictional customer to show the steps you take when Outlit opens a churn case. ## What churn monitoring needs Churn cases only exist where Outlit can see enough to judge risk. Three inputs do the heavy lifting: | Input | Source | Powers | | - | - | - | | **Billing status** | Stripe | Which accounts are in scope — monitoring covers **paying** and **past-due** customers | | **Product activity** | PostHog, Mixpanel, or Outlit SDKs | Inactivity, usage drops, metric deviations from baseline | | **Conversations & support** | Gmail, Slack, Granola/Gong/Fireflies, Pylon | Churn language, escalations, stakeholder changes, call sentiment | A Slack destination is optional but recommended; a shared channel helps the team triage cases. At minimum: Stripe plus one product-usage source. See [Connect your data](/getting-started/connect-data) and the [Stripe](/integrations/stripe) and [PostHog](/integrations/posthog) guides. Open **Responsibilities → Churn**. The status card should read **Monitoring is active** with a customer count. "Reduced coverage" lists exactly what's missing and links to the fix. **Settings → Destinations** → add a Slack channel (requires the Slack connection) and set it as default. Skip this and notifications arrive by email instead — the workspace admin, or the customer owner for case delivery when an owner is set. ## What a case looks like A fictional-but-typical case, for a customer we'll call **Fernline** (`fernline.example`, paying, \~\$24k ARR): 1. Fernline's admin user stops logging in — a 14-day inactivity signal fires, then their only champion's email replies slow. 2. The Churn responsibility investigates: pulls usage, recent tickets, email history, billing state. 3. A case opens in **Attention**: priority **High**, title like *"Engagement dropped after champion went quiet."* 4. The case story shows what changed, with evidence linked to the actual usage series and email thread. ## Working the case Read the assessment top to bottom. Findings carry evidence — excerpts and, where a link exists, a path back to the source (the Slack thread, the ticket, the usage chart). Check it before trusting a surprising conclusion. Where Outlit proposes one, you'll see a primary approach — for Fernline, something like *re-engage the champion and loop in the admin's manager* — possibly with alternatives. Select the one that fits what you know, or take no approach. Each approach is a set of steps: recommendations for you, and any executable actions. If the plan includes outreach, Outlit drafts the Gmail follow-up. Open **Review email**, edit the draft, approve — only then does it send. Nothing goes out unreviewed. The card in Attention shows approach progress (*2 of 4 steps*). Steps are marked off as work lands. When the situation's handled, **Mark resolved**. If Fernline was only under a watch, that watch can close when the concern passes. Resolve the case when your team has handled it. ## What churn watches are Under **Responsibilities → Churn → Overview**, the **Watching** list shows customers Outlit flagged for a reason — declining usage trend, unresolved ticket — but that haven't crossed into a case. Each entry shows the reason and a confidence level. Think of watches as the layer between "fine" and "a case": where monitoring keeps checking before it asks for a person. ## Troubleshooting Check the monitoring status card first — "not running yet" means no paying/past-due customers are in scope (usually billing hasn't synced). Then confirm a product-activity source is connected: billing alone finds billing problems, not usage declines. Check **Settings → Destinations** for a default Slack channel. Without one, notifications go by email to the workspace admin — or the customer owner for case delivery when the account has an owner set. If neither path reaches you, tell your Outlit contact. That happens — monitoring trades on evidence, not certainty. Resolve it with a note; the resolution teaches future review. If it keeps happening on a segment, tell your Outlit contact — signal sensitivity is tuned with you, not self-serve. The source record may have been deleted, or your source-tool access may not cover it. Review the captured evidence and available source metadata in Outlit; a quote or timestamp is shown only when present and accessible. ## Next steps Queue mechanics and the evidence model The flip side: growth inside your base # Find expansion opportunities Source: https://docs.outlit.ai/guides/expansion Use renewal briefs, customer profiles, and external signals to find supported growth opportunities. Outlit brings expansion evidence into the customer work you already review. Look for opportunities in renewal briefs, usage on customer profiles, and external signals attached to accounts. **Attention** contains churn and renewal items; expansion evidence informs those items rather than creating a separate queue. ## Review renewal opportunities An approved renewal brief includes an **Opportunities** section when the evidence supports an upsell path, seat growth, or plan upgrade. Open the renewal from **Responsibilities → Renewals** or **Attention**, read the cited evidence, and choose an approach that fits what you know. Any drafted outreach still needs your review and approval before it sends. [Work a renewal](/guides/renewals) covers the brief, approach, and outcome controls. Renewals is enabled per workspace during onboarding; see [Responsibilities](/product/responsibilities) if it is missing from your sidebar. ## Check customer usage and context On the customer profile, review feature and credit usage where billing or Autumn data is connected. The relationship summary, activity timeline, and active watches add context. Sustained usage growth can also appear as evidence on a monitored case when it changes the account assessment. External signals can add context from public sources: funding, hiring for relevant roles, launches, market expansion, technology changes, or a contact discussing a problem you solve. Funding, hiring, and launches can inform work already underway; a technology change or competitor mention can also raise churn concern. External signals are available only in enabled workspaces. Ask your Outlit contact about coverage for your accounts. ## Act on the evidence 1. **Find the opportunity** in a renewal brief, profile, or external signal. 2. **Check the source.** Follow the evidence link where one exists and confirm the signal applies to this account before outreach. 3. **Choose an approach** on the renewal if Outlit proposes one. Review any email draft before approving it. 4. **Record the outcome** on the renewal cycle by linking the related deal or setting it manually. For example, **Aster Labs** (`asterlabs.example`) renews in 45 days. Its brief cites two new data engineering roles and a self-serve launch as reasons to discuss a Teams upgrade. The team can verify those sources, confirm the new stakeholder, and review a proposed follow-up before sending it. ## If evidence is missing An empty **Opportunities** section means the brief did not find a supported growth signal. Check the customer's **Coverage** tab for connected billing, product, and conversation sources. If expected external signals are missing, ask your Outlit contact whether that coverage is enabled for your workspace. ## Next steps Review briefs and record outcomes. Check usage, relationships, and source coverage. # Work a renewal Source: https://docs.outlit.ai/guides/renewals The renewal portfolio, agent-prepared briefs, and working a cycle from tracked date to recorded outcome. Outlit's Renewals responsibility tracks every renewal cycle it can see — from CRM opportunities or manual entries — prepares an evidence-backed brief before each date, and refreshes that brief as new customer activity arrives. Your work is reviewing briefs, steering the approach, and recording outcomes. If **Renewals** is missing from your sidebar, see [Responsibilities](/product/responsibilities) for workspace availability. Customer profiles can still show renewal dates. ## Prepare renewal data | Prerequisite | Why | | - | - | | **CRM connected** (Salesforce, HubSpot, or Attio) with pipeline mapping finished | Renewal opportunities become cycles automatically — deduplicated against the CRM record; the mapping covers renewal stages, won/lost outcomes, and the field carrying the renewal date | | **Billing connected** (Stripe) | Billing status on the account; billing-confirmed outcomes | | **Conversations connected** (Gmail, Slack, call recorders) | Stakeholder, sentiment, and procurement evidence in the brief | | *or* **manual cycles** | No CRM? Create cycles directly on the customer — see controls below | ## The portfolio **Responsibilities → Renewals** lists upcoming and overdue cycles — the planning view for the whole renewal book: dates, values, readiness (**On track**, **Needs attention**, **At risk**, **Unknown**), and brief state. Renewals that need a person — a brief awaiting review, a milestone reached, an unresolved risk — also appear in **Attention**, which is where day-to-day review happens. ## A renewal, start to finish Fictional example — **Northwind Retail** (`northwindretail.example`), \$36k ARR, renewing in 40 days: A renewal opportunity in your CRM becomes a cycle in the portfolio — or you create it manually. A cycle carries a renewal date (CRM-sourced dates stay distinguishable from manual overrides), an optional **notice deadline** — the last day to cancel without auto-renewing — and an outcome once it resolves. Before the date, Outlit investigates the account and prepares a renewal brief — checked by Outlit before it's published as the cycle's **current approved brief**. When a new brief is approved, it supersedes the previous one; earlier versions stay under **Previous assessments** so you can see how the read has moved. When the renewal needs a person — a milestone reached or new risk — it lands in **Attention** as a **Renewal** item with the date front and center. A renewal attention item Open the renewal — from Attention or the portfolio — for the full workspace. The approved brief covers: * *Executive synthesis* — the renewal in a paragraph * *Value and adoption* — usage and value evidence * *Stakeholders and sentiment* — who's involved and how they lean * *Commercial and procurement* — contract, terms, and buying-process context * *Risks*, *Opportunities*, *Unknowns* — keyed findings with evidence * *Key evidence* — the source records the brief rests on, with provenance For Northwind, the brief might flag that the champion changed roles in September and procurement started a security review — findings carry their supporting evidence. A linked churn case appears alongside when the account has one open, and a **prior-deal comparison** shows how this renewal compares to the customer's previous deal where history exists. Below the brief, check what Outlit proposes — for example, *reconfirm the new budget owner before the notice deadline* — with steps marked as recommendations or actions, where a proposal exists. Pick it, pick an alternative, or switch later if the next call changes the picture. The page also shows **freshness** — when a brief is scheduled, being prepared, may be stale, or delayed — and you can force a refresh with **Request brief now**. Any email in the plan is drafted for your review — edit and approve before it sends via Gmail. See [Actions and approvals](/product/actions-and-approvals). When the deal resolves: link the closing CRM opportunity, or set **Renewed** / **Not renewed** manually. Outcome sources — CRM, manual, or billing-confirmed — are kept distinct so the record shows how you know. ## The controls you have mid-cycle Workspace members with customer-management permission can: * **Request brief now** — refresh the brief on demand (the freshness label shows when the last one was prepared) * **Set the notice deadline** — the auto-renew cutoff, separately from the renewal date * **Override the renewal date** — correct a CRM date in Outlit without touching the CRM; the source date stays visible and your override wins for planning and milestones * **Create or edit a manual cycle** — for renewals that never lived in the CRM * **Void a cycle** — for a renewal that isn't real (duplicate, cancelled contract); voided cycles are excluded, not deleted * **Resolve attention** — close the item with a note when no further work is needed New customer calls trigger a fresh look at the approach automatically — one recheck after each new call, no request needed. After a call syncs (Granola, Gong, Fireflies, or a logged meeting), review the updated approach before you approve any outbound step. ## Troubleshooting Confirm a CRM is connected and its pipeline mapping is finished — renewal stages, won/lost outcomes, and the renewal-date field. If mapping is done and the list is still empty, check that the deals actually carry valid dates in the mapped field. Or create a manual cycle on any customer to prove the loop end to end. For CRM renewals, the opportunity must exist in the synced CRM, map to a renewal stage, and carry a valid value in the mapped renewal-date field — check the CRM connection's pipeline mapping. For anything outside the CRM, create a manual cycle on the customer. Use the renewal date override on the cycle — the CRM source date stays visible, and your override wins for planning and milestones. Check the freshness label at the top of the detail page, then **Request brief now**. Thin briefs usually mean thin sources — the customer's **Coverage** tab shows which systems this account actually has data from. Check whether the CRM opportunity's closed stage maps correctly, then link the outcome opportunity or set the outcome manually. ## Next steps Approaches, approvals, and what executes Growth signals inside renewal work # Know which customers need attention Source: https://docs.outlit.ai/index Connect your customer data. Review churn risks, renewals, and expansion opportunities with the evidence to act. ## How Outlit works Connect the systems that hold your customer context: CRM, billing, product usage, support, and conversations. The Outlit team helps you set up your workspace and monitoring. Outlit connects records into customer profiles and surfaces the accounts that need attention now, with the evidence behind each finding. An item may include a proposed approach. Choose how to proceed, and review any email draft before Outlit sends it. See [actions and approvals](/product/actions-and-approvals). ## For customer success teams Connect data and see your first attention items. What Outlit flags, and the evidence behind each item. Review proposed approaches and approve email drafts before they send. Spot declining usage and quiet champions before renewal. Surface accounts that are ready to grow. Review renewal dates, account evidence, and next steps. ## For developers and agents Connect through the CLI, MCP server, or agent skills. Trace first visits through conversion with the Outlit Browser SDK, then add product events. REST endpoints and the canonical OpenAPI contract. # Salesforce, HubSpot and Attio Source: https://docs.outlit.ai/integrations/crm Connect your CRM for accounts, contacts, opportunities, and renewal cycles — including stage mapping. Your CRM supplies the commercial spine: accounts, contacts, opportunities, owners — and when the Renewals responsibility is enabled, the renewal opportunities that become tracked renewal cycles. ## Connect From **Integrations**, pick your CRM — **Salesforce**, **HubSpot**, or **Attio**. All three connect through OAuth: approve access, return to Outlit, and the initial import begins. | CRM | Notes | | - | - | | **Salesforce** | Use your existing pipeline configuration; map its renewal stages below. | | **HubSpot** | Map pipelines and deal stages after connecting. | | **Attio** | Lists and objects map to accounts and opportunities. | Connecting the CRM is an admin task. OAuth scopes are read-oriented — Outlit reads accounts, opportunities, and contacts; it does not write records back to your CRM. ## Map your renewal pipeline After OAuth completes, the connect flow asks you to configure each pipeline: * which **open stages** represent renewal deals, * which closed stages count as **won** vs **lost** renewal outcomes, * the **renewal date field** on the deal — the field Outlit reads as the cycle's date. This mapping is what turns CRM opportunities into tracked renewal cycles with correct dates and outcomes. Skipping it — or mapping a field that holds no valid dates — leaves the CRM connected but renewals untracked, a common reason the Renewals portfolio is empty. ## What flows in | Data | Where it appears | | - | - | | Accounts | Customer profiles; merged with billing/product identities | | Contacts | Customer → People tab; stakeholders in renewal briefs | | Opportunities | Renewal cycles; prior-deal comparison on renewal detail | | Owners | Customer ownership (drives who members see and who gets notified) | ## Renewal outcomes When a renewal closes, the outcome can be linked to the CRM opportunity that closed it — the renewal detail keeps the outcome source distinct (CRM-linked vs manual vs billing-confirmed). Closed-stage mapping is what lets won/lost opportunities resolve cycles cleanly. ## Troubleshooting Check the CRM connection's pipeline mapping — renewal stages, won/lost outcomes, and the renewal-date field all need to be set. If they're set and still empty, the deals may be missing or carrying invalid values in the mapped date field. Accounts match by domain and identifiers; ambiguous matches wait for confirmation rather than auto-merging. Check the customer's **Coverage** tab for pending matches or identity issues. Ownership typically syncs from the CRM owner field. Fix it in your CRM, or adjust the customer owner directly in Outlit — local owner changes are supported. Stale or non-renewal opportunities can surface during initial mapping. Refine the stage mapping; renewal cycles can also be voided manually if a non-real cycle slipped in. # Gmail and Google Calendar Source: https://docs.outlit.ai/integrations/gmail Per-teammate email and calendar connections — coverage grows as people connect. Gmail and Google Calendar are **personal connections**: each teammate connects their own account, and Outlit sees only the mailboxes and calendars actually connected. Coverage grows as your team connects. ## Who connects it Members can connect their own Gmail and Google Calendar accounts. During onboarding, each invited teammate lands on the connect step; after onboarding, connect anytime from **Integrations → Gmail** or **Google Calendar**. ## Connect OAuth through Google — approve the scopes, return to Outlit. That's the whole setup; there's no workspace-level mapping. ## What it unlocks * **Relationship evidence** — email threads with customer contacts become source evidence on profiles and in case assessments. * **Stakeholder signals** — a champion going quiet, a new procurement contact, sentiment shifts in replies. * **Calendar context** — upcoming customer meetings, recurring cadence gaps. Connecting your own Gmail lets approved follow-ups send from your mailbox. Coverage is additive: if a customer's champion emails three people on your team, Outlit sees the threads for whoever connected. A "no email coverage" note on a case usually means the right teammate hasn't connected yet — not that email is broken. ## Troubleshooting If the threads exist in a teammate's mailbox, they need to connect theirs. If the threads are in yours, give the initial sync time to index. Drafted emails send through the approver's connected Gmail — connect your own mailbox first. Depending on your Google Workspace admin settings, connecting a third-party app may need admin approval — your Google admin can allowlist Outlit. # Integrations overview Source: https://docs.outlit.ai/integrations/overview What to connect, who connects it, and how monitoring readiness reflects coverage. Integrations are how Outlit sees your customers. Connect sources once — Outlit continuously monitors them and surfaces churn and renewal items in Attention. Expansion evidence appears on customer profiles and renewal briefs. The Outlit Browser SDK adds the website journey before a person becomes a known contact; product analytics integrations import product activity into Outlit. Open **Integrations** in the sidebar to browse the catalog, connect, and check each connection's status. ## What to connect, roughly in order | Source | Connects | Unlocks | | - | - | - | | **Stripe** | Billing status, MRR, subscriptions | The set of monitored customers; billing signals | | **PostHog / Mixpanel / Outlit SDKs** | Product activity | Inactivity and usage-trend signals | | **Outlit Browser SDK** | First landing page, referrer and UTM data, anonymous site visits | Connect the path to signup with later activity after browser identification | | **Salesforce / HubSpot / Attio** | Accounts, opportunities, contacts, renewals | Ownership, renewal cycles, commercial context | | **Gmail / Google Calendar** | Your team's mailboxes and calendars | Relationship evidence, stakeholder changes | | **Slack** | Channels | Conversation signals + a destination for notifications | | **Pylon** | Support tickets | Escalation and unresolved-issue signals | | **Gong / Fireflies / Granola** | Call recordings and notes | Sentiment, commitments, competitive mentions | | **Clerk / Supabase Auth** | Auth users | Sign-in activity and identity matching | | **Autumn** | Feature/credit usage | Entitlement and consumption evidence | A billing source and a product-activity source provide important monitoring inputs — but whether monitoring reports **active** depends on coverage *and* the responsibility being enabled on your workspace. The Churn status card is the source of truth for readiness and what's still missing. ## Who connects what * **Admins** connect workspace-wide sources — billing, CRM, Slack, product analytics, support. * **Members** can connect their own accounts — Gmail and personal Granola keys are per-person by design. * Each teammate connecting Gmail adds coverage: Outlit only sees the mailboxes actually connected. ## Connection methods | Method | Used by | What it looks like | | - | - | - | | **OAuth** | Stripe, CRMs, Gmail, Google Calendar, Slack, Gong | Redirect to the provider, approve scopes, return | | **API key or service account** | PostHog, Mixpanel (service-account credentials), Fireflies, Granola, Pylon, Autumn, Clerk, Supabase, Stripe (restricted key) | Paste credentials in the setup form | | **SDK install** | Outlit Browser SDK and Outlit server SDKs | An engineer adds the browser snippet or server package — see [Tracking](/tracking/quickstart) | ## Setup tasks Some sources aren't done after credentials — the connect flow asks for mapping: * **CRM integrations** — map the pipeline: which open stages are renewal deals, which closed stages count as won/lost outcomes, and which deal field carries the renewal date. * **Mixpanel** — confirm the account-property mapping that joins events to customer accounts before import starts. * **Stripe** — webhook setup may be a separate step from credentials. * **Slack** — choose channels and set a notification destination (Settings → Destinations). ## Status and troubleshooting The Integrations page shows each connection's state — connected, syncing, error, or stale. If a connection errors or goes stale, it shows up in churn monitoring's **Reduced coverage** with a link back here. Historical imports run in the background after connection, and completion time varies by source and history. When data is missing, check the connection's sync status on the Integrations page and the customer's Coverage tab. Usually an expired token or revoked key. Reconnect the source — Outlit asks you to reconnect, and reconnecting doesn't lose previously imported data. Workspace-wide sources need an admin. Members see the catalog but can only connect personal-scope accounts. ## Source guides Billing — OAuth or restricted key Product analytics — API key + project mapping Conversation signals + notification destination Per-teammate email connection Salesforce, HubSpot, Attio — accounts, deals, renewals Pylon, Gong, Fireflies, Granola, Google Calendar Other catalog entries (Mixpanel, Autumn, Clerk, Supabase Auth) use their setup flow — paste credentials or start OAuth, then finish any mapping the form asks for. [Outlit SDKs](/tracking/how-it-works) are installed in your code. # PostHog Source: https://docs.outlit.ai/integrations/posthog Connect product analytics: API key, project, and account mapping. The Outlit PostHog integration imports product activity into customer profiles — usage signals for inactivity detection, trend watches, and case evidence. Add the [Outlit Browser SDK](/tracking/browser/script) to your marketing site and your app to see how someone first arrived and browsed your website before signup. It records the landing page, referrer, available UTM parameters, and anonymous site activity. Browser identification links that history to the contact, connecting discovery with later product activity and conversion. The SDK's visitor ID is shared across subdomains of one root domain; see [anonymous visitor ID](/concepts/identity-resolution#anonymous-visitor-id). ## Connect From **Integrations → PostHog**, the setup form asks for: | Field | What it is | | - | - | | **API key** | A PostHog API key (`phx_…`) with scopes `project:read`, `query:read`, and `hog_function:write` — the write scope backs Outlit's managed realtime destination | | **PostHog region** | **US** or **EU** — pick whichever hosts your PostHog Cloud project | | **Project ID** | The numeric project holding your product events | After credentials, confirm the account property that joins PostHog events to customers before import starts. Choose the domain or account ID property your project uses. A wrong mapping can leave product activity unattributed. After the first sync, check a customer profile to confirm the events attached to the right account. ## After connecting * Event history imports; product-activity evidence starts appearing on customer profiles and in case assessments. * Churn monitoring's coverage improves — "no product source" clears once PostHog is healthy. * Distinct users and usage patterns feed the inactivity and trend signals. ## Troubleshooting Check the **account property** first — events can arrive without appearing on a customer profile. Then confirm the project ID matches the project your product actually reports to. Verify the API key has the required scopes (`project:read`, `query:read`, `hog_function:write`) for that project and the region matches where your PostHog Cloud project lives — the flow supports the US and EU cloud regions. The connection may have gone stale — check Integrations for the error state. A rotated PostHog key needs re-entering in Outlit. # Slack Source: https://docs.outlit.ai/integrations/slack Connect Slack for conversation signals and notification destinations. Slack does double duty in Outlit: shared channels with customers (or internal customer threads) become conversation evidence, and Slack is where attention notifications deliver. ## Connect From **Integrations → Slack**, OAuth takes you through Slack's approval screen and back. After connecting, the setup asks you to **choose which channels Outlit can see** — connect the channels where customer work happens (shared channels, account channels), not the whole workspace by default. ## Notifications Slack is the primary notification destination for the Churn responsibility: 1. Open **Settings → Destinations**. 2. Add a Slack channel from the connected workspace. 3. Set it as the **default destination** — new churn cases notify there. Without a Slack destination, notifications fall back to **email** — to the workspace admin or the customer owner — so a destination isn't strictly required, but a shared channel is where cases get triaged fastest. ## What Slack evidence looks like Channel messages tied to a customer appear as evidence in case assessments and profile activity — with links back to the thread. Churn language, unresolved complaints, and stakeholder changes in those channels can help explain a risk. Only connected channels are visible to Outlit. If a customer's shared channel exists but produces no evidence, it probably wasn't selected during setup — revisit the Slack connection's channel list. ## Troubleshooting Check **Settings → Destinations** — is a Slack destination added and set as default? Also check whether the case actually generated a notification (some case activity notifies; quiet status updates don't spam the channel). The channel wasn't selected in the Slack connection's channel list, or the channel was created after you connected Slack — re-open the Slack connection and extend channel access. Reconnect from Integrations. Workspace admin approval in Slack may be required depending on your Slack admin settings. # Stripe Source: https://docs.outlit.ai/integrations/stripe Connect billing: OAuth or a restricted key, plus webhook setup. Stripe is the billing source that defines which customers Outlit monitors — churn monitoring covers **paying** and **past-due** customers, and Stripe supplies MRR, subscription state, and billing-confirmed renewal outcomes. ## Connect Outlit supports two Stripe connection methods. The setup form shows the method enabled for your workspace: | Method | Best for | What happens | | - | - | - | | **OAuth** (Stripe App) | Most workspaces | Redirect to Stripe, approve access, return to Outlit | | **Restricted key** | Environments where the OAuth app isn't available | Paste a key starting with `rk_` | Use a restricted key, not a secret key — it scopes Outlit to read access on customers, subscriptions, invoices, and the related objects it needs. Keep the key rotatable; if you revoke it in Stripe, the connection shows an error until you reconnect. If you need another method, ask your Outlit contact. ## After connecting 1. **Historical sync runs in the background.** Customers, subscriptions, and invoices import; MRR appears on profiles as data lands. 2. **Finish webhook setup if prompted.** Credential connect and event webhooks can be separate steps — the connection's detail panel shows whether webhooks are live. 3. **Billing customers match to your accounts** — see [identity notes](#customers-that-dont-match) below if a Stripe customer lands on the wrong profile. ## What billing enables * The monitored customer set (paying + past-due) for the Churn responsibility * MRR and plan context on customer profiles and Attention cards * Billing signals — delinquency, subscription changes — inside churn cases * Billing-confirmed outcomes on renewal cycles ## Customers that don't match Stripe customers are matched to Outlit customers by domain, email, and stored identifiers. Where a match is ambiguous, the link waits for confirmation rather than guessing — unmatched or mismatched billing customers surface on the customer's **Coverage** tab or as an identity issue. Confirming a match is a deliberate click; see [Customers](/product/customers). ## Troubleshooting The Stripe connection errored or never finished — open Integrations and check its status. Reconnect, then check **Responsibilities → Churn** for current coverage. The Stripe customer isn't matched to the Outlit customer yet — check the Coverage tab for a pending or suggested match. Webhook setup is separate from credentials. Finish it from the Stripe connection detail, or ask your Outlit contact if the workspace was provisioned with guided setup. # Support and meetings Source: https://docs.outlit.ai/integrations/support-and-meetings Pylon, Gong, Fireflies, Granola, Google Calendar — tickets and call evidence. Support tickets and recorded conversations carry some of the strongest churn and expansion evidence — escalations, commitments, sentiment, competitor mentions. Each source connects differently. ## Pylon — support tickets | Detail | Description | | - | - | | **Credentials** | API token from your Pylon workspace | | **Extra step** | Webhook setup so new/updated tickets stream in — the connection detail shows the webhook state | | **What it unlocks** | Open/unresolved ticket signals, escalation evidence on churn cases and renewal briefs | Paste the token in **Integrations → Pylon**, then finish the webhook step if prompted — without it, tickets don't arrive in real time. ## Gong — call recordings | Detail | Description | | - | - | | **Credentials** | OAuth | | **Extra step** | Webhook configuration for recording delivery | | **What it unlocks** | Call transcripts and metadata as evidence — sentiment, commitments, competitive context | OAuth from **Integrations → Gong**, then complete the webhook step shown in the connection detail. ## Fireflies — meeting notes | Detail | Description | | - | - | | **Credentials** | API key from Fireflies | | **What it unlocks** | Meeting transcripts as conversation evidence | Paste the key in **Integrations → Fireflies**. No mapping step. ## Granola — meeting notes | Detail | Description | | - | - | | **Credentials** | Personal API key **or** Granola Enterprise API key | | **Scope** | Personal keys are per-teammate; an Enterprise key covers the team space | | **What it unlocks** | Meeting notes as evidence — including private notes when each teammate connects their own key | Personal Granola keys connect per person (like Gmail). If your team uses Granola Enterprise, one admin connection covers the workspace. ## Google Calendar | Detail | Description | | - | - | | **Credentials** | OAuth | | **Scope** | Per teammate | | **What it unlocks** | Meeting cadence and upcoming customer meetings as context | Covered alongside Gmail — see [Gmail and Google Calendar](/integrations/gmail). ## Troubleshooting For Gong and Pylon, check the webhook step — OAuth/token connect and webhook delivery are separate, and both show their state on the connection detail. For Granola personal keys, the teammate who took the notes must connect their own key. Pylon's webhook may have lapsed — the connection detail shows webhook health. Re-saving the token doesn't restore webhooks; finish the webhook step. Meeting evidence attaches by participant/domain matching. If participants aren't recognized contacts yet, confirm the customer's domain and contacts are set — see [Customers](/product/customers). # Actions and approvals Source: https://docs.outlit.ai/product/actions-and-approvals How Outlit proposes approaches, what you approve, and what actually executes. Outlit investigates customer situations and prepares approaches. You choose a plan and approve each outbound email before it sends. An **approach** contains **steps** for your team or actions Outlit can carry out after approval. ## Approaches: picking the plan When a churn case or renewal item is assessed, Outlit may propose a **primary approach** — sometimes with alternatives — each a short plan of steps. Not every item carries a proposal: where none is proposed yet, the item says so and the assessment and evidence still stand on their own. On the item's detail page you: * **Choose an approach** to start work (or pick an alternative), or * Take **no approach** — the item stays open without a plan. Once an approach is selected, its steps run as tracked work — the card in Attention shows the next step and progress (*3 of 5 steps*). When all steps complete the item shows **Steps complete**; it stays open until you resolve it. Approaches can be replaced. If a later assessment supports a different plan — for example after a new customer call — you can switch; the history keeps the full record of what was tried. ## Steps: recommendations vs executable actions Each step in an approach is one of two kinds, labeled on the step: | Kind | What it is | | - | - | | **Recommendation** | A suggested action for a person — "call the economic buyer," "offer a quarterly plan." Outlit tracks it; a human does it. | | **Action** | An executable step Outlit can perform — but only after explicit approval of the prepared draft. | ## Review and send email The available action is a reviewed customer email. The **Take action** control opens the customer workspace with follow-up context; it does not send anything. | Action | What happens | Approval | | - | - | - | | **Send customer email** | Outlit prepares a Gmail draft (subject, body, recipient from the customer's known contacts). You review it on a dedicated review screen, edit it if needed, and approve. Only then is it sent through your connected Gmail. | Required | Outlit sends email only after you review and approve its draft. It does not write to your CRM or change subscriptions through an approach. Prepared actions can expire, so an old draft does not send late. ### The email review flow 1. The drafted email appears on the attention item — and in Slack/email notifications where configured — with a **Review email** link. 2. The review screen shows the prepared draft and the evidence it was based on. You can edit subject, body, and recipient. 3. Approving sends it via your Gmail connection; the intent is recorded with its outcome on the item. ## Resolving work * **Churn cases** — **Mark resolved** closes the case (with a confirm), **Reopen** restores it. A case can also carry a billing-recovery callout when the risk is a failed payment. * **Renewal items** — resolving records the attention disposition; the cycle itself closes when an outcome (renewed / not renewed) is recorded. See [Work a renewal](/guides/renewals). ## Permissions Approving approaches and executable actions requires customer-management permission — in practice, workspace **admins** and members granted that scope. Members with read access see items and evidence without the action controls. See [Workspace access](/product/workspace-access). ## Next steps Where approvals land and how items flow A case from signal to resolved # Attention Source: https://docs.outlit.ai/product/attention The queue of customer work that needs a person — how items arrive, how to filter and triage them, and what resolving means. **Attention** — the app's home page — is the daily handoff from Outlit to your team. Everything here is a specific customer situation that monitoring has already investigated and flagged for a human: churn cases and renewal items in one queue. The rule of the page is *only work that requires human attention*. Routine monitoring output, healthy customers, and informational signals don't appear here — they live on the customer profile and the responsibilities pages. ## The queue The queue defaults to **Open** items. Each card shows the customer, the kind of item (**Churn risk** or **Renewal**), a priority (**Urgent** / **High** / **Normal**), a plain-language title, account context (ARR and revenue percentile when billing data exists, renewal date for renewal items), approach progress, and the account owner. ### Finding the right items | Control | Options | | - | - | | Search | Customer name, domain, or item title | | Priority | Urgent / High / Normal | | Responsibility | Churn / Renewal | | Owners | Filter to a specific teammate, or unassigned | | Approach status | All approaches / No approach / Awaiting review / In progress / Steps complete | | Sort | Recommended (default), Highest ARR, Recently updated | Filters combine, so "Churn + Awaiting review + Highest ARR" is a quick daily pass for the most expensive unworked risks. A kanban-style board view of the queue is in limited availability — your workspace may show a list only. ## The lifecycle Items have two states: * **Open** — the default queue. Everything that still needs a person. * **Resolved** — the archive at `?status=resolved`. Resolving is never destructive; resolved items keep their full history and can be **reopened** from the detail page. "Steps complete" is progress on an approach, not resolution — an item stays open until a person marks it resolved. ## The evidence model Findings inside an item are backed by **evidence**: captured excerpts and details from source records — a Slack thread, a support ticket, an email, a call transcript, a usage series, a CRM field. Each piece of evidence carries its source type and when it was observed, and links out to the record in the source system where a link exists. A citation without a link still shows what was captured; the source record may not be directly readable from here. The distinction that matters: * **Source evidence** is something a connected tool recorded — "the customer's admin posted this message on Tuesday." * **Inference** is what Outlit concluded from evidence — "engagement is declining." Assessments cite the evidence they rest on; when evidence is thin or a source record can't be read back, the item says so rather than papering over it. When a recommendation looks surprising, read the evidence behind it before acting — it's the fastest way to calibrate how much weight to give the assessment. ## What items become An item is where review happens, not where work ends: * **Approach** — you pick the plan (or none). Approaches carry steps; some steps are recommendations for your team, some are executable actions Outlit performs after approval. See [Actions and approvals](/product/actions-and-approvals). * **Resolution** — when the situation is handled, **Mark resolved** moves it to the archive. For renewal items, resolving closes the attention item. Record **Renewed** or **Not renewed** separately on the renewal cycle. ## Troubleshooting Rule out the view first: the queue defaults to **Open** (check the Resolved view), and an active filter or search produces a "No items match these filters" state. Members also only see items on customers they own or that are shared with them. Then check the empty-state message — it distinguishes "nothing needs attention" from missing data — and **Responsibilities → Churn** for the monitoring status card. Evidence links point at source systems your workspace connected. If a link fails, the underlying record may have been deleted, or your access in the source tool doesn't cover it — the citation can show captured evidence and available source metadata. Quotes, timestamps, and links appear only when available and permitted by your access. Churn monitoring scopes to paying and past-due customers; renewal items require a renewal cycle. If you're seeing items for accounts you don't consider customers, the billing or CRM mapping is the thing to check. ## Next steps The profiles behind every item What Outlit can execute, and what always waits for you # Customers Source: https://docs.outlit.ai/product/customers How Outlit builds customer profiles — the list, the profile tabs, and where the data comes from. **Customers** is the table of accounts Outlit has resolved from your connected sources. Open any row for the full profile: what the account is worth, what's happening, who the people are, and which sources back what's shown. ## The customer list Columns: **Customer** (name and domain), **ARR**, **Attention** (open items at that account), **Renewal date**, **Owner**, **Last activity**, **Billing Status**. Filters and column choices persist in the URL, so a filtered view is shareable by link. Billing status comes from your billing source (Stripe): None, Trialing, Paying, Past due, or Churned. "Last activity" reflects the most recent interaction Outlit has seen across connected sources. ## The customer profile Three tabs: The account at a glance: * **Stakes strip** — owner, MRR, how long they've been a customer, and the next renewal date. * **Active attention** — any open item at this account, so the profile keeps account context next to urgent work. * **Relationship summary** — Outlit's current read of the account, assembled from evidence across sources. * **Active watches** — churn watches monitoring this account, with the reason for each. * **Recent activity** — the latest signals and interactions across sources. * **Billing and feature usage** — subscription state, credit/feature balances where a billing source like Stripe or Autumn provides them. Which connected systems this customer appears in — CRM record, billing account, product analytics, conversation sources — and any **identity issue** Outlit has flagged (for example, sources that don't cleanly resolve to one account). Coverage is where you verify that an account is actually wired to the data monitoring needs. The contact roster at this account: names, emails, roles, and last activity. Admins and members with customer-management permission can edit a contact's relationship role, job title, and department — useful for marking champions, economic buyers, and detractors so assessments and renewal briefs weight them correctly. ## How customers are formed Outlit groups people into accounts primarily by **email domain**: everyone at `@acme.example` resolves into one Acme customer record. Personal email providers (gmail.com, outlook.com, and similar) are handled differently — each address becomes its own customer rather than merging strangers into a shared "gmail.com" account. Billing IDs, CRM account IDs, and workspace identifiers also link activity to accounts. Most merges happen automatically as evidence accumulates. Suggested merges can be reviewed in the app where enabled; the CLI exposes `outlit customers merge` for deliberate consolidation. See [Identity resolution](/concepts/identity-resolution) for the matching rules and edge cases. ## Evidence and provenance on profiles Facts on a profile aren't free-floating claims. They carry provenance — the source type, when the signal was observed, and the underlying quote or record where available — so "customer asked about annual pricing" points back to the email it came from where a link was captured. Where source material can't be read back (a deleted message, a restricted record), entries are marked rather than silently dropped. ## What profiles feed * **Churn monitoring** uses billing status plus activity and conversation signals to decide which accounts are in scope and when a case is warranted. * **Renewals** use CRM deals and billing dates to build renewal cycles and briefs. * **Attention items** link back here — the profile is the permanent record an item refers to. ## Troubleshooting Check whether their only presence is a personal email — personal addresses become individual customers, not part of a domain account. Otherwise the source that knows them (CRM, billing, product analytics) may not have synced them yet; check the customer's Coverage tab and the integration's sync status. Usually a second domain or an unmatched billing record. Merge suggestions surface in the app where the identity review feature is enabled, and deliberate merges are available through the CLI. Identity edge cases are covered in [Identity resolution](/concepts/identity-resolution). Billing status mirrors your billing source. If it lags, check the Stripe connection's sync health on **Integrations**. ## Next steps How account issues surface for review How people and accounts get matched # Responsibilities Source: https://docs.outlit.ai/product/responsibilities The work Outlit owns for you — Churn monitoring and Renewals — and how to read its status. A **responsibility** is customer work Outlit monitors continuously. Your Outlit team enables each responsibility during onboarding: | Responsibility | What it does | | - | - | | **Churn** | Watches every eligible customer for retention risk — billing failures, usage drops, support escalations, stakeholder changes — and opens a case in Attention when something needs a person | | **Renewals** | Tracks renewal cycles from your CRM (or manual entries), prepares evidence-backed renewal briefs ahead of each date, and keeps them current as calls and signals arrive | If **Responsibilities** or **Renewals** is missing from your sidebar, ask your Outlit contact about your workspace setup. ## Churn monitoring **Responsibilities → Churn** has two tabs, **Overview** and **Signals**. ### Overview * **Monitoring status** — the top card reports the health of the whole loop: * *Monitoring is active* — with the count of customers in scope and when the last investigation completed. * *Monitoring is active with reduced coverage* — something's partially degraded. Each reason links to the fix: connect a billing source, connect product analytics or the Outlit SDK, set a Slack destination for notifications, or review an integration that errored or went stale. * *Monitoring is not running yet* — no paying or past-due customers are in scope (usually billing hasn't synced), or the service is temporarily unavailable. * **Open cases** — churn cases awaiting review, with priority and a one-line summary. * **Watching** — customers under an active watch: not yet a case, but Outlit has noted a reason to keep checking, with a confidence level. Watches can escalate into cases or close quietly when the concern passes. ### Signals The **Signals** tab shows what the Churn responsibility listens to — billing delinquency and subscription changes, product inactivity and usage drops, support escalations, technology-stack changes, competitor and vendor mentions, plus external signals like funding events, hiring, and launches. For each, the tab shows whether that coverage is ready or still needs a source connected — ticket signals need Pylon, for example, and external signals are in limited availability. ### What makes a customer eligible Churn monitoring covers customers whose billing status is **Paying** or **Past due** — which means a connected billing source (Stripe) is what puts accounts in scope. Prospects, free users, and churned accounts don't get cases. ### Where notifications go Case notifications deliver to the Slack destination you configure in **Settings → Destinations**. When no Slack destination is set, notifications fall back to email automatically — to the workspace admin or the customer owner. The reduced-coverage warning also points here when no destination exists. ## Renewals **Responsibilities → Renewals** is the renewal portfolio: every upcoming and overdue renewal cycle Outlit is tracking, with dates, values, readiness, and brief status. Renewal cycles come from your CRM's renewal opportunities, or from cycles you create manually. The renewal workspace — briefs, evidence, milestones, and controls — is covered in [Work a renewal](/guides/renewals). A renewal becomes an attention item when it needs a person: a milestone, an approach decision, or a new risk. Work those items from **Attention**; use the Renewals portfolio to plan ahead. ## Responsibility vs attention These two views serve different jobs: * A **responsibility** is ongoing coverage — the watching, tracking, and drafting Outlit does continuously. * An **attention item** is the handoff — the moment coverage produces something a person must decide. You can read responsibilities pages to understand what Outlit is doing; you only *need* to visit when something lands in Attention or coverage degrades. ## Troubleshooting The status card lists the specific reason and a link to fix it — most often a disconnected billing or product source, or no notification destination. Ask your Outlit contact to check whether Renewals is enabled for your workspace. Confirm their billing status is Paying or Past due on the customer profile, and that your billing source is syncing — eligibility is driven by billing status, not CRM stage. ## Next steps End-to-end: from connected data to a resolved churn case Portfolios, briefs, and renewal controls # Workspace access Source: https://docs.outlit.ai/product/workspace-access Roles, invites, personal vs workspace connections, and which settings each role sees. Your Outlit workspace is shared by your team, with two roles and a mix of workspace-wide and per-person connections. ## Roles | Capability | **Admin** | **Member** | | - | - | - | | See customers | All | Owned or shared with them | | Review attention items and evidence | Yes | On owned or shared customers | | Choose approaches, approve actions, resolve items | Yes | With customer-management permission | | Connect workspace-wide sources (Stripe, CRM, Slack, PostHog…) | Yes | — | | Connect own accounts (Gmail, Granola personal key) | Yes | Yes | | Manage members and invitations | Yes | — | | Manage destinations, API keys, tracking settings | Yes | — | "Member" is the default role for invited teammates. Admins get every permission; members get read access plus the ability to connect their own work accounts, unless they are also granted customer-management permission. The customer-scope split is finer than the role names suggest: members see customers they **own** or that are **shared with them explicitly**, while admin permissions cover the whole book. Sharing a specific customer is the least-privilege way to widen a member's view — no role change needed. ## Inviting teammates * During onboarding, admins get an **invite** step — paste emails, they arrive as members. * Anytime after, use **Settings → Members** to invite, change roles, or remove people. * Invited members land on sign-in and then the connect step of onboarding — personal sources like Gmail connect per person. ## Personal vs workspace connections Some sources are per-person by design: | Connection | Scope | Why | | - | - | - | | **Gmail** | Each user connects their own mailbox | Coverage grows as teammates connect; drafts send from the approver's connection | | **Google Calendar** | Each user connects their own calendar account | Adds evidence from the connected calendars | | **Granola** | Personal API key per teammate, or an Enterprise key covering the team space | Connection scope determines which notes can be imported | | Everything else | Connected once for the workspace | Stripe, CRMs, Slack, PostHog, support tools | Connection ownership and evidence visibility are separate. Access to customer records and source content determines what a teammate can read. See [Gmail and Google Calendar](/integrations/gmail) for how personal mailbox connections affect evidence coverage. ## Settings map **Settings** splits into **Workspace** and **Account**: | Page | Access and management | What it holds | | - | - | - | | General | Everyone | Workspace name and profile | | Members | Everyone can view; admins manage | Invites, roles, removal | | Destinations | Admins | Slack channels and the default destination notifications deliver to | | Website Visitors | Everyone | Browser tracking toggle, public key, enabled-domain allowlist, link to the Visitors page | | Activation | Everyone | The product event that marks a customer activated | | Product usage | Admins | Feature/credit usage configuration | | API keys | Admins | `ok_*` keys for the CLI/MCP — workspace principals with their own grants, one-time reveal, revoke/regenerate | | CLI & MCP | Everyone | Workspace MCP URL (`https://mcp.outlit.ai/w/{your-workspace}/mcp`) and CLI auth | | Plan usage, Billing | Everyone | Usage and billing for your Outlit subscription | | Account → Profile, Security | Everyone | Personal profile and sign-in security | API keys are workspace principals, not personal credentials — they carry only the grants in the preset you pick and don't inherit the creator's permissions. Create them in **Settings → API keys**; the full key is shown once. ## Troubleshooting Members see customers they own or that are shared with them. Share the specific customer with them for the least-privilege fix, set them as the owner on the customer profile, or grant an admin role if they should see the whole book. Workspace-wide sources need an admin. Members can only connect personal-scope sources (Gmail, Granola personal keys). The API keys page is admin-only. Ask a workspace admin to create the key. ## Next steps Connect sources once roles are clear What each source needs and who can connect it # Angular Source: https://docs.outlit.ai/tracking/browser/angular Integrate Outlit tracking with Angular applications. ## Installation ```bash npm theme={null} npm install --save @outlit/browser ``` ```bash pnpm theme={null} pnpm add @outlit/browser ``` ```bash yarn theme={null} yarn add @outlit/browser ``` ```bash bun theme={null} bun add @outlit/browser ``` ## Quick start Initialize Outlit using an `APP_INITIALIZER`: ```typescript theme={null} // src/app/app.config.ts import { ApplicationConfig, APP_INITIALIZER } from '@angular/core' import outlit from '@outlit/browser' import { environment } from '../environments/environment' function initializeOutlit() { return () => { outlit.init({ publicKey: environment.outlitPublicKey, trackPageviews: true, }) } } export const appConfig: ApplicationConfig = { providers: [ { provide: APP_INITIALIZER, useFactory: initializeOutlit, multi: true } ] } ``` Configure your environment: ```typescript theme={null} // src/environments/environment.ts export const environment = { production: false, outlitPublicKey: 'pk_your_public_key_here' } ``` For production, create `environment.prod.ts` with your production key. ## Creating a service Create a service for a shorter setup: ```typescript theme={null} // src/app/services/outlit.service.ts import { Injectable } from '@angular/core' import outlit from '@outlit/browser' @Injectable({ providedIn: 'root' }) export class OutlitService { track(eventName: string, properties?: Record): void { outlit.track(eventName, properties) } identify(email: string, traits?: Record): void { outlit.identify({ email, traits }) } setUser(identity: { email?: string; userId?: string; traits?: Record }): void { outlit.setUser(identity) } clearUser(): void { outlit.clearUser() } getVisitorId(): string | null { return outlit.getInstance().getVisitorId() } } ``` ## Tracking events Inject and use the service in components: ```typescript theme={null} // src/app/components/pricing/pricing.component.ts import { Component } from '@angular/core' import { OutlitService } from '../../services/outlit.service' @Component({ selector: 'app-pricing', templateUrl: './pricing.component.html' }) export class PricingComponent { constructor(private outlit: OutlitService) {} handleCheckout(plan: string): void { this.outlit.track('checkout_started', { plan, amount: 99 }) } } ``` ```html theme={null} ``` ## Identifying users Identify users after authentication: ```typescript theme={null} // src/app/components/login/login.component.ts import { Component } from '@angular/core' import { Router } from '@angular/router' import { OutlitService } from '../../services/outlit.service' import { AuthService } from '../../services/auth.service' @Component({ selector: 'app-login', templateUrl: './login.component.html' }) export class LoginComponent { email = '' password = '' constructor( private auth: AuthService, private outlit: OutlitService, private router: Router ) {} async handleLogin(): Promise { const user = await this.auth.login(this.email, this.password) this.outlit.identify(user.email, { name: user.name, plan: user.plan }) this.router.navigate(['/dashboard']) } } ``` ## Router integration Track navigation with Angular Router: ```typescript theme={null} // src/app/app.config.ts import { ApplicationConfig, APP_INITIALIZER } from '@angular/core' import { provideRouter, Router, NavigationEnd } from '@angular/router' import { OutlitService } from './services/outlit.service' import { routes } from './app.routes' function setupRouterTracking(router: Router, outlit: OutlitService) { return () => { router.events.subscribe((event) => { if (event instanceof NavigationEnd) { outlit.track('pageview', { path: event.url }) } }) } } export const appConfig: ApplicationConfig = { providers: [ provideRouter(routes), { provide: APP_INITIALIZER, useFactory: setupRouterTracking, deps: [Router, OutlitService], multi: true } ] } ``` If you set `trackPageviews: true` during init, pageviews are tracked automatically. The above example is only needed for custom pageview logic. ## Auth guard integration Track authentication events: ```typescript theme={null} // src/app/guards/auth.guard.ts import { inject } from '@angular/core' import { Router, CanActivateFn } from '@angular/router' import { OutlitService } from '../services/outlit.service' import { AuthService } from '../services/auth.service' export const authGuard: CanActivateFn = (route, state) => { const auth = inject(AuthService) const outlit = inject(OutlitService) const router = inject(Router) if (auth.isLoggedIn()) { const user = auth.getUser() outlit.setUser({ email: user.email, userId: user.id, traits: { name: user.name } }) return true } outlit.track('auth_required', { path: state.url }) return router.parseUrl('/login') } ``` ## RxJS integration Track with observables: ```typescript theme={null} // src/app/components/dashboard/dashboard.component.ts import { Component, OnInit } from '@angular/core' import { tap } from 'rxjs/operators' import { OutlitService } from '../../services/outlit.service' import { DashboardService } from '../../services/dashboard.service' @Component({ selector: 'app-dashboard', templateUrl: './dashboard.component.html' }) export class DashboardComponent implements OnInit { data$ = this.dashboard.getData().pipe( tap(() => this.outlit.track('dashboard_loaded')) ) constructor( private dashboard: DashboardService, private outlit: OutlitService ) {} ngOnInit(): void { this.outlit.track('dashboard_viewed') } handleExport(): void { this.outlit.track('data_exported', { format: 'csv' }) } } ``` ## Lifecycle hooks Track component lifecycle events: ```typescript theme={null} // src/app/components/feature/feature.component.ts import { Component, OnInit, OnDestroy } from '@angular/core' import { OutlitService } from '../../services/outlit.service' @Component({ selector: 'app-feature', templateUrl: './feature.component.html' }) export class FeatureComponent implements OnInit, OnDestroy { private startTime: number constructor(private outlit: OutlitService) {} ngOnInit(): void { this.startTime = Date.now() this.outlit.track('feature_viewed', { feature: 'export' }) } ngOnDestroy(): void { const duration = Date.now() - this.startTime this.outlit.track('feature_time', { feature: 'export', duration }) } } ``` ## Interceptor for API tracking Track API calls: ```typescript theme={null} // src/app/interceptors/tracking.interceptor.ts import { HttpInterceptorFn, HttpEventType } from '@angular/common/http' import { tap } from 'rxjs/operators' import outlit from '@outlit/browser' export const trackingInterceptor: HttpInterceptorFn = (req, next) => { return next(req).pipe( tap({ next: (event) => { if (event.type === HttpEventType.Response) { outlit.track('api_success', { url: req.url, method: req.method }) } }, error: (error) => { outlit.track('api_error', { url: req.url, method: req.method, status: error.status }) } }) ) } ``` Register the interceptor: ```typescript theme={null} // src/app/app.config.ts import { ApplicationConfig } from '@angular/core' import { provideHttpClient, withInterceptors } from '@angular/common/http' import { trackingInterceptor } from './interceptors/tracking.interceptor' export const appConfig: ApplicationConfig = { providers: [ provideHttpClient( withInterceptors([trackingInterceptor]) ) ] } ``` ## Activation event Track your value milestone as an ordinary event once it succeeds: ```typescript theme={null} // src/app/components/onboarding/onboarding.component.ts import { Component } from '@angular/core' import { Router } from '@angular/router' import { OutlitService } from '../../services/outlit.service' @Component({ selector: 'app-onboarding', templateUrl: './onboarding.component.html' }) export class OnboardingComponent { constructor( private outlit: OutlitService, private router: Router ) {} handleComplete(): void { this.outlit.track('onboarding_completed', { flow: 'onboarding', step: 'completed' }) this.router.navigate(['/dashboard']) } } ``` Choose this event as your activation event in Outlit. The first matching event records activation once for each eligible resolved person and company, independently. Billing status comes from verified integrations such as Stripe. See [Customer journey](/concepts/customer-journey). ## Consent management The default `autoTrack: "auto"` mode already keeps tracking off for visitors in regions that require opt-in consent (EEA, UK, Switzerland, or unknown); use `autoTrack: false` to wait for an explicit consent signal unless a previous opt-in is stored. Handle user consent: ```typescript theme={null} // src/app/services/consent.service.ts import { Injectable } from '@angular/core' import { BehaviorSubject } from 'rxjs' import outlit from '@outlit/browser' @Injectable({ providedIn: 'root' }) export class ConsentService { private consentGiven = new BehaviorSubject(false) consent$ = this.consentGiven.asObservable() constructor() { const hasConsent = localStorage.getItem('tracking-consent') === 'true' if (hasConsent) { this.acceptTracking() } } acceptTracking(): void { localStorage.setItem('tracking-consent', 'true') outlit.enableTracking() this.consentGiven.next(true) } declineTracking(): void { localStorage.setItem('tracking-consent', 'false') this.consentGiven.next(false) } } ``` ```typescript theme={null} // src/app/components/cookie-banner/cookie-banner.component.ts import { Component } from '@angular/core' import { ConsentService } from '../../services/consent.service' @Component({ selector: 'app-cookie-banner', templateUrl: './cookie-banner.component.html' }) export class CookieBannerComponent { showBanner = true constructor(private consent: ConsentService) { this.consent.consent$.subscribe((hasConsent) => { this.showBanner = !hasConsent }) } accept(): void { this.consent.acceptTracking() } decline(): void { this.consent.declineTracking() } } ``` Initialize without auto-tracking: ```typescript theme={null} // src/app/app.config.ts function initializeOutlit() { return () => { outlit.init({ publicKey: environment.outlitPublicKey, autoTrack: false, // Wait for consent }) } } ``` ## TypeScript support Full TypeScript support is included: ```typescript theme={null} import outlit, { type OutlitOptions, type BrowserTrackOptions } from '@outlit/browser' const options: OutlitOptions = { publicKey: environment.outlitPublicKey, trackPageviews: true, flushInterval: 5000 } outlit.init(options) ``` ## Next steps Track events from your backend Learn how visits and events link to people Full API reference Use Outlit with React # Astro Source: https://docs.outlit.ai/tracking/browser/astro Integrate Outlit tracking with Astro applications. ## Installation ```bash npm theme={null} npm install --save @outlit/browser ``` ```bash pnpm theme={null} pnpm add @outlit/browser ``` ```bash yarn theme={null} yarn add @outlit/browser ``` ```bash bun theme={null} bun add @outlit/browser ``` ## Quick start Add Outlit to your base layout: ```astro theme={null} --- // src/layouts/BaseLayout.astro const PUBLIC_OUTLIT_KEY = import.meta.env.PUBLIC_OUTLIT_KEY --- My Site ``` Add your public key to `.env`: ``` PUBLIC_OUTLIT_KEY=pk_your_public_key_here ``` ## Alternative: inline script Use an inline script if you prefer an inline setup: ```astro theme={null} // src/layouts/BaseLayout.astro My Site ``` ## Tracking events Track events in Astro components: ```astro theme={null} // src/components/PricingButton.astro ``` ### With client directives Use client directives for interactive components: ```astro theme={null} // src/pages/index.astro import PricingButton from '../components/PricingButton.jsx' ``` ## Framework islands ### React island ```jsx theme={null} // src/components/PricingButton.jsx import { useEffect } from 'react' import outlit from '@outlit/browser' export default function PricingButton() { function handleClick() { outlit.track('pricing_clicked', { plan: 'pro' }) } return ( ) } ``` ### Vue island ```vue theme={null} ``` ### Svelte island ```svelte theme={null} ``` ## Identifying users Identify users after authentication: ```astro theme={null} // src/components/LoginForm.astro
``` ## Auth state management For apps with authentication, use `setUser()` and `clearUser()` to sync user identity: ```astro theme={null} // src/components/AuthObserver.astro ``` Using `setUser()` ensures the user identity persists across page navigations and is properly set when users return to your site already logged in. ## View transitions Track navigation with Astro's View Transitions: ```astro theme={null} // src/layouts/BaseLayout.astro import { ViewTransitions } from 'astro:transitions' ``` ## API routes Track events from API endpoints: ```typescript theme={null} // src/pages/api/checkout.ts import type { APIRoute } from 'astro' import { Outlit } from '@outlit/node' const outlit = new Outlit({ publicKey: import.meta.env.OUTLIT_KEY }) export const POST: APIRoute = async ({ request }) => { const body = await request.json() outlit.track({ userId: body.userId, eventName: 'checkout_completed', properties: { amount: body.amount, plan: body.plan } }) await outlit.flush() return new Response(JSON.stringify({ success: true }), { status: 200, headers: { 'Content-Type': 'application/json' } }) } ``` For server-side tracking, see the [Node.js integration guide](/tracking/server/nodejs). ## SSR pages For SSR pages, ensure tracking runs client-side: ```astro theme={null} // src/pages/products/[id].astro export const prerender = false // SSR mode const { id } = Astro.params const product = await fetchProduct(id)

{product.name}

``` ## Content collections Track interactions with content: ```astro theme={null} // src/pages/blog/[...slug].astro import { getEntry } from 'astro:content' const entry = await getEntry('blog', Astro.params.slug)

{entry.data.title}

``` ## Activation event Track your value milestone as an ordinary event once it succeeds: ```astro theme={null} // src/components/OnboardingComplete.astro ``` Choose this event as your activation event in Outlit. The first matching event records activation once for each eligible resolved person and company, independently. Billing status comes from verified integrations such as Stripe. See [Customer journey](/concepts/customer-journey). ## Consent management The default `autoTrack: "auto"` mode already keeps tracking off for visitors in regions that require opt-in consent (EEA, UK, Switzerland, or unknown); the example below uses `autoTrack: false` to wait for an explicit consent signal unless a previous opt-in is stored. Handle user consent: ```astro theme={null} // src/components/CookieBanner.astro ``` ## Environment variables Configure environment variables: ```bash theme={null} # .env # Public key (used in browser) PUBLIC_OUTLIT_KEY=pk_your_public_key_here # Server key (used in API routes) OUTLIT_KEY=pk_your_public_key_here ``` Keep server environment variables out of client-side code. Only use `OUTLIT_KEY` in API routes. ## TypeScript support Full TypeScript support is included: ```typescript theme={null} import outlit, { type OutlitOptions } from '@outlit/browser' const options: OutlitOptions = { publicKey: import.meta.env.PUBLIC_OUTLIT_KEY, trackPageviews: true, flushInterval: 5000 } outlit.init(options) ``` ## Next steps Track events from your backend Learn how visits and events link to people Full API reference Use Outlit with React # Calendar embed tracking Source: https://docs.outlit.ai/tracking/browser/calendar-embeds Automatically track when visitors book meetings via Cal.com or Calendly. Outlit automatically detects when visitors complete bookings through embedded calendar widgets from **Cal.com** and **Calendly**. No extra code required—just install the tracking snippet and booking events are captured. ## Supported platforms | Platform | Detection method | | - | - | | Cal.com | Cal.com embed API | | Calendly | Calendly postMessage events | ## What gets tracked When a visitor books a meeting, Outlit captures: | Property | Description | | - | - | | `provider` | `"cal.com"` or `"calendly"` | | `eventType` | Meeting title (e.g., "30 Minute Demo") | | `startTime` | Scheduled start time (ISO format) | | `endTime` | Scheduled end time (ISO format) | | `duration` | Meeting length in minutes | Calendar booking events appear in your timeline as "Meeting requested" activities—distinct from calendar sync events, so you won't see duplicates. ## Configuration Calendar embed tracking is **enabled by default**. To disable it: ```html theme={null} ``` Set to `"false"` to disable automatic calendar booking detection. ## Limitations **Auto-identify is not available** for calendar embed bookings. Cal.com and Calendly do not expose attendee email or name in their client-side events for privacy reasons. To identify visitors who book meetings, use **webhooks** from your calendar provider: 1. Set up a webhook in Cal.com or Calendly 2. Receive the booking details (including email) on your server 3. Call an Outlit server SDK to identify the person by email. Also identify the browser visitor with the same email to link earlier website activity ```javascript theme={null} // Server-side: after receiving webhook await outlit.identify({ email: webhookData.attendee.email, traits: { name: webhookData.attendee.name, bookedMeeting: webhookData.eventType } }) await outlit.flush() ``` ## Example timeline A typical visitor journey with calendar booking: 1. **Pageview** — Visitor lands on `/demo` 2. **Meeting requested** — Visitor books "30 Minute Demo" via Cal.com embed 3. **Identify the person** — Your webhook handler records the attendee email. Identify the browser visitor with the same email to join earlier website activity The booking joins the person’s profile when the browser visitor is also identified with the same email, for example through a form or browser `identify()` call. An email-only server identify cannot link an otherwise anonymous browser visitor by itself. # Next.js Source: https://docs.outlit.ai/tracking/browser/nextjs Integrate Outlit tracking with Next.js App Router and Pages Router. ## Installation ```bash npm theme={null} npm install --save @outlit/browser ``` ```bash pnpm theme={null} pnpm add @outlit/browser ``` ```bash yarn theme={null} yarn add @outlit/browser ``` ```bash bun theme={null} bun add @outlit/browser ``` ## Quick start Create a client component to initialize Outlit: ```typescript theme={null} // app/providers.tsx 'use client' import { useEffect } from 'react' import outlit from '@outlit/browser' export function OutlitProvider() { useEffect(() => { outlit.init({ publicKey: process.env.NEXT_PUBLIC_OUTLIT_KEY!, trackPageviews: true, }) }, []) return null } ``` Add to your root layout: ```tsx theme={null} // app/layout.tsx import { OutlitProvider } from './providers' export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} ) } ``` Initialize in `_app.tsx`: ```tsx theme={null} // pages/_app.tsx import { useEffect } from 'react' import type { AppProps } from 'next/app' import outlit from '@outlit/browser' export default function App({ Component, pageProps }: AppProps) { useEffect(() => { outlit.init({ publicKey: process.env.NEXT_PUBLIC_OUTLIT_KEY!, trackPageviews: true, }) }, []) return } ``` Add your public key to `.env.local`: ``` NEXT_PUBLIC_OUTLIT_KEY=pk_your_public_key_here ``` ## Using the React provider (recommended) For a better experience with React hooks, use the React provider: ```tsx theme={null} // app/providers.tsx 'use client' import { OutlitProvider } from '@outlit/browser/react' export function Providers({ children }: { children: React.ReactNode }) { return ( {children} ) } ``` ```tsx theme={null} // app/layout.tsx import { Providers } from './providers' export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} ) } ``` See the [React integration guide](/tracking/browser/react) for full React provider documentation. ## Tracking events Track custom events in client components: ```tsx theme={null} // app/dashboard/page.tsx 'use client' import { useEffect } from 'react' import outlit from '@outlit/browser' export default function Dashboard() { useEffect(() => { outlit.track('dashboard_viewed') }, []) function handleExport() { outlit.track('data_exported', { format: 'csv', rows: 1500 }) } return (

Dashboard

) } ``` ### With React provider ```tsx theme={null} 'use client' import { useOutlit } from '@outlit/browser/react' export default function PricingButton() { const { track } = useOutlit() return ( ) } ``` ### Activation event Track your value milestone as an ordinary event once it succeeds. For example, after your export action confirms completion, call `outlit.track('report_exported', { format: 'csv' })` from the client component. Choose that event as your activation event in Outlit. The first matching event records activation once for each eligible resolved person and company, independently. Billing status comes from verified integrations such as Stripe. See [Customer journey](/concepts/customer-journey). ## Identifying users Identify users after authentication: ```tsx theme={null} // app/login/page.tsx 'use client' import { useState } from 'react' import { useRouter } from 'next/navigation' import outlit from '@outlit/browser' export default function LoginPage() { const router = useRouter() const [email, setEmail] = useState('') const [password, setPassword] = useState('') async function handleLogin(e: React.FormEvent) { e.preventDefault() const user = await login(email, password) outlit.identify({ email: user.email, userId: user.id, traits: { name: user.name, plan: user.plan } }) router.push('/dashboard') } return (
setEmail(e.target.value)} placeholder="Email" /> setPassword(e.target.value)} placeholder="Password" />
) } ``` ## Server-side auth integration ### With NextAuth ```tsx theme={null} // app/providers.tsx 'use client' import { OutlitProvider } from '@outlit/browser/react' import { useSession } from 'next-auth/react' export function Providers({ children }: { children: React.ReactNode }) { const { data: session, status } = useSession() const user = status === 'authenticated' && session?.user ? { email: session.user.email!, userId: session.user.id, traits: { name: session.user.name } } : null return ( {children} ) } ``` ### With Clerk ```tsx theme={null} // app/providers.tsx 'use client' import { OutlitProvider } from '@outlit/browser/react' import { useUser } from '@clerk/nextjs' export function Providers({ children }: { children: React.ReactNode }) { const { user, isLoaded } = useUser() const outlitUser = isLoaded && user ? { email: user.primaryEmailAddress?.emailAddress, userId: user.id, traits: { name: user.fullName } } : null return ( {children} ) } ``` ### With Supabase ```tsx theme={null} // app/providers.tsx 'use client' import { OutlitProvider } from '@outlit/browser/react' import { createClientComponentClient } from '@supabase/auth-helpers-nextjs' import { useEffect, useState } from 'react' export function Providers({ children }: { children: React.ReactNode }) { const supabase = createClientComponentClient() const [user, setUser] = useState(null) useEffect(() => { supabase.auth.getSession().then(({ data: { session } }) => { setUser(session?.user ?? null) }) const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => { setUser(session?.user ?? null) }) return () => subscription.unsubscribe() }, [supabase]) const outlitUser = user ? { email: user.email, userId: user.id, traits: { name: user.user_metadata?.name } } : null return ( {children} ) } ``` ## Server actions Track events from server actions: ```typescript theme={null} // app/actions.ts 'use server' import { Outlit } from '@outlit/node' const outlit = new Outlit({ publicKey: process.env.OUTLIT_KEY! }) export async function trackServerEvent(userId: string) { outlit.track({ userId, eventName: 'server_action_completed', timestamp: Date.now(), properties: { action: 'data_export' } }) await outlit.flush() } ``` For server-side tracking, see the [Node.js integration guide](/tracking/server/nodejs). ## API routes Track events from API routes: ```typescript theme={null} // app/api/checkout/route.ts import { NextResponse } from 'next/server' import { Outlit } from '@outlit/node' const outlit = new Outlit({ publicKey: process.env.OUTLIT_KEY! }) export async function POST(request: Request) { const { userId, amount } = await request.json() outlit.track({ userId, eventName: 'checkout_completed', properties: { amount } }) await outlit.flush() return NextResponse.json({ success: true }) } ``` ## Middleware Don't track in middleware: it cannot reliably respect the visitor's consent choice or guarantee delivery, and it can double-count pageviews the Outlit Browser SDK already sends. Use browser pageview tracking for page activity and `@outlit/node` in Server Actions or Route Handlers for server events, where you can `await outlit.flush()` (see the [Outlit Node.js SDK guide](/tracking/server/nodejs#outlit-flush)). ## Environment variables Set up your environment variables: ```bash theme={null} # .env.local # Public key (used in browser) NEXT_PUBLIC_OUTLIT_KEY=pk_your_public_key_here # Server key (used in server actions/API routes) OUTLIT_KEY=pk_your_public_key_here ``` Keep server environment variables out of client-side code. Only use `OUTLIT_KEY` in server components, API routes, or server actions. ## TypeScript support Full TypeScript support is included: ```typescript theme={null} import outlit, { type OutlitOptions } from '@outlit/browser' const options: OutlitOptions = { publicKey: process.env.NEXT_PUBLIC_OUTLIT_KEY!, trackPageviews: true, flushInterval: 5000 } outlit.init(options) ``` ## Next steps Full React integration guide Track events from your backend Learn how visits and events link to people Full API reference # npm package Source: https://docs.outlit.ai/tracking/browser/npm Install the Outlit Browser SDK with npm for explicit tracking configuration. The Outlit Browser SDK records landing URLs, browser referrers, UTM parameters, and anonymous site activity. Identify the same browser visitor with an email when it becomes available to connect that history to later product events and conversions. ## Installation ```bash npm theme={null} npm install --save @outlit/browser ``` ```bash pnpm theme={null} pnpm add @outlit/browser ``` ```bash yarn theme={null} yarn add @outlit/browser ``` ```bash bun theme={null} bun add @outlit/browser ``` ## Initialization Initialize the tracker once at app startup, typically in your entry point: ```typescript theme={null} import outlit from '@outlit/browser' outlit.init({ publicKey: 'pk_your_public_key', // Optional configuration trackPageviews: true, trackForms: true, trackEngagement: true, flushInterval: 5000, // ms }) ``` ### Configuration options Your organization's public key from [**Settings → Website Visitors**](https://app.outlit.ai/settings/workspace/website-visitors). The API endpoint for sending events. Override for self-hosted or proxy setups. Automatically track pageviews on navigation. Automatically capture form submissions. Additional field names to exclude from form capture. How often to send batched events (in milliseconds). Controls when tracking starts. `"auto"` (default) asks the Outlit edge endpoint whether the visitor's region requires opt-in consent: tracking enables automatically outside the EEA, UK, and Switzerland, and stays off in those regions or when the region can't be determined. The check fails closed — on a network error or timeout, tracking stays off. `true` enables tracking immediately on init; `false` waits for `enableTracking()`. A persisted opt-in or opt-out decision always takes precedence, and nothing is written to cookies or localStorage until tracking is enabled. Automatically identify users when they submit forms containing an email field. Extracts email and name using field name heuristics. Set to `false` to call `identify()` manually. See [Auto-identify](/concepts/identity-resolution#auto-identify) for details. Track engagement metrics (active time on page). Emits engagement events on page exit and SPA navigation capturing how long users actively engaged with each page. Idle timeout in milliseconds for engagement tracking. After this period of no user interaction, the user is considered idle and active time stops accumulating. Track booking events from calendar embeds (Cal.com, Calendly). Fires a calendar event when bookings are detected. ## Consent management The default `autoTrack: "auto"` mode already keeps tracking off for visitors in the EEA, UK, and Switzerland (or when the region is unknown). Have your consent management platform call `enableTracking()` when the user accepts — that call is what records opt-in consent. To wait for `enableTracking()` instead — unless a previous opt-in is already stored — initialize with `autoTrack: false`: ```typescript theme={null} import outlit from '@outlit/browser' // Initialize with tracking off until consent outlit.init({ publicKey: 'pk_your_public_key', autoTrack: false, // Don't track until consent }) // When user accepts cookies/tracking function onConsentAccepted() { outlit.enableTracking() } // When user declines function onConsentDeclined() { outlit.disableTracking() } ``` ### Check tracking status ```typescript theme={null} import { isTrackingEnabled } from '@outlit/browser' if (isTrackingEnabled()) { console.log('Tracking is active') } ``` ## API reference ### `outlit.init(options)` Initialize the tracker. Must be called before other methods. ```typescript theme={null} import outlit from '@outlit/browser' outlit.init({ publicKey: 'pk_xxx' }) ``` ### `outlit.track(eventName, properties?)` Track a custom event. ```typescript theme={null} outlit.track('feature_used', { feature: 'export', format: 'csv', rowCount: 1500 }) ``` A descriptive name for the event. Use `snake_case`. Additional data to attach to the event. ### `outlit.identify(options)` Identify the current visitor. Links their anonymous history to a known profile. ```typescript theme={null} outlit.identify({ email: 'jane@acme.com', userId: 'usr_12345', customerId: 'cust_123', // Your app's account/workspace/customer ID traits: { name: 'Jane Doe', role: 'Engineering Manager', }, customerTraits: { plan: 'enterprise' } }) ``` The visitor's email address. Primary identifier. Your internal user ID (from Supabase, Auth0, etc.). Additional properties about the user. Your system-owned customer/account/workspace ID. Send this when the visitor belongs to a known account. Additional properties about the customer/account profile. ### `outlit.setUser(identity)` Set the current user identity. Ideal for SPA applications where you know the user's identity after authentication. Can be called before tracking is enabled—the identity is queued and applied when `enableTracking()` is called. ```typescript theme={null} // After user logs in outlit.setUser({ email: 'jane@acme.com', userId: 'usr_12345', customerId: 'cust_123', // Your app's account/workspace/customer ID traits: { name: 'Jane Doe', }, customerTraits: { plan: 'pro' } }) ``` Both `setUser()` and `identify()` link the visitor to a known profile. The difference is `setUser()` can be called before tracking is enabled (identity is queued), while `identify()` requires tracking to be enabled first. ### `outlit.clearUser()` Clear the current user identity. Call this when the user logs out. ```typescript theme={null} // On logout outlit.clearUser() ``` ### Activation event Track your value milestone as an ordinary event once it succeeds: ```typescript theme={null} outlit.track('onboarding_completed', { flow: 'onboarding', step: 'completed' }) ``` Choose this event as your activation event in Outlit. The first matching event records activation once for each eligible resolved person and company, independently. Billing status comes from verified integrations such as Stripe. See [Customer journey](/concepts/customer-journey). ### `outlit.getInstance()` Get the singleton tracker instance. ```typescript theme={null} const tracker = outlit.getInstance() const visitorId = tracker.getVisitorId() ``` ### `outlit.enableTracking()` Enable tracking after consent. This is the call a consent tool makes to grant opt-in — it's only needed when `autoTrack: false` was set, or when `autoTrack: "auto"` resolved to a region that requires consent. ```typescript theme={null} import outlit, { enableTracking } from '@outlit/browser' // Initialize with autoTrack: false outlit.init({ publicKey: 'pk_xxx', autoTrack: false }) // Later, after consent enableTracking() // or outlit.enableTracking() ``` ### `outlit.disableTracking()` Disable tracking and flush any pending events. Use this when a user declines or revokes consent. ```typescript theme={null} // When user declines or revokes consent outlit.disableTracking() ``` ### `outlit.isTrackingEnabled()` Check if tracking is currently enabled. ```typescript theme={null} import { isTrackingEnabled } from '@outlit/browser' if (isTrackingEnabled()) { // Tracking is active } ``` ## TypeScript support The package is written in TypeScript and includes full type definitions: ```typescript theme={null} import outlit, { type OutlitOptions, type BrowserTrackOptions, type BrowserIdentifyOptions, type UserIdentity, } from '@outlit/browser' // Full type safety outlit.init({ publicKey: 'pk_xxx', trackPageviews: true, }) outlit.track('purchase_completed', { orderId: 'ord_123', amount: 99.99, currency: 'USD', }) ``` ## Framework examples ### Next.js (App Router) ```tsx theme={null} // app/providers.tsx 'use client' import { useEffect } from 'react' import outlit from '@outlit/browser' export function OutlitInit() { useEffect(() => { outlit.init({ publicKey: process.env.NEXT_PUBLIC_OUTLIT_KEY! }) }, []) return null } // app/layout.tsx import { OutlitInit } from './providers' export default function RootLayout({ children }) { return ( {children} ) } ``` ### Vue.js ```typescript theme={null} // main.ts import { createApp } from 'vue' import App from './App.vue' import outlit from '@outlit/browser' outlit.init({ publicKey: import.meta.env.VITE_OUTLIT_KEY }) createApp(App).mount('#app') ``` ```vue theme={null} ``` ### Svelte ```typescript theme={null} // +layout.ts import outlit from '@outlit/browser' outlit.init({ publicKey: import.meta.env.VITE_OUTLIT_KEY }) ``` ```svelte theme={null} ``` ## Advanced usage ### Manual flush Force immediate sending of queued events: ```typescript theme={null} await outlit.getInstance().flush() ``` ### Shutdown Shut down gracefully the tracker (flushes remaining events): ```typescript theme={null} await outlit.getInstance().shutdown() ``` ### Access visitor ID Get the current visitor's ID for server-side correlation (returns `null` until tracking is enabled): ```typescript theme={null} const visitorId = outlit.getInstance().getVisitorId() // Send to your backend fetch('/api/correlate', { body: JSON.stringify({ visitorId, data: '...' }) }) ``` ## Engagement tracking Engagement tracking captures how long users actively spend on each page. It's enabled by default and works automatically. ### How it works * **Active time** is tracked when the page is visible and the user is interacting (mouse movement, clicks, scrolling, typing) * **Idle detection** pauses active time after 30 seconds of no interaction (configurable via `idleTimeout`) * **Engagement events** are automatically sent when the user navigates to a new page or leaves the site ### Engagement data Each engagement event includes: | Property | Description | | - | - | | `activeTimeMs` | Time in milliseconds the user was actively engaged | | `totalTimeMs` | Total wall-clock time on the page | | `sessionId` | Session ID for grouping page views | ### Disabling engagement tracking ```typescript theme={null} outlit.init({ publicKey: 'pk_xxx', trackEngagement: false, // Disable engagement tracking }) ``` ## Next steps Use the React provider and hooks Track events from your backend # Nuxt Source: https://docs.outlit.ai/tracking/browser/nuxt Integrate Outlit tracking with Nuxt 3 applications. ## Installation ```bash npm theme={null} npm install --save @outlit/browser ``` ```bash pnpm theme={null} pnpm add @outlit/browser ``` ```bash yarn theme={null} yarn add @outlit/browser ``` ```bash bun theme={null} bun add @outlit/browser ``` ## Quick start Create a Nuxt plugin to initialize Outlit: ```typescript theme={null} // plugins/outlit.client.ts import outlit from '@outlit/browser' export default defineNuxtPlugin(() => { const config = useRuntimeConfig() outlit.init({ publicKey: config.public.outlitKey, trackPageviews: true, }) return { provide: { outlit } } }) ``` Configure your runtime config: ```typescript theme={null} // nuxt.config.ts export default defineNuxtConfig({ runtimeConfig: { public: { outlitKey: process.env.NUXT_PUBLIC_OUTLIT_KEY } } }) ``` Add your public key to `.env`: ``` NUXT_PUBLIC_OUTLIT_KEY=pk_your_public_key_here ``` ## Tracking events Track events using the provided instance: ```vue theme={null} ``` ## Creating a composable Create a reusable composable for a shorter setup: ```typescript theme={null} // composables/useOutlit.ts export function useOutlit() { const { $outlit } = useNuxtApp() function track(eventName: string, properties?: Record) { $outlit.track(eventName, properties) } function identify(options: { email?: string; userId?: string; traits?: Record }) { $outlit.identify(options) } function setUser(identity: { email?: string; userId?: string; traits?: Record }) { $outlit.setUser(identity) } function clearUser() { $outlit.clearUser() } return { track, identify, setUser, clearUser, user: $outlit.user, } } ``` Use in components: ```vue theme={null} ``` ## Identifying users Identify users after authentication: ```vue theme={null} ``` ## Auth integration ### With Nuxt Auth ```typescript theme={null} // plugins/outlit.client.ts import outlit from '@outlit/browser' export default defineNuxtPlugin(() => { const config = useRuntimeConfig() const { data: auth } = useAuth() outlit.init({ publicKey: config.public.outlitKey, trackPageviews: true, }) // Sync user identity with auth state watch( () => auth.value, (session) => { if (session?.user) { outlit.setUser({ email: session.user.email, userId: session.user.id, traits: { name: session.user.name } }) } else { outlit.clearUser() } }, { immediate: true } ) return { provide: { outlit } } }) ``` ### With Supabase ```typescript theme={null} // plugins/outlit.client.ts import outlit from '@outlit/browser' export default defineNuxtPlugin(async () => { const config = useRuntimeConfig() const supabase = useSupabaseClient() const user = useSupabaseUser() outlit.init({ publicKey: config.public.outlitKey, trackPageviews: true, }) // Sync user identity watch( user, (currentUser) => { if (currentUser) { outlit.setUser({ email: currentUser.email!, userId: currentUser.id, traits: { name: currentUser.user_metadata?.name } }) } else { outlit.clearUser() } }, { immediate: true } ) return { provide: { outlit } } }) ``` ## Router integration Track route changes: ```typescript theme={null} // plugins/outlit.client.ts import outlit from '@outlit/browser' export default defineNuxtPlugin(() => { const config = useRuntimeConfig() const router = useRouter() outlit.init({ publicKey: config.public.outlitKey, trackPageviews: false, // Manual pageview tracking }) // Track pageviews on route change router.afterEach((to) => { outlit.track('pageview', { path: to.path, name: to.name, params: to.params, query: to.query }) }) return { provide: { outlit } } }) ``` ## Server-side tracking Track events from server API routes: ```typescript theme={null} // server/api/checkout.post.ts import { Outlit } from '@outlit/node' const outlit = new Outlit({ publicKey: process.env.OUTLIT_KEY! }) export default defineEventHandler(async (event) => { const body = await readBody(event) outlit.track({ userId: body.userId, eventName: 'checkout_completed', properties: { amount: body.amount, plan: body.plan } }) await outlit.flush() return { success: true } }) ``` For server-side tracking, see the [Node.js integration guide](/tracking/server/nodejs). ## Activation event Track your value milestone as an ordinary event once it succeeds: ```vue theme={null} ``` Choose this event as your activation event in Outlit. The first matching event records activation once for each eligible resolved person and company, independently. Billing status comes from verified integrations such as Stripe. See [Customer journey](/concepts/customer-journey). ## Consent management The default `autoTrack: "auto"` mode already keeps tracking off for visitors in regions that require opt-in consent (EEA, UK, Switzerland, or unknown); the example below uses `autoTrack: false` to wait for an explicit consent signal unless a previous opt-in is stored. Handle user consent: ```vue theme={null} ``` Initialize without auto-tracking: ```typescript theme={null} // plugins/outlit.client.ts import outlit from '@outlit/browser' export default defineNuxtPlugin(() => { const config = useRuntimeConfig() outlit.init({ publicKey: config.public.outlitKey, autoTrack: false, // Wait for consent }) return { provide: { outlit } } }) ``` ## Environment variables Configure environment variables: ```bash theme={null} # .env # Public key (used in browser) NUXT_PUBLIC_OUTLIT_KEY=pk_your_public_key_here # Server key (used in server API routes) OUTLIT_KEY=pk_your_public_key_here ``` Keep server environment variables out of client-side code. Only use `OUTLIT_KEY` in server API routes. ## TypeScript support Full TypeScript support is included: ```typescript theme={null} // plugins/outlit.client.ts import outlit, { type OutlitOptions } from '@outlit/browser' export default defineNuxtPlugin(() => { const config = useRuntimeConfig() const options: OutlitOptions = { publicKey: config.public.outlitKey, trackPageviews: true, flushInterval: 5000 } outlit.init(options) return { provide: { outlit } } }) ``` ## Next steps Use Outlit with Vue 3 Track events from your backend Learn how visits and events link to people Full API reference # React Source: https://docs.outlit.ai/tracking/browser/react First-class React support with Provider and hooks. ## Installation ```bash npm theme={null} npm install --save @outlit/browser ``` ```bash pnpm theme={null} pnpm add @outlit/browser ``` ```bash yarn theme={null} yarn add @outlit/browser ``` ```bash bun theme={null} bun add @outlit/browser ``` ## Quick start Wrap your app with `OutlitProvider` and use the `useOutlit` hook in components. If your app has user authentication, pass the user identity via the `user` prop. This is the recommended approach as it automatically handles login/logout transitions. ```tsx theme={null} // app/providers.tsx (Client Component) 'use client' import { OutlitProvider } from '@outlit/browser/react' import { useUser } from '@clerk/nextjs' // or useAuth0, useSession, etc. export function Providers({ children }: { children: React.ReactNode }) { const { user, isLoaded } = useUser() // Build user identity from your auth provider const outlitUser = isLoaded && user ? { email: user.primaryEmailAddress?.emailAddress, userId: user.id, traits: { name: user.fullName, } } : null return ( {children} ) } ``` ```tsx theme={null} // app/layout.tsx import { Providers } from './providers' export default function RootLayout({ children }) { return ( {children} ) } ``` For marketing sites without authentication, add the provider via a client component: ```tsx theme={null} // app/providers.tsx (Next.js App Router) 'use client' import { OutlitProvider } from '@outlit/browser/react' export function Providers({ children }: { children: React.ReactNode }) { return ( {children} ) } ``` ```tsx theme={null} // app/layout.tsx import { Providers } from './providers' export default function RootLayout({ children }) { return ( {children} ) } ``` ### Using the hook Track custom events with the `useOutlit` hook: ```tsx theme={null} // components/pricing-button.tsx 'use client' import { useOutlit } from '@outlit/browser/react' export function PricingButton() { const { track } = useOutlit() return ( ) } ``` ## Provider configuration ### OutlitProvider The `OutlitProvider` initializes tracking and provides context to child components. ```tsx theme={null} import { OutlitProvider } from '@outlit/browser/react' {children} ``` ### Props Your organization's public key. API endpoint for sending events. Automatically track pageviews on route changes. Automatically capture form submissions. Additional field names to exclude from form capture. How often to flush queued events (ms). Controls when tracking starts. `"auto"` (default) asks the Outlit edge endpoint whether the visitor's region requires opt-in consent: tracking enables automatically outside the EEA, UK, and Switzerland, and stays off in those regions or when the region can't be determined. The check fails closed on errors. `true` enables immediately; `false` waits for `enableTracking()` from the `useOutlit` hook. `isTrackingEnabled` updates automatically when auto mode enables tracking asynchronously. Automatically identify users when they submit forms containing an email field. Extracts email and name (first name, last name, full name) using field name heuristics. Set to `false` to disable and call `identify()` manually. See [Auto-identify](/concepts/identity-resolution#auto-identify) for details. Track engagement metrics (active time on page). Emits engagement events on page exit and navigation. Idle timeout in milliseconds for engagement tracking. After this period of no user interaction, the user is considered idle. Track booking events from calendar embeds (Cal.com, Calendly). Current user identity. When provided with email or userId, identifies the user. When null or undefined, clears the user identity. This is the recommended way to handle user identity in server-rendered apps. An existing `Outlit` instance to use instead of creating one internally. When provided, config props (`publicKey`, `apiHost`, etc.) are ignored and `shutdown()` is **not** called on unmount — the caller owns the instance lifecycle. Useful for sharing a single instance between imperative usage and React context. ### Client mode If you need to use the same Outlit instance both imperatively and via React context, pass an existing instance with the `client` prop: ```tsx theme={null} import { Outlit } from '@outlit/browser' import { OutlitProvider } from '@outlit/browser/react' const outlit = new Outlit({ publicKey: 'pk_xxx' }) // Imperative usage outside React outlit.track('app_loaded') function App() { return ( {children} ) } ``` When using `client`, the provider does not call `shutdown()` on unmount — you own the instance lifecycle. Config props like `publicKey` and `trackPageviews` are ignored. ## User identity patterns The `user` prop is the **recommended** way to handle user identity. It's simpler and more reliable than manual `identify()` calls because: * **Automatic lifecycle management**: Login/logout transitions are handled automatically * **No timing issues**: Identity is set before any events are tracked * **Cleaner code**: No need for useEffect + identify() synchronization ### Pattern 1: client-side auth (Clerk, Auth0, etc.) For auth libraries that provide React hooks: ```tsx theme={null} // app/providers.tsx 'use client' import { OutlitProvider } from '@outlit/browser/react' import { useUser } from '@clerk/nextjs' // or useAuth0, useSession, etc. export function Providers({ children }: { children: React.ReactNode }) { const { user, isLoaded } = useUser() const outlitUser = isLoaded && user ? { email: user.primaryEmailAddress?.emailAddress, userId: user.id, traits: { name: user.fullName } } : null return ( {children} ) } ``` ### Pattern 2: server-side auth (NextAuth, Lucia, etc.) For auth systems where session is available server-side, read the session in the Server Component and pass the resolved user into a client wrapper — `OutlitProvider` itself must render inside a client component (the package does not ship a `'use client'` entry). ```tsx theme={null} // app/providers.tsx (Client Component) 'use client' import { OutlitProvider } from '@outlit/browser/react' import type { UserIdentity } from '@outlit/browser' export function Providers({ children, user, }: { children: React.ReactNode user: UserIdentity | null }) { return ( {children} ) } ``` ```tsx theme={null} // app/layout.tsx (Server Component) import { Providers } from './providers' import { auth } from '@/auth' // Your auth library export default async function RootLayout({ children }) { const session = await auth() return ( {children} ) } ``` **Which pattern should I use?** * **Client-side auth hooks** (Clerk, Auth0): Use Pattern 1 with a client providers component * **Server-side sessions** (NextAuth, Lucia, custom JWT): Use Pattern 2 — resolve the session in the server layout, pass `user` into a client `Providers` wrapper Both patterns work equally well - choose based on how your auth library provides user data. In the App Router, `OutlitProvider` (and `useOutlit`) must always render inside a component with `'use client'`. When the `user` prop changes (login/logout), the provider automatically calls `setUser()` or `clearUser()` internally. You don't need to call these methods manually. ## Consent management The default `autoTrack: "auto"` mode already keeps tracking off for visitors in regions that require opt-in consent (EEA, UK, Switzerland, or unknown). `enableTracking()` — typically wired to your consent banner's accept action — is what grants consent and is persisted for future sessions. To wait for `enableTracking()` instead — unless a previous opt-in is stored: ```tsx theme={null} // app/layout.tsx import { OutlitProvider } from '@outlit/browser/react' export default function RootLayout({ children }) { return ( {children} ) } ``` ```tsx theme={null} // components/cookie-banner.tsx 'use client' import { useOutlit } from '@outlit/browser/react' export function CookieBanner() { const { enableTracking, disableTracking, isTrackingEnabled } = useOutlit() // Don't show if already tracking (user previously accepted) if (isTrackingEnabled) return null return (

We use cookies to improve your experience.

) } ``` ### With third-party CMPs ```tsx theme={null} 'use client' import { useOutlit } from '@outlit/browser/react' import { useEffect } from 'react' export function CookiebotListener() { const { enableTracking } = useOutlit() useEffect(() => { const handleConsent = () => { // @ts-ignore - Cookiebot global const consent = window.Cookiebot?.consent if (consent?.marketing || consent?.analytics) { enableTracking() } } window.addEventListener('CookiebotOnAccept', handleConsent) return () => window.removeEventListener('CookiebotOnAccept', handleConsent) }, [enableTracking]) return null } ``` ```tsx theme={null} 'use client' import { useOutlit } from '@outlit/browser/react' import { useEffect } from 'react' export function OneTrustListener() { const { enableTracking } = useOutlit() useEffect(() => { // @ts-ignore - OneTrust global const groups = window.OnetrustActiveGroups || '' // C0002 = Performance, C0004 = Targeting/Marketing if (groups.includes('C0004') || groups.includes('C0002')) { enableTracking() } // Also listen for changes const handleConsent = () => { // @ts-ignore const updatedGroups = window.OnetrustActiveGroups || '' if (updatedGroups.includes('C0004') || updatedGroups.includes('C0002')) { enableTracking() } } window.addEventListener('consent.onetrust', handleConsent) return () => window.removeEventListener('consent.onetrust', handleConsent) }, [enableTracking]) return null } ``` ## Hooks ### useOutlit The primary hook for tracking events and identifying users. ```tsx theme={null} import { useOutlit } from '@outlit/browser/react' function MyComponent() { const { track, identify, setUser, clearUser, user, // User identity helper getVisitorId, isInitialized, isTrackingEnabled, enableTracking, disableTracking, } = useOutlit() // Track an event track('button_clicked', { buttonId: 'cta' }) // Identify the user identify({ email: 'user@example.com', traits: { plan: 'pro' } }) // Get visitor ID (null if tracking not enabled) const visitorId = getVisitorId() // Check if tracking is active if (!isTrackingEnabled) { return } } ``` #### Returns Track a custom event. Identify the current visitor. Set the current user identity. Ideal for SPA authentication flows. Clear the current user identity (on logout). User identity namespace: * `user.identify(options)` - Identify the user Get the current visitor's ID. Returns `null` if tracking is not enabled. Whether tracking is currently enabled. Will be `false` until `enableTracking()` is called when `autoTrack` is `false`, while the `"auto"` region check is pending, or when that check requires consent or fails. Enable tracking. Call this after obtaining user consent. Needed when `autoTrack` is `false`, or in the default `"auto"` mode when the visitor's region requires consent or the region check fails. Disable tracking and flush any pending events. Use this when a user declines or revokes consent. Whether the tracker is ready. ### useTrack A convenience hook that returns just the `track` function: ```tsx theme={null} import { useTrack } from '@outlit/browser/react' function FeatureButton({ featureId }) { const track = useTrack() return ( ) } ``` ### useIdentify A convenience hook that returns just the `identify` function: ```tsx theme={null} import { useIdentify } from '@outlit/browser/react' function LoginForm() { const identify = useIdentify() const onLogin = async (email, password) => { const user = await login(email, password) identify({ email: user.email, userId: user.id, traits: { name: user.name, plan: user.plan } }) } } ``` ## Activation event Track your value milestone as an ordinary event once it succeeds: ```tsx theme={null} 'use client' import { useOutlit } from '@outlit/browser/react' function OnboardingComplete() { const { track } = useOutlit() const handleComplete = () => { track('onboarding_completed', { flow: 'onboarding', step: 'completed' }) } return } ``` Choose this event as your activation event in Outlit. The first matching event records activation once for each eligible resolved person and company, independently. Billing status comes from verified integrations such as Stripe. See [Customer journey](/concepts/customer-journey). ## Framework examples ### Next.js App Router ```tsx theme={null} // app/providers.tsx 'use client' import { OutlitProvider } from '@outlit/browser/react' export function Providers({ children }: { children: React.ReactNode }) { return ( {children} ) } ``` ```tsx theme={null} // app/layout.tsx import { Providers } from './providers' export default function RootLayout({ children }) { return ( {children} ) } ``` ```tsx theme={null} // app/dashboard/page.tsx 'use client' import { useOutlit } from '@outlit/browser/react' import { useEffect } from 'react' export default function Dashboard() { const { track } = useOutlit() useEffect(() => { track('dashboard_viewed') }, [track]) return
Dashboard
} ``` ### Next.js pages router ```tsx theme={null} // pages/_app.tsx import { OutlitProvider } from '@outlit/browser/react' export default function App({ Component, pageProps }) { return ( ) } ``` ### Remix Remix renders the root component in the browser, so the provider can live directly in `root.tsx` (Remix has no server-only component boundary like the Next.js App Router). Use a `VITE_`-prefixed env var for the public key: ```tsx theme={null} // app/root.tsx import { Outlet } from 'react-router' import { OutlitProvider } from '@outlit/browser/react' export default function App() { return ( ) } ``` ## Common patterns ### Track on mount Track when a component mounts (e.g., page viewed): ```tsx theme={null} import { useOutlit } from '@outlit/browser/react' import { useEffect } from 'react' function ProductPage({ productId }) { const { track } = useOutlit() useEffect(() => { track('product_viewed', { productId }) }, [productId, track]) return
Product {productId}
} ``` ### Track form submission Track custom form events: ```tsx theme={null} import { useOutlit } from '@outlit/browser/react' function ContactForm() { const { track, identify } = useOutlit() const handleSubmit = (e) => { e.preventDefault() const formData = new FormData(e.target) track('contact_form_submitted', { subject: formData.get('subject') }) identify({ email: formData.get('email'), traits: { name: formData.get('name'), company: formData.get('company') } }) } return (
) } ``` ### Identify after auth **Prefer the `user` prop instead.** The pattern below works, but using the `user` prop on `OutlitProvider` is cleaner and handles the lifecycle automatically. See [User identity patterns](#user-identity-patterns) above. If you can't use the `user` prop (e.g., the provider is in a different part of the tree), you can sync manually: ```tsx theme={null} import { useOutlit } from '@outlit/browser/react' import { useUser } from '@/hooks/use-user' // Your auth hook function AuthObserver() { const { setUser, clearUser } = useOutlit() const { user, isLoading } = useUser() useEffect(() => { if (isLoading) return if (user) { setUser({ email: user.email, userId: user.id, traits: { name: user.name, plan: user.subscription?.plan } }) } else { clearUser() } }, [user, isLoading, setUser, clearUser]) return null } ``` ## TypeScript Full TypeScript support is included: ```tsx theme={null} import { useOutlit, OutlitProvider, type OutlitProviderProps, type UseOutlitReturn, type UserIdentity, } from '@outlit/browser/react' // Types are inferred const { track, identify, setUser } = useOutlit() // Or explicitly typed const outlit: UseOutlitReturn = useOutlit() // User identity type const user: UserIdentity = { email: 'user@example.com', userId: 'usr_123', traits: { name: 'Jane' } } ``` ## Next steps Track events from your backend Learn how visits and events link to people # Script tag Source: https://docs.outlit.ai/tracking/browser/script The fastest way to add tracking to any website. The script tag is the simplest way to add the Outlit Browser SDK to a website. It works on static HTML, WordPress, Webflow, and other sites. The SDK records the landing URL, browser referrer, available UTM parameters, and anonymous pageviews. Identify the same browser visitor with an email when it becomes available to link that history to a contact. ## Installation Add this snippet before the closing `` tag. This creates a lightweight stub that queues calls until the full Outlit Browser SDK loads, without blocking page load: ```html theme={null} ``` This snippet is safe to place anywhere on the page, including in the ``. Calls to `outlit.track()` or `outlit.user.identify()` will be queued and processed once the SDK loads. ### Simple version If you prefer a cleaner script tag and don't need to call tracking methods before the SDK loads: ```html theme={null} ``` Your organization's public key. Find it in [**Settings → Website Visitors**](https://app.outlit.ai/settings/workspace/website-visitors). Custom API host. Defaults to `https://app.outlit.ai`. Set to `"false"` to disable automatic pageview tracking. Set to `"false"` to disable automatic form capture. Controls when tracking starts. `"auto"` (default when the attribute is absent) asks the Outlit edge endpoint whether the visitor's region requires opt-in consent: tracking enables automatically outside the EEA, UK, and Switzerland, and stays off in those regions, when the region can't be determined, or when the check fails. `"true"` enables tracking immediately on load; `"false"` waits for `window.outlit.enableTracking()`. A persisted opt-in or opt-out decision always takes precedence. Set to `"false"` to disable automatic user identification from form submissions. When enabled (default), submitting a form with an email field automatically calls `identify()` with the extracted email and name. See [Auto-identify](/concepts/identity-resolution#auto-identify) for details. Set to `"false"` to disable automatic tracking of calendar bookings from Cal.com and Calendly embeds. See [Calendar embed tracking](/tracking/browser/calendar-embeds) for details. The script-tag loader reads only the attributes listed above. Engagement tracking (`trackEngagement`) and the idle timeout (`idleTimeout`) are **not** configurable via `data-*` attributes — use the [npm package](/tracking/browser/npm) if you need to change them (`init()` options). ## Consent management The default `"auto"` behavior already keeps tracking off for visitors in regions that require opt-in consent (EEA, UK, Switzerland, or unknown) — no cookies or localStorage keys are written until tracking is enabled. When your consent management platform reports acceptance, call `window.outlit.enableTracking()`. If you want tracking to wait for `enableTracking()` instead — unless a previous opt-in is stored — pass `false` as the last parameter: ```html theme={null} ``` Or with the simple script tag: ```html theme={null} ``` ### With popular CMPs ```javascript theme={null} window.addEventListener('CookiebotOnAccept', function() { // Check for marketing or analytics consent if (Cookiebot.consent.marketing || Cookiebot.consent.analytics) { window.outlit.enableTracking() } }) ``` ```javascript theme={null} // OneTrust calls this when consent changes function OptanonWrapper() { // C0002 = Analytics, C0004 = Targeting/Marketing if (OnetrustActiveGroups.includes('C0004') || OnetrustActiveGroups.includes('C0002')) { window.outlit.enableTracking() } } ``` ```javascript theme={null} document.getElementById('accept-cookies').addEventListener('click', function() { window.outlit.enableTracking() }) document.getElementById('decline-cookies').addEventListener('click', function() { window.outlit.disableTracking() }) // Check on page load — consent state is persisted automatically by the SDK if (window.outlit.isTrackingEnabled()) { console.log('Tracking is active') } ``` ### Check tracking status ```javascript theme={null} // Check if tracking is currently enabled if (window.outlit.isTrackingEnabled()) { console.log('Tracking is active') } ``` Consent state persists across sessions in `localStorage` and a cookie (`outlit_consent`, 1 year). A visitor who declined keeps `disableTracking()` applied on every future visit — even with `data-auto-track` enabled — until they accept again. `enableTracking()` persists the opt-in the same way. ## Automatic tracking Once installed, the tracker automatically captures: ### Pageviews Every page navigation is tracked, including: * Full URL and path * Page title * Referrer * UTM parameters (`utm_source`, `utm_medium`, `utm_campaign`, `utm_term`, `utm_content`) For single-page applications, the tracker automatically detects navigation via `pushState`, `replaceState`, and `popstate` events. ### Form submissions When a form is submitted, the tracker captures: * Form ID or name * All field values (except sensitive fields) **Automatically removed fields:** * Passwords (`password`, `passwd`, `pwd`) * Credit card numbers (`credit_card`, `cc_number`, `cvv`) * Social Security numbers (`ssn`, `social_security`) * API keys and tokens (`api_key`, `token`, `secret`) ## Manual tracking Use the global `window.outlit` object to track custom events: ### Track custom events ```javascript theme={null} window.outlit.track('button_clicked', { buttonId: 'cta-hero', page: '/pricing' }) window.outlit.track('video_played', { videoId: 'demo-video', duration: 120 }) ``` The name of the event. Use snake\_case for consistency. Optional properties to attach to the event. ### Identify visitors When a visitor provides their email (signup, login, contact form), identify them: ```javascript theme={null} window.outlit.identify({ email: 'jane@example.com', userId: 'usr_12345', // optional traits: { name: 'Jane Doe', company: 'Acme Inc', plan: 'enterprise' } }) ``` The visitor's email address. Your internal user ID (from Supabase, Auth0, Firebase, etc.). Additional properties about the user. Pass at least one of `email` or `userId`; without either, `identify()` has no effect. ### Set user identity For SPA applications where you know the user's identity after authentication: ```javascript theme={null} // After user logs in window.outlit.setUser({ email: 'jane@example.com', userId: 'usr_12345', traits: { name: 'Jane Doe', plan: 'pro' } }) ``` `setUser()` can be called before tracking is enabled (with `data-auto-track="false"`). The identity is queued and applied when `enableTracking()` is called. ### Clear user identity Call when the user logs out: ```javascript theme={null} window.outlit.clearUser() ``` ### Activation event Track your value milestone as an ordinary event once it succeeds: ```javascript theme={null} // After user completes onboarding window.outlit.track('onboarding_completed', { flow: 'onboarding' }) ``` Choose this event as your activation event in Outlit. The first matching event records activation once for each eligible resolved person and company, independently. Billing status comes from verified integrations such as Stripe. See [Customer journey](/concepts/customer-journey). ### Get visitor ID Access the current visitor's ID (useful for server-side correlation). Returns `null` until tracking is enabled: ```javascript theme={null} const visitorId = window.outlit.getVisitorId() // Returns: "a1b2c3d4-e5f6-7890-abcd-ef1234567890" ``` ## Queued commands The IIFE snippet creates stub methods that queue all tracking calls until the SDK fully loads. This means you can call `outlit.track()`, `outlit.identify()`, or any other method immediately—even in inline scripts before the SDK has finished loading: ```html theme={null} ``` ## Event batching For performance, events are batched and sent: * Every 5 seconds (if there are pending events) * When the queue reaches 10 events * When the page is about to unload (using `sendBeacon`) ## Example: full integration ```html theme={null} My Website

Welcome

``` ## Troubleshooting 1. Check your public key is correct 2. Verify the domain is in your allowlist ([**Settings → Website Visitors**](https://app.outlit.ai/settings/workspace/website-visitors)) 3. Open DevTools → Network and check for errors in `/api/i/v1/` requests 4. Ensure the script loads without errors (check Console) 1. Form must have a submit event (not prevented with `e.preventDefault()` before capture) 2. Check if field names match the denylist (they may be filtered for security) 3. Ensure `data-track-forms` is not set to `"false"` The tracker automatically listens for `pushState`/`replaceState`. If using a custom router that doesn't use these, manually track: ```javascript theme={null} router.on('routeChange', (url) => { window.outlit.track('pageview', { url }); }); ``` ## Next steps Use the npm package for more control First-class React support with hooks Track Cal.com and Calendly bookings # SvelteKit Source: https://docs.outlit.ai/tracking/browser/sveltekit Integrate Outlit tracking with SvelteKit applications. ## Installation ```bash npm theme={null} npm install --save @outlit/browser ``` ```bash pnpm theme={null} pnpm add @outlit/browser ``` ```bash yarn theme={null} yarn add @outlit/browser ``` ```bash bun theme={null} bun add @outlit/browser ``` ## Quick start Initialize Outlit in your root layout: ```typescript theme={null} // src/routes/+layout.ts import outlit from '@outlit/browser' import { browser } from '$app/environment' import { PUBLIC_OUTLIT_KEY } from '$env/static/public' if (browser) { outlit.init({ publicKey: PUBLIC_OUTLIT_KEY, trackPageviews: true, }) } export const prerender = true export const ssr = false ``` Add your public key to `.env`: ``` PUBLIC_OUTLIT_KEY=pk_your_public_key_here ``` ## Alternative: layout component Initialize in the layout component: ```svelte theme={null} ``` ## Tracking events Track events in your components: ```svelte theme={null} ``` ## Identifying users Identify users after authentication: ```svelte theme={null}
``` ## Navigation tracking Track navigation with SvelteKit's stores: ```svelte theme={null} ``` ## Load function integration Track events from load functions: ```typescript theme={null} // src/routes/product/[id]/+page.ts import type { PageLoad } from './$types' export const load: PageLoad = async ({ params, fetch }) => { const product = await fetch(`/api/products/${params.id}`).then(r => r.json()) // Track on server await fetch('/api/track', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ eventName: 'product_viewed', userId: 'usr_123', properties: { productId: params.id } }) }) return { product } } ``` ## Server-side tracking Track events from API routes: ```typescript theme={null} // src/routes/api/track/+server.ts import { json } from '@sveltejs/kit' import type { RequestHandler } from './$types' import { Outlit } from '@outlit/node' import { OUTLIT_KEY } from '$env/static/private' const outlit = new Outlit({ publicKey: OUTLIT_KEY }) export const POST: RequestHandler = async ({ request }) => { const { eventName, properties, userId, email, customerId } = await request.json() outlit.track({ eventName, userId, email, customerId, properties }) await outlit.flush() return json({ success: true }) } ``` For server-side tracking, see the [Node.js integration guide](/tracking/server/nodejs). ## Auth integration ### With Lucia ```svelte theme={null} ``` ## Activation event Track your value milestone as an ordinary event once it succeeds: ```svelte theme={null} ``` Choose this event as your activation event in Outlit if it represents the value milestone. The first matching event records activation once for each eligible resolved person and company, independently. Billing status comes from verified integrations such as Stripe. See [Customer journey](/concepts/customer-journey). ## Consent management The default `autoTrack: "auto"` mode already keeps tracking off for visitors in regions that require opt-in consent (EEA, UK, Switzerland, or unknown); the example below uses `autoTrack: false` to wait for an explicit consent signal unless a previous opt-in is stored. Handle user consent: ```svelte theme={null} {#if showBanner} {/if} ``` Initialize without auto-tracking: ```typescript theme={null} // src/routes/+layout.ts import outlit from '@outlit/browser' import { browser } from '$app/environment' import { PUBLIC_OUTLIT_KEY } from '$env/static/public' if (browser) { outlit.init({ publicKey: PUBLIC_OUTLIT_KEY, autoTrack: false, // Wait for consent }) } ``` ## Stores integration Create a store for Outlit: ```typescript theme={null} // src/lib/stores/outlit.ts import { writable } from 'svelte/store' import outlit from '@outlit/browser' function createOutlitStore() { const { subscribe } = writable(outlit) return { subscribe, track: (eventName: string, properties?: Record) => { outlit.track(eventName, properties) }, identify: (options: { email?: string; userId?: string; traits?: Record }) => { outlit.identify(options) }, setUser: (identity: { email?: string; userId?: string; traits?: Record }) => { outlit.setUser(identity) }, clearUser: () => { outlit.clearUser() }, user: outlit.user, } } export const outlitStore = createOutlitStore() ``` Use in components: ```svelte theme={null} ``` ## Environment variables Configure environment variables: ```bash theme={null} # .env # Public key (used in browser) PUBLIC_OUTLIT_KEY=pk_your_public_key_here # Server key (used in server API routes) OUTLIT_KEY=pk_your_public_key_here ``` Keep server environment variables out of client-side code. Only use `OUTLIT_KEY` in server-side API routes. ## TypeScript support Full TypeScript support is included: ```typescript theme={null} import outlit, { type OutlitOptions } from '@outlit/browser' const options: OutlitOptions = { publicKey: PUBLIC_OUTLIT_KEY, trackPageviews: true, flushInterval: 5000 } outlit.init(options) ``` ## Next steps Track events from your backend Learn how visits and events link to people Full API reference Use Outlit with React # Vue Source: https://docs.outlit.ai/tracking/browser/vue First-class Vue 3 support with plugin and composables. ## Installation ```bash npm theme={null} npm install --save @outlit/browser ``` ```bash pnpm theme={null} pnpm add @outlit/browser ``` ```bash yarn theme={null} yarn add @outlit/browser ``` ```bash bun theme={null} bun add @outlit/browser ``` ## Quick start Install the `OutlitPlugin` in your app's entry point and use composables in components. If your app has user authentication, use the `useOutlitUser` composable to automatically sync user identity. This is the recommended approach as it automatically handles login/logout transitions. ```typescript theme={null} // main.ts import { createApp } from 'vue' import { OutlitPlugin } from '@outlit/browser/vue' import App from './App.vue' const app = createApp(App) app.use(OutlitPlugin, { publicKey: import.meta.env.VITE_OUTLIT_KEY, trackPageviews: true, }) app.mount('#app') ``` ```vue theme={null} ``` For marketing sites without authentication, just install the plugin: ```typescript theme={null} // main.ts import { createApp } from 'vue' import { OutlitPlugin } from '@outlit/browser/vue' import App from './App.vue' const app = createApp(App) app.use(OutlitPlugin, { publicKey: import.meta.env.VITE_OUTLIT_KEY, trackPageviews: true, }) app.mount('#app') ``` Set your public key in `.env`: ``` VITE_OUTLIT_KEY=pk_your_public_key_here ``` ### Using the composable Track custom events with the `useOutlit` composable: ```vue theme={null} ``` ## Plugin configuration ### OutlitPlugin The `OutlitPlugin` initializes tracking and provides context to all components. ```typescript theme={null} import { OutlitPlugin } from '@outlit/browser/vue' app.use(OutlitPlugin, { publicKey: 'pk_xxx', trackPageviews: true, trackForms: true, trackEngagement: true, formFieldDenylist: ['custom_secret_field'], flushInterval: 5000, }) ``` ### Options Your organization's public key. API endpoint for sending events. Automatically track pageviews on route changes. Automatically capture form submissions. Additional field names to exclude from form capture. How often to flush queued events (ms). Controls when tracking starts. `"auto"` (default) asks the Outlit edge endpoint whether the visitor's region requires opt-in consent: tracking enables automatically outside the EEA, UK, and Switzerland, and stays off in those regions or when the region can't be determined. The check fails closed on errors. `true` enables immediately; `false` waits for `enableTracking()` from the `useOutlit` composable. `isTrackingEnabled` updates automatically when auto mode enables tracking asynchronously. Automatically identify users when they submit forms containing an email field. Set to `false` to disable and call `identify()` manually. Track engagement metrics (active time on page). Idle timeout in milliseconds for engagement tracking. Track booking events from calendar embeds (Cal.com, Calendly). ## User identity patterns The `useOutlitUser` composable is the **recommended** way to handle user identity. It's simpler and more reliable than manual `identify()` calls because: * **Automatic lifecycle management**: Login/logout transitions are handled automatically * **Reactive**: Uses Vue's reactivity system to sync with your auth state * **Cleaner code**: No need for watchers with manual `setUser()` calls ### Pattern 1: with Pinia store ```vue theme={null} ``` ### Pattern 2: with composable auth ```vue theme={null} ``` When the user ref changes (login/logout), `useOutlitUser` automatically calls `setUser()` or `clearUser()` internally. You don't need to call these methods manually. ## Composables ### useOutlit The primary composable for tracking events and identifying users. ```vue theme={null} ``` #### Returns Track a custom event. Identify the current visitor. Set the current user identity. Ideal for SPA authentication flows. Clear the current user identity (on logout). User identity namespace: * `user.identify(options)` - Identify the user Get the current visitor's ID. Returns `null` if tracking is not enabled. Whether tracking is currently enabled. Will be `false` until `enableTracking()` is called when `autoTrack` is `false`, while the `"auto"` region check is pending, or when that check requires consent or fails. Enable tracking. Call this after obtaining user consent. Needed when `autoTrack` is `false`, or in the default `"auto"` mode when the visitor's region requires consent or the region check fails. Disable tracking and flush any pending events. Use this when a user declines or revokes consent. Whether the tracker is ready. ### useTrack A convenience composable that returns just the `track` function: ```vue theme={null} ``` ### useIdentify A convenience composable that returns just the `identify` function: ```vue theme={null} ``` ### useOutlitUser Automatically syncs a reactive user ref with Outlit: ```vue theme={null} ``` ## Consent management The default `autoTrack: "auto"` mode already keeps tracking off for visitors in regions that require opt-in consent (EEA, UK, Switzerland, or unknown). `enableTracking()` — typically wired to your consent banner's accept action — is what grants consent and is persisted for future sessions. To wait for `enableTracking()` instead — unless a previous opt-in is stored: ```typescript theme={null} // main.ts app.use(OutlitPlugin, { publicKey: import.meta.env.VITE_OUTLIT_KEY, autoTrack: false, // Don't track until consent }) ``` ```vue theme={null} ``` ## Activation event Track your value milestone as an ordinary event once it succeeds: ```vue theme={null} ``` Choose this event as your activation event in Outlit. The first matching event records activation once for each eligible resolved person and company, independently. Billing status comes from verified integrations such as Stripe. See [Customer journey](/concepts/customer-journey). ## Vue Router integration Track pageviews automatically with Vue Router: ```typescript theme={null} // router/index.ts import { createRouter, createWebHistory } from 'vue-router' const router = createRouter({ history: createWebHistory(), routes: [] }) export default router ``` If you set `trackPageviews: true` during plugin install (default), pageviews are tracked automatically. Vue Router's History API changes are detected by the SDK. ## Pinia integration Track state changes with Pinia: ```typescript theme={null} // stores/user.ts import { defineStore } from 'pinia' export const useUserStore = defineStore('user', { state: () => ({ user: null as User | null }), actions: { async login(email: string, password: string) { const user = await api.login(email, password) this.user = user // Identity is synced automatically via useOutlitUser() }, logout() { this.user = null // Identity is cleared automatically via useOutlitUser() } } }) ``` ## TypeScript support Full TypeScript support is included: ```typescript theme={null} import { useOutlit, useTrack, useIdentify, useOutlitUser, OutlitPlugin, type OutlitPluginOptions, type UseOutlitReturn, } from '@outlit/browser/vue' import type { UserIdentity } from '@outlit/browser' // Types are inferred const { track, identify, setUser } = useOutlit() // User identity type const user: UserIdentity = { email: 'user@example.com', userId: 'usr_123', traits: { name: 'Jane' } } ``` ## SSR with Nuxt For Nuxt applications, see the dedicated [Nuxt integration guide](/tracking/browser/nuxt). ## Next steps Use Outlit with Nuxt 3 Track events from your backend Learn how visits and events link to people Full API reference # How tracking works Source: https://docs.outlit.ai/tracking/how-it-works Follow a customer from first website visit through identity, product use, and conversion. The Outlit Browser SDK records how a visitor discovers and lands on your site: the first page URL, referrer, and available UTM parameters. It follows anonymous pageviews and site events until the person identifies. Outlit then joins that earlier history to the contact, so you can follow their journey from first visit through conversion and later product use. The [customer context graph](/concepts/customer-context-graph) connects this activity with billing, CRM, conversations, and support. ## Capture activity Use the [Outlit Browser SDK](/tracking/browser/script) for pageviews, form submissions, engagement, and custom events on your site. Use the [Outlit Node.js SDK](/tracking/server/nodejs) or [Outlit Rust SDK](/tracking/server/rust) for backend or native actions such as a completed export or API operation. These Outlit server SDKs can record identified or fingerprint-linked product events, but cannot observe a visitor's browser landing page or link an unknown browser visit by themselves. Identify the browser visitor with their email when it becomes available; an email-only server identify does not link an otherwise anonymous browser visitor. [PostHog](/integrations/posthog) and Mixpanel integrations import product activity into Outlit. The Outlit Browser SDK gives Outlit its own record of the anonymous website journey and links it to the contact when the browser identifies them. Billing status comes from a verified integration such as [Stripe](/integrations/stripe). CRM, conversations, and support integrations add account records and evidence. Start with [Connect your data](/getting-started/connect-data) to see which sources your workspace needs. ## Link activity to people and customers The Outlit Browser SDK gives an anonymous visitor an ID once tracking is enabled. When someone submits an email, signs up, or logs in, browser `identify()` or `setUser()` links that visitor's earlier activity to a known person. Outlit can then associate that person with a customer through a company email domain, explicit `customerId`, or connected source data. Personal-email addresses remain separate unless another source links them. See [Identity resolution](/concepts/identity-resolution) for the matching rules and review steps. With the default `autoTrack: "auto"` mode, the browser SDK keeps tracking off—and stores nothing—in regions that require opt-in consent (EEA, UK, Switzerland, or unknown) until your site calls `enableTracking()`; `autoTrack: false` makes tracking wait for an explicit consent signal unless a previous opt-in is stored. The script and npm guides show the consent pattern. A persisted opt-out continues across visits until tracking is enabled again. ## Read the customer timeline Customer profiles show activity from connected sources in chronological order. Product events, billing changes, support tickets, messages, and meetings appear with their source context where available. Facts and assessments build on that evidence; check the source and timestamp before treating a finding as current. [Customers](/product/customers) explains profile sections and source coverage. Each person's journey stage and the customer's billing status are separate. Outlit derives engagement and inactivity from product activity. If you choose an ordinary event as the activation event, its first match records activation once for each eligible resolved person and company, independently. SDK events do not change billing status. See [Customer journey](/concepts/customer-journey) for all stages, including **Past due**. ## Use the context Your team reviews churn and renewal situations in **Attention**. Developers and agents can also read authorized customer context through the [CLI](/cli/overview), [MCP](/ai-integrations/mcp), or [public tool gateway](/api-reference/tools). Use the [Ingest API](/api-reference/ingest) for custom event ingestion. ## Privacy and delivery The Outlit Browser SDK strips recognized sensitive form fields before sending them. Configure enabled domains under **Settings → Website Visitors**; the public tracking key is visible in browser code by design. If your site needs consent, use the [consent controls](/tracking/browser/script#consent-management) before collecting activity. For retention or deletion requests, ask your Outlit contact about your workspace policy. ## Next steps Install the Outlit Browser SDK and check a customer timeline. See how tracking and integrations feed customer profiles. # Track your first event Source: https://docs.outlit.ai/tracking/quickstart Add the Outlit Browser SDK, identify a visitor, and check that an event reaches Outlit. Use the Outlit Browser SDK script to capture the first visit and send a product event from your site. You need a public tracking key from **Settings → Website Visitors** in the Outlit app and an enabled domain for the site where you test. If your workspace is still being set up, start with [Connect your data](/getting-started/connect-data). ## Add the tracker Place this tag before the closing `` tag and replace the example key. The public key is designed to appear in browser code; the enabled-domain list controls which sites can send events with it. ```html theme={null} ``` Pageviews and form submissions are tracked automatically. Pageview events include the landing URL, browser referrer, and available UTM parameters, preserving how a visitor arrived before they identify. Because the script loads asynchronously, use this helper for the calls below. Put it after the script tag; it waits for the script when needed. ```javascript theme={null} function withOutlit(callback) { if (window.outlit?.track) return callback() document.getElementById('outlit-script').addEventListener('load', callback, { once: true }) } ``` The default `data-auto-track` behavior already keeps tracking off in regions that require opt-in consent (EEA, UK, Switzerland, or unknown). To wait for `enableTracking()` instead — unless a previous opt-in is stored — set `data-auto-track="false"` on the tag and call `window.outlit.enableTracking()` only after the visitor accepts. A saved opt-out persists across visits until tracking is enabled again. See [Consent management](/tracking/browser/script#consent-management) for the complete pattern. ## Identify a visitor After someone signs in or gives you their email, identify the same browser visitor with that email to link earlier website activity to a known contact. Include `userId` when your app has one; a call without either an `email` or `userId` has no effect. ```javascript theme={null} withOutlit(() => window.outlit.identify({ email: 'jane@example.com', userId: 'usr_12345' })) ``` Use real identity values from your app. If you wait for consent, call `identify()` after tracking is enabled. For identity that must be queued before consent, use [the script tag's `setUser()` pattern](/tracking/browser/script#set-user-identity). ## Send a product event Call `track()` when a meaningful action succeeds and tracking is allowed: ```javascript theme={null} withOutlit(() => window.outlit.track('report_exported', { format: 'csv' })) ``` An ordinary event does not change billing status. If this is the value milestone you want to use for activation, select it as your activation event in Outlit. The first matching event records activation once for each eligible resolved person and company, independently. See [Customer journey](/concepts/customer-journey). ## Check the result Open the identified customer's profile in **Customers** and look for the event in its activity timeline. If nothing appears, confirm the test domain is enabled under **Settings → Website Visitors**, tracking consent is active, and the identity call included `email` or `userId`. The [script tag troubleshooting guide](/tracking/browser/script#troubleshooting) covers delivery and loading problems. ## Next steps Configure automatic tracking, consent, and identity. Add tracking to a JavaScript app with explicit configuration. Send events from a backend or background job. See how events join customer profiles and timelines. # Outlit Node.js SDK Source: https://docs.outlit.ai/tracking/server/nodejs Server-side tracking for backend applications. The Outlit Node.js SDK records backend product activity when you have user identity or an account ID. It supports: * **Identified tracking** - Use `email` or `userId` for user-scoped resolution * **Customer attribution** - Add `customerId` for account/workspace context, even before a user is known * **Efficient batching** - Events are queued and sent in batches * **Device tracking** - Use `fingerprint` for desktop/mobile apps ([learn more](/concepts/website-visitors#device-tracking-non-browser)) Use it for product events that happen in your backend: feature usage from APIs, completed report exports, and background jobs. For the visitor's first landing page, referrer, UTM data, and anonymous website activity, use the [Outlit Browser SDK](/tracking/browser/script) and identify that browser visitor when their email becomes available. An email-only server identify cannot link an otherwise anonymous browser visitor. Billing status comes from a verified integration such as Stripe. ## Installation ```bash npm theme={null} npm install --save @outlit/node ``` ```bash pnpm theme={null} pnpm add @outlit/node ``` ```bash yarn theme={null} yarn add @outlit/node ``` ```bash bun theme={null} bun add @outlit/node ``` ## Quick start ```typescript theme={null} import { Outlit } from '@outlit/node' const outlit = new Outlit({ publicKey: 'pk_your_public_key' }) // Track an event outlit.track({ email: 'user@example.com', customerId: 'cust_123', // Your app's account/workspace/customer ID eventName: 'subscription_upgraded', properties: { fromPlan: 'starter', toPlan: 'pro', mrr: 99 } }) // Important: flush before process exits await outlit.flush() ``` ## Configuration ```typescript theme={null} const outlit = new Outlit({ publicKey: 'pk_xxx', flushInterval: 10000, // ms, default 10s maxBatchSize: 100, // events per batch timeout: 10000, // request timeout ms }) ``` ### Options Your organization's public key. API endpoint for sending events. How often to automatically flush events (milliseconds). Maximum events per batch before auto-flush. HTTP request timeout in milliseconds. ## API reference ### `outlit.track(options)` Track a custom event for a user, a customer, or both. ```typescript theme={null} outlit.track({ email: 'jane@acme.com', userId: 'usr_12345', // optional if email provided customerId: 'cust_123', // Your app's account/workspace/customer ID eventName: 'report_exported', properties: { reportType: 'sales', format: 'pdf', rowCount: 1500 }, timestamp: Date.now() // optional, defaults to now }) ``` User's email address. Resolves immediately to a customer profile. Your system-owned user/contact ID. Device identifier for anonymous tracking. See [Device Tracking](/concepts/website-visitors#device-tracking-non-browser). Your system-owned customer/account/workspace ID. Name of the event. Use `snake_case`. Additional data to attach to the event. Unix timestamp in milliseconds. Defaults to current time. At least one of `email`, `userId`, `fingerprint`, or `customerId` is required for `track()`. `customerId`-only events are valid immediately and can later link to a resolved customer when `identify()` uses the same `customerId` with an email. ### `outlit.identify(options)` Update user traits without tracking an event. Customer metadata can be included alongside the user identity. ```typescript theme={null} outlit.identify({ email: 'jane@acme.com', userId: 'usr_12345', customerId: 'cust_123', // Your app's account/workspace/customer ID customerTraits: { plan: 'enterprise', seats: 50 }, traits: { name: 'Jane Doe', role: 'admin' } }) ``` User's email address. Use this or `userId` to establish user-scoped identity. Your system-owned user/contact ID. Device identifier for linking anonymous events. See [Device Tracking](/concepts/website-visitors#device-tracking-non-browser). Properties to update on the user profile. Your system-owned customer/account/workspace ID. Send this with `email` or `userId` to link the account/workspace to the resolved contact profile. Properties to update on the customer profile. At least one of `email` or `userId` is required for `identify()`. Customer fields are optional additions. ### Lifecycle and billing Send ordinary product facts with `track()`. For activation, choose the ordinary event that represents your product's value milestone and track it after the action succeeds: ```typescript theme={null} outlit.track({ email: 'user@example.com', eventName: 'onboarding_completed', properties: { flow: 'onboarding' } }) ``` Choose this event as your activation event in Outlit. The first matching event records activation once for each eligible resolved person and company, independently. Billing status comes from verified integrations such as Stripe. See [Customer journey](/concepts/customer-journey). ### `outlit.flush()` Immediately send all queued events. **Call this before your process exits.** ```typescript theme={null} await outlit.flush() ``` ### `outlit.shutdown()` Shut down gracefully: flushes remaining events and stops the timer. ```typescript theme={null} await outlit.shutdown() ``` ### `outlit.queueSize` Get the number of events waiting to be sent. ```typescript theme={null} console.log(`Pending events: ${outlit.queueSize}`) ``` ## Framework examples ### Express.js ```typescript theme={null} import express from 'express' import { Outlit } from '@outlit/node' const app = express() const outlit = new Outlit({ publicKey: 'pk_xxx' }) // Middleware to attach tracker to request app.use((req, res, next) => { req.outlit = outlit next() }) app.post('/api/reports/export', async (req, res) => { const { user, format } = req.body // Your export logic const report = await generateReport(format) // Track the event req.outlit.track({ email: user.email, eventName: 'report_exported', properties: { format, size: report.size } }) res.json({ success: true }) }) // Graceful shutdown process.on('SIGTERM', async () => { await outlit.shutdown() process.exit(0) }) ``` ### Next.js API routes ```typescript theme={null} // app/api/subscription/upgrade/route.ts import { Outlit } from '@outlit/node' const outlit = new Outlit({ publicKey: process.env.OUTLIT_KEY! }) export async function POST(request: Request) { const { userId, email, customerId, newPlan } = await request.json() // Your upgrade logic await upgradeSubscription(userId, newPlan) // Track the event outlit.track({ email, userId, customerId, eventName: 'subscription_upgraded', properties: { plan: newPlan } }) // Flush immediately (serverless) await outlit.flush() return Response.json({ success: true }) } ``` ### Serverless functions (AWS Lambda, Vercel) In serverless environments, **always call `flush()` before returning**. The function may be terminated before the background flush runs. ```typescript theme={null} import { Outlit } from '@outlit/node' const outlit = new Outlit({ publicKey: process.env.OUTLIT_KEY! }) export async function handler(event) { const { email, action } = JSON.parse(event.body) outlit.track({ email, eventName: action, properties: { source: 'lambda' } }) // Critical: flush before returning await outlit.flush() return { statusCode: 200, body: 'OK' } } ``` ### Stripe webhooks Connect Stripe as a verified Outlit integration. Subscription state then updates account billing status automatically; do not translate Stripe webhooks into SDK events. ### Background jobs (Bull, Agenda) ```typescript theme={null} import { Queue, Worker } from 'bullmq' import { Outlit } from '@outlit/node' const outlit = new Outlit({ publicKey: 'pk_xxx' }) const worker = new Worker('emails', async (job) => { const { email, templateId } = job.data // Your job logic await sendEmail(email, templateId) // Track completion outlit.track({ email, eventName: 'email_sent', properties: { templateId, jobId: job.id } }) }) // Periodic flush (events batch automatically) // No need to flush per job unless critical ``` ## Common events to track Here are recommended server-side events: | Event | When | Properties | | - | - | - | | `feature_used` | Feature usage | `feature`, `count` | | `api_key_created` | API key created | `keyName`, `permissions` | | `export_completed` | Data export | `format`, `rowCount`, `size` | | `invite_sent` | Team invite | `role` | | `user_role_changed` | Permission change | `fromRole`, `toRole` | ## Error handling The SDK silently handles errors to not disrupt your application. For debugging: ```typescript theme={null} const outlit = new Outlit({ publicKey: 'pk_xxx' }) // Events are queued even if the network is down outlit.track({ email: 'user@test.com', eventName: 'test' }) // Check queue size to detect potential issues if (outlit.queueSize > 500) { console.warn('Outlit queue backing up - possible connectivity issue') } ``` ## TypeScript support Full TypeScript support is included: ```typescript theme={null} import { Outlit, type OutlitOptions, type ServerTrackOptions, type ServerIdentifyOptions, } from '@outlit/node' const options: OutlitOptions = { publicKey: 'pk_xxx', flushInterval: 5000, } const outlit = new Outlit(options) const trackOptions: ServerTrackOptions = { email: 'user@test.com', customerId: 'cust_123', // Your app's account/workspace/customer ID eventName: 'test', properties: { key: 'value' } } outlit.track(trackOptions) ``` ## Next steps Learn how activity links across sources Understand contact stages and account billing # Outlit Rust SDK Source: https://docs.outlit.ai/tracking/server/rust Track product events from Rust desktop apps, CLI tools, and backend services. The Outlit Rust SDK records product activity in native applications and backend services: * **Desktop apps** - Tauri, native GUI applications * **CLI tools** - Command-line applications * **Backend services** - Rust-based APIs For anonymous tracking before user sign-in, use `fingerprint`. See [Device Tracking](/concepts/website-visitors#device-tracking-non-browser) for details. To capture how someone first arrived and browsed your website, use the [Outlit Browser SDK](/tracking/browser/script) and identify that browser visitor when their email becomes available. An email-only server identify cannot link an otherwise anonymous browser visit. ## Installation ```bash theme={null} cargo add outlit ``` The SDK is async and requires a tokio runtime. Most async frameworks (Tauri, Axum, Actix) already include tokio. ## Quick start ```rust theme={null} use outlit::{Outlit, email, fingerprint}; #[tokio::main] async fn main() -> Result<(), outlit::Error> { let client = Outlit::builder("pk_your_public_key") .build()?; // Track with email (resolves immediately) client.track("feature_used", email("user@example.com")) .property("feature", "export") .send() .await?; // Flush before exit client.shutdown().await?; Ok(()) } ``` ## Configuration ```rust theme={null} use std::time::Duration; let client = Outlit::builder("pk_xxx") .flush_interval(Duration::from_secs(10)) .max_batch_size(100) .timeout(Duration::from_secs(10)) .build()?; ``` ### Options Your organization's public key. API endpoint for sending events. How often to automatically flush events. Maximum events per batch before auto-flush. HTTP request timeout. ## API reference ### `client.track(event_name, identity)` Track a custom event. ```rust theme={null} // With email client.track("subscription_upgraded", email("user@example.com")) .property("plan", "pro") .property("mrr", 99) .send() .await?; // With user_id client.track_by_user_id("feature_used", user_id("usr_123")) .email("user@example.com") // optional, for linking .property("feature", "export") .send() .await?; // With fingerprint (device tracking) client.track_by_fingerprint("app_opened", fingerprint("device_abc123")) .property("version", "1.2.0") .send() .await?; ``` #### Builder methods Add email to the event (if not primary identity). Add user ID to the event. Add device fingerprint for linking. Add a property to the event. Call multiple times for multiple properties. Set custom timestamp (milliseconds since epoch). ### `client.identify(identity)` Identify a user and optionally link device events. ```rust theme={null} client.identify(email("user@example.com")) .user_id("usr_123") .fingerprint("device_abc123") // Links this device's events .trait_("name", "Jane Doe") .trait_("plan", "enterprise") .send() .await?; ``` Device identifier. All events tracked with this fingerprint are retroactively linked to the user. Your internal user ID. Add a trait to the user profile. ### Lifecycle and billing Track your value milestone as an ordinary event once it succeeds: ```rust theme={null} client.track("onboarding_completed", email("user@example.com")) .property("flow", "onboarding") .send() .await?; ``` Choose this event as your activation event in Outlit. The first matching event records activation once for each eligible resolved person and company, independently. Billing status comes from verified integrations such as Stripe. See [Customer journey](/concepts/customer-journey). ### `client.flush()` Immediately send all queued events. ```rust theme={null} client.flush().await?; ``` ### `client.shutdown()` Shut down gracefully: flushes remaining events and stops the timer. ```rust theme={null} client.shutdown().await?; ``` Always call `shutdown()` or `flush()` before your application exits to ensure all events are sent. ## Tauri integration For Tauri desktop apps, initialize the client in your Rust backend. This example uses the `uuid` crate for generating device IDs. Add it with `cargo add uuid --features v4`. ```rust theme={null} use outlit::{Outlit, email, fingerprint}; use tauri::Manager; use std::sync::Arc; // Store device fingerprint persistently fn get_or_create_device_id(app: &tauri::AppHandle) -> String { let config_dir = app.path().app_config_dir().unwrap(); let id_file = config_dir.join("device_id"); if let Ok(id) = std::fs::read_to_string(&id_file) { return id; } let id = uuid::Uuid::new_v4().to_string(); std::fs::create_dir_all(&config_dir).ok(); std::fs::write(&id_file, &id).ok(); id } #[tauri::command] async fn track_event( app: tauri::AppHandle, event_name: String, properties: serde_json::Value, email: Option, ) -> Result<(), String> { let client = app.state::>(); let device_id = get_or_create_device_id(&app); if let Some(email) = email { // User is signed in client.track(&event_name, outlit::email(&email)) .fingerprint(&device_id) .property("data", properties) .send() .await .map_err(|e| e.to_string())?; } else { // Anonymous tracking client.track_by_fingerprint(&event_name, fingerprint(&device_id)) .property("data", properties) .send() .await .map_err(|e| e.to_string())?; } Ok(()) } fn main() { let client = Arc::new( Outlit::builder("pk_xxx") .build() .expect("Failed to create Outlit client") ); tauri::Builder::default() .manage(client.clone()) .invoke_handler(tauri::generate_handler![track_event]) .build(tauri::generate_context!()) .expect("error building tauri app") .run(|app, event| { if let tauri::RunEvent::Exit = event { // Flush on exit let client = app.state::>(); tauri::async_runtime::block_on(client.shutdown()).ok(); } }); } ``` ## Error handling The SDK returns `Result<(), outlit::Error>` for operations that can fail: ```rust theme={null} match client.track("event", email("user@test.com")).send().await { Ok(()) => println!("Event tracked"), Err(outlit::Error::Http(e)) => eprintln!("HTTP request failed: {}", e), Err(outlit::Error::Api { status, message }) => { eprintln!("API error (status {}): {}", status, message) } Err(outlit::Error::Shutdown) => eprintln!("Client was shutdown"), Err(e) => eprintln!("Error: {}", e), } ``` ## Next steps Anonymous tracking before user identification How activity links across identifiers # OpenClaw + Outlit Source: https://docs.outlit.ai/use-cases/openclaw Use Outlit customer context in OpenClaw and schedule read-only customer digests. OpenClaw can use the Outlit CLI to read authorized customer profiles, revenue, activity, and evidence. You can also schedule read-only digests to a chat channel connected to OpenClaw. Outlit continues to monitor churn and renewals in **Attention**; these digests help you review that work alongside other customer context. ## Setup ```bash theme={null} npm install -g @outlit/cli ``` ```bash theme={null} bun add -g @outlit/cli ``` ```bash theme={null} brew install outlitai/tap/outlit ``` ```bash theme={null} curl -fsSL https://outlit.ai/install.sh | bash ``` Detects your OS and architecture, downloads the latest binary to `~/.outlit/bin`, and adds it to your `PATH`. Verify with `outlit --version`. Get your API key from [**Settings → API keys**](https://app.outlit.ai/settings/workspace/api-keys), then: ```bash theme={null} outlit auth login ``` The CLI prompts for your key, validates it, and stores it securely. For OpenClaw, store `OUTLIT_API_KEY` through [SecretRefs](https://docs.openclaw.ai/gateway/secrets) so the agent can authenticate without a plaintext key in its configuration. Run `openclaw secrets configure` to set it up. Treat your API key like a password. It grants whatever capabilities its configured grants allow — potentially including customer access mutations. Revoke keys anytime from the Outlit app. ```bash theme={null} outlit setup openclaw ``` This installs the `outlit` skill into the OpenClaw skills target through the shared `skills` installer. OpenClaw detects and loads it automatically. ```bash theme={null} outlit doctor ``` You should see: ``` ✓ OpenClaw: Outlit skill installed ``` Then message OpenClaw in your chat platform: ``` List my paying customers sorted by MRR. ``` ## What OpenClaw can do with Outlit Once the skill is installed, OpenClaw learns how to use the full `outlit` CLI. You can ask it for customer context in natural language: | Ask OpenClaw\... | What it runs | | - | - | | "Show me paying customers who went silent this month" | `outlit customers list --billing-status PAYING --no-activity-in 30d` | | "Get the full profile for acme.com" | `outlit customers get acme.com --include users,revenue,recentTimeline` | | "What's been happening across Acme's customer communications?" | `outlit customers timeline acme.com --channels COMMUNICATION --limit 50` | | "Find inactive users at Acme Corp" | `outlit users list --customer-id --journey-stage INACTIVE` | | "Search for pricing concerns across all customers" | `outlit search "pricing concerns"` | | "What do we know about acme.com recently?" | `outlit facts list acme.com --status ACTIVE --limit 20` | | "Open the exact call behind that search result" | `outlit sources get --source-type CALL --source-id call_123` | | "Which event types fire most often?" | `outlit sql "SELECT event_type, COUNT(*) FROM activity GROUP BY 1 ORDER BY 2 DESC LIMIT 10"` | | "Show me the available analytics views" | `outlit schema` | The Outlit skill guides OpenClaw through JSON responses, money values in the billing provider's units (cents for USD, EUR, GBP, and CAD), and pagination. Check its answer against the returned evidence. ## Scheduled customer digests OpenClaw can deliver a recurring summary of Outlit data to your messaging channel. Its cron jobs read context; they do not replace the churn and renewal monitoring Outlit runs in **Attention**. ### Open attention digest (daily) Start with the items Outlit has already assessed and placed in **Attention**. ```bash theme={null} openclaw cron add \ --name "Open Attention Digest" \ --cron "0 9 * * *" \ --tz "America/New_York" \ --message "Run: outlit attention list --json. Summarize the open items by customer and priority, with the evidence available in each item. Link each item for human review. Do not infer that an empty list means monitoring is inactive." \ --announce ``` ### Trial health check (daily) Trials that went quiet after a week probably need a nudge before expiration. ```bash theme={null} openclaw cron add \ --name "Trial Health Check" \ --cron "0 9 * * *" \ --tz "America/New_York" \ --message "Run: outlit customers list --billing-status TRIALING --no-activity-in 7d. Summarize as a trial health report, list each trial, days since last activity, and suggest an outreach action." \ --announce ``` ### Revenue snapshot (weekly) Get a current MRR snapshot for your top accounts. This recipe assumes every account bills in one currency: the `customers` view doesn't name a currency, so the prompt states it. `mrr_cents` is in the billing provider's units: cents for USD, EUR, GBP, and CAD. Change USD to the currency you bill in; for a currency outside those four, replace the divide-by-100 step with your provider's amount rules (see [Stripe's currency docs](https://docs.stripe.com/currencies)). If your accounts bill in more than one currency, the ranking and the total would mix units. Use the `revenue` view instead, which has a `currency` column on each snapshot, and rank and total within each currency. ```bash theme={null} openclaw cron add \ --name "Revenue Snapshot" \ --cron "0 9 * * 1" \ --tz "America/New_York" \ --message "Run: outlit sql \"SELECT domain, mrr_cents FROM customers ORDER BY mrr_cents DESC LIMIT 15\". We bill in USD, so mrr_cents is cents: divide it by 100 for MRR in dollars. Format as a revenue snapshot table, show rank, and note the total MRR across all listed accounts." \ --announce ``` ### Inactive users (twice a week) Individual users who went inactive, even if their company account is still active. ```bash theme={null} openclaw cron add \ --name "Inactive Users" \ --cron "0 9 * * 2,4" \ --tz "America/New_York" \ --message "Run: outlit users list --journey-stage INACTIVE --limit 20. Summarize as an inactive users report, group by customer, show user emails and journey stage, and note any patterns." \ --announce ``` ### New signups overview (weekly) Friday recap of new customers that signed up during the week. ```bash theme={null} openclaw cron add \ --name "New Signups Overview" \ --cron "0 10 * * 5" \ --tz "America/New_York" \ --message "Run: outlit users list --journey-stage SIGNED_UP --order-by first_seen_at --limit 50. Summarize as a weekly new signups report, group users by customer, and note current activity level." \ --announce ``` Replace `America/New_York` with your timezone. OpenClaw supports any valid IANA timezone string. ### Managing cron jobs ```bash theme={null} openclaw cron list # View all scheduled jobs openclaw cron run JOB_ID # Test a job immediately openclaw cron disable JOB_ID # Pause without deleting openclaw cron enable JOB_ID # Resume a paused job openclaw cron rm JOB_ID # Delete a job ``` ## Outlit agent skills For other coding agents or optional SDK instrumentation guidance, see [Agent skills](/ai-integrations/skills). ## Troubleshooting | Problem | Solution | | - | - | | `outlit doctor` shows "Outlit skill not found" | Run `outlit setup openclaw` to install the skill | | OpenClaw says it doesn't know about Outlit | Confirm `OUTLIT_API_KEY` is available to OpenClaw through SecretRefs, then restart its Gateway | | Commands return authentication errors | Run `outlit auth status` to verify your key is valid | | Cron jobs aren't firing | Check `openclaw cron status` and verify timezone settings | | JSON parsing errors | Ensure you're running the latest Outlit CLI (`outlit upgrade`) | ## Next steps Learn more about OpenClaw's features: skills, cron, secrets, and more Review the churn and renewal work Outlit prepares.