Skip to main content

Pipedrive data map — everything that crosses

Audience: developers & administrators · Scope: the quote ↔ deal boundary at a glance — what goes out, what comes back, who the deal belongs to, and what triggers a push · Last reviewed: 2026-08-25

TL;DR: Every website quote is mirrored as a Pipedrive deal. Outbound is a flag drained by a minute cron that sends the complete current picture; inbound is a webhook queued to the database and drained by its own minute cron. Both pass one authentication chokepoint with a company guard.

This page is a map, not the reference. It exists to orient — to show both directions on one screen and let you find the right spoke fast. Each table names the doc that owns the detail; when the two disagree, the spoke is right and this page is stale. Follow the link rather than trusting a row here.


The loop

┌────────────────────────────────┐
│ THE QUOTE │ OUTBOUND — quote → deal
│ │
│ pipedrive_* columns: │ 1. any qualifying change sets
│ deal id · status · │ pipedrive_update_scheduled
│ lost reason · stage + │
│ pipeline · close date │ 2. cb_pipedrive_update_deals.php
│ │ every minute · batch 20 · lock
└────────────────────────────────┘ 3 attempts, reset by next change
▲ │
│ │ full payload — add · update · delete, + Activity
│ ▼
│ ══════════════════════════════════ BcPipedriveAuth
│ token | oauth · company guard · dev-org guard
│ │
│ ▼
│ ┌──────────────────────────────────┐
│ │ PIPEDRIVE │
│ │ deal (native + custom) │ imported daily 06:00–06:15
│ │ organisation ◄── branch │ ─────────────────────────────►
│ │ person ◄── e-mail │ pipelines · stages · users ·
│ │ owner ◄── branch RSM │ fields + options · organisations
│ │ done Activity per follow-up │ into local registries, which the
│ └──────────────────────────────────┘ payload and settings read instead
│ │ of calling the API
│ │ webhook (Basic auth)
│ ▼
│ ┌──────────────────────────────────┐
└───── │ queue → drainer │ INBOUND — deal → quote
writes the │ every minute │ outbound scheduling is
pipedrive_* │ per-item · 3 attempts │ SUPPRESSED while applying
columns └──────────────────────────────────┘ (the echo-loop guard)

Both directions pass BcPipedriveAuth: per-environment credentials (API token or the private OAuth app, chosen by pipedrive_auth_mode, with no silent failover), and a company guard that refuses reads and writes when the credential's Pipedrive company ≠ the declared pipedrive_company_id. Off-live, writes must additionally target the sandbox org (pipedrive_org_id_dev). See oauth-app.md.


Map 1 — quote → deal

Built fresh on every export, so the deal always receives the whole picture.

