Encoding a city parking ordinance as a VerifyAI policy
Cities write parking rules in prose. VerifyAI runs on structure. This
guide walks an operator (or a city working with an operator) through
turning a real municipal micromobility ordinance into a structured
scooter_parking policy — the kind VerifyAI evaluates every end-of-ride
photo against at $0.008 per verification.
We'll use a representative rule set that almost every city ordinance boils down to:
- Don't block the sidewalk.
- Don't park in the roadway or a bike lane.
- Don't block building entrances, ramps, or emergency exits.
- Leave the vehicle upright and stable.
- Park inside a designated bay where one exists.
By the end you'll have a complete policy JSON you can paste into the live demo, test against real photos, and ship.
The mental model: categories vs criteria vs severity
Three concepts do all the work. Get these right and the rest is filling in prose.
| Concept | What it is |
| ------------- | -------------------------------------------------------------------------- |
| Category | The outcome bucket the model must pick exactly one of (e.g. good_parking).|
| Criterion | One individual rule the model checks and returns pass/fail for. |
| Severity | How badly a failed criterion matters: critical, warning, or info. |
Think of it as a funnel. The model evaluates each criterion
independently against the photo, the severity of any failures
decides which category the photo lands in, and the category's
isCompliant flag becomes the final yes/no your app branches on.
The cleanest mapping is one criterion per ordinance clause. "Devices
shall not obstruct pedestrian right-of-way" becomes a
not_blocking_sidewalk criterion. Keeping them one-to-one means your
violation_reasons map straight back to the statute, which is exactly
what a city auditor wants to see.
Categories — the outcome buckets
A category is forced-choice: the model picks exactly one per photo. Each
category carries an isCompliant flag and a color for the scanner
result screen. For a parking ordinance, four buckets cover everything:
| Category | isCompliant | When it's chosen |
| --------------- | ------------- | ------------------------------------------------------ |
| good_parking | yes | Every criterion passes — the vehicle is parked legally.|
| bad_parking | no | One or more clear parking violations detected. |
| poor_photo | no | Image too dark, blurry, or cropped to assess. |
| no_vehicle | no | No scooter or e-bike visible in the frame. |
You can rename these freely. A city ordinance with fine tiers might add
minor_violation and major_violation categories so the operator can
warn vs fine based on the bucket alone.
Criteria — the individual rules
Each criterion has five fields. The description is the load-bearing
one — the model reads it verbatim, so it is effectively a prompt
fragment:
| Field | Description |
| ------------- | -------------------------------------------------------------------- |
| id | Stable identifier — exactly what shows up in violation_reasons. |
| label | Short human-readable name for dashboards. |
| description | The actual rule. Be concrete about what passes AND what fails. |
| severity | critical, warning, or info — drives category resolution. |
| required | If true, the criterion contributes to which category is chosen. |
Severity — how failures roll up
Severity is the dial that decides whether a failed criterion downgrades the whole result. The policy engine resolves the category roughly like this:
- Any
criticalfailure on a required criterion →bad_parking. - Only
warningfailures → still surfaced inviolation_reasons, but the operator decides whether to fail. - Not enough signal in the photo →
poor_photo. - Every criterion passes →
good_parking.
Map ordinance clauses to severity by their real-world consequence.
"Parked in an active traffic lane" is a safety hazard, so it's
critical. "Leaning slightly against street furniture" is a nuisance,
so it's a warning.
The complete sample policy
Here is a full, paste-ready policy that encodes the five-clause
ordinance above. It's modeled on the hardened production
scooter/e-bike parking policy that ships in the VerifyAI demo, generalized
to a generic scooter_parking ID. Note how each ordinance clause maps
to exactly one criterion, and how the descriptions spell out the edge
cases the model would otherwise get wrong (parking lots are not
roadways, edge-of-sidewalk parking is fine, and so on).
{
"id": "scooter_parking",
"name": "City scooter & e-bike parking ordinance",
"description": "End-of-ride parking verification encoding a municipal micromobility ordinance: no sidewalk blocking, not in the roadway, not blocking entrances, upright, in a designated bay.",
"config": {
"mode": "structured",
"categories": [
{
"id": "good_parking",
"label": "Good Parking",
"color": "#22c55e",
"isCompliant": true,
"description": "All criteria pass. Vehicle correctly parked and fully visible."
},
{
"id": "bad_parking",
"label": "Bad Parking",
"color": "#ef4444",
"isCompliant": false,
"description": "One or more clear, unmistakable parking violations detected."
},
{
"id": "poor_photo",
"label": "Poor Photo",
"color": "#f97316",
"isCompliant": false,
"description": "Photo quality prevents assessment AND no parking violations are detectable. Use only when the image is too dark, blurry, or cropped to make any determination."
},
{
"id": "no_vehicle",
"label": "No Vehicle",
"color": "#6b7280",
"isCompliant": false,
"description": "No scooter or e-bike visible in the photo. Only sidewalks, buildings, cars, people, or unrelated scenes."
}
],
"criteria": [
{
"id": "vehicle_visible",
"label": "Vehicle clearly visible",
"description": "A scooter (stem/handlebars, deck, two wheels) or e-bike (wheels, seat, handlebars, battery) must be clearly identifiable. Only FAIL if no micromobility vehicle is visible — just sidewalks, buildings, cars, or unrelated scenes.",
"severity": "critical",
"required": true
},
{
"id": "image_clear",
"label": "Image is clear enough",
"description": "Only FAIL if the photo is completely black with nothing visible, extremely blurry to the point where no shapes are identifiable, or a non-photo (screenshot, blank screen, text-only). Nighttime and low-light photos are FINE as long as you can identify that a scooter/e-bike is present. Minor blur, motion blur, or dim lighting should all PASS.",
"severity": "warning",
"required": true
},
{
"id": "not_blocking_sidewalk",
"label": "Not blocking sidewalk",
"description": "Ordinance: devices must not obstruct the pedestrian right-of-way. Only FAIL if the vehicle is in the MIDDLE of a sidewalk, not pushed to either edge. Also FAIL if it takes up more than 50% of sidewalk width or is on a pedestrian crossing. Vehicles at the edge (against a wall, railing, or curb) are fine.",
"severity": "warning",
"required": true
},
{
"id": "not_in_roadway",
"label": "Not in roadway or bike lane",
"description": "Ordinance: devices must not be left in a travel lane. Only FAIL if the vehicle is parked in an active traffic lane where cars are actively driving (a street, highway, or through-road). Parking lots, parking spaces, parking garages, driveways, and areas adjacent to parked cars are NOT roadways — these are fine. A vehicle on a sidewalk or curb next to a parking lot is fine. Bike lanes and bus-only lanes ARE roadways and should FAIL.",
"severity": "critical",
"required": true
},
{
"id": "not_blocking_entrance",
"label": "Not blocking entrance",
"description": "Ordinance: devices must not block doorways, ramps, or emergency exits. Only FAIL if the vehicle is physically positioned in a doorway, ramp, or emergency exit opening such that it prevents people from entering or exiting. Being parked near, beside, or in front of a building that has an entrance is NOT a violation — the vehicle must actually obstruct the doorway opening itself.",
"severity": "critical",
"required": true
},
{
"id": "vehicle_stable",
"label": "Vehicle stable and upright",
"description": "Ordinance: devices must be left upright on a kickstand or stand. PASS this in nearly all cases. The only exception is if the vehicle is completely horizontal on the ground — wheels in the air, frame flat on pavement, clearly fallen or wrecked. Normal parked scooters photographed from any angle are upright. Do not second-guess this.",
"severity": "warning",
"required": false
},
{
"id": "in_designated_bay",
"label": "Parked in a designated bay",
"description": "Ordinance: where a designated parking bay or corral is provided, the device must be left inside it. PASS if the vehicle is inside a painted box, marked corral, rack, or signed parking area. Only FAIL if a designated bay is clearly visible in the frame and the vehicle is parked outside of it. If no bay markings are visible at all, PASS — absence of a bay is not a violation.",
"severity": "warning",
"required": false
}
],
"maxAttempts": 3,
"autoApproveOnExhaust": true,
"uiCopy": {
"scannerTitle": "End Ride Photo",
"scannerInstructions": "Step back and take a photo showing your entire scooter and its parking location",
"processingMessage": "Checking parking compliance...",
"successMessage": "Parking verified — ride complete!",
"failureMessage": "Parking issue detected",
"retryMessage": "Please reposition your scooter or retake the photo. {remaining} attempts remaining.",
"exhaustedMessage": "Photo submitted for manual review. Your ride has ended."
}
}
}How criteria map to violation reasons
When a verification fails, every failed criterion lands in
violation_reasons as its stable id. This is the bridge back to the
ordinance — each ID is a clause the rider broke.
{
"is_compliant": false,
"category": "bad_parking",
"violation_reasons": ["not_blocking_sidewalk", "not_in_roadway"],
"feedback": "Please move your scooter off the traffic lane and out of the middle of the sidewalk."
}Because the IDs are stable, you can chart violation distribution straight from your verification log and report compliance to the city:
select unnest(violation_reasons) as ordinance_clause, count(*)
from verify_ai_verifications
where policy_id = 'scooter_parking'
and created_at > now() - interval '30 days'
and is_compliant = false
group by ordinance_clause
order by count(*) desc;If not_blocking_sidewalk dominates, you have a rider-education
problem. If a single metadata->>'gps' cluster keeps producing
not_in_roadway, you likely have a location that needs a designated bay.
Test it against the demo before you ship
You don't need to write any integration code to validate the policy. Open the live demo, and run real end-of-ride photos through the parking-compliance flow to sanity-check the categories and criteria. Walk through these cases deliberately:
- A scooter neatly against a wall on a wide sidewalk → should be
good_parking. - A scooter standing in a travel lane → should be
bad_parkingwithnot_in_roadwayinviolation_reasons. - A scooter dead-center on a narrow sidewalk →
bad_parkingwithnot_blocking_sidewalk. - A pitch-black or screenshot image →
poor_photo. - A photo of an empty curb with no vehicle →
no_vehicle.
When the buckets match your intent, point the SDK at the policy:
curl -X POST https://verify.switchlabs.dev/api/v1/verify \
-H "X-API-Key: vai_your_api_key" \
-F "image=@end_of_ride.jpg" \
-F "policy=scooter_parking" \
-F 'metadata={"trip_id":"trip_123","gps":"37.7749,-122.4194"}'If the model gets an edge case wrong, you fix the criterion description — not the app. Descriptions are server-side, so changes take effect immediately with no app store review and no redeploy.
Designated bays and geofences
The in_designated_bay criterion handles the visual case: the model can
see a painted box or corral in the photo and confirm the vehicle is
inside it. That works well for spray-painted corrals and racks.
For ordinances that define bays by GPS coordinates rather than paint on the ground, pair the photo check with a geofence on your side:
- Send the ride's end coordinates in
metadata.gps(shown above). - In your backend, test those coordinates against the city's mandated bay polygons before accepting the ride as compliant.
- Treat the VerifyAI result and the geofence check as an AND: the photo proves the vehicle is parked correctly, the geofence proves it's in a legal location.
This split keeps the policy purely visual (and therefore portable across cities) while location rules stay in your geofencing layer, where they belong.
FAQ
Do I need a separate policy per city?
Usually not. Most ordinances reduce to the same five clauses, so one
scooter_parking policy serves many cities. Use a distinct policy only
when a city has a genuinely different rule (for example, a hard
designated-bay requirement that should be critical instead of a
warning). Keep the criterion IDs identical across policies so your
violation analytics stay comparable.
What if the ordinance has fine tiers?
Add categories. Instead of a single bad_parking bucket, define
minor_violation and major_violation, and route criteria to them by
severity. Your operations team can then warn on minor and fine on major
without any extra logic.
How do I avoid false positives like "parked next to a building"?
Spell the edge case out in the criterion description. The sample
not_blocking_entrance rule explicitly says parking near an entrance
is fine and only obstructing the opening fails. Concrete pass/fail
language is the single biggest lever on accuracy.
Can the city see the underlying photo and reasoning?
Yes. Every verification stores the photo, the chosen category, the
per-criterion results, and violation_reasons. That audit trail is what
lets an operator defend a fine or report aggregate compliance to the
city.
How fast and how expensive is this?
VerifyAI runs at $0.008 per verification with no annual contract and no
minimums — a fraction of legacy micromobility vision vendors. Latency is
well under the threshold riders notice at end of ride.
What's next
- Migrating from Captur — move an existing micromobility parking workflow over to VerifyAI.
- Template: Micromobility parking policy — a ready-made policy you can clone and edit.
- Guide: Custom policies — the full reference for authoring policies beyond parking.
- Try the live demo — run photos through the parking flow now.