Beta-Calco · Pipedrive integration · engineering reference · 2026-08-24 · private-app setup
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).
| Deal field | Fed from | Notes |
|---|---|---|
| Native deal fields | ||
| 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 against the account first; bad id = failed push + metric |
| value / currency | discounted quote total + quote currency | recomputed each export, never stored |
| owner (user_id) | branch RSM e-mail → local pipedrive_users registry | fallback 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 (+ the 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 deal fields — resolved through the field registry (settings are dropdowns over imported fields, company-scoped) | ||
| 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 | test → internal → regular, in that order |
| Deal contains alternatives | any line item flagged alternative → Yes | |
| Agent Feedback Status / Lost Reason | pipedrive_agent_feedback_* | retiring the agent’s own read; being sunset with the cold-quote flow |
| Is OOT | OOT registration approved → Yes | |
| Beyond the deal record | ||
| done Activity | one “Quote Follow-Up: SERIAL” activity per landing-page submission — status, stage, date, comment | replaces the former Note; no owner, so reps’ stats stay clean |
| deal DELETE | quote soft-deleted → the linked deal is deleted | logged in the change log first |
pipedrive_update_scheduled/_attempts, last-export/last-sync dates) — all
local-only quote columns; the comment reaches Pipedrive only inside the Activity.pipedrive_org_id_dev; blank in the OAuth sandbox = unrestricted, the constant 6034 survives
for the legacy DEV setup).| 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 |
| (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 | serial | the pairing key |
| Agent Feedback Status / Lost Reason | pipedrive_agent_feedback_* | option id → label · retiring |
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.
| User story | 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 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) | 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 / 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 incl. the Is-OOT field |
| Moving the deal — status, stage, lost reason | ||
| Agent changes status / stage / lost reason in the My Quotes deal controls | website | full 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 detail | console | full deal update, same semantics |
| Landing-page submission (from the emailed follow-up link) | no-login page | full 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 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 (the echo-loop guard) |