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 field | Fed from | Note |
|---|---|---|
| Native | ||
title | quote name (serial as fallback) | create only — a deal is never re-titled |
org_id | branches.organisation_id of the owner's branch | validated first; a bad id fails the push and emits a metric |
value / currency | discounted quote total + quote currency | recomputed each export, never stored |
owner (user_id) | branch RSM e-mail → local pipedrive_users registry | falls back to pipe@betacalco.com; no API call on the hot path |
person_id | customer e-mail → person search, created if missing | provisioning pre-creates one person per active user |
status + lost_reason | pipedrive_status / pipedrive_lost_reason | reason on update only — Pipedrive rejects it on create, so a lost create is followed immediately by an update carrying it |
stage_id + pipeline_id | pipedrive_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_date | expected-completion date from the follow-up form | sent when set; clearing is not propagated |
| Custom — resolved through the field registry | ||
| Website Quote Number | quote serial | the pairing key for the whole sync |
| Deal Type | derived: name contains "test" → Test; customer e-mail @betacalco.com → Internal; else Regular | evaluated in that order |
| Deal contains alternatives | any line item flagged alternative → Yes | |
| Agent Feedback Status / Lost Reason | pipedrive_agent_feedback_* | retiring — sunset with the cold-quote flow |
| Is OOT | OOT registration approved → Yes | |
| Beyond the deal record | ||
| done Activity | one "Quote Follow-Up: SERIAL" per landing-page submission — status, stage, date, comment | replaces the former Note; no owner, so reps' stats stay clean |
| deal DELETE | quote soft-deleted → linked deal deleted | change-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 field | Quote column | Translation on the way in |
|---|---|---|
id | pipedrive_deal_id | also linked up-front for serial-matched quotes that had no deal id yet |
status | pipedrive_status | a non-lost status also clears the stored lost reason |
lost_reason | pipedrive_lost_reason | raw |
stage_id | pipedrive_stage_id | accepted only if it is an imported stage (any pipeline); unknown → ignored; stamps pipedrive_stage_changed_on |
| (the stage's pipeline) | pipedrive_pipeline_id | derived from the accepted stage — the raw pipeline_id field is deliberately ignored |
expected_close_date | pipedrive_agent_feedback_completion_date | empty → 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 Reason | pipedrive_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.
| Action | Where | What 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) | website | nothing 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) | console | same — 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" flag | website + console | full deal update (value, has-alternatives, …) |
| Renames the project | website | full deal update — the deal keeps its original title by design |
| Console detail save, or save as revision (dates, managers, currency, tax, terms, …) | console | full deal update; a revision exports the new quote |
| Overrides pricing / discount / commission on a line | console | full deal update |
| Submits the out-of-territory form | website | full deal update including the Is-OOT field |
| Moving the deal | ||
| Agent changes status / stage / lost reason in the My Quotes deal controls | website | full 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 detail | console | full deal update, same semantics |
| Landing-page submission (from the emailed follow-up link) | no-login page | full 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 page | no-login page | nothing — deliberately touches no deal, only the local timestamps |
| Ending a quote | ||
| Deletes a quote (My Quotes or the opportunity page) | website + console | deal DELETE, change-logged first |
| Never an exporter | ||
| Anything arriving via the inbound webhook | Pipedrive | nothing — 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 builder | models/bcquotes.php |
| webhook receiver + drainer | models/bcpipedrive.php |
| the two-way field map | BcPipedriveConfig::webhookFieldMap() |
| auth chokepoint & company guard | system_overrides/BcPipedriveAuth.php |
| outbound cron | cli/cb_pipedrive_update_deals.php |