Skip to content

Leads API

Captures homepage contact form submissions in the database and automatically pushes them to Pipedrive’s Leads Inbox via a backend job.

Previously, the homepage contact form called the Pipedrive API directly from the frontend with a hardcoded API token. Lead capture was moved server-side to:

  • Remove the exposed API token from client-side code
  • Store consent timestamps in the database (GDPR compliance)
  • Push to Pipedrive through a queued, retryable backend job (see Pipedrive Sync below)

Public endpoint (no authentication required). Rate-limited to 5 requests per minute per IP.

Request: See StoreLeadRequest for validation rules.

FieldTypeRequiredDescription
namestringYesContact name (max 255)
emailstringYesValid email (max 255)
phonestringNoPhone number (max 50)
regionstringNoFree-text region summary (max 255). Legacy path only — the current homepage form does not send this.
destinationsarrayNoStructured list of selected regions/countries. See shape below. Max 100 items.
preferred_call_timestringNoPreferred callback time (max 255)
consentbooleanYesMust be true (accepted)

Each item must be an object with:

FieldTypeRequiredDescription
typestringYesregion (entire region selected) or country (specific country selected)
idstringNoStable identifier (e.g., CMS slug) up to 100 chars
labelstringYesHuman-readable name shown in the admin (max 100)

Example payload:

{
"name": "María García",
"email": "maria@example.com",
"phone": "+34 612 345 678",
"destinations": [
{"type": "region", "id": "centroamerica", "label": "Centroamérica"},
{"type": "country", "id": "tailandia", "label": "Tailandia"}
],
"preferred_call_time": "Mañana (9:00 – 13:00)",
"consent": true
}

Response: 201 Created

{
"success": true,
"message": "Lead received successfully."
}

Errors:

CodeCause
422Validation failure (missing name/email, invalid email, consent not accepted, invalid destinations.*.type)
429Rate limit exceeded (5 requests/minute)

Leads appear in the Filament admin under Customers > Leads (read-only resource, no create/edit). Admins can view details and bulk-delete.

The Destinos section on the lead view page renders the structured destinations array with visual differentiation:

  • Regiones completas — primary-coloured badges with a globe icon for items where type = region.
  • Países específicos — gray badges with a map-pin icon for items where type = country.
  • Texto original — fallback plain text, shown only for legacy leads that stored a raw region string with no structured destinations.

The leads list table shows the same two badge columns (Regiones, Países) instead of a single joined string, so admins can scan which specific destinations a lead mentioned.

Permissions: view_lead, delete_lead (no create/update since leads come from the public form).

Source: backend/app/Filament/Resources/Leads/LeadResource.php

The HomeContactClosing React component on the home page renders the “Agendar cita” side modal. Destinations are picked via a single DestinationMultiSelect component that combines regions and countries into one multi-select with a region-group accordion. Selecting a whole region disables the individual country checkboxes within it to avoid redundant payloads.

On submit, HomeContactClosing calls submitContactInquiry() from frontend/src/services/leadApi.ts, which POSTs to /api/leads using the apiUrl prop (resolved from API_URL env var). The service maps Spanish form field names to the API contract (e.g., nombre to name) and translates the picker’s selection into the structured destinations array described above. The free-text region field is no longer populated by the frontend; it remains accepted by the API for legacy/manual callers.

The callback-time select is grouped under disabled LUNES-VIERNES / SÁBADOS headers, so the visible option label is time-only and ambiguous once stripped from the UI (e.g. 12:30h-14h is weekday, 12:30h-15h is Saturday). Before submitting, formatHorarioLabel() prefixes the chosen value with Lun-Vie or Sábado, so sales sees unambiguous context in Pipedrive and the admin records (e.g. Lun-Vie 12:30h-14h).

Sources:

  • frontend/src/components/home/HomeContactClosing/HomeContactClosing.react.tsx
  • frontend/src/components/DestinationMultiSelect/DestinationMultiSelect.tsx
  • frontend/src/services/leadApi.ts

New Leads are pushed to Pipedrive’s native Leads Inbox automatically on creation. The sync is one-way (outbound) and create-only: once a Lead has a Pipedrive Lead ID, the job short-circuits. Leads are meant to be triaged (promoted to Deals) in Pipedrive, not edited from Volare, and no inbound webhook handler exists for Leads. Inbound handlers in app/Jobs/Pipedrive/Inbound/ cover Deal, Person, Note, and Activity only.

