DealerGate
DealerGate Developers

Stock Webhook API

Receive a dealer's stock in your own system. When a car the dealer has chosen to share is added, changed or taken off sale, we send it to your endpoint as JSON.

Version 1 · Early access

How it works

DealerGate is a dealer management system for UK car dealers. A dealer can share their stock with a third party through this webhook. There's no sign-up form: the dealer asks us to connect you, and we set it up together.

  1. The dealer asks DealerGate to connect you.
  2. You send us the HTTPS address of your endpoint.
  3. We give you a signing secret for that dealer and send a test event.
  4. The dealer switches on the cars they want to share, and those cars start arriving.

You only receive cars the dealer has switched on. If they switch a car off, or it's sold, we tell you to remove it.

Each dealer has their own secret. You can use one endpoint for several dealers and tell them apart with dealer.id.

Early access: this feed is being rolled out dealer by dealer. Talk to us before you start, so we can agree a test with you.

Your endpoint

Your endpoint needs to:

  • use HTTPS with a valid certificate;
  • accept a POST with a JSON body;
  • check the signature on every request (see Checking signatures);
  • answer with any 2xx status within 10 seconds.

Anything else counts as a failure and we'll retry, including a timeout or a redirect. We don't follow redirects.

Save the delivery and answer first. Do slower work, like downloading images, afterwards.

Requests

Each request is one event for one car. These headers come with every request:

HeaderValue
Content-Typeapplication/json
User-AgentDealerGate-Webhook/1.0
X-DealerGate-EventThe event name, such as vehicle.upsert.
X-DealerGate-DeliveryThe delivery ID. A retry of the same delivery keeps the same ID.
X-DealerGate-TimestampWhen this attempt was sent, in Unix seconds.
X-DealerGate-Signaturesha256= followed by the signature in hex.

Every body has the same outer fields:

FieldTypeNotes
eventstringThe event name.
schema_versionintegerCurrently 1. See Versions.
delivery_idstringUUID. Matches X-DealerGate-Delivery.
sent_atstringWhen this attempt was sent, in UTC (ISO 8601).
dealerobjectSee Dealer fields.
vehicleobjectNot sent on test.ping.

Events

EventMeaningWhat to do
vehicle.upsertA car was switched on or changed.Add the car, or replace what you hold for that vehicle.id.
vehicle.removedThe car is no longer shared.Take the car down.
test.pingA test from DealerGate.Check the signature and answer 200.

vehicle.upsert

Every vehicle.upsert holds the whole car, not just what changed.

vehicle.upsert
{
  "event": "vehicle.upsert",
  "schema_version": 1,
  "delivery_id": "6f1c2e0a-8b47-4c1d-9a35-2f7d1e4b6c90",
  "sent_at": "2026-11-05T09:12:33Z",
  "dealer": {
    "id": "DG-00-0000",
    "name": "Example Motors",
    "phone": "01234 567890",
    "email": "sales@example-motors.co.uk",
    "website": "https://www.example-motors.co.uk",
    "address_line_1": "1 High Street",
    "town": "Luton",
    "postcode": "LU1 1AA"
  },
  "vehicle": {
    "id": "a3e9b7d2-41f6-4c08-b1de-7c5a90e2f314",
    "registration": "AB73CDE",
    "status": "in_stock",
    "vehicle_type": "Car",
    "make": "Vauxhall",
    "model": "Corsa",
    "generation": "Hatchback (2019 onwards)",
    "derivative": "1.2 Turbo Design 5dr",
    "trim": "Design",
    "body_type": "Hatchback",
    "fuel_type": "Petrol",
    "transmission": "Manual",
    "drivetrain": "Front Wheel Drive",
    "colour": "Blue",
    "doors": 5,
    "seats": 5,
    "year": 2024,
    "first_registered": "2024-01-18",
    "mileage": 34515,
    "owners": 1,
    "engine_size_cc": 1199,
    "power_bhp": 99,
    "co2_g_km": 118,
    "emission_class": "Euro 6",
    "mpg_combined": 53.3,
    "insurance_group": "12E",
    "mot_expiry": "2027-01-17",
    "service_history": "Full dealership history",
    "price": 10294,
    "price_on_application": false,
    "vat_status": "No VAT",
    "attention_grabber": "ONE OWNER, FULL HISTORY",
    "description": "A one owner Corsa with full dealership service history.",
    "features": ["Apple CarPlay", "Parking sensors", "Heated seats"],
    "images": [
      "https://images.example.com/ab73cde/1.jpg",
      "https://images.example.com/ab73cde/2.jpg"
    ],
    "videos": [],
    "updated_at": "2026-11-05T09:08:02Z"
  }
}

