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:
- A scanner screen in your mobile app that captures a photo.
- A verification call to
POST /v1/verifywith thepol_forest1policy. - 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:
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:
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:
- Rider takes photo → fails.
- SDK shows
retryMessage("Please reposition your bike or retake the photo. 2 attempts remaining."). - Rider takes photo → fails.
- SDK shows retry message with 1 remaining.
- Rider takes photo → fails.
- 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:
{
"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:
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.