Appearance
vehicledb
39 endpoints.
POST /api/v1/vdb/auth/request-otp
Request Otp
Send a sign-in OTP to an existing account's mobile number.
Request body
json
{
"properties": {
"mobile_number": {
"type": "string",
"maxLength": 32,
"minLength": 6,
"title": "Mobile Number"
}
},
"type": "object",
"required": [
"mobile_number"
],
"title": "MobileIn"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/vdb/auth/signup
Signup
Stage a new account and send its verification OTP.
Nothing is billable until the OTP is verified, but unlike the bench (which held the whole signup in a Redis blob keyed by tmp_id) the app-user row is created here, unverified. A half-finished signup is then visible and recoverable instead of evaporating after 5 minutes.
Request body
json
{
"properties": {
"mobile_number": {
"type": "string",
"maxLength": 32,
"minLength": 6,
"title": "Mobile Number"
},
"name": {
"type": "string",
"maxLength": 255,
"minLength": 1,
"title": "Name"
},
"email": {
"type": "string",
"format": "email",
"title": "Email"
},
"company": {
"anyOf": [
{
"type": "string",
"maxLength": 255
},
{
"type": "null"
}
],
"title": "Company"
},
"reseller_code": {
"anyOf": [
{
"type": "string",
"maxLength": 64
},
{
"type": "null"
}
],
"title": "Reseller Code"
}
},
"type": "object",
"required": [
"mobile_number",
"name",
"email"
],
"title": "SignupIn"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/vdb/auth/verify-otp
Verify Otp
Verify an OTP and return the session token plus the account profile.
On a first verification for a signed-up user this also creates the books customer and grants the promo credit — deferred to here so an abandoned signup never leaves an empty customer behind in the books.
Request body
json
{
"properties": {
"mobile_number": {
"type": "string",
"maxLength": 32,
"minLength": 6,
"title": "Mobile Number"
},
"code": {
"type": "string",
"maxLength": 10,
"minLength": 4,
"title": "Code"
}
},
"type": "object",
"required": [
"mobile_number",
"code"
],
"title": "VerifyOtpIn"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/vdb/auth/sign-out-everywhere
Sign Out Everywhere
Invalidate every outstanding token for this account.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
authorization | header | no |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/vdb/account
Get Account
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
authorization | header | no |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/vdb/account/statement
Get Account Statement
Paged credit-wallet statement.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
from_date | query | no | ||
to_date | query | no | ||
page | query | no | integer | |
limit | query | no | integer | |
authorization | header | no |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/vdb/credits/spend
Spend Credits
Debit the wallet for a lookup the client performed outside the proxy.
Nothing calls this any more. Its reason for existing was the Lightstone flow, which was deleted 2026-08-13, and the app stopped calling it the same day — having it debit alongside /vdb/valuation's own charge is what made one check cost two credits. /vdb/valuation charges as part of the call, which is the safer shape: the charge and the thing charged for are one transaction, where this endpoint can only trust the client's word that a lookup happened.
Left in place solely because builds 4.2.0(3) and (4) are on testers' handsets and do call it; removing it would 404 their check rather than just overcharge it. Delete once nothing in the field is older than 4.2.0(5).
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
authorization | header | no |
Request body
json
{
"properties": {
"qty": {
"type": "number",
"maximum": 1000,
"exclusiveMinimum": 0,
"title": "Qty"
},
"report_type": {
"type": "string",
"maxLength": 64,
"title": "Report Type",
"default": "vin"
},
"reference": {
"anyOf": [
{
"type": "string",
"maxLength": 255
},
{
"type": "null"
}
],
"title": "Reference"
}
},
"type": "object",
"required": [
"qty"
],
"title": "SpendIn"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/vdb/vehicles
List Vehicles
Paginated vehicle reference list. Public — browsing costs no credits.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
search | query | no | ||
make | query | no | ||
model | query | no | ||
sub_type | query | no | ||
year | query | no | ||
page | query | no | integer | |
limit | query | no | integer |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/vdb/vehicles/dropdowns
Vehicle Dropdowns
Distinct filter values for the cascading make/model/variant/year pickers.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
search | query | no | ||
make | query | no | ||
model | query | no | ||
sub_type | query | no | ||
year | query | no |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/vdb/contact
Contact Form
Record the in-app "Contact Us" message, then email it to the helpdesk.
Recorded FIRST, deliberately. This used to be a send and nothing else, so a message lived exactly as long as one SMTP call: a failure returned 502 and the customer's words were gone, with nothing to retry from and no way to count what had been lost.
So a failed send is no longer a failed submission. The row is saved either way and the caller is told the message is in — asking someone to retype it because our mailbox is down would only produce duplicates of a row we already hold. delivered=False rows are what to look at when someone asks whether anything is arriving; nothing sweeps them automatically yet.
Request body
json
{
"properties": {
"full_name": {
"type": "string",
"maxLength": 255,
"minLength": 1,
"title": "Full Name"
},
"phone_number": {
"type": "string",
"maxLength": 32,
"minLength": 3,
"title": "Phone Number"
},
"email": {
"type": "string",
"format": "email",
"title": "Email"
},
"message": {
"type": "string",
"maxLength": 5000,
"minLength": 1,
"title": "Message"
}
},
"type": "object",
"required": [
"full_name",
"phone_number",
"email",
"message"
],
"title": "ContactIn"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/vdb/valuation/makes
Valuation Makes
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
authorization | header | no |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/vdb/valuation/guides
Valuation Guides
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
authorization | header | no |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/vdb/valuation/models
Valuation Models
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
mmMake | query | yes | string | |
authorization | header | no |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/vdb/valuation/years
Valuation Years
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
mmCode | query | yes | string | |
nGuide | query | yes | string | |
authorization | header | no |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/vdb/valuation/extras
Valuation Extras
The factory-fitted extras offered on one vehicle-year. Free, like the rest of the reference cascade — only /valuation/values costs a credit.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
mmCode | query | yes | string | |
mmYear | query | yes | string | |
guide | query | yes | string | |
authorization | header | no |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/vdb/valuation/mileage
Valuation Mileage
The kilometres each mileage band stands for on a car of this year.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
mmYear | query | yes | string | |
guide | query | yes | string | |
authorization | header | no |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/vdb/valuation/values
Valuation Values
Run a valuation. Costs one credit, including when the answer is "no value for that vehicle" — see the service's module docstring.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
vin | query | no | ||
reg | query | no | ||
mmCode | query | no | ||
mmYear | query | no | ||
guide | query | no | ||
condition | query | no | ||
mileage | query | no | ||
options | query | no | ||
authorization | header | no |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/vdb/vehicle/accident-report
Accident Report
Previous claims registered against a VIN. Costs R100.
GET rather than POST although upstream is a POST: from this side it is a read — it creates nothing the caller can refer to afterwards, and the app's other paid search (/vdb/valuation/values) is a GET too. The charge is a side effect of the read in both cases, and making one of them a POST purely because the vendor's own wire protocol is a POST would leak their transport into our API.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
vin | query | yes | string | |
authorization | header | no |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/vdb/payments/paystack/checkout
Paystack Checkout
Open a Paystack checkout for a wallet top-up.
The app opens the returned url. It holds no keys, and it does not say whose wallet to credit — the customer comes from the caller's own session, so a tampered request can only ever top up the account that made it.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
authorization | header | no |
Request body
json
{
"properties": {
"amount": {
"type": "number",
"maximum": 1000000,
"exclusiveMinimum": 0,
"title": "Amount"
},
"credits": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
],
"title": "Credits",
"deprecated": true
}
},
"type": "object",
"required": [
"amount"
],
"title": "PaystackCheckoutIn",
"description": "A wallet top-up.\n\n``amount`` is rands, and is the whole request: since 2026-08-20 a top-up\ncredits the wallet with exactly what was paid (\"R1 paid = R1 in wallet\"),\nso there is no second quantity to disagree with it.\n\n``credits`` is accepted and ignored, for handsets running a build from\nbefore the conversion — they send both, and rejecting the field outright\nwould 422 every top-up from an install that has not updated."
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/vdb/payments/paystack/verify
Paystack Verify
Confirm a payment and apply the top-up. Safe to call repeatedly.
This is the PRIMARY settlement path, not a fallback: the app calls it when it comes back to the foreground after checkout. Webhooks have a history of not reaching this estate, and Paystack — unlike PayFast — lets us ask.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
reference | query | yes | string | |
authorization | header | no |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/vdb/payments/paystack/return
Paystack Return
Where Paystack sends the payer's browser afterwards.
Settles here rather than trusting the app to call verify, so closing the browser at the wrong moment doesn't strand a paid-for top-up. Unauthenticated by necessity — it is a browser redirect carrying no session — which is why it credits strictly by the reference we issued and never by anything in the query string beyond it.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
reference | query | no | ||
trxref | query | no |
200 — Successful Response
422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/vdb/payments/payfast/notify
Payfast Notify
PayFast ITN — disabled. Records, never credits. Always 200.
200 — Successful Response
json
{}POST /api/v1/vdb/payments/apple/notify
Apple Notify
Apple IAP credit purchase. Always 200.
Accepts either the caller's own vdb_app bearer token (what the app sends — it is signed in when it buys) or the legacy shared key header. The token is strictly better: it is per-user and revocable, it tells us who to credit without trusting a field in the body, and it means the app no longer ships a shared secret in its binary. The key path stays only for handsets still running the pre-migration build.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
key | header | no | ||
authorization | header | no |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/vdb/credits/pricing
Credits Pricing
What each report costs. Public — it is a price list, and the buy page needs it before anyone has signed in.
The route keeps its /credits/ path although nothing is called a credit any more: it is compiled into every handset already in the field, and renaming it would 404 the price list for those installs rather than merely showing them a stale one.
tiers is still sent, and is deliberately empty. A build from before the rand conversion reads that key and, finding nothing usable, keeps its own compiled-in fallback bundle prices — which is wrong but harmless (its Buy button opens the website, which quotes the real price). Sending the two service prices under tiers instead would be actively worse: that shape is {credits, amount, per_credit}, so an old build would render "30 credits — R30".
200 — Successful Response
json
{}GET /api/v1/vdb/buy-credits
Buy Credits Page
Permanent redirect to the website's credit bundles.
KEPT as a redirect rather than deleted outright, deliberately. The URL is compiled into every VehicleDB build already on a handset (pricing_screen.dart's buyCreditUrl) and has been printed in the app for customers to read off a screen — deleting the route would 404 the only top-up path those installs have, for as long as people run them. A redirect costs nothing and cannot strand anybody.
308 rather than 301/302: the redirect target is a different origin and the old page was reached by GET only, so the method-preserving code is the honest one and browsers cache it the same way.
200 — Successful Response
json
{}GET /api/v1/vehicledb/wallets
Wallets
The credit wallets this login can search against, with live balances.
The search page reads this first: it is what puts the balance in the header, what decides whether an account picker is needed at all (one wallet — the normal case, and the only one the app has — needs none), and what lets the page say "you have no credits" up front instead of after a customer has filled in a VIN.
scoped: false is the superadmin all-organizations view: a real answer ("you are not looking at a customer"), not an empty list of wallets.
200 — Successful Response
json
{}GET /api/v1/vehicledb/valuation/makes
Valuation Makes
200 — Successful Response
json
{}GET /api/v1/vehicledb/valuation/guides
Valuation Guides
200 — Successful Response
json
{}GET /api/v1/vehicledb/valuation/models
Valuation Models
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
mmMake | query | yes | string |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/vehicledb/valuation/years
Valuation Years
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
mmCode | query | yes | string | |
nGuide | query | yes | string |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/vehicledb/valuation/extras
Valuation Extras
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
mmCode | query | yes | string | |
mmYear | query | yes | string | |
guide | query | yes | string |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/vehicledb/valuation/mileage
Valuation Mileage
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
mmYear | query | yes | string | |
guide | query | yes | string |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/vehicledb/valuation/values
Valuation Values
Run a valuation from the portal. Costs one credit, on the same terms as the app — including the "no book value" case, which is charged because TransUnion bills us for the search either way. See app.apps.vehicledb.services.valuation.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
vin | query | no | ||
reg | query | no | ||
mmCode | query | no | ||
mmYear | query | no | ||
guide | query | no | ||
condition | query | no | ||
mileage | query | no | ||
options | query | no | ||
customer_id | query | no |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/vehicledb/vehicle/accident-report
Accident Report
Run an Accident Report from the portal — previous claims against a VIN.
Added 2026-09-10 (owner: "add accident reports"). Until now this search existed on the handset alone: app.apps.vehicledb.services.accident was written as a service precisely so a second front door could be added without the two drifting on price, refund rules or logging, and this is that door. It is four lines for the same reason the valuation one is — everything that decides what a customer is charged lives in the service, and both doors reach it.
Costs R100 and is charged the same way as the app: debited before the upstream call, refunded only when no search happened (transport failure, non-200, or the empty 200 this vendor answers with when it swallows an error). A clean history is NOT refunded — "no claims on record" is the answer the customer bought, and Imagin8 bills us for it either way.
GET, matching the app's own /vdb/vehicle/accident-report: from this side it is a read whose charge is a side effect, exactly like /valuation/values above.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
vin | query | yes | string | |
customer_id | query | no |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/vehicledb/pricing
Pricing
What each paid lookup costs, in rands.
Served rather than hardcoded in the SPA because the two prices have moved once already (the wallet counted "credits" until the 2026-09-08 conversion and the search page still said "1 credit" for a R30 lookup for two days afterwards), and because ACCIDENT_CHARGE has a second copy in the website catalogue that already has to be kept in step by hand. One more hand-copied price in a frontend build is the one that stays wrong longest — a deploy is not needed to notice this one is stale, only a refresh.
200 — Successful Response
json
{}GET /api/v1/vehicledb/vehicles
List Vehicles
Paginated vehicle reference list. Browsing costs no credits.
Default page size is 100, not the app's 500: a handset pulls a big page once and filters it locally offline, while this backs a table that pages on demand over a connection nobody is paying by the megabyte.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
search | query | no | ||
make | query | no | ||
model | query | no | ||
sub_type | query | no | ||
year | query | no | ||
page | query | no | integer | |
limit | query | no | integer |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}GET /api/v1/vehicledb/vehicles/dropdowns
Vehicle Dropdowns
Distinct filter values for the cascading make/model/variant/year pickers.
Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
search | query | no | ||
make | query | no | ||
model | query | no | ||
sub_type | query | no | ||
year | query | no |
200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/vehicledb/auth/request-otp
Portal Request Otp
Send a sign-in OTP, exactly as the app's own /vdb/auth/request-otp does.
Same service, same rate limit, same adopt-a-books-customer path — so a number that works on a handset works here, and one that doesn't gets the same answer rather than a second, subtly different one.
Request body
json
{
"properties": {
"mobile_number": {
"type": "string",
"maxLength": 32,
"minLength": 6,
"title": "Mobile Number"
}
},
"type": "object",
"required": [
"mobile_number"
],
"title": "MobileIn"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}POST /api/v1/vehicledb/auth/verify-otp
Portal Verify Otp
Verify the OTP and return a PORTAL session.
This is the one place the portal's sign-in differs from the app's: the app mints a vdb_app JWT for its own API, while the portal needs the ordinary User-backed session that the SPA, the grids and customer_scope all authenticate with. Everything up to that point — the OTP, the account lookup, the books-customer link, the promo credit — is the shared service.
Request body
json
{
"properties": {
"mobile_number": {
"type": "string",
"maxLength": 32,
"minLength": 6,
"title": "Mobile Number"
},
"code": {
"type": "string",
"maxLength": 10,
"minLength": 4,
"title": "Code"
}
},
"type": "object",
"required": [
"mobile_number",
"code"
],
"title": "VerifyOtpIn"
}200 — Successful Response
json
{}422 — Validation Error
json
{
"properties": {
"detail": {
"items": {
"properties": {
"loc": {
"items": {
"anyOf": [
{
"type": "string"
},
{
"type": "integer"
}
]
},
"type": "array",
"title": "Location"
},
"msg": {
"type": "string",
"title": "Message"
},
"type": {
"type": "string",
"title": "Error Type"
}
},
"type": "object",
"required": [
"loc",
"msg",
"type"
],
"title": "ValidationError"
},
"type": "array",
"title": "Detail"
}
},
"type": "object",
"title": "HTTPValidationError"
}