vehicle.removed

vehicle.reason is one of:

  • sold: the car has been sold.
  • switched_off: the dealer stopped sharing this car.
  • unavailable: the car is no longer on sale for any other reason.
vehicle.removed
{
  "event": "vehicle.removed",
  "schema_version": 1,
  "delivery_id": "0d4b8a31-7e52-4f6a-8c19-5be2a7f3c021",
  "sent_at": "2026-11-05T15:40:07Z",
  "dealer": {
    "id": "DG-00-0000",
    "name": "Example Motors"
  },
  "vehicle": {
    "id": "a3e9b7d2-41f6-4c08-b1de-7c5a90e2f314",
    "registration": "AB73CDE",
    "reason": "sold"
  }
}

test.ping

test.ping
{
  "event": "test.ping",
  "schema_version": 1,
  "delivery_id": "c27e9f14-3a68-4b0d-a5e1-91d4f06b7e38",
  "sent_at": "2026-11-05T09:00:00Z",
  "dealer": {
    "id": "DG-00-0000",
    "name": "Example Motors"
  }
}

Vehicle fields

Use id as your key for a car. A registration can change, for example when a private plate is removed.

A field marked null can be empty. Text fields hold what the dealer has on record, so don't rely on a fixed list of values.

FieldTypeNotes
idstringUUID. Never changes.
registrationstringUK registration, no spaces.
statusstringin_stock or reserved. A reserved car has a buyer but isn't sold yet.
vehicle_typestring | nullFor example Car or Van.
makestring
modelstring
generationstring | null
derivativestring | nullThe full version name.
trimstring | null
body_typestring | null
fuel_typestring | null
transmissionstring | null
drivetrainstring | null
colourstring | null
doorsinteger | null
seatsinteger | null
yearinteger | nullYear of manufacture.
first_registeredstring | nullDate, YYYY-MM-DD.
mileageinteger | nullIn miles.
ownersinteger | nullNumber of previous owners.
engine_size_ccinteger | null
power_bhpnumber | null
co2_g_kmnumber | null
emission_classstring | nullFor example Euro 6.
mpg_combinednumber | null
insurance_groupstring | null
mot_expirystring | nullDate, YYYY-MM-DD.
service_historystring | null
pricenumber | nullIn pounds. Empty if price_on_application is true.
price_on_applicationboolean
vat_statusstring | nullFor example No VAT, Ex VAT or Inc. VAT.
attention_grabberstring | nullThe dealer's short headline for the car.
descriptionstring | nullThe dealer's advert text.
featuresstring[]Equipment names.
imagesstring[]Image URLs in the dealer's order. The first is the main photo.
videosstring[]Video URLs.
updated_atstringWhen the car last changed, in UTC (ISO 8601).
vinstringOnly present if the dealer has agreed to share it.

Image URLs can change, so always use the ones in the latest delivery.

The feed holds advert details only. It never includes what the dealer paid for a car, profit figures, seller or customer details, or internal notes.

Dealer fields

vehicle.upsert has all of these. vehicle.removed and test.ping have id and name only.

FieldTypeNotes
idstringThe dealer's DealerGate ID, such as DG-00-0000. Never changes.
namestring
phonestring | null
emailstring | null
websitestring | null
address_line_1string | null
townstring | null
postcodestring | null

Checking signatures

The signature proves a request came from DealerGate and wasn't changed on the way. Check it on every request, and keep your secret on your server.

  1. Read the raw request body as bytes, before any JSON parsing.
  2. Join the X-DealerGate-Timestamp value, a full stop and the raw body.
  3. Work out the HMAC-SHA256 of that text with your secret, in lowercase hex.
  4. Add sha256= to the front and compare it to X-DealerGate-Signature, using a constant-time comparison.
  5. Reject the request if the timestamp is more than 5 minutes from your clock. This stops an old request being replayed.

