This is the full developer documentation for pdcli # pdcli > A fast, scriptable, open-source CLI for Pipedrive — built for terminals, CI pipelines, and AI agents. **v0.18.0 · 145 commands · 26 topics Drive the whole\ Pipedrive pipeline\ from your terminal. =================== One install. Every object. `pdcli` turns deals, people, activities, and reports into fast, pipeable commands — for your scripts, your CI, and your agents. [Get started →](/pdcli/start/quickstart/) [Browse 145 commands](/pdcli/reference/commands/) ******zsh · \~/acme*live* ``` ❯ ▋ ``` A scriptable building block for CI pipelines cron jobs Slack bots data exports AI agents internal tooling QUICKSTART ## Three commands to your first deal Install, authenticate, and create — you'll be driving your pipeline from the shell in under a minute. 01 ### Install $ `npm install -g @wavyx/pdcli` Node 20+. Tarballs on GitHub Releases for everything else. 02 ### Authenticate $ `pdcli auth login` Prompts for your API token — stored in your OS keychain. 03 ### Create $ `pdcli deal create --title "Acme" --value 5000` Your first deal, straight from the prompt. [Read the full setup guide ](/pdcli/start/installation/) Why pdcli ## A CLI that respects your workflow Predictable, composable, and quiet by default — designed to slot into scripts and disappear into your muscle memory. ### Backup & data export Stream your entire account to JSON with pdcli backup --resume, or export any resource to CSV. Resumable, scriptable, and perfect for archives or migrations. ### Health reports pdcli pipeline health and pdcli audit --strict surface stale deals, missing fields, and data drift — so you can keep your Pipedrive clean, accurate, and trustworthy. ### Built for AI agents Self-documenting --help on every command and clean JSON output make pdcli easy for agents to discover and drive — a ready-made tool surface for your LLM workflows. ### Scriptable by design Stable flags, deterministic exit codes, and machine-readable output. Pipe it, branch on it, schedule it. ### JSON, jq & tables Switch between human tables and JSON with one flag, then shape results inline with built-in --jq. ### API token or OAuth Start in seconds with an API token, or run the full OAuth flow when you need it. Either way pdcli stores credentials in your OS keychain, never plaintext on disk. IN PRACTICE ## Close deals from the command line. Track your pipeline, manage deals, and log activities without leaving your shell — so you can spend less time clicking and more time selling. `pdcli` is a fast, scriptable companion for the Pipedrive API: 145 commands across 26 topics. $ npm install -g @wavyx/pdcli * Free & open source * macOS, Linux & Windows * API token or OAuth login * Works with your existing account ******\~/acme — pd ``` $ pdcli deal list --status open --limit 4 ID TITLE VALUE STAGE 4821 Acme renewal €12,000 Negotiation 4774 Northwind expansion € 8,500 Proposal 4710 Globex pilot € 4,200 Qualified 4699 Initech seats € 2,750 Contact 4 open deals · €27,450 open value $ pdcli deal update 4821 --status won ✓ Acme renewal → Won · activity logged $ ▋ ``` 145 commands 26 topics 4 output formats 100% test coverage SIGNATURE MOVES ## One-liners that replace a dashboard Learn one pattern, run everything — real commands, copy-paste ready. This is what living in `pdcli` feels like. `pdcli [target] [flags]` Pipeline health `pdcli pipeline health --pipeline 1` Import a CSV `pdcli person import leads.csv --dry-run` Bulk update via pipe `pdcli deal list --status open --jq '.[].id' | pdcli deal bulk-update --stage 5` Audit as a CI gate `pdcli audit --strict` Escape hatch `pdcli api GET /api/v1/currencies` ## Less time managing, more time closing. Install in seconds, automate the rest. Your pipeline, one command away. [Get started](/pdcli/start/quickstart/) [Command reference](/pdcli/reference/commands/) # CI recipes > Run pdcli in CI with env-var auth, nightly backups, audit gates, bulk imports, and deterministic flags. `pdcli` is built for non-interactive use. In CI, authenticate with environment variables (no keychain, no prompts), and use deterministic flags so a run either succeeds or fails cleanly. ## Authenticate with env vars [Section titled “Authenticate with env vars”](#authenticate-with-env-vars) In CI, set the company domain and a personal API token as environment variables instead of running `pdcli auth login`. The token never touches disk and stays out of shell history. ```bash PDCLI_COMPANY_DOMAIN=acme PDCLI_API_TOKEN=xxxxxxxx pdcli deal list ``` Store the token as an encrypted secret and inject it per step. In GitHub Actions: ```yaml name: pipedrive-audit on: schedule: - cron: '0 6 * * 1' # Mondays 06:00 UTC workflow_dispatch: jobs: audit: runs-on: ubuntu-latest env: PDCLI_COMPANY_DOMAIN: acme PDCLI_API_TOKEN: ${{ secrets.PIPEDRIVE_API_TOKEN }} steps: - uses: actions/setup-node@v5 with: node-version: 20 - run: npm install -g @wavyx/pdcli - run: pdcli audit --strict ``` Env auth means no OS keychain is required on the runner — env vars take precedence over the keychain, so writes (login) are never attempted. See [Security model](/pdcli/concepts/security/). ## Deterministic flags [Section titled “Deterministic flags”](#deterministic-flags) CI runs should be predictable. Two flags matter most: * `--no-retry` — disable automatic backoff on `429` and `5xx`. The command fails immediately with exit `75` (rate limited) or `69` (service unavailable) instead of sleeping through a retry window. Use it when you'd rather fail fast and let the CI scheduler retry the job. * `--timeout ` — cap each request (default 30000). A hung network won't stall the job past your budget. ```bash pdcli deal list --no-retry --timeout 10000 --output json ``` Piped output defaults to JSON, so `pdcli ... | jq` works without `--output json`. Errors go to stderr as JSON; see [Exit codes](/pdcli/automation/exit-codes/). ## Audit as a data-quality gate [Section titled “Audit as a data-quality gate”](#audit-as-a-data-quality-gate) `pdcli audit` runs 11 hygiene checks. With `--strict` it exits `1` when any **must-severity** check has findings (stale deals, duplicate persons, missing fields, …), which fails the job. ```bash pdcli audit --strict # narrow the gate to specific checks: pdcli audit --checks duplicate-persons,missing-fields --strict ``` Exit `1` = the gate caught something. Wire it as a required check on a schedule, or before a deploy that depends on clean data. See [Data-hygiene audit](/pdcli/guides/audit/). ## Nightly backup [Section titled “Nightly backup”](#nightly-backup) Export the whole account to a JSON tree on a schedule. `--resume` skips resources already written, so a re-run after a transient failure continues instead of restarting. ```yaml jobs: backup: runs-on: ubuntu-latest env: PDCLI_COMPANY_DOMAIN: acme PDCLI_API_TOKEN: ${{ secrets.PIPEDRIVE_API_TOKEN }} steps: - uses: actions/setup-node@v5 with: { node-version: 20 } - run: npm install -g @wavyx/pdcli - run: pdcli backup --dir ./pipedrive-backup - uses: actions/upload-artifact@v4 with: name: pipedrive-backup path: ./pipedrive-backup ``` `backup` paces itself against the token budget, so it's safe to run alongside other jobs. ## Incremental pulls [Section titled “Incremental pulls”](#incremental-pulls) For a cheap scheduled sync, pull only what changed since the last run rather than the whole account. The list commands take `--updated-since` (RFC3339, no fractional seconds), and `--limit 500` keeps each page at the maximum so you make the fewest calls: ```bash pdcli deal list --updated-since 2026-06-01T00:00:00Z --limit 500 --output json ``` Pass the previous run's timestamp on each schedule tick and you sync deltas instead of the full set. (`product list` supports `--updated-since` only — products have no `--updated-until`.) If the **daily** token budget runs out, pdcli fails fast: a `429` carrying `x-daily-ratelimit-token-remaining: 0` is not retried (backoff would stall until the daily reset at midnight server time), so the command exits `75` immediately with a clear `Daily API token budget exhausted` message. Add `--verbose` to log the remaining daily budget after each request, so a job can warn before it hits the wall. ## Bulk import in a pipeline [Section titled “Bulk import in a pipeline”](#bulk-import-in-a-pipeline) Validate CSV rows first with `--dry-run`, then import with `--yes` to skip the confirmation prompt (there's no TTY in CI): ```bash pdcli person import people.csv --dry-run # fails (exit 65) on bad rows pdcli person import people.csv --yes # creates rows ``` A failed `--dry-run` exits non-zero, so chaining with `&&` stops the import if validation fails. CSV headers map to fields, custom fields by human name. See [Bulk operations & CSV import](/pdcli/guides/bulk/). # Cookbook > Copy-paste pdcli recipes for common Pipedrive tasks — reports, bulk edits, exports, and cron jobs. Each recipe is one command or pipeline. They assume you're authenticated (or have `PDCLI_COMPANY_DOMAIN` + `PDCLI_API_TOKEN` set). Piped commands emit JSON automatically, so `--jq` and `jq` work without `--output json`. ## Stale open deals to CSV [Section titled “Stale open deals to CSV”](#stale-open-deals-to-csv) ```bash pdcli audit --checks stale-deals --output json \ | jq -r '.[0].items[] | [.id, .title, .days] | @csv' > stale-deals.csv ``` Writes one row per open deal untouched for more than 14 days, with the age in days. ## Bulk stage-move via a saved filter [Section titled “Bulk stage-move via a saved filter”](#bulk-stage-move-via-a-saved-filter) ```bash pdcli deal bulk-update --filter 9 --stage 5 --dry-run # preview targets first pdcli deal bulk-update --filter 9 --stage 5 # confirms, then moves ``` `--dry-run` lists every deal the saved filter selects without writing. Drop it (or add `--yes`) to apply. Partial failures are reported per deal and exit `1`. ## Weekly pipeline-health JSON for a dashboard [Section titled “Weekly pipeline-health JSON for a dashboard”](#weekly-pipeline-health-json-for-a-dashboard) ```bash pdcli pipeline health --pipeline 1 --output json > health-$(date +%F).json ``` Per-stage open count, total value, probability-weighted value, stale-deal count, and deals with no next step. Pass `--pipeline` when the account has more than one pipeline. ## Find a person by email [Section titled “Find a person by email”](#find-a-person-by-email) ```bash pdcli search "jane@acme.com" --item-types person --output json \ | jq '.[] | {id, name}' ``` Add `--exact` for an exact match. ### Scoped vs. multi-type search routing [Section titled “Scoped vs. multi-type search routing”](#scoped-vs-multi-type-search-routing) `search` routes by how many item types you ask for: * **A single routable type** — `--item-types deal`, `person`, `organization`, or `product` — hits that entity's dedicated v2 search endpoint (e.g. `/api/v2/persons/search`), which needs only that entity's OAuth scope and accepts entity-specific server-side filters. Its `--limit` is capped at 100 (the endpoint rejects more). * **Anything else** — no `--item-types`, multiple types, or a non-routable type like `lead`, `file`, or `project` — stays on the generic `itemSearch`. Both search endpoints cost the same 20 rate-limit tokens. When the scope is a single **deal** search, three filters narrow it server-side: `--status` (`open`/`won`/`lost`), `--person` (a person ID), and `--org` (an organization ID). These are rejected (exit 64) with any other scope, since only `/api/v2/deals/search` accepts them: ```bash pdcli search "renewal" --item-types deal --status open --org 7 --output json ``` ## Create a deal with custom fields in one line [Section titled “Create a deal with custom fields in one line”](#create-a-deal-with-custom-fields-in-one-line) ```bash pdcli deal create --title "Acme renewal" --value 5000 --currency EUR \ --stage 3 --field "Deal Size=Large" --field "Score=4.5" ``` Custom fields are given by **human name**; pdcli resolves the 40-char hash key and maps option labels (like `Large`) to their IDs. See [Custom fields](/pdcli/guides/custom-fields/). ## Nightly cron backup [Section titled “Nightly cron backup”](#nightly-cron-backup) ```bash # crontab -e 0 2 * * * PDCLI_COMPANY_DOMAIN=acme PDCLI_API_TOKEN=xxxx \ /usr/local/bin/pdcli backup --dir /backups/pipedrive --resume ``` Full-account export to a JSON tree. `--resume` skips resources finished in a prior run, so a re-run after an interruption continues instead of restarting. ## Duplicate-persons report [Section titled “Duplicate-persons report”](#duplicate-persons-report) ```bash pdcli audit --checks duplicate-persons --verbose ``` Lists every email shared by more than one person, with the person IDs, so you can merge them. Output (table mode, `--verbose`): ```text Sev Check Findings ● Persons sharing the same email 3 Persons sharing the same email {"email":"jane@acme.com","ids":[12,87]} … ``` ## Export persons for a mail merge [Section titled “Export persons for a mail merge”](#export-persons-for-a-mail-merge) ```bash pdcli person list --output csv --fields id,name,email > contacts.csv ``` `--fields` selects columns; `--output csv` produces a header row plus one row per contact. Scope it with `--org ` or `--owner `. ## Log an activity after a call [Section titled “Log an activity after a call”](#log-an-activity-after-a-call) ```bash pdcli activity create --subject "Discovery call" --type call \ --due-date 2026-06-04 --done --deal 42 --note "Budget confirmed, sending quote" ``` `--done` marks it complete on creation; `--deal` links it. On v2 activity writes `--person` maps to a primary participant. ## Won-deal report for the quarter [Section titled “Won-deal report for the quarter”](#won-deal-report-for-the-quarter) ```bash pdcli deal list --status won --output json \ | jq '[.[] | select(.won_time >= "2026-04-01")] | {count: length, total: (map(.value) | add)}' ``` Counts deals won since the quarter start and sums their value. For the full sales-velocity breakdown over a trailing window, use `pdcli metrics velocity --period 90d`. # Exit codes > Deterministic sysexits exit codes pdcli returns, what triggers each, and how a script should react. `pdcli` returns deterministic [sysexits](https://man.freebsd.org/cgi/man.cgi?query=sysexits) exit codes so scripts and agents can branch on the failure without parsing text. Every command shares the same ladder. ## The codes [Section titled “The codes”](#the-codes) | Code | Meaning | Pipedrive trigger | What a script should do | | ---- | ------------------------------- | ------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `0` | Success | 2xx | Continue. | | `1` | Generic error | — | Inspect the message; safe to surface and stop. `audit --strict` returns 1 on must-severity findings; a partial bulk/import failure returns 1 when some rows failed for non-data reasons. | | `64` | Usage / bad input | — | A bad invocation: an unknown flag or a missing/invalid argument (caught by the parser), or a value pdcli validated and rejected (invalid `--period`, unknown `--checks` name, several pipelines with no `--pipeline`, a CSV missing its name column, `--ids` over 100). Fix the invocation — retrying won't help. | | `65` | Data / validation | `400`, `422` | The request body or input data was rejected: a bad field value, a non-numeric id cell in a CSV, an ambiguous upsert match, malformed `--body` JSON, or a conversion the API rejected. Fix the payload; don't retry unchanged. | | `69` | Service unavailable | `5xx`, network | Pipedrive is down/erroring, **or the host is unreachable** (DNS failure, connection refused, or a `--timeout`). pdcli already retried with backoff; safe to retry later. | | `70` | Internal CLI bug | — | An unexpected error inside pdcli — and nothing else (network, usage, and API failures all map elsewhere). File an issue; retrying won't help. | | `75` | Rate limited | `429` | Token budget exhausted. pdcli retries 429s with backoff; if they never clear it surfaces `75` (not `69`), and `--no-retry` surfaces the first one. Back off and retry after the reset window. | | `77` | Auth / permission | `401`, `403` | Token is invalid, expired, or lacks scope — including a **failed OAuth refresh** (`invalid_grant`). Re-authenticate (`pdcli auth login`). Don't loop. A `403` after repeated `429`s is a rate-limit hard stop — wait for the reset, don't retry. | | `78` | Config / account | `402`, missing domain/token, host-lock violation, no keychain | The CLI or account is misconfigured: no company domain, no keychain to write to, a `pdcli api` URL outside your host, a redirect off your host, or a `402` (plan lacks the feature). Fix config or the account — don't retry. | | `8` | Findings present (`watch` only) | — | `pdcli watch` exits `8` when it surfaces new anomalies, so `pdcli watch \|\| notify` fires only on findings. Not part of the general ladder — specific to `watch`. | The HTTP-status mapping lives in `src/lib/errors.js` (`exitCodeForStatus`): `400/422 → 65`, `401/403 → 77`, `402 → 78`, `429 → 75`, `5xx → 69`. On top of that: an oclif parse error (unknown flag, missing/invalid argument) exits `64`; an unreachable host or timeout exits `69`; and only a genuinely unexpected throw exits `70`. ## Error output [Section titled “Error output”](#error-output) The error format **mirrors the success output format** (the same `--output` / `default_output` / TTY rule). Whenever output is **not** an interactive table — an explicit `--output json|yaml|csv`, a non-table `default_output` in the profile, **or stdout piped** — the error is written to **stderr** as a single JSON object, so a machine consumer that gets JSON on success always gets a parseable failure. stdout stays clean so a successful-looking pipe can't swallow a failure: ```json { "error": "ApiError", "message": "Pipedrive API 422: Deal title must not be empty", "exitCode": 65, "statusCode": 422, "path": "/api/v2/deals", "errorInfo": "Please provide a valid title" } ``` `error` is the error class name. `statusCode`, `path`, and `errorInfo` appear only for API errors (they're omitted for usage/config errors). Under `--verbose` the full Pipedrive `body` is added too. Only an **interactive table** context prints the message to stderr in human form; `--verbose` there adds the request path, status code, and `error_info`. ## Branching on the code [Section titled “Branching on the code”](#branching-on-the-code) ```bash pdcli deal get 42 --output json > deal.json case $? in 0) echo "ok" ;; 64) echo "bad invocation — fix flags/args" ; exit 1 ;; 65) echo "bad request — not retrying" ; exit 1 ;; 75|69) echo "transient — retry later" ; exit 1 ;; 77) echo "re-auth needed" ; pdcli auth status ; exit 1 ;; 78) echo "config/account problem" ; pdcli doctor ; exit 1 ;; *) echo "failed with $?" ; exit 1 ;; esac ``` `pdcli` also prints `127` (via the command-not-found hook) when you invoke a command name that doesn't exist. See also: [Troubleshooting](/pdcli/reference/troubleshooting/) for what to do per failure, and [How pdcli talks to Pipedrive](/pdcli/concepts/api-model/) for the retry/backoff rules. # Output & filtering > Choose a format, filter with jq, pick columns, and build scriptable pipelines. Every command accepts the same output flags: `--output`, `--jq`, `--fields`, and `--no-color`. They work the same whether you're reading a list or a single record. ## Output formats [Section titled “Output formats”](#output-formats) ```bash pdcli deal list --output table # default in a terminal pdcli deal list --output json # default when piped pdcli deal list --output yaml pdcli deal list --output csv ``` **The default depends on the destination:** a TTY gets a `table`, a pipe or capture gets `json`. So `pdcli deal list` shows a table to you, while `pdcli deal list | cat` emits JSON automatically. Set a persistent default with `pdcli config set default_output json` (see [configuration](/pdcli/guides/configuration/)). Table output: ```text ┌────┬──────────────┬──────────┬────────┬───────┬────────┬─────┬───────┐ │ ID │ Title │ Value │ Status │ Stage │ Person │ Org │ Owner │ ├────┼──────────────┼──────────┼────────┼───────┼────────┼─────┼───────┤ │ 42 │ Acme renewal │ 5000 EUR │ open │ 3 │ 17 │ 7 │ 1 │ └────┴──────────────┴──────────┴────────┴───────┴────────┴─────┴───────┘ ``` ## Filter with `--jq` [Section titled “Filter with --jq”](#filter-with---jq) `--jq` runs a [jq](https://jqlang.github.io/jq/) expression over the JSON result. The native jq binary is bundled and loaded only when you actually use the flag. Lists arrive as arrays (index with `.[]`); single records arrive as the bare object, so a `get` filters directly: ```bash pdcli deal list --jq '.[].id' pdcli deal list --jq '.[] | {id, title, value}' pdcli deal list --status won --jq '[.[].value] | add' pdcli deal get 42 --jq '.title' ``` (Before 0.9, single records were wrapped in a one-element array and needed `.[0]` — that indirection is gone.) `--jq` overrides `--output` and `--fields` — you get exactly what the expression produces. ## Pick columns with `--fields` [Section titled “Pick columns with --fields”](#pick-columns-with---fields) `--fields` limits a `table` or `csv` to the columns you name (by their data key, not the header): ```bash pdcli deal list --fields id,title,value ``` ```text ┌────┬──────────────┬──────────┐ │ ID │ Title │ Value │ ├────┼──────────────┼──────────┤ │ 42 │ Acme renewal │ 5000 EUR │ └────┴──────────────┴──────────┘ ``` ## Disable color [Section titled “Disable color”](#disable-color) `--no-color` (or setting the `NO_COLOR` environment variable to any value) strips ANSI color — useful in logs and CI where escape codes are noise: ```bash pdcli deal list --no-color ``` ## Recipes [Section titled “Recipes”](#recipes) ### CSV export with chosen columns [Section titled “CSV export with chosen columns”](#csv-export-with-chosen-columns) ```bash pdcli person list --output csv --fields id,name,email > contacts.csv ``` ```text ID,Name,Email 17,Jane Doe,jane@acme.com 21,John Roe,john@globex.com ``` Values containing commas, quotes, or newlines are quoted and escaped automatically. ### jq + xargs pipeline [Section titled “jq + xargs pipeline”](#jq--xargs-pipeline) Pull IDs and act on each. Because the output is piped, JSON is implied, but `--jq` makes the shape explicit: ```bash pdcli deal list --status open --owner 42 --jq '.[].id' \ | xargs -I{} pdcli deal get {} --output json ``` You can also stream IDs straight into a bulk command: ```bash pdcli deal list --status open --jq '.[].id' | pdcli deal bulk-update --owner 42 ``` ### YAML for humans [Section titled “YAML for humans”](#yaml-for-humans) YAML is the friendliest format for eyeballing a single nested record: ```bash pdcli deal get 42 --output yaml ``` ```text id: 42 title: Acme renewal value: 5000 currency: EUR status: open stage_id: 3 ``` ## Resolving custom fields in machine output [Section titled “Resolving custom fields in machine output”](#resolving-custom-fields-in-machine-output) By default `json`, `yaml`, and `csv` keep custom-field values raw — hash keys and numeric option IDs — so scripts have a stable shape to parse. On single-record `get` commands you can opt into readable names with `--resolve-fields`, which swaps hash keys for field names and option IDs for labels. See [Custom fields](/pdcli/guides/custom-fields/) for the details. ## See also [Section titled “See also”](#see-also) * [Quickstart for AI agents](/pdcli/start/agents/) — JSON defaults and exit codes. * [Profiles & configuration](/pdcli/guides/configuration/) — `default_output`. # How pdcli talks to Pipedrive > The dual API, the two paginators, and the token-budget rate limiting pdcli handles for you. `pdcli` wraps Pipedrive's REST API. Most of this is handled for you, but knowing the model explains why some commands behave the way they do. ## Dual API: v2 and v1 [Section titled “Dual API: v2 and v1”](#dual-api-v2-and-v1) Pipedrive runs two API versions. `pdcli` routes each topic to the right one. | Topic | API | | ------------------------------------------------------------------------------------------------ | ------ | | deals, persons, organizations, products, pipelines, stages, activities, projects, fields, search | **v2** | | leads, notes, files, filters, webhooks, goals, users | **v1** | Core CRM is on **v2** because Pipedrive deprecated \~59 v1 core endpoints after 2025-12-31, so pdcli never builds core CRUD on v1. The v1-only topics above have no v2 equivalent yet and are unaffected. Notably, **`pdcli user me` is v1-only** — `/api/v2/users/me` 404s into the web app's HTML page, so the Users API stays on v1. A few v2 specifics that surface in commands: * Updates use **PATCH**, not PUT — only the fields you pass change (this is why update commands say "only provided fields change"). * v2 accepts **JSON bodies only**. * v2 custom-field values are nested under a `custom_fields` object on the entity. Hit anything not yet wrapped with the host-locked [`pdcli api`](/pdcli/guides/api/) escape hatch (`pdcli api GET /api/v2/deals`, `pdcli api GET /api/v1/currencies`). ## Two paginators, never mixed [Section titled “Two paginators, never mixed”](#two-paginators-never-mixed) `list` commands auto-page through every result, capped by `--limit`. The two API versions paginate differently, and pdcli keeps their state separate: * **v2 — cursor.** Sends `cursor` + `limit`, follows `additional_data.next_cursor` until it's null. * **v1 — offset.** Sends `start` + `limit`, follows `additional_data.pagination.next_start` / `more_items_in_collection` until done. The maximum `limit` is **500** for both; pdcli clamps anything higher to 500. (Search is separately capped — itemSearch tops out at 100.) Offset and cursor state are never shared. ## Token-budget rate limiting [Section titled “Token-budget rate limiting”](#token-budget-rate-limiting) Pipedrive doesn't rate-limit by requests-per-second; it spends from a **token budget**. The cost depends on the verb: | Operation | Token cost | | ------------------ | ---------- | | GET one | 2 | | GET list | 20 | | POST / PUT / PATCH | 10 | | DELETE one | 6 | | DELETE list | 10 | | Search | 40 | There's a rolling **2-second burst window** plus a **daily budget** (scales with plan and seats, resets at midnight server time). On a `429`, pdcli reads `x-ratelimit-reset` (falling back to `Retry-After`, then a 2s default) to decide how long to wait before retrying. ### Backoff and the hard stop [Section titled “Backoff and the hard stop”](#backoff-and-the-hard-stop) On a `429`, pdcli waits for the window indicated by `x-ratelimit-reset` (falling back to `Retry-After`, then a 2s default) and retries with exponential backoff. `5xx` responses are also retried with backoff. If Pipedrive escalates persistent rate-limit abuse from `429` to a **`403`**, pdcli treats it as a **hard stop** — it does not retry into it, and exits `77`. Wait for your budget to reset before trying again. `--no-retry` turns off all of this: a `429` becomes an immediate exit `75`, and a `5xx` becomes exit `69`, with no sleeping. Use it in [CI](/pdcli/automation/ci/) when you'd rather fail fast. ## A note on `updated_since` [Section titled “A note on updated\_since”](#a-note-on-updated_since) The analytics commands (`metrics velocity`, `funnel`) query deals by `updated_since`. The v2 API requires **RFC 3339 timestamps at seconds precision** — fractional seconds (milliseconds) are rejected. pdcli formats these for you (e.g. `2026-03-06T00:00:00Z`); if you pass a timestamp through `pdcli api`, drop the milliseconds yourself. See [Exit codes](/pdcli/automation/exit-codes/) for how the HTTP statuses above map to deterministic exit codes. # Security model > Where pdcli keeps your tokens, how the raw api passthrough is host-locked, and how credentials stay out of logs. `pdcli` is designed so a credential is hard to leak, even by mistyping a command. Three properties enforce that: tokens live only in the OS keychain, the raw `api` passthrough is host-locked, and tokens are redacted from output. ## Tokens live only in the OS keychain [Section titled “Tokens live only in the OS keychain”](#tokens-live-only-in-the-os-keychain) Credentials are stored exclusively in your operating system keychain via [`@napi-rs/keyring`](https://www.npmjs.com/package/@napi-rs/keyring) — macOS Keychain, Windows Credential Manager, or libsecret / GNOME Keyring on Linux. They are **never** written to disk in plaintext. Only the company domain and non-secret per-profile settings go into the config file; the token does not. ### Hard-fail writes when there's no keychain [Section titled “Hard-fail writes when there's no keychain”](#hard-fail-writes-when-theres-no-keychain) If no keychain is available, writes that would store a credential **fail by design** rather than fall back to plaintext: ```text OS keychain unavailable. pdcli stores credentials in your operating system keychain (macOS Keychain, Windows Credential Manager, or libsecret on Linux) and refuses to write them to disk in plaintext. Enable a system keychain and retry. ``` This is exit `78`. Reads and deletes **degrade gracefully** — `getToken` returns `null` (you'll be prompted to authenticate) and `logout` is a no-op rather than an error. The practical consequence: on a headless box with no keychain, use env-var auth (below) instead of `auth login`. See [Troubleshooting](/pdcli/reference/troubleshooting/). ### OAuth bundle, including the client secret [Section titled “OAuth bundle, including the client secret”](#oauth-bundle-including-the-client-secret) When you authenticate with `auth login --oauth`, the **entire OAuth bundle** is kept in the keychain: the access token, refresh token, expiry, `api_domain`, client ID, and the **client secret** — never in config. Access tokens refresh automatically before expiry and once on a `401`. The refreshed bundle is written straight back to the keychain. ## Env-var auth keeps tokens out of shell history [Section titled “Env-var auth keeps tokens out of shell history”](#env-var-auth-keeps-tokens-out-of-shell-history) For CI and scripts, set the token in the environment instead of passing it as a flag: ```bash PDCLI_COMPANY_DOMAIN=acme PDCLI_API_TOKEN=xxxxxxxx pdcli deal list ``` Env auth takes precedence over the keychain, so it needs no keychain at all — ideal for headless runners. Prefer the prompt or env over `--api-token` on the command line, which would land the token in your shell history. See [CI recipes](/pdcli/automation/ci/). ## The raw `api` passthrough is host-locked [Section titled “The raw api passthrough is host-locked”](#the-raw-api-passthrough-is-host-locked) `pdcli api ` lets you hit endpoints pdcli doesn't wrap yet. It is **locked to your Pipedrive host** — the company domain (`https://{company}.pipedrive.com`) in token mode, or the OAuth `api_domain`. Before sending, pdcli resolves the path against that base origin and **compares origins**; if they don't match, it refuses: ```text Refusing to send request outside your Pipedrive company host (https://acme.pipedrive.com): https://evil.example.com ``` This is why you pass **paths, not full URLs** (`pdcli api GET /api/v2/deals`). A hallucinated or mistyped absolute URL can't exfiltrate your token to another host. There is no generic `api.pipedrive.com` data host — every request goes to your company subdomain. The same lock applies to file downloads and uploads. ## Redaction and identification [Section titled “Redaction and identification”](#redaction-and-identification) * **Redaction.** Tokens are sent only as headers (`x-api-token` for tokens, `Authorization: Bearer` for OAuth) and are never echoed in logs, errors, or `--verbose` output. `--verbose` shows the request path, status, and Pipedrive's `error_info`, but not credentials. * **User-Agent.** Every request identifies itself as `pdcli/` (e.g. `pdcli/0.5.0`). For where each setting is stored and the full precedence order, see [Config & environment](/pdcli/reference/config/). # Contributing > How to set up pdcli for development, the TDD workflow, and how changes get released. `pdcli` is open source (MIT) and contributions are welcome. This is the short version — the canonical, always-current guide lives in [`CONTRIBUTING.md`](https://github.com/wavyx/pdcli/blob/main/CONTRIBUTING.md) in the repository. ## Setup [Section titled “Setup”](#setup) You need **Node.js 20+**. Clone, install, and run the CLI straight from source — no build, no global install: ```bash git clone https://github.com/wavyx/pdcli.git cd pdcli npm install ./bin/dev.js --help ``` `bin/dev.js` loads a local `.env`, so you can point at a sandbox with `PDCLI_COMPANY_DOMAIN` / `PDCLI_API_TOKEN` without touching your keychain. ## How we work [Section titled “How we work”](#how-we-work) * **Test-driven, always.** Red → green → refactor: write the failing test first, watch it fail for the right reason, then write the minimal code to pass. No production code lands without a failing test. * **Coverage is enforced at 100%** (statements, branches, functions, lines) — exercise the error paths, not just the happy path. * **Lint before every commit:** `npm run lint` runs the same checks as CI (`eslint . && prettier --check .`); `npm run lint:fix` autofixes. * **[Conventional Commits](https://www.conventionalcommits.org/)**, scoped when useful — `feat(deal): …`, `fix(client): …`, `docs: …`, `test: …`. * **Stage explicit paths** — never `git add -A`. Scratch/handoff docs, the `design/` directory, and screenshots are git-ignored and must not be committed. ```bash npm test # full suite npx vitest run test/deal/list.test.js # a single file npm run test:coverage # suite + coverage report ``` ## Release flow [Section titled “Release flow”](#release-flow) Releases are tag-driven and automated (maintainers only): bump the version and update `CHANGELOG.md`, then push a `vX.Y.Z` tag. CI re-runs lint and coverage, publishes to npm via OIDC trusted publishing with provenance, packs native tarballs, and creates the GitHub Release from the changelog. ## Security [Section titled “Security”](#security) Don't put secrets — API tokens, OAuth credentials — in issues, PRs, or test fixtures. Report vulnerabilities privately to the maintainers rather than opening a public issue. # Sales analytics > Sales velocity, an approximate conversion funnel, and a per-stage pipeline health snapshot — computed locally from your deals. pdcli ships a handful of read-only analytics commands. Each fetches deals (and stages/activities or goals where needed) and computes the numbers locally, so they work against any account without extra setup. All accept the global `--output`/`--jq`/`--fields` flags. ## `metrics velocity` [Section titled “metrics velocity”](#metrics-velocity) The Sales Velocity Equation: how much deal value your pipeline produces per day. ```plaintext velocity/day = (open opportunities × win rate × avg won value) / avg cycle days ``` The four levers are measured over a trailing window (`--period`, default `90d`, accepts `Nd` or `Nm`), scoped optionally to a `--pipeline` and/or `--owner`: * **Open opportunities** — count of currently open deals. * **Win rate** — won / (won + lost) among deals *decided* in the window, keyed on `won_time`/`lost_time`. * **Avg won value** — mean value of deals won in the window. * **Avg cycle days** — mean of `won_time − add_time` for those won deals. ```bash pdcli metrics velocity --period 30d --pipeline 1 ``` ```text ┌────────────────────┬───────────────────┐ │ Metric │ Value │ ├────────────────────┼───────────────────┤ │ Open opportunities │ 58 │ │ Win rate (30d) │ 41.2% (7W/10L) │ │ Avg won value │ 8200 │ │ Avg cycle (days) │ 34.5 │ │ Velocity / day │ 5676 │ └────────────────────┴───────────────────┘ ``` If a lever can't be computed (no decided deals, no won deals, or a zero cycle), it shows `n/a` and velocity is `n/a` rather than a misleading zero. ## `deal summary` [Section titled “deal summary”](#deal-summary) A one-call rollup of deal value, computed **server-side**: Pipedrive returns per-currency totals, a probability-weighted total, and a deal count, so there's no list to page through. It's the cheapest way to ask "what's my pipeline worth right now?" ```bash pdcli deal summary pdcli deal summary --status open --pipeline 1 ``` ```text ┌──────────┬─────────┬──────────┬───────┐ │ Currency │ Total │ Weighted │ Count │ ├──────────┼─────────┼──────────┼───────┤ │ EUR │ €148000 │ €132000 │ 24 │ │ USD │ $52000 │ $41600 │ 9 │ └──────────┴─────────┴──────────┴───────┘ ``` Narrow the set with `--status` (`open`/`won`/`lost`), `--pipeline`, `--stage`, or a saved `--filter`. Totals come pre-formatted per currency from the API; `--output json` returns the raw summary object (per-currency totals, grand totals, and counts) for scripting. ## `funnel` [Section titled “funnel”](#funnel) Stage-to-stage conversion. **This is an approximation, stated honestly:** Pipedrive doesn't hand back per-deal stage history in a single cheap call, so the funnel infers stage reach from each closed deal's *final* stage — a deal counts as having reached every stage up to and including the one it ended in, and won deals count for every stage. Accurate per-stage flow would need per-deal history mining; this stays one list call per status. Closed deals are taken over `--period` (default `90d`). The current open distribution per stage is shown alongside. ```bash pdcli funnel --pipeline 1 --period 180d ``` ```text ┌──────────────────────┬──────────────────────────┬─────────────────┬──────────┬────────────┐ │ Stage │ Reached (closed, 180d) │ Conv. from prev │ Open now │ Open value │ ├──────────────────────┼──────────────────────────┼─────────────────┼──────────┼────────────┤ │ Qualified │ 120 │ │ 54 │ 4391545 │ │ Proposal Made │ 64 │ 53% │ 12 │ 980000 │ │ Negotiations Started │ 28 │ 44% │ 2 │ 81909 │ └──────────────────────┴──────────────────────────┴─────────────────┴──────────┴────────────┘ ``` When your account has more than one pipeline, `--pipeline ` is **required** — without it pdcli lists the pipeline IDs and exits 64. With a single pipeline it's inferred. ### Exact transitions: `--exact` [Section titled “Exact transitions: --exact”](#exact-transitions---exact) When the approximation isn't good enough, `--exact` mines the **real** stage transitions from each deal's changelog instead of inferring reach from the final stage. A deal counts as *entering* a stage only when it was actually observed there: every stage the deal moved into, plus its starting stage. A deal created directly in stage 3 — with no stage transitions — counts as entering stage 3 only, not stages 1 and 2. ```bash pdcli funnel --pipeline 1 --exact ``` The accuracy costs one request per deal. Above 100 deals, pdcli warns on stderr about the request volume before it starts mining (the rate limiter then paces the calls); the table trades the approximate `Reached` / `Open` columns for an observed `Entered` count, with the won total reported on a single summary line under the table. The per-stage ratio is labelled `Entered vs prev` rather than a conversion percentage, because exact entries are non-monotonic and the ratio can exceed 100%. In `--exact` mode, `--period` scopes only the closed (won/lost) deals it mines; **open deals are always included** regardless of the window. If a single deal's changelog can't be fetched, that deal is skipped (not the whole run) and pdcli notes how many were skipped on stderr. ## Time intelligence [Section titled “Time intelligence”](#time-intelligence) Three commands answer questions Pipedrive's UI simply doesn't: **where are deals rotting?** (`metrics aging`), **which close dates keep slipping?** (`metrics slippage`), and **what's the real transition graph between stages, backward edges and all?** (`metrics conversion-matrix`). All three reconstruct stage and close-date history by **mining each deal's changelog — one request per deal, 20 tokens each.** That's the cost of accuracy: the changelog is the only source that records when a deal actually entered a stage or had its close date moved. Above 100 deals, pdcli warns on stderr before it starts (the rate limiter then paces the calls); a deal whose changelog can't be fetched is skipped, counted, and reported on stderr rather than aborting the run. `--pipeline ` is required when the account has more than one pipeline, inferred otherwise. ### `metrics aging` [Section titled “metrics aging”](#metrics-aging) Days-in-current-stage for every open deal, bucketed, so you can see at a glance how much value is going stale and where. For each stage it also mines the **completed** dwell distribution (entry → next-entry across all deals) and reports per-stage **p50/p90**, then flags how many open deals have now sat in the stage longer than its own p90 — the deals most likely to be quietly dying. ```bash pdcli metrics aging --pipeline 1 --buckets 30,60,90 ``` ```text ┌──────────────────────┬───────────┬───────────┬───────────┬───────────┬──────────────────┐ │ Stage │ 0-30 │ 30-60 │ 60-90 │ 90+ │ > p90 dwell │ ├──────────────────────┼───────────┼───────────┼───────────┼───────────┼──────────────────┤ │ Qualified │ 31 (820000) │ 12 (410000) │ 6 (180000) │ 5 (240000) │ 4 (p90 47d) │ │ Proposal Made │ 7 (390000) │ 3 (120000) │ 1 (60000) │ 1 (80000) │ 2 (p90 22d) │ │ Negotiations Started │ 1 (40000) │ 1 (42000) │ 0 │ 0 │ — │ └──────────────────────┴───────────┴───────────┴───────────┴───────────┴──────────────────┘ ``` Each bucket cell is `count (summed value)`; `0` when empty. Buckets are `0-N1 / N1-N2 / … / last+` with the lower bound inclusive and the upper exclusive, so a deal sitting exactly 30 days lands in `30-60`. The `> p90 dwell` column shows the count past p90 with the threshold itself (`—` when the stage has no completed-dwell history to learn a p90 from). **One limitation, stated honestly:** a deal's **starting** stage shows an `Unknown` dwell. The changelog records stage *transitions*, and a deal is created *in* its first stage — there is no entry-transition into it — so pdcli can't timestamp when it arrived. Those deals are counted in an `Unknown` column (shown only when present) rather than bucketed against a guess. ### `metrics slippage` [Section titled “metrics slippage”](#metrics-slippage) Open deals whose `expected_close_date` keeps getting pushed out. pdcli walks each deal's close-date changes, counts the forward **pushes**, and reports the **net days slipped** (original → current). `--min-pushes` (default 1) filters to the serial offenders — the deals whose timeline you can no longer trust. ```bash pdcli metrics slippage --pipeline 1 --min-pushes 2 ``` ```text ┌──────┬──────────────────┬───────┬────────┬──────────────────┬──────────────────────────┐ │ Deal │ Title │ Owner │ Pushes │ Net days slipped │ Close date │ ├──────┼──────────────────┼───────┼────────┼──────────────────┼──────────────────────────┤ │ 204 │ Acme renewal │ 42 │ 4 │ 96 │ 2026-02-15 → 2026-05-22 │ │ 311 │ Globex expansion │ 17 │ 3 │ 61 │ 2026-03-01 → 2026-05-01 │ └──────┴──────────────────┴───────┴────────┴──────────────────┴──────────────────────────┘ ``` A high push count with a large net slip is the classic "always closing next month" deal. `Owner` is a user ID — resolve it with `user find` / `user list` (see the [audit guide](/pdcli/guides/audit/)). ### `metrics conversion-matrix` [Section titled “metrics conversion-matrix”](#metrics-conversion-matrix) The real stage-transition graph. Unlike `funnel`, which hides backward moves and collapses re-entries, this counts **every** `stage_id` hop as a directed edge and reports its occurrence count — so a deal that bounces 1→2→1→2 contributes two 1→2 edges and one 2→1 edge. Won/Lost are added as terminal columns, attributed to the stage the deal sat in when it closed. The full source×destination grid is too wide to read, so the table renders a long-format **edge list** with a forward/backward tag, plus a per-source **forward-rate** summary underneath. ```bash pdcli metrics conversion-matrix --pipeline 1 ``` ```text ┌──────────────────────┬──────────────────────┬───────┬───────────┐ │ From │ To │ Edges │ Direction │ ├──────────────────────┼──────────────────────┼───────┼───────────┤ │ Qualified │ Proposal Made │ 64 │ forward │ │ Qualified │ Lost │ 22 │ forward │ │ Proposal Made │ Negotiations Started │ 28 │ forward │ │ Proposal Made │ Qualified │ 9 │ backward │ │ Negotiations Started │ Won │ 18 │ forward │ │ Negotiations Started │ Proposal Made │ 4 │ backward │ └──────────────────────┴──────────────────────┴───────┴───────────┘ ┌──────────────────────┬───────────┬───────────┐ │ Source stage │ Edges out │ Forward % │ ├──────────────────────┼───────────┼───────────┤ │ Qualified │ 86 │ 100% │ │ Proposal Made │ 41 │ 78% │ │ Negotiations Started │ 22 │ 82% │ └──────────────────────┴───────────┴───────────┘ ``` `--output json` returns the raw object — `sources`, `destinations`, the dense `matrix`, and the `edges` / `backwardEdges` lists — for feeding into a graph or a spreadsheet. The backward edges are the ones worth a second look: they're deals your reps walked *back* down the pipeline, often the precursor to a slip or a loss. The process-compliance side of this same changelog data — gate-skips and regressions with actor attribution — lives in [`audit stage-skips`](/pdcli/guides/audit/#process-compliance-audit-stage-skips). ## `metrics coverage` [Section titled “metrics coverage”](#metrics-coverage) Are you carrying enough pipeline to hit your number? `metrics coverage` weighs your **open** pipeline against the revenue still needed to reach a quota and reports the coverage ratio. The classic 3× rule is defined on raw pipeline value, so that drives the verdict; a probability-weighted coverage figure (same weighting `pipeline health` uses) is shown alongside as the risk-adjusted view. The quota comes from your active revenue goal via the [Goals API](https://developers.pipedrive.com/docs/api/v1/Goals), measured over `--period` (default `90d`). `--target ` overrides it with a manual number and skips the Goals API entirely — useful when no goal is configured. Coverage collapses open value into a single ratio, so it can't mix currencies. If the pipeline holds deals in more than one currency the command exits **64**; scope it to one with `--currency ` (e.g. `--currency USD`). ```bash pdcli metrics coverage --pipeline 1 pdcli metrics coverage --target 500000 ``` ```text ┌────────────────────┬──────────┐ │ Metric │ Value │ ├────────────────────┼──────────┤ │ Open pipeline │ 1480000 │ │ Weighted pipeline │ 1320000 │ │ Quota │ 500000 │ │ Progress │ 180000 │ │ Remaining │ 320000 │ │ Coverage ratio │ 4.6x │ │ Weighted coverage │ 4.1x │ │ Verdict │ healthy │ └────────────────────┴──────────┘ ``` The verdict applies the classic 3× rule of thumb against `open pipeline ÷ remaining gap`: **≥ 3× is `healthy`, 2–3× is `borderline`, below 2× is `low`** (and `covered` once progress already meets the quota). When there's no active revenue goal and you didn't pass `--target`, the command exits **64** with guidance to create a goal or supply a target. ## `pipeline health` [Section titled “pipeline health”](#pipeline-health) A per-stage snapshot of your **open** deals — what's there, what it's worth, and what's neglected. ```bash pdcli pipeline health --pipeline 1 ``` ```text ┌──────────────────────┬──────┬────────────┬──────────┬────────────┬──────────────┬────────────┐ │ Stage │ Open │ Value │ Weighted │ Stale >14d │ No next step │ Past close │ ├──────────────────────┼──────┼────────────┼──────────┼────────────┼──────────────┼────────────┤ │ Qualified │ 54 │ 4391545 │ 878309 │ 12 │ 31 │ 4 │ │ Proposal Made │ 12 │ 980000 │ 490000 │ 3 │ 5 │ 1 │ │ Negotiations Started │ 2 │ 81909 │ 65527 │ 1 │ 2 │ 0 │ └──────────────────────┴──────┴────────────┴──────────┴────────────┴──────────────┴────────────┘ ``` Column by column: | Column | Meaning | | ---------------- | ----------------------------------------------------------------------------------------------------------- | | **Open** | Open deals currently in the stage. | | **Value** | Sum of those deals' `value`. | | **Weighted** | Value × probability. Per-deal `probability` wins; otherwise the stage's `deal_probability`; otherwise 100%. | | **Stale >14d** | Open deals not updated in more than 14 days. | | **No next step** | Open deals with no future, not-done activity scheduled. | | **Past close** | Open deals whose `expected_close_date` is before today. | Like `funnel`, `--pipeline` is required when there's more than one pipeline and inferred otherwise. These same hygiene signals drive the [data-hygiene audit](/pdcli/guides/audit/) — `audit` applies them account-wide with severities and a CI gate. ## `metrics forecast` [Section titled “metrics forecast”](#metrics-forecast) Pipedrive's forecast view is UI-only. `metrics forecast` reconstructs it on the command line: your **open** pipeline bucketed by close-month into three views. * **Commit** — full value of deals whose effective win-probability clears `--commit-threshold` (default `70`): the deals you're confident will close. * **Best case** — every open deal at full value: the optimistic ceiling. * **Weighted** — each deal's value × probability (`deal probability ?? stage default ?? 100`). Values are **segregated per currency** — a USD deal and an EUR deal are different units, so they are never summed together (the same rule `metrics coverage` and `deal summary` follow). A deal with no `expected_close_date` lands in a `no-date` bucket; one with no currency under `(none)`. ```bash pdcli metrics forecast --pipeline 1 pdcli metrics forecast --commit-threshold 80 --output json ``` ```text ┌─────┬─────────┬───────┬──────────┬──────────┐ │ Cur │ Month │ Deals │ Commit │ Best case│ … Weighted ├─────┼─────────┼───────┼──────────┼──────────┤ │ EUR │ 2026-07 │ 2 │ 40000 │ 55000 │ 48200 │ USD │ 2026-07 │ 6 │ 120000 │ 180000 │ 142500 │ USD │ 2026-08 │ 4 │ 90000 │ 140000 │ 101000 │ USD │ no-date │ 1 │ 0 │ 20000 │ 12000 └─────┴─────────┴───────┴──────────┴──────────┘ Totals by currency: … ``` `--pipeline` is required only when the account has more than one. For quota-vs-pipeline coverage, see [`metrics coverage`](#metrics-coverage); to combine forecast with everything else in one shot, see [`digest`](#digest). ## `rep scorecard` [Section titled “rep scorecard”](#rep-scorecard) Per-rep performance across **all** pipelines (narrow with `--pipeline`/`--owner`). It reuses the velocity equation per owner and adds the hygiene a manager actually chases. ```bash pdcli rep scorecard --period 90d pdcli rep scorecard --owner 42 --output json ``` ```text ┌───────┬────────┬──────┬───────────────────┬───────────┬────────────┬───────┬────────────┬─────────┬────────────┐ │ Rep │ Active │ Open │ Win rate │ Cycle (d) │ Velocity/d │ Stale │ Past close │ No date │ No contact │ ├───────┼────────┼──────┼───────────────────┼───────────┼────────────┼───────┼────────────┼─────────┼────────────┤ │ Alice │ yes │ 14 │ 58% (11W/8L) │ 34 │ 2380 │ 2 │ 1 │ 0 │ 0 │ │ Bob │ no │ 9 │ n/a │ n/a │ n/a │ 4 │ 2 │ 3 │ 1 │ └───────┴────────┴──────┴───────────────────┴───────────┴────────────┴───────┴────────────┴─────────┴────────────┘ ``` Win rate and cycle are decided over the trailing `--period` (default `90d`). Owners with no matching user fall back to `#`; deals with no owner roll up under **Unassigned**. Reps are ordered by velocity per day (those with too little data to compute it sort last). ## `digest` [Section titled “digest”](#digest) The Monday packet in one command. A single pipeline-scoped fetch is fanned into velocity, pipeline health, coverage, funnel, forecast and the must-fix hygiene checks — no need to run six commands. ```bash pdcli digest --pipeline 1 # structured table in the terminal pdcli digest --output json # the whole packet for agents pdcli digest --deep # + changelog-mined aging/slippage/stage-skips pdcli digest --format md --out monday.md # shareable markdown artifact pdcli digest --format html --out monday.html ``` By default `digest` skips changelog mining and stays cheap. `--deep` mines each deal's history (one request per deal, the usual >100-deal warning applies) to add aging, close-date slippage and stage-skip sections. The quota comes from your revenue goal over `--period`, or `--target `; if no goal is configured the coverage section is simply omitted (the digest never fails for that reason). `--format md|html` renders the packet as a shareable document — pipe it to Slack/email from cron, or write it to a file with `--out`. These artifact formats are distinct from the global `--output table|json|yaml|csv` (which stays for scripting the structured packet). # The raw api escape hatch > Call any Pipedrive v1 or v2 endpoint directly — host-locked to your own company domain so a mistyped URL can't leak your token. `pdcli api` is the escape hatch for anything the typed commands don't wrap yet. It sends a raw request to a path you give and prints the JSON response, while still using your stored credentials and the same rate-limit handling as every other command. ```bash pdcli api [--body ...] ``` `METHOD` is one of `GET`, `POST`, `PUT`, `PATCH`, `DELETE`. The path can target **either API version** — v1 and v2 both work: ```bash pdcli api GET /api/v2/deals pdcli api GET /api/v1/currencies pdcli api POST /api/v2/deals --body '{"title":"New deal"}' pdcli api DELETE /api/v1/webhooks/1 ``` ## Host-locking [Section titled “Host-locking”](#host-locking) The request is **locked to your own Pipedrive host** — the `{company}.pipedrive.com` origin from your token-mode login, or the `api_domain` returned by OAuth. pdcli resolves the path against that origin and refuses anything that resolves elsewhere, including **other `*.pipedrive.com` subdomains**. A mistyped, hallucinated, or attacker-supplied absolute URL can't be used to exfiltrate your token. The refusal exits 78: ```bash pdcli api GET https://evil.example.com/steal ``` ```text Error: Refusing to send request outside your Pipedrive company host (https://acme.pipedrive.com): https://evil.example.com ``` There is no generic `api.pipedrive.com` data host — always use your company origin via a relative path like `/api/v2/deals`. ## Request bodies [Section titled “Request bodies”](#request-bodies) For `POST`, `PUT`, and `PATCH`, supply a JSON body with `--body`, which takes either an inline string or an `@file` path (`GET` and `DELETE` ignore it): ```bash pdcli api POST /api/v2/deals --body '{"title":"Inline JSON"}' # inline string pdcli api POST /api/v2/deals --body @new-deal.json # @file ``` The body is parsed as JSON before sending; invalid JSON fails before any request. ## Filtering with `--jq` [Section titled “Filtering with --jq”](#filtering-with---jq) The response is printed as pretty JSON by default. Add `--jq` to filter it with a jq expression — handy for pulling one value out of a raw call: ```bash pdcli api GET /api/v2/pipelines --jq '.data[] | {id, name}' ``` ## v2 notes [Section titled “v2 notes”](#v2-notes) When you hit v2 endpoints directly, remember the v2 conventions: * **Use `PATCH` for updates**, not `PUT` — v2 update semantics are partial (only the fields you send change). * **Bodies are JSON only.** v2 doesn't take form-encoded data. * Lists use **cursor** pagination (`cursor`/`limit`, `limit` max 500); v1 lists use **offset** pagination (`start`/`limit`). See [How pdcli talks to Pipedrive](/pdcli/concepts/api-model/). `pdcli api` makes a single request — it does **not** auto-paginate — so pass `cursor`/`start` yourself for more pages. ## When to reach for it [Section titled “When to reach for it”](#when-to-reach-for-it) Use the typed commands when they exist — they resolve custom-field names, format tables, and paginate for you. Reach for `pdcli api` when you need an endpoint pdcli doesn't wrap yet, for example: * **Currencies** — `GET /api/v1/currencies` * **Lead labels** — `GET /api/v1/leadLabels` * **Recents / changelog** — `GET /api/v1/recents` * Any other v1/v2 endpoint, including newer ones, against your own host. Everything else carries over: errors map to the same [exit codes](/pdcli/automation/exit-codes/), 429s back off, and the token is never printed. # Data-hygiene audit > Eleven data-quality checks for stale deals, missing fields, duplicates, and overdue pileups — with a CI gate. `pdcli audit` runs 11 data-hygiene checks across your deals, persons, organizations, and activities, and prints a findings count per check. Run a subset with `--checks`, see the flagged items with `--verbose`, and fail CI on serious findings with `--strict`. ```bash pdcli audit ``` ```text ┌─────┬──────────────────────────────────────────────────────┬──────────┐ │ Sev │ Check │ Findings │ ├─────┼──────────────────────────────────────────────────────┼──────────┤ │ ● │ Open deals untouched for >14 days │ 18 │ │ ● │ Open deals with no future activity scheduled │ 31 │ │ ● │ Open deals past their expected close date │ 4 │ │ ● │ Open deals missing owner, person/org, value, or currency │ 7 │ │ ● │ Open deals far older than the typical won cycle │ 2 │ │ ○ │ Closed deals missing their close timestamp │ 0 │ │ ● │ Persons sharing the same email │ 3 │ │ ● │ Persons with neither email nor phone │ 12 │ │ ○ │ Organizations with the same normalized name │ 5 │ │ ○ │ Overdue open activities piling up per owner │ 44 │ │ ○ │ Deals with a value but no currency │ 0 │ └─────┴──────────────────────────────────────────────────────┴──────────┘ ``` `●` is a **must**-severity check, `○` is a **should**. Only `must` checks gate `--strict`. ## The checks [Section titled “The checks”](#the-checks) | Name | Sev | Detection rule | | ----------------------- | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `stale-deals` | must | Open deal where `now − update_time > 14 days`. | | `no-next-activity` | must | Open deal with no not-done activity due today or later linked to it. | | `past-close-date` | must | Open deal whose `expected_close_date` is before today. | | `missing-fields` | must | Open deal missing any of: owner; both person and org; a value > 0; or currency when it has a value. | | `ancient-deals` | must | Open deal whose age (`now − add_time`) exceeds 2× the avg won cycle, or 168 days when no won deals exist. | | `missing-close-time` | should | A `won` deal with no `won_time`, or a `lost` deal with no `lost_time`. | | `duplicate-persons` | must | Two or more persons sharing the same email (trimmed, lowercased). | | `uncontactable-persons` | must | Person with neither a non-empty email nor phone. | | `duplicate-orgs` | should | Two or more orgs with the same normalized name (lowercased, common suffixes like Inc/Ltd/LLC/GmbH stripped, non-alphanumerics removed), **plus** fuzzy near-matches that normalize differently but score Jaro-Winkler ≥ 0.92. | | `overdue-activities` | should | Open (not-done) activities past due, counted per owner. | | `currency-missing` | should | Open deal with a value > 0 but no currency. | The thresholds are constants in the audit: stale = 14 days, ancient = 2× the avg won cycle (`won_time − add_time` across won deals) with a 168-day fallback when there are no won deals to learn from. The `Findings` count is the number of flagged items per check — except `overdue-activities`, where it's the total overdue activities summed across owners. `duplicate-orgs` reports two shapes of finding. Exact groups are tagged `kind: exact` with the shared normalized `name` and the `ids`. Fuzzy near-matches — pairs whose names normalize differently but still score [Jaro-Winkler](https://en.wikipedia.org/wiki/Jaro%E2%80%93Winkler_distance) ≥ 0.92 — are tagged `kind: fuzzy`, carry both `names` and `ids`, and include the `score`: ```text {"kind":"exact","name":"acme","ids":[12,48]} {"kind":"fuzzy","names":["acmecorp","acmecrop"],"ids":[12,93],"score":0.9667} ``` ## Fix what the audit finds [Section titled “Fix what the audit finds”](#fix-what-the-audit-finds) The duplicate checks pair naturally with the merge commands — close a duplicate finding by folding the loser into the survivor: ```bash pdcli person merge 123 --into 456 pdcli org merge 12 --into 48 ``` **The positional `` is the losing record and Pipedrive deletes it**; `--into` is the survivor whose data wins on conflict. Both commands confirm before deleting unless you pass `--yes`. So a `duplicate-persons` or `duplicate-orgs` finding becomes a one-liner: merge the duplicate `id` into the one you want to keep. When the right fix is to change a record's *type* — a misfiled deal that's really an early lead, or a duplicate that should fold back into the lead inbox — the convert commands move it across without re-keying. `lead convert ` turns a lead into a deal (and `deal convert ` goes the other way); both run as async jobs, so add `--wait` to block until the conversion finishes before you act on the result. On success the source record is deleted, so `deal convert` confirms first unless you pass `--yes`: ```bash pdcli lead convert adf21080-0e10-11eb-879b-05d71fb426ec --wait # promote a lead to a deal pdcli deal convert 204 --wait --yes # demote a misfiled deal to a lead ``` When a deal finding needs investigating before you touch it — *why* is this deal stale, or who changed its stage — `deal history ` prints its field-change audit trail (who changed what, when), newest-first. Narrow to one field with `--field`: ```bash pdcli deal history 204 # full change log for a flagged deal pdcli deal history 204 --field stage_id # just the stage moves ``` Findings carry owner **user IDs**, not names. Resolve them with `user list` (the whole directory) or `user find ` (by name, or `--by-email` for an address): ```bash pdcli user list --output json pdcli user find "jane" --output json ``` ## Run a subset [Section titled “Run a subset”](#run-a-subset) ```bash pdcli audit --checks stale-deals,duplicate-persons --verbose ``` Pass a comma-separated list of check names (the `Name` column above). An unknown name fails fast with exit 64 and lists the valid names. ## See the items: `--verbose` [Section titled “See the items: --verbose”](#see-the-items---verbose) In table output, `--verbose` prints the flagged items under each check that has findings, as one JSON object per line, **truncated at 25 items** with a "… N more" footer: ```text Open deals untouched for >14 days {"id":204,"title":"Acme renewal","days":31} {"id":311,"title":"Globex expansion","days":19} {"id":340,"title":"Initech seats","days":17} Persons sharing the same email {"email":"jane@acme.com","ids":[88,142]} ``` A check with more than 25 findings prints the first 25 followed by a `… N more` footer (N = total − 25). For full machine-readable output (no truncation), use `--output json` — every check includes its full `items` array. ## Gate CI: `--strict` [Section titled “Gate CI: --strict”](#gate-ci---strict) `--strict` exits **1** if any `must`-severity check has findings, naming them; otherwise exit 0. `should` findings never fail the gate. ```bash pdcli audit --strict ``` ```text Error: 7 must-severity checks found issues: stale-deals, no-next-activity, past-close-date, missing-fields, ancient-deals, duplicate-persons, uncontactable-persons ``` ## Recipe: weekly hygiene report [Section titled “Recipe: weekly hygiene report”](#recipe-weekly-hygiene-report) Run a scoped, strict audit on a schedule and let the exit code decide whether to alert. With env-var auth it's fully non-interactive: ```bash PDCLI_COMPANY_DOMAIN=acme PDCLI_API_TOKEN=$PD_TOKEN \ pdcli audit --checks stale-deals,no-next-activity,duplicate-persons --strict --output json \ > hygiene-$(date +%F).json \ || echo "Hygiene issues found — see report" ``` See [CI recipes](/pdcli/automation/ci/) and [Exit codes](/pdcli/automation/exit-codes/) for wiring this into a pipeline. ## Process compliance: `audit stage-skips` [Section titled “Process compliance: audit stage-skips”](#process-compliance-audit-stage-skips) Where the 11 checks above grade your *data*, `audit stage-skips` grades your *process*: did deals move through the pipeline the way they're supposed to? It mines each deal's changelog (open, won, and lost — history matters in every state) and walks the `stage_id` transitions chronologically, flagging two kinds of out-of-band move with **who made them**: * **Forward skips** — a deal that jumped a gate (destination order > source order + 1), listing the stages it leapt over. A clean single-stage advance is normal and never flagged. * **Backward moves** — a deal pulled back to an earlier stage (often sandbagging: dragging a deal down to look conservative, or to reset its age). ```bash pdcli audit stage-skips --pipeline 1 ``` ```text ┌──────┬──────────┬──────────────────────────┬──────────────────────┬──────────────────────┬───────┐ │ Deal │ Kind │ From │ To │ Skipped gates │ Actor │ ├──────┼──────────┼──────────────────────────┼──────────────────────┼──────────────────────┼───────┤ │ 204 │ skip │ Qualified (1) │ Negotiations Started (3) │ Proposal Made │ 42 │ │ 311 │ backward │ Negotiations Started (3) │ Qualified (1) │ │ 17 │ └──────┴──────────┴──────────────────────────┴──────────────────────┴──────────────────────┴───────┘ ``` `From`/`To` carry the stage's order number in parentheses; `Skipped gates` names the stages a forward skip leapt over (empty for a backward move). `Actor` is the user ID who made the change — resolve it with `user find` / `user list`, exactly as for the deal findings above. This is **informational and always exits 0** — it surfaces moves for review rather than gating CI. Cross-pipeline hops and moves touching a since-deleted stage are ignored (stage order is only comparable within one pipeline). Like the time-intelligence metrics, it costs **one changelog request per deal (20 tokens each)** and warns on stderr above 100 deals. The conversion-side view of the same data — every transition counted, with backward edges and Won/Lost terminals — is [`metrics conversion-matrix`](/pdcli/guides/analytics/#metrics-conversion-matrix). # Authentication > Token mode, OAuth mode, CI patterns, and how to switch between them. pdcli supports two auth modes. Credentials always live **only in your OS keychain** — never in plaintext on disk. If no keychain is available, commands that store credentials hard-fail by design; use [environment variables](#ci-and-headless) instead. ## Token mode (default) [Section titled “Token mode (default)”](#token-mode-default) A personal API token is the quickest way in. Generate one at [app.pipedrive.com/settings/api](https://app.pipedrive.com/settings/api) — it is tied to your user and inherits your permissions. ```bash pdcli auth login ``` You are prompted for the company domain (`acme` from `acme.pipedrive.com`) and the token. The token is validated against the API, then stored in the keychain; the company domain is saved to your profile config. pdcli sends it as the `x-api-token` header on every request, host-locked to your domain. Non-interactively: ```bash pdcli auth login --company acme --api-token ``` Prefer the prompt or an environment variable over `--api-token` so the token stays out of your shell history. ## OAuth mode [Section titled “OAuth mode”](#oauth-mode) Use OAuth 2.0 with your **own** Pipedrive Developer Hub app (bring-your-own client). In the Developer Hub, register the callback URL exactly as: ```text http://127.0.0.1:9999/callback ``` Then run the browser authorization-code flow: ```bash pdcli auth login --oauth pdcli auth login --oauth --client-id --client-secret ``` pdcli opens your browser, captures the code on a local loopback server, and exchanges it for tokens. The **entire bundle — access token, refresh token, `api_domain`, and the client secret — is stored only in the keychain.** Config records just the mode and display domain. Access tokens refresh automatically: proactively when they're within \~5 minutes of expiry, and reactively on a 401. You never run a manual refresh. If port 9999 is in use, pick another with `--port` — but it must match the callback URL registered in your Developer Hub app. ## Check status and log out [Section titled “Check status and log out”](#check-status-and-log-out) ```bash pdcli auth status ``` Token mode shows the host and a best-effort identity check: ```text Auth Status Profile: default Keychain: OS keychain API host: https://acme.pipedrive.com Token: present (keychain) Authenticated User Name: Jane Doe Email: jane@acme.com ``` OAuth mode shows the mode and time to expiry: ```text Auth mode: OAuth (auto-refresh) Token: present (expires in 47m) ``` Log out to clear stored credentials for the active profile (both modes): ```bash pdcli auth logout ``` ## CI and headless [Section titled “CI and headless”](#ci-and-headless) For pipelines and agents, skip the keychain entirely and pass credentials via the environment. They take precedence over any stored profile: ```bash PDCLI_COMPANY_DOMAIN=acme PDCLI_API_TOKEN=$PIPEDRIVE_TOKEN pdcli deal list ``` For OAuth client credentials in CI, `PDCLI_CLIENT_ID` and `PDCLI_CLIENT_SECRET` are read by `auth login --oauth`. Store all secrets in your CI secret manager, never in the repo. ## Switching modes [Section titled “Switching modes”](#switching-modes) Auth mode is per-profile. Running `auth login` (token) or `auth login --oauth` on the same profile replaces the stored credentials and updates the recorded `auth_mode`. To keep both around, use separate [profiles](/pdcli/guides/configuration/): ```bash pdcli auth login --profile token-acme pdcli auth login --oauth --profile oauth-acme ``` # Full-account backup > Export your whole Pipedrive account to a JSON tree with a resumable, checkpointed run. `pdcli backup` exports your entire account to a directory of JSON files — one per resource — with a manifest checkpoint written after each resource so an interrupted run can pick up where it left off. ```bash pdcli backup # → ./pipedrive-backup pdcli backup --dir ./my-backup # choose the directory pdcli backup --dir ./my-backup --resume # skip resources already done ``` The default directory is `pipedrive-backup` in the current working directory. ## What gets exported [Section titled “What gets exported”](#what-gets-exported) 18 resources, fetched sequentially. Each becomes a `.json` file containing the full list of items: | File | Source | Pager | | ------------------------- | ---------------------------- | --------- | | `deals.json` | `/api/v2/deals` | v2 cursor | | `persons.json` | `/api/v2/persons` | v2 cursor | | `organizations.json` | `/api/v2/organizations` | v2 cursor | | `activities.json` | `/api/v2/activities` | v2 cursor | | `products.json` | `/api/v2/products` | v2 cursor | | `pipelines.json` | `/api/v2/pipelines` | v2 cursor | | `stages.json` | `/api/v2/stages` | v2 cursor | | `dealFields.json` | `/api/v2/dealFields` | v2 cursor | | `personFields.json` | `/api/v2/personFields` | v2 cursor | | `organizationFields.json` | `/api/v2/organizationFields` | v2 cursor | | `productFields.json` | `/api/v2/productFields` | v2 cursor | | `activityFields.json` | `/api/v2/activityFields` | v2 cursor | | `leads.json` | `/api/v1/leads` | v1 offset | | `notes.json` | `/api/v1/notes` | v1 offset | | `users.json` | `/api/v1/users` | plain | | `filters.json` | `/api/v1/filters` | plain | | `webhooks.json` | `/api/v1/webhooks` | plain | | `currencies.json` | `/api/v1/currencies` | plain | The five `*Fields.json` files are your field **definitions** — including the per-account custom-field hash keys and their option labels — so a restored record's `custom_fields` hashes stay meaningful. Core CRM uses v2; leads, notes, users, filters, webhooks, and currencies come from v1 (which is where those endpoints live). ## The manifest and `--resume` [Section titled “The manifest and --resume”](#the-manifest-and---resume) After each resource is written, pdcli updates `manifest.json` in the backup directory with the completed resources and their item counts: ```json { "started_at": "2026-06-04T09:00:00.000Z", "completed": ["deals", "persons", "organizations"], "counts": { "deals": 1840, "persons": 5210, "organizations": 612 }, "updated_at": "2026-06-04T09:01:12.000Z" } ``` If the run is interrupted, re-run with `--resume` and pdcli skips every resource already in the manifest, refetching only what's missing: ```bash pdcli backup --dir ./my-backup --resume ``` ```text Backup complete: 4/18 resources exported to ./my-backup (14 skipped) ``` Without `--resume`, a fresh run starts a new manifest and re-exports everything. ## Token-cost awareness [Section titled “Token-cost awareness”](#token-cost-awareness) The export is **sequential by design** to keep the rate-limit budget predictable. Each list page costs 20 tokens; the client's 429 backoff handles any bursts (see [How pdcli talks to Pipedrive](/pdcli/concepts/api-model/)). A large account with hundreds of pages will take time — that's the trade for not tripping the limiter. ## Scheduling [Section titled “Scheduling”](#scheduling) Because `backup` is one non-interactive command with a deterministic exit code, it drops straight into cron or CI. Use env-var auth so nothing prompts: ```bash # nightly at 02:00 — see CI recipes for full examples 0 2 * * * PDCLI_COMPANY_DOMAIN=acme PDCLI_API_TOKEN=$PD_TOKEN \ pdcli backup --dir /backups/pipedrive-$(date +\%F) ``` For incremental safety on a flaky link, point repeated runs at the same directory with `--resume`. See [CI recipes](/pdcli/automation/ci/) for a scheduled-job example. ## Diffing snapshots: `backup diff` [Section titled “Diffing snapshots: backup diff”](#diffing-snapshots-backup-diff) Because pdcli owns the backup format, it can diff two snapshots **entirely locally — zero API calls**. Point it at two backup directories (older first): ```bash pdcli backup diff ./backup-2026-06-10 ./backup-2026-06-11 ``` ```text ┌───────────┬─────┬──────────┬────────────┬──────────┬──────────┐ │ Resource │ ID │ Change │ Field │ Old │ New │ ├───────────┼─────┼──────────┼────────────┼──────────┼──────────┤ │ deals │ 42 │ modified │ value │ 100000 │ 150000 │ │ deals │ 42 │ modified │ Region │ EMEA │ APAC │ │ deals │ 99 │ added │ │ │ │ │ persons │ 7 │ removed │ │ │ │ └───────────┴─────┴──────────┴────────────┴──────────┴──────────┘ 3 added · 1 removed · 5 modified (11 field changes) ``` Each record is classified `added` / `removed` / `modified`, and every changed field is one row with its old → new value. Custom-field hash keys and option ids are resolved to names and labels using each snapshot's own captured `*Fields.json` (still no network); pass `--raw` to keep the raw hashes. Resources present in only one snapshot are reported separately rather than shown as wholesale added/removed. `--output json` emits the full `{ summary, skipped, changes }` object for diffing in CI. ## Incremental export: `sync warehouse` [Section titled “Incremental export: sync warehouse”](#incremental-export-sync-warehouse) Where `backup` writes a full snapshot every run, `sync warehouse` appends only what changed since the last run — an **incremental NDJSON feed** for loading into a data warehouse: ```bash pdcli sync warehouse --dir ./warehouse # first run seeds a full export pdcli sync warehouse --dir ./warehouse # later runs append only deltas pdcli sync warehouse --dir ./warehouse --full # rebuild from scratch ``` Each of the five incremental entities (deals, persons, organizations, activities, products) appends to `.ndjson` (one JSON object per line) and advances its **own** high-water mark in `manifest.json`. The watermark moves to the newest `update_time` seen **+ 1 second** (the API's `updated_since` is inclusive, so this avoids re-emitting the boundary record), and only after the append succeeds — an interrupted run replays rather than skips. `--since` overrides the start for every entity. Hard deletes are not captured Pull-based change capture sees creates and updates only. A record hard-deleted in Pipedrive simply stops appearing — the NDJSON log keeps its last-known row. Treat the output as an append log keyed by `(entity, id)` and dedupe / last-write-wins on load, and reconcile deletions periodically against a full `backup` key-set. # Bulk operations & CSV import > Update many deals at once by IDs, a saved filter, or piped stdin, and bulk-create persons and organizations from CSV. pdcli has two bulk workflows: `deal bulk-update` for changing many existing deals at once, and `person import` / `org import` for creating records from a CSV. Both pace their writes to stay inside Pipedrive's rate limits and report per-item failures honestly. ## Bulk-updating deals [Section titled “Bulk-updating deals”](#bulk-updating-deals) `deal bulk-update` selects a set of deals and applies the same change to each. Pick the targets with **exactly one** selector: ```bash pdcli deal bulk-update --ids 1,2,3 --stage 5 # explicit IDs pdcli deal bulk-update --filter 9 --status won # a Pipedrive saved filter ID pdcli deal list --status open --jq '.[].id' | pdcli deal bulk-update --owner 42 # stdin ``` `--ids` and `--filter` are mutually exclusive. With neither flag and no piped input you get exit 64 (`No targets — pass --ids, --filter, or pipe ids on stdin`). ### Scoping the set with list power-params [Section titled “Scoping the set with list power-params”](#scoping-the-set-with-list-power-params) The cleanest way to choose what bulk work touches is to scope it with `deal list` (or `person list`, `org list`, `product list`) and pipe the result in. The list commands share a small set of selectors: * `--filter ` — a Pipedrive **saved filter** ID. Build and discover IDs with [`filter list`](/pdcli/reference/commands/) (`pdcli filter list --type deals`), then reuse the same ID for both listing and `deal bulk-update --filter`. * `--ids ` — fetch an explicit set (max 100). * `--sort-by` / `--sort-direction` — order the results; the available sort fields vary by entity (e.g. deals sort on `id`, `update_time`, or `add_time`). * `--updated-since` / `--updated-until` — bound by last-modified time. Both take **RFC3339 with no fractional seconds** (`2026-06-01T00:00:00Z`). `product list` supports only `--updated-since` — products have no `--updated-until`. Because a saved filter ID works identically in `deal list` and `deal bulk-update`, you can preview the exact set, then apply the change to it: ```bash pdcli deal list --filter 9 --sort-by update_time --sort-direction asc # see the set pdcli deal list --filter 9 --jq '.[].id' | pdcli deal bulk-update --stage 5 # act on it ``` ### What you can pipe on stdin [Section titled “What you can pipe on stdin”](#what-you-can-pipe-on-stdin) When stdin is not a TTY, pdcli reads it and accepts three shapes: * **Newline-separated IDs** — one integer per line. * **A JSON array of IDs** — `[1, 2, 3]`. * **A JSON array of objects** with an `id` field — e.g. the output of `deal list --output json`, so `pdcli deal list --status open | pdcli deal bulk-update --status won` works. ### What you can change [Section titled “What you can change”](#what-you-can-change) Any combination of `--stage`, `--pipeline`, `--status` (`open|won|lost`), `--owner`, repeatable `--field "Name=Value"` (resolved like everywhere — see [Custom fields](/pdcli/guides/custom-fields/)), and a raw `--body` JSON merge (typed flags win). You must pass at least one change, or it's exit 64. ### Preview, then confirm [Section titled “Preview, then confirm”](#preview-then-confirm) `--dry-run` lists the targets and the change without touching anything: ```bash pdcli deal bulk-update --filter 9 --stage 5 --dry-run ``` ```text Would update 12 deals: 1, 2, 5, 8, 9, 13, 21, 34, 55, 89, 144, 233 Change: {"stage_id":5} ``` Without `--dry-run`, pdcli confirms before writing. Pass `-y`/`--yes` to skip the prompt in scripts. Declining aborts with exit 1. ### Pacing and partial failures [Section titled “Pacing and partial failures”](#pacing-and-partial-failures) Updates run **sequentially** with a short gap between them, so a burst of writes stays within the 2-second rate-limit window (each write costs 10 tokens); the client's 429 backoff covers anything that slips through. Per-deal failures are collected, not thrown — you get a full summary, and any failure exits 1: ```text Updated 10/12 deals ✘ deal 55: Pipedrive API 404: Deal not found ✘ deal 233: Pipedrive API 403: insufficient permissions Error: 2 of 12 updates failed ``` Note Pipedrive v2 has **no bulk-by-IDs endpoints**, so the loop is client-side: one `PATCH /api/v2/deals/{id}` per target. That's why pacing and the partial-failure report matter — each deal succeeds or fails on its own. ## Importing persons and organizations from CSV [Section titled “Importing persons and organizations from CSV”](#importing-persons-and-organizations-from-csv) `person import ` and `org import ` bulk-create records from a CSV. The first row is the header; **a `name` column is required** (case-insensitive), or it's exit 64. Duplicate column headers are rejected (exit 65) rather than silently keeping only the last cell. Each header maps to a field. Recognized special columns build standard values directly; every other header is resolved through the entity's field definitions — by human name, `field_code`, or hash key — including custom fields and their option labels. | Entity | Special columns | | ------ | ---------------------------------------------- | | person | `name`, `email`, `phone`, `org_id`, `owner_id` | | org | `name`, `owner_id` | For persons, `email` and `phone` become the primary email/phone. Empty cells are skipped. ```csv name,email,Segment,owner_id Jane Doe,jane@acme.com,Enterprise,7 John Roe,john@acme.com,SMB,7 ``` ```bash pdcli person import people.csv --dry-run # validate every row, create nothing pdcli person import people.csv # confirms, then creates (custom fields by name) ``` `--dry-run` runs the full validation — header mapping, field resolution, option-label lookups — against every row and reports the count without writing: ```text 24 rows valid — nothing created ``` A bad value fails the whole run before any writes, naming the offending row (exit 65): ```text Error: CSV row 5: Unknown option "Huge" for field "Deal Size". Valid: Small, Medium, Large ``` A real import confirms (skip with `-y`/`--yes`), paces the creates like bulk-update, and reports partial failures the same way — any failure exits 1: ```text Imported 23/24 persons ✘ John Roe: Pipedrive API 422: email already in use Error: 1 of 24 rows failed ``` ## Idempotent upsert (match-or-create) [Section titled “Idempotent upsert (match-or-create)”](#idempotent-upsert-match-or-create) `person upsert`, `org upsert`, and `deal upsert` make a write **safe to re-run**: match an existing record, then **create** it if absent or **PATCH only the fields that changed** if exactly one matches. Re-running the same command a second time is a no-op. ```bash pdcli person upsert a@x.com --by email --field "Tier=Gold" pdcli deal upsert "Acme expansion" --by title --body '{"value":5000}' pdcli org upsert "D-42" --by "External ID" --field "Status=Active" ``` The match value is the positional argument; `--by` names the field to match on — a built-in key or a **searchable** custom field (`address`, `varchar`, `text`, `double`, `monetary`, `phone`): | Entity | Built-in `--by` keys | | ------ | ------------------------ | | person | `email`, `name`, `phone` | | org | `name` | | deal | `title` | ### Why it refuses instead of guessing [Section titled “Why it refuses instead of guessing”](#why-it-refuses-instead-of-guessing) Pipedrive's `exact_match` search is **not** a unique-key lookup — it's case-insensitive, and a custom-field search scans every custom field at once. So pdcli re-verifies each candidate client-side and counts the survivors: * **0 matches** → create (the match value is injected into the new record). * **1 match** → PATCH only the differing fields; if nothing differs, it reports `unchanged` and issues no write. * **more than 1** → **refuse with exit 65**, listing the colliding IDs. It never picks one. ```text Error: --by email="a@x.com" matches 2 person records (ids: 7, 12) — refusing to guess. Narrow the match value. ``` `--dry-run` previews the action without writing. Table output prints a one-line summary (`update person #7 (2 fields)`); `--output json` emits the full action result (`action`, `id`, `changed`). ### Upserting a whole CSV [Section titled “Upserting a whole CSV”](#upserting-a-whole-csv) `person import` and `org import` gain `--upsert --match-on `: each row is matched on its value in the `--match-on` column, then created or PATCHed like the single-record upsert. ```bash pdcli person import contacts.csv --upsert --match-on email --dry-run # preview the counts pdcli person import contacts.csv --upsert --match-on email # apply ``` `--match-on` is required with `--upsert` and must name a column present in the CSV (else exit 64). Rows are paced like the other bulk flows; per-row failures — an ambiguous match or an empty match cell — are collected without aborting the batch and summarized by action: ```text 18 created, 5 updated, 1 unchanged ✘ email="dup@x.com": --by email="dup@x.com" matches 2 person records (ids: 7, 12) — refusing to guess. Narrow the match value. Error: 1 of 24 rows failed ``` When every failed row is a data-validation error (ambiguous match, empty match value) the command exits **65**; a mix that includes a transport/API error exits **1**. Caution Matching relies on Pipedrive's **search index, which is eventually consistent** — a just-created record is not searchable for a short window. So upserting the *same* match value twice in quick succession (the same key on two CSV rows, or an upsert moments after a create) can still create a duplicate, because the second lookup runs before the first write is indexed. For repeatable sync, key on a stable external ID and let the index settle between runs; a periodic `backup diff` or a search will surface any duplicates that slip through. # Profiles & configuration > Manage multiple accounts with profiles, set per-profile config, and understand precedence. pdcli stores settings per **profile**. Each profile has its own company domain, auth mode, and stored credentials, so you can keep several Pipedrive accounts side by side. The active profile defaults to `default`. ## Profiles [Section titled “Profiles”](#profiles) Create a profile by logging in with `--profile`; the name is created on first use: ```bash pdcli auth login --profile work pdcli auth login --profile personal ``` List, switch, and inspect the active profile: ```bash pdcli profile list ``` ```text * default (authenticated) work (authenticated) personal (authenticated) ``` The `*` marks the active profile. ```bash pdcli profile use work # switch the active profile pdcli profile current # print the active profile name ``` Run a single command against another profile without switching — the global `--profile` flag works on every command: ```bash pdcli deal list --profile work ``` ## Per-profile config [Section titled “Per-profile config”](#per-profile-config) `config get/set/list` operate on the active profile (or the one named with `--profile`): ```bash pdcli config set company_domain acme pdcli config set default_output json pdcli config get company_domain pdcli config list pdcli config unset default_output # remove a key from the active profile ``` ```text company_domain=acme auth_mode=token default_output=json ``` `config set default_output` validates the value — it must be `table`, `json`, `yaml`, or `csv`, otherwise the command exits 64 and lists the accepted formats. Common keys: | Key | Set by | Purpose | | ---------------- | ------------ | -------------------------------------------------------- | | `company_domain` | `auth login` | The `acme` in `acme.pipedrive.com`; builds the API host. | | `auth_mode` | `auth login` | `token` or `oauth`. | | `default_output` | you | Default output format when no `--output` flag is given. | Credentials (the token, or the OAuth bundle) are **never** in config — they live only in the OS keychain. ## Aliases [Section titled “Aliases”](#aliases) Save a frequently typed command as a short alias, then invoke it by name: ```bash pdcli alias set wd "deal list --status won" pdcli alias list pdcli alias unset wd ``` Aliases are stored **globally**, not per profile — they're shared across every profile. A name that would shadow a real pdcli command is refused (exit 64), so an alias can never mask a built-in. Aliases execute with your full credentials and can wrap destructive commands (`api` DELETE, `merge`) — review aliases in shared configs before running them. Alias writes are not safe under concurrent pdcli invocations sharing one config (last write wins). ## Precedence [Section titled “Precedence”](#precedence) For any setting, the most specific source wins: ```text command-line flag > environment variable > profile config ``` So `PDCLI_COMPANY_DOMAIN=acme pdcli deal list` overrides the profile's `company_domain`, and `PDCLI_API_TOKEN` overrides the keychain token for that run. (The `--company` and `--api-token` flags exist only on `pdcli auth login`, where they take precedence over the environment.) ## Environment variables [Section titled “Environment variables”](#environment-variables) | Variable | Effect | | ---------------------- | --------------------------------------------------------------------- | | `PDCLI_COMPANY_DOMAIN` | Company subdomain (`acme`). Overrides profile config. | | `PDCLI_API_TOKEN` | Personal API token. Used instead of the keychain; no keychain needed. | | `PDCLI_PROFILE` | Active profile name (same as `--profile`). | | `PDCLI_CLIENT_ID` | OAuth app client ID, read by `auth login --oauth`. | | `PDCLI_CLIENT_SECRET` | OAuth app client secret, read by `auth login --oauth`. | | `NO_COLOR` | Any value disables colored output (same as `--no-color`). | Together, `PDCLI_COMPANY_DOMAIN` + `PDCLI_API_TOKEN` are all you need for a fully non-interactive, keychain-free run — ideal for CI and agents: ```bash PDCLI_COMPANY_DOMAIN=acme PDCLI_API_TOKEN=$TOKEN pdcli deal list --output json ``` ## See also [Section titled “See also”](#see-also) * [Authentication](/pdcli/guides/authentication/) — token vs OAuth, switching modes. * [Output & filtering](/pdcli/automation/output/) — `default_output` and `--output`. # Custom fields > Discover, read, and write Pipedrive custom fields by human name — pdcli resolves the per-account hash keys and option labels for you. In Pipedrive, every custom field has a **40-character hexadecimal hash key** instead of a readable name, and those keys are **unique to your account**. A field you call "Deal Size" might be `dcf558aac1ae4e8c4f849ba5e668430d8df9be12` in one company and a completely different hash in another. Dropdown options are stored as numeric IDs, not their labels. pdcli hides all of that. You discover fields, read them, and write them by their human names — the CLI resolves names to keys and labels to option IDs on every call. ## Discover fields [Section titled “Discover fields”](#discover-fields) `field list ` shows every field for an entity, custom fields included, with their hash keys, types, and option labels. Entities: `deal`, `person`, `org`, `product`, `activity`, `lead`, and `note`. (`field get` reads the same set; a lead inherits its deal fields, and `note` exposes the note schema.) ```bash pdcli field list deal ``` ```text ┌──────────────────────────────────────────┬───────────┬──────────┬──────────────────────┐ │ Key │ Name │ Type │ Options │ ├──────────────────────────────────────────┼───────────┼──────────┼──────────────────────┤ │ title │ Title │ varchar │ │ │ value │ Value │ monetary │ │ │ dcf558aac1ae4e8c4f849ba5e668430d8df9be12 │ Deal Size │ enum │ Small, Medium, Large │ │ a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2 │ Score │ double │ │ └──────────────────────────────────────────┴───────────┴──────────┴──────────────────────┘ ``` `field get ` shows one field by either its human name or its hash key. For enum/set fields it lists each option's ID next to its label: ```bash pdcli field get deal "Deal Size" pdcli field get deal dcf558aac1ae4e8c4f849ba5e668430d8df9be12 ``` ```text ┌──────────────────────────────────────────┬───────────┬──────┬──────────────────────────┐ │ Key │ Name │ Type │ Options │ ├──────────────────────────────────────────┼───────────┼──────┼──────────────────────────┤ │ dcf558aac1ae4e8c4f849ba5e668430d8df9be12 │ Deal Size │ enum │ 12=Small,13=Medium,14=Large │ └──────────────────────────────────────────┴───────────┴──────┴──────────────────────────┘ ``` ## Managing fields [Section titled “Managing fields”](#managing-fields) Beyond discovery, pdcli manages the field schema itself — create a field, rename it, add or drop dropdown options, or delete it. These run on the v2 fields endpoints and cover the writable core entities: `deal`, `person`, `org`, and `product`. (`activity`, `lead`, and `note` fields are read-only — they show up in `field list`/`field get` but can't be created or altered here.) Every schema change **invalidates pdcli's in-process field cache automatically**, so the name⇄key and label⇄ID resolution on subsequent writes in the same run picks up the new shape without a manual refresh. ### Create a field [Section titled “Create a field”](#create-a-field) `--name` and `--type` are required. For an `enum` or `set` (multi-select) field, pass the dropdown labels with `--options` — it's required for those types and ignored for the rest: ```bash pdcli field create deal --name "Budget" --type double pdcli field create person --name "Tier" --type enum --options "Gold,Silver,Bronze" ``` The created field is returned with its freshly minted hash `Key`, so you can pipe it straight into a write: ```text ┌──────────────────────────────────────────┬────────┬──────┬─────────────────────────┐ │ Key │ Name │ Type │ Options │ ├──────────────────────────────────────────┼────────┼──────┼─────────────────────────┤ │ a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2 │ Tier │ enum │ 1=Gold,2=Silver,3=Bronze │ └──────────────────────────────────────────┴────────┴──────┴─────────────────────────┘ ``` ### Rename a field [Section titled “Rename a field”](#rename-a-field) `field update ` renames a field via `--name`. The field is addressed by its hash key (the `field_code` from `field list`); the `field_code` and `field_type` themselves can't be changed: ```bash pdcli field update deal dcf558aac1ae4e8c4f849ba5e668430d8df9be12 --name "Deal Value" ``` ### Manage dropdown options [Section titled “Manage dropdown options”](#manage-dropdown-options) For `enum`/`set` fields, add and remove options without touching the rest of the schema: ```bash pdcli field option add deal dcf558aac1ae4e8c4f849ba5e668430d8df9be12 --label "Critical" pdcli field option remove deal dcf558aac1ae4e8c4f849ba5e668430d8df9be12 --option 4 ``` `add` takes the new label; `remove` takes the numeric **option ID** (run `field get` to see each option's ID next to its label). Removing an option is destructive — records that held that value lose it — so it confirms first; `-y`/`--yes` skips the prompt for scripts. ### Delete a field [Section titled “Delete a field”](#delete-a-field) ```bash pdcli field delete deal dcf558aac1ae4e8c4f849ba5e668430d8df9be12 pdcli field delete deal dcf558aac1ae4e8c4f849ba5e668430d8df9be12 --yes ``` Caution Deleting a field **discards the data every record stored in it** — there's no undo. The command confirms first and the prompt **defaults to No**; pass `-y`/`--yes` only when you're sure (e.g. in a script). ## Reading: names in tables, raw keys in JSON [Section titled “Reading: names in tables, raw keys in JSON”](#reading-names-in-tables-raw-keys-in-json) In table output, custom-field values are shown under their human names, and option IDs are swapped back to their labels. JSON output stays **raw** — hash keys and numeric option IDs intact — so scripts have something stable to parse. ```bash pdcli deal get 42 # table: shows "Deal Size: Large" pdcli deal get 42 --output json # JSON: custom_fields.{hash}: 14 ``` v2 nests custom-field values under a `custom_fields` object on the record. ### Resolve in JSON too: `--resolve-fields` [Section titled “Resolve in JSON too: --resolve-fields”](#resolve-in-json-too---resolve-fields) When you want the readable names in machine output as well, opt in with `--resolve-fields`. It resolves hash keys to names and option IDs to labels in `json`, `yaml`, and `csv` output — matching what the table always shows: ```bash pdcli deal get 1 --output json --resolve-fields ``` ```text { "id": 1, "title": "Acme renewal", "custom_fields": { "Deal Size": "Large", "Score": 4.5 } } ``` This is **opt-in**: it applies to the `get` commands and the core `list` commands (`deal`, `person`, `org`, `activity`, `product` — one field-definitions fetch covers the whole list). On lists, resolution affects json/yaml/csv only (list tables don't render custom-field columns); on `get`, the table view always resolves. If two custom fields share the same name, the second is disambiguated with a short key fragment (e.g. `Region (a1b2c3d4)`) so neither value is clobbered. ## Writing: `--field "Name=Value"` [Section titled “Writing: --field "Name=Value"”](#writing---field-namevalue) On every write command that accepts custom fields (`deal`, `person`, `org`, `product`, `activity` — `create`, `update`, and `deal bulk-update`), pass `--field "Name=Value"`. The flag is repeatable, one field per flag: ```bash pdcli deal create --title "Acme renewal" \ --field "Deal Size=Large" \ --field "Score=4.5" ``` What resolution does for you: * **Name to key** — `Deal Size` becomes its 40-char hash. You can also pass the hash key or `field_code` directly if you prefer. * **Label to option ID** — for `enum` fields, `Large` becomes the numeric option ID. * **Set (multi-option) fields** — give comma-separated labels, each resolved to its ID: `--field "Tags=VIP,Renewal"`. * **Numeric coercion** — `double`, `monetary`, and `int` field values are converted to numbers, so `Score=4.5` is sent as `4.5`, not the string `"4.5"`. * **Custom fields nest** under `custom_fields`; standard fields go at the top level. ## When a label is wrong [Section titled “When a label is wrong”](#when-a-label-is-wrong) If you give an option label that doesn't exist, pdcli stops before writing and lists the valid options (exit code 65): ```bash pdcli deal create --title "Sized" --field "Deal Size=Huge" ``` ```text Error: Unknown option "Huge" for field "Deal Size". Valid: Small, Medium, Large ``` An unknown field name also fails fast, pointing you at discovery (exit code 65): ```text Error: Unknown field "Dela Size". Run: pdcli field list ``` ## Same resolution everywhere [Section titled “Same resolution everywhere”](#same-resolution-everywhere) The exact same name-and-label resolution runs on single writes (`create`/`update`), on [`deal bulk-update`](/pdcli/guides/bulk/), and on [CSV imports](/pdcli/guides/bulk/) — where each CSV header is treated as a field name. So a `Deal Size` column in your spreadsheet resolves the same way `--field "Deal Size=Large"` does on the command line. ## Raw escape hatch [Section titled “Raw escape hatch”](#raw-escape-hatch) If you'd rather supply the raw v2 shape yourself, `--body` takes JSON and typed flags win over it. You'd use the hash keys directly: ```bash pdcli deal create --title "Raw" \ --body '{"custom_fields":{"dcf558aac1ae4e8c4f849ba5e668430d8df9be12":14}}' ``` # Deal products (line items) > Attach products to a deal as line items — add, list, update, and remove — with server-computed line sums. A deal's **line items** are the products attached to it, each with its own price, quantity, and discount. pdcli manages them under `deal product`, a v2 command group that maps onto `/api/v2/deals/{id}/products`. The deal's product catalog entry lives in `product`; the line item that ties a product to a deal is the *attachment* you operate on here. ## List the line items [Section titled “List the line items”](#list-the-line-items) ```bash pdcli deal product list 42 ``` ```text ┌────┬─────────┬────────────┬────────────┬─────┬──────────┬──────┐ │ ID │ Product │ Name │ Item price │ Qty │ Discount │ Sum │ ├────┼─────────┼────────────┼────────────┼─────┼──────────┼──────┤ │ 3 │ 10 │ Consulting │ 150 │ 4 │ 0 │ 600 │ │ 7 │ 12 │ Onboarding │ 90 │ 1 │ 10 │ 80 │ └────┴─────────┴────────────┴────────────┴─────┴──────────┴──────┘ ``` The `ID` column is the **attachment ID** — the handle you pass to `--attachment` when updating or removing a line, distinct from the catalog `Product` ID. Sort with `--sort-by` (`id`, `add_time`, `update_time`, or `order_nr`) and `--sort-direction` (`asc`/`desc`): ```bash pdcli deal product list 42 --sort-by add_time --sort-direction desc ``` ## Add a line item [Section titled “Add a line item”](#add-a-line-item) `--product` (catalog ID) and `--price` (per-unit item price) are required; `--quantity` defaults to `1`. Discounts, tax, and a comment are optional: ```bash pdcli deal product add 42 --product 10 --price 90 pdcli deal product add 42 --product 10 --price 90 --quantity 3 pdcli deal product add 42 --product 10 --price 90 --discount 10 --discount-type percentage ``` `--discount-type` is `percentage` or `amount`. You don't compute the line total yourself — Pipedrive derives **`sum`** server-side from `item_price × quantity` less the discount, and returns it on the created record. That's why the `Sum` column above (e.g. `150 × 4 = 600`) isn't something pdcli adds up locally. ## Update a line item [Section titled “Update a line item”](#update-a-line-item) `update` is a v2 `PATCH` — only the fields you pass change; everything else is left alone. Identify the line with `--attachment` (the `ID` from `deal product list`): ```bash pdcli deal product update 42 --attachment 3 --quantity 5 pdcli deal product update 42 --attachment 3 --price 120 pdcli deal product update 42 --attachment 3 --discount 15 --discount-type amount ``` Pass at least one field flag, or it's exit 64 (`Nothing to update`). Any of `--product`, `--price`, `--quantity`, `--discount`, `--discount-type`, `--tax`, and `--comments` may be combined; the server recomputes `sum` from the new values. Note There is **no per-line duration or billing-cycle flag**. Pipedrive replaced the old `duration`/`duration_unit` line-item fields with a product-level `billing_frequency` in 2024, so recurring terms are configured on the product (`product`), not on each deal attachment. ## Remove a line item [Section titled “Remove a line item”](#remove-a-line-item) ```bash pdcli deal product remove 42 --attachment 3 pdcli deal product remove 42 --attachment 3 --yes ``` Like every destructive command, it confirms first; `-y`/`--yes` skips the prompt for scripts. ## Worked example [Section titled “Worked example”](#worked-example) Find a product in the catalog, attach it to a deal, and confirm the line: ```bash pdcli product list --jq '.[] | {id, name}' # find the catalog product ID pdcli deal product add 42 --product 10 --price 150 --quantity 4 pdcli deal product list 42 # the new line, with its server-computed Sum ``` # Relations: participants, followers, org hierarchy > Manage the people and links around a record — deal participants (the buying committee), followers on deals/persons/orgs, and parent/related links between organizations. Records in Pipedrive don't stand alone. A deal has a **buying committee** of extra people, a deal/person/org can have **followers** who get notified of changes, and organizations link into a **hierarchy** of parents and related companies. pdcli manages each of these as a small command group that maps onto the matching Pipedrive endpoint. ## Deal participants (the buying committee) [Section titled “Deal participants (the buying committee)”](#deal-participants-the-buying-committee) A deal's *primary* contact is its `person`; **participants** are the other people involved — the champion, the economic buyer, the blockers. Manage them under `deal participant`. ```bash pdcli deal participant list 42 ``` ```text ┌────┬────────┬───────────┬──────────────────────┐ │ ID │ Person │ Name │ Added │ ├────┼────────┼───────────┼──────────────────────┤ │ 3 │ 10 │ Jane Doe │ 2026-05-01 09:12:00 │ │ 7 │ 21 │ John Roe │ 2026-05-03 14:40:00 │ └────┴────────┴───────────┴──────────────────────┘ ``` Add a person by their person ID, and remove a participant by the **deal-participant `ID`** from the list (the left column, distinct from the person ID): ```bash pdcli deal participant add 42 --person 10 pdcli deal participant remove 42 --participant 3 pdcli deal participant remove 42 --participant 3 --yes ``` `remove` confirms first like every destructive command; `-y`/`--yes` skips the prompt. ## Followers [Section titled “Followers”](#followers) A **follower** is a *user* (a teammate, by user ID) who gets notified about a record's activity. Deals, persons, and organizations each have an identical `follower` group: ```bash pdcli deal follower list 42 pdcli person follower list 17 pdcli org follower list 7 ``` ```text ┌──────┬──────────────────────┐ │ User │ Added │ ├──────┼──────────────────────┤ │ 5 │ 2026-05-01 09:12:00 │ └──────┴──────────────────────┘ ``` Add and remove a follower by **user ID** (resolve names with `user find`/`user list`). The same shape works for `deal`, `person`, and `org`: ```bash pdcli deal follower add 42 --user 5 pdcli deal follower remove 42 --user 5 pdcli deal follower remove 42 --user 5 --yes pdcli person follower add 17 --user 5 pdcli org follower add 7 --user 5 ``` Unlike participants, followers are keyed directly by the user ID on both add and remove — there's no separate follower handle to track. ## Organization hierarchy [Section titled “Organization hierarchy”](#organization-hierarchy) Organizations link to each other two ways: a **parent** relationship (a corporate parent/subsidiary tree) and a looser **related** link between peers. Manage both under `org relationship`. List every relationship an org takes part in — on either side — with `--org`: ```bash pdcli org relationship list --org 1481 ``` ```text ┌────┬────────┬───────────┬───────────┬─────────┐ │ ID │ Type │ Owner Org │ Linked Org │ Related │ ├────┼────────┼───────────┼───────────┼─────────┤ │ 7 │ parent │ 1481 │ 1480 │ Globex │ └────┴────────┴───────────┴───────────┴─────────┘ ``` Create a link with `--type parent|related`, naming the two orgs with `--owner` and `--linked`. **For a `parent` relationship the `--owner` is the parent and `--linked` is the daughter**; for `related` the two are interchangeable peers: ```bash pdcli org relationship add --type parent --owner 1481 --linked 1480 pdcli org relationship add --type related --owner 1 --linked 2 ``` Remove a link by its **relationship `ID`** (the left column from `list`, not an org ID): ```bash pdcli org relationship remove 7 pdcli org relationship remove 7 --yes ``` It confirms first; `-y`/`--yes` skips the prompt for scripts. # Command reference > Every pdcli command, flag, and example — generated from the CLI manifest. All 145 commands in `pdcli` v0.18.0. Every command also accepts the [global flags](/pdcli/reference/config/) `--output`, `--jq`, `--fields`, `--profile`, `--limit`, `--no-color`, `--verbose`, `--no-retry`, and `--timeout`. Run `pdcli --help` for the live version. ## Top-level commands [Section titled “Top-level commands”](#top-level-commands) ### `pdcli api` [Section titled “pdcli api”](#pdcli-api) Make a raw API request (host-locked to your Pipedrive company domain) ```text pdcli api [flags] ``` | Flag | Description | | ----------------- | ------------------------------------------------ | | `--body` \ | Request body (JSON string, @file, or pipe stdin) | ```bash pdcli api GET /api/v2/deals pdcli api GET /api/v1/currencies pdcli api POST /api/v2/deals --body '{"title":"New deal"}' pdcli api DELETE /api/v1/webhooks/1 ``` ### `pdcli audit` [Section titled “pdcli audit”](#pdcli-audit) Data-quality audit: stale deals, missing fields, duplicates, overdue pileups ```text pdcli audit [flags] ``` | Flag | Description | | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `--checks` \ | Comma-separated subset of checks (stale-deals, no-next-activity, past-close-date, missing-fields, ancient-deals, missing-close-time, duplicate-persons, uncontactable-persons, duplicate-orgs, overdue-activities, currency-missing) | | `--strict` | Exit 1 when any must-severity check has findings | ```bash pdcli audit pdcli audit --checks stale-deals,duplicate-persons --verbose pdcli audit --strict # exit 1 on must-severity findings (CI) ``` ### `pdcli backup` [Section titled “pdcli backup”](#pdcli-backup) Export the whole account to a JSON tree (resumable, one file per resource) ```text pdcli backup [flags] ``` | Flag | Description | | ---------------- | -------------------------------------------------- | | `--dir` \ | Target directory for the export | | `--resume` | Skip resources already completed in a previous run | ```bash pdcli backup pdcli backup --dir ./my-backup pdcli backup --dir ./my-backup --resume ``` ### `pdcli changes` [Section titled “pdcli changes”](#pdcli-changes) Incremental change feed across deals/persons/orgs/activities/products. Self-advancing watermark: each run resumes where the last left off and advances it past the newest change only after a successful emit, so a failed run replays rather than skips (use --peek to read without advancing). ```text pdcli changes [flags] ``` | Flag | Description | | ------------------ | ---------------------------------------------------------------------------------- | | `--since` \ | Start point: RFC3339 timestamp or Nd/Nm. Omit to resume from the stored watermark. | | `--peek` | Read without advancing the stored watermark | ```bash pdcli changes --since 7d pdcli changes pdcli changes --peek --output json ``` ### `pdcli digest` [Section titled “pdcli digest”](#pdcli-digest) Monday packet: one pipeline-scoped fetch fanned into velocity, health, coverage, funnel, forecast and hygiene. --deep adds changelog-mined aging/slippage/stage-skips; --format md|html (+ --out) writes a shareable artifact for cron → Slack/email. ```text pdcli digest [flags] ``` | Flag | Description | | ----------------------------- | --------------------------------------------------------------------------------------------------- | | `--pipeline` \ | Pipeline ID (required when the account has several) | | `--period` \ | Trailing window for closed deals / the goal (Nd or Nm) | | `--target` \ | Manual revenue quota override (skips the Goals API) | | `--commit-threshold` \ | Min effective win-probability (%) counted toward commit | | `--deep` | Mine each deal’s changelog to add aging/slippage/stage-skips (one request per deal; warns over 100) | | `--format` \ | Render the packet as a shareable artifact | | `--out` \ | Write the --format artifact to this file instead of stdout | ```bash pdcli digest pdcli digest --pipeline 1 --output json pdcli digest --deep pdcli digest --format md --out monday.md ``` ### `pdcli doctor` [Section titled “pdcli doctor”](#pdcli-doctor) Run diagnostic checks on the CLI environment ```text pdcli doctor [flags] ``` ```bash pdcli doctor ``` ### `pdcli funnel` [Section titled “pdcli funnel”](#pdcli-funnel) Stage-to-stage conversion approximated from closed deals (final stage reached) ```text pdcli funnel [flags] ``` | Flag | Description | | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `--period` \ | Trailing window for closed deals (Nd or Nm) | | `--pipeline` \ | Pipeline ID (required when the account has several) | | `--exact` | Mine real stage transitions from each deal’s changelog instead of approximating from the final stage (one request per deal). --period scopes only closed (won/lost) deals; open deals are always included. | ```bash pdcli funnel pdcli funnel --pipeline 1 --period 180d ``` ### `pdcli search` [Section titled “pdcli search”](#pdcli-search) Search across deals, persons, organizations, products, leads, files, and projects ```text pdcli search [flags] ``` | Flag | Description | | ----------------------------- | ------------------------------------------------------------------------------------------------ | | `--item-types` \ | Comma-separated item types (deal,person,organization,product,lead,file,mail\_attachment,project) | | `--exact` | Exact match (allows 1-character terms) | | `--status` \ | Filter by deal status (only with --item-types deal) | | `--person` \ | Filter by person ID (only with --item-types deal) | | `--org` \ | Filter by organization ID (only with --item-types deal) | ```bash pdcli search "acme" pdcli search "acme" --item-types deal,person --output json pdcli search "acme" --item-types deal --status open ``` ### `pdcli version` [Section titled “pdcli version”](#pdcli-version) Show CLI version and environment info ```text pdcli version [flags] ``` ```bash pdcli version ``` ### `pdcli watch` [Section titled “pdcli watch”](#pdcli-watch) Anomaly poller: run the hygiene checks and emit only findings that are NEW since the last run, advancing a per-profile state. Exits 8 when new findings arm the gate (default: must-severity) so cron can branch — `pdcli watch \|\| notify`. --peek reads without advancing state. ```text pdcli watch [flags] ``` | Flag | Description | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `--checks` \ | Comma-separated subset of checks (stale-deals, no-next-activity, past-close-date, missing-fields, ancient-deals, missing-close-time, duplicate-persons, uncontactable-persons, duplicate-orgs, overdue-activities, currency-missing) | | `--severity` \ | Which severities arm the exit-8 gate | | `--peek` | Emit and gate without advancing the stored state | ```bash pdcli watch pdcli watch --checks stale-deals,past-close-date pdcli watch --severity all --output json pdcli watch --peek ``` ## pdcli activity [Section titled “pdcli activity”](#pdcli-activity) ### `pdcli activity create` [Section titled “pdcli activity create”](#pdcli-activity-create) Create an activity ```text pdcli activity create [flags] ``` | Flag | Description | | --------------------- | -------------------------------------------------- | | `--subject` \ | Activity subject | | `--type` \ | Activity type | | `--due-date` \ | Due date (YYYY-MM-DD) | | `--due-time` \ | Due time (HH:MM) | | `--duration` \ | Duration (HH:MM) | | `--deal` \ | Linked deal ID | | `--person` \ | Linked person ID | | `--org` \ | Linked organization ID | | `--owner` \ | Owner (user) ID | | `--note` \ | Activity note | | `--done` | Mark the activity as done | | `--field` \ | Custom/standard field as "Name=Value" (repeatable) | | `--body` \ | Raw JSON body to merge (flags win) | ```bash pdcli activity create --subject "Demo call" --type call --due-date 2026-06-10 pdcli activity create --subject "Follow up" --field "Outcome=Positive" pdcli activity create --subject "Raw" --body '{"priority":5}' ``` ### `pdcli activity delete` [Section titled “pdcli activity delete”](#pdcli-activity-delete) Delete an activity ```text pdcli activity delete [flags] ``` | Flag | Description | | ----------- | ---------------------------- | | `-y, --yes` | Skip the confirmation prompt | ```bash pdcli activity delete 9 pdcli activity delete 9 --yes ``` ### `pdcli activity get` [Section titled “pdcli activity get”](#pdcli-activity-get) Get an activity by ID ```text pdcli activity get [flags] ``` ```bash pdcli activity get 9 pdcli activity get 9 --output json ``` ### `pdcli activity list` [Section titled “pdcli activity list”](#pdcli-activity-list) List activities ```text pdcli activity list [flags] ``` | Flag | Description | | ----------------------------------------------------- | --------------------------------------------------------------------- | | `--owner` \ | Filter by owner (user) ID | | `--deal` \ | Filter by deal ID | | `--person` \ | Filter by person ID | | `--org` \ | Filter by organization ID | | `--type` \ | Filter by activity type key (applied client-side) | | `--done` | Only completed activities | | `--todo` | Only open (not done) activities | | `--filter` \ | Filter by saved filter ID | | `--ids` \ | Comma-separated IDs to fetch (max 100) | | `--sort-by` \ | Sort field | | `--sort-direction` \ | Sort direction | | `--updated-since` \ | Only items updated at/after this RFC3339 time (no fractional seconds) | | `--updated-until` \ | Only items updated before this RFC3339 time (no fractional seconds) | ```bash pdcli activity list pdcli activity list --todo --deal 42 pdcli activity list --type call --output json ``` ### `pdcli activity type list` [Section titled “pdcli activity type list”](#pdcli-activity-type-list) List activity types. The Key (key\_string) is what `activity --type` takes. ```text pdcli activity type list [flags] ``` ```bash pdcli activity type list pdcli activity type list --output json ``` ### `pdcli activity update` [Section titled “pdcli activity update”](#pdcli-activity-update) Update an activity (v2 PATCH — only provided fields change) ```text pdcli activity update [flags] ``` | Flag | Description | | --------------------- | -------------------------------------------------- | | `--subject` \ | Activity subject | | `--type` \ | Activity type | | `--due-date` \ | Due date (YYYY-MM-DD) | | `--due-time` \ | Due time (HH:MM) | | `--duration` \ | Duration (HH:MM) | | `--deal` \ | Linked deal ID | | `--person` \ | Linked person ID | | `--org` \ | Linked organization ID | | `--owner` \ | Owner (user) ID | | `--note` \ | Activity note | | `--done` | Mark the activity as done | | `--undone` | Mark the activity as not done | | `--field` \ | Custom/standard field as "Name=Value" (repeatable) | | `--body` \ | Raw JSON body to merge (flags win) | ```bash pdcli activity update 9 --subject "Renamed" pdcli activity update 9 --done pdcli activity update 9 --field "Outcome=Positive" ``` ## pdcli alias [Section titled “pdcli alias”](#pdcli-alias) ### `pdcli alias list` [Section titled “pdcli alias list”](#pdcli-alias-list) List all configured aliases ```text pdcli alias list [flags] ``` ```bash pdcli alias list ``` ### `pdcli alias set` [Section titled “pdcli alias set”](#pdcli-alias-set) Create or update an alias ```text pdcli alias set [flags] ``` ```bash pdcli alias set wd "deal list --status won" pdcli alias set open "deal list --status open --limit 50" ``` ### `pdcli alias unset` [Section titled “pdcli alias unset”](#pdcli-alias-unset) Remove an alias ```text pdcli alias unset [flags] ``` ```bash pdcli alias unset wd ``` ## pdcli audit [Section titled “pdcli audit”](#pdcli-audit-1) ### `pdcli audit stage-skips` [Section titled “pdcli audit stage-skips”](#pdcli-audit-stage-skips) Stage-skip & sandbagging audit: deals that jumped gates or were pulled backward, mined from each deal’s changelog (one request per deal) ```text pdcli audit stage-skips [flags] ``` | Flag | Description | | --------------------- | --------------------------------------------------- | | `--pipeline` \ | Pipeline ID (required when the account has several) | ```bash pdcli audit stage-skips pdcli audit stage-skips --pipeline 1 --output json ``` ## pdcli auth [Section titled “pdcli auth”](#pdcli-auth) ### `pdcli auth login` [Section titled “pdcli auth login”](#pdcli-auth-login) Authenticate with Pipedrive (personal API token, or OAuth with --oauth) ```text pdcli auth login [flags] ``` | Flag | Description | | -------------------------- | --------------------------------------------------------------------------------------------------------------------- | | `--company` \ | Company domain ("acme" from acme.pipedrive.com — full URL accepted) | | `--api-token` \ | Personal API token (app.pipedrive.com/settings/api). Prefer the prompt or env so the token stays out of shell history | | `--oauth` | Use OAuth 2.0 via your own Developer Hub app (browser flow) | | `--client-id` \ | OAuth app client ID (--oauth; env PDCLI\_CLIENT\_ID) | | `--client-secret` \ | OAuth app client secret (--oauth; env PDCLI\_CLIENT\_SECRET) | | `--port` \ | OAuth callback port — must match the app's registered callback URL (--oauth) | ```bash pdcli auth login pdcli auth login --company acme --api-token pdcli auth login --oauth pdcli auth login --oauth --client-id --client-secret ``` ### `pdcli auth logout` [Section titled “pdcli auth logout”](#pdcli-auth-logout) Log out and remove the stored API token ```text pdcli auth logout [flags] ``` ```bash pdcli auth logout ``` ### `pdcli auth status` [Section titled “pdcli auth status”](#pdcli-auth-status) Show current authentication status ```text pdcli auth status [flags] ``` ```bash pdcli auth status ``` ## pdcli backup [Section titled “pdcli backup”](#pdcli-backup-1) ### `pdcli backup diff` [Section titled “pdcli backup diff”](#pdcli-backup-diff) Field-level diff between two backup snapshots — added/removed/modified records and per-field changes, computed locally with no API calls ```text pdcli backup diff [flags] ``` | Flag | Description | | ------- | ------------------------------------------------------------------------ | | `--raw` | Do not resolve custom-field names/option labels (show raw hash keys/ids) | ```bash pdcli backup diff ./backup-mon ./backup-tue pdcli backup diff ./old ./new --output json pdcli backup diff ./old ./new --raw ``` ## pdcli config [Section titled “pdcli config”](#pdcli-config) ### `pdcli config get` [Section titled “pdcli config get”](#pdcli-config-get) Get a config value for the active profile ```text pdcli config get [flags] ``` ```bash pdcli config get company_domain pdcli config get default_output ``` ### `pdcli config list` [Section titled “pdcli config list”](#pdcli-config-list) List all config for the active profile ```text pdcli config list [flags] ``` ```bash pdcli config list ``` ### `pdcli config set` [Section titled “pdcli config set”](#pdcli-config-set) Set a config value for the active profile ```text pdcli config set [flags] ``` ```bash pdcli config set company_domain acme pdcli config set default_output json ``` ### `pdcli config unset` [Section titled “pdcli config unset”](#pdcli-config-unset) Remove a config key from the active profile ```text pdcli config unset [flags] ``` ```bash pdcli config unset default_output ``` ## pdcli deal [Section titled “pdcli deal”](#pdcli-deal) ### `pdcli deal bulk-update` [Section titled “pdcli deal bulk-update”](#pdcli-deal-bulk-update) Update many deals at once (by --ids, a saved --filter, or ids piped on stdin) ```text pdcli deal bulk-update [flags] ``` | Flag | Description | | ----------------------------- | -------------------------------------------------- | | `--ids` \ | Comma-separated deal IDs | | `--filter` \ | Pipedrive saved filter ID to select deals | | `--stage` \ | Move to stage ID | | `--pipeline` \ | Move to pipeline ID | | `--status` \ | Set status | | `--owner` \ | Assign owner (user) ID | | `--field` \ | Custom/standard field as "Name=Value" (repeatable) | | `--body` \ | Raw JSON body to merge (flags win) | | `--dry-run` | List the targets without updating anything | | `-y, --yes` | Skip the confirmation prompt | ```bash pdcli deal bulk-update --ids 1,2,3 --stage 5 pdcli deal bulk-update --filter 9 --status won pdcli deal list --status open --jq '.[].id' | pdcli deal bulk-update --owner 42 pdcli deal bulk-update --filter 9 --stage 5 --dry-run ``` ### `pdcli deal context` [Section titled “pdcli deal context”](#pdcli-deal-context) One-call denormalized deal bundle — deal + person + org + activities + notes + products + participants, custom fields resolved to names and risk flags derived. Prompt-ready for agents (the joins v2 will not do). ```text pdcli deal context [flags] ``` | Flag | Description | | --------------------------- | --------------------------- | | `--no-activities` | Skip the activities slice | | `--no-notes` | Skip the notes slice | | `--no-products` | Skip the products slice | | `--no-participants` | Skip the participants slice | | `--activity-limit` \ | Max activities to include | | `--note-limit` \ | Max notes to include | ```bash pdcli deal context 42 pdcli deal context 42 --no-notes --no-products pdcli deal context 42 --output json ``` ### `pdcli deal convert` [Section titled “pdcli deal convert”](#pdcli-deal-convert) Convert a deal to a lead. The conversion runs as an async job; use --wait to poll until it finishes. WARNING: on success the source deal is deleted. ```text pdcli deal convert [flags] ``` | Flag | Description | | ------------------------- | -------------------------------------------- | | `--wait` | Poll the conversion status until it finishes | | `--timeout-secs` \ | Max seconds to poll when --wait is set | | `-y, --yes` | Skip the confirmation prompt | ```bash pdcli deal convert 42 pdcli deal convert 42 --yes pdcli deal convert 42 --wait ``` ### `pdcli deal create` [Section titled “pdcli deal create”](#pdcli-deal-create) Create a deal ```text pdcli deal create [flags] ``` | Flag | Description | | -------------------------------- | -------------------------------------------------- | | `--title` \ | Deal title | | `--value` \ | Deal value | | `--currency` \ | Deal currency (e.g. EUR) | | `--status` \ | Deal status | | `--stage` \ | Stage ID | | `--pipeline` \ | Pipeline ID | | `--person` \ | Linked person ID | | `--org` \ | Linked organization ID | | `--owner` \ | Owner (user) ID | | `--probability` \ | Success probability (0-100) | | `--expected-close-date` \ | Expected close date (YYYY-MM-DD) | | `--field` \ | Custom/standard field as "Name=Value" (repeatable) | | `--body` \ | Raw JSON body to merge (flags win) | ```bash pdcli deal create --title "Acme renewal" --value 5000 --currency EUR pdcli deal create --title "Sized" --field "Deal Size=Large" pdcli deal create --title "Raw" --body '{"probability":75}' ``` ### `pdcli deal delete` [Section titled “pdcli deal delete”](#pdcli-deal-delete) Delete a deal ```text pdcli deal delete [flags] ``` | Flag | Description | | ----------- | ---------------------------- | | `-y, --yes` | Skip the confirmation prompt | ```bash pdcli deal delete 42 pdcli deal delete 42 --yes ``` ### `pdcli deal follower add` [Section titled “pdcli deal follower add”](#pdcli-deal-follower-add) Add a follower (user) to a deal ```text pdcli deal follower add [flags] ``` | Flag | Description | | ----------------- | ----------- | | `--user` \ | User ID | ```bash pdcli deal follower add 42 --user 5 pdcli deal follower add 42 --user 5 --output json ``` ### `pdcli deal follower list` [Section titled “pdcli deal follower list”](#pdcli-deal-follower-list) List followers of a deal ```text pdcli deal follower list [flags] ``` ```bash pdcli deal follower list 42 pdcli deal follower list 42 --output json ``` ### `pdcli deal follower remove` [Section titled “pdcli deal follower remove”](#pdcli-deal-follower-remove) Remove a follower from a deal ```text pdcli deal follower remove [flags] ``` | Flag | Description | | ----------------- | ---------------------------- | | `--user` \ | User ID | | `-y, --yes` | Skip the confirmation prompt | ```bash pdcli deal follower remove 42 --user 5 pdcli deal follower remove 42 --user 5 --yes ``` ### `pdcli deal get` [Section titled “pdcli deal get”](#pdcli-deal-get) Get a deal by ID ```text pdcli deal get [flags] ``` ```bash pdcli deal get 42 pdcli deal get 42 --output json ``` ### `pdcli deal history` [Section titled “pdcli deal history”](#pdcli-deal-history) Field-change history for a deal, newest-first (the API’s native order) ```text pdcli deal history [flags] ``` | Flag | Description | | ------------------ | ---------------------------------------------------- | | `--field` \ | Show only changes to this field key (e.g. stage\_id) | ```bash pdcli deal history 42 pdcli deal history 42 --field stage_id pdcli deal history 42 --limit 20 --resolve-fields ``` ### `pdcli deal list` [Section titled “pdcli deal list”](#pdcli-deal-list) List deals ```text pdcli deal list [flags] ``` | Flag | Description | | ------------------------------------------ | --------------------------------------------------------------------- | | `--archived` | List archived deals instead of active ones | | `--status` \ | Filter by status | | `--stage` \ | Filter by stage ID | | `--pipeline` \ | Filter by pipeline ID | | `--owner` \ | Filter by owner (user) ID | | `--person` \ | Filter by person ID | | `--org` \ | Filter by organization ID | | `--filter` \ | Filter by saved filter ID | | `--ids` \ | Comma-separated IDs to fetch (max 100) | | `--sort-by` \ | Sort field | | `--sort-direction` \ | Sort direction | | `--updated-since` \ | Only items updated at/after this RFC3339 time (no fractional seconds) | | `--updated-until` \ | Only items updated before this RFC3339 time (no fractional seconds) | ```bash pdcli deal list pdcli deal list --status won --limit 50 pdcli deal list --stage 3 --output json ``` ### `pdcli deal participant add` [Section titled “pdcli deal participant add”](#pdcli-deal-participant-add) Add a participant (person) to a deal ```text pdcli deal participant add [flags] ``` | Flag | Description | | ------------------- | ----------- | | `--person` \ | Person ID | ```bash pdcli deal participant add 42 --person 10 pdcli deal participant add 42 --person 10 --output json ``` ### `pdcli deal participant list` [Section titled “pdcli deal participant list”](#pdcli-deal-participant-list) List participants of a deal ```text pdcli deal participant list [flags] ``` ```bash pdcli deal participant list 42 pdcli deal participant list 42 --output json ``` ### `pdcli deal participant remove` [Section titled “pdcli deal participant remove”](#pdcli-deal-participant-remove) Remove a participant from a deal ```text pdcli deal participant remove [flags] ``` | Flag | Description | | ------------------------ | ---------------------------- | | `--participant` \ | Deal-participant ID | | `-y, --yes` | Skip the confirmation prompt | ```bash pdcli deal participant remove 42 --participant 3 pdcli deal participant remove 42 --participant 3 --yes ``` ### `pdcli deal product add` [Section titled “pdcli deal product add”](#pdcli-deal-product-add) Attach a product to a deal ```text pdcli deal product add [flags] ``` | Flag | Description | | --------------------------------------- | ---------------------- | | `--product` \ | Product ID | | `--price` \ | Item price (per unit) | | `--quantity` \ | Quantity | | `--discount` \ | Discount value | | `--discount-type` \ | Discount type | | `--tax` \ | Product tax percentage | | `--comments` \ | Comments | ```bash pdcli deal product add 42 --product 10 --price 90 pdcli deal product add 42 --product 10 --price 90 --quantity 3 pdcli deal product add 42 --product 10 --price 90 --discount 10 --discount-type percentage ``` ### `pdcli deal product list` [Section titled “pdcli deal product list”](#pdcli-deal-product-list) List products attached to a deal ```text pdcli deal product list [flags] ``` | Flag | Description | | ----------------------------------------------------- | ---------------- | | `--sort-by` \ | Field to sort by | | `--sort-direction` \ | Sort direction | ```bash pdcli deal product list 42 pdcli deal product list 42 --sort-by add_time --sort-direction desc pdcli deal product list 42 --output json ``` ### `pdcli deal product remove` [Section titled “pdcli deal product remove”](#pdcli-deal-product-remove) Remove a product attached to a deal ```text pdcli deal product remove [flags] ``` | Flag | Description | | ----------------------- | ---------------------------- | | `--attachment` \ | Deal-product (attachment) ID | | `-y, --yes` | Skip the confirmation prompt | ```bash pdcli deal product remove 42 --attachment 3 pdcli deal product remove 42 --attachment 3 --yes ``` ### `pdcli deal product update` [Section titled “pdcli deal product update”](#pdcli-deal-product-update) Update a product attached to a deal (v2 PATCH — only provided fields change) ```text pdcli deal product update [flags] ``` | Flag | Description | | --------------------------------------- | ---------------------------- | | `--attachment` \ | Deal-product (attachment) ID | | `--product` \ | Product ID | | `--price` \ | Item price (per unit) | | `--quantity` \ | Quantity | | `--discount` \ | Discount value | | `--discount-type` \ | Discount type | | `--tax` \ | Product tax percentage | | `--comments` \ | Comments | ```bash pdcli deal product update 42 --attachment 3 --quantity 5 pdcli deal product update 42 --attachment 3 --price 120 pdcli deal product update 42 --attachment 3 --discount 15 --discount-type amount ``` ### `pdcli deal summary` [Section titled “pdcli deal summary”](#pdcli-deal-summary) Summary of open/won/lost deals, totalled per currency ```text pdcli deal summary [flags] ``` | Flag | Description | | ----------------------------- | ------------------------- | | `--status` \ | Filter by status | | `--pipeline` \ | Filter by pipeline ID | | `--stage` \ | Filter by stage ID | | `--filter` \ | Filter by saved filter ID | ```bash pdcli deal summary pdcli deal summary --status open --pipeline 1 pdcli deal summary --output json ``` ### `pdcli deal update` [Section titled “pdcli deal update”](#pdcli-deal-update) Update a deal (v2 PATCH — only provided fields change) ```text pdcli deal update [flags] ``` | Flag | Description | | -------------------------------- | -------------------------------------------------- | | `--title` \ | Deal title | | `--value` \ | Deal value | | `--currency` \ | Deal currency (e.g. EUR) | | `--status` \ | Deal status | | `--stage` \ | Stage ID | | `--pipeline` \ | Pipeline ID | | `--person` \ | Linked person ID | | `--org` \ | Linked organization ID | | `--owner` \ | Owner (user) ID | | `--probability` \ | Success probability (0-100) | | `--expected-close-date` \ | Expected close date (YYYY-MM-DD) | | `--field` \ | Custom/standard field as "Name=Value" (repeatable) | | `--body` \ | Raw JSON body to merge (flags win) | ```bash pdcli deal update 42 --stage 5 pdcli deal update 42 --status won pdcli deal update 42 --field "Deal Size=Large" ``` ### `pdcli deal upsert` [Section titled “pdcli deal upsert”](#pdcli-deal-upsert) Idempotent deal upsert: match by --by, then create or PATCH only the changed fields. Refuses (exit 65) if more than one record matches. ```text pdcli deal upsert [flags] ``` | Flag | Description | | ------------------ | ------------------------------------------------ | | `--by` \ | Match field: title, or a searchable custom field | | `--field` \ | Field to set as "Name=Value" (repeatable) | | `--body` \ | Raw JSON body to merge | | `--dry-run` | Preview the action without writing | ```bash pdcli deal upsert "Acme expansion" --by title --field "Stage=Won" pdcli deal upsert "D-42" --by "External ID" --body '{"value":5000}' pdcli deal upsert "Acme expansion" --by title --field "Stage=Won" --dry-run ``` ## pdcli field [Section titled “pdcli field”](#pdcli-field) ### `pdcli field create` [Section titled “pdcli field create”](#pdcli-field-create) Create a custom field on an entity ```text pdcli field create [flags] ``` | Flag | Description | | -------------------- | ------------------------------------------------------ | | `--name` \ | Field name (label) | | `--type` \ | Field type (e.g. varchar, double, monetary, enum, set) | | `--options` \ | Comma-separated option labels (required for enum/set) | ```bash pdcli field create deal --name "Budget" --type double pdcli field create person --name "Tier" --type enum --options "Gold,Silver,Bronze" ``` ### `pdcli field delete` [Section titled “pdcli field delete”](#pdcli-field-delete) Delete a custom field (data stored on records is lost) ```text pdcli field delete [flags] ``` | Flag | Description | | ----------- | ---------------------------- | | `-y, --yes` | Skip the confirmation prompt | ```bash pdcli field delete deal dcf558aac1ae4e8c4f849ba5e668430d8df9be12 pdcli field delete deal dcf558aac1ae4e8c4f849ba5e668430d8df9be12 --yes ``` ### `pdcli field get` [Section titled “pdcli field get”](#pdcli-field-get) Show one field by human name or hashed key ```text pdcli field get [flags] ``` ```bash pdcli field get deal "Deal Size" pdcli field get deal dcf558aac1ae4e8c4f849ba5e668430d8df9be12 ``` ### `pdcli field list` [Section titled “pdcli field list”](#pdcli-field-list) List fields for an entity, including custom-field hash keys ```text pdcli field list [flags] ``` ```bash pdcli field list deal pdcli field list person --output json ``` ### `pdcli field option add` [Section titled “pdcli field option add”](#pdcli-field-option-add) Add an option to an enum/set custom field ```text pdcli field option add [flags] ``` | Flag | Description | | ------------------ | ------------------------ | | `--label` \ | Label for the new option | ```bash pdcli field option add deal dcf558aac1ae4e8c4f849ba5e668430d8df9be12 --label "Critical" ``` ### `pdcli field option remove` [Section titled “pdcli field option remove”](#pdcli-field-option-remove) Remove an option from an enum/set custom field (records lose the value) ```text pdcli field option remove [flags] ``` | Flag | Description | | ------------------- | ----------------------------------- | | `--option` \ | Option ID to remove (see field get) | | `-y, --yes` | Skip the confirmation prompt | ```bash pdcli field option remove deal dcf558aac1ae4e8c4f849ba5e668430d8df9be12 --option 4 ``` ### `pdcli field update` [Section titled “pdcli field update”](#pdcli-field-update) Update a custom field (field\_code and field\_type cannot change) ```text pdcli field update [flags] ``` | Flag | Description | | ----------------- | ---------------------- | | `--name` \ | New field name (label) | ```bash pdcli field update deal dcf558aac1ae4e8c4f849ba5e668430d8df9be12 --name "New name" ``` ## pdcli file [Section titled “pdcli file”](#pdcli-file) ### `pdcli file delete` [Section titled “pdcli file delete”](#pdcli-file-delete) Delete a file ```text pdcli file delete [flags] ``` | Flag | Description | | ----------- | ---------------------------- | | `-y, --yes` | Skip the confirmation prompt | ```bash pdcli file delete 5 pdcli file delete 5 --yes ``` ### `pdcli file download` [Section titled “pdcli file download”](#pdcli-file-download) Download a file by ID ```text pdcli file download [flags] ``` | Flag | Description | | ---------------- | ------------------------------------- | | `--out` \ | Path to write to (default: file name) | ```bash pdcli file download 5 pdcli file download 5 --out ./report.pdf ``` ### `pdcli file get` [Section titled “pdcli file get”](#pdcli-file-get) Get a file by ID ```text pdcli file get [flags] ``` ```bash pdcli file get 5 pdcli file get 5 --output json ``` ### `pdcli file list` [Section titled “pdcli file list”](#pdcli-file-list) List files ```text pdcli file list [flags] ``` ```bash pdcli file list pdcli file list --limit 50 --output json ``` ### `pdcli file remote-link` [Section titled “pdcli file remote-link”](#pdcli-file-remote-link) Link an existing remote file (Google Drive) to an item ```text pdcli file remote-link [flags] ``` | Flag | Description | | ---------------------------------- | ------------------------------------------------- | | `--deal` \ | Link to a deal ID | | `--org` \ | Link to an organization ID | | `--person` \ | Link to a person ID | | `--remote-id` \ | ID of the remote file (e.g. Google Drive file ID) | | `--remote-location` \ | Remote storage location | ```bash pdcli file remote-link --deal 42 --remote-id 1AbC pdcli file remote-link --person 9 --remote-id 1AbC --output json ``` ### `pdcli file update` [Section titled “pdcli file update”](#pdcli-file-update) Update a file name and/or description ```text pdcli file update [flags] ``` | Flag | Description | | ------------------------ | ---------------------------- | | `--name` \ | The visible name of the file | | `--description` \ | The description of the file | ```bash pdcli file update 5 --name report.pdf pdcli file update 5 --description "Signed contract" ``` ### `pdcli file upload` [Section titled “pdcli file upload”](#pdcli-file-upload) Upload a file ```text pdcli file upload [flags] ``` | Flag | Description | | ------------------- | --------------------------------- | | `--deal` \ | Associate with a deal ID | | `--person` \ | Associate with a person ID | | `--org` \ | Associate with an organization ID | ```bash pdcli file upload ./report.pdf pdcli file upload ./report.pdf --deal 42 ``` ## pdcli filter [Section titled “pdcli filter”](#pdcli-filter) ### `pdcli filter delete` [Section titled “pdcli filter delete”](#pdcli-filter-delete) Delete a filter ```text pdcli filter delete [flags] ``` | Flag | Description | | ----------- | ---------------------------- | | `-y, --yes` | Skip the confirmation prompt | ```bash pdcli filter delete 5 pdcli filter delete 5 --yes ``` ### `pdcli filter get` [Section titled “pdcli filter get”](#pdcli-filter-get) Get a filter by ID ```text pdcli filter get [flags] ``` ```bash pdcli filter get 5 pdcli filter get 5 --output json ``` ### `pdcli filter list` [Section titled “pdcli filter list”](#pdcli-filter-list) List filters ```text pdcli filter list [flags] ``` | Flag | Description | | ------------------------------------------------------------------- | -------------- | | `--type` \ | Filter by type | ```bash pdcli filter list pdcli filter list --type deals --output json ``` ## pdcli goal [Section titled “pdcli goal”](#pdcli-goal) ### `pdcli goal list` [Section titled “pdcli goal list”](#pdcli-goal-list) List goals ```text pdcli goal list [flags] ``` | Flag | Description | | --------------------- | ---------------------------- | | `--assignee` \ | Filter by assignee (user) ID | | `--type` \ | Filter by goal type name | ```bash pdcli goal list pdcli goal list --assignee 7 --type deals_won --output json ``` ## pdcli lead [Section titled “pdcli lead”](#pdcli-lead) ### `pdcli lead convert` [Section titled “pdcli lead convert”](#pdcli-lead-convert) Convert a lead to a deal. The conversion runs as an async job; use --wait to poll until it finishes. On success the lead is deleted. ```text pdcli lead convert [flags] ``` | Flag | Description | | ------------------------- | ---------------------------------------------------------- | | `--stage` \ | Stage ID for the new deal (a pipeline is inferred from it) | | `--pipeline` \ | Pipeline ID for the new deal (ignored when --stage is set) | | `--wait` | Poll the conversion status until it finishes | | `--timeout-secs` \ | Max seconds to poll when --wait is set | ```bash pdcli lead convert adf21080-0e10-11eb-879b-05d71fb426ec pdcli lead convert adf21080-0e10-11eb-879b-05d71fb426ec --stage 7 pdcli lead convert adf21080-0e10-11eb-879b-05d71fb426ec --wait ``` ### `pdcli lead create` [Section titled “pdcli lead create”](#pdcli-lead-create) Create a lead ```text pdcli lead create [flags] ``` | Flag | Description | | -------------------------------- | --------------------------------------- | | `--title` \ | Lead title | | `--person` \ | Linked person ID | | `--org` \ | Linked organization ID | | `--owner` \ | Owner (user) ID | | `--value` \ | Lead value amount (requires --currency) | | `--currency` \ | Lead value currency (requires --value) | | `--expected-close-date` \ | Expected close date (YYYY-MM-DD) | | `--body` \ | Raw JSON body to merge (flags win) | ```bash pdcli lead create --title "Acme renewal" --value 5000 --currency EUR pdcli lead create --title "Linked" --person 4 --org 5 pdcli lead create --title "Raw" --body '{"visible_to":"3"}' ``` ### `pdcli lead delete` [Section titled “pdcli lead delete”](#pdcli-lead-delete) Delete a lead ```text pdcli lead delete [flags] ``` | Flag | Description | | ----------- | ---------------------------- | | `-y, --yes` | Skip the confirmation prompt | ```bash pdcli lead delete adf21080-0e10-11eb-879b-05d71fb426ec pdcli lead delete adf21080-0e10-11eb-879b-05d71fb426ec --yes ``` ### `pdcli lead get` [Section titled “pdcli lead get”](#pdcli-lead-get) Get a lead by ID ```text pdcli lead get [flags] ``` ```bash pdcli lead get adf21080-0e10-11eb-879b-05d71fb426ec pdcli lead get adf21080-0e10-11eb-879b-05d71fb426ec --output json ``` ### `pdcli lead label list` [Section titled “pdcli lead label list”](#pdcli-lead-label-list) List lead labels ```text pdcli lead label list [flags] ``` ```bash pdcli lead label list pdcli lead label list --output json ``` ### `pdcli lead list` [Section titled “pdcli lead list”](#pdcli-lead-list) List leads ```text pdcli lead list [flags] ``` | Flag | Description | | ------------------- | ------------------------- | | `--owner` \ | Filter by owner (user) ID | | `--person` \ | Filter by person ID | | `--org` \ | Filter by organization ID | ```bash pdcli lead list pdcli lead list --owner 3 --output json ``` ### `pdcli lead update` [Section titled “pdcli lead update”](#pdcli-lead-update) Update a lead (v1 PATCH — only provided fields change) ```text pdcli lead update [flags] ``` | Flag | Description | | -------------------------------- | --------------------------------------- | | `--title` \ | Lead title | | `--person` \ | Linked person ID | | `--org` \ | Linked organization ID | | `--owner` \ | Owner (user) ID | | `--value` \ | Lead value amount (requires --currency) | | `--currency` \ | Lead value currency (requires --value) | | `--expected-close-date` \ | Expected close date (YYYY-MM-DD) | | `--body` \ | Raw JSON body to merge (flags win) | ```bash pdcli lead update adf21080-0e10-11eb-879b-05d71fb426ec --title "Renamed" pdcli lead update adf21080-0e10-11eb-879b-05d71fb426ec --value 7500 --currency USD ``` ## pdcli metrics [Section titled “pdcli metrics”](#pdcli-metrics) ### `pdcli metrics aging` [Section titled “pdcli metrics aging”](#pdcli-metrics-aging) Deal aging: days-in-current-stage per open deal, bucketed, with a p90-dwell flag (mines each open deal’s changelog, one request per deal) ```text pdcli metrics aging [flags] ``` | Flag | Description | | --------------------- | --------------------------------------------------------------------------------------------------------- | | `--pipeline` \ | Pipeline ID (required when the account has several) | | `--buckets` \ | Comma-separated day thresholds; cohorts are 0-N1/N1-N2/.../last+ (lower bound inclusive, upper exclusive) | ```bash pdcli metrics aging pdcli metrics aging --pipeline 1 --buckets 30,60,90 ``` ### `pdcli metrics conversion-matrix` [Section titled “pdcli metrics conversion-matrix”](#pdcli-metrics-conversion-matrix) Stage-transition matrix: every stage move (incl. backward & re-entry) mined from per-deal changelogs, with Won/Lost terminal columns ```text pdcli metrics conversion-matrix [flags] ``` | Flag | Description | | --------------------- | --------------------------------------------------- | | `--pipeline` \ | Pipeline ID (required when the account has several) | ```bash pdcli metrics conversion-matrix pdcli metrics conversion-matrix --pipeline 1 --output json ``` ### `pdcli metrics coverage` [Section titled “pdcli metrics coverage”](#pdcli-metrics-coverage) Pipeline coverage: probability-weighted open pipeline vs the revenue still needed to hit quota ```text pdcli metrics coverage [flags] ``` | Flag | Description | | --------------------- | ------------------------------------------------------------------------------------------------------------------- | | `--pipeline` \ | Pipeline ID (required when the account has several) | | `--period` \ | Goal measurement window (Nd or Nm) | | `--target` \ | Manual revenue quota override (skips the Goals API entirely) | | `--currency` \ | Restrict the open pipeline to this currency code (required when the pipeline holds deals in more than one currency) | ```bash pdcli metrics coverage pdcli metrics coverage --pipeline 1 pdcli metrics coverage --target 250000 pdcli metrics coverage --period 1m --output json ``` ### `pdcli metrics forecast` [Section titled “pdcli metrics forecast”](#pdcli-metrics-forecast) Time-phased forecast: open pipeline bucketed by close-month into commit / best-case / weighted views, segregated per currency ```text pdcli metrics forecast [flags] ``` | Flag | Description | | ----------------------------- | ------------------------------------------------------------------- | | `--pipeline` \ | Pipeline ID (required when the account has several) | | `--commit-threshold` \ | Min effective win-probability (%) for a deal to count toward commit | ```bash pdcli metrics forecast pdcli metrics forecast --pipeline 1 pdcli metrics forecast --commit-threshold 80 --output json ``` ### `pdcli metrics slippage` [Section titled “pdcli metrics slippage”](#pdcli-metrics-slippage) Close-date slippage: open deals whose expected close date keeps getting pushed out (mined per-deal changelog) ```text pdcli metrics slippage [flags] ``` | Flag | Description | | ----------------------- | ------------------------------------------------------- | | `--pipeline` \ | Pipeline ID (required when the account has several) | | `--min-pushes` \ | Only show deals pushed forward at least this many times | ```bash pdcli metrics slippage pdcli metrics slippage --pipeline 1 pdcli metrics slippage --min-pushes 2 --output json ``` ### `pdcli metrics velocity` [Section titled “pdcli metrics velocity”](#pdcli-metrics-velocity) Sales Velocity Equation: (open × win rate × avg won value) / cycle days ```text pdcli metrics velocity [flags] ``` | Flag | Description | | --------------------- | ------------------------------------------- | | `--period` \ | Trailing window for closed deals (Nd or Nm) | | `--pipeline` \ | Restrict to a pipeline ID | | `--owner` \ | Restrict to an owner (user) ID | ```bash pdcli metrics velocity pdcli metrics velocity --period 30d --pipeline 1 ``` ## pdcli note [Section titled “pdcli note”](#pdcli-note) ### `pdcli note comment add` [Section titled “pdcli note comment add”](#pdcli-note-comment-add) Add a comment to a note ```text pdcli note comment add [flags] ``` | Flag | Description | | -------------------- | --------------- | | `--content` \ | Comment content | ```bash pdcli note comment add 5 --content "Nice work" pdcli note comment add 5 --content "Reviewed" --output json ``` ### `pdcli note comment delete` [Section titled “pdcli note comment delete”](#pdcli-note-comment-delete) Delete a comment from a note ```text pdcli note comment delete [flags] ``` | Flag | Description | | -------------------- | ---------------------------- | | `--comment` \ | Comment ID (UUID) | | `-y, --yes` | Skip the confirmation prompt | ```bash pdcli note comment delete 5 --comment pdcli note comment delete 5 --comment --yes ``` ### `pdcli note comment list` [Section titled “pdcli note comment list”](#pdcli-note-comment-list) List comments on a note ```text pdcli note comment list [flags] ``` ```bash pdcli note comment list 5 pdcli note comment list 5 --output json ``` ### `pdcli note comment update` [Section titled “pdcli note comment update”](#pdcli-note-comment-update) Update a comment on a note ```text pdcli note comment update [flags] ``` | Flag | Description | | -------------------- | ------------------- | | `--comment` \ | Comment ID (UUID) | | `--content` \ | New comment content | ```bash pdcli note comment update 5 --comment --content "Edited" ``` ### `pdcli note create` [Section titled “pdcli note create”](#pdcli-note-create) Create a note ```text pdcli note create [flags] ``` | Flag | Description | | -------------------- | ---------------------------------- | | `--content` \ | Note content | | `--deal` \ | Attach to deal ID | | `--person` \ | Attach to person ID | | `--org` \ | Attach to organization ID | | `--lead` \ | Attach to lead ID (UUID) | | `--body` \ | Raw JSON body to merge (flags win) | ```bash pdcli note create --content "Called the lead" pdcli note create --content "Follow up" --deal 42 pdcli note create --content "Pinned" --body '{"pinned_to_deal_flag":1}' ``` ### `pdcli note delete` [Section titled “pdcli note delete”](#pdcli-note-delete) Delete a note ```text pdcli note delete [flags] ``` | Flag | Description | | ----------- | ---------------------------- | | `-y, --yes` | Skip the confirmation prompt | ```bash pdcli note delete 5 pdcli note delete 5 --yes ``` ### `pdcli note get` [Section titled “pdcli note get”](#pdcli-note-get) Get a note by ID ```text pdcli note get [flags] ``` ```bash pdcli note get 5 pdcli note get 5 --output json ``` ### `pdcli note list` [Section titled “pdcli note list”](#pdcli-note-list) List notes ```text pdcli note list [flags] ``` | Flag | Description | | ------------------- | ------------------------- | | `--deal` \ | Filter by deal ID | | `--person` \ | Filter by person ID | | `--org` \ | Filter by organization ID | ```bash pdcli note list pdcli note list --deal 42 --output json ``` ### `pdcli note update` [Section titled “pdcli note update”](#pdcli-note-update) Update a note (only provided fields change) ```text pdcli note update [flags] ``` | Flag | Description | | -------------------- | ---------------------------------- | | `--content` \ | Note content | | `--body` \ | Raw JSON body to merge (flags win) | ```bash pdcli note update 5 --content "Revised note" pdcli note update 5 --body '{"pinned_to_deal_flag":1}' ``` ## pdcli org [Section titled “pdcli org”](#pdcli-org) ### `pdcli org create` [Section titled “pdcli org create”](#pdcli-org-create) Create an organization ```text pdcli org create [flags] ``` | Flag | Description | | ------------------ | -------------------------------------------------- | | `--name` \ | Organization name | | `--owner` \ | Owner (user) ID | | `--field` \ | Custom/standard field as "Name=Value" (repeatable) | | `--body` \ | Raw JSON body to merge (flags win) | ```bash pdcli org create --name "Acme Corp" pdcli org create --name "Tiered" --field "Tier=Gold" pdcli org create --name "Raw" --body '{"visible_to":3}' ``` ### `pdcli org delete` [Section titled “pdcli org delete”](#pdcli-org-delete) Delete an organization ```text pdcli org delete [flags] ``` | Flag | Description | | ----------- | ---------------------------- | | `-y, --yes` | Skip the confirmation prompt | ```bash pdcli org delete 7 pdcli org delete 7 --yes ``` ### `pdcli org follower add` [Section titled “pdcli org follower add”](#pdcli-org-follower-add) Add a follower (user) to an organization ```text pdcli org follower add [flags] ``` | Flag | Description | | ----------------- | ----------- | | `--user` \ | User ID | ```bash pdcli org follower add 42 --user 5 pdcli org follower add 42 --user 5 --output json ``` ### `pdcli org follower list` [Section titled “pdcli org follower list”](#pdcli-org-follower-list) List followers of an organization ```text pdcli org follower list [flags] ``` ```bash pdcli org follower list 42 pdcli org follower list 42 --output json ``` ### `pdcli org follower remove` [Section titled “pdcli org follower remove”](#pdcli-org-follower-remove) Remove a follower from an organization ```text pdcli org follower remove [flags] ``` | Flag | Description | | ----------------- | ---------------------------- | | `--user` \ | User ID | | `-y, --yes` | Skip the confirmation prompt | ```bash pdcli org follower remove 42 --user 5 pdcli org follower remove 42 --user 5 --yes ``` ### `pdcli org get` [Section titled “pdcli org get”](#pdcli-org-get) Get an organization by ID ```text pdcli org get [flags] ``` ```bash pdcli org get 7 pdcli org get 7 --output json ``` ### `pdcli org import` [Section titled “pdcli org import”](#pdcli-org-import) Bulk-create organizations from a CSV (headers map to fields, custom fields by name) ```text pdcli org import [flags] ``` | Flag | Description | | --------------------- | --------------------------------------------------- | | `--upsert` | Match each row on --match-on, then create or update | | `--match-on` \ | Field to match rows on in --upsert mode (e.g. name) | | `--dry-run` | Validate every row without creating anything | | `-y, --yes` | Skip the confirmation prompt | ```bash pdcli org import orgs.csv pdcli org import orgs.csv --dry-run ``` ### `pdcli org list` [Section titled “pdcli org list”](#pdcli-org-list) List organizations ```text pdcli org list [flags] ``` | Flag | Description | | ------------------------------------------ | --------------------------------------------------------------------- | | `--owner` \ | Filter by owner (user) ID | | `--filter` \ | Filter by saved filter ID | | `--ids` \ | Comma-separated IDs to fetch (max 100) | | `--sort-by` \ | Sort field | | `--sort-direction` \ | Sort direction | | `--updated-since` \ | Only items updated at/after this RFC3339 time (no fractional seconds) | | `--updated-until` \ | Only items updated before this RFC3339 time (no fractional seconds) | ```bash pdcli org list pdcli org list --owner 3 --output json ``` ### `pdcli org merge` [Section titled “pdcli org merge”](#pdcli-org-merge) Merge one organization into another. WARNING: the positional \ is the LOSING record — Pipedrive deletes it. --into is the surviving record whose data wins on conflict. All related data (deals, activities, notes, files) is transferred to the survivor. ```text pdcli org merge [flags] ``` | Flag | Description | | ----------------- | ----------------------------------------------------- | | `--into` \ | ID of the surviving organization to keep (the winner) | | `-y, --yes` | Skip the confirmation prompt | ```bash pdcli org merge 123 --into 456 pdcli org merge 123 --into 456 --yes ``` ### `pdcli org relationship add` [Section titled “pdcli org relationship add”](#pdcli-org-relationship-add) Create an organization relationship. For a parent relationship the --owner organization is the parent and --linked is the daughter. ```text pdcli org relationship add [flags] ``` | Flag | Description | | --------------------------- | ----------------------------------------------------- | | `--type` \ | Relationship type | | `--owner` \ | Owner organization ID (the parent for type parent) | | `--linked` \ | Linked organization ID (the daughter for type parent) | ```bash pdcli org relationship add --type parent --owner 1481 --linked 1480 pdcli org relationship add --type related --owner 1 --linked 2 ``` ### `pdcli org relationship list` [Section titled “pdcli org relationship list”](#pdcli-org-relationship-list) List relationships for an organization ```text pdcli org relationship list [flags] ``` | Flag | Description | | ---------------- | ----------------------------------------- | | `--org` \ | Organization ID to list relationships for | ```bash pdcli org relationship list --org 1481 pdcli org relationship list --org 1481 --output json ``` ### `pdcli org relationship remove` [Section titled “pdcli org relationship remove”](#pdcli-org-relationship-remove) Delete an organization relationship ```text pdcli org relationship remove [flags] ``` | Flag | Description | | ----------- | ---------------------------- | | `-y, --yes` | Skip the confirmation prompt | ```bash pdcli org relationship remove 7 pdcli org relationship remove 7 --yes ``` ### `pdcli org update` [Section titled “pdcli org update”](#pdcli-org-update) Update an organization (v2 PATCH — only provided fields change) ```text pdcli org update [flags] ``` | Flag | Description | | ------------------ | -------------------------------------------------- | | `--name` \ | Organization name | | `--owner` \ | Owner (user) ID | | `--field` \ | Custom/standard field as "Name=Value" (repeatable) | | `--body` \ | Raw JSON body to merge (flags win) | ```bash pdcli org update 7 --name "Acme Inc" pdcli org update 7 --owner 9 pdcli org update 7 --field "Tier=Gold" ``` ### `pdcli org upsert` [Section titled “pdcli org upsert”](#pdcli-org-upsert) Idempotent organization upsert: match by --by, then create or PATCH only the changed fields. Refuses (exit 65) if more than one record matches. ```text pdcli org upsert [flags] ``` | Flag | Description | | ------------------ | ----------------------------------------------- | | `--by` \ | Match field: name, or a searchable custom field | | `--field` \ | Field to set as "Name=Value" (repeatable) | | `--body` \ | Raw JSON body to merge | | `--dry-run` | Preview the action without writing | ```bash pdcli org upsert Acme --by name --field "Tier=Gold" pdcli org upsert "D-42" --by "External ID" --body '{"owner_id":42}' pdcli org upsert Acme --by name --field "Tier=Gold" --dry-run ``` ## pdcli person [Section titled “pdcli person”](#pdcli-person) ### `pdcli person create` [Section titled “pdcli person create”](#pdcli-person-create) Create a person ```text pdcli person create [flags] ``` | Flag | Description | | ------------------ | -------------------------------------------------- | | `--name` \ | Person name | | `--email` \ | Email address (repeatable; first is primary) | | `--phone` \ | Phone number (repeatable; first is primary) | | `--org` \ | Linked organization ID | | `--owner` \ | Owner (user) ID | | `--field` \ | Custom/standard field as "Name=Value" (repeatable) | | `--body` \ | Raw JSON body to merge (flags win) | ```bash pdcli person create --name "Jane Doe" --email jane@acme.com pdcli person create --name "Jane" --field "Segment=Enterprise" pdcli person create --name "Raw" --body '{"visible_to":"3"}' ``` ### `pdcli person delete` [Section titled “pdcli person delete”](#pdcli-person-delete) Delete a person ```text pdcli person delete [flags] ``` | Flag | Description | | ----------- | ---------------------------- | | `-y, --yes` | Skip the confirmation prompt | ```bash pdcli person delete 42 pdcli person delete 42 --yes ``` ### `pdcli person follower add` [Section titled “pdcli person follower add”](#pdcli-person-follower-add) Add a follower (user) to a person ```text pdcli person follower add [flags] ``` | Flag | Description | | ----------------- | ----------- | | `--user` \ | User ID | ```bash pdcli person follower add 42 --user 5 pdcli person follower add 42 --user 5 --output json ``` ### `pdcli person follower list` [Section titled “pdcli person follower list”](#pdcli-person-follower-list) List followers of a person ```text pdcli person follower list [flags] ``` ```bash pdcli person follower list 42 pdcli person follower list 42 --output json ``` ### `pdcli person follower remove` [Section titled “pdcli person follower remove”](#pdcli-person-follower-remove) Remove a follower from a person ```text pdcli person follower remove [flags] ``` | Flag | Description | | ----------------- | ---------------------------- | | `--user` \ | User ID | | `-y, --yes` | Skip the confirmation prompt | ```bash pdcli person follower remove 42 --user 5 pdcli person follower remove 42 --user 5 --yes ``` ### `pdcli person get` [Section titled “pdcli person get”](#pdcli-person-get) Get a person by ID ```text pdcli person get [flags] ``` ```bash pdcli person get 5 pdcli person get 5 --output json ``` ### `pdcli person import` [Section titled “pdcli person import”](#pdcli-person-import) Bulk-create persons from a CSV (headers map to fields, custom fields by name) ```text pdcli person import [flags] ``` | Flag | Description | | --------------------- | ---------------------------------------------------- | | `--upsert` | Match each row on --match-on, then create or update | | `--match-on` \ | Field to match rows on in --upsert mode (e.g. email) | | `--dry-run` | Validate every row without creating anything | | `-y, --yes` | Skip the confirmation prompt | ```bash pdcli person import people.csv pdcli person import people.csv --dry-run ``` ### `pdcli person list` [Section titled “pdcli person list”](#pdcli-person-list) List persons (contacts) ```text pdcli person list [flags] ``` | Flag | Description | | ------------------------------------------ | --------------------------------------------------------------------- | | `--owner` \ | Filter by owner (user) ID | | `--org` \ | Filter by organization ID | | `--filter` \ | Filter by saved filter ID | | `--ids` \ | Comma-separated IDs to fetch (max 100) | | `--sort-by` \ | Sort field | | `--sort-direction` \ | Sort direction | | `--updated-since` \ | Only items updated at/after this RFC3339 time (no fractional seconds) | | `--updated-until` \ | Only items updated before this RFC3339 time (no fractional seconds) | ```bash pdcli person list pdcli person list --org 7 --output json ``` ### `pdcli person merge` [Section titled “pdcli person merge”](#pdcli-person-merge) Merge one person into another. WARNING: the positional \ is the LOSING record — Pipedrive deletes it. --into is the surviving record whose data wins on conflict. All related data (deals, activities, notes, files) is transferred to the survivor. ```text pdcli person merge [flags] ``` | Flag | Description | | ----------------- | ----------------------------------------------- | | `--into` \ | ID of the surviving person to keep (the winner) | | `-y, --yes` | Skip the confirmation prompt | ```bash pdcli person merge 123 --into 456 pdcli person merge 123 --into 456 --yes ``` ### `pdcli person update` [Section titled “pdcli person update”](#pdcli-person-update) Update a person (v2 PATCH — only provided fields change) ```text pdcli person update [flags] ``` | Flag | Description | | ------------------ | -------------------------------------------------- | | `--name` \ | Person name | | `--email` \ | Email address (repeatable; first is primary) | | `--phone` \ | Phone number (repeatable; first is primary) | | `--org` \ | Linked organization ID | | `--owner` \ | Owner (user) ID | | `--field` \ | Custom/standard field as "Name=Value" (repeatable) | | `--body` \ | Raw JSON body to merge (flags win) | ```bash pdcli person update 42 --name "New name" pdcli person update 42 --email new@acme.com pdcli person update 42 --field "Segment=Enterprise" ``` ### `pdcli person upsert` [Section titled “pdcli person upsert”](#pdcli-person-upsert) Idempotent person upsert: match by --by, then create or PATCH only the changed fields. Refuses (exit 65) if more than one record matches. ```text pdcli person upsert [flags] ``` | Flag | Description | | ------------------ | ------------------------------------------------------------- | | `--by` \ | Match field: email, name, phone, or a searchable custom field | | `--field` \ | Field to set as "Name=Value" (repeatable) | | `--body` \ | Raw JSON body to merge | | `--dry-run` | Preview the action without writing | ```bash pdcli person upsert a@x.com --by email --field "Tier=Gold" pdcli person upsert "Jane Doe" --by name --body '{"owner_id":42}' pdcli person upsert a@x.com --by email --field "Tier=Gold" --dry-run ``` ## pdcli pipeline [Section titled “pdcli pipeline”](#pdcli-pipeline) ### `pdcli pipeline get` [Section titled “pdcli pipeline get”](#pdcli-pipeline-get) Get a pipeline by ID ```text pdcli pipeline get [flags] ``` ```bash pdcli pipeline get 1 pdcli pipeline get 1 --output json ``` ### `pdcli pipeline health` [Section titled “pdcli pipeline health”](#pdcli-pipeline-health) Per-stage pipeline health: value, weighted value, stale deals, missing next steps ```text pdcli pipeline health [flags] ``` | Flag | Description | | --------------------- | --------------------------------------------------- | | `--pipeline` \ | Pipeline ID (required when the account has several) | ```bash pdcli pipeline health pdcli pipeline health --pipeline 1 ``` ### `pdcli pipeline list` [Section titled “pdcli pipeline list”](#pdcli-pipeline-list) List pipelines ```text pdcli pipeline list [flags] ``` ```bash pdcli pipeline list pdcli pipeline list --output json ``` ## pdcli product [Section titled “pdcli product”](#pdcli-product) ### `pdcli product create` [Section titled “pdcli product create”](#pdcli-product-create) Create a product ```text pdcli product create [flags] ``` | Flag | Description | | ------------------------ | -------------------------------------------------- | | `--name` \ | Product name | | `--code` \ | Product code (SKU) | | `--unit` \ | Unit of measure | | `--description` \ | Product description | | `--owner` \ | Owner (user) ID | | `--price` \ | Unit price (requires --currency) | | `--currency` \ | Price currency (requires --price) | | `--field` \ | Custom/standard field as "Name=Value" (repeatable) | | `--body` \ | Raw JSON body to merge (flags win) | ```bash pdcli product create --name "Widget" --code W-1 --price 9.99 --currency EUR pdcli product create --name "Sized" --field "Material=Steel" pdcli product create --name "Raw" --body '{"tax":19}' ``` ### `pdcli product delete` [Section titled “pdcli product delete”](#pdcli-product-delete) Delete a product ```text pdcli product delete [flags] ``` | Flag | Description | | ----------- | ---------------------------- | | `-y, --yes` | Skip the confirmation prompt | ```bash pdcli product delete 7 pdcli product delete 7 --yes ``` ### `pdcli product get` [Section titled “pdcli product get”](#pdcli-product-get) Get a product by ID ```text pdcli product get [flags] ``` ```bash pdcli product get 7 pdcli product get 7 --output json ``` ### `pdcli product list` [Section titled “pdcli product list”](#pdcli-product-list) List products ```text pdcli product list [flags] ``` | Flag | Description | | ------------------------------------------------ | --------------------------------------------------------------------- | | `--owner` \ | Filter by owner (user) ID | | `--filter` \ | Filter by saved filter ID | | `--ids` \ | Comma-separated IDs to fetch (max 100) | | `--sort-by` \ | Sort field | | `--sort-direction` \ | Sort direction | | `--updated-since` \ | Only items updated at/after this RFC3339 time (no fractional seconds) | ```bash pdcli product list pdcli product list --owner 3 --output json ``` ### `pdcli product update` [Section titled “pdcli product update”](#pdcli-product-update) Update a product (v2 PATCH — only provided fields change) ```text pdcli product update [flags] ``` | Flag | Description | | ------------------------ | -------------------------------------------------- | | `--name` \ | Product name | | `--code` \ | Product code (SKU) | | `--unit` \ | Unit of measure | | `--description` \ | Product description | | `--owner` \ | Owner (user) ID | | `--price` \ | Unit price (requires --currency) | | `--currency` \ | Price currency (requires --price) | | `--field` \ | Custom/standard field as "Name=Value" (repeatable) | | `--body` \ | Raw JSON body to merge (flags win) | ```bash pdcli product update 7 --name "New name" pdcli product update 7 --price 12.50 --currency USD pdcli product update 7 --field "Material=Steel" ``` ## pdcli profile [Section titled “pdcli profile”](#pdcli-profile) ### `pdcli profile current` [Section titled “pdcli profile current”](#pdcli-profile-current) Show the active profile ```text pdcli profile current [flags] ``` ```bash pdcli profile current ``` ### `pdcli profile list` [Section titled “pdcli profile list”](#pdcli-profile-list) List all configured profiles ```text pdcli profile list [flags] ``` ```bash pdcli profile list ``` ### `pdcli profile use` [Section titled “pdcli profile use”](#pdcli-profile-use) Switch the active profile ```text pdcli profile use [flags] ``` ```bash pdcli profile use work ``` ## pdcli project [Section titled “pdcli project”](#pdcli-project) ### `pdcli project create` [Section titled “pdcli project create”](#pdcli-project-create) Create a project ```text pdcli project create [flags] ``` | Flag | Description | | ------------------------ | ---------------------------------- | | `--title` \ | Project title | | `--description` \ | Project description | | `--status` \ | Project status | | `--start-date` \ | Start date (YYYY-MM-DD) | | `--end-date` \ | End date (YYYY-MM-DD) | | `--owner` \ | Owner (user) ID | | `--board` \ | Board ID | | `--phase` \ | Phase ID | | `--body` \ | Raw JSON body to merge (flags win) | ```bash pdcli project create --title "Launch" pdcli project create --title "Launch" --owner 3 --status open pdcli project create --title "Raw" --body '{"deal_ids":[1,2]}' ``` ### `pdcli project delete` [Section titled “pdcli project delete”](#pdcli-project-delete) Delete a project ```text pdcli project delete [flags] ``` | Flag | Description | | ----------- | ---------------------------- | | `-y, --yes` | Skip the confirmation prompt | ```bash pdcli project delete 7 pdcli project delete 7 --yes ``` ### `pdcli project get` [Section titled “pdcli project get”](#pdcli-project-get) Get a project by ID ```text pdcli project get [flags] ``` ```bash pdcli project get 3 pdcli project get 3 --output json ``` ### `pdcli project list` [Section titled “pdcli project list”](#pdcli-project-list) List projects ```text pdcli project list [flags] ``` | Flag | Description | | ------------ | --------------------------------------------- | | `--archived` | List archived projects instead of active ones | ```bash pdcli project list pdcli project list --output json ``` ### `pdcli project update` [Section titled “pdcli project update”](#pdcli-project-update) Update a project (v2 PATCH — only provided fields change) ```text pdcli project update [flags] ``` | Flag | Description | | ------------------------ | ---------------------------------- | | `--title` \ | Project title | | `--description` \ | Project description | | `--status` \ | Project status | | `--start-date` \ | Start date (YYYY-MM-DD) | | `--end-date` \ | End date (YYYY-MM-DD) | | `--owner` \ | Owner (user) ID | | `--board` \ | Board ID | | `--phase` \ | Phase ID | | `--body` \ | Raw JSON body to merge (flags win) | ```bash pdcli project update 7 --title "Relaunch" pdcli project update 7 --status closed pdcli project update 7 --owner 9 ``` ## pdcli rep [Section titled “pdcli rep”](#pdcli-rep) ### `pdcli rep scorecard` [Section titled “pdcli rep scorecard”](#pdcli-rep-scorecard) Per-rep scorecard: win rate, cycle, velocity and deal hygiene by owner, across all pipelines (account-wide) unless narrowed ```text pdcli rep scorecard [flags] ``` | Flag | Description | | --------------------- | ------------------------------------------- | | `--period` \ | Trailing window for closed deals (Nd or Nm) | | `--pipeline` \ | Restrict to a pipeline ID | | `--owner` \ | Restrict to a single owner (user) ID | ```bash pdcli rep scorecard pdcli rep scorecard --period 30d --pipeline 1 pdcli rep scorecard --owner 42 --output json ``` ## pdcli stage [Section titled “pdcli stage”](#pdcli-stage) ### `pdcli stage get` [Section titled “pdcli stage get”](#pdcli-stage-get) Get a stage by ID ```text pdcli stage get [flags] ``` ```bash pdcli stage get 5 pdcli stage get 5 --output json ``` ### `pdcli stage list` [Section titled “pdcli stage list”](#pdcli-stage-list) List stages ```text pdcli stage list [flags] ``` | Flag | Description | | --------------------- | --------------------- | | `--pipeline` \ | Filter by pipeline ID | ```bash pdcli stage list pdcli stage list --pipeline 1 --output json ``` ## pdcli sync [Section titled “pdcli sync”](#pdcli-sync) ### `pdcli sync warehouse` [Section titled “pdcli sync warehouse”](#pdcli-sync-warehouse) Incremental NDJSON export for a data warehouse: appends only records changed since the last run, per-entity, with high-water marks in manifest.json. First run seeds a full export. NOTE: pull-based CDC sees creates/updates only — hard deletes are not captured; reconcile against a periodic full `backup`. ```text pdcli sync warehouse [flags] ``` | Flag | Description | | ------------------ | ------------------------------------------------------------ | | `--dir` \ | Output directory for the NDJSON files + manifest | | `--since` \ | Override the start for all entities (RFC3339 or Nd/Nm) | | `--full` | Ignore watermarks and rebuild from scratch (truncates files) | | `-y, --yes` | Skip the --full truncation confirmation | ```bash pdcli sync warehouse --dir ./warehouse pdcli sync warehouse --dir ./warehouse --since 7d pdcli sync warehouse --dir ./warehouse --full ``` ## pdcli task [Section titled “pdcli task”](#pdcli-task) ### `pdcli task create` [Section titled “pdcli task create”](#pdcli-task-create) Create a task ```text pdcli task create [flags] ``` | Flag | Description | | ------------------------ | ---------------------------------- | | `--title` \ | Task title | | `--project` \ | Project ID | | `--description` \ | Task description | | `--assignee` \ | Assignee (user) ID | | `--due-date` \ | Due date (YYYY-MM-DD) | | `--parent` \ | Parent task ID | | `--body` \ | Raw JSON body to merge (flags win) | ```bash pdcli task create --title "Write spec" --project 3 pdcli task create --title "Subtask" --project 3 --parent 5 --assignee 7 pdcli task create --title "Raw" --project 3 --body '{"priority":5}' ``` ### `pdcli task delete` [Section titled “pdcli task delete”](#pdcli-task-delete) Delete a task ```text pdcli task delete [flags] ``` | Flag | Description | | ----------- | ---------------------------- | | `-y, --yes` | Skip the confirmation prompt | ```bash pdcli task delete 7 pdcli task delete 7 --yes ``` ### `pdcli task get` [Section titled “pdcli task get”](#pdcli-task-get) Get a task by ID ```text pdcli task get [flags] ``` ```bash pdcli task get 9 pdcli task get 9 --output json ``` ### `pdcli task list` [Section titled “pdcli task list”](#pdcli-task-list) List tasks ```text pdcli task list [flags] ``` | Flag | Description | | --------------------- | ---------------------------- | | `--project` \ | Filter by project ID | | `--assignee` \ | Filter by assignee (user) ID | | `--parent` \ | Filter by parent task ID | | `--done` | Only completed tasks | | `--todo` | Only open (not done) tasks | ```bash pdcli task list pdcli task list --project 3 --todo pdcli task list --assignee 7 --output json ``` ### `pdcli task update` [Section titled “pdcli task update”](#pdcli-task-update) Update a task (v2 PATCH — only provided fields change) ```text pdcli task update [flags] ``` | Flag | Description | | ------------------------ | ---------------------------------- | | `--title` \ | Task title | | `--project` \ | Project ID | | `--description` \ | Task description | | `--assignee` \ | Assignee (user) ID | | `--due-date` \ | Due date (YYYY-MM-DD) | | `--parent` \ | Parent task ID | | `--done` | Mark the task as done | | `--undone` | Mark the task as not done | | `--body` \ | Raw JSON body to merge (flags win) | ```bash pdcli task update 7 --title "Renamed" pdcli task update 7 --done pdcli task update 7 --assignee 9 ``` ## pdcli user [Section titled “pdcli user”](#pdcli-user) ### `pdcli user find` [Section titled “pdcli user find”](#pdcli-user-find) Find users by name ```text pdcli user find [flags] ``` | Flag | Description | | ------------ | ------------------------------------------- | | `--by-email` | Match the term against email addresses only | ```bash pdcli user find "jane" pdcli user find "jane@acme.com" --by-email --output json ``` ### `pdcli user list` [Section titled “pdcli user list”](#pdcli-user-list) List all users ```text pdcli user list [flags] ``` ```bash pdcli user list pdcli user list --output json ``` ### `pdcli user me` [Section titled “pdcli user me”](#pdcli-user-me) Show the authenticated user ```text pdcli user me [flags] ``` ```bash pdcli user me ``` ## pdcli webhook [Section titled “pdcli webhook”](#pdcli-webhook) ### `pdcli webhook create` [Section titled “pdcli webhook create”](#pdcli-webhook-create) Create a webhook ```text pdcli webhook create [flags] ``` | Flag | Description | | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------- | | `--url` \ | Webhook subscription URL | | `--event-action` \ | Event action to subscribe to | | `--event-object` \ | Event object to subscribe to | | `--name` \ | Webhook name | | `--version` \ | Webhook payload version | | `--http-auth-user` \ | HTTP basic auth username for the endpoint | | `--http-auth-password` \ | HTTP basic auth password for the endpoint | ```bash pdcli webhook create --url https://example.com/hook --event-action change --event-object deal pdcli webhook create --url https://example.com/hook --event-action "*" --event-object "*" ``` ### `pdcli webhook delete` [Section titled “pdcli webhook delete”](#pdcli-webhook-delete) Delete a webhook ```text pdcli webhook delete [flags] ``` | Flag | Description | | ----------- | ---------------------------- | | `-y, --yes` | Skip the confirmation prompt | ```bash pdcli webhook delete 3 pdcli webhook delete 3 --yes ``` ### `pdcli webhook list` [Section titled “pdcli webhook list”](#pdcli-webhook-list) List webhooks ```text pdcli webhook list [flags] ``` ```bash pdcli webhook list pdcli webhook list --output json ``` # Config & environment > Profile config keys, environment variables, global flags, the config file location, and precedence. A flat reference for how `pdcli` is configured. For the workflow, see [Profiles & configuration](/pdcli/guides/configuration/). ## Precedence (highest wins) [Section titled “Precedence (highest wins)”](#precedence-highest-wins) 1. **Flags** — `--output`, `--profile`, `--timeout`, … (`--company` and `--api-token` exist only on `pdcli auth login`.) 2. **Environment variables** — `PDCLI_COMPANY_DOMAIN`, `PDCLI_API_TOKEN`, `PDCLI_PROFILE`, … 3. **Profile config** — per-profile keys in the config file. 4. **Global config** — shared defaults. 5. **Built-in defaults.** The company domain and the token resolve **independently**, each down this chain. So a profile domain plus an env token (`PDCLI_API_TOKEN=… pdcli deal list`), or an env domain plus the keychain token, both work. ## Environment variables [Section titled “Environment variables”](#environment-variables) | Variable | Purpose | | ---------------------- | --------------------------------------------------------------------------------------------------------- | | `PDCLI_COMPANY_DOMAIN` | Company subdomain (`acme` from `acme.pipedrive.com`). Forms and locks the API host. | | `PDCLI_API_TOKEN` | Personal API token, sent as `x-api-token`. Preferred over `--api-token` so it stays out of shell history. | | `PDCLI_PROFILE` | Active profile name (same as `--profile`). | | `PDCLI_CLIENT_ID` | OAuth app client ID (used with `auth login --oauth`). | | `PDCLI_CLIENT_SECRET` | OAuth app client secret (used with `auth login --oauth`). | | `NO_COLOR` | Any value disables colored output (same as `--no-color`). | | `DEBUG` | Set to `pd:*` for debug logging (`--verbose` enables this automatically). | Setting `PDCLI_COMPANY_DOMAIN` + `PDCLI_API_TOKEN` is the recommended way to run in CI — no keychain needed. Env values take precedence over stored credentials. ## Global flags [Section titled “Global flags”](#global-flags) Every command accepts these. Run any command with `--help` to confirm. | Flag | Default | Description | | ------------------ | -------------------------- | ---------------------------------------------------------------------------------- | | `--output`, `-o` | table (TTY) / json (piped) | Output format: `table`, `json`, `yaml`, or `csv`. | | `--jq ` | — | Filter output through a jq expression. | | `--fields ` | — | Comma-separated columns to display. | | `--profile ` | `default` | Named auth profile (env `PDCLI_PROFILE`). | | `--no-color` | off | Disable colored output. | | `--verbose` | off | Show request path, status, and `error_info` on errors; enables `pd:*` debug. | | `--no-retry` | off | Disable automatic retry on `429` and `5xx`. | | `--timeout ` | `30000` | Per-request timeout in milliseconds. | | `--limit ` | — | Max items to return from a `list` (auto-pages up to this; API caps a page at 500). | Output defaults to a table in a TTY and JSON when piped; `--output` overrides both. ## Profile config keys [Section titled “Profile config keys”](#profile-config-keys) Config is per profile. `pdcli config set ` writes to the active profile and `pdcli config get ` reads it back (`config get` prints `not set` for an unset key). These keys are written and read by pdcli: | Key | Set by | Description | | ---------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------- | | `company_domain` | `auth login` | Bare company subdomain for the profile's API host. | | `auth_mode` | `auth login` | `token` or `oauth`. Selects how credentials are resolved. | | `default_output` | you | Default output format (`table`/`json`/`yaml`/`csv`) when no `--output` flag is given. Applies to errors too when set to `json`. | ```bash pdcli config set company_domain acme pdcli config get company_domain # → acme pdcli config list # all keys for the active profile ``` The **token is never stored in config** — only in the OS keychain. See [Security model](/pdcli/concepts/security/). ## Config file location [Section titled “Config file location”](#config-file-location) Config is managed by [`conf`](https://www.npmjs.com/package/conf) under the project name `pdcli`, so the file lives in your platform's standard config directory: | Platform | Path | | -------- | ------------------------------------------------ | | Linux | `~/.config/pdcli/config.json` (XDG) | | macOS | `~/Library/Preferences/pdcli-nodejs/config.json` | | Windows | `%APPDATA%\pdcli-nodejs\Config\config.json` | It holds only `activeProfile`, the non-secret per-profile keys above, and any command [aliases](/pdcli/guides/configuration/) under the global `aliases` key (shared across profiles). Credentials are in the OS keychain, not this file. # Troubleshooting > Common pdcli failures — auth, rate limits, keychain, stale manifests, custom fields, pipelines — and how to fix each. Each entry pairs the symptom with the fix. When in doubt, start with `pdcli doctor`, which checks config access, keychain, active profile, company domain, token presence, and API reachability. ## `401` — invalid or expired token [Section titled “401 — invalid or expired token”](#401--invalid-or-expired-token) ```text Pipedrive API 401: ... (exit 77) ``` The token is wrong, revoked, or missing. Re-authenticate: ```bash pdcli auth status # shows profile, host, and token presence pdcli auth login # re-enter the token (or --oauth) ``` OAuth access tokens refresh automatically before expiry and once on a `401`; a persistent `401` means the refresh token itself expired — log in again. ## `402` — required suites missing [Section titled “402 — required suites missing”](#402--required-suites-missing) ```text Pipedrive API 402: ... (exit 78) ``` Your Pipedrive plan doesn't include the feature you're calling. The most common case is **Projects** on a plan without the Projects suite — `pdcli project list` and friends return `402`. There's nothing to retry; upgrade the plan or avoid the command. ## `403` after repeated `429`s — rate-limit hard stop [Section titled “403 after repeated 429s — rate-limit hard stop”](#403-after-repeated-429s--rate-limit-hard-stop) ```text Pipedrive API 403: ... (403 after 429: rate-limit escalation — stopping) (exit 77) ``` Pipedrive escalates sustained rate-limit abuse from `429` to `403`. pdcli treats this as a **hard stop** and won't retry into it. **Wait for your token budget to reset** (per the `x-ratelimit-reset` window, or midnight server time for the daily budget) before retrying. Reduce load: prefer single GETs over lists, and let `backup` pace itself rather than running many list commands in parallel. See [How pdcli talks to Pipedrive](/pdcli/concepts/api-model/). ## Keychain unavailable (headless Linux) [Section titled “Keychain unavailable (headless Linux)”](#keychain-unavailable-headless-linux) ```text OS keychain unavailable. pdcli ... refuses to write them to disk in plaintext. (exit 78) ``` `auth login` needs an OS keychain to store the token, and **fails by design** rather than writing plaintext. On a headless Linux box, either: * Install a keyring backend: `libsecret` + `gnome-keyring` (and ensure the secret service is running), then `pdcli auth login` again; **or** * Skip login entirely and use env-var auth: ```bash PDCLI_COMPANY_DOMAIN=acme PDCLI_API_TOKEN=xxxx pdcli deal list ``` Reads and logout degrade gracefully without a keychain — only writes hard-fail. See [Security model](/pdcli/concepts/security/). ## `X is not a pdcli command` after an upgrade [Section titled “X is not a pdcli command after an upgrade”](#x-is-not-a-pdcli-command-after-an-upgrade) ```text Error: is not a pdcli command. Run pdcli help for a list of available commands. ``` After an upgrade, a stale generated manifest can hide newly added commands. Reinstall to regenerate it: ```bash npm install -g @wavyx/pdcli pdcli --help # confirm the command now lists ``` (If you globally linked from source, re-run your install/link step so the manifest regenerates.) ## A custom field isn't resolving [Section titled “A custom field isn't resolving”](#a-custom-field-isnt-resolving) If `--field "Name=Value"` errors that the field is unknown, the human name doesn't match a field on that entity. List the fields to see the exact names (and their hash keys and option labels): ```bash pdcli field list deal pdcli field get deal "Deal Size" ``` Match the name exactly as shown. For enum/set fields, the **value** must be an existing option label — pdcli maps labels to option IDs, but only for labels that exist. See [Custom fields](/pdcli/guides/custom-fields/). ## "Account has N pipelines — pass --pipeline" [Section titled “"Account has N pipelines — pass --pipeline"”](#account-has-n-pipelines--pass---pipeline) ```text Account has 3 pipelines — pass --pipeline (1=Sales, 2=Renewals, 3=Partners) (exit 64) ``` `funnel` and `pipeline health` analyze a single pipeline. When the account has more than one, they can't guess which — pass `--pipeline` with one of the IDs listed in the message: ```bash pdcli pipeline list # see IDs and names pdcli pipeline health --pipeline 1 pdcli funnel --pipeline 1 ``` With exactly one pipeline, the flag is optional. # Quickstart for AI agents > Run pdcli non-interactively with env-var auth, JSON output, and deterministic exit codes. pdcli is built to be driven by agents (Claude Code, Codex, CI bots). It is self-describing, emits machine-readable JSON, and uses deterministic exit codes. This page covers the conventions an agent needs. ## Authenticate with environment variables [Section titled “Authenticate with environment variables”](#authenticate-with-environment-variables) Avoid the interactive `auth login` flow. Set credentials in the environment so no prompt blocks the run and no token lands in command history or a stored profile: ```bash export PDCLI_COMPANY_DOMAIN=acme export PDCLI_API_TOKEN= pdcli deal list --status open ``` Env vars take precedence over a stored profile and require no keychain, which makes them the right choice for ephemeral or headless environments. The token must stay in the environment, never on a flag like `--api-token` where it would be visible in the process list. ## Output is JSON when piped [Section titled “Output is JSON when piped”](#output-is-json-when-piped) In a TTY the default output is a table; when stdout is **not** a TTY (piped or captured), pdcli defaults to JSON. Be explicit to remove all doubt: ```bash pdcli deal get 42 --output json ``` Refine without leaving the CLI using `--jq` (native jq), `--fields`, or `--output yaml|csv`. See [Output & filtering](/pdcli/automation/output/). ## Branch on exit codes [Section titled “Branch on exit codes”](#branch-on-exit-codes) pdcli uses [sysexits](https://man.freebsd.org/cgi/man.cgi?query=sysexits) codes so a script can react to the failure class without parsing text: | Code | Meaning | | ---- | --------------------------------------------------------------------------------------------- | | 0 | Success | | 1 | Generic error | | 64 | Usage / bad flags or arguments (unknown flag, missing arg, invalid value, missing CSV column) | | 65 | Bad input data (API 400 / 422) | | 69 | Service unavailable (API 5xx, or unreachable) | | 70 | Internal software error (unexpected — a genuine bug, not a usage mistake) | | 75 | Rate limited (API 429) — retry later | | 77 | Not authenticated / forbidden (API 401 / 403) | | 78 | Configuration error (e.g. API 402, missing domain) | Errors print a JSON object on **stderr** whenever output is JSON — with `--output json`, a `json` profile default, **or when stdout is piped** (the same TTY→JSON rule as success output), so a non-interactive consumer always gets a parseable failure: ```json { "error": "ApiError", "message": "Pipedrive API 401: invalid token", "exitCode": 77, "statusCode": 401, "path": "/api/v2/deals" } ``` ## Self-description [Section titled “Self-description”](#self-description) Every command and flag is discoverable: ```bash pdcli --help pdcli deal --help pdcli deal create --help ``` The full reference is at [/pdcli/reference/commands/](/pdcli/reference/commands/). Beyond CRUD, agents have `deal history ` for a deal's field-change audit trail, `deal product` for line items, `field create`/`field update`/`field option` to manage the custom-field schema, `deal participant` and `deal`/`person`/`org follower` for the people around a record, `task` for project action items, `lead convert`/`deal convert` (with `--wait`) to change a record's type, `deal summary` for a server-side per-currency value rollup, time-intelligence metrics (`metrics aging`/`slippage`/`conversion-matrix`) plus `audit stage-skips` for stage-skip compliance — all mined from per-deal changelogs — `metrics forecast` for a per-currency commit/best-case/weighted close-month forecast, `rep scorecard` for per-owner performance, `digest` for the whole Monday packet in one fetch (`--format md|html --out` for a cron → Slack/email artifact), `changes` for an incremental cross-entity change feed with a self-advancing watermark (no receiver to host, unlike webhooks), `deal context ` for a one-call denormalized, prompt-ready bundle (deal + person + org + activities + notes + products + flags), `backup diff` for a zero-API field-level diff of two snapshots, `watch` for an exit-code-gated anomaly poller that fires only on findings new since the last run (`pdcli watch || notify`), `sync warehouse` for an incremental NDJSON export with per-entity high-water marks, and `--updated-since` on the list commands for incremental polling. For writes that must be safe to retry, `person`/`org`/`deal upsert` match a record by `--by` (a built-in key or a searchable custom field) and then create or PATCH only what changed — and **refuse with exit 65** when more than one record matches, so an agent never silently writes the wrong one. `person import`/`org import --upsert --match-on ` apply the same match-or-create per CSV row, reporting created/updated/unchanged counts. Pair upsert with an external key (a custom field carrying your system's ID) for clean, repeatable sync. ## The host-locked `api` escape hatch [Section titled “The host-locked api escape hatch”](#the-host-locked-api-escape-hatch) When no dedicated command exists, call any endpoint directly. The request is host-locked to your authenticated company domain (or the OAuth `api_domain`), so a hallucinated host cannot leak the token. There is no generic data host. ```bash pdcli api GET /api/v2/pipelines pdcli api POST /api/v2/deals --body '{"title":"Raw deal"}' ``` `api` always prints raw JSON. Both v1 and v2 paths work. ## Machine-readable docs [Section titled “Machine-readable docs”](#machine-readable-docs) The whole documentation site is available as plain text for ingestion: * [/pdcli/llms.txt](/pdcli/llms.txt) — index of pages. * [/pdcli/llms-full.txt](/pdcli/llms-full.txt) — the full docs in one file. * [/pdcli/llms-small.txt](/pdcli/llms-small.txt) — a compact variant. ## End-to-end example [Section titled “End-to-end example”](#end-to-end-example) Find open deals owned by user 42 that are missing a value, and report how many: ```bash export PDCLI_COMPANY_DOMAIN=acme export PDCLI_API_TOKEN=$PIPEDRIVE_TOKEN deals=$(pdcli deal list --status open --owner 42 --output json) status=$? if [ "$status" -ne 0 ]; then echo "pdcli failed (exit $status)" >&2 # branch on the sysexits code exit "$status" fi count=$(echo "$deals" | jq '[.[] | select(.value == null)] | length') echo "Open deals missing a value: $count" ``` # Installation > Install pdcli, verify it, and turn on shell completions. `pdcli` is published to npm as [`@wavyx/pdcli`](https://www.npmjs.com/package/@wavyx/pdcli) and runs on Node 20 or newer. ## Requirements [Section titled “Requirements”](#requirements) * **Node.js 20+** (LTS). Check with `node --version`. * An OS keychain for credential storage (macOS Keychain, GNOME Keyring / libsecret on Linux, Windows Credential Manager). Without one, writes that store tokens hard-fail by design; reads still work via environment variables. ## Install globally [Section titled “Install globally”](#install-globally) ```bash npm install -g @wavyx/pdcli ``` This puts a `pdcli` binary on your `PATH`. ## Run without installing [Section titled “Run without installing”](#run-without-installing) ```bash npx @wavyx/pdcli deal list ``` `npx` is handy for CI or a one-off, but the global install is faster for daily use. ## Verify the install [Section titled “Verify the install”](#verify-the-install) `pdcli version` prints the version and environment: ```bash pdcli version ``` ```text pdcli 0.5.0 Node: v20.11.1 API base: https://acme.pipedrive.com Platform: darwin-arm64 ``` `API base` shows `(not set)` until you authenticate. ## Run the doctor [Section titled “Run the doctor”](#run-the-doctor) `pdcli doctor` runs environment checks — config store, keychain, profile, domain, token, and live API reachability: ```bash pdcli doctor ``` ```text Pipedrive CLI Diagnostics ✔ Config directory accessible ✔ Keychain available ✔ Active profile set (default) ✔ Company domain set (acme) ✔ API token present ✔ API reachable All checks passed ``` Before you log in, several checks fail with a hint — for example `✘ Company domain set (Run: pdcli auth login)` and `✘ API token present (Run: pdcli auth login)`. If `Keychain available` fails, your OS keychain is unreachable and pdcli cannot store credentials; use [environment-variable auth](/pdcli/guides/authentication/) instead. ## Shell completions [Section titled “Shell completions”](#shell-completions) Completions ship via the bundled oclif autocomplete plugin. Run `autocomplete` for your shell to print the exact setup steps to add to your shell profile: ```bash pdcli autocomplete bash pdcli autocomplete zsh pdcli autocomplete powershell ``` Each command prints the lines to source (and how) for that shell, then enables tab-completion of commands and flags. Running `pdcli autocomplete` with no shell argument detects your current shell. ## Next steps [Section titled “Next steps”](#next-steps) * [Quickstart](/pdcli/start/quickstart/) — authenticate and run your first commands. * [Quickstart for AI agents](/pdcli/start/agents/) — env-var auth, JSON, exit codes. * [Authentication](/pdcli/guides/authentication/) — token vs OAuth, CI patterns. # Quickstart > Authenticate and run your first pdcli commands in about five minutes. This walks you from a fresh install to listing deals, piping output, and resolving custom fields. It assumes pdcli is [installed](/pdcli/start/installation/). ## 1. Log in [Section titled “1. Log in”](#1-log-in) ```bash pdcli auth login ``` You are prompted for two things: * **Company domain** — the `acme` in `acme.pipedrive.com` (a full URL is accepted too). * **API token** — find it at [app.pipedrive.com/settings/api](https://app.pipedrive.com/settings/api). The prompt masks it so it never lands in your shell history. The token is validated, then stored **only in your OS keychain**. The company domain goes in your profile config. ```text Logged in to acme.pipedrive.com as Jane Doe (jane@acme.com) Profile: default — token in keychain ``` Prefer OAuth or a non-interactive flow? See [Authentication](/pdcli/guides/authentication/). ## 2. Confirm you're authenticated [Section titled “2. Confirm you're authenticated”](#2-confirm-youre-authenticated) ```bash pdcli auth status ``` ```text Auth Status Profile: default Keychain: OS keychain API host: https://acme.pipedrive.com Token: present (keychain) Authenticated User Name: Jane Doe Email: jane@acme.com ``` ## 3. List deals [Section titled “3. List deals”](#3-list-deals) ```bash pdcli deal list --status open --limit 5 ``` In a terminal you get a table: ```text ┌────┬───────────────┬───────────┬────────┬───────┬────────┬─────┬───────┐ │ ID │ Title │ Value │ Status │ Stage │ Person │ Org │ Owner │ ├────┼───────────────┼───────────┼────────┼───────┼────────┼─────┼───────┤ │ 42 │ Acme renewal │ 5000 EUR │ open │ 3 │ 17 │ 7 │ 1 │ │ 43 │ Globex expand │ 12000 USD │ open │ 2 │ 21 │ 9 │ 1 │ └────┴───────────────┴───────────┴────────┴───────┴────────┴─────┴───────┘ ``` When the output is piped, pdcli switches to JSON automatically. ## 4. Pipe into other tools [Section titled “4. Pipe into other tools”](#4-pipe-into-other-tools) Use the built-in `--jq` to pull just the IDs: ```bash pdcli deal list --status open --jq '.[].id' ``` ```text 42 43 ``` That stream feeds straight into other commands — for example, `pdcli deal list --status open --jq '.[].id' | pdcli deal bulk-update --owner 42`. More patterns in [Output & filtering](/pdcli/automation/output/). ## 5. Discover custom fields [Section titled “5. Discover custom fields”](#5-discover-custom-fields) Pipedrive custom fields are 40-character hash keys. `field list` reveals the human names, types, and keys for an entity: ```bash pdcli field list deal ``` ```text ┌──────────────────────────────────────────┬───────────┬──────┬──────────────────┐ │ Key │ Name │ Type │ Options │ ├──────────────────────────────────────────┼───────────┼──────┼──────────────────┤ │ dcf558aac1ae4e8c4f849ba5e668430d8df9be12 │ Deal Size │ enum │ Small, Med, Large│ └──────────────────────────────────────────┴───────────┴──────┴──────────────────┘ ``` Inspect one field by name or key: ```bash pdcli field get deal "Deal Size" ``` You can then write to it by **name** — pdcli resolves the label to the option ID: ```bash pdcli deal update 42 --field "Deal Size=Large" ``` ## Next steps [Section titled “Next steps”](#next-steps) * [Custom fields](/pdcli/guides/custom-fields/) — name/key resolution in depth. * [Authentication](/pdcli/guides/authentication/) — OAuth, CI, switching modes. * [Profiles & configuration](/pdcli/guides/configuration/) — multiple accounts. * [Command reference](/pdcli/reference/commands/) — every command and flag.