DealerCRM

Partner API

Read from and write to a DealerCRM dealer's data — leads, customers, inventory, and activity — from your own application. This is the complete reference: authentication, every endpoint, exact data contracts, integration playbooks, webhooks, and errors.

The dealer creates an API key for you inside their DealerCRM admin and shares it securely. You send it as a Bearer token on every request. You never need a DealerCRM login yourself.

Getting started

Base URL

https://dealercrm.com/partner/v1
Always call the bare host dealercrm.com — not www.. The www redirect strips the Authorization header, so requests would fail auth.

Conventions

TopicDetail
FormatJSON 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.
IdempotencySend Idempotency-Key: <unique> on any POST. A repeat returns the original result instead of duplicating.
Rate limitsPer-minute limit + daily quota per key. Over → 429 with a Retry-After header (seconds).
Body sizeRequests over 256 KB → 413.
Timestampscreated / 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.

ScopeGrants
pingDiscovery label retained for compatibility; GET /, GET /ping, GET /schema, and GET /capabilities are available to every valid key
customers:readGET /customers, GET /customers/{id}
customers:writePOST /customers
inventory:readGET /inventory, GET /inventory/{id}
inventory:writePOST /inventory (status)
activities:readGET /activities
activities:writePOST /activities
leads:writePOST /leads
vin:decodeGET /vin-decode
catalog:readGET /catalog, GET /catalog/{vertical}/makes|models|trims
audit:readGET /changelog
product_updates:readGET /product-updates
users:readGET /users — includes each user’s primary, authorized, and locked store scope.
stores:readGET /stores
permissions:readGET /permission-groups
permissions:writePUT /users/{id}/permission-groups — atomic replacement; send the current expected_group_ids, and set confirm_empty:true to remove every group.
direct_connect:readGET /direct-connect/numbers
direct_connect:requests:readGET /direct-connect/provisioning-requests
direct_connect:requests:writePOST /direct-connect/provisioning-requests — creates an approval-gated request and never bypasses carrier approval.
email_identities:readGET /email-identities
email_identities:writePUT /email-identities — associates one address/type with a user; a system alias must use that user’s store domain.
email_forwarding:readGET /email-forwarding/rules
lead_routing:readGET /lead-routing/rules
campaigns:readGET /campaigns
users:writePOST /users — requires Idempotency-Key; the initial password is hashed before staging and must be changed at first login.
users:profile:writePATCH /users/{id} — requires Idempotency-Key; safely updates profile and home/authorized/locked store scope, then revokes existing browser and mobile authority.
users:lifecycle:writePOST /users/{id}/lifecycle — suspend revokes browser and mobile access; reactivate restores fresh-login authority.
webhooks:managePOST / 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.

AreaPush into DealerCRMPull from DealerCRMImportant boundary
CustomersCreate or match/enrich with POST /customersList or fetch one with GET /customersThis is safe identity enrichment, not arbitrary replacement of assignment, DNC, or status fields.
LeadsSubmit with POST /leadsRead the resulting lead and follow-up timeline through GET /activities?customer_id=…No unbounded tenant-wide lead dump.
ActivitiesLog communications and 11 typed dealership eventsRead a customer's complete history, page by pageUse next_before_id as before_id; logging an opt-out, payment, or decision records the fact, not the external action.
InventoryChange an existing unit's real-time statusList all/active/sold units or fetch oneBulk unit, photo, and price ingestion stays on the dealer's feed/DMS pipeline.
Reference data—VIN decode and all 12 make/model/trim catalogsReference-data scopes are issued to dealer-less data-partner credentials.
WebhooksRegister/list/deactivate endpointsReceive signed confirmations of successful Partner API writesNative DealerCRM changes are pulled from the read endpoints today; they are not yet emitted to partner webhooks.
Reconciliation rule: treat successful write responses as the immediate receipt, webhooks as retryable asynchronous confirmation for Partner API-originated writes, and bounded GET pagination as the authoritative reconciliation path.

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.

LimitDefaultWhen exceeded
Requests per minute (per key)120429 rate_limited + Retry-After header
Requests per rolling 24 h (per account)50,000429 rate_limited + Retry-After
Request body size256 KB413
Rows per list response≤ 100 (catalog reference lists are bounded server-side)use the cursor / narrower filters
Request duration10 s hard caprequest is terminated
Key lifetimeset per key at issue time401 key_expired — request a fresh key

Best practices

  • Honor Retry-After on 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.