When a Lead is created, PipedriveSyncObserver dispatches SyncLeadToPipedriveJob on the pipedrive-sync queue (configurable via the PIPEDRIVE_QUEUE env var, default pipedrive-sync). The job:

  1. Resolves a Pipedrive Person for the Lead’s email, in this order:
    1. Reuse the Person linked to a local Client with a matching email, if that Client is already synced.
    2. Otherwise, call GET /persons/search?term={email}&fields=email&exact_match=true and reuse the first match.
    3. Otherwise, POST /persons to create a new Person with name, email, and phone.
  2. Creates the Lead via POST /leads with title, person_id, and the custom fields below. The returned Pipedrive Lead UUID is persisted on the pipedrive_syncs.pipedrive_id row for this Lead.

title format: "{name} - {destinations}" when destinations exist, otherwise "Web inquiry from {name}".

Four Lead-specific custom fields are attached to the Pipedrive Lead:

Volare keyPipedrive labelSource
volare_lead_idVolare Lead IDLead.id
lead_source_nameVolare Lead SourceLead.source (e.g. homepage_contact)
lead_destinationsVolare Lead DestinationsComma-separated region + country labels from Lead.destinations
preferred_call_timePreferred Call TimeLead.preferred_call_time

Pipedrive has no /leadFields endpoint — Leads share the Deal custom-field schema because a Lead converts to a Deal when qualified. These four fields are therefore registered under /dealFields by pipedrive:setup-custom-fields, and the hash keys carry over automatically on lead-to-deal conversion.

SyncLeadToPipedriveJob implements ShouldBeUnique (unique id = Lead.id, uniqueFor = 600s). This prevents duplicate POST /leads calls when a second dispatch races in during the ~1s window between reading pipedrive_id == null and writing it back. The filled($pipedrive_id) short-circuit remains as a secondary guard.

pipedrive:sync-all --force is loudly downgraded to non-force mode for create-only models (currently Lead). Re-dispatching a synced Lead is a no-op because the job short-circuits, so --force cannot repair data drift on these records — it would silently enqueue dead jobs.

  • PIPEDRIVE_API_TOKEN must be set. If it’s empty, PipedriveSyncObserver silently skips dispatch (there’s no error).
  • A queue worker must consume the pipedrive-sync queue (or whatever PIPEDRIVE_QUEUE is set to).
  • Run php artisan pipedrive:setup-custom-fields once per environment to register the custom fields in Pipedrive and store their hash keys in pipedrive_settings. The command is idempotent: already-registered fields are skipped by key lookup.

The Filament page at /admin/pipedrive-sync-monitor shows per-record sync status (model_type, model_id, pipedrive_id, status, last error) across all synced models, with a re-dispatch action for failed rows.

ComponentFile
Modelbackend/app/Models/Lead.php
Controllerbackend/app/Http/Controllers/Api/LeadController.php
Form Requestbackend/app/Http/Requests/Api/StoreLeadRequest.php
Migrationsbackend/database/migrations/2026_04_16_141305_create_leads_table.php, ..._add_destinations_to_leads_table.php, ..._drop_budget_from_leads_table.php
Factorybackend/database/factories/LeadFactory.php
Filament Resourcebackend/app/Filament/Resources/Leads/LeadResource.php
Pipedrive Sync Jobbackend/app/Jobs/Pipedrive/SyncLeadToPipedriveJob.php
Pipedrive Observerbackend/app/Observers/PipedriveSyncObserver.php (registered in AppServiceProvider::boot())
Pipedrive Registrybackend/app/Services/Pipedrive/PipedriveSyncRegistry.php ('lead' entry, endpoint /leads)
Custom Fields Setupbackend/app/Console/Commands/PipedriveSetupCustomFieldsCommand.php
Bulk Sync Commandbackend/app/Console/Commands/PipedriveSyncAllCommand.php
Sync Monitorbackend/app/Filament/Pages/PipedriveSyncMonitor.php
Frontend Formfrontend/src/components/home/HomeContactClosing/HomeContactClosing.react.tsx
Frontend Pickerfrontend/src/components/DestinationMultiSelect/DestinationMultiSelect.tsx
Frontend Servicefrontend/src/services/leadApi.ts