Developer reference

API Documentation

One multipart request in, structured JSON out: plate number, country, car brand, model, body type and color — each with an honest confidence score. This page covers authentication, every endpoint, response shapes, webhooks, error handling and rate limits.

150 free API calls per month. No credit card required.

Quick start

Three steps from zero to your first plate read. There is no SDK to install — any HTTP client that can send a multipart form works, from curl to a camera firmware script.

1. Get an API key

Request access and we provision a key the same day. The free tier includes 150 API calls per month — no credit card required.

Request API access

2. Send an image

POST a photo as a multipart form field named file, with your key in the Authorization header. The API finds every plate in the frame and reads it in a single pass.

curl -X POST "https://api.example.com/v1/detect" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -F "file=@car_image.jpg"

3. Parse the JSON

Each plate comes back with a bounding box, the decoded text, per-character confidence and country attributes — ready to match against your allow-list or billing system.

{
  "success": true,
  "plates_found": 1,
  "plates": [
    {
      "plate_id": 0,
      "position": { "x1": 120, "y1": 340, "x2": 280, "y2": 390 },
      "confidence": 0.95,
      "text": { "full": "7TRX294" },
      "attributes": { "country": "us" }
    }
  ],
  "cost": 0.02,
  "timestamp": "2026-07-08T09:12:44.412633"
}

Authentication

Every request is authenticated with a bearer token. Pass your API key in the Authorization header — no request signing, no OAuth dance, no session management.

Authorization: Bearer YOUR_API_KEY
Note

Keep your API key server-side. Never embed it in client-side JavaScript, mobile apps or public repositories — anyone holding the key can spend your wallet balance. If a key leaks, rotate it from the dashboard; the old key is invalidated immediately.

Endpoints

The API is a small, predictable surface: two GET endpoints and four POST endpoints. Every image endpoint accepts multipart form data with a file field and returns JSON.

All endpoints are served over HTTPS from a single base URL: https://api.example.com

MethodEndpointDescription
GET/healthService and model status — free, no authentication
POST/v1/detectDetect and read every plate in an image, plus optional car analysis
POST/v1/detect/{country}Detection hinted to one country for faster, more accurate reads
POST/v1/carCar brand, model, body type, color and view angle — no plate reading
POST/v1/bodyVehicle body classification with full class probabilities
POST/v1/readOCR a plate you have already cropped
GET/v1/usageYour request volume, spend and remaining balance
GET

/health

Returns the status of the service and of each model in the inference pipeline. Useful for uptime monitors and readiness probes. This endpoint is free and requires no authentication.

Request

curl https://api.example.com/health

Response

{
  "status": "healthy",
  "models": {
    "detection": true,
    "classification": true,
    "ocr": true
  },
  "storage": true,
  "timestamp": "2026-07-08T09:12:44.412633"
}
POST

/v1/detect

The main endpoint. Upload a photo and get every license plate in the frame — localized, cropped and read in one pass — plus optional vehicle analysis (brand, model, body type, color, view angle) in the same response. Typical end-to-end latency is under 200ms.

Query parameters

camera_idOptional camera identifier stored with the detection and forwarded to your webhooks — useful when many cameras share one API key.
plate_onlySkip vehicle analysis and return plates only. Roughly 2.5x faster — use it whenever you only need the plate text.
main_onlyReturn only the most prominent plate in the frame instead of every detection.

Request

curl -X POST "https://api.example.com/v1/detect?camera_id=cam-north-01" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -F "file=@car_image.jpg" \
  -F 'params={"location":"gate-1","lane":"entry"}'

Response — plate found, with car analysis