Answer 401 if a check fails.

Node.js (Express)

server.js
const crypto = require('crypto');
const express = require('express');

const app = express();
const secret = process.env.DEALERGATE_SECRET;

// express.raw keeps the exact bytes we sent, which is what the signature covers.
app.post('/dealergate', express.raw({ type: 'application/json' }), (req, res) => {
  const timestamp = req.get('X-DealerGate-Timestamp') || '';
  const signature = req.get('X-DealerGate-Signature') || '';

  const expected = 'sha256=' + crypto.createHmac('sha256', secret)
    .update(timestamp + '.')
    .update(req.body)
    .digest('hex');

  const recent = Math.abs(Date.now() / 1000 - Number(timestamp)) <= 300;
  const matches = signature.length === expected.length
    && crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
  if (!recent || !matches) return res.sendStatus(401);

  const delivery = JSON.parse(req.body);
  // Save the delivery here, then answer. Do slow work afterwards.
  res.sendStatus(200);
});

app.listen(process.env.PORT || 3000);

PHP

dealergate.php
<?php
$secret = getenv('DEALERGATE_SECRET');

// Read the exact bytes we sent, which is what the signature covers.
$body = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_DEALERGATE_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_DEALERGATE_SIGNATURE'] ?? '';

$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $body, $secret);

$recent = ctype_digit($timestamp) && abs(time() - (int) $timestamp) <= 300;
if (!$recent || !hash_equals($expected, $signature)) {
    http_response_code(401);
    exit;
}

$delivery = json_decode($body, true);
// Save the delivery here, then answer. Do slow work afterwards.
http_response_code(200);

Python (Flask)

app.py
import hashlib
import hmac
import os
import time

from flask import Flask, abort, request

app = Flask(__name__)
SECRET = os.environ['DEALERGATE_SECRET'].encode()


@app.post('/dealergate')
def dealergate():
    timestamp = request.headers.get('X-DealerGate-Timestamp', '')
    signature = request.headers.get('X-DealerGate-Signature', '')

    # get_data() returns the exact bytes we sent, which is what the signature covers.
    signed = timestamp.encode() + b'.' + request.get_data()
    expected = 'sha256=' + hmac.new(SECRET, signed, hashlib.sha256).hexdigest()

    recent = timestamp.isdigit() and abs(time.time() - int(timestamp)) <= 300
    if not recent or not hmac.compare_digest(expected, signature):
        abort(401)

    delivery = request.get_json()
    # Save the delivery here, then answer. Do slow work afterwards.
    return '', 200

Test your code

These made-up values should give your code exactly this signature. Skip the 5-minute check for this test, because the timestamp is in the past.

Secretdgwh_test_secret
Timestamp1762333200
Body{"event":"test.ping"}
Signaturesha256=d35db8d212fd4138ddf7f7b745cc80be244ef31045a68c71875d113a285aae05

Delivery and retries

Timing

We check for changes every 5 minutes and send only the cars that changed, so a change normally reaches you within a few minutes.

Volume

We send one car per request, at up to 2 requests a second. When a dealer first switches on their stock, expect a burst of hundreds of requests.

Duplicates and order

You may occasionally receive the same delivery twice, so make sure handling it twice does no harm. Replacing the car by vehicle.id does this for you.

After a retry, deliveries can arrive out of order. Compare sent_at and ignore anything older than what you hold for that car.

Failures

If a delivery fails, we try again at the next check, then at growing gaps of up to an hour, for up to 24 hours. After that we pause the feed and contact you. Once it's fixed, we send everything that changed while it was paused.

Getting back in step

If your copy of a dealer's stock goes wrong, ask us to resend it. Every car the dealer has switched on arrives again as vehicle.upsert.

Versions

We may add new fields at any time, so ignore any you don't recognise. If we ever remove or change a field, schema_version goes up and we'll tell you first.

VersionDateChanges
1November 2026First release.

Support

Email support@dealergate.co.uk or call 0333 772 4639. If a delivery went wrong, send us the dealer ID and the delivery ID.