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:

text
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 uses

The reference shape lives in PolicyConfig in the codebase:

ts
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:

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

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

json
{
  "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
}
Writing good criterion descriptions

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:

  1. The model evaluates every criterion individually and returns a pass/fail for each.
  2. The policy engine picks a category based on which criteria failed and at what severity:
    • Any critical failure on a required criterion → typically the unsafe (or domain equivalent) category.
    • Only warning failures → improvable.
    • Insufficient signal in the photo → lacks_info / poor_photo.
    • All criteria pass → compliant / good_parking.
  3. violation_reasons is the list of failed criterion ids.
  4. is_compliant is the category's isCompliant flag.

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.

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

Get in Touch

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