API Documentation
Everything you need to integrate license plate recognition into your application.
Always up to date: the interactive OpenAPI reference is available at api.anpr.software/docs
Quick Start
Get started with the ANPR.software API in under 5 minutes. Follow these steps to make your first API call.
1. Get your API key
Sign up for a free account to get your API key. Your first 100 credits are free.
Sign Up Free2. Make your first request
curl -X POST "https://api.anpr.software/v1/detect" \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "file=@car_image.jpg"3. Get your result
{
"success": true,
"plates_found": 1,
"plates": [
{
"plate_id": 0,
"position": {
"x1": 120, "y1": 340, "x2": 280, "y2": 390
},
"detection_confidence": 0.95,
"text": {
"plate_code_en": "AZ",
"plate_code_ar": "أ م",
"plate_number_en": "53",
"plate_number_ar": "٥٣",
"confidence": 0.9375,
"min_char_confidence": 0.92
},
"attributes": {
"country": "uae",
"emirate": "dubai",
"color": "white",
"type": "private",
"size": "long"
},
"quality": {
"plate_width_px": 160,
"plate_height_px": 50,
"band": "optimal",
"note": "plate 150px or wider - the capture size ANPR cameras are specified for"
}
}
],
"body": { "label": "suv", "make_model": { "brand": "Toyota", "model": "Land Cruiser" } },
"cost": 0.01,
"credits": 2,
"timestamp": "2026-02-14T05:16:17.412633"
}Authentication
All protected API requests require authentication using a Bearer token. Include your API key in the Authorization header.
Authorization: Bearer YOUR_API_KEYKeep your API key secret. Never expose it in client-side code or public repositories.
Endpoints
Base URL: https://api.anpr.software
/health/v1/detect/v1/detect/{country}/v1/car/v1/body/v1/frames/v1/frames/{job_id}/v1/read/v1/usage/v1/plate-logoGET /health - Health Check
Public endpoint - Check API health and model status.
Request
curl https://api.anpr.software/healthResponse
{
"status": "healthy",
"models": {
"detection": true,
"classification": true,
"ocr": true
},
"storage": true,
"timestamp": "2026-02-14T05:16:17.412633"
}POST /v1/detect - Plate Detection
Protected endpoint - Detects all visible plates, classifies each one (country, emirate, color, type, size) and runs OCR in both Arabic and English. The response also includes a body field with the full car profile (body type, color, view angle, brand, model) of the main detected car.
Optional query parameters
camera_id | Camera source identifier. When it matches one of your active cameras (see Parking configuration), an entry/exit movement is logged for each detected plate. |
main_only | When true, skip the full-frame background pass and return only the main detected car's plate - faster, and avoids picking up other vehicles' plates. |
plate_only | When true, skip every car-related model and return only plate data - roughly 2.5x faster. The body field is null. |
plate_attributes | Default true. false runs the plate classifier's first stage only: the country is still predicted — it selects the reading weights — and the per-country attributes (emirate, colour, type, size, text colour) are skipped. One model pass cheaper per country group in the frame. Does not change the price. |
body, color, position, brand, car_model | All default true. Turn off one car classifier at a time. Setting all five to false is exactly plate_only=true, billing included. Turning off position costs you parking direction: it is the view angle a camera's entrance and exit configuration is matched against, so movements logged with camera_id come out as unknown. |
all_cars | When true, classify every car in the frame instead of only the main one. Adds a cars array - one entry per vehicle with its own body type, colour, view angle, brand and model - and sets car_id on every plate. |
Request
curl -X POST "https://api.anpr.software/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 (plates found)
{
"success": true,
"plates_found": 1,
"plates": [
{
"plate_id": 0,
"position": {
"x1": 120,
"y1": 340,
"x2": 280,
"y2": 390
},
"detection_confidence": 0.95,
"text": {
"plate_code_en": "AZ",
"plate_code_ar": "أ م",
"plate_number_en": "53",
"plate_number_ar": "٥٣",
"confidence": 0.9375,
"min_char_confidence": 0.92,
"characters": [
{ "char": "A", "ocr_class": "A", "script": "en", "confidence": 0.96, "bbox": [12, 6, 28, 40] },
{ "char": "Z", "ocr_class": "Z", "script": "en", "confidence": 0.94, "bbox": [30, 6, 46, 40] },
{ "char": "5", "ocr_class": "5", "script": "en", "confidence": 0.92, "bbox": [70, 5, 88, 41] },
{ "char": "3", "ocr_class": "3", "script": "en", "confidence": 0.93, "bbox": [90, 5, 108, 41] }
],
"plate_number_ar_source": "transliterated",
"plate_code_ar_source": "detected"
},
"attributes": {
"country": "uae",
"emirate": "dubai",
"color": "white",
"type": "private",
"size": "long",
"text_color": "black"
},
"quality": {
"plate_width_px": 160,
"plate_height_px": 50,
"band": "optimal",
"note": "plate 150px or wider - the capture size ANPR cameras are specified for"
}
}
],
"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": "back",
"confidence": 0.97,
"probs": { "front": 0.005, "back": 0.97, "side": 0.005, "semiside_front_left": 0.005, "semiside_front_right": 0.005, "semiside_back_left": 0.005, "semiside_back_right": 0.005 },
"probs_coarse": { "front": 0.005, "back": 0.97, "side": 0.005, "semi_side": 0.02 }
},
"brand": {
"label": "Toyota",
"confidence": 0.82,
"probs": { "Toyota": 0.82, "Lexus": 0.06, "Nissan": 0.05, "Honda": 0.04, "Mitsubishi": 0.03 }
},
"model": {
"label": "Toyota|Land Cruiser",
"confidence": 0.71,
"probs": { "Toyota|Land Cruiser": 0.71, "Toyota|Prado": 0.12, "Lexus|LX": 0.08, "Nissan|Patrol": 0.06, "Toyota|Fortuner": 0.03 }
},
"make_model": {
"brand": "Toyota",
"brand_confidence": 0.82,
"brand_source": "stage2",
"model": "Land Cruiser",
"model_full": "Toyota|Land Cruiser",
"model_confidence": 0.71,
"brand_uncertain": false,
"decision": "agreement"
}
},
"cost": 0.01,
"credits": 2,
"timestamp": "2026-02-14T05:16:17.412633"
}Response (no plates)
{
"success": true,
"plates_found": 0,
"plates": [],
"body": {
"label": "sedan",
"confidence": 0.72,
"probs": {
"sedan": 0.72, "hatchback": 0.08, "wagon": 0.03, "coupe": 0.09,
"convertible": 0.01, "suv": 0.05, "pickup": 0.01, "minivan": 0.01
},
"car_bbox": null,
"car_confidence": null,
"color": { "name": "black", "hex": "#1a1a1a", "rgb": [26, 26, 26] },
"position": {
"label": "front",
"confidence": 0.94,
"probs": { "front": 0.94, "back": 0.01, "side": 0.01, "semiside_front_left": 0.01, "semiside_front_right": 0.01, "semiside_back_left": 0.01, "semiside_back_right": 0.01 },
"probs_coarse": { "front": 0.94, "back": 0.01, "side": 0.01, "semi_side": 0.04 }
},
"brand": {
"label": "Nissan",
"confidence": 0.68,
"probs": { "Nissan": 0.68, "Toyota": 0.11, "Honda": 0.09, "Hyundai": 0.07, "Kia": 0.05 }
},
"model": {
"label": "Nissan|Altima",
"confidence": 0.54,
"probs": { "Nissan|Altima": 0.54, "Nissan|Maxima": 0.18, "Toyota|Camry": 0.12, "Honda|Accord": 0.10, "Hyundai|Sonata": 0.06 }
},
"make_model": {
"brand": "Nissan",
"brand_confidence": 0.68,
"brand_source": "stage2",
"model": "Altima",
"model_full": "Nissan|Altima",
"model_confidence": 0.54,
"brand_uncertain": false,
"decision": "agreement"
}
},
"cost": 0.005,
"credits": 1,
"timestamp": "2026-02-14T05:16:17.412633"
}Several cars in one frame
By default a response describes one car: the most prominent one, in the body field. Other plates in the frame are still returned, but nothing says which car they belong to - so if you store the result, attaching body to every plate files one car under several identities. Pass all_cars=true and each car is classified in its own right, in a cars array, with every plate tagged with the car_id of the car it sits on.
Request
curl -X POST "https://api.anpr.software/v1/detect?all_cars=true&main_only=true" \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "file=@parking_camera.jpg"Response
{
"success": true,
"plates_found": 2,
"plates": [
{
"plate_id": 0,
"car_id": 0,
"position": { "x1": 334, "y1": 583, "x2": 401, "y2": 625 },
"detection_confidence": 0.861,
"text": {
"plate_code_en": "GC", "plate_number_en": "74523",
"plate_code_ar": "", "plate_number_ar": "",
"confidence": 0.941, "min_char_confidence": 0.902
},
"attributes": { "country": "uae", "emirate": "dubai", "color": "white", "type": "private" }
},
{
"plate_id": 1,
"car_id": 1,
"position": { "x1": 1599, "y1": 392, "x2": 1651, "y2": 410 },
"detection_confidence": 0.769,
"text": {
"plate_code_en": "", "plate_number_en": "138813",
"confidence": 0.812, "min_char_confidence": 0.633
},
"attributes": { "country": "uae", "emirate": "abudhabi", "color": "white", "type": "private" }
}
],
"cars": [
{
"car_id": 0,
"plate_ids": [0],
"car_bbox": { "x1": 206, "y1": 300, "x2": 588, "y2": 682 },
"car_confidence": 0.934,
"label": "suv",
"confidence": 0.8342,
"color": { "name": "gray", "hex": "#818488", "rgb": [129, 136, 138] },
"position": { "label": "front", "confidence": 0.9831 },
"make_model": {
"brand": "Jeep", "brand_confidence": 0.751,
"model": "Cherokee", "model_full": "Jeep|Cherokee", "model_confidence": 0.6658,
"brand_uncertain": false, "decision": "agreement"
}
},
{
"car_id": 1,
"plate_ids": [1],
"car_bbox": { "x1": 1385, "y1": 229, "x2": 1702, "y2": 466 },
"car_confidence": 0.384,
"label": "suv",
"confidence": 0.8053,
"color": { "name": "dark_gray", "hex": "#56585b", "rgb": [86, 88, 91] },
"position": { "label": "semiside_front_right", "confidence": 1.0 },
"make_model": {
"brand": "Nissan", "brand_confidence": 0.8313,
"model": "Patrol", "model_full": "Nissan|Patrol", "model_confidence": 0.4233,
"brand_uncertain": false, "decision": "agreement"
}
}
],
"body": { "label": "suv", "make_model": { "brand": "Jeep", "model": "Cherokee" } },
"cost": 0.02,
"credits": 4,
"timestamp": "2026-08-04T11:19:24.350986"
}- cars is ordered by detection confidence and body still repeats cars[0], so integrations that only read body are unaffected.
- A plate that sits on no detected car has car_id: null. It is still returned and read - it simply has no vehicle attributes attached, rather than borrowing another car's.
- A car whose plate could not be read still appears in cars, with an empty plate_ids.
- With camera_id, each car is logged as its own parking movement carrying its own attributes and direction. Combining with main_only=true returns only plates that sit on a detected car - the usual choice for a fixed camera.
- Costs one pass of the car classifiers per detected car. A plate read from its own car's crop is often sharper than the same plate read off the full frame.
- Car detection sweeps the frame in overlapping tiles rather than looking at it once, so vehicles parked far from the camera are found instead of ignored - on a real 6-car podium frame, 4 cars identified instead of 2. Detection input size also scales with your image, so a wide 2560px camera frame keeps small plates legible to the detector.
POST /v1/detect/ - Known-Country Detection
Identical to /v1/detect, but the plate country is fixed to the path value, so country classification is bypassed entirely - faster inference. Use this when your camera or site only ever sees one country's plates. Supported values: uae, saudi_arabia, kuwait, qatar, bahrain, oman. Combine with plate_only=true for the fastest possible response.
curl -X POST "https://api.anpr.software/v1/detect/uae?plate_only=true" \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "file=@car_image.jpg"POST /v1/frames - Streaming Cameras
/v1/detect runs the models before it answers, which is what you want when someone is waiting for the result. A camera streaming continuously is a different problem: inference takes about a second, so a site pushing tens of frames a second queues up behind it and times out. /v1/frames accepts the frame and returns straight away - measured at 49 requests a second with a 14ms median - and the detection runs on a worker behind it. Results are written to your parking data and pushed to your plate_detected webhooks; you can also poll for them.
Request
curl -X POST "https://api.anpr.software/v1/frames?camera_id=cam-north-01" \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "[email protected]"Response
{
"success": true,
"accepted": true,
"job_id": "e345ba0fd88f44099279d378f689667a",
"camera_id": "cam-north-01",
"status": "queued",
"superseded_job_id": "008c3608a2d240b19fd4341504677192",
"pending_cameras": 2,
"timestamp": "2026-08-04T18:37:24.101Z"
}Only the newest frame per camera is processed
This is the part to understand before building against it. The queue holds one pending frame per camera. If a newer frame from the same camera arrives before a worker picks up the previous one, the older job ends as superseded and is never processed - and never billed. The response tells you when that has just happened, via superseded_job_id.
- The backlog is bounded by how many cameras you have, not by how fast you send. 40 frames a second or 400 - the queue depth is the same.
- Workers always see the current state of a camera. A strict first-in-first-out queue would fall further behind every second, and you would be reading plates from a scene that had already changed.
- The frames it discards are the redundant ones - the same parked cars, which merge into the movement already recorded for them. In a 30-frame burst across two cameras, 3 frames were processed and 27 superseded.
Query parameters
camera_id | Required. Must be one of your active cameras - unknown cameras are rejected with 404 rather than queued. |
main_only | Default true. Only plates on the detected car, which is usually what a fixed camera wants. |
all_cars | Identify every car in the frame separately, as on /v1/detect. |
plate_only | Skip the car models entirely and return plate data only. |
plate_attributes | As on /v1/detect. Carried in the job, so the worker honours it. |
country | Force the plate country, as on /v1/detect/. |
Checking a frame
GET /v1/frames/ returns the job state, and the full detection result once it is done. Job records are kept for one hour. GET /v1/frames returns the current queue depth.
curl "https://api.anpr.software/v1/frames/e345ba0fd88f44099279d378f689667a" \
-H "Authorization: Bearer YOUR_API_KEY"queued | Waiting for a worker. |
processing | A worker has it. |
done | Processed - result holds the same body /v1/detect returns. |
superseded | A newer frame from this camera replaced it. Not processed, not billed. |
failed | Something went wrong; error explains. |
Billing and limits
You are billed in credits for frames that are actually processed - one credit per plate processed and one per vehicle analyzed, exactly as on /v1/detect. Superseded frames cost nothing - in the burst above, with plate_only=true and one plate per frame, the three processed frames come to 3 credits ($0.015) rather than the 30 credits ($0.15) that processing all 30 would have cost. Your monthly quota still applies at submission. The per-minute rate limit does not: a streaming camera is expected to exceed it, and the one-frame-per-camera rule is what bounds the real work. Maximum frame size is 2 MB.
POST /v1/car - Car Recognition
Protected endpoint - Detects the main car in the image and returns its full profile - body type, dominant color, view angle, brand and model - with no plate detection or OCR. Each component can be toggled off with a query parameter to skip its model and speed up the response.
Component toggles (query parameters, all default to true)
body_type | Body-type classification (8 classes) |
color | Dominant color analysis |
position | View-angle classification |
brand | Brand classification (181 brands) |
car_model | Model classification (2,003 classes) |
Request
curl -X POST "https://api.anpr.software/v1/car" \
-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": "semiside_front_left",
"confidence": 0.9998,
"probs": { "front": 0.0, "back": 0.0, "side": 0.0, "semiside_front_left": 1.0, "semiside_front_right": 0.0, "semiside_back_left": 0.0, "semiside_back_right": 0.0 },
"probs_coarse": { "front": 0.0, "back": 0.0, "side": 0.0, "semi_side": 1.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, "Li|L6": 0.0031, "Volkswagen|beetle": 0.0023, "Porsche|Cayman": 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.005,
"credits": 1,
"timestamp": "2026-07-04T12:00:00.000000"
}The make_model object is the fused brand+model verdict and the recommended field to build on. On an unresolvable disagreement between the two classifiers the API withholds the answer (null with brand_uncertain: true) instead of guessing; the decision field tells you which fusion rule produced the verdict.
POST /v1/body - Vehicle Body Classification
Protected endpoint - Classifies the car and returns the same response shape as /v1/car with every component enabled.
Request
curl -X POST "https://api.anpr.software/v1/body" \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "file=@car_image.jpg"Response
{
"success": true,
"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": "back",
"confidence": 0.97,
"probs": { "front": 0.005, "back": 0.97, "side": 0.005, "semiside_front_left": 0.005, "semiside_front_right": 0.005, "semiside_back_left": 0.005, "semiside_back_right": 0.005 },
"probs_coarse": { "front": 0.005, "back": 0.97, "side": 0.005, "semi_side": 0.02 }
},
"brand": {
"label": "Toyota",
"confidence": 0.82,
"probs": { "Toyota": 0.82, "Lexus": 0.06, "Nissan": 0.05, "Honda": 0.04, "Mitsubishi": 0.03 }
},
"model": {
"label": "Toyota|Land Cruiser",
"confidence": 0.71,
"probs": { "Toyota|Land Cruiser": 0.71, "Toyota|Prado": 0.12, "Lexus|LX": 0.08, "Nissan|Patrol": 0.06, "Toyota|Fortuner": 0.03 }
},
"make_model": {
"brand": "Toyota",
"brand_confidence": 0.82,
"brand_source": "stage2",
"model": "Land Cruiser",
"model_full": "Toyota|Land Cruiser",
"model_confidence": 0.71,
"brand_uncertain": false,
"decision": "agreement"
}
},
"cost": 0.005,
"credits": 1,
"timestamp": "2026-02-14T05:16:17.412633"
}Legacy endpoint: new integrations should prefer /v1/car, which adds per-component query toggles for faster responses.
Possible labels: sedan, hatchback, wagon, coupe, convertible, suv, pickup, minivan. The label "unknown" is only returned if the classifier model failed to load - surface it as "unavailable".
car_bbox is null when the car detector could not find a high-confidence vehicle and the classifier ran on the full frame as a fallback. Do not draw a crop overlay when this is null.
confidence is the top-1 softmax score (0–1) and is not calibrated - treat the label as the answer, not the magnitude. The probs object contains the full distribution across all 8 classes.
POST /v1/read - Read a Cropped Plate
Protected endpoint - Upload a pre-cropped license-plate image to classify and OCR it directly. Plate detection is skipped, making this faster when you already have an isolated plate crop.
Request
curl -X POST "https://api.anpr.software/v1/read" \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "file=@plate_crop.jpg"Response
{
"success": true,
"text": {
"plate_code_en": "AZ",
"plate_code_ar": "أ م",
"plate_number_en": "53",
"plate_number_ar": "٥٣",
"characters": [
{ "char": "A", "ocr_class": "A", "script": "en", "confidence": 0.96, "bbox": [12, 6, 28, 40] },
{ "char": "5", "ocr_class": "5", "script": "en", "confidence": 0.92, "bbox": [70, 5, 88, 41] }
],
"plate_number_ar_source": "transliterated",
"plate_code_ar_source": "detected"
},
"confidence": 0.94,
"attributes": {
"country": "uae",
"emirate": "dubai",
"color": "white",
"type": "private",
"size": "long",
"text_color": "black"
},
"quality": {
"plate_width_px": 220,
"plate_height_px": 70,
"band": "optimal",
"note": "plate 150px or wider - the capture size ANPR cameras are specified for"
},
"cost": 0.005,
"credits": 1,
"timestamp": "2026-02-14T05:16:17.412633"
}GET /v1/usage - Usage Statistics
Protected endpoint - Get usage statistics and credits remaining.
Request
curl "https://api.anpr.software/v1/usage?days=30" \
-H "Authorization: Bearer YOUR_API_KEY"Response (Free)
{
"tier": "free",
"plan": "free",
"credits": {
"balance": 94,
"held": 0,
"unit": "credits",
"unit_price": 0.005,
"never_expires": true
},
"limits": {
"monthly_limit": 1000,
"requests_remaining": 997,
"rate_limit_per_minute": 20
},
"usage_this_month": {
"requests": 3,
"plates_detected": 2,
"cost": 0.03
},
"period_days": 30,
"daily_usage": [
{
"date": "2026-02-14",
"requests": 3,
"plates_detected": 2,
"cost": 0.03
}
]
}Response (Business)
{
"tier": "business",
"plan": "business",
"credits": {
"balance": 8432,
"held": 0,
"unit": "credits",
"unit_price": 0.005,
"never_expires": true
},
"limits": {
"monthly_limit": 60000,
"requests_remaining": 58750,
"rate_limit_per_minute": 100
},
"usage_this_month": {
"requests": 1250,
"plates_detected": 980,
"cost": 11.15
},
"period_days": 30,
"daily_usage": [
{
"date": "2026-02-13",
"requests": 320,
"plates_detected": 245,
"cost": 2.825
},
{
"date": "2026-02-14",
"requests": 180,
"plates_detected": 142,
"cost": 1.61
}
]
}POST /v1/plate-logo - Plate Logo Replacement
Replaces the detected license plate in a car photo with your uploaded logo and returns the edited image.
Try the free Plate Logo toolForm fields (multipart/form-data)
file | Required. The car photo (full vehicle), JPEG or PNG. |
logo | Required. The logo image. A transparent PNG gives the cleanest result. |
Query parameter
opacity | Controls how much of the original plate is covered, from 0 to 1 (default 0.95). Use 1 for the strongest coverage. Lower values let the original plate show through. |
Request
curl -X POST "https://api.anpr.software/v1/plate-logo?opacity=0.95" \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "[email protected]" \
-F "[email protected]" \
--output result.jpgSuccess response (200 OK)
The body is the edited image as binary image/jpeg - not JSON. Detection metadata is returned in response headers:
Content-Type: image/jpeg
X-Plate-Bbox: 302,802,899,970
X-Plate-Skew-Deg: 5.27
X-Plate-Quad-Method: bbox+ocr-angle
X-Plate-Detection-Confidence: 0.8
X-Response-Time-Ms: 471Note: to read these X-* headers from browser JavaScript, the API must be configured to expose them via CORS. The image body itself is always readable cross-origin.
Error Handling
When an error occurs, the API returns an appropriate HTTP status code and error message.
| Code | Description |
|---|---|
| 400 | Bad Request - Invalid image or parameters |
| 401 | Unauthorized - Missing or invalid API key |
| 402 | Payment Required - Not enough credits in your wallet |
| 429 | Rate Limited - Too many requests |
| 500 | Server Error - Try again later |
Error Response Examples
401 Unauthorized
{"detail": "Missing Authorization header"}
{"detail": "Invalid API key"}429 Rate Limited
{"detail": "Rate limit exceeded. Maximum 20 requests per minute for free tier."}
{"detail": "Monthly limit exceeded. Maximum 1000 requests per month for free tier."}402 Out of Credits
{"detail": "Out of credits. Required: 2, remaining: 0. Top up at https://anpr.software/dashboard/billing — $0.005 per credit, credits never expire — or turn on auto top-up so this does not happen again."}400 Bad Request
{"detail": "Invalid image"}Custom Parameters (params)
You can attach custom metadata to any detection request. It is stored alongside the detection result and forwarded as-is to your webhook endpoint. This is useful for passing through your own identifiers like locations, lane IDs, gate numbers, or any context your system needs to process the webhook on the receiving end.
How to send params
When calling /v1/detect or /v1/read, include a params form field in the multipart POST body containing a JSON-encoded object, alongside the file upload.
cURL
curl -X POST "https://api.anpr.software/v1/detect" \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "[email protected]" \
-F 'params={"location":"gate-1","lane":"entry","site":"warehouse-a"}'JavaScript (fetch)
const form = new FormData();
form.append("file", imageFile);
form.append("params", JSON.stringify({ location: "gate-1", lane: "entry" }));
await fetch("/v1/detect", {
method: "POST",
headers: { Authorization: `Bearer ${token}` },
body: form,
});Python (requests)
import json, requests
resp = requests.post(
"https://api.anpr.software/v1/detect",
headers={"Authorization": f"Bearer {token}"},
files={"file": open("photo.jpg", "rb")},
data={"params": json.dumps({"location": "gate-1", "lane": "entry"})},
)What your webhook receives
When params are provided, the webhook payload includes them at the top level:
{
"event": "plate_detected",
"timestamp": "2026-02-01T11:30:00.123456",
"data": {
"success": true,
"plates_found": 1,
"plates": [ ... ],
"cost": 0.005,
"credits": 1,
"timestamp": "2026-02-01T11:30:00"
},
"params": {
"location": "gate-1",
"lane": "entry"
}
}When no params are sent, the params field is simply absent from the payload (not null). Existing integrations are not affected.
Rules
- params must be a valid JSON object (not an array, string, or number)
- Values can be any valid JSON type (string, number, boolean, nested object, array, null)
- No strict size limit, but keep it reasonable (a few KB max)
- Params are stored with the detection result and forwarded to your webhooks
Common use cases
camera_id— identify where the image was captured (note: camera_id is also a dedicated query parameter that logs parking movements)building_id/location_id— identify the physical locationgate_id— identify entrance/exit gate in parking systemssession_id— correlate with your own session tracking- Any custom key-value pairs your system needs
What a call costs
Two meters at one price, $0.005 each: one credit per plate processed, and one credit per vehicle analyzed — charged once per vehicle however many of the five car classifiers ran.
An image that yields neither still costs one credit: the models ran. Nothing is charged for a request that is rejected before inference (402, 403, 429).
| Call | Credits |
|---|---|
POST /v1/detect | 2 for one car with one plate — one for the plate, one for the vehicle. More plates or more described cars add one each. |
POST /v1/detect?plate_only=true | 1 for one plate. The car classifiers do not run, so there is no vehicle to charge for. |
POST /v1/detect?all_cars=true | One per plate plus one per DESCRIBED vehicle. A counted-but-too-small detection (described: false) costs nothing. |
POST /v1/read | 1 — one plate, no vehicle. A cropped plate has no car in it. |
POST /v1/body | 1 — one vehicle, no plate. |
POST /v1/car | 1 — one vehicle, no plate. Still 1 however many of the five classifiers you leave on: the unit is the vehicle, not the model. |
POST /v1/frames | Ingest is free. The worker bills each frame it processes on the meters above; a superseded frame is never processed and never billed. |
Every response carries credits and cost. cost is 0 when your free monthly allowance paid for the call.
Rate Limits
Rate limits are enforced per API key and depend on your plan. When a limit is exceeded the API returns 429 with a Retry-After header. Every plan spends credits at the same price: one credit ($0.005) per plate processed and one per vehicle analyzed.
| Plan | Rate Limit | Included Credits |
|---|---|---|
| Free | 20 requests/min | 100 credits every month |
| Starter | 60 requests/min | 3,000 credits/month |
| Business | 100 requests/min | 10,000 credits/month |
| Growth | 200 requests/min | 30,000 credits/month |
| Scale | 400 requests/min | 60,000 credits/month |