FieldApplies toNotes
kind (req)allcomms: 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 / phoneallat least one — resolves the existing customer
bodyallmessage / summary / notes (≤20k)
occurred_atallISO time it happened (defaults to now)
employee_nameallrep/agent involved
duration_secondscallscall length
recording_urlcallslink to the recording (shown on the timeline)
transcriptcallsfull transcript (≤100k; becomes the body)
from / tocalls, email, textendpoints (numbers / addresses)
outcomecalls, emailcalls: connected/voicemail/no_answer/missed/busy · email: sent/delivered/opened/clicked/bounced/unsubscribed
subjectemailemail subject
campaignemail, marketingcampaign label (also becomes the lead source)
statusemail, text, credit_app, service_rofree-form status label (delivery status, credit decision, RO status)
amountappraisal, trade_in, credit_app, service_ro, quote, write_up, equity_alertmoney — meaning is per-kind: appraisal offer, trade allowance, RO customer-pay, quote price, equity position
referencetyped eventsyour external number (appraisal id, RO #, quote #)
ro_numberservice_rothe repair-order number
lendercredit_applender / bank name
scheduled_atappointmentISO time of the appointment (defaults to occurred_at)
appointment_typeappointmentmeeting (default) · delivery · service · contact · other
appointment_statusappointmentscheduled · confirmed · completed · no_show · canceled · rescheduled
originating_lead_activity_idappointmentoptional DealerCRM lead row id; accepted only for the same dealer and customer, and returned with the attribution method
vehicleall{ year, make, model, vin, stock } — the unit discussed / appraised / traded
metaallany extra JSON (≤6k) — stored verbatim (scores, sentiment, IDs, extra amounts…)
store_account_idalloptional store id; must belong to the credential's dealer group
external_idallyour 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.

kindShows asKey fields
appraisalAppraisalamount (offer), vehicle, reference, body
trade_inTrade-Inamount (allowance), vehicle, body
credit_appCredit Appstatus (decision), lender, amount, body
service_roRepair Orderro_number, status, amount (customer pay), vehicle
appointmentAppt Scheduledscheduled_at, appointment_type, appointment_status, vehicle
test_driveTest Drivevehicle, body
meetingMeetingbody, vehicle
write_upWriteUpamount, vehicle, body
quoteQuoteamount, vehicle, body
deliveryDeliveryvehicle, body
equity_alertNote*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)

#SignalMatch against
1Owned VINa vehicle the customer owns (owned_vin / trade) — never a sales-inquiry/inventory VIN
2Emailcase-insensitive
3Phonedigits-only, normalized to 10 digits
4Trade VINthe 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:

FieldMeaning
dedup_methodowned_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 }.

HTTPcodeMeaning
400invalid_json / bad_id / customer_id_requiredMalformed request
401missing_key / invalid_key / key_expiredBad, missing, or expired key
402subscription_expiredThe dealer's Partner API subscription lapsed
403subscription_inactive / insufficient_scope / webhooks_disabledNot enabled, or the key lacks the scope
404not_found / no_routeNo such record or endpoint
413payload_too_largeBody over 256 KB
422validation_failedA field failed validation (see details)
429rate_limitedOver the rate limit / daily quota (see Retry-After)
500internal / query_failed / write_failed / decode_failedTransient 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

FieldValues
lead intentsales · acquisition · consignment · post_sales · parts · service · finance · other (default sales)
activity kindcomms: 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 outcomeconnected · voicemail · no_answer · missed · busy
email outcomesent · delivered · opened · clicked · bounced · unsubscribed
sale_status_type1 available (in stock) · 5 sold. Active inventory = anything except 5 and 6. Prefer status: "sold"|"available".
dedup_methodowned_vehicle_vin · email · phone · trade_vin · new_customer
webhook eventslead.created · customer.created · customer.matched · activity.logged · inventory.status_changed · *

Field formats

TypeFormat
VIN11–17 chars, A–Z + 0–9 excluding I/O/Q; uppercased automatically
Phoneany format accepted; normalized to 10 US digits (a leading 1 is dropped)
Emaillowercased; 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
IDscustomer_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

#GoalCall + key fields
1Inbound sales call with recording + transcriptPOST /activities · kind=call_inbound, recording_url, transcript, duration_seconds, from, to, outcome=connected
2Outbound follow-up call, connectedPOST /activities · kind=call_outbound, outcome=connected, duration_seconds, employee_name
3Missed inbound callPOST /activities · kind=call_inbound, outcome=missed, duration_seconds=0
4Voicemail leftPOST /activities · kind=call_outbound, outcome=voicemail, recording_url
5First-time caller (unknown number) → new opportunityPOST /leads {customer.phone}, then log the call activity
6Call score / sentiment from call-analytics AIPOST /activities · kind=call_inbound, meta={call_score, sentiment}
7Click-to-call / text-to-connect outcomePOST /activities · kind=call_outbound, outcome, duration_seconds
8Log a service-line callPOST /activities · kind=call_inbound, vehicle, meta={department:"service"}; records context, does not change routing

