Post-deployment playbook — Pipedrive integration + Quote Follow-Up (go-live runbook)
Audience: administrators & operators doing the deploy · Scope: the exact, ordered steps to run after the code is deployed to an environment — migrations, backend settings, the setup CLI, cron jobs, webhook security, and a smoke test — for the Pipedrive integration and the Quote Follow-Up landing page (they ship together) · Last reviewed: 2026-07-03
Use this once per environment, right after deploying the code. It is the linear go-live checklist; for the "why" and for day-to-day operation/troubleshooting, see the reference manual admin-manual.md and the architecture doc README.md.
Moving onto the private app (OAuth)? That is a different job with a different order — see go-live-private-app.md. This runbook covers standing the quote↔deal integration up in the first place, and assumes token authentication throughout.
The checklist (do these in order)
- 0a. BEFORE the deploy, on live: set the webhook credentials. The inbound endpoint is now
fail-closed on live — with
pipedrive_webhook_auth_user/_passwordblank it refuses every call. Set them and re-register the webhook as one operation (§4); doing only the first stops inbound sync with401s. - 0. Prerequisites — you have the Pipedrive credential (API token, or the app's client
id/secret), the Pipedrive company id the environment may talk to, the "Quote number"
deal-field key, and know the environment you're deploying to (
live/staging/dev). - 1. Migrations applied — hit any page once, then verify the schema (§1).
- 1.5 Confirm the credential and the company —
pipedrive_auth_mode,pipedrive_company_id,pipedrive_org_id_dev(§1.5). Every call is refused until the company is declared. - 2. Backend settings — credential, webhook auth + URL, the Is-OOT field/options, the follow-up pipeline id, token lifetime, cold-quotes sheet (§2).
- 1.6 Import the field registry —
cb_pipedrive_import_fields.php --adopt-legacy=1, then read its config audit (§1.6). - 3. Run the setup CLI — creates/reuses the deal fields, filters, and webhook (§3).
- 3.5 Import follow-up stages — set the pipeline id, import, curate the stage UI (§3.5).
- 4. Secure the webhook — set the credentials and re-register (§4).
- 5. Install cron jobs — the two every-minute runners + cold-quote jobs, the optional pipelines/stages refreshes, and (if a log sheet is configured) the stage-change-log mirror; remove the deprecated runners (§5).
- 6. Smoke test — outbound, inbound, and the landing page (§6).
- 7. (Optional) Run the E2E suite — automated verification as the test account, capture mode (§7).
- 8. Confirm the safety flags — no
PIPEDRIVE_TESTon live, one outbound cron (§8). - 9. Run the connection watchdog —
cb_pipedrive_oauth_watchdog.php; it exits 1 if anything needs a human, and it is the fastest confirmation that the credential, scopes and webhook are all actually in place.
⚠️ Three things that will bite you if you skip them: never run setup with
--replace-setup=1on live (it deletes & recreates the deal fields — see §3); never setPIPEDRIVE_TEST=captureon live (it silently stops all real syncing — see §8); and never changepipedrive_company_idwithout changing the credential in the same breath, or the guard refuses every call (which is the point — see §1.5).
Setup writes to THIS environment's settings row only. It used to write every row, on the reasoning that setup values are account-wide — true only while every environment shared one Pipedrive account. Use
--all-environments=1if you really want the old behaviour.
1. Confirm migrations applied
Schema/settings changes apply automatically on the next page load (see migrations.md).
Load any front-end page once, then verify (local prefix e5xae_; adjust for the live prefix):
# Quote Follow-Up columns (0.5.44) + comment-note queue (0.5.50)
mysql -u… -p DB -e "SHOW COLUMNS FROM e5xae_configbox_external_user_quotes LIKE 'pipedrive_agent_feedback_%';"
# Unified outbound flag (0.5.49) + retry counter (0.5.52)
mysql -u… -p DB -e "SHOW COLUMNS FROM e5xae_configbox_external_user_quotes LIKE 'pipedrive_update_%';"
# Pipedrive settings extension table (0.5.56) — webhook auth (0.5.48) + Project Phase key (0.5.50)
# now live HERE, not in external_settings:
mysql -u… -p DB -e "SHOW TABLES LIKE 'e5xae_configbox_external_settings_pipedrive';"
mysql -u… -p DB -e "SHOW COLUMNS FROM e5xae_configbox_external_settings_pipedrive LIKE 'pipedrive_webhook_auth%';"
mysql -u… -p DB -e "SHOW COLUMNS FROM e5xae_configbox_external_settings_pipedrive LIKE 'pipedrive_auth_mode';"
mysql -u… -p DB -e "SHOW COLUMNS FROM e5xae_configbox_external_settings_pipedrive LIKE 'pipedrive_company_id';"
# Follow-up native-stage sync: pipeline-id setting (0.5.63), stages table (0.5.64), quote stage id (0.5.65)
mysql -u… -p DB -e "SHOW COLUMNS FROM e5xae_configbox_external_settings_pipedrive LIKE 'pipedrive_pipeline_id';"
mysql -u… -p DB -e "SHOW TABLES LIKE 'e5xae_configbox_external_pipedrive_stages';"
mysql -u… -p DB -e "SHOW COLUMNS FROM e5xae_configbox_external_user_quotes LIKE 'pipedrive_stage_id';"
# Quote Follow-Up settings extension table (0.5.69): token lifetime (0.5.53) + the two quote-stage-sync
# settings now live here, moved out of external_settings / _pipedrive:
mysql -u… -p DB -e "SHOW COLUMNS FROM e5xae_configbox_external_settings_quote_follow_up;"
# 0.5.69 DROPS them from their old homes — these should now return nothing:
mysql -u… -p DB -e "SHOW COLUMNS FROM e5xae_configbox_external_settings LIKE 'quote_followup_token_lifetime_days';"
mysql -u… -p DB -e "SHOW COLUMNS FROM e5xae_configbox_external_settings_pipedrive LIKE 'quote_stage_sync_sheet';"
# 0.5.56 also DROPS the pipedrive_ columns from external_settings — this should now return nothing:
mysql -u… -p DB -e "SHOW COLUMNS FROM e5xae_configbox_external_settings LIKE 'pipedrive_%';"
# Inbound webhook queue table (0.5.51) + change-log table (0.5.55)
mysql -u… -p DB -e "SHOW TABLES LIKE 'e5xae_configbox_external_pipedrive_webhook_queue';"
mysql -u… -p DB -e "SHOW TABLES LIKE 'e5xae_configbox_external_pipedrive_change_log';"
# 0.5.55 also DROPS the old diag toggles — this should now return nothing:
mysql -u… -p DB -e "SHOW COLUMNS FROM e5xae_configbox_external_settings LIKE 'pipedrive_diag_log_%';"
The relevant migrations for this deploy: 0.5.44 (follow-up fields), 0.5.45 (stage default +
backfill), 0.5.46 (follow-up SEF menu item), 0.5.48 (webhook HTTP Basic auth), 0.5.49
(unified outbound queue), 0.5.50 (Project Phase key + comment queue), 0.5.51 (inbound webhook
queue table), 0.5.52 (bounded retries + diag toggles), 0.5.53 (follow-up token lifetime),
0.5.54 (test-support endpoint secret), 0.5.55 (change-log table; drops the diag toggles),
0.5.56 (moves the 12 Pipedrive settings columns into the #__configbox_external_settings_pipedrive
extension table; drops them from external_settings), 0.5.63 (follow-up pipeline-id setting),
0.5.64 (imported follow-up stages table), 0.5.65 (native pipedrive_stage_id quote column).
If a migration didn't run, check the ConfigBox update log. The follow-up project stage now uses the deal's native
stage_id; configure the pipeline and import its stages in §3.5 (the legacy free-text "Project Phase" field is no longer used).
1.5 Confirm the credential and the company
Two settings decide who this environment talks to, and nothing works until they agree:
| Setting | Set it to |
|---|---|
pipedrive_auth_mode | token (a personal API token) or oauth (the registered app) |
pipedrive_company_id | the Pipedrive company id that credential belongs to |
pipedrive_org_id_dev | non-live only: the organisation writes are confined to. Blank = unrestricted |
BcPipedriveAuth compares every resolved credential against pipedrive_company_id — on reads as
well as writes — and refuses on a mismatch:
Refused to talk to Pipedrive company 20731130 from the "dev" environment,
which is configured for company 1638657. Nothing was sent.
An unset company is refused just as loudly. Find the id with tools/pd me, or on the Imported
Pipedrive Fields list. Full detail: oauth-app.md.
1.6 Import the field registry
php docroot/cli/cb_pipedrive_import_fields.php --adopt-legacy=1
This imports every custom field and option of the connected account, and makes this environment's implicit configuration explicit. Read what it prints — the trailing warnings are the config audit, one line per configured field this account does not have, plus one per field left blank (which is legitimate, but a silently skipped field is exactly what nobody notices for years).
--adopt-legacy only fills blanks, and refuses to adopt a value the connected account does not have.
See field-registry.md.
2. Backend settings
ConfigBox backend → Settings → the collapsible groups below. Settings are per environment — set them on the row for the environment you're deploying. (Field keys and filter IDs are written by the setup CLI in §3; leave those blank here.)
Group: Pipedrive Integration
| Setting | Set to | Required? |
|---|---|---|
| Pipedrive API Token | the account API token | Yes |
| Pipedrive webhook HTTP auth username | a value unique to this environment | Recommended (§4) |
| Pipedrive webhook HTTP auth password | a value unique to this environment | Recommended (§4) |
| Pipedrive Pipeline ID for follow-up stages | the numeric id of the pipeline whose stages the follow-up page offers | Yes for the stage picker (§3.5) |
| Pipedrive field key for is OOT field | the Pipedrive Is-OOT deal-field key | Yes — not auto-created |
| Pipedrive option ID for is OOT → yes | that field's "yes" option id | Yes |
| Pipedrive option ID for is OOT → no | that field's "no" option id | Yes |
The Is-OOT field is not created by setup. Unlike the three fields in §3, the OOT field must already exist in Pipedrive; you paste its key + option ids here by hand.
The same group also holds the three Pipedrive app OAuth settings (client ID, client secret, callback URL). They are not part of this checklist — no flow uses them, and the sync works without them. They belong to the separately registered Developer Hub app; see oauth-app.md.
Group: Quote Follow-Up Landing Page
| Setting | Set to |
|---|---|
| Follow-up link token lifetime (days) | default 5 (0 = never expires) — change only if you want a different link validity |
| Follow-up link API secret | a long random string to require auth on the Link API (external automation, e.g. n8n, fetching follow-up links). Blank = the API is open (unauthenticated, fail-open). See quote-follow-up/authentication.md → Link API. |
Group: Cold Quotes (only if using the cold-quote nudges)
| Setting | Set to |
|---|---|
| Cold Quotes Report Sheet | the Google Sheet the cold-quote job writes to |
3. Run the setup CLI
Creates-or-reuses the Pipedrive artifacts and writes their keys to every environment settings row (they're account-wide):
PIPEDRIVE_API_TOKEN=<token> php docroot/cli/cb_pipedrive_setup_integration.php \
--field-key-quote-serial=<key> \
--replace-setup=0 --replace-data=0
What it does (all find-or-create by name — reused if they already exist):
- 2 custom deal fields: Agent Feedback Quote Status (enum open/won/lost), Agent Feedback Lost
Reason (text). (The old free-text Project Phase field is no longer created — the follow-up stage
now uses the deal's native
stage_id; see §3.5.) - 2 deal filters: "API: Deals with a website quote number", "API: Cold Quotes".
- 1 deal webhook (all actions) →
https://betacalco.com/pipedrive-quote-updates/updateQuote.
⚠️ Never
--replace-setup=1on live unless you truly mean it — that deletes and recreates the deal fields (losing their data/history) and the filters/webhook. Normal runs use--replace-setup=0.--replace-data=1runs the destructive quote backfill — also off for a normal deploy.
Verify the field keys landed:
php docroot/cli/cb_pipedrive_test_support.php field-keys # prints the resolved keys (and pipelineId) as JSON
3.5 Import the follow-up pipeline stages
The Quote Follow-Up stage picker is the live stages of a configured Pipedrive pipeline (picking a
stage moves the deal along it via native stage_id). One-time setup per environment:
- Set Pipedrive Pipeline ID for follow-up stages (
pipedrive_pipeline_id) in §2. - Import the stages — either the backend button (ConfigBox settings → Quote Follow-ups → Imported
Pipedrive Stages → Import pipelines & stages from Pipedrive, which imports both) or the CLI:
php docroot/cli/cb_pipedrive_import_stages.php
- Curate each stage's editable settings once: Hint, tick Marks quote won on the "won" stage, set the Nudge days cadence, and untick Visible to agents for any stage to hide. Re-import preserves these; it only refreshes the Pipedrive-owned fields (which are read-only in the admin).
Until a pipeline is set and stages imported, the picker is empty and the stage is silently not pushed (status / expected date / comment note are unaffected). Verify:
mysql -u… -p DB -e "SELECT pipedrive_stage_id, name, order_nr, published, is_won_stage FROM e5xae_configbox_external_pipedrive_stages ORDER BY order_nr;"
4. Secure the webhook
On live the endpoint is fail-closed: with both credentials blank it refuses every call, so on
production this is a pre-deploy step, not a post-deploy one. On dev and staging it stays
fail-open, which is what keeps local testing painless.
Also set pipedrive_webhook_url for this environment — the subscription URL used to be hardcoded
to betacalco.com, so setup could never register a webhook anywhere else. Blank makes setup refuse
rather than guess. New environments should use /cb-api/bcpipedrive/updateQuote, which resolves from
code rather than from a per-database menu row.
To lock it:
- Set both webhook auth fields in §2 (unique per environment).
- Re-register so Pipedrive sends the credentials — this needs
--replace-setup=1(otherwise setup sees the webhook already exists and won't touch it):PIPEDRIVE_API_TOKEN=<token> php docroot/cli/cb_pipedrive_setup_integration.php \--field-key-quote-serial=<key> --replace-setup=1 --replace-data=0
⚠️ Do steps 1 and 2 together. If you set the credentials but don't re-register, the site starts rejecting Pipedrive's calls with
401and inbound sync stops (Pipedrive is still calling with no credentials). Full detail: pipedrive-admin-manual.md §2.6.
5. Install cron jobs
Two runners must run every minute. Without them, sync silently stops.
* * * * * php /path/to/docroot/cli/cb_pipedrive_update_deals.php # outbound: quotes → deals
* * * * * php /path/to/docroot/cli/cb_pipedrive_process_webhook_queue.php # inbound: drain webhook queue
# Imported Pipedrive pipelines + stages — optional refresh (or use the backend buttons on demand)
0 6 * * * php /path/to/docroot/cli/cb_pipedrive_import_pipelines.php
0 6 * * * php /path/to/docroot/cli/cb_pipedrive_import_stages.php
# Quote stage-change log → Google Sheet — ADD ONLY IF a log sheet is configured (recording is always-on
# and synchronous; this only drains rows to the sheet and no-ops when no sheet is set)
*/5 * * * * php /path/to/docroot/cli/cb_quote_stage_sync_export_sheet.php
# Follow-up NUDGE sheet — ADD ONLY IF a nudge sheet is configured. Rebuilds/reconciles the "quotes due for
# a follow-up nudge" sheet (an n8n workflow reads it). Daily is typical; safe to run hourly. No-ops when no
# nudge sheet is set. See docs/quote-follow-up/nudge-cadence.md.
15 6 * * * php /path/to/docroot/cli/cb_quote_nudge_build_sheet.php
# Cold-quote nudges — not every-minute; pick a cadence (e.g. daily)
0 13 * * * php /path/to/docroot/cli/cb_pipedrive_send_cold_quote_prompts.php
0 13 * * * php /path/to/docroot/cli/cb_pipedrive_update_cold_quote_sheet.php
Remove the deprecated outbound runners if this environment had them:
cb_pipedrive_export_scheduled.phpandcb_pipedrive_sync_quote_data.phpwere merged intocb_pipedrive_update_deals.php. Keep only one outbound entry so the queue isn't processed twice.
6. Smoke test
- Outbound: edit a quote (add/price a line, or set an agent status). Within ~1 minute a deal is
created/updated in Pipedrive. Verify: the quote's
pipedrive_date_last_exportgets stamped andpipedrive_update_scheduledreturns to0(cb_pipedrive_test_support.php quote-state <serial>), and thecustom_pipedrive_exportslog shows the push. - Inbound: edit that deal in Pipedrive. Within ~1 minute the change reflects on the quote.
Verify: the
custom_pipedrive_sync_quote_datalog shows a queued + processed webhook, and no rows are stuckstatus='failed'in…_pipedrive_webhook_queue. - Landing page: the SEF URL resolves — open
/quote-follow-up/<a real serial>while logged in as that quote's agent (or via a minted link). The page loads and a submit saves. (The clean URL depends on migration0.5.46's menu item.)
7. Run the E2E suite (optional but recommended — the automated version of §6)
Playwright drives real UI actions as the automation test account
(test-automation-agent@betacalco.com, whose quotes/deals are confined to the bogus Pipedrive org, so
it's safe here) and asserts the outbound deal payloads (in capture mode — nothing is sent) plus
the landing-page/auth behaviour. Full detail: tests/README.md (tests/README.md).
Runs from any machine (e.g. your laptop) — no checkout on the server. The browser drives
TARGET/BASE_URL, and the DB-backed steps call the authenticated test-support endpoint on the
same environment. Enable that endpoint first:
- Set
test_support_secret(Backend → Settings → Functional Test Harness) to a strong, environment-unique value. Blank = disabled (fail-closed); leave it blank when not testing. - Put the same value in
tests/.envasTEST_SUPPORT_SECRET.
One-time (on the machine you run from):
cd tests
npm install
npm run install:browsers
cp .env.example .env # set E2E_AGENT_PASS (test account password) + TEST_SUPPORT_SECRET
Run:
TARGET=live npm test # whole suite vs https://betacalco.com (dev/staging: TARGET=dev|staging)
npm run test:quote-followup-status # just the anon quote-follow-up "Lost" → deal-payload path
npm run report # open the HTML report
What it does safely:
- Acts as the test account only; seeds a realistic quote via the configurator and deletes it in teardown.
- Runs the deal-update flow in capture mode — payloads are asserted, never sent to Pipedrive.
- Capture-mode processing does stamp the test account's quote rows (a synthetic deal id) before teardown removes them — fine for this disposable account, which is exactly why the suite never runs as a real user.
This is not the §8
PIPEDRIVE_TESTwarning. Capture mode lives entirely inside the endpoint's per-request handling; it is never set in the server env or cron, so it does not affect live syncing. What §8 forbids is leavingPIPEDRIVE_TESTset in the server env/cron.
When done testing, blank
test_support_secretagain so the endpoint returns to disabled.
8. Confirm the safety flags (before you walk away)
-
PIPEDRIVE_TESTis NOT set on the live server (any value, especiallycapture) — it would stop real syncing. It's a testing-only switch (testing.md). - Exactly one outbound cron entry (
cb_pipedrive_update_deals.php) — not the old two. - Webhook auth is set on live and re-registered (§4). Blank on live now means the endpoint refuses everything — it is no longer a valid "leave it open" choice there.
-
pipedrive_company_idmatches the account the credential belongs to (§1.5). -
test_support_secretis blank (Functional Test Harness) unless you're actively running the E2E suite — a set secret leaves the test-support endpoint open to holders of that secret.
9. Run the connection watchdog
php docroot/cli/cb_pipedrive_oauth_watchdog.php
The fastest single confirmation that the credential, the scopes and the webhook are all actually in place. It exits 1 when something needs a human and names it:
PROBLEM: No Pipedrive webhook is registered for https://…/cb-api/bcpipedrive/updateQuote.
Inbound sync is silently doing nothing; re-run the setup CLI to register it.
Install it as a daily cron (scheduled-jobs.md) — it is also what keeps an app installation's refresh token alive on an environment that is otherwise quiet.
If something's wrong
Go to the symptom → fix runbook in pipedrive-admin-manual.md §4 (outbound stopped, inbound not reflecting, a stuck quote, stage not showing, wrong-org writes). Query the always-on change log (§5 there) to see exactly which fields synced in each direction and when.
Related docs
- admin-manual.md — full operator reference (settings, data maps, troubleshooting)
- README.md — architecture, the four flows, deploy detail
- quote-follow-up/landing-page.md — the landing page feature (states, auth, tokens)
- migrations.md — how the schema changes above apply and are tracked