Beta-Calco · Pipedrive integration · engineering reference · 2026-08-24 · private-app setup

Pipedrive Data Map

Every website quote is mirrored as a Pipedrive deal. This page maps each datum that crosses — outbound, quote → deal and inbound, deal → quote — the org/branch/person plumbing around it, the machinery that keeps both sides honest, and (at the bottom) every user story that triggers an export.

Outbound — quote → deal

flag → minute cron → full payload

Any qualifying change sets one flag (pipedrive_update_scheduled). A minute cron (cb_pipedrive_update_deals.php) sends the complete current picture — batch 20, 3 attempts, then parked until the next website change resets the counter.

Inbound — deal → quote

webhook → queue → minute drainer

Pipedrive posts deal changes to a Basic-auth endpoint; they land in a DB queue and a minute cron applies them field-by-field. While applying, outbound scheduling is suppressed — the echo-loop guard.

Both directions pass one authentication chokepoint, BcPipedriveAuth: per-environment credentials (API token or the private OAuth app, switched by the pipedrive_auth_mode setting, 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).

The quote pipedrive_* columns: deal id · status · lost reason · stage + pipeline · close date · flags any qualifying change sets the flag cb_pipedrive_update_deals every minute · batch 20 · lock · 3 attempts, reset by the next change add·update·delete deal + Activity BcPipedriveAuth: token|oauth · company guard · dev-org guard Pipedrive deal (native + custom fields) organisation ← branch person ← customer e-mail owner ← branch RSM done Activity per follow-up pipelines · stages · users · fields + options · organisations ↓ imported daily 06:00–06:15 Local registries pipelines · stages · users · fields · orgs webhook (Basic auth) webhook queue → drainer every minute · per-item isolation · 3 attempts writes the quote's pipedrive_* columns — outbound scheduling SUPPRESSED while applying
The loop. Outbound is a flag drained by a minute cron sending the full payload; inbound is a queued webhook drained by its own minute cron. The dashed line is the shared auth chokepoint. Daily imports (teal, top) fill the local registries — pipelines, stages, users, fields, organisations — that the outbound payload and the settings dropdowns read instead of calling the API.
MAP 1

Quote → deal: every field that crosses

built fresh on every export — the deal always gets the whole picture
Deal fieldFed fromNotes
Native deal fields
titlequote name (serial as fallback)create only — a deal is never re-titled
org_idbranches.organisation_id of the owner's branchvalidated against the account first; bad id = failed push + metric
value / currencydiscounted quote total + quote currencyrecomputed each export, never stored
owner (user_id)branch RSM e-mail → local pipedrive_users registryfallback 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 (+ the 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 deal fields — resolved through the field registry (settings are dropdowns over imported fields, company-scoped)
Website Quote Numberquote serialthe pairing key for the whole sync
Deal Typederived: name contains “test” → Test; customer e-mail @betacalco.com → Internal; else Regulartest → internal → regular, in that order
Deal contains alternativesany line item flagged alternative → Yes
Agent Feedback Status / Lost Reasonpipedrive_agent_feedback_*retiring the agent’s own read; being sunset with the cold-quote flow
Is OOTOOT registration approved → Yes
Beyond the deal record
done Activityone “Quote Follow-Up: SERIAL” activity per landing-page submission — status, stage, date, commentreplaces the former Note; no owner, so reps’ stats stay clean
deal DELETEquote soft-deleted → the linked deal is deletedlogged in the change log first
Never crosses: the bid date, follow-up comments/timestamps, the stage-change stamp, the export bookkeeping (pipedrive_update_scheduled/_attempts, last-export/last-sync dates) — all local-only quote columns; the comment reaches Pipedrive only inside the Activity.
MAP 2

Organisations, branches, persons, owner

who the deal belongs to
Quote’s user the agent / customer Branch branches.organisation_id PD Organisation deal’s org_id · DEV org guard off-live custom_2 Maintained by rep-master Google Sheet import · provisioning (adopt-by-name or create, ids written back to the sheet, rep codes stamped) Person customer e-mail → search, create if missing Owner (deal user) branch RSM e-mail → local users registry
The quote itself stores no org/person/owner — all three resolve at export time from the user, the branch and the local registries. Off-live, every write must target the sandbox org (pipedrive_org_id_dev; blank in the OAuth sandbox = unrestricted, the constant 6034 survives for the legacy DEV setup).
MAP 3

Deal → quote: what flows back

the webhook’s field map — everything else on the deal is derived, 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
(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 Numberserialthe pairing key
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 — a change that arrived from Pipedrive can’t bounce straight back to Pipedrive. And webhook-sourced changes carry no acting user anywhere they’re logged.
MAP 4

The foundation underneath

private app, field registry, provisioning — the new setup in three sentences each

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-refresh with a 5-minute skew under a DB lock, needs_reauth on terminal errors). The company guard fails closed on every call when the credential’s company ≠ the declared pipedrive_company_id; a daily watchdog (cb_pipedrive_oauth_watchdog.php) exercises the refresh, checks the webhook subscription and scopes, and exits non-zero when a human is needed.

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 fallback — correct on live only.

Provisioning. A versioned manifest (v4) declares what an account must contain — integration fields, realism fields, pipeline + stages, activity type, webhooks, settings. The backend page reports what’s 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.

DETAIL

Every user story that exports to Pipedrive

each row sets the flag; the next minute-cron run sends the full payload (differences noted)
User storyWhereWhat 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 yet by itself — the stage/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 / 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 incl. the Is-OOT field
Moving the deal — status, stage, lost reason
Agent changes status / stage / lost reason in the My Quotes deal controlswebsitefull deal update — native status + stage move together (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 + 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 (the echo-loop guard)
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.