For dealers and developers · API v1.0.0
MotorHangar Inventory API
Publish, update and mark as sold your cars from your own website, store or system, and read the buyers who wrote to you. It's free.
Quick start
- In your dealer panel, open API keys and create one. Copy it: it is shown once.
- Check it works:
curl https://motorhangar.com/api/v1/me \
-H "Authorization: Bearer mh_your_key"
- Publish a car with your own stock ID. Sending it again updates it; it never duplicates.
curl -X PUT https://motorhangar.com/api/v1/vehicles/STOCK-001 \
-H "Authorization: Bearer mh_your_key" \
-H "Content-Type: application/json" \
-d '{"brand": "Toyota", "model": "RAV4", "year": 2019, "version": "XLE AWD",
"price": 18500, "mileage": 45000, "fuel": "petrol", "transmission": "automatic",
"photos": ["https://example.com/1.jpg", "https://example.com/2.jpg", "https://example.com/3.jpg"]}'
The answer is 201 the first time and 200 afterwards. The car shows as importing_photos while we download the photos, then active.
Authentication
Send the key in a header, Authorization: Bearer mh_… or X-API-Key: mh_…. Never in the URL: URLs end up in server logs. We only store a hash of your key, so if you lose it, revoke it and create another. A key belongs to one dealership and can only see and change that dealership's cars.
If your account sells in more than one market, say which one with market (in the body or the query string). GET /markets/{code} tells you what that market accepts: currencies, fuels and whether mileage goes in km or miles.
The vehicle
The rules are the same as publishing by hand or by spreadsheet: brand and model must exist in our catalog (we never invent models), and every car needs 3 to 10 photos.
| Field | Type | Description |
|---|---|---|
brand required |
string | Name or slug from GET /catalog/brands. Unknown brands are rejected. e.g. Toyota |
model required |
string | Name or slug from GET /catalog/brands/{slug}/models. The base model, not the trim. e.g. RAV4 |
year required |
integer | e.g. 2019 |
version |
string | Trim or version, free text. e.g. XLE AWD |
price required |
number | e.g. 18500 |
currency |
string | One of the market's currencies (GET /markets/{code}). Defaults to the market's own. e.g. USD |
mileage required |
integer | In the market's mileage_unit: miles in the US and UK, km elsewhere. 0 for a new car. e.g. 45000 |
fuel |
string | One of the market's fuels. e.g. petrol |
transmission |
string · manual | automatic | |
color |
string | e.g. White |
condition |
string · used | new | |
city |
string | City name or slug in the market. Defaults to your dealership's city. e.g. Lima |
description |
string | |
photos required |
array | 3 to 10 public image URLs. We download them within minutes and the car goes live when they arrive. Optional on updates if the car already has its photos. |
market |
string | Required only if your account sells in more than one market. e.g. pe |
source |
string | Where this car comes from. Marking missing cars as sold only touches cars of the same source. |
Status values: active, in_review, importing_photos, sold, expired, withdrawn. Sources: api, woocommerce, wordpress, shopify, prestashop, odoo, tiendanube, n8n, zapier, make, feed.
Endpoints
Base URL: https://motorhangar.com/api/v1
- Vehicles
-
GET
/vehiclesList your vehicles Newest updates first. Useupdated_sinceto poll for changes. -
GET
/vehicles/{external_id}Get one vehicle -
PUT
/vehicles/{external_id}Create or update a vehicle Idempotent: sending the same stock ID again updates the car instead of duplicating it. A sold car sent again is back for sale. -
DELETE
/vehicles/{external_id}Withdraw a vehicle It stops being published. Nothing is deleted: its page keeps answering, as a sold car's does. -
POST
/vehicles/{external_id}/soldMark a vehicle as sold - Sync
-
POST
/vehicles/syncCreate or update up to 500 vehicles in one call Each vehicle is validated and applied on its own: one bad car never blocks the rest. For more than 500 cars, send several calls withoutmark_missing_as_soldand finish withPOST /vehicles/reconcile. -
POST
/vehicles/reconcileMark as sold the cars of a source that are no longer in your stock Send every stock ID of the source that is still for sale. The others of that source are marked as sold. Guard: if that would mark more than 40% of the source's cars at once (and at least 5), nothing is marked and you get 409 — usually a partial export. An empty list is always refused. -
POST
/feeds/{token}/Push your whole inventory file (for providers) For your website company, DMS or inventory provider: POST the WHOLE inventory file (CSV, Excel, JSON or XML, up to 10 MB) to the private address shown in the dealer panel (Inventory feed → My provider will send it). No API key: the token in the address is the secret. Same rules as /vehicles/sync, including the sold guard. - Catalog
-
GET
/markets/{code}What a market accepts: currencies, fuels, mileage unit -
GET
/catalog/brandsBrands in the catalog -
GET
/catalog/brands/{slug}/modelsModels and years of a brand - Leads
-
GET
/leadsBuyers who wrote about your cars Newest first, up to 100. Usesinceto poll. The buyer's contact details are not included: they are shared in the conversation when the buyer chooses to. - Webhooks
-
GET
/webhooksYour webhooks and the events you can subscribe to -
POST
/webhooksSubscribe a URL to events For n8n, Make or Zapier triggers that register themselves. Up to 20 per account. -
GET
/webhooks/{id}One webhook -
DELETE
/webhooks/{id}Unsubscribe - Account
-
GET
/meYour dealership, markets and limits
Syncing a whole stock
Each car carries a source (woocommerce, shopify, api…). Marking missing cars as sold only ever touches cars of the same source, so a store plugin never marks as sold the cars you uploaded by spreadsheet.
- Up to 500 cars: one
POST /vehicles/syncwith"mark_missing_as_sold": true. - More than 500: several
POST /vehicles/synccalls without it, then onePOST /vehicles/reconcilewith every stock ID that is still for sale.
One bad car never blocks the rest: you get 207 and an errors list, and nothing is marked as sold in that call. Two guards protect you from a broken export: an empty list is never "everything was sold", and if more than 40% of a source's cars (and at least 5) would be marked as sold at once, nothing is marked and you get 409. Send "allow_mass_sold": true when it is real.
Webhooks
Instead of polling, let us call you. Add a URL in your dealer panel (Webhooks) or with POST /webhooks, which is what n8n, Make or Zapier triggers use to register themselves. Events:
lead.created— a buyer wrote to you about one of your cars (the conversation link, not their contact details).vehicle.sold— one of your cars became sold, from any channel.vehicle.rejected— a car you sent by API or feed failed validation; errors says why.sync.failed— a feed could not be read, or we refused to mark many cars as sold at once.
Every delivery is a POST with this body, and the headers X-MotorHangar-Event, X-MotorHangar-Delivery (the event id: use it to ignore repeats) and X-MotorHangar-Signature.
{"id": "8b1c…", "type": "vehicle.sold", "created_at": "2026-10-15T14:02:11+00:00",
"data": {"external_id": "STOCK-001", "listing_id": "MH7K2Q", "market": "pe", "source": "feed",
"vehicle": "Toyota RAV4 2019", "url": "https://motorhangar.com/pe/car/…"}}
The signature is t=<unix time>,v1=<hex>, where v1 is the HMAC-SHA256 of "<t>.<raw body>" with your signing secret. Reject it if it doesn't match or if t is more than 5 minutes old.
import hmac, hashlib, time
def valid(secret, body: bytes, header: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t = int(parts["t"])
mac = hmac.new(secret.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest()
return abs(time.time() - t) < 300 and hmac.compare_digest(mac, parts["v1"])
Answer with any 2xx within 10 seconds. Timeouts, 5xx and 429 are retried after 1, 5, 30, 120 and 360 minutes; other 4xx are not. A webhook that fails 20 deliveries in a row is turned off (you can turn it back on by sending a test from the panel). Buyers' contact details are never included.
Inventory feeds
No code at all: in Inventory feed give us the address of a CSV, Excel, JSON or XML file, or a Google Sheet, that your website, DMS or provider already produces. We show you the columns we found and how the first cars would look, you confirm, and we read it every few hours. If a provider sends your stock to other portals (HomeNet and similar), ask them to add MotorHangar as a destination and give them the private push address from that page.
Errors and limits
Errors always look like {"error": {"code": "…", "message": "…"}}. A 422 adds fields with each broken rule, written to be shown to a person.
401missing or revoked key ·404no such car ·409refused by the sold guard ·413too many cars in one call ·422the car breaks a rule.429: more than 120 calls a minute per key. Wait the seconds inRetry-After.- Retry
429and5xx. Don't retry other4xx: the same request will fail the same way.
Plugins and no-code tools
Each one has its own page with the full setup, what syncs and its limits. All integrations, with what is available, in beta and planned.
WordPress without WooCommerce
MotorHangar Connect reads the cars your theme stores (Motors, ACF or any custom post type): you match the fields once, with a live preview.
Download pluginWordPress → Plugins → Add new → Upload plugin, then Settings → MotorHangar Connect.
WooCommerce (WordPress)
Your products sync when you save them; out of stock marks them sold; a daily check catches anything missed.
Download pluginWordPress → Plugins → Add new → Upload plugin, then WooCommerce → MotorHangar.
PrestaShop
A module for PrestaShop 1.7.8 to 8: realtime on product save and stock changes, plus a cron URL for the daily check.
Download moduleModules → Module Manager → Upload a module.
Shopify
Nothing to install: connect your store from your dealer panel and add the webhook address we give you in Shopify → Settings → Notifications → Webhooks. Tag the cars with motorhangar and their details (year:2019, model:RAV4…).
n8n
Install the community node n8n-nodes-motorhangar (Settings → Community nodes). It has every operation plus a trigger for new leads and updated cars, and works as a tool for AI agents.
Make, Zapier, Postman, any HTTP client
Use an HTTP module with a header Authorization: Bearer mh_…, or import openapi.json. In n8n without the node: HTTP Request → Header Auth credential.
Code samples
PHP
$ch = curl_init('https://motorhangar.com/api/v1/vehicles/' . rawurlencode($stockId));
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => 'PUT',
CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . $key, 'Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode($car),
CURLOPT_RETURNTRANSFER => true,
]);
$response = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE); // 200, 201 or 422
Python
import requests
s = requests.Session()
s.headers["Authorization"] = f"Bearer {key}"
r = s.post("https://motorhangar.com/api/v1/vehicles/sync", json={"source": "api", "mark_missing_as_sold": True, "vehicles": cars})
for e in r.json()["errors"]:
print(e["external_id"], e["message"])
JavaScript
const r = await fetch(`https://motorhangar.com/api/v1/leads?since=${encodeURIComponent(lastCheck)}`, {
headers: { Authorization: `Bearer ${key}` },
});
const { data } = await r.json();