Lead Webhook Export
In Dealer Studio, lead exports can POST JSON to a third-party URL. The webhook is configured under dealership notifications (medium: webhook). You can optionally send a Bearer token using the notification’s API token and header key settings. You can also optionally have each payload signed, so your endpoint can verify the delivery came from Dealer Studio: see Payload signature.
Three schema versions
Section titled “Three schema versions”The notification’s API schema version (v1, v2, or v3) selects which JSON shape is sent. These are three different payloads, not additive flags on one object: fields are renamed, nested, or removed between versions.
| Version | Status | Summary |
| ------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| v1 | Deprecated | Legacy flat shape: dealership_id, dealership_name, website_url, top-level status / status_type, item_id, created / modified, assignee_email, subscription, and a broader prospect object (including combined name and fields dropped in later versions). |
| v2 | Recommended baseline for new integrations | Restructured payload: nested dealership, website, lead_status_option, assigned_salesperson, updated_at, streamlined prospect (first_name, opt_in_to_marketing, …), and category-driven blocks such as trade_in_data / offer_data / booking_date / booking_time aligned in one schema. Update webhooks use the same body as create (no extra top-level status merge). |
| v3 | Extends v2 | Everything in v2, plus has_test_drives and appointments (test-drive appointments only). |
Deprecation: v1 is deprecated. Prefer v2 for a stable, documented shape, or v3 if you need test-drive appointment rows and the has_test_drives flag. New integrations should not target v1.
Version differences (high level)
Section titled “Version differences (high level)”- v1 → v2 — Dealership and website move into objects; lead status moves into
lead_status_option; assignee becomesassigned_salesperson; timestamps useupdated_atinstead ofmodifiedfor the cluster-oriented view; several v1-only keys are removed from the root anditem/prospectare trimmed and normalised (see examples below). Root-levellocation_idis removed in v2+; uselocation.idinstead (present in all versions). - v2 → v3 — Adds
appointments(array, always present) andhas_test_drives(boolean).appointmentslists only appointments on the lead cluster with purpose Test Drive, ordered bystarts_atascending (earliest first). Each element describes one appointment row;has_test_drivesistrueif the cluster has at least one TestDrive record (lead_cluster.test_drives), otherwisefalse. That flag is independent ofappointments(you can have test-drive appointments without a linked TestDrive, or TestDrives without matching appointment rows in the payload).
location object (all versions)
Section titled “location object (all versions)”| Field | Description |
| --------------- | ------------------------------------------------ |
| id | Dealer Studio location id, or null if unset. |
| street | Location street address. |
| city | Location city. |
| state | Location state. |
| postcode | Location postcode. |
| location_name | Location display name. |
appointments entries (v3 only)
Section titled “appointments entries (v3 only)”Each object can include:
| Field | Description |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| id | Appointment id. |
| ai_generated | Whether the booking was created by AI. |
| user | Object with assigned user id and name, or null values if missing. |
| starts_at | Start time in the dealership’s timezone (or Australia/Brisbane if unset), as YYYY-MM-DD HH:MM:SS, or null if not set. |
| created_at | Appointment created_at in the same timezone format, or null. |
| updated_at | Appointment updated_at in the same timezone format, or null. |
| status | Appointment status (for example pending, completed, cancelled, no-show, interest_registered). |
| length_in_minutes | Scheduled duration. |
| note | Optional note; may be null. |
| car_for_test_drive | If the schedulable is a TestDrive with a stock car, the car’s display name. If the test drive uses a generic car (car lead cluster only), the string <car lead cluster name> (generic car). If not linked to a TestDrive schedulable, null. |
Example payloads
Section titled “Example payloads”The JSON below is illustrative (same fictional lead, trimmed where versions match). Use it to compare shapes; real payloads include whatever optional blocks apply to the lead category and data.
v1 uses the legacy root fields and prospect shape. Update webhooks merge extra top-level keys (for example status-related fields from the lead cluster) into this body—behaviour differs from v2.
{ "id": 138559, "item_id": 138559, "dealership_id": 3, "dealership_name": "Orange Motor Group", "lead_cluster_id": 289, "marketing_source": "Unknown", "utm_campaign": null, "website_url": "https://www.orangemotorgroup.com.au", "provider": "Dealer Studio", "source": "Website", "category": "Sell My Car Enquiry", "status": "New", "status_type": "New", "location": { "id": null, "city": "Fake city", "state": "QLD", "street": "123 fake street", "postcode": 1234, "location_name": "Orange Motor Group" }, "location_id": null, "prospect": { "title": null, "name": "xcvxcvdcz don", "middle_name": null, "last_name": "don", "email": "email@example.com", "postcode": 1234, "home_phone": null, "mobile_phone": "0410000000", "address": { "city": "Browns Plains", "state": "Queensland", "street": "4 Myola Street", "country": "Australia", "postcode": 4118, "state_long": "Queensland" }, "dob": null, "licence_type": null, "employment_type": null, "driver_licence_number": null, "living_situation": null, "residency_status": null }, "subscription": { "newsletter": true }, "comments": "comments here", "budget": null, "purchase_price": null, "created": "2025-09-16T00:30:52.297Z", "modified": "2025-09-16T00:30:52.297Z", "assignee_email": null, "item": { "type": "Car", "car_type": null, "id": null, "vin": null, "status": null, "colour": null, "body_type": null, "stock_number": null, "redbook_code": null, "build_date": null, "transmission": null, "interior_color": null, "deposit": null, "make": "subaru", "model": "Forester", "badge": null, "year": 2018, "rego": "151vev", "price": null, "engine_size": null, "km": null, "series": null, "comments": null, "url": "https://www.orangesubaru.com.au/trade-in", "spin_code": null, "third_party_integrations": [{ "service": "Loopit", "external_id": "12312" }] }, "lead_events": [], "pipeline": { "id": 496, "name": "Sales" }, "additional_details": { "range": 10405608, "additional_data": "test data" }, "trade_in_data": { "kms": 210000, "make": "subaru", "rego": "151vev", "year": 2018, "badge": null, "model": "Forester", "state": null, "title": null, "series": null, "offer_price": null, "trade_price": 60900, "retail_price": null, "offer_accepted": false, "price_adjustment": null }, "subscription_plan": { "name": "Premium", "subscription_id": 4, "daily_price_for_car": 46.9, "weekly_price_for_car": 329, "monthly_price_for_car": 1425.7, "fortnightly_price_for_car": 658 }}v2 does not include appointments or has_test_drives. Update webhooks reuse the same JSON as create (no extra merged status block on the root).
{ "id": 138559, "item": { "id": null, "km": null, "url": "https://www.orangesubaru.com.au/trade-in", "vin": null, "make": "subaru", "rego": "151vev", "type": "Car", "year": 2018, "badge": null, "model": "Forester", "price": null, "colour": null, "series": null, "status": null, "deposit": null, "car_type": null, "comments": null, "body_type": null, "spin_code": null, "build_date": null, "engine_size": null, "stock_number": null, "transmission": null, "interior_color": null, "third_party_integrations": [{ "service": "Loopit", "external_id": "12312" }] }, "source": "Website", "created": "2025-09-16T00:30:52.297Z", "website": { "id": 794, "url": "https://www.orangemotorgroup.com.au", "name": "Orange Motor Group", "slug": "orange-motor-group" }, "category": "Sell My Car Enquiry", "comments": "comments here", "location": { "id": null, "city": "Fake city", "state": "QLD", "street": "123 fake street", "postcode": 1234, "location_name": "Orange Motor Group" }, "prospect": { "dob": null, "email": "email@example.com", "title": null, "address": { "city": "Browns Plains", "state": "Queensland", "street": "4 Myola Street", "country": "Australia", "postcode": 4118, "state_long": "Queensland" }, "postcode": 1234, "last_name": "don", "first_name": "xcvxcvdcz", "middle_name": null, "mobile_phone": "0410000000", "opt_in_to_marketing": true }, "provider": "Dealer Studio", "dealership": { "id": 3, "name": "Orange Motor Group", "slug": "orange-motor-group" }, "updated_at": "2025-09-16T00:30:52.297Z", "lead_events": [], "utm_campaign": null, "trade_in_data": { "kms": 210000, "make": "subaru", "rego": "151vev", "year": 2018, "badge": null, "model": "Forester", "state": null, "title": null, "series": null, "offer_price": null, "trade_price": 60900, "retail_price": null, "offer_accepted": false, "price_adjustment": null }, "subscription_plan": { "name": "Premium", "subscription_id": 4, "daily_price_for_car": 46.9, "weekly_price_for_car": 329, "monthly_price_for_car": 1425.7, "fortnightly_price_for_car": 658 }, "lead_cluster_id": 289, "marketing_source": "Unknown", "additional_details": { "range": 10405608, "additional_data": "test data" }, "lead_status_option": { "id": 138, "name": "new", "status_type": "new" }, "assigned_salesperson": { "id": null, "name": null, "email": null }, "subcategory": null, "booking_date": null, "booking_time": null, "offer_data": { "manufacturer": null, "offer_name": null }, "pipeline": { "id": 496, "name": "Sales" }, "lost_reason_comment": null, "lost_reason": null}v3 is v2 plus has_test_drives and appointments (see table above).
{ "id": 138559, "item": { "id": null, "km": null, "url": "https://www.orangesubaru.com.au/trade-in", "vin": null, "make": "subaru", "rego": "151vev", "type": "Car", "year": 2018, "badge": null, "model": "Forester", "price": null, "colour": null, "series": null, "status": null, "deposit": null, "car_type": null, "comments": null, "body_type": null, "spin_code": null, "build_date": null, "engine_size": null, "stock_number": null, "transmission": null, "interior_color": null, "third_party_integrations": [{ "service": "Loopit", "external_id": "12312" }] }, "source": "Website", "created": "2025-09-16T00:30:52.297Z", "website": { "id": 794, "url": "https://www.orangemotorgroup.com.au", "name": "Orange Motor Group", "slug": "orange-motor-group" }, "category": "Sell My Car Enquiry", "comments": "comments here", "location": { "id": null, "city": "Fake city", "state": "QLD", "street": "123 fake street", "postcode": 1234, "location_name": "Orange Motor Group" }, "prospect": { "dob": null, "email": "email@example.com", "title": null, "address": { "city": "Browns Plains", "state": "Queensland", "street": "4 Myola Street", "country": "Australia", "postcode": 4118, "state_long": "Queensland" }, "postcode": 1234, "last_name": "don", "first_name": "xcvxcvdcz", "middle_name": null, "mobile_phone": "0410000000", "opt_in_to_marketing": true }, "provider": "Dealer Studio", "dealership": { "id": 3, "name": "Orange Motor Group", "slug": "orange-motor-group" }, "updated_at": "2025-09-16T00:30:52.297Z", "lead_events": [], "utm_campaign": null, "trade_in_data": { "kms": 210000, "make": "subaru", "rego": "151vev", "year": 2018, "badge": null, "model": "Forester", "state": null, "title": null, "series": null, "offer_price": null, "trade_price": 60900, "retail_price": null, "offer_accepted": false, "price_adjustment": null }, "subscription_plan": { "name": "Premium", "subscription_id": 4, "daily_price_for_car": 46.9, "weekly_price_for_car": 329, "monthly_price_for_car": 1425.7, "fortnightly_price_for_car": 658 }, "lead_cluster_id": 289, "marketing_source": "Unknown", "additional_details": { "range": 10405608, "additional_data": "test data" }, "lead_status_option": { "id": 138, "name": "new", "status_type": "new" }, "assigned_salesperson": { "id": null, "name": null, "email": null }, "subcategory": null, "booking_date": null, "booking_time": null, "offer_data": { "manufacturer": null, "offer_name": null }, "pipeline": { "id": 496, "name": "Sales" }, "has_test_drives": true, "appointments": [ { "id": 9041, "ai_generated": false, "user": { "id": 88, "name": "Alex Demo" }, "starts_at": "2025-09-18 12:00:00", "created_at": "2025-09-16 09:00:00", "updated_at": "2025-09-16 09:00:00", "status": "pending", "length_in_minutes": 30, "note": "Customer requested SUV demo", "car_for_test_drive": "2018 Subaru Forester Sport" }, { "id": 9042, "ai_generated": true, "user": { "id": 88, "name": "Alex Demo" }, "starts_at": "2025-09-20 13:30:00", "created_at": "2025-09-16 10:15:00", "updated_at": "2025-09-17 08:00:00", "status": "completed", "length_in_minutes": 45, "note": null, "car_for_test_drive": "Toyota Corolla Ascent 2022 (generic car)" } ], "lost_reason_comment": null, "lost_reason": null}Payload signature (optional)
Section titled “Payload signature (optional)”If you need to verify that a delivery genuinely came from Dealer Studio, we can configure a shared signing secret for your webhook. Email support@dealerstudio.com.au to have one set up. Store your copy somewhere safe when we share it: the secret cannot be read back out of Dealer Studio afterwards, so a lost secret has to be replaced with a new one.
When a secret is configured, every POST carries one extra header:
| Header | Value |
| ------------- | ---------------------------------------------------------------------------------- |
| X-Signature | Base64-encoded HMAC-SHA256 of the raw request body, keyed with your shared secret |
When no secret is configured the header is absent and delivery is unchanged, so this is safe to adopt at any point without a coordinated cutover.
Verifying the signature
Section titled “Verifying the signature”Compute the HMAC over the raw request body exactly as received, before any JSON parsing. Parsing and re-serialising the JSON can reorder keys or change whitespace, which produces a different signature. Compare the result in constant time rather than with plain string equality.
const crypto = require('crypto')
function signatureValid(rawBody, secret, headerValue) { const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('base64') const a = Buffer.from(expected) const b = Buffer.from(headerValue || '') return a.length === b.length && crypto.timingSafeEqual(a, b)}Most web frameworks buffer and discard the raw body once they have parsed JSON, so you may need to opt in to keeping it. In Express, capture it with the verify option on express.json(). In Rails, read request.raw_post. In Django, read request.body before touching the parsed payload.
What the signature does and does not prove
Section titled “What the signature does and does not prove”A valid signature proves the payload was produced by someone holding the shared secret and that it has not been altered in transit.
It does not include a timestamp or nonce, so on its own it does not tell you whether a request is a replay of an earlier captured delivery. If replay protection matters to you, reject deliveries carrying a lead or cluster ID and payload you have already processed.
The signature is independent of the Bearer token described above. You can use either, both, or neither.
Retry behaviour
Section titled “Retry behaviour”Lead webhook delivery is asynchronous. Dealer Studio POSTs the payload to your configured URL from a background job.
Transient network failures (for example connection timeouts, read timeouts, or the endpoint being briefly unreachable) cause the delivery job to fail and be automatically retried by the job queue. Retries use exponential backoff so a short outage on your side usually recovers without action from you.
Non-success HTTP responses (for example 4xx or 5xx from your endpoint) are recorded as a failed delivery and are not automatically retried. Return a 2xx status as soon as you have accepted the payload so we treat the delivery as successful.
If delivery still fails after retries are exhausted, the attempt is logged as unsuccessful. Contact support@dealerstudio.com.au if you need a lead re-sent.