A complete guide for migrating from ADF-XML (Auto-lead Data Format) to ACP. Includes bidirectional field mapping, conversion code in JavaScript, Python, and PHP, data loss analysis, and a phased rollout strategy for production environments.
ADF-XML was designed in 2000 for form-fill lead submissions. It captures roughly 15 flat fields: name, email, phone, and a vehicle. AI-powered interactions generate 10–50× more structured data — intents, scores, transcripts, behavioral events, deal state — none of which ADF can carry. Every ADF submission is a lossy compression of what the AI actually captured.
ACP is not a replacement that requires ripping out ADF overnight. It's designed for parallel operation: produce ACP envelopes natively, and downgrade to ADF-XML for CRMs that haven't adopted ACP yet.
The recommended approach is dual-output with phased cutover:
| ADF-XML Path | ACP Path | Notes |
|---|---|---|
| <name part="first"> | lead.contact.first_name | Direct mapping. |
| <name part="last"> | lead.contact.last_name | Direct mapping. |
| <email> | lead.contact.email | Direct mapping. |
| <phone type="voice"> | lead.contact.phone | ACP recommends E.164 format. |
| <address>...<street> | lead.contact.address.street | Direct mapping. |
| <address>...<city> | lead.contact.address.city | Direct mapping. |
| <address>...<regioncode> | lead.contact.address.state | ADF uses regioncode, ACP uses state. |
| <address>...<postalcode> | lead.contact.zip | Also available at lead.contact.address.zip. |
| ADF-XML Path | ACP Path | Notes |
|---|---|---|
| <vehicle interest="buy"> | lead.vehicle_interests[0] | ADF allows multiple <vehicle> elements; each maps to an array item. |
| <year> | .year | Direct mapping. Integer in ACP. |
| <make> | .make | Direct mapping. |
| <model> | .model | Direct mapping. |
| <trim> | .trim | Direct mapping. |
| <stock> | .stock_number | Renamed for clarity. |
| <vin> | .vin | Direct mapping. |
| <colorcombination>...<color> | .color | ADF nests under colorcombination; ACP flattens. |
| status="new|used" | .condition | ACP adds "certified" option. |
| — | .features | No ADF equivalent. ACP-only field. |
| ADF-XML Path | ACP Path | Notes |
|---|---|---|
| <vendorname> | dealership.name | Direct mapping. |
| <id> (vendor child) | dealership.id | ADF vendor ID if present; otherwise producer-generated. |
| — | dealership.crm_provider | No ADF equivalent. |
| — | dealership.dms_provider | No ADF equivalent. |
| — | dealership.timezone | No ADF equivalent. |
| ADF-XML Path | ACP Path | Notes |
|---|---|---|
| <provider>...<name> | source.provider | Direct mapping. |
| <provider>...<url> | source.page_url | Approximate mapping. |
| — | source.agent_type | No ADF equivalent. ACP-only. |
| — | source.channel | No ADF equivalent. |
| — | source.session_id | No ADF equivalent. |
| — | source.utm | No ADF equivalent. |
| — | source.device | No ADF equivalent. |
| ADF-XML Path | ACP Path | Notes |
|---|---|---|
| <requestdate> | acp.generated_at | ADF uses requestdate; ACP uses ISO 8601 generated_at. |
| prospect status="new" | lead.status | ADF only has new/resend. ACP has 5 statuses. |
| <comments> | conversation.messages[] | ADF dumps all context into a single comments field. ACP has structured messages. |
Converting ACP → ADF-XML is a deterministic but lossy downgrade. The following ACP objects and fields have no ADF equivalent and are discarded during conversion:
| ACP Object / Field | Status | Impact |
|---|---|---|
| lead.intents[] | LOST | All detected purchase intents and confidence scores. CRM cannot see what the customer wants. |
| lead.score | LOST | Lead qualification score. CRM cannot prioritize leads by quality. |
| lead.score_factors[] | LOST | Transparent scoring breakdown. CRM has no audit trail. |
| lead.budget | LOST | Target price, monthly payment, down payment. Sales rep starts blind. |
| lead.trade_in | LOST | Trade-in vehicle, mileage, condition, payoff. Must be re-gathered. |
| transaction (entire) | LOST | Deal stage, offer history, finance terms, appointments, delivery. |
| conversation.messages[] | LOST | Full transcript. CRM gets nothing about what was actually discussed. |
| conversation.handoff | LOST | AI-to-human escalation context. Human agent starts from scratch. |
| conversation.sentiment | LOST | Customer mood. Was the customer happy, angry, or neutral? |
| events[] (entire) | LOST | All 18 behavioral event types. No audit trail of the interaction. |
| ai_analysis (entire) | LOST | Purchase probability, recommended actions, objections, summary. |
| source.utm | LOST | Marketing attribution. Cannot attribute conversions to campaigns. |
| source.device | LOST | Device type, OS, browser. No mobile vs desktop analytics. |
| source.session_id | LOST | Cross-channel linking. Cannot connect chat to return visit. |
| lead.contact | MAPPED | Name, email, phone, address survive. |
| lead.vehicle_interests[] | MAPPED | Year, make, model, trim, stock survive. Features lost. |
| dealership.name | MAPPED | Vendor name survives. |
The downgrade converter extracts the ADF-compatible subset from an ACP envelope and emits valid ADF-XML. This is deterministic — the same ACP envelope always produces the same ADF-XML.
function acpToAdf(envelope) { const lead = envelope.lead; const c = lead.contact || {}; const v = (lead.vehicle_interests || [])[0] || {}; const d = envelope.dealership || {}; const esc = s => s ? s.replace(/[<>&"]/g, m => ({'&':'&','<':'<','>':'>','"':'"'})[m]) : ''; const status = v.condition === 'certified' ? 'used' : (v.condition || 'new'); return `<?xml version="1.0" encoding="UTF-8"?> <?adf version="1.0"?> <adf> <prospect status="new"> <requestdate>${envelope.acp.generated_at}</requestdate> <vehicle interest="buy" status="${status}"> ${v.year ? `<year>${v.year}</year>` : ''} ${v.make ? `<make>${esc(v.make)}</make>` : ''} ${v.model ? `<model>${esc(v.model)}</model>` : ''} ${v.trim ? `<trim>${esc(v.trim)}</trim>` : ''} ${v.stock_number ? `<stock>${esc(v.stock_number)}</stock>` : ''} ${v.vin ? `<vin>${esc(v.vin)}</vin>` : ''} </vehicle> <customer> <contact> ${c.first_name ? `<name part="first">${esc(c.first_name)}</name>` : ''} ${c.last_name ? `<name part="last">${esc(c.last_name)}</name>` : ''} ${c.email ? `<email>${esc(c.email)}</email>` : ''} ${c.phone ? `<phone type="voice">${esc(c.phone)}</phone>` : ''} </contact> </customer> <vendor> <vendorname>${esc(d.name)}</vendorname> </vendor> <provider> <name>${esc(envelope.source.provider)}</name> </provider> </prospect> </adf>`; }
from xml.sax.saxutils import escape def acp_to_adf(envelope: dict) -> str: lead = envelope.get("lead", {}) c = lead.get("contact") or {} v = (lead.get("vehicle_interests") or [{}])[0] d = envelope.get("dealership", {}) src = envelope.get("source", {}) gen = envelope["acp"]["generated_at"] condition = v.get("condition", "new") status = "used" if condition == "certified" else condition def tag(name, val, **attrs): if not val: return "" a = " ".join(f'{k}="{escape(str(v))}"' for k, v in attrs.items()) return f"<{name}{' ' + a if a else ''}>{escape(str(val))}</{name}>" return f"""<?xml version="1.0" encoding="UTF-8"?> <?adf version="1.0"?> <adf> <prospect status="new"> <requestdate>{gen}</requestdate> <vehicle interest="buy" status="{status}"> {tag("year", v.get("year"))} {tag("make", v.get("make"))} {tag("model", v.get("model"))} {tag("trim", v.get("trim"))} {tag("stock", v.get("stock_number"))} {tag("vin", v.get("vin"))} </vehicle> <customer> <contact> {tag("name", c.get("first_name"), part="first")} {tag("name", c.get("last_name"), part="last")} {tag("email", c.get("email"))} {tag("phone", c.get("phone"), type="voice")} </contact> </customer> <vendor>{tag("vendorname", d.get("name"))}</vendor> <provider>{tag("name", src.get("provider"))}</provider> </prospect> </adf>"""
function acpToAdf(array $envelope): string { $lead = $envelope['lead'] ?? []; $c = $lead['contact'] ?? []; $v = ($lead['vehicle_interests'] ?? [[]])[0] ?? []; $d = $envelope['dealership'] ?? []; $src = $envelope['source'] ?? []; $gen = $envelope['acp']['generated_at']; $status = ($v['condition'] ?? 'new') === 'certified' ? 'used' : ($v['condition'] ?? 'new'); $e = fn($s) => htmlspecialchars($s ?? '', ENT_XML1); $tag = fn($name, $val, $attrs = '') => $val ? "<{$name}{$attrs}>{$e($val)}</{$name}>" : ''; return <<<XML <?xml version="1.0" encoding="UTF-8"?> <?adf version="1.0"?> <adf> <prospect status="new"> <requestdate>{$gen}</requestdate> <vehicle interest="buy" status="{$status}"> {$tag('year', $v['year'] ?? null)} {$tag('make', $v['make'] ?? null)} {$tag('model', $v['model'] ?? null)} {$tag('trim', $v['trim'] ?? null)} {$tag('stock', $v['stock_number'] ?? null)} {$tag('vin', $v['vin'] ?? null)} </vehicle> <customer><contact> {$tag('name', $c['first_name'] ?? null, ' part="first"')} {$tag('name', $c['last_name'] ?? null, ' part="last"')} {$tag('email', $c['email'] ?? null)} {$tag('phone', $c['phone'] ?? null, ' type="voice"')} </contact></customer> <vendor>{$tag('vendorname', $d['name'] ?? null)}</vendor> <provider>{$tag('name', $src['provider'] ?? null)}</provider> </prospect> </adf> XML; }
Ingesting legacy ADF-XML into an ACP-native system. This is a lossless upgrade — no ADF data is discarded. The resulting ACP envelope will have many empty optional fields, but it validates and can be enriched later.
import { parseStringPromise } from 'xml2js'; import crypto from 'crypto'; async function adfToAcp(adfXml) { const parsed = await parseStringPromise(adfXml); const prospect = parsed.adf.prospect[0]; const customer = prospect.customer?.[0]?.contact?.[0] || {}; const vehicle = prospect.vehicle?.[0] || {}; const vendor = prospect.vendor?.[0] || {}; const provider = prospect.provider?.[0] || {}; const names = (customer.name || []).reduce((acc, n) => { acc[n.$.part] = n._; return acc; }, {}); const now = new Date().toISOString(); const rand = crypto.randomBytes(4).toString('hex'); return { acp: { version: "1.0.0", protocol: "acp", generated_at: prospect.requestdate?.[0] || now, envelope_id: `acp_${Date.now()}_${rand}`, parent_envelope_id: null }, source: { provider: provider.name?.[0] || "adf_import", agent_type: "sales_chat", channel: "web_chat" }, dealership: { id: vendor.id?.[0] || `dealer_${rand}`, name: vendor.vendorname?.[0] || "Unknown" }, lead: { id: `lead_adf_${Date.now()}`, status: "new", contact: { first_name: names.first || null, last_name: names.last || null, email: customer.email?.[0] || null, phone: customer.phone?.[0]?._ || customer.phone?.[0] || null }, vehicle_interests: vehicle.year ? [{ year: parseInt(vehicle.year[0]), make: vehicle.make?.[0] || null, model: vehicle.model?.[0] || null, trim: vehicle.trim?.[0] || null, condition: vehicle.$?.status || "new", stock_number: vehicle.stock?.[0] || null, vin: vehicle.vin?.[0] || null }] : [], intents: [], score: null, score_factors: [] }, transaction: { id: `txn_adf_${Date.now()}`, stage: "inquiry", last_updated: now }, conversation: { id: `conv_adf_${Date.now()}`, started_at: prospect.requestdate?.[0] || now, message_count: 0, messages: [] }, events: [{ type: "conversation_started", timestamp: prospect.requestdate?.[0] || now, data: { channel: "adf_import", source: "legacy" } }] }; }
import xml.etree.ElementTree as ET import uuid from datetime import datetime, timezone def adf_to_acp(adf_xml: str) -> dict: root = ET.fromstring(adf_xml) prospect = root.find("prospect") customer = prospect.find(".//customer/contact") vehicle = prospect.find("vehicle") vendor = prospect.find("vendor") # Extract name parts names = {} if customer is not None: for n in customer.findall("name"): names[n.get("part", "")] = n.text now = datetime.now(timezone.utc).isoformat() rand = uuid.uuid4().hex[:8] req_date = prospect.findtext("requestdate", now) return { "acp": { "version": "1.0.0", "protocol": "acp", "generated_at": req_date, "envelope_id": f"acp_{int(datetime.now().timestamp())}_{rand}", "parent_envelope_id": None }, # ... same structure as JS version # Populate source, dealership, lead, transaction, # conversation, events from parsed XML }
function adfToAcp(string $adfXml): array { $xml = simplexml_load_string($adfXml); $prospect = $xml->prospect; $contact = $prospect->customer->contact; $vehicle = $prospect->vehicle; $vendor = $prospect->vendor; $names = []; foreach ($contact->name ?? [] as $n) { $names[(string) $n['part']] = (string) $n; } $now = gmdate('c'); $rand = bin2hex(random_bytes(4)); $reqDate = (string) ($prospect->requestdate ?? $now); return [ 'acp' => [ 'version' => '1.0.0', 'protocol' => 'acp', 'generated_at' => $reqDate, 'envelope_id' => "acp_" . time() . "_{$rand}", 'parent_envelope_id' => null, ], 'source' => [ 'provider' => (string) ($prospect->provider->name ?? 'adf_import'), 'agent_type' => 'sales_chat', 'channel' => 'web_chat', ], 'dealership' => [ 'id' => (string) ($vendor->id ?? "dealer_{$rand}"), 'name' => (string) ($vendor->vendorname ?? 'Unknown'), ], 'lead' => [ 'id' => "lead_adf_" . time(), 'status' => 'new', 'contact' => [ 'first_name' => $names['first'] ?? null, 'last_name' => $names['last'] ?? null, 'email' => (string) ($contact->email ?? null), 'phone' => (string) ($contact->phone ?? null), ], 'vehicle_interests' => $vehicle ? [[ 'year' => (int) $vehicle->year, 'make' => (string) $vehicle->make, 'model' => (string) $vehicle->model, 'trim' => (string) ($vehicle->trim ?? null), 'condition' => (string) ($vehicle['status'] ?? 'new'), 'stock_number' => (string) ($vehicle->stock ?? null), ]] : [], 'intents' => [], 'score' => null, ], // transaction, conversation, events... ]; }
During migration, produce both formats from the same interaction. The ACP envelope is the source of truth; ADF-XML is deterministically derived from it.
async function deliverTocrm(envelope, dealer) { // ACP-native delivery (preferred) if (dealer.supportsAcp) { await fetch(dealer.acpEndpoint, { method: "POST", headers: { "Content-Type": "application/acp+json", "X-ACP-Version": "1.0.0", "X-ACP-Envelope-Id": envelope.acp.envelope_id, }, body: JSON.stringify(envelope) }); } // ADF-XML fallback (lossy but compatible) if (dealer.adfEndpoint) { const adfXml = acpToAdf(envelope); await fetch(dealer.adfEndpoint, { method: "POST", headers: { "Content-Type": "application/xml" }, body: adfXml }); } }
acpToAdf() converter<vehicle> elements.condition: "certified" → ADF status="used" (ADF has no certified option).