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 | Discovery label retained for compatibility; GET /, GET /ping, GET /schema, and GET /capabilities are available to every valid key |
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 |
vin:decode | GET /vin-decode |
catalog:read | GET /catalog, GET /catalog/{vertical}/makes|models|trims |
audit:read | GET /changelog |
product_updates:read | GET /product-updates |
users:read | GET /users — includes each user’s primary, authorized, and locked store scope. |
stores:read | GET /stores |
permissions:read | GET /permission-groups |
permissions:write | PUT /users/{id}/permission-groups — atomic replacement; send the current expected_group_ids, and set confirm_empty:true to remove every group. |
direct_connect:read | GET /direct-connect/numbers |
direct_connect:requests:read | GET /direct-connect/provisioning-requests |
direct_connect:requests:write | POST /direct-connect/provisioning-requests — creates an approval-gated request and never bypasses carrier approval. |
email_identities:read | GET /email-identities |
email_identities:write | PUT /email-identities — associates one address/type with a user; a system alias must use that user’s store domain. |
email_forwarding:read | GET /email-forwarding/rules |
lead_routing:read | GET /lead-routing/rules |
campaigns:read | GET /campaigns |
users:write | POST /users — requires Idempotency-Key; the initial password is hashed before staging and must be changed at first login. |
users:profile:write | PATCH /users/{id} — requires Idempotency-Key; safely updates profile and home/authorized/locked store scope, then revokes existing browser and mobile authority. |
users:lifecycle:write | POST /users/{id}/lifecycle — suspend revokes browser and mobile access; reactivate restores fresh-login authority. |
webhooks:manage | POST / GET / DELETE /webhooks |
Controlled-write fields. POST /direct-connect/provisioning-requests accepts phone, account_id, employee_id, phone_type, label, and keep_old; it creates an approval-gated request and never bypasses carrier approval. PUT /email-identities accepts employee_id, email, email_type, and system_alias; a system alias must use that user’s store domain. POST /users requires Idempotency-Key with first_name, last_name, username, initial_password, primary_store_id, role, department, and optional email, phone, permission_group_id; the initial password is hashed before staging and must be changed at first login. PATCH /users/{id} accepts first_name, last_name, role, department, primary_store_id, store_ids, and store_lock_account_id; store-scope changes revoke browser and mobile authority.
Push/pull coverage & boundaries
The API is bidirectional as a whole: partners push validated changes into DealerCRM and pull the current dealer-authoritative state back out. Read and write scopes are independent, so each credential can be least-privilege.
| Area | Push into DealerCRM | Pull from DealerCRM | Important boundary |
|---|---|---|---|
| Customers | Create or match/enrich with POST /customers | List or fetch one with GET /customers | This is safe identity enrichment, not arbitrary replacement of assignment, DNC, or status fields. |
| Leads | Submit with POST /leads | Read the resulting lead and follow-up timeline through GET /activities?customer_id=… | No unbounded tenant-wide lead dump. |
| Activities | Log communications and 11 typed dealership events | Read a customer's complete history, page by page | Use next_before_id as before_id; logging an opt-out, payment, or decision records the fact, not the external action. |
| Inventory | Change an existing unit's real-time status | List all/active/sold units or fetch one | Bulk unit, photo, and price ingestion stays on the dealer's feed/DMS pipeline. |
| Reference data | — | VIN decode and all 12 make/model/trim catalogs | Reference-data scopes are issued to dealer-less data-partner credentials. |
| Webhooks | Register/list/deactivate endpoints | Receive signed confirmations of successful Partner API writes | Native DealerCRM changes are pulled from the read endpoints today; they are not yet emitted to partner webhooks. |
Availability & rollout status
Released now: the routes, fields, scopes, activity kinds, limits, and Partner API-originated webhook events on this page are production contract. A key can call GET /schema or its alias GET /capabilities to retrieve the live machine-readable catalog and its own grants.
Released now: GET /changelog exposes tenant-scoped audit history under audit:read, and GET /product-updates exposes the published dealer-facing What’s New feed under product_updates:read. Native DealerCRM-change webhooks are not yet emitted; use the bounded pull endpoints to reconcile them.
Why this distinction matters: DealerCRM does not publish internal/admin-only or incomplete capabilities simply because they exist in the product. A capability is added here only after its authorization, tenant boundaries, data contract, limits, and replay behavior are ready for third-party use. When that happens, it appears in GET /schema and this public reference together.
Pull data (read)
GET/ping — any valid key
Returns the key's account, plan, and granted scopes. The best first call.
GET/schema — any valid key
A live, machine-readable catalog of everything this API can do — every route and direction, scope, resource, activity kind, exact write field rules (required/default/nullable values and length/range/item caps), conventions, and webhook boundary — plus your key's granted scopes. GET /capabilities is an exact alias for GET /schema. It's generated from the running API, so it always matches reality and grows automatically as new capabilities ship. Point your integration tooling at it to auto-discover what's available.
curl https://dealercrm.com/partner/v1/schema -H "Authorization: Bearer $DEALERCRM_KEY"
GET/changelog — audit:read
Tenant-scoped, newest-first audit history. Filter by category, entity_type, or entity_id; use the returned next_before timestamp to continue. Network addresses and internal support-session attribution are not shared with partner credentials.
curl "https://dealercrm.com/partner/v1/changelog?limit=100" \ -H "Authorization: Bearer $DEALERCRM_KEY"
GET/product-updates — product_updates:read
The complete published dealer-facing What’s New feed. Page with limit and the returned next_before timestamp so no published note is omitted. Every item is marked rollout_status: "released"; unpublished or internal operational notes are never returned.
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"
GET/vin-decode — vin:decode
Decodes a 17-character VIN to the full vehicle record: year / make / model / trim, typed specifications (dimensions, weights, towing, fuel economy, ~50 feature flags), standard_features, granular equipment grouped by category, factory packages with their component lists and prices, and — when a window sticker exists for the exact VIN — a sticker block with the as-built exterior/interior colors, itemized priced factory options (package options carry an includes list), unit base/total MSRP, destination charge, assembly plant, EPA figures, and warranty. sticker.data_source is factory_label (original manufacturer Monroney) or simulation (clearly-flagged build-data reconstruction — treat as believed-accurate, not authoritative). catalog_matched reports whether the VIN resolved to a full catalog trim. Pass &detail=basic for a light identity-only response. Read-only — no CRM data is read or written. An invalid VIN returns 200 with valid:false and an error reason; a missing vin parameter returns 400 vin_required.
curl "https://dealercrm.com/partner/v1/vin-decode?vin=4T1DAACK8SU046040" \
-H "Authorization: Bearer $DEALERCRM_KEY"
# → { "object":"vin_decode", "vin":"4T1DAACK8SU046040", "valid":true, "decoded":true,
# "catalog_matched":true, "vehicle":{ "year":2025, "make":"Toyota", "model":"Camry",
# "trim":"LE", "msrp":28855, "engine":"2.5L 4-Cylinder Gas Engine", "drivetrain":"FWD",
# "colors":{ "exterior":["Wind Chill Pearl", ...], "interior":[...] },
# "standard_features":{ ... }, "equipment":{ "Engine":[...], "Safety":[...] },
# "packages":[{ "package_code":"...", "package_name":"...", "msrp_adder":1200,
# "components":["..."] }],
# "specs":{ ...typed dimensions/weights/towing/mpg/feature flags... },
# "sticker":{ "data_source":"factory_label", "exterior_color":"Wind Chill Pearl",
# "options":[{ "code":"...", "msrp":840, "description":"...",
# "includes":["..."] }],
# "base_msrp":28855, "total_msrp":31120, ... } } }GET/catalog — catalog:read
The vehicle reference catalog: make → model → trim for every vertical DealerCRM carries — cars & trucks, RVs, marine, powersports, trailers, golf carts, agriculture, construction, commercial trucks, outdoor power equipment, manufactured housing, and e-bikes. GET /catalog lists the vertical codes; then drill down. For RVs, make is the brand and trim is the floorplan.
curl https://dealercrm.com/partner/v1/catalog -H "Authorization: Bearer $DEALERCRM_KEY"
# → { "object":"catalog", "verticals":[ {"code":"auto","label":"Cars & Trucks"}, {"code":"rv","label":"RVs"}, ... ] }GET/catalog/{vertical}/makes — catalog:read
Every make (RVs: brand) in the vertical, alphabetical.
curl https://dealercrm.com/partner/v1/catalog/auto/makes -H "Authorization: Bearer $DEALERCRM_KEY"
GET/catalog/{vertical}/models — catalog:read
Requires ?make=. Returns each model with its available years, a category (body type / RV type / equipment class where known), and how many trims it carries.
curl "https://dealercrm.com/partner/v1/catalog/rv/models?make=Jayco" -H "Authorization: Bearer $DEALERCRM_KEY"
GET/catalog/{vertical}/trims — catalog:read
Requires ?make=&model=; year optional. Returns the trim rows (RVs: floorplans), newest year first.
curl "https://dealercrm.com/partner/v1/catalog/auto/trims?make=Toyota&model=Camry&year=2025" \
-H "Authorization: Bearer $DEALERCRM_KEY"
# → { "object":"list", "items":[ { "year":2025, "make":"Toyota", "model":"Camry", "trim":"LE", "category":"Sedan" }, ... ] }Usage guidelines & limits
These limits protect the platform for every dealer and partner. They are enforced automatically on every request — design your integration to live comfortably inside them.
| Limit | Default | When exceeded |
|---|---|---|
| Requests per minute (per key) | 120 | 429 rate_limited + Retry-After header |
| Requests per rolling 24 h (per account) | 50,000 | 429 rate_limited + Retry-After |
| Request body size | 256 KB | 413 |
| Rows per list response | ≤ 100 (catalog reference lists are bounded server-side) | use the cursor / narrower filters |
| Request duration | 10 s hard cap | request is terminated |
| Key lifetime | set per key at issue time | 401 key_expired — request a fresh key |
Best practices
- Honor
Retry-Afteron every 429 and back off exponentially. Do not tight-loop retries — repeated bursts count against your daily quota. - Cache catalog data. Makes/models/trims change on the scale of days, not seconds. Pull once, cache 24 h; never proxy live end-user traffic straight through to
/catalog/*or/vin-decode. - Cache basic VIN identity indefinitely. For the full response, refresh after a miss or periodically because DealerCRM may add richer equipment, package, or sticker data later.
- Use webhooks for Partner API write confirmations. Avoid polling for your own writes. Use bounded scheduled pulls to reconcile native DealerCRM changes, which are not currently emitted to partner webhooks.
- One integration, one key. Ask the dealer for a separate labeled key per system so usage is attributable and a single key can be rotated or revoked without breaking everything else.
Every authenticated request is logged and metered per key. Sustained abuse (rate-limit hammering, scraping patterns, sharing keys across products) can result in a key being revoked.
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. Accepted top-level fields: customer, vehicle, trade, intent, customer_owns_vehicle, consent, source, store_account_id, and external_id. Customer accepts first, last, full, email, phone, comments; vehicle accepts year, make, model, trim, vin, stock, condition, color_ext, mileage, price, comments; trade accepts year/make/model/VIN/mileage; consent accepts nullable sms_optin, email_optin, and phone_optin. Unknown fields fail validation; use GET /schema for every type, default, nullability rule, and limit.
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, external_id. At least one name/email/phone identifier is required. A match fills missing identity data; it does not overwrite assignment, DNC, or customer-status fields.
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 |
originating_lead_activity_id | appointment | optional DealerCRM lead row id; accepted only for the same dealer and customer, and returned with the attribution method |
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…) |
store_account_id | all | optional store id; must belong to the credential's dealer group |
external_id | all | your stable source-record id (≤120); may be used instead of the Idempotency-Key header |
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; send either status or an exact sale_status_type, plus optional external_id. 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 with replay:true — no second write. Header keys must be unique across the whole subscription; body external_id values are namespaced by resource.
-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 confirmations when a write succeeds through this Partner API. Requires the webhooks:manage scope and webhooks enabled on the dealer's subscription. URLs on localhost, private/link-local networks, cloud metadata hosts, DealerCRM, or voAIce are rejected.
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} deactivates one. These events describe Partner API-originated writes; native DealerCRM edits are reconciled with the read endpoints.
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) |
| 500 | internal / query_failed / write_failed / decode_failed | Transient server failure; retry with backoff and the same idempotency key |
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",
"lead_source_canonical": "Partner API", "lead_source_category": "other",
"customer_intent": "sales", "lead_classification": "sales",
"vehicle": { "year": 2024, "make": "Ford", "model": "F-150", "vin": "…" },
"is_olivia": false } Enums & codes
| Field | Values |
|---|---|
lead intent | sales · acquisition · consignment · 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())End-to-end integration playbooks
Use these sequences as the production skeleton around the individual cookbook recipes.
1. Bootstrap and capability negotiation
Call GET /ping, then GET /schema. Persist the API version, granted scopes, route catalog, accepted fields, defaults, caps, and event catalog. Fail setup early when a required scope is absent.
2. Safe write pipeline
Choose a stable source id before sending. Put it in Idempotency-Key (preferred) or external_id; submit the write; retain the HTTP status and returned DealerCRM ids; retry network/5xx/429 failures with the same id and exponential backoff. Do not retry a 4xx response unchanged.
3. Lead-to-appointment workflow
POST /leads and keep the returned customer_id and activity_id. Log calls/texts with POST /activities. When an appointment is set, send kind:"appointment", scheduled_at, status/type, and the original lead's activity_id as originating_lead_activity_id. Reconcile with GET /activities?customer_id=…, repeating with before_id from next_before_id until it is null.
4. Inventory status reconciliation
Page through GET /inventory?status=all&limit=100, compare by DealerCRM inventory_id (or VIN/stock), and send only changed availability through POST /inventory. The endpoint does not create units or replace feed pricing/photos.
5. Service-history ingestion
Resolve/create the customer, then send each RO milestone as kind:"service_ro" with ro_number, status, customer-pay amount, vehicle, body, and a stable source id. This creates a Repair Order timeline card; it does not alter the source DMS.
6. Webhook receiver and reconciliation
Register a webhook, store its one-time secret, verify the HMAC against the untouched raw body, enforce a five-minute timestamp window, return a fast 2xx, and process asynchronously. Deduplicate on X-DealerCRM-Delivery or body id. Webhooks confirm Partner API writes; continue scheduled bounded pulls for native DealerCRM changes.
Cookbook — 100 real integrations
Concrete recipes for how different dealer tools use this API. Every supported route and every activity kind appears below. The workhorse is POST /activities; new opportunities go through POST /leads; identity through POST /customers; real-time inventory status through POST /inventory; authoritative state through bounded GET calls; and webhooks confirm Partner API-originated writes. Bodies show the key fields — add occurred_at, employee_name, meta, and a stable Idempotency-Key freely. A recipe that logs consent, a payment, or a decision records that fact on the timeline; it does not execute the action or mutate a separate system of record.
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 | Log a service-line call | POST /activities · kind=call_inbound, vehicle, meta={department:"service"}; records context, does not change routing |
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 | Log SMS opt-in evidence | POST /activities · kind=note, meta={sms_optin:true, method}; timeline evidence only |
| 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 | Log an unsubscribe event | POST /activities · kind=email_outbound, outcome=unsubscribed; does not itself change DNC |
| 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 quote issued after lead resolution | POST /leads, then POST /activities · kind=quote, amount, reference, vehicle |
| 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 unit | 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 | Confirm your status write asynchronously | subscribe to inventory.status_changed; native CRM inventory edits still require reconciliation pulls |
| 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 meeting handoff | POST /activities · kind=meeting, employee_name, body, vehicle |
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 | Vehicle delivered | POST /activities · kind=delivery, vehicle, body, reference |
| 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 / write-up started | POST /leads, then POST /activities · kind=write_up, amount, vehicle, body |
| 78 | Prequalified online | POST /activities · kind=credit_app, status="Prequalified", 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 | Test drive completed | POST /activities · kind=test_drive, vehicle, body, employee_name |
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 | OliviaAI-set appointment | POST /activities · kind=appointment, scheduled_at, appointment_status="confirmed", meta={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 plus full history | GET /customers/{id} + GET /activities?customer_id=…; repeat with before_id=next_before_id until null |
| 96 | Inventory snapshot for BI | GET /inventory?status=all (paginate) + GET /inventory/{id} for a known unit |
| 97 | Verify key and negotiate capabilities | GET / or GET /ping, then GET /schema (exact alias: GET /capabilities) |
| 98 | Decode and enrich a VIN | GET /vin-decode?vin=…; add detail=basic for identity-only |
| 99 | Build a cached vehicle picker | GET /catalog → GET /catalog/{vertical}/makes → GET /catalog/{vertical}/models?make= → GET /catalog/{vertical}/trims?make=&model= |
| 100 | Manage write-confirmation webhooks | POST /webhooks, GET /webhooks, then DELETE /webhooks/{id}; verify HMAC and dedupe deliveries |