PlateDetect Backend API Setup
Connect PlateDetect to your own HTTPS API for detections, media uploads, and hot-list import.
Overview
PlateDetect is local-first. Scanning, OCR, history, watchlists, vehicle color/type estimates, plate classification, and exports work on the iPhone. Bring Your Own Backend sync is optional and only runs after the user enters an HTTPS endpoint and API token in the app.
Use this page to build the API that receives saved detections from PlateDetect. QnSub does not receive scan data unless the user intentionally points the app at a QnSub-controlled endpoint.
What you need
- An HTTPS base URL, for example
https://api.example.com/platedetect. - A private API token sent as
Authorization: Bearer <token>. GET /healthandPOST /detections.- Optional
GET /watchlistfor hot-list import. - Optional
POST /detections/{id}/mediafor snapshots and plate crops.
Connect the app
- Open PlateDetect.
- Go to Settings, Integrations, then Bring Your Own Backend.
- Enter the Server URL. Use the base URL only; do not include
/detections. - Enter the API token. PlateDetect stores it in the iOS Keychain.
- Tap Test connection. PlateDetect calls
GET /health. - Turn on Send media if you want image upload.
- Turn on Enable sync and accept the disclosure.
Request rules
- Every request includes
Authorization: Bearer <token>. - Detection and media uploads include
Idempotency-Key: <detection id>. POST /detectionsusesContent-Type: application/json.- Any
2xxresponse is treated as success. 401or403pauses sync and shows an auth error.- Network errors and other non-2xx responses remain queued for retry.
Endpoints
| Method | Path | Required | Purpose |
|---|---|---|---|
GET | /health | Yes | Used by Test connection. Return any 2xx. |
POST | /detections | Yes | Receives one saved detection as JSON. |
POST | /detections/{id}/media | No | Receives JPEG snapshot and/or crop files. |
GET | /watchlist | No | Returns hot-list plates for local matching. |
Detection payload
PlateDetect sends one JSON object per saved detection. Nullable fields may be omitted or sent as null, depending on what was captured.
{
"id": "F2C9A7E0-1234-49AB-9C3D-7A1B2C3D4E5F",
"plate_text": "ABC 1234",
"plate_normalized": "ABC1234",
"det_confidence": 0.97,
"ocr_confidence": 0.91,
"format_valid": true,
"region_guess": "AAA ####",
"captured_at": "2026-09-01T04:30:00Z",
"source": "live",
"session_id": "D1E71597-32A0-4954-955E-72E16E87C47A",
"review_state": "confirmed",
"duplicate_count": 1,
"best_frame_score": 0.88,
"save_mode": "cropOnly",
"tags": ["visitor"],
"note": "front gate",
"location": {
"lat": 37.7749,
"lon": -122.4194,
"region": "San Francisco, CA"
},
"watchlist_hit": false,
"access_rule_state": "restricted",
"access_rule_name": "Monday morning restriction",
"access_rule_summary": "Restrict endings 1, 2 on Monday, 7:00 AM-10:00 AM",
"plate_country": "US",
"plate_jurisdiction": "California candidate",
"plate_category": "Passenger",
"plate_classification_confidence": 0.66,
"plate_classification_note": "Confirm state from the image when needed.",
"vehicle_color": "White",
"vehicle_color_confidence": 0.74,
"vehicle_type": "SUV",
"vehicle_type_confidence": 0.62,
"media_hash": "sha256:...",
"app_version": "1.0.1",
"model_version": "plate-yolov8n-2026.05"
}
Make id unique in your database. If the same id arrives again, return 200 or 202 without creating a duplicate.
Media upload
When Send media is enabled, PlateDetect sends POST /detections/{id}/media after the detection upload succeeds. The request is multipart/form-data and may include:
snapshot: JPEG full saved image or blurred image, depending on privacy mode.crop: JPEG plate crop.
Return any 2xx. If media upload fails, PlateDetect keeps the detection pending and retries.
Watchlist / hot-list import
PlateDetect can fetch hot-list plates from your backend. Matching still happens on device.
["ABC1234", "NKT2345"]
Object arrays are also accepted:
[
{ "plate": "ABC1234", "label": "Staff", "note": "front gate", "enabled": true },
{ "plate_normalized": "NKT2345", "label": "Visitor" }
]
Wrapped responses can use items or watchlist.
Minimal Express server
import express from "express";
const app = express();
app.use(express.json({ limit: "2mb" }));
const TOKEN = process.env.PLATEDETECT_TOKEN || "change-me";
const detections = new Map();
function requireAuth(req, res, next) {
if (req.get("authorization") !== `Bearer ${TOKEN}`) {
return res.status(401).json({ error: "unauthorized" });
}
next();
}
app.get("/health", requireAuth, (_req, res) => {
res.json({ ok: true });
});
app.post("/detections", requireAuth, (req, res) => {
const detection = req.body;
if (!detection?.id || !detection?.plate_normalized) {
return res.status(400).json({ error: "missing id or plate_normalized" });
}
if (!detections.has(detection.id)) {
detections.set(detection.id, detection);
console.log("PlateDetect:", detection.plate_normalized, detection.captured_at);
}
res.status(202).json({ accepted: true, id: detection.id });
});
app.get("/watchlist", requireAuth, (_req, res) => {
res.json({
items: [
{ plate: "ABC1234", label: "Staff", enabled: true },
{ plate: "NKT2345", label: "Visitor", enabled: true }
]
});
});
app.listen(8080, () => {
console.log("PlateDetect API listening on port 8080");
});
Put the server behind HTTPS with Caddy, Nginx, a load balancer, or a secure tunnel. PlateDetect requires a valid certificate.
AI coding agent prompt
Copy this single prompt into Codex, Cursor, Claude Code, or another AI coding agent to generate a compatible backend.
cURL checks
curl -H "Authorization: Bearer change-me" \
https://api.example.com/platedetect/health
curl -X POST https://api.example.com/platedetect/detections \
-H "Authorization: Bearer change-me" \
-H "Idempotency-Key: test-1" \
-H "Content-Type: application/json" \
-d '{"id":"test-1","plate_text":"ABC 1234","plate_normalized":"ABC1234","det_confidence":0.97,"ocr_confidence":0.91,"format_valid":true,"captured_at":"2026-09-01T04:30:00Z","source":"test","review_state":"confirmed","duplicate_count":1,"save_mode":"cropOnly","tags":[],"watchlist_hit":false,"app_version":"1.0.1"}'
Troubleshooting
- Test connection fails: check the URL, HTTPS certificate,
/health, and token. - Auth failed: the server returned
401or403; save the same token in the app and server. - Pending count grows: check server logs and firewall rules for non-2xx responses.
- Duplicate rows: use
idorIdempotency-Keyfor idempotency. - No images arrive: turn on Send media and implement multipart upload.