Skip to main content

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/_password blank it refuses every call. Set them and re-register the webhook as one operation (§4); doing only the first stops inbound sync with 401s.
  • 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 companypipedrive_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 registrycb_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_TEST on live, one outbound cron (§8).
  • 9. Run the connection watchdogcb_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=1 on live (it deletes & recreates the deal fields — see §3); never set PIPEDRIVE_TEST=capture on live (it silently stops all real syncing — see §8); and never change pipedrive_company_id without 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=1 if 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:

SettingSet it to
pipedrive_auth_modetoken (a personal API token) or oauth (the registered app)
pipedrive_company_idthe Pipedrive company id that credential belongs to
pipedrive_org_id_devnon-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

SettingSet toRequired?
Pipedrive API Tokenthe account API tokenYes
Pipedrive webhook HTTP auth usernamea value unique to this environmentRecommended (§4)
Pipedrive webhook HTTP auth passworda value unique to this environmentRecommended (§4)
Pipedrive Pipeline ID for follow-up stagesthe numeric id of the pipeline whose stages the follow-up page offersYes for the stage picker (§3.5)
Pipedrive field key for is OOT fieldthe Pipedrive Is-OOT deal-field keyYes — not auto-created
Pipedrive option ID for is OOT → yesthat field's "yes" option idYes
Pipedrive option ID for is OOT → nothat field's "no" option idYes

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

SettingSet 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 secreta 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)

SettingSet to
Cold Quotes Report Sheetthe 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=1 on 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=1 runs 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:

  1. Set Pipedrive Pipeline ID for follow-up stages (pipedrive_pipeline_id) in §2.
  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
  3. 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:

  1. Set both webhook auth fields in §2 (unique per environment).
  2. 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 401 and 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.php and cb_pipedrive_sync_quote_data.php were merged into cb_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_export gets stamped and pipedrive_update_scheduled returns to 0 (cb_pipedrive_test_support.php quote-state <serial>), and the custom_pipedrive_exports log shows the push.
  • Inbound: edit that deal in Pipedrive. Within ~1 minute the change reflects on the quote. Verify: the custom_pipedrive_sync_quote_data log shows a queued + processed webhook, and no rows are stuck status='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 migration 0.5.46's menu item.)

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:

  1. 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.
  2. Put the same value in tests/.env as TEST_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_TEST warning. 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 leaving PIPEDRIVE_TEST set in the server env/cron.

When done testing, blank test_support_secret again so the endpoint returns to disabled.


8. Confirm the safety flags (before you walk away)

  • PIPEDRIVE_TEST is NOT set on the live server (any value, especially capture) — 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_id matches the account the credential belongs to (§1.5).
  • test_support_secret is 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.