> ## 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.
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.
## 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.
## 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.
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)
```
## 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
We use cookies to improve your experience.
```
## 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 (
)
}
```
## 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}
We use cookies to improve your experience.
```
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 `
Welcome
` 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}