Texting / SMS

#GoalCall + key fields
9Inbound text receivedPOST /activities · kind=text_inbound, body, from
10Outbound text sentPOST /activities · kind=text_outbound, body, status=sent
11Delivery receiptPOST /activities · kind=text_outbound, status=delivered, external_id
12New number texts in → leadPOST /leads {customer.phone, comments}
13Log SMS opt-in evidencePOST /activities · kind=note, meta={sms_optin:true, method}; timeline evidence only
14MMS with a photoPOST /activities · kind=text_inbound, body, meta={media:["https://…"]}

Email & marketing automation

#GoalCall + key fields
15Marketing email sentPOST /activities · kind=email_outbound, subject, campaign, outcome=sent
16Email openedPOST /activities · kind=email_outbound, subject, campaign, outcome=opened
17Email link clickedPOST /activities · kind=email_outbound, outcome=clicked, meta={url}
18Email bouncedPOST /activities · kind=email_outbound, outcome=bounced
19Log an unsubscribe eventPOST /activities · kind=email_outbound, outcome=unsubscribed; does not itself change DNC
20Inbound email replyPOST /activities · kind=email_inbound, subject, body, from
21Nurture-sequence touchPOST /activities · kind=email_outbound, campaign, meta={step}
22Newsletter signupPOST /activities · kind=note, campaign="Newsletter", meta={list}

Web chat & bots

#GoalCall + key fields
23Chat lead capturedPOST /leads {customer, comments, source:"Web Chat"}
24Chat transcript loggedPOST /activities · kind=note, body=transcript, meta={channel:"chat"}
25Bot → human handoffPOST /activities · kind=note, meta={handoff:true}
26After-hours chat requestPOST /leads {intent:"sales", comments}
27Chat appointment askPOST /leads {comments:"wants Sat 10am"}
28Chat satisfaction scorePOST /activities · kind=note, meta={csat}

Lead providers & marketplaces

#GoalCall + key fields
29New sales leadPOST /leads {customer, vehicle, source, intent:"sales"}
30Price quote issued after lead resolutionPOST /leads, then POST /activities · kind=quote, amount, reference, vehicle
31Finance pre-qual leadPOST /leads {intent:"finance"}
32Trade-in leadPOST /leads {trade:{year,make,model,vin}}
33Service leadPOST /leads {intent:"service"}
34Same person, second lead → auto-dedupPOST /leads (same email/phone) → response is_existing_customer:true

Appraisal & trade-in

#GoalCall + key fields
35Trade appraisal received on a customer's unitPOST /customers {owned_vin, full_name} → then POST /activities kind=appraisal, amount:18500, vehicle, meta={book:"ICO"} (→ Appraisal card)
36Instant cash offer issuedPOST /activities · kind=appraisal, amount:18500, vehicle, meta={ico:true, expires}
37Condition reportPOST /activities · kind=appraisal, vehicle, meta={condition, photos:[…]}
38Payoff / lien capturedPOST /activities · kind=trade_in, meta={payoff:14200, lienholder}
39Customer started an online appraisalPOST /leads {trade}
40Trade accepted into a dealPOST /activities · kind=trade_in, amount, vehicle, meta={accepted:true} (→ Trade-In card)
41Appraisal expiredPOST /activities · kind=note, meta={expired:true}

Inventory, merchandising & pricing

#GoalCall + key fields
42Unit just soldPOST /inventory {vin, status:"sold"}
43Deal fell through → unit back availablePOST /inventory {vin, status:"available"}
44Unit on hold / pendingPOST /inventory {vin, sale_status_type:<code>}
45Syndicate active inventory to a listing siteGET /inventory?status=active (paginate with after)
46Nightly sold reconcileGET /inventory?status=sold + POST /inventory for diffs
47Look up a unit by your VINGET /inventory then match VIN, or GET /inventory/{id}
48Confirm your status write asynchronouslysubscribe to inventory.status_changed; native CRM inventory edits still require reconciliation pulls
49Get one unit's full detailGET /inventory/{id}
50Feed the whole lot to a BI toolGET /inventory?status=all (paginate)

BDC / appointment setters

#GoalCall + key fields
51Contact attempt, no answerPOST /activities · kind=call_outbound, outcome=no_answer
52Appointment setPOST /activities · kind=appointment, scheduled_at, appointment_type="meeting" (→ Appt Scheduled + counts)
53Appointment confirmedPOST /activities · kind=appointment, appointment_status="confirmed", scheduled_at
54Reminder sentPOST /activities · kind=text_outbound, campaign="Appt reminder"
55No-showPOST /activities · kind=appointment, appointment_status="no_show"
56RescheduledPOST /activities · kind=appointment, appointment_status="rescheduled", scheduled_at
57BDC → sales meeting handoffPOST /activities · kind=meeting, employee_name, body, vehicle