Deal fieldFed fromNote
Native
titlequote name (serial as fallback)create only — a deal is never re-titled
org_idbranches.organisation_id of the owner's branchvalidated first; a bad id fails the push and emits a metric
value / currencydiscounted quote total + quote currencyrecomputed each export, never stored
owner (user_id)branch RSM e-mail → local pipedrive_users registryfalls back to pipe@betacalco.com; no API call on the hot path
person_idcustomer e-mail → person search, created if missingprovisioning pre-creates one person per active user
status + lost_reasonpipedrive_status / pipedrive_lost_reasonreason on update only — Pipedrive rejects it on create, so a lost create is followed immediately by an update carrying it
stage_id + pipeline_idpipedrive_stage_id (+ that stage's own pipeline)any imported stage, cross-pipeline moves included; configured defaults on create when the quote carries none
expected_close_dateexpected-completion date from the follow-up formsent when set; clearing is not propagated
Custom — resolved through the field registry
Website Quote Numberquote serialthe pairing key for the whole sync
Deal Typederived: name contains "test" → Test; customer e-mail @betacalco.com → Internal; else Regularevaluated in that order
Deal contains alternativesany line item flagged alternative → Yes
Agent Feedback Status / Lost Reasonpipedrive_agent_feedback_*retiring — sunset with the cold-quote flow
Is OOTOOT registration approved → Yes
Beyond the deal record
done Activityone "Quote Follow-Up: SERIAL" per landing-page submission — status, stage, date, commentreplaces the former Note; no owner, so reps' stats stay clean
deal DELETEquote soft-deleted → linked deal deletedchange-logged first

Never crosses: the bid date, follow-up comments and timestamps, the stage-change stamp, and the export bookkeeping (pipedrive_update_scheduled / _attempts, last-export and last-sync dates) are local-only quote columns. The comment reaches Pipedrive only inside the Activity.

Detail lives in deal-updates.md — create vs update, how each derived value is decided, and the special cases.


Map 2 — organisation, branch, person, owner

The quote itself stores no org, person or owner. All three resolve at export time.

Quote's user ──custom_2──► Branch ──branches.organisation_id──► PD ORGANISATION
(agent / ▲ = the deal's org_id
customer) │ DEV-org guard off-live

maintained by
rep-master Google Sheet import · provisioning
(adopt-by-name or create · ids written back to the
sheet · rep codes and quota codes stamped)

customer e-mail ─────────────► PERSON search, create if missing
branch RSM ─────────────► OWNER via the local pipedrive_users registry

Off-live every write must target the sandbox org (pipedrive_org_id_dev). Blank in the OAuth sandbox means unrestricted; the constant 6034 survives for the legacy DEV setup. See provisioning.md for how the mapping is created and repaired.


Map 3 — deal → quote

The webhook's field map. Everything else on the deal is derived from the quote, so it never returns.

Deal fieldQuote columnTranslation on the way in
idpipedrive_deal_idalso linked up-front for serial-matched quotes that had no deal id yet
statuspipedrive_statusa non-lost status also clears the stored lost reason
lost_reasonpipedrive_lost_reasonraw
stage_idpipedrive_stage_idaccepted only if it is an imported stage (any pipeline); unknown → ignored; stamps pipedrive_stage_changed_on
(the stage's pipeline)pipedrive_pipeline_idderived from the accepted stage — the raw pipeline_id field is deliberately ignored
expected_close_datepipedrive_agent_feedback_completion_dateempty → NULL
Website Quote Number(nothing — refused)Detect-only. The serial is site-owned and travels outbound only. An edit in Pipedrive is refused, change-logged as quote serial (rejected), counted, and the true value is re-pushed. Still read here to link a deal that has no quote yet.
Agent Feedback Status / Lost Reasonpipedrive_agent_feedback_*option id → label · retiring

Never an echo: while the drainer applies a webhook, outbound scheduling is suppressed process-wide, so a change that arrived from Pipedrive cannot bounce straight back to Pipedrive. Webhook-sourced changes also carry no acting user anywhere they are logged.

Both directions are generated from one map, BcPipedriveConfig::webhookFieldMap(), so they cannot drift. Detail: README.md and admin-manual.md.


Map 4 — the foundation underneath

Private OAuth app. Each environment authenticates with its own credential; pipedrive_auth_mode picks API token or the OAuth app (tokens in #__bc_pipedrive_oauth_tokens, auto-refreshed with a 5-minute skew under a DB lock, needs_reauth on terminal errors). The company guard fails closed on every call. A daily watchdog (cli/cb_pipedrive_oauth_watchdog.php) exercises the refresh, checks the webhook subscription and scopes, and exits non-zero when a human is needed. → oauth-app.md

Field registry. Custom-field keys are no longer pasted hashes: a daily import fills local pipedrive_fields / pipedrive_field_options tables (company-scoped, never deleting), and every "which field means what" setting is a dropdown over that registry. Two option-id settings keep live's historical constants as a fallback — correct on live only. → field-registry.md

Provisioning. A versioned manifest declares what an account must contain: integration fields, realism fields, pipeline and stages, activity type, webhooks, settings. The backend page reports what is missing, adds it idempotently (adopt-by-name, verify after write), records the manifest version it ran against, and flags environments that are behind. A data half seeds organisations and persons, and refuses to run against production. → provisioning.md


Every action that exports

Each row sets the flag; the next minute-cron run sends the full payload. Differences are noted.

ActionWhereWhat goes out
Creating quotes — every new quote starts with a stage (its creator picks it; customers get the default)
Agent creates a quote (configurator "Add to Quote" popover, or the My Quotes "Create" row)websitenothing by itself — stage and status ride along with the first real export (first line item, rename, …)
Inside sales creates or duplicates a quote (Quote Console modal)consolesame — the copy schedules a full update as part of copying its line items
Working the quote's content
Adds / edits / copies / deletes a line item, changes a quantity, toggles the "alternative" flagwebsite + consolefull deal update (value, has-alternatives, …)
Renames the projectwebsitefull deal update — the deal keeps its original title by design
Console detail save, or save as revision (dates, managers, currency, tax, terms, …)consolefull deal update; a revision exports the new quote
Overrides pricing / discount / commission on a lineconsolefull deal update
Submits the out-of-territory formwebsitefull deal update including the Is-OOT field
Moving the deal
Agent changes status / stage / lost reason in the My Quotes deal controlswebsitefull deal update — native status and stage move together; a won-flagged stage wins the deal; a lost create is followed by the reason as an update
Inside sales edits the same three on the console detailconsolefull deal update, same semantics
Landing-page submission (from the emailed follow-up link)no-login pagefull deal update plus one done Activity (status, stage, date, comment); won-stage wins, non-won reopens, lost carries the reason (free text for "Other")
"Nothing has changed" confirmation on that pageno-login pagenothing — deliberately touches no deal, only the local timestamps
Ending a quote
Deletes a quote (My Quotes or the opportunity page)website + consoledeal DELETE, change-logged first
Never an exporter
Anything arriving via the inbound webhookPipedrivenothing — scheduling suppressed while it applies

Failure behaviour: a failed push keeps the quote flagged and counts an attempt; after 3 it waits for the next website change, which resets the counter. In tests, capture mode intercepts every write before the org guard and records it to JSONL — nothing reaches Pipedrive.

The authoritative list of triggers, with the code paths, is deal-updates.md.


Key files

outbound payload buildermodels/bcquotes.php
webhook receiver + drainermodels/bcpipedrive.php
the two-way field mapBcPipedriveConfig::webhookFieldMap()
auth chokepoint & company guardsystem_overrides/BcPipedriveAuth.php
outbound croncli/cb_pipedrive_update_deals.php