[args] [options]
```
Phone numbers are **E.164** (`+14155552671`). Datetimes are **ISO-8601 with an offset** (`2026-05-23T14:30:00-04:00`).
## Every command at a glance
Read looks, never changes anything
·
Write creates or updates a record
·
Sensitive spends money or mints
a shareable link ·
Destructive removes access or
data
| Command | Type | Guard |
| ------------------------------------------------------------------------------------------------------ | ------------------------ | --------------------------- |
| [`contacts search`](#contacts-search) | Read | — |
| [`contacts get`](#contacts-get) | Read | — |
| [`contacts by-outcome`](#contacts-by-outcome) | Read | — |
| [`contacts create`](#contacts-create) | Write | — |
| [`contacts update`](#contacts-update) | Write | — |
| [`contacts delete`](#contacts-delete) | Destructive | `--confirm-phone` + `--yes` |
| [`leads search`](#leads-search) | Read | — |
| [`leads reveal`](#leads-reveal) | Sensitive | `--yes` |
| [`leads import`](#leads-import) | Write | — |
| [`sessions create`](#sessions-create) | Sensitive | `--yes` |
| [`sessions get`](#sessions-get) | Read | — |
| [`sessions update`](#sessions-update) | Write | — |
| [`sessions revoke`](#sessions-revoke) | Destructive | `--yes` |
| [`calls list`](#calls-list) | Read | — |
| [`outcomes log`](#outcomes-log) | Write | — |
| [`callbacks create`](#callbacks-create) | Write | — |
| [`callbacks list`](#callbacks-list) | Read | — |
| [`meetings schedule`](#meetings-schedule) | Write | — |
| [`campaigns list`](#campaigns-list) / [`campaigns get`](#campaigns-get) | Read | — |
| [`campaigns status`](#campaigns-status) | Write | org admin |
| [`campaigns leads`](#campaigns-leads) | Read | — |
| [`campaigns add-lead`](#campaigns-add-lead) / [`campaigns import-leads`](#campaigns-import-leads) | Write | org admin |
| [`campaigns delete-lead`](#campaigns-delete-lead) | Destructive | `--yes` + org admin |
| [`campaigns analytics`](#campaigns-analytics) | Read | — |
| [`analytics calls`](#analytics-calls) / [`analytics day`](#analytics-day) | Read | — |
| [`dnc list`](#dnc-list) | Read | — |
| [`dnc add`](#dnc-add) | Write | — |
| [`dnc remove`](#dnc-remove) | Destructive | `--yes` |
| [`pipelines list`](#pipelines-list) / [`pipelines results`](#pipelines-results) | Read | admin policy |
| [`voice-agents list`](#voice-agents-list) | Read | organization |
| [`voice-agents call`](#voice-agents-call) | Sensitive | `--yes` + organization |
| [`voice-agents call-result`](#voice-agents-call-result) | Read | organization |
| [`config show`](#config-and-knowledge) / [`config check`](#config-and-knowledge) | Read | — |
| [`tools`](#config-and-knowledge) / [`flow`](#config-and-knowledge) / [`prompt`](#config-and-knowledge) | Read | works offline |
## Contacts
### `contacts search`
```text theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee contacts search
```
Search by name, phone, email or company.
| Option | Description |
| ----------------- | ----------------- |
| `-p, --page ` | Page number |
| `-l, --limit ` | Page size, max 50 |
```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee contacts search acme
ringee contacts search "ada lovelace" --limit 5
```
### `contacts get`
```text theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee contacts get
```
Full details for one contact, including recent calls, notes and meetings.
### `contacts by-outcome`
```text theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee contacts by-outcome
```
Find contacts whose calls reached given outcomes — useful for deriving an ICP from closed business.
| Option | Description |
| ----------------------- | --------------------------------------------------- |
| `--match ` | `any` matches any call; `last` only the most recent |
| `--include-unreachable` | Include `doNotCall` and `unsubscribed` contacts |
| `-p, --page ` | Page number |
| `-l, --limit ` | Page size, max 50 |
```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee contacts by-outcome sale interested --match last
```
Valid outcomes: `meeting_booked`, `sale`, `interested`, `follow_up`, `callback_scheduled`, `not_interested`, `no_answer`, `voicemail`, `wrong_number`, `gatekeeper`.
### `contacts create`
| Option | Description |
| ------------------------------------------- | --------------------------------------------- |
| `--phone ` | **Required.** Must be unique in the workspace |
| `--name ` | Display name |
| `--first ` / `--last ` | Name parts |
| `--email ` | Email |
| `--title ` | Job title |
| `--company ` | Company name |
| `--note ` | Initial note |
```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee contacts create --phone +14155552671 --name "Ada Lovelace" --company "Babbage Engines"
```
### `contacts update`
```text theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee contacts update
```
Accepts `--phone`, `--name`, `--first`, `--last`, `--email`, `--title`, `--company`. Only the flags you pass change.
### `contacts delete`
```text theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee contacts delete
```
Destructive
**Destructive.** Requires both `--confirm-phone` matching the stored number
**and** `--yes`.
```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee contacts get # verify the number first
ringee contacts delete --confirm-phone +14155552671 --yes
```
## Leads
Prospecting uses your connected enrichment provider. These commands never spend Ringee calling credits.
### `leads search`
Returns a `jobId` you pass to `reveal` and `import`.
| Option | Description |
| ----------------------------------- | ------------------------------------------------- |
| `--provider ` | `apollo` or `prospeo`. Auto-selected when omitted |
| `--keywords ` | Free-text keywords |
| `--title ` | Job title (repeatable or comma-separated) |
| `--seniority ` | Seniority (repeatable) |
| `--department ` | Department (repeatable) |
| `--industry ` | Industry (repeatable) |
| `--country ` / `--city ` | Person location (repeatable) |
| `--company ` / `--domain ` | Company filters (repeatable) |
| `--has-email` / `--has-phone` | Only candidates with that field |
| `--page ` / `--per-page ` | Pagination, max 25 per page |
```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee leads search --title "VP Sales" --country US --industry Software
```
### `leads reveal`
```text theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee leads reveal
```
Sensitive
Spends provider credits
Unlocks email — and optionally phone — and upserts a Ringee contact.
| Option | Description |
| ----------- | --------------------------------------------------- |
| `--phone` | Also reveal a mobile phone (extra provider credits) |
| `-y, --yes` | **Required.** Confirms the credit spend |
### `leads import`
```text theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee leads import
```
Bulk-imports candidates as contacts, deduplicated by phone number. Does not reveal hidden emails or phones.
## Call sessions
### `sessions create`
Sensitive
Mints a magic link someone can open to dial through a queue without logging in.
| Option | Description |
| ------------------------- | ----------------------------------------------- |
| `--contact ` | Contact in the queue (repeatable) |
| `--phone ` | Raw phone in the queue (repeatable) |
| `--title ` | Session title |
| `--campaign ` | Attribute calls to a campaign |
| `--max-calls ` | Auto-complete after N calls |
| `-y, --yes` | **Required.** Confirms minting a shareable link |
```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee sessions create --contact --contact --title "Tue outbound" --yes
```
The join URL is printed **once** and cannot be re-fetched. Share it only with
someone authorized to call on the workspace's behalf.
### `sessions get`
```text theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee sessions get
```
Status and progress. Never prints the raw token.
### `sessions update`
```text theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee sessions update
```
| Option | Description |
| ------------------------- | ----------------------------------- |
| `--title ` | New title |
| `--campaign ` | Set a campaign, or `null` to detach |
| `--expires ` | Extend validity |
### `sessions revoke`
```text theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee sessions revoke
```
Destructive
**Destructive.** Requires `--yes`. The link stops working immediately; call
history is preserved.
## Calls and follow-ups
### `calls list`
Full call detail, newest first.
| Option | Description |
| ------------------------------------ | ------------------------------------------------- |
| `--contact ` | Filter to one contact |
| `--outcome ` | Filter by logged outcome |
| `--status ` | Filter by status, e.g. `completed` |
| `--from ` / `--to ` | Created-at range |
| `--full` | Print the full transcription instead of a preview |
| `-p, --page ` / `-l, --limit ` | Pagination, max 50 |
```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee calls list --outcome sale --from 2026-05-01T00:00:00Z --full
```
### `outcomes log`
```text theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee outcomes log
```
| Option | Description |
| --------------- | ------------------------ |
| `--note ` | Free-text follow-up note |
### `callbacks create`
```text theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee callbacks create
```
| Option | Description |
| ----------------- | -------------- |
| `--call ` | Source call id |
| `--note ` | Free-text note |
```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee callbacks create 2026-05-24T10:00:00-04:00 --note "Asked for Monday"
```
### `callbacks list`
```text theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee callbacks list
```
Lists callbacks soonest first, with the contact and originating campaign.
| Option | Description |
| ------------------------------------ | ----------------------------------------------------------------------- |
| `--status ` | `scheduled`, `due`, `in_progress`, `completed`, `missed` or `cancelled` |
| `-p, --page ` / `-l, --limit ` | Pagination, max 50 |
### `meetings schedule`
```text theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee meetings schedule
```
| Option | Description |
| ------------------------- | ------------------------------------------------------ |
| `--title ` | Meeting title |
| `--duration ` | Defaults to 30 |
| `--location ` | Address or video URL |
| `--email ` | Send an external calendar invitation when connected |
| `--call ` | Source call; also sets its outcome to `meeting_booked` |
| `--notes ` | Notes |
## Campaigns
Campaign commands require an active organization workspace. Every write is restricted to organization admins.
### `campaigns list`
```text theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee campaigns list
```
| Option | Description |
| ------------------------------------ | ------------------------------------------ |
| `-s, --search ` | Filter by name or description |
| `--status ` | `draft`, `active`, `paused` or `completed` |
| `-p, --page ` / `-l, --limit ` | Pagination, max 50 |
### `campaigns get`
```text theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee campaigns get
```
Returns the campaign configuration, lead count, dialer mode, attempts, retry delay, wrap-up time and working hours.
### `campaigns status`
```text theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee campaigns status
```
The server accepts only these transitions:
* `draft` → `active`
* `active` → `paused` or `completed`
* `paused` → `active` or `completed`
* `completed` is terminal
Activating a campaign also requires at least one lead, one configured disposition and a usable outbound number.
### `campaigns leads`
```text theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee campaigns leads
```
| Option | Description |
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--status ` | `pending`, `queued`, `locked`, `dialing`, `in_call`, `wrap_up`, `dispositioned`, `scheduled`, `completed`, `exhausted`, `dnc`, `called` or `dead` |
| `-p, --page ` / `-l, --limit ` | Pagination, max 50 |
### `campaigns add-lead`
```text theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee campaigns add-lead --phone --name
```
Reuses an existing contact with the same phone number when one exists.
| Option | Description |
| --------------------- | ------------------------------- |
| `--phone ` | **Required.** Lead phone number |
| `--name ` | **Required.** Lead name |
| `--email ` | Email |
| `--company ` | Company |
| `--title ` | Job title |
| `--state ` | State or region |
| `--website ` | Website |
### `campaigns import-leads`
```text theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee campaigns import-leads
```
The file must contain a non-empty JSON array with at most 200 entries:
```json theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
[
{
"name": "Ada Lovelace",
"phone": "+14155552671",
"email": "ada@example.com",
"company": "Analytical Engines",
"jobTitle": "Founder",
"state": "CA",
"website": "https://example.com"
}
]
```
### `campaigns delete-lead`
```text theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee campaigns delete-lead --yes
```
**Destructive.** Removes the lead, its call attempts and its campaign
callbacks. The underlying contact is preserved. A lead that is locked,
dialing, in a call or wrapping up cannot be removed.
### `campaigns analytics`
```text theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee campaigns analytics
```
| Option | Description |
| ----------------------------- | ---------------------------------------- |
| `--from ` / `--to ` | Reporting window |
| `--no-agents` | Skip the per-agent breakdown |
| `--hourly` | Include the hourly call-volume histogram |
The result includes attempts, connections, conversions, rates, handle time, dispositions and lead status totals.
## Analytics
### `analytics calls`
```text theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee analytics calls
```
Reads the same call-performance data shown on the dashboard overview.
| Option | Description |
| ------------------------------- | -------------------------------------------------------------------------------- |
| `-r, --range ` | `today`, `yesterday`, `7d`, `30d`, `this_month` or `last_month` |
| `--from ` / `--to ` | Custom reporting window |
| `--campaign ` | One campaign, or only calls outside campaigns |
| `--outcome ` | Restrict every metric to one outcome |
| `--scope ` | `personal` or `organization` |
| `--member ` | One member; organization admins only |
| `--include ` | Any of `kpis`, `funnel`, `by-outcome`, `over-time`, `best-time-of-day`, `agents` |
If `--include` is omitted, Ringee computes `kpis`, `funnel` and `by-outcome`.
```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee analytics calls --range 30d --campaign none
ringee analytics calls --scope organization --include kpis agents
```
### `analytics day`
```text theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee analytics day
```
Returns calls, callbacks and meetings for one calendar day.
| Option | Description |
| ------------------------------- | ---------------------------------------------------------- |
| `--offset ` | Day boundary offset, for example `-04:00`; defaults to UTC |
| `--campaign ` | One campaign, or calls outside campaigns |
| `--outcome ` | Include only these call outcomes |
| `--no-callbacks` | Skip callbacks |
| `--no-meetings` | Skip meetings |
| `-l, --limit ` | Maximum calls returned, default 50 and max 100 |
## Do-not-call list
### `dnc list`
```text theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee dnc list
```
| Option | Description |
| ------------------------------------ | ------------------------------- |
| `-s, --search ` | Filter by phone-number fragment |
| `-p, --page ` / `-l, --limit ` | Pagination, max 50 |
### `dnc add`
```text theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee dnc add
```
Adds one or more E.164 numbers to the workspace suppression list. Every future Ringee dial surface, including AI Voice Agents, checks this list before calling.
| Option | Description |
| ------------------- | ------------------------------- |
| `--reason ` | Why the numbers were suppressed |
### `dnc remove`
```text theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee dnc remove --yes
```
**Destructive compliance change.** Releasing a number makes it callable again.
Verify the entry with `dnc list` and remove it only when the person has asked
to be callable.
## AI pipelines
Pipeline reads follow the admin policy: freelancers can read their personal context; inside an organization, the caller must be an organization admin.
### `pipelines list`
```text theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee pipelines list
```
Lists the available pipelines, enabled contexts, pending actions and newly eligible calls.
### `pipelines results`
```text theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee pipelines results [context]
```
`` is one of `follow_up_recommendations`, `script_optimization` or `objection_intelligence`.
Choose exactly one analysis context:
| Option | Description |
| ------------------------- | -------------------------------------------------------------------- |
| `--campaign ` | Calls attributed to one campaign |
| `--org` | Organization calls outside campaigns; the default in an organization |
| `--personal` | Personal calls in a freelancer workspace |
| `--status ` | `pending` (default), `completed`, `dismissed` or `snoozed` actions |
```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee pipelines results objection_intelligence --org
ringee pipelines results follow_up_recommendations --campaign --status pending
```
## AI Voice Agents
These commands require an active organization workspace. A voice-agent call runs server-side; the CLI can exit while the conversation continues.
### `voice-agents list`
```text theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee voice-agents list
```
Lists agents, their type and status, required variables, default caller number and usable Ringee caller-number ids.
### `voice-agents call`
```text theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee voice-agents call --to --var --yes
```
This starts a real outbound phone call and spends Ringee credits. `--yes` is
required.
| Option | Description |
| -------------------- | ------------------------------------------------------------------------------------------------ |
| `--to ` | **Required.** Destination in E.164 |
| `--from ` | Ringee phone-number id to call from; otherwise use the agent default or the sole eligible number |
| `--var ` | Dynamic conversation variable; repeatable and values may contain `=` |
| `--external-id ` | Your id, returned later under `metadata.external_id` |
| `-y, --yes` | **Required.** Confirm the real, billed call |
```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee voice-agents call \
--to +14155552671 \
--var first_name=Ada \
--var appointment_date=2026-09-10 \
--var appointment_time=14:00 \
--yes
```
### `voice-agents call-result`
```text theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee voice-agents call-result
```
Read the current status and, once analysis is ready, the outcome, summary, sentiment, extracted data and metadata. Poll deliberately; retrying `voice-agents call` starts another call.
See [AI Voice Agents from the CLI](/voice-agents/cli) for the end-to-end workflow and type-specific variables.
## Config and knowledge
| Command | Description |
| --------------------- | -------------------------------------------------- |
| `ringee config show` | Resolved connection, token masked |
| `ringee config check` | Connect and list every tool the server exposes |
| `ringee tools` | The agent capability catalog with sensitivity tags |
| `ringee flow` | The outbound sales flow, step by step |
| `ringee prompt` | The shared agent system prompt |
`tools`, `flow` and `prompt` need no connection — they print the shared knowledge from `@ringee-io/agent`, which is handy when briefing your own agent.
## Global options
| Option | Description |
| --------------- | ---------------------------------- |
| `--json` | Raw JSON instead of formatted text |
| `-v, --version` | Print the version |
| `--help` | Help for any command or subcommand |
## Next steps
Use the CLI as an agent's Ringee interface
# Configuration
Source: https://docs.ringee.io/cli/configuration
Point the ringee CLI at your workspace
The CLI connects to the Ringee MCP server. It needs one thing: the URL of that server for your workspace.
## Get your connection details
From the dashboard, open **Settings → MCP / Integrations**, or call the API while signed in:
```http theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
GET /api/mcp/connection-info
```
```json theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
{
"mode": "freelancer",
"userId": "a91c…",
"organizationId": null,
"url": "https://api.ringee.io/api/mcp/a91c…/sse"
}
```
## Environment variables
The full SSE URL. **Preferred** — takes precedence over everything else.
Your Ringee API base URL. Used together with `RINGEE_USER_ID` when `RINGEE_MCP_URL` is not set.
Your Ringee user UUID.
Organization UUID, to start in an organization workspace instead of your personal one.
Optional bearer token sent as an `Authorization` header.
```bash Full URL (preferred) theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
export RINGEE_MCP_URL="https://api.ringee.io/api/mcp//sse"
```
```bash Organization workspace theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
export RINGEE_MCP_URL="https://api.ringee.io/api/mcp///sse"
```
```bash Built from parts theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
export RINGEE_BACKEND_URL="https://api.ringee.io"
export RINGEE_USER_ID=""
export RINGEE_ORG_ID="" # optional
```
```bash Self-hosted theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
export RINGEE_BACKEND_URL="https://ringee-api.example.com"
export RINGEE_USER_ID=""
```
`RINGEE_BACKEND_URL` tolerates a trailing `/api` — the CLI strips it before building the path, so both `https://api.ringee.io` and `https://api.ringee.io/api` work.
The MCP URL is a credential for that workspace. Keep it in your shell profile or secret manager, not in a committed `.env` or a shared script.
## Verify
```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee config show # resolved connection, token masked
ringee config check # connect and list every tool the server exposes
```
`config check` is the fastest way to answer "is my Ringee connection working?" — if it prints tools, every other command will work, and so will any MCP client pointed at the same URL.
## Choosing a workspace
The URL sets your starting workspace, but it is not a lock. Anything that changes the active workspace — the dashboard switcher, an assistant calling `switch_workspace` — applies to the CLI too, because the active workspace is stored server-side.
Seeing results you did not expect? Your active workspace has probably changed. See [Workspaces](/mcp/workspaces).
## Errors
| Message | Fix |
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| `Missing Ringee connection config` | Set `RINGEE_MCP_URL`, or `RINGEE_BACKEND_URL` **and** `RINGEE_USER_ID`. |
| Connection refused or timeout | Confirm the backend is reachable from this machine and the URL includes `/api/mcp/…/sse`. |
| Empty tool list from `config check` | The URL resolved but the ids do not match a real user or organization. Re-run `GET /api/mcp/connection-info`. |
## Next steps
Every command, argument and flag
Give a shell-capable agent access to Ringee
# CLI overview
Source: https://docs.ringee.io/cli/overview
Operate Ringee from the terminal
The `ringee` CLI puts contacts, leads, campaigns, analytics, DNC, AI pipelines, AI Voice Agents, dialing sessions and follow-ups behind a consistent command line. It is a thin front end over the Ringee [MCP server](/mcp/overview)—the same tools an AI assistant would call, with the same guardrails.
```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
npm i -g ringee
export RINGEE_MCP_URL="https://api.ringee.io/api/mcp//sse"
ringee config check
ringee contacts search acme
```
Point the CLI at your workspace
Every command, argument and flag
## Install
```bash Global theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
npm i -g ringee
ringee --help
```
```bash No install theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
npx ringee --help
```
```bash From the repo theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
pnpm ringee contacts search acme
pnpm --filter ringee dev -- config check
```
Requires **Node 20 or newer**. The published binary pulls in three dependencies — `@modelcontextprotocol/sdk`, `zod` and `commander`.
## What it is good for
| Use case | Why the CLI |
| ----------------- | --------------------------------------------------------- |
| Quick lookups | `ringee contacts search acme` beats opening the dashboard |
| Scripting | `--json` on any command gives machine-readable output |
| AI agents | Any harness that can run shell commands can drive Ringee |
| Debugging MCP | `ringee config check` lists every tool the server exposes |
| CI and automation | No browser, no session cookie — just an env var |
## Command groups
| Group | Purpose |
| ----------------------------------- | ----------------------------------------------------------------- |
| `contacts` | Search, view, create, update, delete; find by call outcome |
| `leads` | Prospect via Apollo or Prospeo; reveal and import |
| `sessions` | Magic-link dialing queues |
| `calls` | Browse call history with outcomes, transcriptions and recordings |
| `outcomes`, `callbacks`, `meetings` | Log results and book the next touch |
| `campaigns` | Inspect campaign configuration, manage leads and read performance |
| `analytics` | Dashboard metrics and one-day activity |
| `dnc` | Manage the workspace Do Not Call suppression list |
| `pipelines` | Read AI pipeline state and results |
| `voice-agents` | List agents, start a real billed call and read its result |
| `config` | Connection diagnostics and the shared agent knowledge |
## Safety
Sensitive and destructive commands refuse to run without an explicit flag, so neither a typo nor an autonomous agent can spend credits, mint a shareable link or delete data by accident.
```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee leads reveal --yes
ringee sessions create --contact --yes
ringee sessions revoke --yes
ringee contacts delete --confirm-phone +14155550123 --yes
ringee campaigns delete-lead --yes
ringee dnc remove +14155550123 --yes
ringee voice-agents call --to +14155550123 --yes
```
See [Safety and confirmations](/mcp/safety).
## Output modes
By default the CLI prints readable, formatted output. Add `--json` to any command for the raw payload:
```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee contacts search acme --json | jq '.contacts[].phoneNumber'
```
## Next steps
Environment variables and diagnostics
Drive Ringee from Claude Code or any shell-capable harness
Place and monitor autonomous calls from the CLI
# Clerk Configuration
Source: https://docs.ringee.io/configuration/clerk
Set up Clerk for authentication
Ringee uses [Clerk](https://clerk.com) for authentication and multi-tenant team management. Clerk handles user sign-up, sign-in, organization management, and role-based access control.
## Setup
1. Go to [clerk.com](https://clerk.com) and create an account
2. Create a new application
3. Select the authentication methods you want to support (e.g., Email, Google, GitHub)
In the Clerk dashboard, go to **API Keys** and copy:
* **Publishable key** (starts with `pk_`)
* **Secret key** (starts with `sk_`)
Ringee needs Clerk webhooks to sync user and organization events:
1. Go to **Webhooks** in the Clerk dashboard
2. Click **Add Endpoint**
3. Set the endpoint URL to: `https://your-backend-url/api/clerk/webhook`
4. Select the following events:
* `user.created`
* `user.updated`
* `user.deleted`
* `organization.created`
* `organization.updated`
* `organizationMembership.created`
* `organizationMembership.deleted`
5. Copy the **Signing Secret**
In the Clerk dashboard, go to **Paths** and configure:
* **Sign-in URL**: `/auth/sign-in`
* **Sign-up URL**: `/auth/sign-up`
* **After sign-in URL**: `/dashboard/overview`
* **After sign-up URL**: `/dashboard/overview`
Add the following to your `.env` file:
```env theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY="pk_test_your_publishable_key"
CLERK_PUBLISHABLE_KEY="pk_test_your_publishable_key"
CLERK_SECRET_KEY="sk_test_your_secret_key"
CLERK_WEBHOOK_SIGNING_SECRET="whsec_your_webhook_secret"
NEXT_PUBLIC_CLERK_SIGN_IN_URL="/auth/sign-in"
NEXT_PUBLIC_CLERK_SIGN_UP_URL="/auth/sign-up"
NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL="/dashboard/overview"
NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL="/dashboard/overview"
```
## Environment Variables
| Variable | Required | Description |
| ------------------------------------- | -------- | ------------------------------------------------------- |
| `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` | Yes | Clerk publishable key (exposed to frontend) |
| `CLERK_PUBLISHABLE_KEY` | Yes | Clerk publishable key (backend) |
| `CLERK_SECRET_KEY` | Yes | Clerk secret key |
| `CLERK_WEBHOOK_SIGNING_SECRET` | Yes | Webhook signing secret |
| `NEXT_PUBLIC_CLERK_SIGN_IN_URL` | No | Sign-in page path (default: `/auth/sign-in`) |
| `NEXT_PUBLIC_CLERK_SIGN_UP_URL` | No | Sign-up page path (default: `/auth/sign-up`) |
| `NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL` | No | Redirect after sign-in (default: `/dashboard/overview`) |
| `NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL` | No | Redirect after sign-up (default: `/dashboard/overview`) |
`NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` and `CLERK_PUBLISHABLE_KEY` can be set to the same value. The `NEXT_PUBLIC_` prefix makes it available to the Next.js frontend.
# Docker Compose Configuration
Source: https://docs.ringee.io/configuration/docker-compose
Docker-specific configuration and overrides
## Overview
When running Ringee with `docker-compose.app.yml`, several environment variables are **automatically overridden** to use Docker's internal networking. You don't need to manually set these.
## Automatic Overrides
The following variables are set automatically by Docker Compose:
| Variable | Docker Value |
| -------------- | -------------------------------------------------------------------------------------- |
| `DATABASE_URL` | `postgresql://ringee-user:ringee-password@postgres:5432/ringee-db-local?schema=public` |
| `REDIS_HOST` | `redis` |
| `REDIS_PORT` | `6379` |
| `REDIS_URL` | `redis://redis:6379` |
The hostnames `postgres` and `redis` refer to the Docker internal service names. These are resolved automatically by Docker's internal DNS.
## Volumes
The default Docker Compose configuration creates the following **persistent volumes**:
| Volume | Purpose |
| --------------- | ------------------------- |
| `postgres-data` | PostgreSQL database files |
| `redis-data` | Redis persistence data |
## Ports
Default port mapping:
| Service | Host Port | Container Port |
| ----------- | --------- | -------------- |
| Frontend | `4200` | `4200` |
| Backend API | `3000` | `3000` |
| PostgreSQL | `5432` | `5432` |
| Redis | `6379` | `6379` |
| Temporal | `7233` | `7233` |
| Temporal UI | `8080` | `8080` |
## Customizing ports
To change the host ports, modify the `ports` section in `docker-compose.app.yml`:
```yaml theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
services:
frontend:
ports:
- "8080:4200" # Access admin on port 8080 instead
```
If you change the frontend or backend ports, remember to update the corresponding `FRONTEND_URL`, `PUBLIC_BACKEND_URL`, and `NEXT_PUBLIC_API_BASE_URL` environment variables accordingly.
## Rebuilding
After making changes to the codebase or environment:
```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
# Rebuild and restart all containers
docker-compose -f docker-compose.app.yml up --build -d
# Rebuild only a specific service
docker-compose -f docker-compose.app.yml up --build -d backend
```
# Email Configuration
Source: https://docs.ringee.io/configuration/email
Set up transactional email with Resend
Ringee uses [Resend](https://resend.com) for sending transactional emails such as notifications and team invitations.
## Setup
Go to [resend.com](https://resend.com) and create an account.
In the Resend dashboard, navigate to **Domains** and add your domain. Follow the instructions to verify it by adding the required DNS records.
Go to **API Keys** in the Resend dashboard and create a new key.
Add the following to your `.env` file:
```env theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
RESEND_API_KEY="re_your_api_key_here"
EMAIL_FROM_ADDRESS="no-reply@yourdomain.com"
EMAIL_FROM_NAME="Ringee"
```
## Environment Variables
| Variable | Required | Description |
| -------------------- | -------- | --------------------------------------------------------- |
| `RESEND_API_KEY` | Yes | Your Resend API key |
| `EMAIL_FROM_ADDRESS` | Yes | The sender email address (must be from a verified domain) |
| `EMAIL_FROM_NAME` | Yes | The sender display name shown in emails |
The `EMAIL_FROM_ADDRESS` must use a domain that you've verified in Resend. Using an unverified domain will cause email delivery failures.
# R2 Configuration
Source: https://docs.ringee.io/configuration/r2
Set up Cloudflare R2 for file storage
Ringee uses [Cloudflare R2](https://developers.cloudflare.com/r2/) for storing files such as call recordings, contact imports, and other media.
## Setup
If you don't have one already, create an account at [cloudflare.com](https://dash.cloudflare.com/sign-up).
1. Navigate to **R2 Object Storage** in the Cloudflare dashboard
2. Click **Create bucket**
3. Give it a name (e.g., `ringee-storage`)
4. Select a location (or leave as automatic)
1. Go to **R2** → **Manage R2 API Tokens**
2. Click **Create API token**
3. Grant **Object Read & Write** permissions
4. Copy the **Access Key ID** and **Secret Access Key**
If you need public URLs for stored files:
1. Go to your bucket → **Settings** → **Public Access**
2. Enable public access and note the public bucket URL
Add the following to your `.env` file:
```env theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
CLOUDFLARE_BUCKETNAME="ringee-storage"
CLOUDFLARE_ACCOUNT_ID="your_account_id"
CLOUDFLARE_ACCESS_KEY="your_access_key"
CLOUDFLARE_SECRET_ACCESS_KEY="your_secret_access_key"
CLOUDFLARE_BUCKET_URL="https://your-bucket-url.r2.dev"
CLOUDFLARE_REGION="auto"
STORAGE_PROVIDER="cloudflare"
```
## Environment Variables
| Variable | Required | Description |
| ------------------------------ | -------- | ---------------------------------------- |
| `CLOUDFLARE_BUCKETNAME` | Yes | Name of your R2 bucket |
| `CLOUDFLARE_ACCOUNT_ID` | Yes | Your Cloudflare account ID |
| `CLOUDFLARE_ACCESS_KEY` | Yes | R2 API access key ID |
| `CLOUDFLARE_SECRET_ACCESS_KEY` | Yes | R2 API secret access key |
| `CLOUDFLARE_BUCKET_URL` | Yes | Public URL for the bucket |
| `CLOUDFLARE_REGION` | No | Region (default: `auto`) |
| `STORAGE_PROVIDER` | No | Storage provider (default: `cloudflare`) |
Your **Cloudflare Account ID** can be found in the Cloudflare dashboard URL or under **Account Home** → **Overview** on the right sidebar.
# Configuration Reference
Source: https://docs.ringee.io/configuration/reference
Environment variables reference for Ringee
Ringee is configured entirely through environment variables. Any change to environment variables requires an **application restart**.
The full example file can be found at: [`.env.example` on GitHub](https://github.com/ringee-io/ringee-app/blob/main/.env.example)
## Required Settings
### Application
#### `DATABASE_URL`
PostgreSQL connection string. Ringee uses Prisma ORM.
```
postgresql://ringee-user:ringee-password@localhost:5432/ringee-db-local?schema=public
```
#### `REDIS_URL`
```
redis://localhost:6379
```
#### `REDIS_HOST`
```
localhost
```
#### `REDIS_PORT`
```
6379
```
#### `FRONTEND_URL`
URL where the admin frontend is served.
```
http://localhost:4200
```
#### `BACKEND_URL` / `PUBLIC_BACKEND_URL`
URL where the backend API is served. In production, set both variables to the same public HTTPS origin without an `/api` suffix. Ringee uses it to build provider callback URLs for AI Voice Agents and desk phones.
```
http://localhost:3000
```
#### `APP_ENCRYPTION_SECRET`
A random string used for encrypting sensitive data at the application level. Should be unique per installation.
***
### Background jobs (Temporal)
The orchestrator (`apps/orchestrator`) runs durable workflows and schedules. Both the backend and the orchestrator read these.
| Variable | Description |
| --------------------- | --------------------------------------------------- |
| `TEMPORAL_ADDRESS` | Temporal server address (default: `localhost:7233`) |
| `TEMPORAL_NAMESPACE` | Temporal namespace (default: `default`) |
| `TEMPORAL_TASK_QUEUE` | Task queue name (default: `ringee`) |
***
### Authentication (Clerk)
See the [Clerk Configuration](/configuration/clerk) guide for setup details.
| Variable | Description |
| ------------------------------------- | ------------------------------------------------------- |
| `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` | Clerk publishable key |
| `CLERK_PUBLISHABLE_KEY` | Clerk publishable key (backend) |
| `CLERK_SECRET_KEY` | Clerk secret key |
| `CLERK_WEBHOOK_SIGNING_SECRET` | Webhook signing secret |
| `NEXT_PUBLIC_CLERK_SIGN_IN_URL` | Sign-in page URL (default: `/auth/sign-in`) |
| `NEXT_PUBLIC_CLERK_SIGN_UP_URL` | Sign-up page URL (default: `/auth/sign-up`) |
| `NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL` | Redirect after sign-in (default: `/dashboard/overview`) |
| `NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL` | Redirect after sign-up (default: `/dashboard/overview`) |
***
### Telephony (Telnyx)
See the [Telnyx Configuration](/providers/telnyx) guide for setup details.
| Variable | Description |
| ---------------------------------- | ------------------------------------------------------------------------------------------ |
| `TELNYX_API_KEY` | Your Telnyx API key |
| `TELNYX_CONNECTION_ID` | Telnyx WebRTC connection ID |
| `TELNYX_CALL_CONTROL_APP_ID` | Call Control Application for server-originated calls; falls back to `TELNYX_CONNECTION_ID` |
| `TELNYX_OUTBOUND_VOICE_PROFILE_ID` | Outbound Voice Profile used by AI Voice Agents and desk phones |
| `TELNYX_PUBLIC_KEY` | Public key used to verify Telnyx webhooks |
| `TELNYX_SIP_USER` | SIP username for WebRTC |
| `TELNYX_SIP_PASSWORD` | SIP password for WebRTC |
| `NEXT_PUBLIC_TELNYX_LOGIN` | SIP login exposed to frontend |
| `NEXT_PUBLIC_TELNYX_PASSWORD` | SIP password exposed to frontend |
`TELNYX_CALL_CONTROL_APP_ID` may use its connection-id fallback when both ids point to the same Telnyx resource. `TELNYX_OUTBOUND_VOICE_PROFILE_ID` is recommended for explicit AI Voice Agent routing and billing, and is required when Desk Phones are enabled; without it, AI Voice Agents retain the provider's default outbound route.
***
### Payments (Stripe)
See the [Stripe Configuration](/configuration/stripe) guide for setup details.
| Variable | Description |
| ----------------------- | ----------------------------- |
| `STRIPE_SECRET_KEY` | Your Stripe secret key |
| `STRIPE_WEBHOOK_SECRET` | Stripe webhook signing secret |
***
### Email (Resend)
See the [Email Configuration](/configuration/email) guide for details.
| Variable | Description |
| -------------------- | -------------------- |
| `RESEND_API_KEY` | Your Resend API key |
| `EMAIL_FROM_ADDRESS` | Sender email address |
| `EMAIL_FROM_NAME` | Sender display name |
***
### Storage (Cloudflare R2)
See the [R2 Configuration](/configuration/r2) guide for details.
| Variable | Description |
| ------------------------------ | ---------------------------------------- |
| `CLOUDFLARE_BUCKETNAME` | R2 bucket name |
| `CLOUDFLARE_ACCOUNT_ID` | Cloudflare account ID |
| `CLOUDFLARE_ACCESS_KEY` | R2 access key |
| `CLOUDFLARE_SECRET_ACCESS_KEY` | R2 secret access key |
| `CLOUDFLARE_BUCKET_URL` | Public bucket URL |
| `CLOUDFLARE_REGION` | Region (default: `auto`) |
| `STORAGE_PROVIDER` | Storage provider (default: `cloudflare`) |
***
## Optional Settings
### Frontend
| Variable | Description |
| -------------------------- | ---------------------------------------------- |
| `NEXT_PUBLIC_API_BASE_URL` | Backend API URL for the frontend |
| `NEXT_PUBLIC_SITE_URL` | Public site URL |
| `NEXT_PUBLIC_APP_ENV` | App environment (`development` / `production`) |
### Sentry (Error Tracking)
| Variable | Description |
| ----------------------------- | --------------------------------- |
| `NEXT_PUBLIC_SENTRY_DSN` | Sentry DSN |
| `NEXT_PUBLIC_SENTRY_ORG` | Sentry organization slug |
| `NEXT_PUBLIC_SENTRY_PROJECT` | Sentry project slug |
| `SENTRY_AUTH_TOKEN` | Sentry auth token |
| `NEXT_PUBLIC_SENTRY_DISABLED` | Disable Sentry (`true` / `false`) |
### AI
| Variable | Description |
| ---------------- | ------------------------------------- |
| `OPENAI_API_KEY` | OpenAI API key (used for AI features) |
### AI Voice Agents
AI Voice Agents use the normal PostgreSQL, Redis, Temporal, Telnyx and storage settings in addition to these controls. See [Self-host AI Voice Agents](/voice-agents/self-hosting) for the deployment flow.
| Variable | Default | Description |
| ----------------------------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------- |
| `AI_VOICE_AGENT_PROFIT_MARGIN` | `1` | Multiplier applied to the provider's AI conversation cost before debiting Ringee credits; minimum 1 |
| `AI_VOICE_AGENT_RINGEE_MODEL` | `moonshotai/Kimi-K2.6` | Concrete model behind the Ringee AI choice |
| `AI_VOICE_AGENT_OPENAI_MODEL` | `openai/gpt-5.2` | Concrete model behind the OpenAI BYOK choice |
| `AI_VOICE_AGENT_ANTHROPIC_MODEL` | `anthropic/claude-haiku-4-5` | Concrete model behind the Anthropic BYOK choice |
| `AI_VOICE_AGENT_GOOGLE_MODEL` | `google/gemini-2.5-flash` | Concrete model behind the Google BYOK choice |
| `AI_VOICE_AGENT_MAX_CALL_SECONDS` | `900` | Hard limit for one agent call; minimum 30 seconds |
| `AI_VOICE_AGENT_TEST_SESSION_TTL_SECONDS` | `600` | Browser test-session lifetime; minimum 60 seconds |
| `AI_VOICE_AGENT_STORAGE_REGION` | `us-central-1` | Provider object-storage region for agent knowledge bases |
| `AI_VOICE_AGENT_MAX_DOCUMENT_MB` | `20` | Maximum uploaded knowledge-document size in megabytes |
OpenAI, Anthropic and Google keys selected in the product are per-agent BYOK
credentials. Ringee verifies each key, stores it in the voice provider's
secret store and persists only the provider reference; they are not global
environment variables.
### MCP and AI assistants
Only needed for the authenticated ChatGPT connector. The public MCP endpoints work with no extra configuration — see [Connect a client](/mcp/connect).
| Variable | Description |
| --------------------------------- | ---------------------------------------------------------------------------------------- |
| `MCP_OAUTH_AUTHORIZATION_SERVERS` | OAuth authorization server(s) advertised to ChatGPT — your Clerk issuer. Comma-separated |
| `MCP_OAUTH_SCOPES` | Scopes advertised (default: `ringee:use`) |
| `OPENAI_APPS_CHALLENGE_TOKEN` | Domain-verification token served at `/.well-known/openai-apps-challenge` |
# Stripe Configuration
Source: https://docs.ringee.io/configuration/stripe
Set up Stripe for billing and payments
Ringee uses [Stripe](https://stripe.com) for managing credits, subscriptions, and phone number purchases.
## Setup
Go to [stripe.com](https://stripe.com) and create an account if you don't have one.
1. In the Stripe dashboard, go to **Developers** → **API keys**
2. Copy your **Secret key**
For development, use **test mode** keys (prefix `sk_test_`). Switch to live keys only for production.
Ringee needs Stripe webhooks to process payment events:
1. Go to **Developers** → **Webhooks**
2. Click **Add endpoint**
3. Set the endpoint URL to: `https://your-backend-url/api/stripe/webhook`
4. Select the events to listen to:
* `checkout.session.completed`
* `customer.subscription.created`
* `customer.subscription.updated`
* `customer.subscription.deleted`
* `invoice.payment_succeeded`
* `invoice.payment_failed`
5. Copy the **Signing secret**
Add the following to your `.env` file:
```env theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
STRIPE_SECRET_KEY="sk_test_your_secret_key"
STRIPE_WEBHOOK_SECRET="whsec_your_webhook_secret"
```
## Environment Variables
| Variable | Required | Description |
| ----------------------- | -------- | ------------------------------------------- |
| `STRIPE_SECRET_KEY` | Yes | Your Stripe secret API key |
| `STRIPE_WEBHOOK_SECRET` | Yes | Webhook signing secret for verifying events |
## Testing locally
For local development, use the [Stripe CLI](https://stripe.com/docs/stripe-cli) to forward webhook events:
```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
stripe listen --forward-to localhost:3000/api/stripe/webhook
```
This will output a webhook signing secret that you can use as `STRIPE_WEBHOOK_SECRET` during development.
The Stripe CLI is the easiest way to test webhooks locally. Install it with `brew install stripe/stripe-cli/stripe` on macOS.
# Inline bar
Source: https://docs.ringee.io/dialer-sdk/bar
Render the dialer inside an element of your own layout
Bar renders the dialer inside a container you provide. It adapts to the available width and creates no floating launcher — use it when your layout already has a dedicated place for calling, like a toolbar, a sidebar, or a contact record panel.
## CDN
```html theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
```
## npm
```ts theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
import { createBar } from "@ringee/dialer-sdk/ui";
const ringeeBar = createBar({
key: "pk_live_xxxxx",
container: document.getElementById("ringee-bar")!,
agentEmail: currentUser.email,
locale: "en",
});
```
## The container
`container` is **required** and accepts:
| Form | Example |
| -------------- | --------------------------------------- |
| An element | `document.getElementById("ringee-bar")` |
| An id | `"ringee-bar"` |
| A CSS selector | `"#sidebar .dialer"` |
The element must exist when `createBar()` runs — the SDK throws otherwise. In a framework, create the controller after the container has rendered.
Bar accepts every [common option](/dialer-sdk/reference#common-ui-options). The Floating-only options (`side`, `defaultOpen`, `rememberOpen`) do not apply.
## Controller methods
| Method | Description |
| --------------------- | -------------------------------------- |
| `setContact(contact)` | Attach the contact currently on screen |
| `prefill(number)` | Prefill just the destination |
| `setTheme(theme)` | Update the theme at runtime |
| `on(event, handler)` | Subscribe to a headless event |
| `destroy()` | Remove the UI and its listeners |
| `dialer` | The underlying headless instance |
Bar has no `open()`, `close()`, `toggle()` or `startCall()` — it is always visible, and calls start from the rendered controls. To place a call programmatically, go through the headless instance:
```ts theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
await ringeeBar.dialer.call({ to: "+13055550142" });
```
## Follow the selected record
```ts theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
function onContactSelected(contact) {
ringeeBar.setContact({
name: contact.fullName,
number: contact.phone,
imageUrl: contact.avatarUrl,
externalContactId: contact.id,
});
}
```
See [CRM contacts](/dialer-sdk/contacts) for how `contactId` and `externalContactId` differ.
## Layout notes
The bar sizes itself to the container. Make sure your layout does not collapse it:
```css theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
#ringee-bar {
width: 100%;
min-width: 320px;
}
```
If the bar does not appear, a zero-width or zero-height container is the usual cause. See [Troubleshooting](/dialer-sdk/troubleshooting).
## Clean up
```ts theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringeeBar.destroy();
await ringeeBar.dialer.destroy();
```
## Sharing one engine between surfaces
If you render both a Floating panel and a Bar, pass the same headless instance so they share one session and one active call:
```ts theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
import { RingeeDialer } from "@ringee/dialer-sdk";
import { createBar, createFloating } from "@ringee/dialer-sdk/ui";
const dialer = new RingeeDialer({ key: "pk_live_xxxxx" });
const bar = createBar({ key: "pk_live_xxxxx", container: "#ringee-bar", dialer });
const floating = createFloating({ key: "pk_live_xxxxx", dialer });
```
Destroy the shared dialer only after every consumer is done with it.
## Next steps
Build the UI yourself
Blend the bar into your design system
# CRM contacts
Source: https://docs.ringee.io/dialer-sdk/contacts
Attach the record your app has on screen
Tell the dialer who the agent is looking at. The bundled UIs show the name and avatar, prefill the destination, and Ringee attributes the call to the right contact.
```ts theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee.setContact({
name: "Morgan Reed",
number: "+13055550142",
imageUrl: "https://crm.example.com/avatars/294.png",
externalContactId: "crm-contact-294",
});
```
## Fields
| Field | Purpose |
| ------------------- | ------------------------------ |
| `name` | Visual label in the bundled UI |
| `number` | Prefills the destination |
| `imageUrl` | Avatar in the bundled UI |
| `contactId` | Native Ringee contact UUID |
| `externalContactId` | The contact's id in your CRM |
## `contactId` or `externalContactId`?
You need one, not both.
Use it when you already know the internal Ringee UUID — for example because
your app reads Ringee data directly.
Use it when your [Custom Integration](/api/overview) maps a CRM id to a
Ringee contact.
`externalContactId` is the same value you send as `data.externalId` in [`contact.upserted`](/api/inbound-events#contact-upserted). Sync contacts through the Public API first and Ringee resolves the link automatically.
Keep `externalContactId` consistent so Ringee resolves the correct contact.
When you process [`call.completed`](/api/events-reference#call-completed), do
not rely on a contact reference alone: keep a normalized phone-number fallback
for activity matching.
## Prefill just a number
When there is no contact record — a manual dial, an inbound lookup:
```ts theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee.prefill("+13055550142");
```
## Start a call directly
Floating can skip straight to dialing:
```ts theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee.startCall({
to: "+13055550142",
name: "Morgan Reed",
externalContactId: "crm-contact-294",
});
```
In Headless:
```ts theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
await dialer.call({
to: "+13055550142",
callerIdId: selectedCallerId,
externalContactId: "crm-contact-294",
});
```
## Phone number format
Destinations must be **E.164** — a `+`, the country code, then the number, with no extension:
```text theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
+13055550198
+34911234567
+18095550123
```
Anything else fails with `INVALID_PHONE_NUMBER`. Normalize in your CRM before passing it in; the SDK validates locally before it reaches the network.
## Keeping it in sync
Call `setContact()` whenever the selected record changes. It is cheap and idempotent:
```ts theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
function onRecordOpened(contact) {
ringee.setContact({
name: contact.fullName,
number: contact.phone,
imageUrl: contact.avatarUrl,
externalContactId: contact.id,
});
}
```
To detach the current record — the user closed the panel, navigated away, deselected — pass `null`:
```ts theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee.setContact(null);
```
## After the call
The SDK emits `ended` with a call snapshot in the browser, but the durable record of what happened arrives through your [outbound webhooks](/api/outbound-webhooks):
| Event | Carries |
| ---------------------- | ------------------------------------------- |
| `call.completed` | Direction, status, timing, duration |
| `call.outcome.updated` | The outcome the agent logged, plus the note |
| `recording.ready` | Recording URL and transcript |
Write your CRM activity from the webhooks — they are signed, retried and idempotent. Use the browser events for live UI only.
## Next steps
Establish the `externalId` link
Close the loop back into your CRM
# Errors
Source: https://docs.ringee.io/dialer-sdk/errors
Every RingeeError code and how to react to it
Rejected promises throw a `RingeeError` carrying a stable `code`, a human-readable `message`, and a `retryable` flag.
```ts theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
import { RingeeError } from "@ringee/dialer-sdk";
try {
await dialer.call({ to: "+13055550198" });
} catch (error) {
if (error instanceof RingeeError) {
console.log(error.code);
console.log(error.message);
console.log(error.retryable);
}
}
```
The bundled UIs already translate these codes into actionable messages. In [Headless](/dialer-sdk/headless), that mapping is yours.
## Installation
Something is wrong with the key or the origin. These are configuration problems, not runtime ones — fail loudly during development.
| Code | Meaning | Fix |
| ------------------------- | --------------------------------------------------------------------- | ------------------------------------------- |
| `INVALID_PUBLISHABLE_KEY` | Malformed key, deleted integration, or the secret API key was rotated | Generate a new publishable key |
| `DOMAIN_NOT_ALLOWED` | `window.location.origin` is not in the key's allowed origins | Add the exact origin and generate a new key |
| `INTEGRATION_DISABLED` | The Custom Integration is not active | Re-enable it in the dashboard |
See [Publishable keys](/dialer-sdk/publishable-keys).
## Authentication
| Code | Meaning | Fix |
| ------------------------------ | ------------------------------------- | --------------------------------------- |
| `INVALID_EMAIL` | The address is not well-formed | Validate before submitting |
| `INVALID_EMAIL_CODE` | Wrong code | Let the agent retry |
| `EMAIL_CHALLENGE_EXPIRED` | The challenge timed out | Request a new code |
| `EMAIL_CODE_ATTEMPTS_EXCEEDED` | Too many wrong attempts | Start a new challenge |
| `AUTH_REQUIRED` | No authenticated agent | Run the OTP flow, then wait for `ready` |
| `SESSION_EXPIRED` | The stored session is no longer valid | Re-authenticate |
## Permissions
| Code | Meaning |
| ------------------------ | -------------------------------------------------------- |
| `AGENT_NOT_ALLOWED` | This agent may not use this installation |
| `AGENT_NOT_IN_WORKSPACE` | The agent is not a member of the integration's workspace |
| `USER_BLOCKED` | The account is blocked |
| `CALLING_DISABLED` | Calling is disabled for this workspace or user |
These are decided server-side, on every call. A publishable key never grants calling rights on its own.
## Calls
| Code | Meaning | Fix |
| ----------------------- | --------------------------------------------- | ---------------------------------------------- |
| `INVALID_PHONE_NUMBER` | Not valid E.164 | Send `+`, country code, number, no extension |
| `NO_CALLER_ID` | No caller ID available to this agent | Add or verify one in Ringee |
| `CALLER_ID_NOT_ALLOWED` | The requested `callerIdId` is not permitted | Use one from `getCallerIds()` |
| `INSUFFICIENT_CREDIT` | Not enough balance | Top up the workspace |
| `DNC_BLOCKED` | The destination is on the Do Not Call list | Do not retry; disable the call button |
| `CALL_ALREADY_ACTIVE` | A call exists in this instance or another tab | End it first |
| `NO_ACTIVE_CALL` | A control method ran with no call in progress | Guard on `getActiveCall()` |
| `CALL_FAILED` | The call could not be completed | Show the message; the reason varies by carrier |
## Browser and network
| Code | Meaning | Fix | `retryable` |
| -------------------------- | ----------------------------------- | -------------------------------------------------------------------- | ---------------- |
| `MICROPHONE_DENIED` | Permission refused or unavailable | Serve over HTTPS, allow the mic, add `allow="microphone"` to iframes | No |
| `NO_AUDIO_DEVICE` | No usable input device | Ask the agent to connect a microphone | No |
| `AUDIO_PLAYBACK_BLOCKED` | Autoplay policy blocked audio | Start calls from a real click or tap | No |
| `TELNYX_CONNECTION_FAILED` | The provider connection dropped | Back off and retry | Yes |
| `NETWORK_ERROR` | Request failed in transit | Back off and retry | Yes |
| `TIMEOUT` | The operation exceeded its deadline | Back off and retry | Yes |
## Retry policy
`retryable` is `true` for known transient failures — rate limits, timeouts, network failures and provider connection failures. Every other code in this page reports No.
Never auto-retry permission, credit, Do Not Call or authentication errors. Retrying `INSUFFICIENT_CREDIT` or `DNC_BLOCKED` cannot succeed, and retrying auth errors can lock the agent out of the challenge.
```ts theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
async function callWithRetry(input, attempts = 3) {
for (let i = 0; i < attempts; i++) {
try {
return await dialer.call(input);
} catch (error) {
const last = i === attempts - 1;
if (!(error instanceof RingeeError) || !error.retryable || last) throw error;
await new Promise((r) => setTimeout(r, 2 ** i * 500));
}
}
}
```
## Where errors surface
| Channel | When |
| ---------------- | --------------------------------------------------------------- |
| Rejected promise | The method you awaited failed |
| `failed` event | Authorization or the call itself failed, with `{ call, error }` |
| `error` event | A typed general error occurred outside a call |
| `onError` option | Convenience hook on the UI controllers |
Subscribe **before** `initialize()` so errors raised during startup are not missed.
## Next steps
Symptom-first fixes for the common cases
CSP, microphone and iframe requirements
# Floating
Source: https://docs.ringee.io/dialer-sdk/floating
A launcher in the page corner that opens a complete dialer panel
Floating mounts a launcher in a corner of the page. The panel handles agent sign-in, caller ID selection, number entry and in-call controls — nothing else to build.
## CDN
No build step required.
```html theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
```
`Ringee.mount(options)` is an alias for `Ringee.createFloating(options)`.
Pin a version in production:
```html theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
```
## npm
```ts theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
import { createFloating } from "@ringee/dialer-sdk/ui";
const ringee = createFloating({
key: "pk_live_xxxxx",
agentEmail: currentUser.email,
locale: "en",
side: "right",
defaultOpen: false,
rememberOpen: true,
allowHold: true,
});
```
The UI calls `initialize()` for you. `agentEmail` only prefills the email field — Ringee always requires the agent to prove access to that address via one-time code.
## Options
Floating accepts every [common option](/dialer-sdk/reference#common-ui-options) plus:
| Option | Type | Default | Description |
| -------------- | ------------------- | --------------- | -------------------------------------- |
| `side` | `"left" \| "right"` | `"right"` | Which side the launcher sits on |
| `defaultOpen` | `boolean` | `false` | Open the panel on mount |
| `rememberOpen` | `boolean` | `true` | Remember the open state within the tab |
| `container` | `HTMLElement` | `document.body` | Shadow DOM parent |
## Control it from your app
```ts theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee.open();
ringee.close();
ringee.toggle();
ringee.setContact({
name: "Morgan Reed",
number: "+13055550142",
externalContactId: "crm-contact-294",
});
ringee.startCall({
to: "+13055550142",
name: "Morgan Reed",
externalContactId: "crm-contact-294",
});
```
`startCall()` is safe to call before session restoration finishes. The controller keeps the requested call and places it once the dialer reaches `ready`. If the agent is not authenticated, the OTP flow appears first.
## Wiring it to a contact record
```ts theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
// When the user opens a contact
ringee.setContact({
name: contact.fullName,
number: contact.phone,
imageUrl: contact.avatarUrl,
externalContactId: contact.id,
});
// When they click "Call"
callButton.addEventListener("click", () => {
ringee.startCall({ to: contact.phone, externalContactId: contact.id });
});
```
Start calls from a real click or tap. Browsers block audio playback that was not triggered by a user gesture — auto-dialing on page load produces `AUDIO_PLAYBACK_BLOCKED`.
## React to call state
The controller forwards the headless events:
```ts theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee.on("answered", ({ call }) => startTimer(call.answeredAt));
ringee.on("ended", ({ call }) => logActivity(call));
ringee.on("failed", ({ error }) => toast(error.message));
```
Full list: [Events](/dialer-sdk/reference#events).
## Clean up
```ts theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
ringee.destroy(); // removes the UI
await ringee.dialer.destroy(); // releases WebRTC, audio and the call lock
```
`destroy()` on the controller only unmounts the visual surface. Destroy the underlying dialer too, or WebRTC stays connected.
## Next steps
Match your product's look
Mount and unmount safely in a component
# Headless
Source: https://docs.ringee.io/dialer-sdk/headless
Authentication, calls, devices and events with no UI at all
Headless exposes the full engine and renders nothing. You build every screen and react to every state. Choose it only when you need that control — the bundled UIs already handle sign-in, caller ID selection, keypad, call controls and error copy.
## Create an instance
```ts theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
import { RingeeDialer, RingeeError } from "@ringee/dialer-sdk";
const dialer = new RingeeDialer({
key: "pk_live_xxxxx",
debug: false,
});
// Subscribe BEFORE initialize() so the first state changes are not missed.
dialer.on("authStateChanged", ({ state }) => renderAuthState(state));
dialer.on("stateChanged", ({ state }) => renderCallState(state));
dialer.on("authRequired", () => showEmailForm());
dialer.on("ready", () => enableDialButton());
dialer.on("answered", ({ call }) => startTimer(call.answeredAt));
dialer.on("ended", ({ call }) => showSummary(call));
dialer.on("failed", ({ error }) => showError(error.code, error.message));
try {
await dialer.initialize();
} catch (error) {
if (error instanceof RingeeError) {
showError(error.code, error.message);
}
}
```
`initialize()` may resolve authenticated or anonymous:
* a valid session in `sessionStorage` is restored, emitting `signedIn` and `ready`;
* otherwise it emits `authRequired` and waits for the OTP flow.
## Email one-time code
```ts theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
const challenge = await dialer.requestEmailCode("agent@company.com");
const agent = await dialer.verifyEmailCode({
challengeId: challenge.id,
code: "184279",
});
console.log("Signed in as", agent.email);
```
Resend:
```ts theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
const nextChallenge = await dialer.resendEmailCode(challenge.id);
```
Use `challenge.resendAvailableAt` to gate the resend button and `challenge.expiresAt` to show a countdown.
Ringee does not reveal whether an email exists. A code is "sent" for any well-formed address — do not build enumeration hints into your UI.
## Place and control a call
```ts theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
const call = await dialer.call({
to: "+13055550198",
callerIdId: selectedCallerId,
externalContactId: "crm-contact-294",
});
dialer.mute();
dialer.unmute();
await dialer.hold();
await dialer.resume();
dialer.sendDigits("123#");
await dialer.hangup();
```
Only one call can be active per instance or agent. The SDK uses Web Locks to prevent a second tab from starting one, and the server validates the restriction again.
## Caller IDs
```ts theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
const callerIds = dialer.getCallerIds();
```
Returns the caller IDs the authenticated agent may use. Render them as a selector and pass the chosen `callerIdId` to `call()`. Omit it to let Ringee choose.
## Audio devices
```ts theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
const inputs = await dialer.getInputDevices();
const outputs = await dialer.getOutputDevices();
await dialer.setInputDevice(inputs[0].id);
await dialer.setOutputDevice(outputs[0].id);
```
Output selection depends on browser support. `deviceChanged` fires when the selection changes.
## Read current state
```ts theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
dialer.getState(); // DialerState
dialer.getAuthState(); // AuthState
dialer.getAgent(); // RingeeAgent | null
dialer.getActiveCall(); // RingeeCall | null
```
Full lists: [call states](/dialer-sdk/reference#call-states) and [auth states](/dialer-sdk/reference#authentication-states).
## Unsubscribe
Every `on()` returns an unsubscribe function:
```ts theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
const off = dialer.on("stateChanged", ({ state }) => console.log(state));
off();
```
## Sign out and clean up
```ts theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
await dialer.signOut(); // removes the persisted agent session
await dialer.destroy(); // disconnects WebRTC and releases browser resources
```
These are different operations. Destroying an instance does **not** sign the agent out; signing out does **not** release WebRTC.
## Building a good headless UI
The exact sequence varies with the browser, the network and the remote destination. Render from the current state rather than from an expected transition order.
The bundled UIs turn backend error codes into actionable messages. Headless gives you raw `RingeeError` codes — map them to copy your users understand. See [Errors](/dialer-sdk/errors).
Start calls from a click. Browsers block audio playback that was not user-initiated.
`reconnecting` is a normal transient state on flaky networks. Show it, but do not tear the call down.
## Next steps
Every method, event and state
Every error code and how to react
# Dialer SDK overview
Source: https://docs.ringee.io/dialer-sdk/overview
Embed a real Ringee dialer inside your own web app
The **Ringee Dialer SDK** puts working outbound calling inside your CRM, back office or web app. Your agents never leave your product, and you never touch WebRTC, SIP credentials or a telephony provider's SDK.
```html theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
```
That is a complete integration: a floating launcher, agent sign-in, caller ID selection, keypad, and in-call controls.
From zero to a working call in a few minutes
Create a `pk_live_` key scoped to your origin
## Three integration modes
| Mode | Best for | Initialization | UI |
| ------------------------------------ | ----------------------------------- | -------------- | ------------------------- |
| [**Floating**](/dialer-sdk/floating) | Fastest path; any page | Automatic | Floating launcher + panel |
| [**Bar**](/dialer-sdk/bar) | Toolbars, sidebars, contact records | Automatic | Inline dialer |
| [**Headless**](/dialer-sdk/headless) | Fully custom design and state | Manual | None — you build it |
Start with **Floating**. Choose **Bar** when your layout already has a place for calling. Choose **Headless** only when you need control over every screen, form, state and message.
## What you get
* **No React dependency.** The bundled UIs render in an isolated Shadow DOM and work with plain JavaScript, TypeScript or any framework.
* **English and Spanish**, plus per-label copy overrides.
* **Themeable** via a theme object or CSS custom properties.
* **No provider objects.** The SDK encapsulates WebRTC and Telnyx entirely — your app never sees a SIP password or a Telnyx JWT.
* **Real Ringee calls.** Every call lands in call history, respects credits, caller ID rules and Do Not Call, and fires your [webhooks](/api/outbound-webhooks).
## How agents sign in
A publishable key identifies an *installation*, not a person. Agent identity is proven separately with a **one-time code sent by email**:
```mermaid theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
flowchart LR;
classDef step fill:#15803D,color:white,stroke:#fff
a[Validate pk + origin]:::step
b[Agent enters email]:::step
c[Ringee emails a code]:::step
d[Agent verifies]:::step
e[Ready to call]:::step
a --> b --> c --> d --> e
```
That means it is safe to ship `pk_live_…` in your frontend. It is not safe to ship the `cik_live_…` Public API key — those are different credentials. See [Publishable keys](/dialer-sdk/publishable-keys).
## Requirements
Before embedding:
* an active Ringee **Custom Integration**;
* a `pk_live_…` publishable key scoped to your exact origin;
* a Ringee agent with access to that workspace;
* at least one caller ID available to that agent or workspace;
* enough credit and permission to place calls;
* **HTTPS** in production — browsers require it for microphone access;
* a modern browser with WebRTC and `navigator.mediaDevices`.
Destination numbers must be **E.164**: `+13055550198`, `+34911234567`, `+18095550123`.
This is a **browser-only** package. Do not initialize it in Node.js, a React Server Component, an API route or any SSR process.
## Not included in this release
* There is no public Ringee URL you can drop straight into `