{
  "success": true,
  "plates_found": 1,
  "plates": [
    {
      "plate_id": 0,
      "position": { "x1": 120, "y1": 340, "x2": 280, "y2": 390 },
      "confidence": 0.95,
      "text": {
        "full": "7TRX294",
        "characters": [
          { "class": "7", "confidence": 0.98 },
          { "class": "T", "confidence": 0.97 },
          { "class": "R", "confidence": 0.97 },
          { "class": "X", "confidence": 0.96 },
          { "class": "2", "confidence": 0.98 },
          { "class": "9", "confidence": 0.97 },
          { "class": "4", "confidence": 0.98 }
        ]
      },
      "attributes": { "country": "us" }
    }
  ],
  "body": {
    "label": "suv",
    "confidence": 0.87,
    "probs": {
      "sedan": 0.04, "hatchback": 0.01, "wagon": 0.01, "coupe": 0.02,
      "convertible": 0.00, "suv": 0.87, "pickup": 0.04, "minivan": 0.01
    },
    "car_bbox": { "x1": 120, "y1": 240, "x2": 980, "y2": 880 },
    "car_confidence": 0.94,
    "color": { "name": "white", "hex": "#f2f2f2", "rgb": [242, 242, 242] },
    "position": {
      "label": "front",
      "confidence": 0.97,
      "probs": { "back": 0.01, "front": 0.97, "semi_side": 0.02, "side": 0.0 }
    },
    "brand": {
      "label": "Toyota",
      "confidence": 0.84,
      "probs": { "Toyota": 0.84, "Honda": 0.05, "Lexus": 0.04, "Subaru": 0.04, "Mazda": 0.03 }
    },
    "model": {
      "label": "Toyota|RAV4",
      "confidence": 0.74,
      "probs": { "Toyota|RAV4": 0.74, "Toyota|Highlander": 0.09, "Honda|CR-V": 0.07, "Subaru|Forester": 0.05, "Mazda|CX-5": 0.03 }
    },
    "make_model": {
      "brand": "Toyota",
      "brand_confidence": 0.84,
      "brand_source": "stage2",
      "model": "RAV4",
      "model_full": "Toyota|RAV4",
      "model_confidence": 0.74,
      "brand_uncertain": false,
      "decision": "agreement"
    }
  },
  "cost": 0.02,
  "timestamp": "2026-07-08T09:12:44.412633"
}

Response — no plates found

{
  "success": true,
  "plates_found": 0,
  "plates": [],
  "cost": 0.0,
  "timestamp": "2026-07-08T09:12:44.412633"
}
Note

A frame with no readable plate still returns success true with plates_found 0 — check plates_found, not the HTTP status. Requests that find no plates are not billed.

POST

/v1/detect/{country}

If you know which market your cameras operate in, pass the country code in the path. The pipeline applies that market format priors — stacked characters, state and province layouts, character sets — which improves both speed and read accuracy on hard plates.

curl -X POST "https://api.example.com/v1/detect/us?plate_only=true" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -F "file=@car_image.jpg"

Supported country codes

us

United States

ca

Canada

gb

United Kingdom

au

Australia

nz

New Zealand

kz

Kazakhstan

Plate format details for each market are covered in the per-country guides:

POST

/v1/car

Vehicle analysis without plate reading: brand across 181 makes, model across 2,003 variants spanning 145 brands, body type, dominant color and view angle. Every classifier can be toggled off individually so you never pay latency for signals you do not use.

Query toggles — all default to true

body_typeBody type across 8 classes: sedan, hatchback, wagon, coupe, convertible, suv, pickup, minivan
colorDominant vehicle color as a human-readable name plus hex value and RGB triple
positionView angle across 4 classes: front, back, side, semi_side
brandCar make across 181 brands
car_modelCar model across 2,003 models spanning 145 brands

Request

curl -X POST "https://api.example.com/v1/car?brand=true&car_model=true&color=false" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -F "file=@car_image.jpg"

Response

{
  "success": true,
  "body": {
    "label": "coupe",
    "confidence": 0.7519,
    "probs": {
      "sedan": 0.037, "hatchback": 0.0268, "wagon": 0.0068, "coupe": 0.7519,
      "convertible": 0.0668, "suv": 0.059, "pickup": 0.0128, "minivan": 0.039
    },
    "car_bbox": { "x1": 234, "y1": 616, "x2": 1166, "y2": 1071 },
    "car_confidence": 0.956,
    "color": { "name": "silver", "hex": "#b9c2cb", "rgb": [185, 194, 203] },
    "position": {
      "label": "semi_side",
      "confidence": 0.9998,
      "probs": { "back": 0.0, "front": 0.0002, "semi_side": 0.9998, "side": 0.0 }
    },
    "brand": {
      "label": "Porsche",
      "confidence": 0.7751,
      "probs": { "Porsche": 0.7751, "Hyundai": 0.0037, "Volkswagen": 0.0034, "Honda": 0.0033, "Toyota": 0.003 }
    },
    "model": {
      "label": "Porsche|911",
      "confidence": 0.6876,
      "probs": { "Porsche|911": 0.6876, "Porsche|Boxster": 0.0042, "Porsche|Cayman": 0.0031, "Volkswagen|beetle": 0.0023, "Audi|TT": 0.0014 }
    },
    "make_model": {
      "brand": "Porsche",
      "brand_confidence": 0.7751,
      "brand_source": "stage2",
      "model": "911",
      "model_full": "Porsche|911",
      "model_confidence": 0.6876,
      "brand_uncertain": false,
      "decision": "agreement"
    }
  },
  "cost": 0.02,
  "timestamp": "2026-07-08T09:12:44.412633"
}
Note

The make_model object is the field to consume in production. Brand and model come from two independent networks with calibrated confidence gates — when they disagree, the API returns null with brand_uncertain true and a decision field explaining why, instead of guessing. Honest answers, not guesses.

Read the full car recognition guide
POST

