Developers
Dealership.nz API
Sync your stock from your DMS and show it on your own website. The API and bulk import come with the Dealership plan ($50 a month); the public read API and the website widget need no key.
?format=json for JSON. Feed it to a client generator.On this page
Base URL for every example: https://dealership.nz. Authenticated examples assume export DEALERSHIP_API_KEY=dnz_live_....
Which API do I need?
| Public read API | Authenticated API | |
|---|---|---|
| Base path | /api/v1/public/ | /api/v1/ |
| Credentials | None | API key |
| Runs from | The browser, on your own website | Your servers: DMS, sync scripts, back office |
| Access | Anyone: published listings of active dealerships, read-only | Only your own listings, read and write |
| CORS | Access-Control-Allow-Origin: * | Not CORS-enabled |
| Caching | Cache-Control: public, max-age=60 | None |
| Rate limit | 300 requests/min per IP | 120 requests/min per account (import: 60/hour) |
Rule of thumb: never put an API key in a web page. If the data is already public (your published stock), use the public API or the embed widget; if you need to change anything, use the authenticated API from a server.
Authentication
Every authenticated endpoint requires an account on the Dealership plan with an active subscription. Other accounts, including lapsed dealerships, get:
HTTP 403
{"detail": "Active dealership subscription required for API access."}
API keys
Create keys at https://dealership.nz/my/api-keys/ (Account → API keys). Each key has a name and a scope, and is shown once when created; only a hash is stored. Send it in either header:
curl -H "Authorization: Bearer $DEALERSHIP_API_KEY" https://dealership.nz/api/v1/listings/
curl -H "X-API-Key: $DEALERSHIP_API_KEY" https://dealership.nz/api/v1/listings/
| Format | dnz_live_ + 43 random characters; the first 12 characters are shown in the key list so you can tell keys apart |
Scope read_write | The default. Every endpoint |
Scope read | GET only. Any write returns 403 {"detail": "This API key is read-only."} |
| Limit | 20 active keys per account. Use one key per integration so you can revoke one without breaking the others |
| Revocation | Immediate. Requests with a revoked key return 401 {"detail": "This API key has been revoked."} |
| Last used | Recorded per key (at most once a minute) and shown in the key list |
Unknown or malformed keys return 401 {"detail": "Invalid API key."} with WWW-Authenticate: Bearer realm="api". That covers any Authorization: Bearer … value or X-API-Key header that isn't one of your keys (wrong prefix, spaces, a token from another service); only a request with no credential at all gets {"detail": "Authentication credentials were not provided."}.
API keys are the only credential for integrations.
Session auth and the Swagger UI
If you are signed in to dealership.nz in your browser, the interactive reference at https://dealership.nz/api/v1/docs/ can call the API with your session: sign in as your dealership account, open the docs and use “Try it out”. You can also paste an API key with the “Authorize” button.
Rate limits and errors
| Scope | Limit | Keyed by |
|---|---|---|
| Authenticated requests | 120 / minute | Account (the key's owner) |
| Public read API | 300 / minute | IP address |
POST /api/v1/listings/import/ | 60 / hour | Account (replaces the 120/minute limit for that endpoint) |
Requests to authenticated endpoints without a valid credential are rejected with 401 before any throttle applies; there is no separate anonymous limit there.
Over the limit you get 429 with a Retry-After header (seconds):
HTTP 429
Retry-After: 3599
{"detail": "Request was throttled. Expected available in 3599 seconds."}
Error bodies are JSON:
| Status | Shape | Example |
|---|---|---|
| 400 validation | {"<field>": ["message", ...]}; non-field problems under non_field_errors | {"model_id": ["This field is required."], "km": ["This field is required."]}{"non_field_errors": ["Invalid year/make/model combination."]} |
| 400 bad query parameter | {"<param>": "message"} | {"status": "must be one of draft, published, sold"}{"make": "must be an integer"} |
| 401 | {"detail": "..."} plus WWW-Authenticate: Bearer realm="api" | {"detail": "Authentication credentials were not provided."} |
| 403 | {"detail": "..."} | {"detail": "This API key is read-only."} |
| 404 | {"detail": "..."} | {"detail": "No Listing matches the given query."} |
| 429 | {"detail": "..."} plus Retry-After | see above |
The import endpoint is the exception: a malformed envelope is a 400, but a bad item never fails the batch; it is reported per item (see Response).
Public read API
Base: https://dealership.nz/api/v1/public/. No credentials (an Authorization header is ignored, never rejected). Every response, including errors and the 404 {"detail": "Not found."} for an unknown path or a malformed listing id under this prefix, carries:
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, OPTIONS
Access-Control-Allow-Headers: Content-Type
Access-Control-Max-Age: 86400
Successful GETs (and HEADs) also carry Cache-Control: public, max-age=60, so changes take up to a minute to appear. OPTIONS (CORS preflight) returns 204 with no body:
curl -i -X OPTIONS https://dealership.nz/api/v1/public/dealers/your-slug/listings/ \
-H "Origin: https://www.your-dealership.example" -H "Access-Control-Request-Method: GET"
Only active dealerships are visible: a lapsed subscription makes every endpoint under dealers/<slug>/ return 404, the same as the portal page. <slug> is the last part of your portal URL, https://dealership.nz/dealerships/<slug>/.
Dealer profile
GET /api/v1/public/dealers/<slug>/
curl https://dealership.nz/api/v1/public/dealers/your-slug/
{
"slug": "auckland-auto-gallery",
"name": "Auckland Auto Gallery",
"tagline": "Family-owned European specialists since 1998",
"about": "Two acres of hand-picked stock in Ahuroa …",
"mvt_number": "M123456",
"established_year": 1998,
"website": "https://www.auckland-auto-gallery.example",
"public_email": "sales@auckland-auto-gallery.example",
"street_address": "1 Example Street",
"locality": {"name": "Ahuroa", "region": "Auckland"},
"latitude": "-36.852100",
"longitude": "174.763300",
"services": ["Servicing & mechanical", "WoF inspections", "Parts", "Valet & detailing"],
"brands": ["Audi", "BMW", "Volkswagen"],
"open_days": ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
"opening_hours": {"0": {"open": "08:30", "close": "17:30"}, "6": null},
"vehicle_conditions": ["new", "used", "demo"],
"offers_finance": true,
"accepts_trade_ins": true,
"phone": "09 555 1001",
"logo_url": null,
"cover_image_url": null,
"portal_url": "https://dealership.nz/dealerships/auckland-auto-gallery/",
"listing_count": 6
}
phone is null unless the dealership has chosen to show its number on listings; public_email is a contact address the dealership chooses to publish and is "" when they have not set one (the login email is never exposed). opening_hours keys are weekday ints as strings ("0" is Monday) with null for a day the dealership is closed, and open_days is the same fact as a list of labels. latitude/longitude are the yard's own pin and are null until the dealership drops one. brands are the franchises the dealership specialises in. listing_count counts published listings only.
Listings
GET /api/v1/public/dealers/<slug>/listings/ — published listings only, newest first, paginated.
curl "https://dealership.nz/api/v1/public/dealers/your-slug/listings/?sort=price_asc&page_size=2"
{
"count": 6,
"next": "https://dealership.nz/api/v1/public/dealers/auckland-auto-gallery/listings/?page=2&page_size=2&sort=price_asc",
"previous": null,
"results": [
{
"public_id": "312b8518-bcff-4489-90da-a6f6a1c13305",
"external_ref": "",
"url": "https://dealership.nz/cars/312b8518-bcff-4489-90da-a6f6a1c13305/",
"year": 2020,
"make": "NISSAN",
"model": "LEAF",
"submodel": "40kWh",
"title": "2020 NISSAN LEAF 40kWh",
"category": "Hatch",
"fuel_type": "Electric",
"colour": "BLACK",
"km": 52000,
"price": 24990,
"summary": "Affordable EV with good range for daily commuting.",
"locality": {"name": "Ahuroa", "region": "Auckland"},
"published_at": "2026-09-01T09:12:44.118237+12:00",
"trades_considered": true,
"photos": [
{"url": "https://dealership.nz/media/listing_photos/312b8518-.../front.jpg", "thumbnail_url": "https://dealership.nz/media/listing_photos/312b8518-.../front-thumb.jpg", "position": 0}
],
"cover_photo_url": "https://dealership.nz/media/listing_photos/312b8518-.../front.jpg",
"cover_thumbnail_url": "https://dealership.nz/media/listing_photos/312b8518-.../front-thumb.jpg"
}
]
}
price is null when the dealer wants enquiries instead of a sticker price; cover_photo_url is the first photo or null; cover_thumbnail_url is the same photo at most 800×600, which is what a listing card needs. Every photo has both url and thumbnail_url. external_ref is your own stock number (empty if you don't use one) so your site can link cars back to your DMS.
Query parameters (all optional; ids come from the catalog endpoints):
| Parameter | Type | Meaning |
|---|---|---|
year, year_min, year_max | int | Exact model year, or an inclusive range |
category | int | Body style id of the listing (SUV, Hatch, Ute, ...), from the categories endpoint |
make, model | int | Make id, model id |
fuel_type, colour | int | Fuel type id, colour id |
region, locality | int | Region id, locality id |
price_min, price_max | int | Whole dollars, inclusive |
km_min, km_max | int | Odometer, inclusive |
submodel | string | Submodel contains (case-insensitive) |
q | string | Free text across make, model, submodel and summary |
near, radius | int, number | Locality id and distance in km; results within the radius, nearest first |
sort | string | price_asc, price_desc, km_asc, km_desc, year_asc, year_desc. Default: newest first |
page | int | Page number. A value that isn't a number, or is past the last page, is 404 {"detail": "Invalid page."} |
page_size | int | Items per page, default 25, max 100 |
Unknown parameters and malformed filter values are ignored, never a 400, so a typo in a website snippet degrades to “unfiltered” rather than an error. page is the one exception (a 404, above); near/radius need each other and are ignored unless both are valid.
Single listing
GET /api/v1/public/dealers/<slug>/listings/<public_id>/ — the same object as one item of the list. 404 if the listing is not published, or belongs to another dealership.
curl https://dealership.nz/api/v1/public/dealers/your-slug/listings/312b8518-bcff-4489-90da-a6f6a1c13305/
Catalog: makes and models
Site-wide (not per dealership) lists of makes and models that currently have at least one published listing, for building filter dropdowns.
curl https://dealership.nz/api/v1/public/catalog/makes/
# [{"id": 1898, "name": "FORD"}, {"id": 1957, "name": "HONDA"}, {"id": 2232, "name": "TOYOTA"}, ...]
curl "https://dealership.nz/api/v1/public/catalog/models/?make=2232"
# [{"id": 81627, "name": "COROLLA", "make": 2232, "category": "Hatch"}, {"id": 81670, "name": "HILUX", "make": 2232, "category": "Ute"}, ...]
make is optional on the models endpoint; a non-integer value is the one public request that returns 400 {"make": "must be an integer"}.
JavaScript example
// Public API from the browser: no key, CORS-enabled, cached for 60 s.
const DEALER = "your-slug";
const API = "https://dealership.nz/api/v1/public";
async function loadStock() {
const params = new URLSearchParams({ sort: "price_asc", page_size: "12" });
const resp = await fetch(`${API}/dealers/${DEALER}/listings/?${params}`);
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const page = await resp.json(); // { count, next, previous, results }
for (const car of page.results) {
console.log(car.title, car.price ?? "Enquire for price", car.url, car.cover_photo_url);
}
return page;
}
loadStock().catch(console.error);
Embed widget
A dependency-free script that renders your published stock as a responsive card grid on any website, using the public API. Paste this where the cars should appear:
<div data-dealership-listings="your-slug" data-limit="12" data-sort="price_asc" data-theme="light"></div>
<script src="https://dealership.nz/static/js/embed.js" async></script>
Preview it, with your slug filled in and a copy button, at https://dealership.nz/dealerships/your-slug/embed/. The page deliberately looks like a generic third-party site so you can judge how the widget sits on yours.
| Attribute | Values | Default |
|---|---|---|
data-dealership-listings | Your slug (required) | — |
data-limit | 1–100 cards | 12 |
data-sort | price_asc, price_desc, km_asc, km_desc, year_asc, year_desc | newest first |
data-make | A make id from /api/v1/public/catalog/makes/; shows only that make | all makes |
data-theme | light or dark | light |
data-origin | Override the API origin (normally derived from the script's src) | script origin |
- Each card links to the listing on dealership.nz (new tab) and shows the cover photo, title, price (“Enquire for price” when unset), odometer, fuel type and locality. A footer links to your portal (“View all N vehicles on dealership.nz”).
- While loading, when you have no published stock, or if the API is unreachable, the container shows a short message with a link to your portal instead of breaking the page.
- The script only ever inserts API data as text, never as HTML, and the script URL is stable across deploys.
Theming
The widget injects one stylesheet (#dnz-embed-style) and sets CSS variables on the container, so you can restyle it without touching the script:
.dnz-embed { --dnz-accent: #c2410c; --dnz-card: #fffaf5; --dnz-border: #f3d9c4; }
Variables: --dnz-bg, --dnz-card, --dnz-border, --dnz-text, --dnz-muted, --dnz-accent, --dnz-photo. Classes: .dnz-grid, .dnz-card, .dnz-photo, .dnz-body, .dnz-title, .dnz-price, .dnz-meta, .dnz-footer, .dnz-state; data-theme="dark" adds .dnz-theme-dark.
Single-page apps
The script mounts every [data-dealership-listings] element once, when it loads (or on DOMContentLoaded). If your framework renders the container later, call:
window.DealershipNZEmbed.init(); // scans for containers that haven't been mounted yet
Already-mounted containers (marked data-dnz-mounted="1") are skipped, so calling it repeatedly is safe.
Listings API (authenticated)
Base: https://dealership.nz/api/v1/. All requests need an API key and an active Dealership subscription. JSON in and out unless noted. You only ever see your own listings; another account's public_id is a 404.
The listing object
{
"public_id": "54b11f78-8ded-4132-8f74-02bda5a3686d",
"year": 2021,
"make": "TOYOTA",
"model": "COROLLA",
"submodel": "GX Hatch",
"category": "Hatch",
"category_id": 31,
"fuel_type": "Petrol",
"colour": "SILVER",
"km": 45210,
"price": 21990,
"summary": "One owner, full service history.",
"locality": {"id": 194, "name": "Albany", "postcode": "0632", "territory": "Auckland", "region": "Auckland"},
"status": "draft",
"published_at": null,
"trades_considered": true,
"external_ref": "STK123",
"photos": [
{"id": 5, "url": "https://dealership.nz/media/listing_photos/54b11f78-.../front.jpg", "thumbnail_url": "https://dealership.nz/media/listing_photos/54b11f78-.../front-thumb.jpg", "position": 0}
],
"created_at": "2026-09-13T03:41:50.255953+12:00",
"updated_at": "2026-09-13T03:41:50.255958+12:00"
}
Timestamps are ISO 8601 in New Zealand time. make, model, category, fuel_type and colour are names on the way out; on the way in you send ids (make_id, model_id, category_id, fuel_type_id, colour_id, locality_id). The import endpoint accepts names instead.
category is the listing's own body style (SUV, Sedan, Hatch, Wagon, Ute, Coupe, Convertible, Van, Truck, Motorcycle), not a property of the model: many models ship as several bodies (a Corolla is a hatch, a sedan or a wagon). It defaults to the model's most common NZ body when you create a listing or change its model, and you can set category_id to say otherwise.
List your listings
GET /api/v1/listings/ — newest first, 25 per page (?page=2).
| Parameter | Meaning |
|---|---|
external_ref | Exact match on your stock number |
status | draft, published or sold; anything else is 400 {"status": "must be one of draft, published, sold"}. Publishing requires the dealership to have its Motor Vehicle Trader (MVT) number on its Dealership page; until then published is refused with 400 {"status": ["Add your Motor Vehicle Trader (MVT) registration number…"]} (bulk import reports the same per item). |
curl -H "Authorization: Bearer $DEALERSHIP_API_KEY" "https://dealership.nz/api/v1/listings/?status=published"
curl -H "Authorization: Bearer $DEALERSHIP_API_KEY" "https://dealership.nz/api/v1/listings/?external_ref=STK123"
# {"count": 1, "next": null, "previous": null, "results": [ { ...listing... } ]}
Retrieve
GET /api/v1/listings/<public_id>/
curl -H "Authorization: Bearer $DEALERSHIP_API_KEY" https://dealership.nz/api/v1/listings/54b11f78-8ded-4132-8f74-02bda5a3686d/
Create
POST /api/v1/listings/ → 201 with the listing object.
| Field | Type | Required | Notes |
|---|---|---|---|
year | int | yes | Together with make_id and model_id must be a catalog model year, else 400 {"non_field_errors": ["Invalid year/make/model combination."]} |
make_id | int | yes | From GET /api/v1/makes/ |
model_id | int | yes | From GET /api/v1/models/?make=<make_id> |
km | int ≥ 0 | yes | Odometer |
submodel | string ≤ 200 | no | Trim / variant, e.g. GX Hatch |
category_id | int or null | no | Body style, from GET /api/v1/categories/. Omit for the model's default (a Corolla → Hatch); send another id when this car differs (a Corolla wagon → Wagon). null resets to the default. On update, omitting it keeps the current value unless make_id/model_id move the listing to a different model |
fuel_type_id | int or null | no | From GET /api/v1/fuel-types/ |
colour_id | int or null | no | From GET /api/v1/colours/ |
price | int ≥ 0 or null | no | Whole dollars. null shows “Enquire for price” |
summary | string ≤ 500 | no | Plain text |
locality_id | int or null | no | From GET /api/v1/localities/?region=<id> |
status | draft or published | no | Default draft. published stamps published_at |
trades_considered | bool | no | Default false |
external_ref | string ≤ 100 | no | Your stock number; must be unique among your listings (blank is always allowed) |
| vehicle details and CIN fields | no | See Vehicle details and the Consumer Information Notice |
curl -X POST https://dealership.nz/api/v1/listings/ \
-H "Authorization: Bearer $DEALERSHIP_API_KEY" -H "Content-Type: application/json" \
-d '{
"year": 2021, "make_id": 2232, "model_id": 81627, "submodel": "GX Hatch",
"fuel_type_id": 66, "colour_id": 54, "km": 45210, "price": 21990,
"summary": "One owner, full service history.", "locality_id": 194,
"status": "draft", "trades_considered": true, "external_ref": "STK123"
}'
A second listing with the same external_ref is rejected: 400 {"external_ref": ["Another of your listings already uses this external_ref."]}.
Vehicle details and the Consumer Information Notice
Every listing can carry these optional details; the listing page shows them and the read API returns them.
| Field | Type | Notes |
|---|---|---|
condition | used, demo or new | Default used |
transmission | automatic, manual or "" | |
engine_cc | int 0–20000 or null | Engine capacity as on the motor vehicle register |
import_status | nz_new, import or "" | import = a used import |
plate | string | Registration plate; spaces and dashes are removed (abc 123 → ABC123), more than six letters/numbers is 400 |
vin | string ≤ 30 | VIN, or the chassis number when the register has no VIN |
has_wof, wof_expiry | bool or null, YYYY-MM-DD or null | Current WoF / CoF and its expiry |
has_licence, licence_expiry | bool or null, date or null | Current vehicle licence (rego) and the expiry on the latest licence |
first_registered_nz_year | int or null |
Motor vehicle traders must show a Consumer Information Notice (CIN) with every used vehicle they offer for sale, online included (Consumer Information Standards (Used Motor Vehicles) Regulations 2008). A Dealership-plan account can't publish a used or demo listing — or keep one published through an update — until its CIN is complete; the error lists what is missing: 400 {"status": ["Motor vehicle traders must show a Consumer Information Notice ... Still needed: VIN or chassis number, ..."]} (the bulk import reports it per item and leaves that car unchanged). new vehicles need no CIN. Either send the CIN fields below (with price, engine_cc, vin, fuel_type, plate, the WoF/licence fields, first_registered_nz_year and import_status above; your business name, address and MVT number come from your Dealership page), or send cin_url, a link to the CIN your dealer management system already publishes.
| Field | Type | Notes |
|---|---|---|
odometer_status | accurate, uncertain or inaccurate | Default accurate (the km reading is the distance travelled); the other two print the regulations' statements instead |
is_registered, re_registered | bool or null | Currently registered; re-registered after deregistration (plate is needed when registered) |
security_interest | bool or null | A security interest is recorded on the PPSR |
radio_88_108 | bool or null | The radio receives 88–108 MHz without a band expander |
ruc_applies, ruc_outstanding | bool or null | Road user charges apply; any outstanding (needed when they apply) |
first_registered_overseas_year, last_registered_country, imported_damaged | int, string ≤ 60, bool | Needed for a used import (import_status: "import") |
cin_url | URL ≤ 500 | Instead of the fields above |
price must be the full cash price including GST and on-road costs: a CIN can't say POA, so a dealer's used car without a price can't go live.
Update (PUT and PATCH)
PUT /api/v1/listings/<public_id>/ takes the same fields as create (year, make_id, model_id, km required). Any optional field you omit keeps its current value: a PUT without status never unpublishes, a PUT without price keeps the price. To clear a nullable field send null explicitly.
PATCH /api/v1/listings/<public_id>/ accepts any subset of fields. year, make_id and model_id may be sent individually; the others fall back to the listing's current vehicle. Both return 200 with the updated listing object.
curl -X PATCH https://dealership.nz/api/v1/listings/54b11f78-8ded-4132-8f74-02bda5a3686d/ \
-H "Authorization: Bearer $DEALERSHIP_API_KEY" -H "Content-Type: application/json" \
-d '{"price": 20990}'
curl -X PUT https://dealership.nz/api/v1/listings/54b11f78-8ded-4132-8f74-02bda5a3686d/ \
-H "Authorization: Bearer $DEALERSHIP_API_KEY" -H "Content-Type: application/json" \
-d '{"year": 2021, "make_id": 2232, "model_id": 81627, "km": 46000, "submodel": "GX Hatch"}'
# status, price, colour, locality... unchanged
Delete
DELETE /api/v1/listings/<public_id>/ → 204, no body. Photos go with it. Deleting works on any status, including sold.
curl -X DELETE -H "Authorization: Bearer $DEALERSHIP_API_KEY" https://dealership.nz/api/v1/listings/54b11f78-8ded-4132-8f74-02bda5a3686d/
Status, published_at and sold listings
statusisdraft,publishedorsold. The API can setdraftandpublished; a car is marked sold from the site (My listings).- Moving from
drafttopublishedstampspublished_at, which drives buyers' saved-search alerts. Moving back todraftkeeps the oldpublished_at(“when it last went live”); publishing again re-stamps it. - Sold listings are frozen. Any PUT or PATCH returns
400 {"non_field_errors": ["Sold listings cannot be edited."]}, and the importer skips them witherrors.status = ["sold listings are frozen"]. They can still be read and deleted.
curl -X PATCH https://dealership.nz/api/v1/listings/54b11f78-8ded-4132-8f74-02bda5a3686d/ \
-H "Authorization: Bearer $DEALERSHIP_API_KEY" -H "Content-Type: application/json" \
-d '{"status": "published"}'
# ... "status": "published", "published_at": "2026-09-13T03:41:50.463831+12:00" ...
external_ref
Your stock number, up to 100 characters. It is unique among your listings (two dealerships can both have STK123), searchable with ?external_ref=, shown on the public API so your website can link back to your DMS, and it is the key the import endpoint upserts on. Set it on everything you create through the API.
Photos
POST /api/v1/listings/<public_id>/photos/ — multipart/form-data, one image per request.
| Field | Required | Notes |
|---|---|---|
image | yes | JPEG, PNG, WebP or HEIC by file extension; max 10 MB; the bytes must decode as an image. Stored as an upright, resized JPEG whatever you send |
position | no | Integer ≥ 0: the photo is inserted at that index and the photos from there on move up one, so 0 makes it the cover. Default appends after the existing photos; a value past the end is clamped to the count |
curl -X POST https://dealership.nz/api/v1/listings/54b11f78-8ded-4132-8f74-02bda5a3686d/photos/ \
-H "Authorization: Bearer $DEALERSHIP_API_KEY" \
-F "image=@front.jpg" -F "position=0"
# HTTP 201
# {"id": 212, "url": "https://dealership.nz/media/listing_photos/54b1.../3f9c....jpg", "position": 0}
A listing holds at most 9 photos; the tenth returns 400 {"error": "Maximum 9 photos."}. Other failures:
| Problem | Response |
|---|---|
| No file | 400 {"image": ["No file was submitted."]} |
| Not an image | 400 {"image": ["That file isn't a readable image."]} |
| Wrong extension | 400 {"image": ["Unsupported image type. Use JPEG, PNG, WebP or HEIC."]} |
A successful upload returns the new photo as {"id", "url", "thumbnail_url", "position"} (HTTP 201); the same objects appear in the listing's photos array, ordered by position. In the example above position=0 makes front.jpg the cover and renumbers the others 1, 2, …
Delete a photo — DELETE /api/v1/listings/<public_id>/photos/<photo_id>/ → 204. Works for manual and imported photos; the remaining photos are renumbered so position stays gap-free. Unknown ids return 404.
Reorder photos — PUT /api/v1/listings/<public_id>/photos/order/ with {"order": [id, id, ...]} listing every photo id of the listing exactly once; the first becomes the cover. Returns the photos in their new order. A missing or extra id returns 400 {"order": ["must contain every photo id of this listing exactly once"]}.
curl -X PUT https://dealership.nz/api/v1/listings/54b11f78-8ded-4132-8f74-02bda5a3686d/photos/order/ \
-H "Authorization: Bearer $DEALERSHIP_API_KEY" -H "Content-Type: application/json" \
-d '{"order": [212, 210, 211]}'
If you use the importer, let it own the photos it fetched from URLs and reserve these endpoints for manual photos.
Catalog endpoints
Read-only reference data for the ids used above. All need authentication.
| Endpoint | Returns | Notes |
|---|---|---|
GET /api/v1/makes/ | [{"id", "name"}] | All makes (~575), not paginated |
GET /api/v1/models/?make=<id>&category=<id> | Paginated {"count", "next", "previous", "results": [{"id", "name", "make", "category", "range_km", "battery_kwh", "charge_time_hours", "fast_charge_minutes"}]} | make and category here are names; the EV fields are null for non-EVs. Both filters optional, must be integers |
GET /api/v1/categories/ | [{"id", "name"}] | Body styles a listing can carry: SUV, Sedan, Hatch, Wagon, Ute, Coupe, Convertible, Van, Truck, Motorcycle (the catalog also has Bus, Caravan, Trailer, Other). A model's category is its most common body in the NZ fleet — the default for new listings of it, not a rule |
GET /api/v1/fuel-types/ | [{"id", "name"}] | Petrol, Diesel, Electric, Petrol Hybrid, ... |
GET /api/v1/colours/ | [{"id", "name"}] | BLACK, BLUE, SILVER, ... |
GET /api/v1/regions/ | [{"id", "name", "island"}] | 16 regions; island is north or south |
GET /api/v1/localities/?region=<id>&page_size=<n> | Paginated {"count", "next", "previous", "results": [{"id", "name", "postcode", "territory", "region"}]} | Suburbs and towns ordered by region, then name; region in the output is a name. 25 per page by default; page_size goes up to 500, so ?region=<id>&page_size=500 fetches a whole region in one call |
curl -H "Authorization: Bearer $DEALERSHIP_API_KEY" "https://dealership.nz/api/v1/models/?make=2232&category=31"
curl -H "Authorization: Bearer $DEALERSHIP_API_KEY" "https://dealership.nz/api/v1/localities/?region=2"
If your DMS holds names rather than ids, skip all of this and use the resolve helper or the importer.
Import / sync
POST /api/v1/listings/import/ upserts up to 100 listings in one request, keyed by external_ref. It is the endpoint to use for a DMS feed: it takes names instead of ids, fetches photos from URLs, reports per vehicle, and never fails the whole batch because one car is wrong. Rate limit: 60 requests per hour; needs a read_write key.
Request
{
"listings": [ { ...item... }, ... ],
"prune_missing": false,
"dry_run": false
}
| Field | Type | Default | Meaning |
|---|---|---|---|
listings | array, 1–100 items | required | One object per vehicle (below). An empty array, more than 100, or not an array is a 400 for the whole request |
prune_missing | bool | false | After processing, set your published listings whose external_ref is not in this batch to draft (see Idempotency for the safety rules) |
confirm_prune | bool | false | Let prune_missing demote more than half of your published stock; without it such a request is refused with 400 |
dry_run | bool | false | Validate and report only; write nothing, fetch no photos |
Item fields
Only external_ref, year, km and a vehicle (names or ids) are required. On update, any field you omit keeps its current value; send "" or null to clear an optional one.
| Field | Type | Notes |
|---|---|---|
external_ref | string ≤ 100 | Required. Your stock number; the upsert key. Must be unique within the batch |
year | int 1886–2100 | Required. Model year |
make, model | string | Vehicle by name, e.g. "Toyota", "Corolla". model may repeat the make ("Toyota Corolla", "VW Golf") and may carry a variant ("Hilux SR5" → HILUX, with SR5 moved into submodel); see resolution |
make_id, model_id | int | Vehicle by catalog id, as an alternative to names; must be sent together |
km | int ≥ 0 | Required. Odometer |
submodel | string ≤ 200 | Trim / variant |
category / category_id | string / int | Body style by name ("Hatch", case-insensitive: SUV, Sedan, Hatch, Wagon, Ute, Coupe, Convertible, Van, Truck, Motorcycle) or id. Omit for the model's default; ""/null resets to it. On update, omitted keeps the current value unless the vehicle moves to another model |
fuel_type / fuel_type_id | string / int | By name ("Petrol", also Hybrid, EV, PHEV, Unleaded…) or id; ""/null clears. A name that can't be resolved is a warning, not a skip: the listing is saved with the field blank |
colour / colour_id | string / int | By name ("Silver", also Gray, Charcoal, Pearl White, Dark Blue…) or id; ""/null clears. Unresolved names are a warning and left blank, like fuel_type |
price | int ≥ 0 or null | Whole dollars; null = enquire |
summary | string | Longer than 500 characters is truncated with a warning |
locality / locality_id | string / int | Suburb or town by name or id; ""/null clears. A name shared by several localities needs postcode or region, or the "Richmond (Tasman)" form the error suggests |
postcode | string | Disambiguates locality (several NZ suburbs share a name) |
region | string | Region name; also disambiguates locality |
status | draft or published | Default published, on update as well as create. Send "draft" explicitly to keep a car off the site |
trades_considered | bool | |
| vehicle details and CIN fields | As on the listings API; omitted fields keep their value. A dealer's used car needs a complete CIN (or cin_url) to be published, so a feed that publishes should send them | |
photos | array of URL strings | Up to 9 public http(s) image URLs (ports 80/443 only) in display order. Omit the key to leave photos alone; [] removes every imported photo. At most 60 new photos are downloaded per request; see Photo sync |
Vehicle and name resolution
Names are matched against the NZTA catalog case-insensitively, then ignoring punctuation and whitespace ("Mercedes Benz" = MERCEDES-BENZ), with common aliases (VW, Merc, Mercedes, Benz, Chev, Chevy, Landrover, Alfa). Range Rover and Rangerover both resolve to the LAND ROVER make (the fleet data also lists a “RANGE ROVER” make, which we don't use); the Range Rover models live under it as RANGE ROVER, RANGE ROVER EVOQUE, RANGE ROVER SPORT…, and make: "Range Rover", model: "Evoque" finds RANGE ROVER EVOQUE on its own.
model is forgiving about what a DMS puts in that column:
- It may repeat the make, including an alias:
"Toyota Corolla","VW Golf","Mercedes Benz C-Class". - It may carry a variant. When the whole string isn't a catalog model, the longest catalog model that starts the string at a word boundary is used and the rest goes into
submodel:"Hilux SR5"→HILUX+SR5,"Golf GTI"→GOLF+GTI,"Corolla Hatch"→COROLLA+Hatch,"Land Cruiser Prado VX"→LANDCRUISER PRADO+VX. The item gets a warning ("model 'Hilux SR5' resolved to HILUX; 'SR5' moved to submodel"); if you sentsubmodelas well, yours is kept and the leftover is dropped with a warning."Hiluxe"does not matchHILUX(no word boundary).
A vehicle name that still doesn't match skips the item with a “did you mean” error:
"errors": {"make": ["Unknown make 'Toyotaa'. Did you mean: TOYOTA?"]}
"errors": {"year": ["No 2010 model year for TOYOTA COROLLA. Known years: 2018-2020."]}
"errors": {"locality": ["Unknown locality 'Albony'. Did you mean: Albany?"]}
"errors": {"locality": ["Ambiguous locality 'Richmond' — send region or postcode, or use one of: Richmond (Canterbury), Richmond (Southland), Richmond (Tasman)"]}
Category (body style) and locality resolve the same way and are errors on a miss; body styles also understand the usual synonyms (Hatchback, Saloon, Station Wagon/Estate, Utility/Pickup, 4WD/Crossover, Cabriolet, People mover), and a miss reads "errors": {"category": ["Unknown category 'Wagn'. Did you mean: Wagon?"]}. A locality name that several NZ localities share (three Richmonds, two Newtowns) is ambiguous rather than unknown: send region or postcode, or send the suggested "Richmond (Tasman)" form as locality.
Fuel type and colour are soft. They understand the spellings feeds use — fuel: Hybrid → Petrol Hybrid, EV/BEV/Electric Vehicle → Electric, PHEV/Plug-in Hybrid → Plug-in Petrol Hybrid, Petrol/Electric → Petrol Hybrid, Diesel/Electric → Diesel Hybrid, Unleaded/ULP/Gasoline → Petrol; colour: Gray/Charcoal → GREY, Navy → BLUE, Burgundy/Maroon → RED, and any shade that names exactly one catalog colour (Pearl White, Metallic Silver, Dark Blue) — and when a name still can't be resolved the car is not skipped: it is saved with that field blank and the item carries a warning such as "Unknown colour 'Teal'; left blank." or "Unknown fuel type 'Petrl'; left blank. Did you mean: Petrol?". A wrong fuel_type_id/colour_id is still an error.
Check a feed before you push it with the resolve helper.
Photo sync
The feed is the source of truth for the photos it supplies; photos uploaded by hand are never touched.
- Every fetched photo remembers the URL it came from. On the next import, URLs already fetched are kept without re-downloading (exact string match, so keep URLs stable), URLs that disappeared from the list are removed, and new URLs are fetched. The final order is the feed order, followed by any manually uploaded photos.
- Fetch problems (
HTTP 404, timeouts, an HTML page instead of an image, a private address, a storage failure on our side:"photo 2: could not be stored; resend to retry") are per-photo warnings: the listing is still created or updated, just without that photo. - Limits: 9 photos per listing in total, counting manual ones (
"photo 8: skipped, listing already has 9 photos"); more than 9 URLs are cut with a warning; 10 MB per image; 20 seconds of wall-clock time per photo (redirects included; any single connect or read stalls out after 10 s); at most 5 redirects. - 60 new photos per request. Photos already fetched cost nothing, but a request downloads at most 60 new ones; the rest are skipped with
"photo 3: deferred; resend to fetch (60 new photos per request)"and are fetched when the feed is sent again. A batch of 100 brand-new cars with 9 photos each therefore takes a few passes to fill in; nothing is lost. - URL rules.
http://orhttps://only, on port 80 or 443 only ("only ports 80 and 443 are allowed"), no credentials in the URL, and the host (and every redirect target) must resolve to a public address:localhost, loopback, private ranges (10.x,172.16.x,192.168.x), link-local and reserved ranges are refused with"host resolves to a private or reserved address". The connection is made to the exact address that passed the check (theHostheader and TLS name stay as in the URL), so a name that changes its answer mid-request can't redirect the download; every redirect hop is checked and pinned the same way. The bytes must decode as a JPEG, PNG, WebP or HEIC image, whatever theContent-Typesays; an extension-less URL is typed byContent-Type. - Nothing is fetched during a
dry_run; you get"dry run: N photo(s) would be fetched"/"... would be removed"instead.
Response
Always 200 once the envelope is valid, whatever happened to individual items.
{
"dry_run": false,
"summary": {"created": 1, "updated": 1, "unchanged": 0, "skipped": 1, "pruned": 1},
"warnings": [],
"results": [
{
"external_ref": "STK123",
"action": "created",
"public_id": "e2e1114a-2a26-4b8c-9e8e-e9edf88ab993",
"url": "https://dealership.nz/cars/e2e1114a-2a26-4b8c-9e8e-e9edf88ab993/",
"warnings": ["model 'Corolla Hatch' resolved to COROLLA; 'Hatch' moved to submodel"],
"errors": {}
},
{
"external_ref": "STK124",
"action": "updated",
"public_id": "6fe8ee94-017b-4b48-b392-ffc665c67b2b",
"url": "https://dealership.nz/cars/6fe8ee94-017b-4b48-b392-ffc665c67b2b/",
"warnings": ["photo 2: host resolves to a private or reserved address", "photo 3: HTTP 404"],
"errors": {}
},
{
"external_ref": "STK125",
"action": "skipped",
"public_id": null,
"url": null,
"warnings": [],
"errors": {"make": ["Unknown make 'Toyotaa'. Did you mean: TOYOTA?"]}
}
],
"pruned": ["STK099"]
}
| Field | Meaning |
|---|---|
summary | Counts per action, plus pruned |
warnings | Batch-level notes, currently only "prune_missing ignored: every item in the batch was skipped, so nothing was pruned" |
results[].action | created, updated, unchanged (nothing differed, including photos) or skipped (see errors) |
results[].public_id, url | The listing's id and public page. null for skipped items that don't exist yet, and for created items in a dry run. A sold listing that was skipped still reports its public_id |
results[].warnings | Non-fatal notes: photo problems and deferrals, an unresolved fuel type or colour left blank, a model variant moved into submodel, summary truncation, dry-run photo counts |
results[].errors | {"field": ["message", ...]}; the item was skipped. List-field errors are flattened the same way ("photos": ["photos 1: Not a valid string."]) |
pruned | external_refs demoted to draft by prune_missing |
Results are in request order. Item-level errors you may see: "external_ref": ["duplicate external_ref in this batch"], "make_id": ["make_id and model_id must be given together."], "status": ["sold listings are frozen"], "external_ref": ["another import is creating this listing; retry"] (two imports raced; run again).
Envelope errors (whole request rejected with 400, nothing written): {"listings": ["This field is required."]}, {"listings": ["This list may not be empty."]}, {"listings": ["Expected a list of items but got type \"str\"."]}, {"listings": ["Ensure this field has no more than 100 elements."]}, {"non_field_errors": ["Invalid data. Expected a dictionary, but got list."]}, and the prune refusal {"prune_missing": ["This batch would demote 41 of your 60 published listings that carry an external_ref (more than half), so prune_missing was refused and nothing was imported. Send \"confirm_prune\": true if the feed really is complete."]}.
Idempotency and prune_missing
The upsert key is (your account, external_ref). Sending the same item twice creates once, then reports unchanged; changing a field reports updated. So a feed can simply be re-sent in full, as often as you like: the endpoint compares and only writes what changed.
prune_missing: true turns a full feed into a mirror: any of your published listings with a non-empty external_ref that is not in this batch is set to draft (never deleted; sold listings and listings without an external_ref are left alone) and listed under pruned. Items that were skipped for validation errors still count as “in the feed”, so a bad row never gets its car pruned. Because status defaults to published, a pruned car that reappears in a later feed goes live again automatically.
Three rules stop a broken export from taking your stock off the site:
- An empty
listingsarray is a 400 ({"listings": ["This list may not be empty."]}), whatever the other flags say. - If every item in the batch was skipped, nothing is pruned. The response is still
200, with"warnings": ["prune_missing ignored: every item in the batch was skipped, so nothing was pruned"]andpruned: []. - A prune that would demote more than half of your published listings with an
external_refis refused with400 {"prune_missing": ["This batch would demote 41 of your 60 published listings … Send \"confirm_prune\": true if the feed really is complete."]}. The check runs before anything is written, so the batch's upserts are not applied either. Send"confirm_prune": truewhen that is what you mean, e.g. the first sync after re-numbering your stock or after clearing out most of it.
Never set prune_missing on a partial feed: an event-driven “just this one car changed” request must leave it false.
dry_run
"dry_run": true runs the whole batch inside a transaction that is rolled back: names are resolved, every item is validated, prune_missing is evaluated (including the refusal above, so a dry run also tells you when confirm_prune will be needed), and you get the exact report a real run would give, except that photos are not fetched (counted as warnings instead) and created items have no public_id yet. Use it when connecting a new feed, and again whenever a summary shows skipped > 0 before retrying.
curl -X POST https://dealership.nz/api/v1/listings/import/ \
-H "Authorization: Bearer $DEALERSHIP_API_KEY" -H "Content-Type: application/json" \
-d '{
"dry_run": true,
"prune_missing": false,
"listings": [
{
"external_ref": "STK123",
"make": "Toyota", "model": "Corolla", "year": 2021, "submodel": "GX Hatch",
"fuel_type": "Petrol", "colour": "Silver", "km": 45210, "price": 21990,
"summary": "One owner, full service history.",
"locality": "Albany", "postcode": "0632",
"status": "published", "trades_considered": true,
"photos": ["https://cdn.dms.example/stk123/front.jpg", "https://cdn.dms.example/stk123/rear.jpg"]
},
{
"external_ref": "STK124",
"make_id": 2232, "model_id": 81670, "year": 2023,
"fuel_type_id": 56, "colour_id": 43, "locality_id": 194,
"km": 12000, "price": 58990,
"photos": ["https://cdn.dms.example/stk124/front.jpg"]
},
{"external_ref": "STK125", "make": "Toyotaa", "model": "Hilux", "year": 2020, "km": 80000}
]
}'
{
"dry_run": true,
"summary": {"created": 2, "updated": 0, "unchanged": 0, "skipped": 1, "pruned": 0},
"results": [
{"external_ref": "STK123", "action": "created", "public_id": null, "url": null,
"warnings": ["dry run: 2 photo(s) would be fetched"], "errors": {}},
{"external_ref": "STK124", "action": "created", "public_id": null, "url": null,
"warnings": ["dry run: 1 photo(s) would be fetched"], "errors": {}},
{"external_ref": "STK125", "action": "skipped", "public_id": null, "url": null,
"warnings": [], "errors": {"make": ["Unknown make 'Toyotaa'. Did you mean: TOYOTA?"]}}
],
"pruned": []
}
Fix STK125, flip dry_run to false, send the same body again.
Catalog resolve helper
GET /api/v1/catalog/resolve/ runs the importer's name matching on a single vehicle, so you can validate a feed, or build a mapping table, before pushing anything. All parameters are optional strings except year.
| Parameter | Meaning |
|---|---|
make, model | Names, with the same aliases and model + variant handling as the importer; model is looked up within the resolved make and any leftover variant text comes back as submodel |
year | Model year; model_year_exists says whether the catalog has it for that make/model |
category | Body style name to check (Hatch). Leave it out to see the resolved model's default body style instead |
fuel_type, colour | Names, with the importer's aliases (Hybrid, EV, Gray, Pearl White…) |
locality, region, postcode | Locality name, narrowed by region name and/or postcode; the "Richmond (Tasman)" form is accepted too |
curl -G -H "Authorization: Bearer $DEALERSHIP_API_KEY" https://dealership.nz/api/v1/catalog/resolve/ \
--data-urlencode "make=Toyotaa" --data-urlencode "model=Corolla" --data-urlencode "year=2019" \
--data-urlencode "fuel_type=petrol" --data-urlencode "colour=Silver" \
--data-urlencode "locality=Albany" --data-urlencode "postcode=0632"
{
"make": null,
"model": null,
"submodel": null,
"year": 2019,
"model_year_exists": false,
"category": null,
"fuel_type": {"id": 66, "name": "Petrol"},
"colour": {"id": 54, "name": "SILVER"},
"locality": {"id": 194, "name": "Albany", "postcode": "0632", "territory": "Auckland", "region": "Auckland"},
"suggestions": {"makes": ["TOYOTA"], "models": [], "categories": [], "fuel_types": [], "colours": [], "localities": []}
}
A resolved field is {"id", "name"} (locality: the full locality object); an unresolved one is null with up to five close matches under suggestions. model is null whenever make is, since models are searched within a make. With make=VW&model=Golf GTI&year=2019 you would get "make": {"id": 2268, "name": "VOLKSWAGEN"}, "model": {"id": 87745, "name": "GOLF"}, "submodel": "GTI", "model_year_exists": true and "category": {"id": 31, "name": "Hatch"} — the body style the importer would give a Golf unless your feed says otherwise. An ambiguous locality=Richmond gives "locality": null with "suggestions": {"localities": ["Richmond (Canterbury)", "Richmond (Southland)", "Richmond (Tasman)"]}; send one of those back as locality, or add region/postcode. A non-integer year is 400 {"year": "must be an integer"}.
Recommended sync strategy
Pick one of two patterns (or combine them):
Nightly full sync with prune. Export every vehicle currently for sale, send it in batches of up to 100 (well inside 60 requests/hour), and set prune_missing: true on the last batch only, after all earlier batches succeeded — pruning compares against that one request's listings, so an earlier batch's cars would otherwise be demoted. Simplest mirror; stock is at most a night stale. With more than 100 cars the last batch alone will look like “less than half the stock”, so send confirm_prune: true with it; the guard is there for the day the export comes out empty or truncated.
Event-driven upserts. When a vehicle is added, changed or sold in the DMS, send that one item (prune_missing: false). To take a car off the site send it with "status": "draft"; to bring it back, send it again (status defaults to published). Fast, and cheap on the rate limit.
Either way:
- Set
external_refon every vehicle and keep it stable; it is the only thing that ties a DMS record to a listing. - Run the first import (and any change to the mapping code) with
dry_run: true, and fix everyskippeditem; usecatalog/resolve/to build a make/model alias table if your DMS spells things differently. - Send photos in the same request as the vehicle, with stable URLs, and send the full list each time: already-fetched URLs are free, and only differences are downloaded. Photo fetching happens while you wait, so allow a request timeout of a couple of minutes for a batch full of new cars; a request downloads at most 60 new photos and defers the rest (
"deferred; resend to fetch"), so the first sync of a big stock needs a few passes. Don't cache-bust photo URLs on every export, or every sync re-downloads everything. - Treat
warningsas something to log anderrorsas something to fix; neither is a reason to retry the same body blindly. Retry only429(afterRetry-After) and the"another import is creating this listing; retry"error. - Marking a car sold on the site freezes it; from then on the feed's copy is skipped (
"sold listings are frozen") until you delete it.
Python example
requests is not part of the dealership.nz project; install it with pip install requests. The script dry-runs two vehicles, stops if anything would be skipped, then syncs them for real. Re-running it is safe.
"""Sync two vehicles from your DMS into dealership.nz.
Needs the ``requests`` package (``pip install requests``) -- it is not part
of the dealership.nz project. Set DEALERSHIP_API_KEY to a read & write key.
"""
import os
import sys
import requests
BASE = os.environ.get("DEALERSHIP_API_BASE", "https://dealership.nz/api/v1")
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['DEALERSHIP_API_KEY']}"
vehicles = [
{
"external_ref": "STK123",
"make": "Toyota", "model": "Corolla", "year": 2021, "submodel": "GX Hatch",
"fuel_type": "Petrol", "colour": "Silver", "km": 45210, "price": 21990,
"summary": "One owner, full service history.",
"locality": "Albany", "postcode": "0632",
"photos": ["https://cdn.dms.example/stk123/front.jpg"],
},
{
"external_ref": "STK124",
"make": "Toyota", "model": "Hilux", "year": 2023,
"fuel_type": "Diesel", "colour": "Black", "km": 12000, "price": 58990,
"locality": "Albany", "postcode": "0632",
},
]
def sync(dry_run):
resp = session.post(
f"{BASE}/listings/import/",
json={"listings": vehicles, "prune_missing": False, "dry_run": dry_run},
timeout=120, # photos are fetched server-side while you wait
)
resp.raise_for_status()
report = resp.json()
for item in report["results"]:
line = f"{item['external_ref']}: {item['action']}"
if item["errors"]:
line += f" errors={item['errors']}"
for warning in item["warnings"]:
line += f"\n warning: {warning}"
print(line)
return report
# 1. Validate first: a dry run resolves names, reports what would happen,
# writes nothing and fetches no photos.
preview = sync(dry_run=True)
if preview["summary"]["skipped"]:
sys.exit("Fix the skipped items above, then run again.")
# 2. Same payload for real. Re-running it later is safe: unchanged vehicles
# report "unchanged" and already-fetched photos are not downloaded again.
report = sync(dry_run=False)
print("Summary:", report["summary"])
Output on first run:
STK123: created
warning: dry run: 1 photo(s) would be fetched
STK124: created
STK123: created
STK124: created
Summary: {'created': 2, 'updated': 0, 'unchanged': 0, 'skipped': 0, 'pruned': 0}
OpenAPI reference
The schema at https://dealership.nz/api/v1/schema/ (OpenAPI 3; YAML by default, ?format=json for JSON) is generated from the code and always matches the deployed version. The Swagger UI at https://dealership.nz/api/v1/docs/ groups operations as Public, Listings, Photos, Import and Catalog, and can call the API with a pasted key (Authorize) or your browser session. Feed the schema to your client generator of choice; the security scheme is ApiKeyAuth (HTTP bearer).