Getting started
Base URL
https://dealercrm.com/partner/v1
dealercrm.com — not www.. The www redirect strips the Authorization header, so requests would fail auth.Conventions
| Topic | Detail |
|---|---|
| Format | JSON in and out. Send Content-Type: application/json on writes. |
| Lists | { "object":"list", "items":[…], "next_cursor":<id|null>, "limit":N }. Page with ?limit= (max 100) and ?after=<last id>; stop when next_cursor is null. |
| Idempotency | Send Idempotency-Key: <unique> on any POST. A repeat returns the original result instead of duplicating. |
| Rate limits | Per-minute limit + daily quota per key. Over → 429 with a Retry-After header (seconds). |
| Body size | Requests over 256 KB → 413. |
| Timestamps | created / activity times are unix seconds; expires_at and webhook times are ISO 8601. |
Authentication
Every request carries the API key the dealer created for you, as a Bearer token:
Authorization: Bearer dcp_live_xxxxxxxx.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
X-Api-Key: <key> is accepted as an alternative. The key is shown only once when created — store it securely; if it's lost or exposed, the dealer rotates it (a rotate issues a new key and instantly disables the old one). Verify a key works with a single call:
curl https://dealercrm.com/partner/v1/ping \ -H "Authorization: Bearer $DEALERCRM_KEY"
Scopes
A key can only reach the endpoints its scopes cover (the dealer chooses them per key). A call outside scope returns 403 insufficient_scope.
Read and write are independent per area. Each major area — Customers, Inventory, Activities — has a separate :read and :write scope, so a key can be granted read, write, both, or neither for each area, in any combination (e.g. a marketing tool that only logs emails gets activities:write alone; a listing site that only reads inventory gets inventory:read alone). The dealer picks these per key in the DealerCRM admin, and a key can never exceed what the dealer's subscription allows.
| Scope | Grants |
|---|---|
ping | GET /ping |
customers:read | GET /customers, GET /customers/{id} |
customers:write | POST /customers |
inventory:read | GET /inventory, GET /inventory/{id} |
inventory:write | POST /inventory (status) |
activities:read | GET /activities |
activities:write | POST /activities |
leads:write | POST /leads |
webhooks:manage | POST / GET / DELETE /webhooks |
Pull data (read)
GET/ping — any valid key
Returns the key's account, plan, and granted scopes. The best first call.
GET/customers — customers:read
Paginated. Query: limit (≤100), after (last customer_id).
curl "https://dealercrm.com/partner/v1/customers?limit=50" \ -H "Authorization: Bearer $DEALERCRM_KEY"
{
"object": "list",
"items": [
{ "customer_id": 10000001, "first_name": "Jane", "last_name": "Doe",
"full_name": "Jane Doe", "email": "jane@example.com",
"phone_mobile": "3105551234", "do_not_contact": false,
"preferred_contact_channel": "sms",
"point_person": { "id": 42, "name": "Alex Rivera" },
"customer_since": "2025-03-01", "created": 1740787200 }
],
"next_cursor": 10000001,
"limit": 50
}GET/customers/{id} — customers:read
One customer, plus full phones[], emails[], and names[] arrays.
GET/inventory — inventory:read
Query: limit, after (last inventory_id), status = active (default), sold, or all.
curl "https://dealercrm.com/partner/v1/inventory?status=active&limit=25" \ -H "Authorization: Bearer $DEALERCRM_KEY"
{ "object":"list", "items":[
{ "inventory_id": 139450, "year": 2024, "make": "Ford", "model": "F-150",
"trim": "XLT", "vin": "1FTFW1E86PKE00000", "stock_number": "A1234",
"condition": "new", "mileage": 12, "ext_color": "Oxford White",
"msrp": 58230, "asking_price": 55990, "internet_price": 54990,
"sale_status_type": 1, "primary_image_url": "https://…",
"updated": "2026-07-09T16:15:49Z" }
], "next_cursor": 139450, "limit": 25, "status": "active" }GET/inventory/{id} — inventory:read
One unit by inventory_id.
GET/activities — activities:read
Requires ?customer_id= (the API never dumps a whole tenant). Newest first; limit ≤100.
curl "https://dealercrm.com/partner/v1/activities?customer_id=10000001&limit=20" \ -H "Authorization: Bearer $DEALERCRM_KEY"
Push data (write)
All writes accept an Idempotency-Key header. Unknown fields, a bad VIN/email/phone, or an out-of-range value return 422 validation_failed with a per-field details array — nothing is written on a validation failure.
POST/leads — leads:write
Submits a lead through the same pipeline as native leads (duplicate detection, enrichment, owned-vehicle attach, rep routing). Requires at least one of customer.email / phone / full.
curl -X POST https://dealercrm.com/partner/v1/leads \
-H "Authorization: Bearer $DEALERCRM_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: acme-lead-9931" \
-d '{
"customer": { "full": "Jane Doe", "email": "jane@example.com", "phone": "310-555-1234" },
"vehicle": { "year": 2024, "make": "Ford", "model": "F-150", "vin": "1FTFW1E86PKE00000" },
"trade": { "year": 2019, "make": "Toyota", "model": "Camry" },
"intent": "sales",
"source": "Acme Marketing",
"consent": { "sms_optin": true },
"store_account_id": 1
}'{ "object":"lead", "lead_id": 812, "customer_id": 10000042,
"activity_id": 48500441, "dedup_method": "new_customer",
"intent": "sales", "is_existing_customer": false }POST/customers — customers:write
Creates or matches a customer (duplicate detection runs first; on a match it enriches instead of duplicating). Fields: first_name, last_name, full_name, email, phone, owned_vin, store_account_id. At least one identifier required.
curl -X POST https://dealercrm.com/partner/v1/customers \
-H "Authorization: Bearer $DEALERCRM_KEY" \
-H "Content-Type: application/json" \
-d '{ "full_name": "Jane Doe", "email": "jane@example.com", "phone": "3105551234" }'{ "object":"customer", "customer_id": 10000042, "created": true, "dedup_method": null }POST/activities — activities:write
Logs an interaction on an existing customer (resolve by customer_id, or by email/phone; it never creates a customer — use /leads or /customers for that). This one endpoint represents almost any "X happened with this customer" — calls, texts, emails, notes — with rich, typed fields. The full JSON you send is also stored verbatim for audit.
| Field | Applies to | Notes |
|---|---|---|
kind (req) | all | comms: note, call_inbound, call_outbound, text_inbound, text_outbound, email_inbound, email_outbound · first-class typed events: appraisal, trade_in, credit_app, service_ro, appointment, test_drive, meeting, write_up, quote, delivery, equity_alert (see the table below the examples) |
customer_id / email / phone | all | at least one — resolves the existing customer |
body | all | message / summary / notes (≤20k) |
occurred_at | all | ISO time it happened (defaults to now) |
employee_name | all | rep/agent involved |
duration_seconds | calls | call length |
recording_url | calls | link to the recording (shown on the timeline) |
transcript | calls | full transcript (≤100k; becomes the body) |
from / to | calls, email, text | endpoints (numbers / addresses) |
outcome | calls, email | calls: connected/voicemail/no_answer/missed/busy · email: sent/delivered/opened/clicked/bounced/unsubscribed |
subject | email subject | |
campaign | email, marketing | campaign label (also becomes the lead source) |
status | email, text, credit_app, service_ro | free-form status label (delivery status, credit decision, RO status) |
amount | appraisal, trade_in, credit_app, service_ro, quote, write_up, equity_alert | money — meaning is per-kind: appraisal offer, trade allowance, RO customer-pay, quote price, equity position |
reference | typed events | your external number (appraisal id, RO #, quote #) |
ro_number | service_ro | the repair-order number |
lender | credit_app | lender / bank name |
scheduled_at | appointment | ISO time of the appointment (defaults to occurred_at) |
appointment_type | appointment | meeting (default) · delivery · service · contact · other |
appointment_status | appointment | scheduled · confirmed · completed · no_show · canceled · rescheduled |
vehicle | all | { year, make, model, vin, stock } — the unit discussed / appraised / traded |
meta | all | any extra JSON (≤6k) — stored verbatim (scores, sentiment, IDs, extra amounts…) |
Example — a call-tracking tool pushes a recorded, transcribed call
curl -X POST https://dealercrm.com/partner/v1/activities \
-H "Authorization: Bearer $DEALERCRM_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: calltrack-8891" \
-d '{
"customer_id": 10000042,
"kind": "call_inbound",
"duration_seconds": 214,
"outcome": "connected",
"from": "+13105551234", "to": "+13235550100",
"recording_url": "https://calls.example.com/rec/8891.mp3",
"transcript": "Customer: Is the F-150 still available?\nAgent: Yes — want to come see it Saturday?",
"employee_name": "Alex Rivera",
"occurred_at": "2026-07-09T18:30:00Z",
"meta": { "call_score": 92, "sentiment": "positive" }
}'Example — a marketing platform reports an email it sent
curl -X POST https://dealercrm.com/partner/v1/activities \
-H "Authorization: Bearer $DEALERCRM_KEY" -H "Content-Type: application/json" \
-d '{ "email": "jane@example.com", "kind": "email_outbound",
"subject": "Your F-150 is waiting", "campaign": "July Truck Event",
"outcome": "sent", "body": "Hi Jane — the truck you looked at is still here." }'Response: { "object":"activity", "activity_id": 48567841, "customer_id": 10000042, "kind": "call_inbound" }
First-class typed events
Beyond comms, kind also accepts real dealership events. Each lands as its own labeled card on the customer's profile timeline (an Appraisal, a Trade-In, a Credit App, a Repair Order, an Appt Scheduled) — never a generic note — and appointments additionally count as scheduled appointments. Use the typed fields (amount, status, lender, ro_number, scheduled_at, appointment_status, vehicle) and add anything else in meta.
kind | Shows as | Key fields |
|---|---|---|
appraisal | Appraisal | amount (offer), vehicle, reference, body |
trade_in | Trade-In | amount (allowance), vehicle, body |
credit_app | Credit App | status (decision), lender, amount, body |
service_ro | Repair Order | ro_number, status, amount (customer pay), vehicle |
appointment | Appt Scheduled | scheduled_at, appointment_type, appointment_status, vehicle |
test_drive | Test Drive | vehicle, body |
meeting | Meeting | body, vehicle |
write_up | WriteUp | amount, vehicle, body |
quote | Quote | amount, vehicle, body |
delivery | Delivery | vehicle, body |
equity_alert | Note* | amount (equity), vehicle, body — *no native equity type; renders as a clearly-prefixed Note |
Example — an appraisal tool pushes a trade offer
curl -X POST https://dealercrm.com/partner/v1/activities \
-H "Authorization: Bearer $DEALERCRM_KEY" -H "Content-Type: application/json" \
-H "Idempotency-Key: appraise-5521" \
-d '{
"email": "jane@example.com",
"kind": "appraisal",
"amount": 18500,
"reference": "ICO-5521",
"vehicle": { "year": 2021, "make": "Toyota", "model": "RAV4", "vin": "2T3P1RFV5MW000000" },
"body": "Instant cash offer, valid 7 days. Clean CARFAX, 2 keys.",
"employee_name": "Appraisal Bot",
"meta": { "book": "ICO", "condition": "clean", "expires": "2026-07-16" }
}'Example — a BDC tool sets an appointment
curl -X POST https://dealercrm.com/partner/v1/activities \
-H "Authorization: Bearer $DEALERCRM_KEY" -H "Content-Type: application/json" \
-d '{ "customer_id": 10000042, "kind": "appointment",
"scheduled_at": "2026-07-12T15:00:00Z", "appointment_type": "meeting",
"appointment_status": "confirmed", "employee_name": "BDC — Sam",
"body": "Confirmed Saturday 11am to see the RAV4." }'POST/inventory — inventory:write
A real-time status signal — mark a unit sold or available as it happens. Match the unit by inventory_id, vin, or stock. This is for status changes between feed syncs; bulk unit ingestion (new units, photos, full pricing) is handled by your inventory feed / DMS integration, not this endpoint.
# A real-time inventory system tells us a unit just sold:
curl -X POST https://dealercrm.com/partner/v1/inventory \
-H "Authorization: Bearer $DEALERCRM_KEY" -H "Content-Type: application/json" \
-d '{ "vin": "1FTFW1E86PKE00000", "status": "sold" }'{ "object":"inventory", "inventory_id": 139450, "vin": "1FTFW1E86PKE00000",
"stock": "A1234", "sale_status_type": 5, "status": "sold",
"previous_sale_status_type": 1 }Use "status": "available" when a deal falls through, or "sale_status_type": <code> for an exact code. A change fires the inventory.status_changed webhook.
Duplicate handling
You never have to worry about creating duplicate customers. Every lead/customer push runs through a dedup waterfall before anything is written; on a match we enrich the existing record instead of creating a new one.
The waterfall (first match wins)
| # | Signal | Match against |
|---|---|---|
| 1 | Owned VIN | a vehicle the customer owns (owned_vin / trade) — never a sales-inquiry/inventory VIN |
| 2 | case-insensitive | |
| 3 | Phone | digits-only, normalized to 10 digits |
| 4 | Trade VIN | the VIN being traded in |
On a hit, we fill in blank fields, add any new phone/email we didn't have, attach owned/trade vehicles, and log conflicts — we never overwrite good data or split the customer across two records. The response tells you exactly what happened:
| Field | Meaning |
|---|---|
dedup_method | owned_vehicle_vin / email / phone / trade_vin when matched; new_customer when created |
is_existing_customer (leads) | true if we matched an existing person |
created (customers) | true only when a brand-new record was made |
Exactly-once with Idempotency-Key
Dedup prevents different records for the same person. To make a retry of the same request safe (network blip, your queue re-runs), send an Idempotency-Key header (or a body external_id) that's stable for your source record. A repeat returns the original result — no second write.
-H "Idempotency-Key: yourcrm-lead-000123" # same key on every retry of this lead
Best practices
• Send as many identifiers as you have (email + phone + name) so the waterfall can match confidently.
• Use a stable, unique Idempotency-Key per source record — your own primary key is ideal.
• Only pass owned_vin for a vehicle the customer actually owns/trades — never the inventory VIN they're asking about (many people inquire on the same stock #).
• Activities and inventory-status calls resolve an existing record (customer, or unit by VIN/stock) and update in place — they never create duplicates.
Webhooks (real-time push to you)
Register an HTTPS endpoint to receive events as they happen. Requires the webhooks:manage scope and webhooks enabled on the dealer's subscription.
POST/webhooks
curl -X POST https://dealercrm.com/partner/v1/webhooks \
-H "Authorization: Bearer $DEALERCRM_KEY" \
-H "Content-Type: application/json" \
-d '{ "url": "https://your-app.com/hooks/dealercrm",
"events": ["lead.created","customer.created","activity.logged"] }'The response includes a secret — shown only once. Use ["*"] for all events. GET /webhooks lists them; DELETE /webhooks/{id} removes one.
Delivery
Each event is POSTed as JSON with these headers:
X-DealerCRM-Signature: t=1752083400,v1=<hex hmac-sha256> X-DealerCRM-Event: lead.created X-DealerCRM-Delivery: 8471
Body: { "id", "type", "created", "subdomain", "data" }. Failed deliveries retry with exponential backoff (up to ~24h); a chronically-failing endpoint is auto-disabled. Event types: lead.created, customer.created, customer.matched, activity.logged.
Verify the signature — required
Recompute the HMAC over <t>.<raw request body> with your signing secret, constant-time compare to v1, and reject if t is more than a few minutes old (replay protection).
// Node.js
const crypto = require('crypto');
function verify(rawBody, sigHeader, secret) {
const p = Object.fromEntries(sigHeader.split(',').map(kv => kv.split('=')));
const expected = crypto.createHmac('sha256', secret)
.update(p.t + '.' + rawBody).digest('hex');
const ok = expected.length === p.v1.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(p.v1));
if (!ok) throw new Error('bad signature');
if (Math.abs(Date.now()/1000 - Number(p.t)) > 300) throw new Error('stale');
}# Python
import hmac, hashlib, time
def verify(raw_body: bytes, sig_header: str, secret: str):
p = dict(kv.split('=', 1) for kv in sig_header.split(','))
expected = hmac.new(secret.encode(),
p['t'].encode() + b'.' + raw_body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, p['v1']):
raise ValueError('bad signature')
if abs(time.time() - int(p['t'])) > 300:
raise ValueError('stale')Error codes
Errors are { "error": { "code", "message", … } }. Validation errors add a details array of { field, message }.
| HTTP | code | Meaning |
|---|---|---|
| 400 | invalid_json / bad_id / customer_id_required | Malformed request |
| 401 | missing_key / invalid_key / key_expired | Bad, missing, or expired key |
| 402 | subscription_expired | The dealer's Partner API subscription lapsed |
| 403 | subscription_inactive / insufficient_scope / webhooks_disabled | Not enabled, or the key lacks the scope |
| 404 | not_found / no_route | No such record or endpoint |
| 413 | payload_too_large | Body over 256 KB |
| 422 | validation_failed | A field failed validation (see details) |
| 429 | rate_limited | Over the rate limit / daily quota (see Retry-After) |
Data dictionary
Object shapes
Customer (from GET /customers)
{ "customer_id": 10000001, // integer, stable id
"first_name": "Jane", "last_name": "Doe", "full_name": "Jane Doe",
"email": "jane@example.com", // primary email
"phone_mobile": "3105551234", "phone_home": null, "phone_work": null,
"do_not_contact": false,
"status_type": 1,
"preferred_contact_channel": "sms", // sms | email | call | null
"point_person": { "id": 42, "name": "Alex Rivera" }, // assigned rep, or null
"customer_since": "2025-03-01",
"created": 1740787200 } // unix seconds
// GET /customers/{id} adds: phones[], emails[], names[]Inventory unit (from GET /inventory)
{ "inventory_id": 139450, "year": 2024, "make": "Ford", "model": "F-150",
"trim": "XLT", "vin": "1FTFW1E86PKE00000", "stock_number": "A1234",
"condition": "new", "mileage": 12,
"ext_color": "Oxford White", "int_color": "Black",
"msrp": 58230, "asking_price": 55990, "internet_price": 54990,
"sale_status_type": 1, // 1 = available, 5 = sold
"primary_image_url": "https://…", "updated": "2026-07-09T16:15:49Z" }Activity (from GET /activities)
{ "id": 48567841, "customer_id": 10000001, "activity_type": 2,
"channel": "call", "direction": "1", // "1" inbound, "2" outbound
"created": 1752083400, "local_date": "2026-07-09",
"body": "Connected • 3m 34s • from +13105551234\nRecording: https://…\n\n",
"employee_name": "Alex Rivera", "lead_source": "partner_api",
"vehicle": { "year": 2024, "make": "Ford", "model": "F-150", "vin": "…" },
"is_olivia": false } Enums & codes
| Field | Values |
|---|---|
lead intent | sales · post_sales · parts · service · finance · other (default sales) |
activity kind | comms: note · call_inbound · call_outbound · text_inbound · text_outbound · email_inbound · email_outbound · typed events: appraisal · trade_in · credit_app · service_ro · appointment · test_drive · meeting · write_up · quote · delivery · equity_alert |
call outcome | connected · voicemail · no_answer · missed · busy |
email outcome | sent · delivered · opened · clicked · bounced · unsubscribed |
sale_status_type | 1 available (in stock) · 5 sold. Active inventory = anything except 5 and 6. Prefer status: "sold"|"available". |
dedup_method | owned_vehicle_vin · email · phone · trade_vin · new_customer |
| webhook events | lead.created · customer.created · customer.matched · activity.logged · inventory.status_changed · * |
Field formats
| Type | Format |
|---|---|
| VIN | 11–17 chars, A–Z + 0–9 excluding I/O/Q; uppercased automatically |
| Phone | any format accepted; normalized to 10 US digits (a leading 1 is dropped) |
| lowercased; standard address format | |
| Timestamps (in) | send ISO 8601 for occurred_at / expires_at (e.g. 2026-07-09T18:30:00Z) |
| Timestamps (out) | created / activity times are unix seconds; updated / webhook created are ISO 8601 |
| IDs | customer_id, inventory_id are integers |
Full code examples
Node.js — pull all active inventory
const KEY = process.env.DEALERCRM_KEY;
const BASE = 'https://dealercrm.com/partner/v1';
async function allInventory() {
const out = [];
let after = null;
do {
const url = new URL(BASE + '/inventory');
url.searchParams.set('status', 'active');
url.searchParams.set('limit', '100');
if (after) url.searchParams.set('after', after);
const res = await fetch(url, { headers: { Authorization: `Bearer ${KEY}` } });
if (!res.ok) throw new Error('HTTP ' + res.status);
const page = await res.json();
out.push(...page.items);
after = page.next_cursor;
} while (after);
return out;
}Python — submit a lead
import os, requests
KEY = os.environ["DEALERCRM_KEY"]
r = requests.post("https://dealercrm.com/partner/v1/leads",
headers={"Authorization": f"Bearer {KEY}",
"Idempotency-Key": "acme-lead-9931"},
json={"customer": {"full": "Jane Doe", "email": "jane@example.com",
"phone": "310-555-1234"},
"vehicle": {"year": 2024, "make": "Ford", "model": "F-150"},
"source": "Acme Marketing"})
r.raise_for_status()
print(r.json())Cookbook — 100 real integrations
Concrete recipes for how different dealer tools use this API. Every one maps to the endpoints above. The workhorse is POST /activities (any "X happened with this customer" — calls, texts, emails, notes, with typed fields + a free-form meta); new opportunities go through POST /leads; identity through POST /customers; real-time inventory status through POST /inventory; reads through GET; and webhooks push events back to you. Bodies below show the key fields — add occurred_at, employee_name, meta, and Idempotency-Key freely.
Call tracking & telephony
| # | Goal | Call + key fields |
|---|---|---|
| 1 | Inbound sales call with recording + transcript | POST /activities · kind=call_inbound, recording_url, transcript, duration_seconds, from, to, outcome=connected |
| 2 | Outbound follow-up call, connected | POST /activities · kind=call_outbound, outcome=connected, duration_seconds, employee_name |
| 3 | Missed inbound call | POST /activities · kind=call_inbound, outcome=missed, duration_seconds=0 |
| 4 | Voicemail left | POST /activities · kind=call_outbound, outcome=voicemail, recording_url |
| 5 | First-time caller (unknown number) → new opportunity | POST /leads {customer.phone}, then log the call activity |
| 6 | Call score / sentiment from call-analytics AI | POST /activities · kind=call_inbound, meta={call_score, sentiment} |
| 7 | Click-to-call / text-to-connect outcome | POST /activities · kind=call_outbound, outcome, duration_seconds |
| 8 | Service-line call (route to fixed-ops) | POST /activities · kind=call_inbound, vehicle, meta={department:"service"} |
Texting / SMS
| # | Goal | Call + key fields |
|---|---|---|
| 9 | Inbound text received | POST /activities · kind=text_inbound, body, from |
| 10 | Outbound text sent | POST /activities · kind=text_outbound, body, status=sent |
| 11 | Delivery receipt | POST /activities · kind=text_outbound, status=delivered, external_id |
| 12 | New number texts in → lead | POST /leads {customer.phone, comments} |
| 13 | SMS opt-in captured | POST /activities · kind=note, meta={sms_optin:true, method} |
| 14 | MMS with a photo | POST /activities · kind=text_inbound, body, meta={media:["https://…"]} |
Email & marketing automation
| # | Goal | Call + key fields |
|---|---|---|
| 15 | Marketing email sent | POST /activities · kind=email_outbound, subject, campaign, outcome=sent |
| 16 | Email opened | POST /activities · kind=email_outbound, subject, campaign, outcome=opened |
| 17 | Email link clicked | POST /activities · kind=email_outbound, outcome=clicked, meta={url} |
| 18 | Email bounced | POST /activities · kind=email_outbound, outcome=bounced |
| 19 | Unsubscribe | POST /activities · kind=email_outbound, outcome=unsubscribed |
| 20 | Inbound email reply | POST /activities · kind=email_inbound, subject, body, from |
| 21 | Nurture-sequence touch | POST /activities · kind=email_outbound, campaign, meta={step} |
| 22 | Newsletter signup | POST /activities · kind=note, campaign="Newsletter", meta={list} |
Web chat & bots
| # | Goal | Call + key fields |
|---|---|---|
| 23 | Chat lead captured | POST /leads {customer, comments, source:"Web Chat"} |
| 24 | Chat transcript logged | POST /activities · kind=note, body=transcript, meta={channel:"chat"} |
| 25 | Bot → human handoff | POST /activities · kind=note, meta={handoff:true} |
| 26 | After-hours chat request | POST /leads {intent:"sales", comments} |
| 27 | Chat appointment ask | POST /leads {comments:"wants Sat 10am"} |
| 28 | Chat satisfaction score | POST /activities · kind=note, meta={csat} |
Lead providers & marketplaces
| # | Goal | Call + key fields |
|---|---|---|
| 29 | New sales lead | POST /leads {customer, vehicle, source, intent:"sales"} |
| 30 | Price-alert lead | POST /leads {vehicle, source, comments} |
| 31 | Finance pre-qual lead | POST /leads {intent:"finance"} |
| 32 | Trade-in lead | POST /leads {trade:{year,make,model,vin}} |
| 33 | Service lead | POST /leads {intent:"service"} |
| 34 | Same person, second lead → auto-dedup | POST /leads (same email/phone) → response is_existing_customer:true |
Appraisal & trade-in
| # | Goal | Call + key fields |
|---|---|---|
| 35 | Trade appraisal received on a customer's car | POST /customers {owned_vin, full_name} → then POST /activities kind=appraisal, amount:18500, vehicle, meta={book:"ICO"} (→ Appraisal card) |
| 36 | Instant cash offer issued | POST /activities · kind=appraisal, amount:18500, vehicle, meta={ico:true, expires} |
| 37 | Condition report | POST /activities · kind=appraisal, vehicle, meta={condition, photos:[…]} |
| 38 | Payoff / lien captured | POST /activities · kind=trade_in, meta={payoff:14200, lienholder} |
| 39 | Customer started an online appraisal | POST /leads {trade} |
| 40 | Trade accepted into a deal | POST /activities · kind=trade_in, amount, vehicle, meta={accepted:true} (→ Trade-In card) |
| 41 | Appraisal expired | POST /activities · kind=note, meta={expired:true} |
Inventory, merchandising & pricing
| # | Goal | Call + key fields |
|---|---|---|
| 42 | Unit just sold | POST /inventory {vin, status:"sold"} |
| 43 | Deal fell through → unit back available | POST /inventory {vin, status:"available"} |
| 44 | Unit on hold / pending | POST /inventory {vin, sale_status_type:<code>} |
| 45 | Syndicate active inventory to a listing site | GET /inventory?status=active (paginate with after) |
| 46 | Nightly sold reconcile | GET /inventory?status=sold + POST /inventory for diffs |
| 47 | Look up a unit by your VIN | GET /inventory then match VIN, or GET /inventory/{id} |
| 48 | React to a sale in real time | subscribe webhook inventory.status_changed |
| 49 | Get one unit's full detail | GET /inventory/{id} |
| 50 | Feed the whole lot to a BI tool | GET /inventory?status=all (paginate) |
BDC / appointment setters
| # | Goal | Call + key fields |
|---|---|---|
| 51 | Contact attempt, no answer | POST /activities · kind=call_outbound, outcome=no_answer |
| 52 | Appointment set | POST /activities · kind=appointment, scheduled_at, appointment_type="meeting" (→ Appt Scheduled + counts) |
| 53 | Appointment confirmed | POST /activities · kind=appointment, appointment_status="confirmed", scheduled_at |
| 54 | Reminder sent | POST /activities · kind=text_outbound, campaign="Appt reminder" |
| 55 | No-show | POST /activities · kind=appointment, appointment_status="no_show" |
| 56 | Rescheduled | POST /activities · kind=appointment, appointment_status="rescheduled", scheduled_at |
| 57 | BDC → sales handoff | POST /activities · kind=note, employee_name |
F&I / credit
| # | Goal | Call + key fields |
|---|---|---|
| 58 | Credit app submitted | POST /activities · kind=credit_app, status="Submitted", lender, amount (→ Credit App card) |
| 59 | Prequal result / tier | POST /activities · kind=credit_app, status="Prequalified", meta={tier:"A"} |
| 60 | F&I product sold (VSC/GAP) | POST /activities · kind=note, meta={product:"VSC", amount:1899} |
| 61 | Deal funded | POST /activities · kind=note, meta={funded:true, amount} |
| 62 | Contract signed | POST /activities · kind=note, meta={contract_id} |
| 63 | Finance lead | POST /leads {intent:"finance"} |
Reviews & reputation
| # | Goal | Call + key fields |
|---|---|---|
| 64 | Review request sent | POST /activities · kind=text_outbound or email_outbound, campaign="Review request" |
| 65 | Review received | POST /activities · kind=note, meta={platform:"Google", rating:5, text} |
| 66 | Low-rating alert | POST /activities · kind=note, meta={rating:1, platform} |
| 67 | CSI survey sent | POST /activities · kind=email_outbound, campaign="CSI" |
| 68 | Survey response | POST /activities · kind=note, meta={csi_score} |
| 69 | Reputation opt-out | POST /activities · kind=note, meta={opt_out:true} |
Service & fixed-ops
| # | Goal | Call + key fields |
|---|---|---|
| 70 | Service appointment booked | POST /activities · kind=appointment, appointment_type="service", scheduled_at, vehicle |
| 71 | Repair order opened | POST /activities · kind=service_ro, ro_number:"12345", status="open", vehicle (→ Repair Order card) |
| 72 | Repair order closed | POST /activities · kind=service_ro, ro_number:"12345", status="closed", amount:642, vehicle |
| 73 | Declined service (upsell opportunity) | POST /activities · kind=service_ro, meta={declined:["brakes"]} |
| 74 | Open recall on an owned vehicle | POST /customers {owned_vin} → POST /activities kind=service_ro, meta={recall} |
| 75 | Service visit completed | POST /activities · kind=service_ro, status="completed", vehicle |
| 76 | Multi-point inspection results | POST /activities · kind=note, meta={mpi:{…}} |
Digital retail & website
| # | Goal | Call + key fields |
|---|---|---|
| 77 | Online deal started | POST /leads {intent:"sales", source:"Digital Retail", vehicle} |
| 78 | Prequalified online | POST /activities · kind=note, meta={tier} |
| 79 | Trade started online | POST /leads {trade} |
| 80 | Deposit paid | POST /activities · kind=note, meta={deposit:500} |
| 81 | Website form submission | POST /leads {customer, comments} |
| 82 | High-intent VDP view | POST /activities · kind=note, vehicle, meta={event:"VDP view"} |
| 83 | Deal / cart abandoned | POST /activities · kind=note, meta={abandoned:true} |
Equity mining & in-market
| # | Goal | Call + key fields |
|---|---|---|
| 84 | Equity alert | POST /activities · kind=equity_alert, amount:3200, vehicle, meta={payoff} |
| 85 | Lease-end opportunity | POST /activities · kind=note, meta={lease_end} |
| 86 | Service-to-sales opportunity | POST /activities · kind=note, meta={signal:"high-mileage"} |
| 87 | In-market score | POST /activities · kind=note, meta={in_market_score:88} |
| 88 | Upgrade / loyalty offer sent | POST /activities · kind=email_outbound, campaign="Upgrade" |
Other AI tools
| # | Goal | Call + key fields |
|---|---|---|
| 89 | AI conversation summary | POST /activities · kind=note, body=summary, meta={ai:true} |
| 90 | AI-qualified lead | POST /leads {source:"AI Assistant", comments} |
| 91 | AI-set appointment | POST /activities · kind=note, meta={appt_at, ai:true} |
| 92 | AI sentiment / intent tag | POST /activities · kind=note, meta={sentiment, intent} |
| 93 | AI voice agent handled a call | POST /activities · kind=call_inbound, transcript, recording_url, meta={ai:true} |
Analytics & data sync
| # | Goal | Call + key fields |
|---|---|---|
| 94 | Nightly customer export | GET /customers (paginate with after) |
| 95 | One customer's full history | GET /activities?customer_id=… |
| 96 | Inventory snapshot for BI | GET /inventory?status=all (paginate) |
| 97 | Verify a key / health check | GET /ping |
| 98 | Real-time event stream | POST /webhooks {events:["*"]} |
Video & engagement
| # | Goal | Call + key fields |
|---|---|---|
| 99 | Personalized video sent | POST /activities · kind=email_outbound or note, campaign="Video", meta={video_url} |
| 100 | Video watched | POST /activities · kind=note, meta={video_url, watched_seconds:47} |