Verifying parked vehicles

This guide builds a real end-of-ride parking check, end to end. We'll use the Forest (HumanForest) e-bike policy as the worked example because it has every interesting wrinkle: multiple violation types, retry behaviour, custom categories, and an auto-approve fallback.

By the end you'll have:

  1. A scanner screen in your mobile app that captures a photo.
  2. A verification call to POST /v1/verify with the pol_forest1 policy.
  3. Branching logic on the result — end the ride, prompt for retake, or send to manual review.

1. Understand the policy

Forest's pol_forest1 policy is a structured policy with four categories and thirteen criteria. The categories are domain-specific rather than the generic compliant / unsafe defaults:

| Category | Compliant | When it's chosen | | -------------- | --------- | --------------------------------------------------- | | good_parking | yes | Bike correctly parked, no violations detected. | | bad_parking | no | One or more parking violations (not_* criteria). | | poor_photo | no | Image too cropped to assess parking. | | no_bike | no | No Forest bike visible in the frame. |

The criteria cover three things:

  • Photo quality — is the bike visible? Is enough surrounding context shown?
  • Bike identity — is the bike actually a Forest bike (not a competitor)?
  • Parking violations — yellow lines, tactile paving, blocked pedestrian flow, blocked driveways, unpermitted surfaces, leaning / fallen, and so on.

Every criterion has a severity (critical, warning, or info) and a required flag. The full list is in supabase/migrations/20260304_forest_parking_policy.sql. The TL;DR: any critical failure on a parking criterion downgrades the ride to bad_parking.

You can fetch the policy's runtime config (categories + scanner copy) from the API:

bash
curl https://verify.switchlabs.dev/api/v1/policies/pol_forest1/config \
  -H "X-API-Key: vai_your_api_key"

2. Capture and submit

The simplest integration is to drop in the SDK scanner — the React Native version below also handles retries and shows the result screen.

import { useVerifyAI } from "@switchlabs/verify-ai-react-native";
import { VerifyAIScanner } from "@switchlabs/verify-ai-react-native/scanner";

function EndRideScreen({ tripId, userId }: { tripId: string; userId: string }) {
const { verify } = useVerifyAI({
  apiKey: process.env.EXPO_PUBLIC_VERIFY_AI_KEY!,
});

return (
  <VerifyAIScanner
    policy="pol_forest1"
    onCapture={(base64) =>
      verify({
        image: base64,
        policy: "pol_forest1",
        metadata: { trip_id: tripId, user_id: userId },
      })
    }
    onResult={(result) => {
      if (result.is_compliant) {
        // good_parking: end the ride immediately
        endRide(tripId);
      } else if (result.category === "bad_parking") {
        // SDK already shows the retry overlay; nothing else needed
      }
    }}
    onExhausted={(result) => {
      // 3 attempts done — autoApproveOnExhaust is true on this policy,
      // so the ride is closed and flagged for review.
      endRide(tripId, { manualReview: true, verificationId: result.id });
    }}
  />
);
}

3. Branch on the result

Once you have a result back, branch on category rather than just is_compliant. The Forest policy uses distinct buckets for "bad parking" vs "bad photo" — your UX should treat them differently:

ts
switch (result.category) {
  case "good_parking":
    // End the ride immediately.
    endRide(tripId, { verificationId: result.id });
    break;
 
  case "bad_parking":
    // Tell the rider what's wrong; SDK already shows result.feedback.
    // Allow them to reposition the bike and retake.
    showRetryPrompt(result.violation_reasons);
    break;
 
  case "poor_photo":
    // Image isn't good enough to decide. Ask for a wider shot.
    showRetryPrompt(["framing"]);
    break;
 
  case "no_bike":
    // No Forest bike in the photo. The rider may have grabbed the
    // wrong bike — escalate to support.
    routeToSupport(userId, tripId);
    break;
}

4. Handle retries and the exhaustion path

pol_forest1 ships with maxAttempts: 3 and autoApproveOnExhaust: true. The SDK handles the loop automatically:

  1. Rider takes photo → fails.
  2. SDK shows retryMessage ("Please reposition your bike or retake the photo. 2 attempts remaining.").
  3. Rider takes photo → fails.
  4. SDK shows retry message with 1 remaining.
  5. Rider takes photo → fails.
  6. SDK shows exhaustedMessage ("Photo submitted for manual review. Your ride has ended.") and resolves the ride.

The ride is always closed at the end — autoApproveOnExhaust means the rider isn't held hostage by a model that won't pass them. Your review queue picks up these cases (metadata.manualReview = true) and operations can fine or warn the rider after the fact.

5. Inspect the failure modes

Each failed criterion lands in violation_reasons as a stable ID:

json
{
  "is_compliant": false,
  "category": "bad_parking",
  "violation_reasons": [
    "not_on_tactile_paving",
    "not_blocking_pedestrian"
  ],
  "feedback": "Please move your bike off the tactile paving and away from the pedestrian path."
}

Use these IDs for analytics. Forest charts violation distribution weekly:

sql
select unnest(violation_reasons) as reason, count(*)
from verify_ai_verifications
where policy_id = 'pol_forest1'
  and created_at > now() - interval '7 days'
  and is_compliant = false
group by reason
order by count(*) desc;

That tells you whether riders need more education ("not_on_yellow_lines" dominating), or whether a particular location keeps producing bad parking (metadata->>'gps').

What's next

  • Concepts: Policies — the structured policy shape used here.
  • API reference: Verify — request and response schema in detail.
  • Custom policy creation (coming soon) — author your own policy for a different vehicle class or workflow.

Get in Touch

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