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:

  1. Capture a photo.
  2. Submit it to POST /api/v1/verify for the configured policy.
  3. Read is_compliant on the returned Verification object.
  4. If compliant, show the success screen and resolve.
  5. 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:

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

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

Attempts are policy-scoped, not global

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:

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

Don't end the action on the first non-compliant verification

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

Get in Touch

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