Skip to content

Pipedrive CRM Sync

Pushes Volāre records (leads, clients, bookings, notes, passengers, products) to Pipedrive CRM via queued jobs, with a polymorphic sync tracking system and a Filament monitor page.

  • Keep the sales team’s Pipedrive account in sync with Volāre data automatically
  • Monitor and retry failed syncs from the admin panel
  • Reconcile local sync state against the live Pipedrive account
  • Provision or heal the Pipedrive pipeline, stages, and custom fields

Mapping defined in PipedriveSyncRegistry::MODELS:

Volāre model Pipedrive entity Job
Lead Lead (/leads) SyncLeadToPipedriveJob
Client Person (/persons) SyncClientToPipedriveJob
Passenger Person (/persons) SyncPassengerToPipedriveJob
Booking Deal (/deals) SyncBookingToPipedriveJob
BookingUpsell Product (/products) SyncProductToPipedriveJob
ProductByMarket Product (/products) SyncProductByMarketToPipedriveJob
BookingNote Note on a deal (/notes) SyncBookingNoteToPipedriveJob

BookingNote is intentionally outside the registry: notes have no standalone endpoint for reconciliation, so they are dispatched directly by the observer.

AttachProductsToDealJob links products to a deal’s Products tab: the main trip (the booking’s ProductByMarket, quantity 1 at the booking base price) plus each upsell. It deduplicates against products already attached, is unique per booking, and is (re-)dispatched by the booking and product sync jobs.

Model saved (created/updated)
-> PipedriveSyncObserver dispatches sync job (skipped if no API token)
-> Job on the `pipedrive-sync` queue (tries: 3, backoff: 30/60/180s)
-> AbstractPipedriveSyncJob.handle()
-> PipedriveSync row: Pending -> Syncing -> Synced/Failed/Ignored
-> PipedriveClient: POST to create, PUT to `{endpoint}/{pipedrive_id}` to update

Key components:

  • PipedriveSyncObserver — Registered on Client, Booking, Passenger, BookingUpsell, BookingNote, Lead in AppServiceProvider. Fires on created and updated. No-ops entirely when pipedrive.api_token is empty.
  • HasPipedriveSync trait — Gives models the pipedriveSync() morph relation, getOrCreatePipedriveSync(), and a deterministic pipedriveExternalId() (volare_<table>_<id>) for idempotency.
  • PipedriveSync model — Tracking row per synced model: pipedrive_id, external_id, status, attempts, error, last sent payload. Statuses: Pending, Syncing, Synced, Failed, Skipped, Ignored (see PipedriveSyncStatus).
  • PipedriveClient — HTTP client with timeout/retry, typed exceptions (auth, rate limit, validation, timeout), and credential-sanitized logging.
  • PipedriveAuthService — Static API token auth via the x-api-token header (no OAuth flow).
  • PipedriveSetting model — DB key-value store (pipedrive_settings table) for pipeline ID, stage IDs, and custom-field hash keys, cached forever and cleared on write. Populated by the setup commands, never hardcoded.
  • BuildsPipedriveCustomFields / LoadsPipedriveCustomFields traits — Payload builders translate logical field names into stored field_<name> hashes (blank values are skipped so an unresolved field never wipes a CRM value); commands load live account fields to detect orphaned keys.

Source: backend/app/Services/Pipedrive/, backend/app/Jobs/Pipedrive/

  • One-way sync (Volāre → Pipedrive). Volāre is the source of truth; nothing is read back into Volāre models.
  • Records deleted by hand in Pipedrive stay deleted. An update against a deleted entity marks the sync row Ignored — final and non-retryable — instead of re-creating it. Use the monitor’s Reactivate action after restoring the record from Pipedrive’s recycle bin.
  • Notes only sync outward. Only BookingNote rows with source Volare are synced, preventing loops with notes that originated in Pipedrive.
  • Rate limits release, they don’t fail. A 429 resets the row to Pending and releases the job for the Retry-After delay. Non-retryable errors (e.g. orphaned custom-field hashes) are reported to Sentry/Nightwatch and fail the job.
  • Leads are create-only. pipedrive:sync-all --force re-syncs already-synced records but skips leads.

Required env vars: PIPEDRIVE_API_TOKEN (from Pipedrive → Settings → Personal preferences → API), PIPEDRIVE_API_URL

Optional env var: PIPEDRIVE_QUEUE (defaults to pipedrive-sync)

Config file: backend/config/pipedrive.php (timeout, retry, logging)

Pipeline/stage IDs and custom-field keys live in the pipedrive_settings table, not in config.

PipedriveSyncMonitor (System → Pipedrive Sync, admin-only) lists all PipedriveSync rows with status/model filters, a stats widget, and 30s polling. Row actions:

  • Retry (Failed rows) — resets to Pending; re-processed by pipedrive:sync-all or the next model update.
  • Reactivate (Ignored rows) — resets to Pending after the record has been restored in Pipedrive.

Source: backend/app/Filament/Pages/PipedriveSyncMonitor.php

Command Purpose
pipedrive:setup-pipeline Create the sales pipeline and its 8 stages
pipedrive:setup-custom-fields Create required deal/person custom fields
pipedrive:sync-all Dispatch jobs for unsynced records (--model, --limit=500, --force)
pipedrive:retry-failed Re-dispatch failed/pending sync rows (--status, --model, --limit=100)
pipedrive:reconcile Compare local sync rows against Pipedrive, report drift (--fix resets broken rows to pending)
pipedrive:reconcile-custom-fields Re-map stored field keys to live account fields by name, healing orphans (--dry-run)
pipedrive:ignore-deleted Park rows stuck retrying because their Pipedrive entity was deleted (--dry-run)
pipedrive:recover-overflowed-syncs Reset rows that crashed on the attempts smallint overflow so they re-sync (--dry-run)
pipedrive:cleanup-legacy-stages Delete legacy CheckoutStep-based stages and their settings rows

Source: backend/app/Console/Commands/Pipedrive*.php

  • Queue System — the pipedrive-sync queue must have a worker
  • Source: backend/app/Services/Pipedrive/PipedriveClient.php
  • Source: backend/app/Jobs/Pipedrive/AbstractPipedriveSyncJob.php
  • Source: backend/app/Observers/PipedriveSyncObserver.php