Policies
A policy is the rule set that VerifyAI evaluates a photo against.
Every verification request has to specify a policy ID. Built-in
policies cover the most common workflows; custom policies let you
codify your operations team's rules in a structured, version-controlled
way.
Anatomy of a policy
A structured policy has four parts:
Policy
├── categories — outcome buckets (e.g. compliant / unsafe / lacks_info)
├── criteria — individual rules the model evaluates
├── maxAttempts — how many retries the SDK should allow
└── uiCopy — translatable strings the scanner usesThe reference shape lives in PolicyConfig in the codebase:
interface PolicyConfig {
mode: "structured" | "advanced";
categories: ComplianceCategory[];
criteria: PolicyCriterion[];
maxAttempts?: number;
autoApproveOnExhaust?: boolean;
uiCopy?: UICopyConfig;
}Categories
A category is an outcome bucket. The model is forced to pick exactly one per verification. Two flags drive how the SDK behaves:
| Flag | Effect |
| ------------- | ---------------------------------------------------------------------- |
| isCompliant | When true, the verification passes (is_compliant: true on the API).|
| color | Drives the result-screen color in the scanner overlay. |
The default set covers most use cases:
[
{ "id": "compliant", "label": "Compliant", "color": "#22c55e", "isCompliant": true },
{ "id": "improvable", "label": "Improvable", "color": "#eab308", "isCompliant": false },
{ "id": "unsafe", "label": "Unsafe", "color": "#ef4444", "isCompliant": false },
{ "id": "lacks_info", "label": "Lacks Information","color": "#6b7280", "isCompliant": false }
]Custom policies override this list. For example, the Forest e-bike policy uses domain-specific buckets:
[
{ "id": "good_parking", "label": "Good Parking", "color": "#22c55e", "isCompliant": true },
{ "id": "bad_parking", "label": "Bad Parking", "color": "#ef4444", "isCompliant": false },
{ "id": "poor_photo", "label": "Poor Photo", "color": "#f97316", "isCompliant": false },
{ "id": "no_bike", "label": "No Bike", "color": "#6b7280", "isCompliant": false }
]Criteria
A criterion is one rule the model checks. Each criterion has:
| Field | Description |
| ------------- | -------------------------------------------------------------------- |
| id | Stable identifier — what shows up in violation_reasons. |
| label | Short human-readable name. |
| description | The actual rule. The model reads this verbatim — be specific. |
| severity | critical, warning, or info. Drives category resolution. |
| required | If true, this criterion contributes to category resolution. |
A real example from Forest's policy:
{
"id": "not_on_yellow_lines",
"label": "Not on yellow lines",
"description": "Look at what the bike wheels are standing on. If the wheels are on PAVING STONES, TILES, CONCRETE SLABS, or BRICKS (individual pieces with visible joints/gaps between them), the bike is on a SIDEWALK and this criterion PASSES regardless of any yellow paint visible elsewhere in the scene. Yellow paint on curb stones is NOT the same as yellow road lines. Only FAIL this criterion if the bike wheels are on SMOOTH DARK ASPHALT (a road surface without tile joints) AND there are continuous yellow painted lines on that road surface.",
"severity": "critical",
"required": true
}Treat the description as a prompt fragment for the model. Be concrete about what passes and what fails, name visual features explicitly, and disambiguate edge cases. Vague rules ("looks safe") produce vague outputs.
How a result is computed
Roughly:
- The model evaluates every criterion individually and returns a pass/fail for each.
- The policy engine picks a
categorybased on which criteria failed and at what severity:- Any
criticalfailure on a required criterion → typically theunsafe(or domain equivalent) category. - Only
warningfailures →improvable. - Insufficient signal in the photo →
lacks_info/poor_photo. - All criteria pass →
compliant/good_parking.
- Any
violation_reasonsis the list of failed criterionids.is_compliantis the category'sisCompliantflag.
The full evaluation logic lives in lib/verify-ai/policy-engine.ts.
Retries
maxAttempts controls how many photos the scanner accepts before
giving up and surfacing the exhaustedMessage. The Forest policy, for
example, allows three attempts and then auto-approves the ride and
flags it for manual review — that's autoApproveOnExhaust: true.
See Concepts: Retries for the SDK-side behavior.
UI copy
uiCopy lets you customise the scanner overlay without rebuilding the
mobile app. All strings support {remaining} interpolation for retry
counts.
{
"scannerTitle": "End Ride Photo",
"scannerInstructions": "Step back and take a photo showing your entire bike and its parking location",
"processingMessage": "Checking parking compliance...",
"successMessage": "Parking verified — ride complete!",
"failureMessage": "Parking issue detected",
"retryMessage": "Please reposition your bike or retake the photo. {remaining} attempts remaining.",
"exhaustedMessage": "Photo submitted for manual review. Your ride has ended."
}Account-level defaults are merged in automatically. The SDK fetches
this whole bundle from
GET /api/v1/policies/:id/config.
Built-in policies
| ID | Use case |
| ------------------------ | ---------------------------------------------- |
| scooter_parking | Generic e-scooter end-of-ride parking check. |
| bike_parking | Generic e-bike end-of-ride parking check. |
| pol_forest1 | Forest (HumanForest) e-bike parking, London. |
| forest_green_bay | Forest e-bike in a designated green bay. |
| forest_designated_bay | Forest e-bike in a designated bay. |
What's next
- API reference: Policies — the runtime config endpoint.
- Guide: Verifying parked vehicles — end-to-end walkthrough using the Forest policy.
- Custom policy creation (coming soon) — how to author your own policy.