Current File : /www/capitalgmcbuickregina_830/public/wp-content/plugins/leadbox/docs/lead-intake-migration.md
# Lead Intake Migration — Legacy → Leadbox Platform (LBT-1468)
How to cut a dealer's Ninja Forms lead submissions over from the legacy backend
(`leadbox-be`, `POST /api/v1/lead`) to **Leadbox Platform** (`POST /api/v1/leads`),
without a deploy, using the runtime routing switch shipped in this plugin.
The switch has three modes, controlled per-site:
| Mode | Behavior |
|------|----------|
| `legacy` *(default)* | Leads go only to the legacy backend. Byte-for-byte identical to pre-LBT-1468 behavior. |
| `both` | **Dual-write.** Legacy is authoritative (drives success/retry); Leadbox Platform receives a copy, fired non-blocking. Used to validate parity before cut-over. |
| `leadbox_os` | Leads go only to Leadbox Platform. Full cut-over. |
---
## How the switch is resolved
The effective target is resolved in this order (first match wins):
1. **`LEADBOX_LEAD_INTAKE_TARGET` PHP constant** in `wp-config.php` — for ops-driven
cut-over without DB access. An invalid value is ignored.
2. **`lead_intake_target` option** — set via WP admin or WP-CLI.
3. Default: **`legacy`**.
> When the constant is defined it **overrides** the option. The admin UI disables
> the dropdown and shows a notice, and `wp leadbox lead-intake-target get|set`
> warns that the option is being overridden.
---
## Prerequisites (per dealer)
Before flipping a dealer to `both`/`leadbox_os`, get from the Leadbox Platform side:
1. **A website lead-intake API key** (`wl_key_…`). Issued by Leadbox Platform via:
- Artisan: `sail artisan leads:website-key:generate {tenant_id} --location={id} --name="..."`, or
- Admin API: `POST /api/leads/api-keys` (`{ location_id, tenant_id, name }`).
2. **Location binding decision.** Prefer a **location-scoped key** (key bound to one
location). Then the plugin does **not** need to send `LocationId` — Leadbox Platform
resolves the location from the key. The mapper only sends `LocationId` if a form
explicitly provides a numeric one (multi-location sites).
> **Base URL is configurable, with a default.** Leave the **Leadbox Platform
> Lead Intake URL** field blank to use the default
> (`LB_API_Settings::LEADBOX_OS_LEAD_INTAKE_URL` = `https://os.leadboxhq.com`).
> Set it only to point a site at a different Platform host (e.g. staging). The
> value is normalised (scheme forced to `https`, trailing slash stripped); an
> invalid URL is ignored and the default is used. The client appends `/api/v1/leads`.
Configure these in **WP Admin → Leadbox settings → "Backend Connection"**:
- **Lead Intake Target** → the mode (or use WP-CLI). The URL/token fields below
are hidden while this is `legacy`, since they don't apply.
- **Leadbox Platform Lead Intake URL** → optional override; blank = default host.
- **Leadbox Platform Lead Intake Token** → the `wl_key_…`.
---
## Step 1 — Deploy the plugin update (zero behavior change)
Ship the plugin with `lead_intake_target = legacy` everywhere. Because `legacy` is
the default and delegates 1:1 to the existing legacy client, there is **no behavior
change** for any site until a dealer is explicitly flipped.
Verify a dealer is in legacy mode:
```bash
wp leadbox lead-intake-target get
# Effective target : legacy
```
---
## Step 2 — Enable dual-write (`both`) and monitor parity ≥ 24h
For a migrated dealer:
1. Set the URL + token (above).
2. Flip to `both`:
```bash
wp leadbox lead-intake-target set both
```
In `both` mode:
- The **legacy POST stays authoritative** — it drives the form's success path and
the failed-lead retry queue. Leadbox Platform receives a copy fired **non-blocking**, so
a Leadbox Platform error can never block, fail, or delay the legacy submission, and never
enqueues a retry.
Monitor for **at least 24 hours** and compare legacy vs. Leadbox Platform:
- **Counts** — leads landing in legacy vs. created in Leadbox Platform (per hour).
- **Field parity** — name/email/phone/message/source/vehicle resolve correctly in OS.
- **Error rate** — Leadbox Platform rejections (see *Observability* below).
> Because OS has no idempotency mechanism, every POST creates a new conversation/deal
> in OS (contact is deduped by email/phone, conversation is not). In `both` mode this
> is fine — OS is a shadow copy. Do not retry OS failures in `both` mode (the plugin
> doesn't).
---
## Step 3 — Cut over (`leadbox_os`)
Once parity is confirmed:
```bash
wp leadbox lead-intake-target set leadbox_os
```
Now leads go **only** to Leadbox Platform. The legacy-only Ninja Forms action toggles
(`Use Backend Autoresponder?`, `Send ADF To`) are hidden in the form action editor;
the skip flags (ADF / Shift Digital / Delivr) remain because Leadbox Platform honors them.
Validate: submit a test lead and confirm it appears in Leadbox Platform and **not** in the
legacy backend.
> A failed OS POST in `leadbox_os` mode IS authoritative — it enqueues to the
> `{prefix}leadbox_leads` retry table and `retryFailedLeads()` re-sends it to OS.
---
## Rollback
Rollback needs **no deploy** — just flip the target back:
```bash
wp leadbox lead-intake-target set both # or: legacy
```
For an emergency, ops can hard-pin the target in `wp-config.php` (overrides the
option and disables the admin control):
```php
define('LEADBOX_LEAD_INTAKE_TARGET', 'legacy');
```
---
## WP-CLI quick reference
```bash
# Show effective target, stored option, and any constant override
wp leadbox lead-intake-target get
# Set the stored option (validated: legacy | leadbox_os | both)
wp leadbox lead-intake-target set both
wp leadbox lead-intake-target set leadbox_os
wp leadbox lead-intake-target set legacy
```
---
## Observability
Every dispatch is logged via `leadbox_logger()`:
- **Success** → `info` level (message `lead_dispatch`), with `target`, `backend`,
`form_id`, `success`, `error_code`.
- **Failure** → `error` level (`lead_dispatch_failed`), plus the OS client logs the
HTTP status, latency, and error.
> The logger's default minimum level is `ERROR` (production-safe), so routine `info`
> dispatch logs are suppressed unless `LEADBOX_LOG_LEVEL` is lowered in `wp-config.php`
> (e.g. `define('LEADBOX_LOG_LEVEL', 'info');`). Failures are always recorded.
> When `WP_DEBUG_LOG` is on, entries also land in `wp-content/leadbox-debug.log`.
### Dispatch log (`wp-content/leadbox-lead-dispatch.log`)
For the `both` parity window there is a **dedicated text log**, separate from the
PSR-3 logger and written **always** (independent of `WP_DEBUG_LOG` / log level).
It records one line per dispatch **only when `lead_intake_target` is `both` or
`leadbox_os`** — default-`legacy` sites write nothing.
Each line tells you whether the send succeeded and **why**, with PII partially
masked (`j***@gmail.com`, `***1234`):
```
[2026-06-05 14:23:01] OK both/leadbox_os form=12 sub=4567 http=200 lat=312ms lead_id=cnv_abc contact=j***@gmail.com phone=***1234
[2026-06-05 14:23:05] FAIL both/leadbox_os form=12 http=422 err=validation_failed lat=240ms contact=j***@gmail.com phone=***1234 msg="Email or Phone required"
```
Read it from the terminal:
```bash
wp leadbox lead-log tail # last 50 lines
wp leadbox lead-log tail --lines=200 --failures # last 200, failures only
# or plain grep:
grep ' FAIL ' wp-content/leadbox-lead-dispatch.log
```
> **`both` mode captures the real platform outcome without delaying the visitor.**
> The platform copy is dispatched **after** the HTTP response is sent
> (`fastcgi_finish_request()`), then read in blocking mode to log the true status.
> This requires **PHP-FPM** (LocalWP and most hosts use it). Without FPM it falls
> back to fire-and-forget and the line is tagged `note=sent_no_confirmation`
> (the lead is still sent; only the status read is skipped to avoid blocking).
>
> Disable the log entirely with `define('LEADBOX_LEAD_DISPATCH_LOG', false);`.
> Relocate it with the `leadbox_dispatch_log_path` filter.
### Leadbox Platform error codes (from `LB_OS_API_Client`)
| `error_code` | HTTP | Meaning |
|--------------|------|---------|
| `os_not_configured` | — | `wl_key_` token missing in settings |
| `network_error` | — | Connection/timeout/DNS failure |
| `spam_rejected` | 400 | Caught by the OS spam filter |
| `auth_failed` | 401 | Invalid/revoked/missing `wl_key_` |
| `location_mismatch` | 403 | Payload `LocationId` ≠ the key's bound location |
| `validation_failed` | 422 | Field validation (e.g. missing Email and Phone) |
| `location_unresolved` | 422 | Tenant has no location to receive the lead |
| `rate_limited` | 429 | Over the per-key rate limit (300/min) |
| `server_error` | 5xx | Leadbox Platform internal error |
---
## Vehicle resolution (stock enrichment)
The Platform resolves the vehicle of interest **exactly**, by either:
1. `VehicleId` — but this is the **website's** numeric vehicle id, which does **not**
match the Platform's own inventory ids; it only resolves where the two id spaces
happen to coincide. A miss returns `warnings: ["vehicle_not_found"]` (HTTP 200) and
no vehicle is linked.
2. `AdditionalData.stock` — the **stock number**, which is stable across systems.
So before dispatching, `LB_OS_API_Client` runs a **best-effort enrichment**: when the
lead has no stock, it determines the vehicle id and looks the vehicle up via the
inventory API (`LB_API_Client::VehicleDetail`), then adds its `stocknumber` to the lead.
The mapper forwards it as `AdditionalData.stock`, so the Platform resolves the vehicle
by stock regardless of id-space mismatches. The lookup is cached, never throws, and
never blocks the dispatch — a failure just sends the lead without a resolved stock.
The vehicle id is taken from, in order:
1. the form's `vehicleid` field — populated from the `?vehicle_id=` query string on the
dedicated lead-form pages (`/form/...?vehicle_id=167`); then
2. a fallback parsed from the `referrer` (the page URL the plugin always sends): forms
**embedded on a VDP** (clean permalink, no query string) submit an empty `vehicleid`,
but the VDP slug ends in `-{id}` (see `get_vdp_slug()`), so the id is recovered from
the URL.
> No form change is required. Sites whose forms already supply a `stocknumber`/`stock`
> field skip the lookup entirely.
## Notes / limitations
- **No autoresponder in Leadbox Platform.** The legacy `backend-autoresponder` toggle is
removed from the action UI in `leadbox_os` mode and has no OS equivalent.
- **No ADF recipient list.** `SendADFTo` is hidden in `leadbox_os` mode and the mapper
never forwards it.
- **Skip flags are preserved.** `SkipADF`, `SkipShiftDigital`, `SkipDelivr` are valid
in Leadbox Platform (they suppress ADF/Serti/Cochrane, ShiftDigital, FordDirect
respectively) and are forwarded as real booleans.
- **Timeout** is 5s, matching the legacy client.
Thank you for choosing Capital GMC Buick | Regina, your premier certified Buick and GMC dealership proudly serving drivers in Regina and the surrounding communities. Whether you’re searching for a brand-new Buick or GMC vehicle or a meticulously inspected pre-owned model, we have a diverse selection to match your needs and lifestyle.
Beyond our impressive inventory, we offer a seamless and stress-free financing experience through our well-connected finance centre, where our team of experts is dedicated to securing the best loan or lease options for you, quickly, transparently, and hassle-free.
But our commitment to you doesn’t stop at the sale. Our state-of-the-art service centre is staffed with skilled Buick and GMC technicians who use the latest equipment and genuine OEM parts to keep your vehicle running at its best. From routine maintenance to complex repairs, we’ve got you covered.
Experience top-tier customer service, quality vehicles, and expert care, all in one place. Visit Capital GMC Buick | Regina today or call us at 306-205-8072 with any questions. We’re here to help!
Have a question or need assistance? Fill out the form and we will reach out to you as soon as possible.
Schedule a Visit
Let us know when you are coming and how we can assist you. We can ensure someone will be on hand to help you out at the desired date and time.
Find a Career
Have a look at our list of available positions and apply online today to join our team!
Opportunities to Grow
The auto industry is constantly changing and we want to continue to grow. We offer growth, leadership & mentorship programs to allow our staff to grow with us.
Competitive Salary
We have a significant earning potential with incentive-based pay in most roles. We also offer an employee referral bonus with paid bonuses.
Health & Dental
We offer a comprehensive benefits package including extended health, dental, and vision care. We also include paramedical, life insurance, paid sick leave, short & long-term disability coverages.
Vacation
We value our employees and want everyone to take their vacation time. We offer a minimum of 2 weeks vacation each year.
Training & Development
We have many opportunities for paid education and training in-house as well as training from the Manufacturer.
$10,000 Cash Giveaway – Terms & Conditions
All October long, stop by Capital GMC Buick Cadillac, to enter for your chance to win $10,000 cash. No purchase is required, but entries must be made in-store.
The contest is open to residents of Saskatchewan who are 18+. Dealership employees and their households are not eligible. Entries will be accepted from October 1 to October 31, 2025. A random draw will take place on November 1, 2025.
The prize is one $10,000 award, paid by cheque, and must be accepted as awarded. Winner will be contacted by phone or email and must respond within 7 days or another entry may be drawn. Odds of winning depend on the number of entries received.By entering, you agree that Capital Automotive Group may use your name and photo for winner announcements. The contest is governed by the laws of Saskatchewan.