Retries
A single photo doesn't always pass on the first try — a rider parks half on the curb, the light is bad, the framing cuts off a wheel. Rather than hard-failing the end user, the VerifyAI SDK scanners give them a bounded number of attempts to fix the problem and resubmit. Retries are an SDK-side capture loop, not a server feature: every attempt is still a real verification, and the policy decides when the loop ends.
This page covers how that loop works. For the guide-level walkthrough (with code), see Handling retries.
The capture loop
When a user opens a scanner, the SDK runs the same sequence on every photo:
- Capture a photo.
- Submit it to
POST /api/v1/verifyfor the configured policy. - Read
is_complianton the returned Verification object. - If compliant, show the success screen and resolve.
- If not compliant, show the failure/retry screen and let the user capture again — until attempts run out.
Each pass through that loop is one attempt, and each attempt is a
fully independent verification with its own ver_ id, category, and
violation_reasons. Retrying does not amend a previous verification; it
creates a new one.
maxAttempts
maxAttempts lives on the policy's PolicyConfig and caps how many
photos the scanner will accept before it stops asking the user to try
again. It's read straight from the policy config the SDK fetches at
GET /api/v1/policies/:id/config, so you
can tune it without shipping a new app build.
The production-style Levy Electric parking policy
(LEVY_PARKING_POLICY in lib/demo/policies.ts) sets:
{
"maxAttempts": 3,
"autoApproveOnExhaust": true
}With maxAttempts: 3, a rider gets up to three photos. On a failing
attempt with attempts left, the scanner shows the policy's
retryMessage, interpolating how many tries remain:
{
"retryMessage": "Please reposition your scooter or retake the photo. {remaining} attempts remaining."
}The {remaining} token is substituted by the SDK from the live attempt
count, so attempt 1 of 3 reads "2 attempts remaining," and so on.
The counter resets every time the user starts a fresh scan session. It is not a rolling quota across rides or sessions — it only bounds how many photos a single end-of-action flow will accept before exhausting.
Exhaustion
If the user uses up all maxAttempts without ever producing a compliant
photo, the loop exhausts. What happens next is controlled by
autoApproveOnExhaust.
autoApproveOnExhaust: true
This is the Levy parking behavior. When attempts run out, the scanner
does not trap the user — it lets the action complete and shows the
policy's exhaustedMessage:
{
"exhaustedMessage": "Photo submitted for manual review. Your ride has ended."
}Concretely, on exhaustion the SDK:
- Resolves the scan as complete so the user can move on (the ride ends, the handover finishes).
- Flags the final non-compliant verification for manual review so an operator can look at it later.
The product reasoning: a rider should never be stranded by an imperfect parking photo. You accept the small operational cost of a human review in exchange for never blocking a legitimate end-of-ride.
autoApproveOnExhaust: false
When this flag is off (or absent), exhaustion is a hard stop. The
scanner surfaces the exhaustedMessage and resolves the session as a
failure — the action does not auto-complete. Use this when a
non-compliant outcome must block the workflow (for example, a damage
inspection that cannot be waved through).
Where retries sit in the lifecycle
Retries happen entirely before any verification reaches your
backend's webhooks or polling. Each attempt fires its own
verification.completed event, so a three-attempt session that ends in
auto-approval produces three verifications:
- Attempts 1 and 2:
is_compliant: false, surfaced to the user as retries. - Attempt 3:
is_compliant: false, but the session auto-approves and the verification is flagged for manual review.
If an operator later overturns that verdict in the dashboard, you get a
separate verification.reviewed event. So
the SDK retry loop and the server-side review path are distinct stages:
retries decide what the user sees in the moment; review decides the
final recorded verdict.
A non-compliant verification mid-loop is normal — it's a retry, not a
failure. Let the scanner own the loop and act on the resolved session
outcome (success, exhausted-and-approved, or exhausted-and-failed)
rather than branching off a single attempt's is_compliant.
UI copy reference
The strings the retry loop shows all come from the policy's uiCopy, so
you localize and reword them without an app release:
| Field | Shown when |
| ------------------- | -------------------------------------------------------------- |
| failureMessage | A photo is non-compliant. |
| retryMessage | Non-compliant with attempts left. Supports {remaining}. |
| successMessage | A photo passes. |
| exhaustedMessage | All maxAttempts used up. |
What's next
- Handling retries — the SDK-side guide with scanner code.
- Policies — where
maxAttemptsandautoApproveOnExhaustare configured. - Verifications — the object each attempt produces.