F&I / credit

#GoalCall + key fields
58Credit app submittedPOST /activities · kind=credit_app, status="Submitted", lender, amount (→ Credit App card)
59Prequal result / tierPOST /activities · kind=credit_app, status="Prequalified", meta={tier:"A"}
60F&I product sold (VSC/GAP)POST /activities · kind=note, meta={product:"VSC", amount:1899}
61Vehicle deliveredPOST /activities · kind=delivery, vehicle, body, reference
62Contract signedPOST /activities · kind=note, meta={contract_id}
63Finance leadPOST /leads {intent:"finance"}

Reviews & reputation

#GoalCall + key fields
64Review request sentPOST /activities · kind=text_outbound or email_outbound, campaign="Review request"
65Review receivedPOST /activities · kind=note, meta={platform:"Google", rating:5, text}
66Low-rating alertPOST /activities · kind=note, meta={rating:1, platform}
67CSI survey sentPOST /activities · kind=email_outbound, campaign="CSI"
68Survey responsePOST /activities · kind=note, meta={csi_score}
69Reputation opt-outPOST /activities · kind=note, meta={opt_out:true}

Service & fixed-ops

#GoalCall + key fields
70Service appointment bookedPOST /activities · kind=appointment, appointment_type="service", scheduled_at, vehicle
71Repair order openedPOST /activities · kind=service_ro, ro_number:"12345", status="open", vehicle (→ Repair Order card)
72Repair order closedPOST /activities · kind=service_ro, ro_number:"12345", status="closed", amount:642, vehicle
73Declined service (upsell opportunity)POST /activities · kind=service_ro, meta={declined:["brakes"]}
74Open recall on an owned vehiclePOST /customers {owned_vin} → POST /activities kind=service_ro, meta={recall}
75Service visit completedPOST /activities · kind=service_ro, status="completed", vehicle
76Multi-point inspection resultsPOST /activities · kind=note, meta={mpi:{…}}

Digital retail & website

#GoalCall + key fields
77Online deal / write-up startedPOST /leads, then POST /activities · kind=write_up, amount, vehicle, body
78Prequalified onlinePOST /activities · kind=credit_app, status="Prequalified", meta={tier}
79Trade started onlinePOST /leads {trade}
80Deposit paidPOST /activities · kind=note, meta={deposit:500}
81Website form submissionPOST /leads {customer, comments}
82High-intent VDP viewPOST /activities · kind=note, vehicle, meta={event:"VDP view"}
83Test drive completedPOST /activities · kind=test_drive, vehicle, body, employee_name

Equity mining & in-market

#GoalCall + key fields
84Equity alertPOST /activities · kind=equity_alert, amount:3200, vehicle, meta={payoff}
85Lease-end opportunityPOST /activities · kind=note, meta={lease_end}
86Service-to-sales opportunityPOST /activities · kind=note, meta={signal:"high-mileage"}
87In-market scorePOST /activities · kind=note, meta={in_market_score:88}
88Upgrade / loyalty offer sentPOST /activities · kind=email_outbound, campaign="Upgrade"

Other AI tools

#GoalCall + key fields
89AI conversation summaryPOST /activities · kind=note, body=summary, meta={ai:true}
90AI-qualified leadPOST /leads {source:"AI Assistant", comments}
91OliviaAI-set appointmentPOST /activities · kind=appointment, scheduled_at, appointment_status="confirmed", meta={ai:true}
92AI sentiment / intent tagPOST /activities · kind=note, meta={sentiment, intent}
93AI voice agent handled a callPOST /activities · kind=call_inbound, transcript, recording_url, meta={ai:true}

Analytics & data sync

#GoalCall + key fields
94Nightly customer exportGET /customers (paginate with after)
95One customer plus full historyGET /customers/{id} + GET /activities?customer_id=…; repeat with before_id=next_before_id until null
96Inventory snapshot for BIGET /inventory?status=all (paginate) + GET /inventory/{id} for a known unit
97Verify key and negotiate capabilitiesGET / or GET /ping, then GET /schema (exact alias: GET /capabilities)
98Decode and enrich a VINGET /vin-decode?vin=…; add detail=basic for identity-only
99Build a cached vehicle pickerGET /catalog → GET /catalog/{vertical}/makes → GET /catalog/{vertical}/models?make= → GET /catalog/{vertical}/trims?make=&model=
100Manage write-confirmation webhooksPOST /webhooks, GET /webhooks, then DELETE /webhooks/{id}; verify HMAC and dedupe deliveries