Going live on the private app — runbook
Audience: developers & ops · Scope: the sequenced runbook for moving live from the personal API token to the private OAuth app · Last reviewed: 2026-08-14
Part of Pipedrive. Related: oauth-app.md (how the credential layer works), provisioning.md (the page that reports and repairs an account), deployment.md (the original quote↔deal go-live runbook — this one assumes that already happened), field-registry.md, testing.md.
Moving live from one personal API token to a private Pipedrive app (OAuth), without a window in which quotes stop syncing.
What actually changes
Only how the site authenticates. Not what it sends, not the field keys, not the webhook payloads.
Every call still goes to the same company; BcPipedriveAuth just hands the HTTP client a bearer
token instead of x-api-token.
That is what makes this safe to stage: pipedrive_auth_mode is a per-environment setting, so the
switch and the rollback are both a settings change with no deploy.
Two consequences that are not cosmetic, and are the reason for the install rules in §3:
- Attribution moves to the installing user. Deals, notes and activities created by the site will
read as that person. Install as
pipe@betacalco.com— the same identity the token belongs to — or months of reps' activity statistics get quietly contaminated. - Per-user permissions bound the app. If the installer cannot see a deal, neither can the site.
searchDeals()returning nothing does not fail loudly — it makes the export take the create path and duplicate a deal on every push. The installer must be an admin.
Before you start: the honest risk
OAuth has never been exercised end to end anywhere. As of 2026-08-14 all three environments are
still token mode and #__configbox_external_pipedrive_oauth_tokens is empty. The code is written, reviewed and
unit-covered; the handshake, the refresh, the uninstall callback and the watchdog have never run
against a real Pipedrive app.
Do §0 before anything else. It costs an hour and it is the difference between finding a bug on the sandbox and finding it on production.
§0. Prove it on dev first (do not skip)
The sandbox was emptied on 2026-08-14 precisely so this can be done from nothing.
- In the sandbox's Developer Hub, finish the dev app
CBX-BetaCalco(client idf138ad047ebea735): set the Installation URL tohttps://pipedrive-app.dev.betacalco.com/cb-api/bcpipedriveoauth/installand confirm the callback URL is…/cb-api/bcpipedriveoauth/callback. Draft is fine — "Install & test" works from draft. - On the worktree site, set
pipedrive_oauth_client_id,pipedrive_oauth_client_secretandpipedrive_oauth_redirect_urion the dev row. The secret is in.claude/secrets/credentials.env. - Visit
/cb-api/bcpipedriveoauth/install, complete the handshake, and confirm a row appears in#__configbox_external_pipedrive_oauth_tokenswithstatus = 'active', the rightcompany_id, and ascopecovering the eight the integration needs. - Flip dev's
pipedrive_auth_modetooauth. - Provision the empty sandbox from the page — this now exercises every write path under OAuth in one go. Then push a real quote end to end and change its stage in Pipedrive to test the inbound webhook.
- Force a refresh (or wait one out) and confirm the token rotates rather than 401ing.
- Uninstall the dev app; confirm the row flips to
uninstalledand the next sync throws instead of silently doing nothing. Re-install.
Gate: do not proceed to live until §0.3 through §0.7 have all been seen working.
§1. Prerequisites
| Check | |
|---|---|
| Branch merged | worktree-pipedrive-app reviewed and merged to master |
| Migrations | 0.5.90–0.5.103 are numbered against the main checkout's 0.5.87 — re-check nothing else claimed those numbers before merging |
| Known issue #37 | 18 admin controllers are still anonymous over /cb-api/…. Not a blocker for this change, but the callback lives on the same route family — see _known-issues.md #37 |
| Live account audit | Run the provisioning page on live in report mode (it changes nothing) and read it. This is now the best pre-flight there is: it will tell you if a field key in live's settings no longer resolves |
§2. Register the LIVE app
An app has one callback URL, so live needs its own app — CBX-BetaCalco is the dev app and must
stay pointed at the worktree.
- Sandbox Developer Hub → new private app, e.g.
Beta-Calco Site. - Callback URL:
https://betacalco.com/cb-api/bcpipedriveoauth/callback - Installation URL:
https://betacalco.com/cb-api/bcpipedriveoauth/install - Scopes: grant the full set (decided 2026-08-07), including
admin/ Administer account — creating the pipeline and its stages needs it, and without it provisioning stops with a 403Scope and URL mismatch. The nine the integration needs are listed in oauth-app.md; never un-grant those.search:readis the dangerous omission — see the duplicate-deal note above. Two traps onadmin: scopes are fixed at install time, so adding one later needs a re-install; and Pipedrive capsadminby the installing user's permission set, so a non-admin installer gets nothing from it however it is ticked. - Leave it in draft until §4.
The redirect URI must match exactly — a trailing slash produces an opaque Pipedrive error and is the classic time-sink here.
BcPipedriveOauth::isConfigured()checks the shape; trust it.
§3. Deploy the code to live
Normal deploy (master:for_live). Then, before touching any auth setting:
# migrations applied?
php docroot/cli/… # or just load a page; they apply on the next request
SELECT latest_customization_update_version, failed_update_detected FROM <prefix>configbox_settings;
failed_update_detected must be 0 and the version at least 0.5.103.
Then the field registry, which the settings now reference:
php docroot/cli/cb_pipedrive_import_fields.php --adopt-legacy=1
It prints which configured field reference no longer resolves. That list must be empty. It is the config audit, and on live it is the one that matters.
At this point live is running the new code still on the token. Nothing has changed operationally. Bake here as long as you like.
§4. Install the app into the live company
- Flip the live app from draft to LIVE in the Developer Hub. This click is irreversible and is
the only moment Pipedrive validates the callback URL — so the code must already be deployed
(§3) and
curl https://betacalco.com/cb-api/bcpipedriveoauth/callbackmust return the app's own 400 "Nothing to do" page, not a Joomla 404. - Set
pipedrive_oauth_client_id,pipedrive_oauth_client_secret,pipedrive_oauth_redirect_urion the live settings row. - Sign into Pipedrive as
pipe@betacalco.com— not as yourself. - Visit
https://betacalco.com/cb-api/bcpipedriveoauth/installand complete the handshake. - Confirm the token row:
status = 'active',company_id= live's,client_id= the live app's, and a scope set covering the eight.
Still on the token. The installation exists but nothing uses it yet.
§5. The flip
One setting: pipedrive_auth_mode on the live row, token → oauth. No deploy.
Do it on a weekday morning with someone watching. Then, within five minutes:
- Sync health panel (backend → the change-log screen) — its
authblock should report modeoauth, the right company, the installer, the scope and an expiry. - A real quote push. Watch a deal appear/update and confirm it is attributed to the API user.
php docroot/cli/cb_pipedrive_api_parity.php— read-only, exits 0 when both API versions agree. Under OAuth it also proves both versions work under the new auth.- CloudWatch
Test1-Pipedrive-Export—Count-Successful-Quote-Exportsstill moving,Count-Failed-Quote-Exportsflat.
Rollback is the same setting, back to token. The personal token is still valid and still in the
setting; nothing else has to be undone. Keep it that way until §8.
§6. Bake — about two weeks
Watch, and do nothing else to the integration:
| Signal | Where | What "bad" looks like |
|---|---|---|
| Refresh actually happened | #__configbox_external_pipedrive_oauth_tokens.date_modified moves | Still the install timestamp after a day |
| Auth failures | CloudWatch Pipedrive-Oauth-Refresh-Failure, Pipedrive-Oauth-Needs-Reauth | Any datapoint at all |
| Uninstall | Pipedrive-App-Uninstalled | Any datapoint at all |
| Rate limiting | Pipedrive-Rate-Limited, and x-daily-requests-left in the logs | Trending down day over day |
| Duplicate deals | Two deals with the same Website Quote Number | The search:read symptom |
Install the watchdog cron if it is not already there — it is what catches a dying refresh token before it dies:
# daily
php docroot/cli/cb_pipedrive_oauth_watchdog.php
The 60-day rule cuts both ways. A refresh token dies after 60 days of non-use, and its clock
resets on each use — so regular traffic is the cure. getAccessToken() forces a refresh when the
token has not been used in 45 days, which is what saves a low-traffic environment.
§7. Move the webhook (a separate day)
Never on the same day as the auth flip. One reversible step at a time.
App-created webhooks are type: "application", are deletable only by the app, and are removed
automatically when the app is uninstalled — which is why this comes after the app is proven.
- Set
pipedrive_webhook_urlon the live row tohttps://betacalco.com/cb-api/bcpipedrive/updateQuote.curlit first — the oldpipedrive-quote-updatesalias is a per-database menu row and 404s wherever that row is absent;/cb-api/…resolves by code on every host. - Set
pipedrive_webhook_auth_user/_passwordand re-register together. Ordering trap: set one without the other and inbound sync 401s. - Re-register from the provisioning page's webhook row.
- Change a deal's stage in Pipedrive; watch a row land in
#__configbox_external_pipedrive_webhook_queueand the quote update after the drainer runs. - Delete the old subscription only once the new one has delivered.
Leave the webhooks v2 group off — /api/v2/webhooks is still absent from Pipedrive's official
v2 spec.
§8. Retire the personal token
After ~30 clean days, and in this order:
- Migrate
tools/pdusage (PD_ENV=livealready works). Do this before revoking or you lose your debugging tool on the day you need it. - Migrate
ConfigboxModelBcpipedriveparityand the sandbox mirror. - Revoke the token in Pipedrive, then blank
pipedrive_api_tokenon the live row.
Until step 3, rollback to token mode remains available. That is worth more than the tidiness.
Staging is a permanent exception
staging.betacalco.com is source-filtered at the hoster's AWS firewall and Pipedrive's callers are
blocked, so it can receive neither the OAuth callback nor webhooks. OAuth is genuinely impossible
there.
Run staging on token mode against the sandbox — outbound HTTP is unaffected by an inbound
filter — and accept that inbound webhook sync is untestable on staging. Write it on the ticket
rather than rediscovering it.
What will bite
- A stale
api_domainafter a company-domain rename → force one refresh on a 401/404 and retry. - The installing user's seat being deactivated kills the installation. Exactly the sort of thing
that gets trimmed for cost. The watchdog catches it; the runbook line is "re-install as
pipe@betacalco.com". - OAuth does not give a separate daily budget. It gives ~4× burst. The daily budget is per company and shared with every other app on the account, so it can shrink with no warning from something nobody told you about.
tools/sync_dev_from_live.shwill put a live refresh token on a laptop unless the token table is inSETTINGS_TABLESand non-devrows are scrubbed after a restore. This is a higher-probability route to a dev-writes-to-live incident than any code bug, because someone runs it deliberately.
Rollback summary
| Step | How to undo |
|---|---|
| §5 auth flip | pipedrive_auth_mode → token. Immediate, no deploy. |
| §7 webhook move | Point pipedrive_webhook_url back and re-register. |
| §4 installation | Uninstall the app; the row flips to uninstalled and auth throws — so flip §5 back first. |
| §3 deploy | Ordinary deploy rollback. The registry tables are additive and harmless if left. |
| §8 token revoke | Not reversible. A new token means re-issuing and re-entering it. |