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

Looking for license plate recognition outside the GCC?

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 Free

2. 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_KEY

Keep your API key secret. Never expose it in client-side code or public repositories.

Endpoints

Base URL: https://api.anpr.software

GET/health
POST/v1/detect
POST/v1/detect/{country}
POST/v1/car
POST/v1/body
POST/v1/frames
GET/v1/frames/{job_id}
POST/v1/read
GET/v1/usage
POST/v1/plate-logo

GET /health - Health Check

Public endpoint - Check API health and model status.

Request

curl https://api.anpr.software/health

Response

{
  "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_idCamera source identifier. When it matches one of your active cameras (see Parking configuration), an entry/exit movement is logged for each detected plate.
main_onlyWhen 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_onlyWhen true, skip every car-related model and return only plate data - roughly 2.5x faster. The body field is null.
plate_attributesDefault 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_modelAll 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_carsWhen 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_idRequired. Must be one of your active cameras - unknown cameras are rejected with 404 rather than queued.
main_onlyDefault true. Only plates on the detected car, which is usually what a fixed camera wants.
all_carsIdentify every car in the frame separately, as on /v1/detect.
plate_onlySkip the car models entirely and return plate data only.
plate_attributesAs on /v1/detect. Carried in the job, so the worker honours it.
countryForce 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"
queuedWaiting for a worker.
processingA worker has it.
doneProcessed - result holds the same body /v1/detect returns.
supersededA newer frame from this camera replaced it. Not processed, not billed.
failedSomething 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_typeBody-type classification (8 classes)
colorDominant color analysis
positionView-angle classification
brandBrand classification (181 brands)
car_modelModel 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.

Learn more about car recognition coverage and use cases

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
    }
  ]
}

Error Handling

When an error occurs, the API returns an appropriate HTTP status code and error message.

CodeDescription
400Bad Request - Invalid image or parameters
401Unauthorized - Missing or invalid API key
402Payment Required - Not enough credits in your wallet
429Rate Limited - Too many requests
500Server 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 location
  • gate_id — identify entrance/exit gate in parking systems
  • session_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).

CallCredits
POST /v1/detect2 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=true1 for one plate. The car classifiers do not run, so there is no vehicle to charge for.
POST /v1/detect?all_cars=trueOne per plate plus one per DESCRIBED vehicle. A counted-but-too-small detection (described: false) costs nothing.
POST /v1/read1 — one plate, no vehicle. A cropped plate has no car in it.
POST /v1/body1 — one vehicle, no plate.
POST /v1/car1 — 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/framesIngest 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.

PlanRate LimitIncluded Credits
Free20 requests/min100 credits every month
Starter60 requests/min3,000 credits/month
Business100 requests/min10,000 credits/month
Growth200 requests/min30,000 credits/month
Scale400 requests/min60,000 credits/month