Motorstory Marketplace API — Plate Lookup
Status: Pilot / live. Deployed 2026-06-14. Verified-history coverage for a license plate — designed as badge-not-dump: partners learn how trustworthy a car's service history is, never the raw history.
A partner portal (e.g. a used-car marketplace) queries a license plate and gets back a tier describing what verified maintenance data Motorstory holds for that car. There is no endpoint that returns the underlying service events or any owner PII — the only place to read a car's history is the owner-published public report at https://motorstory.app/r/{token}, which a badge response links to.
Base URL
https://qxrfzhwnvmzexcrmthea.supabase.co/functions/v1/plate-lookupA friendly https://api.motorstory.app/plate-lookup custom domain is a planned follow-up; until then use the URL above.Authentication
Every request needs two headers:
| Header | Value | Purpose |
|---|---|---|
X-API-Key | mk_live_… | Your Motorstory partner key. This is the real auth. Keep it secret. |
apikey | the project anon (publishable) key | Gateway routing only (hosted-Supabase requirement). This value is public and safe to embed. |
- Keys are issued manually by Motorstory during the pilot. A key looks like
mk_live_followed by 48 hex chars. - Only a SHA-256 hash of your key is stored on our side — if you lose it we reissue, we can't recover it.
- A missing, unknown, inactive, or malformed
X-API-Keyreturns401 unauthorized(generic, by design — we never confirm whether a key exists).
Request
Two equivalent forms:
GET
GET /plate-lookup?plate=AB123CD&country=ARPOST
POST /plate-lookup
Content-Type: application/json
{ "plate": "AB123CD", "country": "AR" }Parameters
| Name | Required | Default | Notes |
|---|---|---|---|
plate | yes | — | License plate. Whitespace/dashes are stripped and it's upper-cased server-side, so ab 123 cd == AB123CD. |
country | no | AR | ISO country code. Only AR (Argentina) is in service during the pilot. |
An empty/missing plate returns 400 missing_plate.
Response tiers
Every successful response is 200 OK with a JSON body containing a tier field and a charged field (USD billed for this call — see Billing).
no_data — plate unknown or no history (free)
The plate isn't in Motorstory, or it is but has no recorded service events.
{
"tier": "no_data",
"reason": "not_found", // or "no_history"
"country": "AR",
"plate": "ZZ999ZZ",
"charged": 0
}summary — has history, not publicly published
KPIs only — no events, no owner PII. This is Motorstory's exclusive, unscrapeable signal: a partner learns the car has a verified history and how deep it is, without seeing the history itself.
{
"tier": "summary",
"country": "AR",
"plate": "AB123CD",
"vehicle": { "brand": "Volkswagen", "model": "Gol Trend", "year": 2019 },
"coverage": { "total": 9, "validated": 7, "documented": 2, "declared": 0 },
"current_km": 67500,
"last_service_date": "2025-01-15",
"charged": 0.1
}badge — the owner has published a public report
Verification badge + health score + a link to the already-public report. Still not a data dump — the linked report is where detail lives.
{
"tier": "badge",
"verified": true,
"country": "AR",
"plate": "LDS988",
"vehicle": { "brand": "Ford", "model": "Fiesta", "year": 2012 },
"coverage": { "total": 2, "validated": 1, "documented": 0, "declared": 1 },
"health_score": 70,
"report_url": "https://motorstory.app/r/1cd08df90bd8",
"charged": 0.1
}Field reference
| Field | Tiers | Meaning |
|---|---|---|
tier | all | no_data | summary | badge |
reason | no_data | not_found (plate unknown) or no_history (known, no events) |
plate | all | Normalized (upper-cased, separators stripped) |
country | all | Echoed back |
vehicle | summary, badge | { brand, model, year } — basic identity, never VIN/owner |
coverage | summary, badge | Event counts by verification level. total = all events; validated = recorded by a registered workshop; documented = backed by a photo/receipt; declared = owner-stated only |
current_km | summary | Latest known odometer reading |
last_service_date | summary | Date of the most recent service event |
verified | badge | Always true on this tier |
health_score | badge | 0–100, weighted (validated=100, documented=70, declared=40), averaged over events. Matches the public report exactly |
report_url | badge | Public report — the place to read full history |
charged | all | USD billed for this call |
Billing
Pilot pricing (flat):
| Tier | Charge |
|---|---|
no_data | free |
summary | $0.10 |
badge | $0.10 |
48-hour dedup: a charged lookup of the same plate with the same key within 48h is free (charged: 0) on the repeat hit. no_data lookups are free and never count toward dedup. Every call — including free ones — is metered for rate-limiting and your monthly usage report.
Rate limiting
Per-key, 60 requests per rolling 60-second window by default (your key's limit may differ). Exceeding it returns:
HTTP 429 Too Many Requests
Retry-After: 60{ "error": "rate_limited" }Errors
| HTTP | Body | Meaning |
|---|---|---|
400 | { "error": "missing_plate" } | No plate supplied |
401 | { "error": "unauthorized" } | Missing / unknown / inactive X-API-Key |
429 | { "error": "rate_limited" } | Over your per-minute limit (Retry-After header set) |
500 | { "error": "lookup_failed" } | Lookup RPC failed |
500 | { "error": "internal_error" } | Unexpected server error |
Examples
Replace mk_live_YOUR_KEY with your partner key and <ANON_KEY> with the project anon (publishable) key Motorstory gives you.
BASE="https://qxrfzhwnvmzexcrmthea.supabase.co/functions/v1/plate-lookup"
# GET
curl -s "$BASE?plate=AB123CD&country=AR" \
-H "apikey: <ANON_KEY>" \
-H "x-api-key: mk_live_YOUR_KEY"
# POST
curl -s "$BASE" \
-H "apikey: <ANON_KEY>" \
-H "x-api-key: mk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "plate": "AB123CD", "country": "AR" }'const res = await fetch(
"https://qxrfzhwnvmzexcrmthea.supabase.co/functions/v1/plate-lookup?plate=AB123CD&country=AR",
{ headers: { apikey: ANON_KEY, "x-api-key": MK_LIVE_KEY } },
);
const data = await res.json();
if (data.tier === "badge") {
// show "Verified history ✓ — health {data.health_score}/100", link data.report_url
} else if (data.tier === "summary") {
// show "{data.coverage.total} verified services on record"
} else {
// no_data → show nothing / "no history on record"
}Notes & guarantees
- No data dump, ever. There is no parameter or tier that returns individual service events, photos, VIN, or owner contact details. The richest readable artifact is the owner-published report linked from a
badgeresponse. summaryis the deliberately asymmetric tier: it proves depth of history without exposing it, so a marketplace can surface "this car has a verified history" as a trust signal that can't be scraped into a competing dataset.health_scoreonbadgeis recomputed live from the same weighting as the public report, so the API and the report can never disagree.
Implementation: edge function `supabase/functions/plate-lookup/` + SECURITY DEFINER RPC `api_plate_lookup` (migration `20260614000001`). Design rationale lives in `planning/features.md` (Marketplace API key decisions) and `planning/business-plan.md` (Business Model C — badge-not-dump).