/v1/body

A focused endpoint that runs the vehicle pipeline and returns the classification block on its own. The response includes the full probability distribution across all 8 body classes, so you can apply your own thresholds instead of trusting a single top label.

Request

curl -X POST "https://api.example.com/v1/body" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -F "file=@car_image.jpg"

Response

{
  "success": true,
  "body": {
    "label": "pickup",
    "confidence": 0.91,
    "probs": {
      "sedan": 0.01,
      "hatchback": 0.00,
      "wagon": 0.01,
      "coupe": 0.01,
      "convertible": 0.00,
      "suv": 0.05,
      "pickup": 0.91,
      "minivan": 0.01
    },
    "car_bbox": { "x1": 88, "y1": 210, "x2": 1012, "y2": 860 },
    "car_confidence": 0.95
  },
  "cost": 0.02,
  "timestamp": "2026-07-08T09:12:44.412633"
}
Note

The classifier automatically localizes and crops the most prominent vehicle before classifying. If no vehicle is confidently found, car_bbox and car_confidence come back null and the full frame is classified instead.

Note

Read the probabilities honestly: confidence spread across several classes means the photo is genuinely ambiguous — an SUV shot from the front can look like a minivan. Filter on confidence rather than acting on every top label.

POST

/v1/read

If you already run your own detector, send a tightly cropped plate image and get OCR only. This skips detection entirely — the lowest-latency path when your camera or edge device produces plate crops itself.

Request

curl -X POST "https://api.example.com/v1/read" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -F "file=@plate_crop.jpg"

Response

{
  "success": true,
  "text": {
    "full": "7TRX294",
    "characters": [
      { "class": "7", "confidence": 0.98 },
      { "class": "T", "confidence": 0.97 },
      { "class": "R", "confidence": 0.97 },
      { "class": "X", "confidence": 0.96 },
      { "class": "2", "confidence": 0.98 },
      { "class": "9", "confidence": 0.97 },
      { "class": "4", "confidence": 0.98 }
    ]
  },
  "attributes": { "country": "us" },
  "cost": 0.02,
  "timestamp": "2026-07-08T09:12:44.412633"
}
GET

/v1/usage

Query your own consumption programmatically: requests this month, plates detected, spend and a per-day breakdown. Use the days query parameter to set the reporting window. Useful for dashboards and monthly reconciliation.

Request

curl "https://api.example.com/v1/usage?days=30" \
  -H "Authorization: Bearer YOUR_API_KEY"

Response — free tier

{
  "tier": "free",
  "credits_remaining": null,
  "estimated_requests_remaining": null,
  "requests_this_month": 3,
  "plates_detected_this_month": 2,
  "cost_this_month": 0.0,
  "daily_usage": [
    { "date": "2026-07-08", "requests": 3, "plates": 2, "cost": 0.0 }
  ]
}

Response — paid tier

{
  "tier": "growth",
  "credits_remaining": 112.50,
  "estimated_requests_remaining": 7500,
  "requests_this_month": 2500,
  "plates_detected_this_month": 2210,
  "cost_this_month": 37.50,
  "daily_usage": [
    { "date": "2026-07-07", "requests": 320, "plates": 291, "cost": 4.80 },
    { "date": "2026-07-08", "requests": 180, "plates": 164, "cost": 2.70 }
  ]
}

Image requirements

The API accepts standard camera output. Anything outside these bounds is rejected with a 413 or 422 before any model runs, so you are never billed for an unusable frame.

FormatsJPEG, PNG or WebP
Maximum file size10 MB
Minimum resolution640 x 480 pixels
Note

Send the sharpest frame you have. Higher resolution helps as long as the plate itself is legible — avoid heavy compression, motion blur and crops that cut off plate edges. If you control the camera, a snapshot at the moment the vehicle slows near the gate beats a frame grabbed at speed.

Webhooks

Instead of polling, register an HTTPS endpoint from the dashboard and the API will POST to it in real time whenever a detection completes. You can register up to 10 webhooks, subscribe each to specific events, and toggle, test or rotate them independently.

Delivery payload

Each delivery is a JSON POST carrying the event name, a timestamp and the full detection result — the same object the synchronous API returned. Any custom params attached to the original request are forwarded untouched at the top level.

Content-Type: application/json
X-Webhook-Signature: sha256=a1b2c3d4...
X-Webhook-Timestamp: 2026-07-08T11:30:00.123456
X-Webhook-Attempt: 1
{
  "event": "plate_detected",
  "timestamp": "2026-07-08T11:30:00.123456",
  "data": {
    "success": true,
    "plates_found": 1,
    "plates": [
      {
        "plate_id": 0,
        "position": { "x1": 120, "y1": 340, "x2": 280, "y2": 390 },
        "confidence": 0.95,
        "text": { "full": "7TRX294" },
        "attributes": { "country": "us" }
      }
    ],
    "cost": 0.02,
    "timestamp": "2026-07-08T11:30:00"
  },
  "params": {
    "camera_id": "cam-north-01",
    "location": "gate-1",
    "lane": "entry"
  }
}

