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:

  1. Don't block the sidewalk.
  2. Don't park in the roadway or a bike lane.
  3. Don't block building entrances, ramps, or emergency exits.
  4. Leave the vehicle upright and stable.
  5. 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.

One ordinance clause = one criterion

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 critical failure on a required criterion → bad_parking.
  • Only warning failures → still surfaced in violation_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).

json
{
  "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.

json
{
  "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:

sql
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_parking with not_in_roadway in violation_reasons.
  • A scooter dead-center on a narrow sidewalk → bad_parking with not_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"}'
Iterate on descriptions, not code

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

Get in Touch

Questions about pricing, integrations, or custom deployments? We'd love to hear from you.