Verifying signatures

Every delivery is signed with HMAC SHA-256 using your webhook secret — shown once at creation, rotatable at any time. Compute the digest of the raw request body and compare it to the X-Webhook-Signature header before trusting the payload.

import hmac, hashlib

def verify(secret: str, raw_body: bytes, signature_header: str) -> bool:
    expected = "sha256=" + hmac.new(
        secret.encode(), raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature_header)

Retry policy

  • Attempt 1 fires immediately when the event occurs
  • Attempt 2 retries after 5 seconds, attempt 3 after 30 seconds
  • A delivery counts as successful on any 2xx response from your server
  • If all 3 attempts fail, the webhook stays active and fires again on the next event — every attempt is logged in the delivery history

Custom parameters

Attach your own metadata — camera IDs, gate numbers, session IDs — to any detection request as a params form field containing a JSON object. The API never stores it; it rides along verbatim to your webhook so your server knows the context of each event.

cURL

curl -X POST "https://api.example.com/v1/detect" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -F "[email protected]" \
  -F 'params={"location":"gate-1","lane":"entry","site":"warehouse-a"}'

JavaScript

const form = new FormData();
form.append("file", imageFile);
form.append("params", JSON.stringify({ location: "gate-1", lane: "entry" }));

await fetch("https://api.example.com/v1/detect", {
  method: "POST",
  headers: { Authorization: `Bearer ${apiKey}` },
  body: form,
});

Python

import json, requests

resp = requests.post(
    "https://api.example.com/v1/detect",
    headers={"Authorization": f"Bearer {api_key}"},
    files={"file": open("photo.jpg", "rb")},
    data={"params": json.dumps({"location": "gate-1", "lane": "entry"})},
)
  • params must be a valid JSON object — not an array, string or number
  • Values can be anything valid in JSON: strings, numbers, booleans, nested objects, arrays
  • Keep it small — a few kilobytes at most
  • Params are never stored server-side; they are only forwarded to your webhooks
Note

When no params are sent, the params field is omitted from the payload entirely — not set to null — so existing integrations are unaffected.

Connecting an IP camera or NVR? Read the camera integration guide

Error handling

The API uses conventional HTTP status codes. Errors return a JSON body with a single detail field describing exactly what went wrong — no error code lookup tables required.

CodeMeaning
400Bad request — the image could not be decoded or a parameter is malformed
401Unauthorized — missing or invalid API key
402Insufficient balance — your wallet cannot cover this request; top up to continue
413Payload too large — the image exceeds the 10 MB limit
422Unprocessable — unsupported image format or missing file field
429Rate limited — you exceeded your plan per-minute rate or monthly volume
500Server error — rare; safe to retry with exponential backoff

Example error responses

401 Unauthorized

{"detail": "Missing Authorization header"}
{"detail": "Invalid API key"}

402 Insufficient Balance

{"detail": "Insufficient balance. Required: $0.0100, Available: $0.0000. Please top up your wallet."}

413 Payload Too Large

{"detail": "Image too large. Maximum file size is 10MB."}

422 Unprocessable Entity

{"detail": "Unsupported image format. Use JPEG, PNG, or WebP."}

429 Rate Limited

{"detail": "Rate limit exceeded. Maximum 60 requests per minute for your plan."}
{"detail": "Monthly limit exceeded. Maximum 150 requests per month for free tier."}

Rate limits and billing

Billing is pay-as-you-go from a prepaid USD wallet — no subscriptions, no seat fees, no expiring credits. Your per-request price and rate limit follow your monthly volume tier.

PlanRate limitMonthly volumePrice per request
FreeFair-use150 requests / monthFree
Pro60 req / minUp to 10,000$0.02
Growth100 req / min10,000 to 50,000$0.015
Professional200 req / min50,000 to 200,000$0.01
EnterpriseCustom200,000+From $0.007
  • Top-ups are one-time wallet loads: minimum $100 on Pro, $150 on Growth, $500 on Professional. Wallet credit never expires.
  • When the free tier is exhausted, additional recognitions bill at $0.01 each from your wallet.
  • Email alerts fire at 80% and 100% of your balance, and you can set a hard spend limit so a runaway script cannot drain the wallet.
  • A 429 response includes a detail message naming the exact limit you hit — back off and retry after the window resets.
See full pricing

Get your API key today

Start on the free tier — 150 recognitions a month, no credit card. Integrating cameras or evaluating on-premise deployment? Ask us; engineers answer, not sales.