Sending an inspection link

Sometimes the person doing the inspection isn't using your app. They might be a driver handing over a press car, a renter at the end of a trip, or a customer who hasn't installed anything. Inspection links solve that: you create an inspection, we return a URL, and the person does the whole walk-around in a mobile browser. Every photo is checked as it is taken, and the finished inspection comes back to you as a webhook, an API record, and a PDF condition report.

By the end of this guide you'll have:

  1. A Delivery inspection for a vehicle, created with the vehicle_handoff template and opened inside your own app.
  2. A Pickup inspection for the same vehicle that is compared with the Delivery photos, view by view.
  3. A handler that reads the inspection.submitted webhook and gates on the inspection's outcome.

1. Create a Delivery inspection

bash
curl -X POST https://verdictcapture.com/api/v1/inspection-sessions \
  -H "X-API-Key: vai_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "template": "vehicle_handoff",
    "stage": "delivery",
    "reference_id": "LOAN-48213",
    "vehicle": {
      "vin": "4T1C11AK5RU123456",
      "plate": "8ABC123",
      "year": 2024,
      "make": "Toyota",
      "model": "Camry"
    },
    "recipient": { "name": "Alex Rivera", "locale": "en" },
    "context": { "driver_id": "drv_311", "entered_odometer": 18354 },
    "return_url": "https://fleet.example.com/loans/LOAN-48213/delivered",
    "require_signature": false,
    "send_immediately": false
  }'

The response (201 Created):

json
{
  "id": "6a1f3c52-8d0e-4b7a-9f21-3c5d7e9a0b14",
  "url": "https://verify.switchlabs.dev/inspect/vai_lnk_eyJzaWQiOi...",
  "token": "vai_lnk_eyJzaWQiOi...",
  "expires_at": "2026-10-15T15:00:00.000Z",
  "status": "pending",
  "stage": "delivery",
  "reference_id": "LOAN-48213"
}

id is the inspection id you use with your API key from here on. url is the link the person opens; the token inside it is the only credential the capture page needs, so treat it like a one-off password.

Request fields

FieldTypeNotes
templatestring"vehicle_handoff" sets the views, the policy for each view, and require_signature: false.
policystringRequired when there is no template. The policy for views that don't name their own.
required_shotsarrayYour own views instead of the template's. Each: slot, label, optional instructions, required, policy, repeatable (1–12). Up to 30 possible photos in total after expanding repeatable slots, with at least one required view. With custom views the inspection's template is null.
stagestring"delivery" or "pickup". A pickup needs a reference_id. Leave it out for a standalone inspection with no comparison (see Standalone inspections).
reference_idstringYour loan, rental or activity id, up to 120 characters. Pairs a Pickup with its Delivery and is how you look inspections up later.
vehicleobjectvin, plate, year, make, model, description. Shown on the capture page and the PDF.
recipientobjectname, email, phone, locale (en, es, fr, de).
contextobjectAnything you want back. Copied into every photo's verification metadata, with reference_id, stage and the VIN added. entered_odometer is checked (see below).
return_urlstringWhere the person goes when they finish. Must be https.
require_signaturebooleanAsk for a signature before submitting. Defaults to true, or false with vehicle_handoff.
send_immediatelybooleanDefaults to true: email the link when recipient.email is set. Set false to deliver the link yourself.
channelstringweb (default), sms or whatsapp. SMS and WhatsApp invites are not sent yet; the returned url always works.
ttl_secondsintegerLink lifetime. Default 7 days, minimum 60 seconds, maximum 30 days.

Every policy the inspection uses must be active and available to your account, or the request fails with 400 Invalid policy: <id>.

2. The views

The vehicle_handoff template asks for seven views and one optional step. Each view is checked against its own policy, and the check is told which view was requested, so a cabin photo is never held to the exterior rules and a photo of the wrong side is caught:

SlotViewChecked byRequired
frontFront of vehiclepol_handoff_exteriorYes
driver_sideDriver sidepol_handoff_exteriorYes
rearRear of vehiclepol_handoff_exteriorYes
passenger_sidePassenger sidepol_handoff_exteriorYes
cabin_frontFront cabinpol_handoff_interiorYes
cabin_rearRear cabinpol_handoff_interiorYes
odometerOdometerpol_handoff_odometerYes
damageDamage close-upspol_handoff_damage_closeupNo, up to 8 (damage_1…damage_8)

Each photo gets one of three results the moment it is taken:

  • Accepted. The right view, clear enough to judge.
  • Retake. The wrong view, too dark or blurred, or an odometer that can't be read in full. The person is asked to take it again, and the server enforces it: the inspection cannot be submitted while any photo still needs a retake.
  • Kept and flagged. A usable photo that shows damage, a cabin issue, or items left behind. It is saved as evidence and flagged for review instead of sending the person round in circles.

After three attempts at the same view, the person can continue with their last photo, so a dark garage doesn't strand a driver. That view is marked as continued without an accepted photo, and the inspection's outcome becomes review. Each link allows up to 8 photo checks per view. Its total check budget is the greater of 60 or four times the configured photo capacity, up to 120 checks for a standard 30-photo inspection. Beyond either limit, uploads answer 429 attempt_limit and you should issue a new inspection.

The odometer policy reads the total mileage, the unit, and the fuel or charge level. If you pass context.entered_odometer, the reading is compared with it. A difference of more than 0.5% or 5 units, whichever is larger, sends the inspection to review. Add context.entered_odometer_unit ("mi" or "km") if your mileage may be in a different unit from the vehicle's display; the two are converted before comparing.

The capture page shrinks each photo in the browser before uploading it. If you upload photos yourself, keep each upload under about 4 MB: larger request bodies can be refused before the photo is checked, and the check itself rejects anything over 10 MB (413 image_too_large).

With send_immediately: false nothing is sent; you put url in front of the person yourself. Three ways that work:

  • The device browser. Open url with the system browser.
  • An in-app browser. SFSafariViewController on iOS, Custom Tabs on Android. The person stays in your app and the camera works with no extra setup.
  • A WebView. The capture page takes photos through a standard <input type="file" accept="image/*" capture="environment">. In a WebView, your app has to allow that: on Android, implement WebChromeClient.onShowFileChooser and hold the camera permission; on iOS, include NSCameraUsageDescription in your Info.plist. If the camera never opens inside your WebView, this is why.

When the person submits, the finalize response carries your return_url with the result appended:

text
https://fleet.example.com/loans/LOAN-48213/delivered
  ?inspection_id=6a1f3c52-8d0e-4b7a-9f21-3c5d7e9a0b14
  &inspection_status=submitted
  &stage=delivery
  &reference_id=LOAN-48213

In a browser tab or a WebView, the done screen sends the person there after three seconds. Inside a frame it shows a Return to … button instead, which opens the URL in the top window.

The page also tells a host that it's done, with this message:

json
{
  "type": "verifyai:inspection_submitted",
  "inspection_id": "6a1f3c52-8d0e-4b7a-9f21-3c5d7e9a0b14",
  "outcome": "clear",
  "stage": "delivery",
  "reference_id": "LOAN-48213"
}

It is posted to the parent window when the page is in a frame, to the opener window when your app opened it as a popup, to window.ReactNativeWebView (as a JSON string) in a React Native WebView, and to a verifyai script message handler in a WKWebView. From a web page:

js
window.addEventListener("message", (e) => {
  if (e.origin !== "https://verify.switchlabs.dev") return;
  if (e.data?.type === "verifyai:inspection_submitted") {
    closeInspection(e.data.inspection_id);
  }
});

Treat both as navigation hints, not as the result. A pickup has no outcome yet at this point (null): it is set once the comparison with the delivery finishes. The record is finished, with its final outcome and PDF, when the inspection.submitted webhook arrives, or when you read the inspection with your API key and completed_at is set. A Pickup's comparison.status then ends as complete, no_baseline or failed; the last two always mean review.

Finishing later or on another device

Every photo is saved as soon as it is checked, so an unfinished inspection can be picked up again from the same url, on the same phone or a different one. The page reopens with the photos already taken and continues from the next view. This works until the inspection is submitted or the link expires (ttl_seconds, 7 days by default). After submission the link shows that the inspection is complete.

Building your own capture screen

The capture page uses these token-authenticated endpoints. You can call them yourself if you'd rather build the screen natively:

EndpointBodyWhat it does
GET /api/v1/inspection-sessions/{token}—The views to capture, their instructions, branding, localized strings, and each capture's current state.
POST /api/v1/inspection-sessions/{token}/submitmultipart/form-data: image, slotCheck one photo. Returns the capture's state: accepted, needs_review, attempts, can_override, feedback. 429 attempt_limit past the per-link limits. Close-up slots are numbered damage_1, damage_2, … with no leading zeros.
POST /api/v1/inspection-sessions/{token}/override{ "slot": "rear" }Continue with the last photo after three attempts. 409 retake_first before that.
POST /api/v1/inspection-sessions/{token}/remove{ "slot": "damage_2" }Remove an optional close-up. Required views can only be retaken.
POST /api/v1/inspection-sessions/{token}/finalize{ "signature_data_url"?, "signer_name"? }Submit. 422 incomplete lists the blocking views; 422 signature_required when a signature is required and missing or isn't a PNG/JPEG image. Safe to repeat: a second call returns the same receipt. 409 busy if photos were still being saved; retry.

finalize answers straight away:

json
{
  "status": "submitted",
  "inspection_id": "6a1f3c52-8d0e-4b7a-9f21-3c5d7e9a0b14",
  "outcome": "clear",
  "comparison_pending": false,
  "return_url": "https://fleet.example.com/loans/LOAN-48213/delivered?inspection_id=6a1f3c52-8d0e-4b7a-9f21-3c5d7e9a0b14&inspection_status=submitted&stage=delivery&reference_id=LOAN-48213"
}

For a pickup, outcome is null and comparison_pending is true.

After the response, we run the Pickup comparison (for a pickup), render the PDF, and send the webhook, once per inspection per endpoint. If that work is interrupted, it is finished automatically within a few minutes. Once an inspection is submitted, the capture endpoints answer 410, including a photo that was still being checked at that moment; the PDF stays downloadable with the same token.

4. The outcome

Every submitted inspection has an outcome you can gate on, with plain-language outcome_reasons:

OutcomeWhen
clearEvery required view was captured and accepted, and nothing needs a person to look at it.
reviewAny of the reasons below. review.status starts as pending until someone records a decision.

Reasons that send an inspection to review:

  • A view was flagged: damage noted, or a cabin issue.
  • A damage close-up was documented.
  • The person continued without an accepted photo.
  • The odometer reading doesn't match context.entered_odometer.
  • Pickup only: new damage since the delivery, a view that could not be matched to or compared with its delivery photo, no submitted delivery for the reference_id, or a comparison that could not complete.

A pickup has no outcome (null) while comparison.status is pending.

5. Create the Pickup inspection

Use the same reference_id with "stage": "pickup":

bash
curl -X POST https://verdictcapture.com/api/v1/inspection-sessions \
  -H "X-API-Key: vai_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "template": "vehicle_handoff",
    "stage": "pickup",
    "reference_id": "LOAN-48213",
    "vehicle": { "vin": "4T1C11AK5RU123456", "plate": "8ABC123" },
    "return_url": "https://fleet.example.com/loans/LOAN-48213/returned",
    "send_immediately": false
  }'

When the Pickup is submitted, we find the most recent submitted Delivery inspection on your account with the same reference_id and compare the two:

  • View by view. For each exterior and cabin view captured at both stages, the Delivery photo and the Pickup photo are analyzed together, and only damage visible at Pickup that was not visible at Delivery is reported. Differences from dirt, water, reflections, lighting, framing or angle are ignored. Damage close-ups are kept as evidence but not paired.
  • Odometer and fuel. The Delivery and Pickup readings, and the distance driven between them.

The pickup is compared with the latest Delivery for the same reference_id that was submitted before the pickup; that choice is fixed at submission. The result lands in comparison. Its status is pending while it runs, then complete, no_baseline (no submitted Delivery with that reference_id), or failed. A pickup is clear only when the comparison completed and found nothing; no_baseline, failed, new damage, or a view that couldn't be compared all mean review.

Standalone inspections

Some inspections stand on their own, such as a one-day work order or a periodic condition check. Create them like a Delivery but leave out stage:

bash
curl -X POST https://verdictcapture.com/api/v1/inspection-sessions \
  -H "X-API-Key: vai_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "template": "vehicle_handoff",
    "reference_id": "WO-20931",
    "vehicle": { "vin": "4T1C11AK5RU123456", "plate": "8ABC123" },
    "send_immediately": false
  }'

A standalone inspection has the same views, checks, outcome, condition report and webhook as a Delivery. It is never compared with another inspection and never used as a Pickup's baseline, so comparison stays null. reference_id is optional; set it to your work order id to find the inspection later.

6. The inspection.submitted webhook

Subscribe an endpoint to inspection.submitted in Settings → Webhooks (see Setting up webhooks). One delivery arrives per submitted inspection, after the comparison and the PDF, so it is the finished record: completed_at is set, the outcome is final, and pdf_url links the condition report. If the PDF can't be generated within 15 minutes, the webhook is sent without it (pdf_url is null) and you can still download it with the link token from GET /api/v1/inspection-sessions/{token}/pdf, which renders one on demand. When you read inspections with your API key instead, a non-null completed_at means the same thing. A Pickup with new damage, trimmed to three of its views:

json
{
  "event": "inspection.submitted",
  "created_at": "2026-10-08T15:01:12.000Z",
  "data": {
    "id": "9c2e7b41-0f3a-4d58-8e6b-1a2b3c4d5e6f",
    "object": "inspection",
    "status": "submitted",
    "stage": "pickup",
    "reference_id": "LOAN-48213",
    "template": "vehicle_handoff",
    "vehicle": { "vin": "4T1C11AK5RU123456", "plate": "8ABC123" },
    "context": { "reference_id": "LOAN-48213", "stage": "pickup", "vin": "4T1C11AK5RU123456" },
    "created_at": "2026-10-08T14:20:00.000Z",
    "opened_at": "2026-10-08T14:52:31.000Z",
    "submitted_at": "2026-10-08T15:00:47.000Z",
    "completed_at": "2026-10-08T15:00:58.000Z",
    "expires_at": "2026-10-15T14:20:00.000Z",
    "outcome": "review",
    "outcome_reasons": [
      "New damage since delivery: 1 item (Rear of vehicle)",
      "Flagged for review: Rear of vehicle (Damage noted)"
    ],
    "views_required": 7,
    "views_captured": 7,
    "views": [
      {
        "slot": "front",
        "base_slot": "front",
        "label": "Front of vehicle",
        "required": true,
        "status": "accepted",
        "category": "no_damage",
        "category_label": "No damage",
        "feedback": "The front of the vehicle is fully in frame and no damage is visible.",
        "warning": null,
        "damage": [],
        "readings": null,
        "verification_id": "ver_7Kq2...",
        "captured_at": "2026-10-08T14:53:10.000Z",
        "attempts": 1,
        "image_url": "<signed URL, valid for 7 days>"
      },
      {
        "slot": "rear",
        "base_slot": "rear",
        "label": "Rear of vehicle",
        "required": true,
        "status": "flagged",
        "category": "damage_noted",
        "category_label": "Damage noted",
        "feedback": "Scratch on the rear bumper, left of the plate.",
        "warning": null,
        "damage": [{ "type": "scratch", "panel": "car_rear_bumper", "severity": "light" }],
        "readings": null,
        "verification_id": "ver_9Tz4...",
        "captured_at": "2026-10-08T14:55:02.000Z",
        "attempts": 1,
        "image_url": "<signed URL, valid for 7 days>"
      },
      {
        "slot": "odometer",
        "base_slot": "odometer",
        "label": "Odometer",
        "required": true,
        "status": "accepted",
        "category": "reading_captured",
        "category_label": "Reading captured",
        "feedback": "Odometer reads 18,766 mi",
        "warning": null,
        "damage": [],
        "readings": { "odometer": 18766, "odometer_unit": "mi", "fuel_level": "1/2" },
        "verification_id": "ver_2Lp8...",
        "captured_at": "2026-10-08T14:58:40.000Z",
        "attempts": 2,
        "image_url": "<signed URL, valid for 7 days>"
      }
    ],
    "odometer": { "reading": 18766, "unit": "mi", "fuel_level": "1/2" },
    "comparison": {
      "status": "complete",
      "baseline_inspection_id": "6a1f3c52-8d0e-4b7a-9f21-3c5d7e9a0b14",
      "baseline_submitted_at": "2026-10-01T14:03:55.000Z",
      "views": [
        {
          "slot": "rear",
          "label": "Rear of vehicle",
          "baseline_verification_id": "ver_1Ab3...",
          "verification_id": "ver_9Tz4...",
          "comparable": true,
          "new_damage": [
            { "description": "Scratch about 4 inches long", "location": "rear bumper, left of the plate", "severity": "minor" }
          ],
          "notes": "New scratch on the rear bumper."
        }
      ],
      "new_damage_count": 1,
      "odometer": { "baseline": 18354, "current": 18766, "unit": "mi", "distance": 412 },
      "fuel": { "baseline": "3/4", "current": "1/2" },
      "computed_at": "2026-10-08T15:01:09.000Z"
    },
    "review": { "status": "pending", "note": null, "reviewed_by": null, "reviewed_at": null },
    "pdf_url": "<signed URL, valid for 7 days>",
    "signature": false
  }
}

View status is one of accepted, flagged, override (continued without an accepted photo), retake_needed, missing, or skipped (an optional step with no photo). Photo and PDF links in a webhook are signed and stay valid for 7 days; download anything you need to keep.

A handler that gates on the outcome:

ts
app.post("/webhooks/verify-ai", express.raw({ type: "application/json" }), async (req, res) => {
  if (!verifySignature(req.body, req.get("X-VerifyAI-Signature"))) return res.sendStatus(401);
  const { event, data } = JSON.parse(req.body.toString("utf8"));
  res.sendStatus(200);
 
  if (event === "inspection.submitted") {
    const comparedOk = data.stage !== "pickup" || data.comparison?.status === "complete";
    if (data.outcome === "clear" && comparedOk) await releaseVehicle(data.reference_id);
    else await openReviewTask(data.reference_id, data.id, data.outcome_reasons);
  }
});

verifySignature is the check described in Setting up webhooks.

7. Read, list and review

Read one inspection with your API key, at any status. The body is the same object as the webhook's data, with photo links valid for an hour:

bash
curl https://verdictcapture.com/api/v1/inspection-sessions/6a1f3c52-8d0e-4b7a-9f21-3c5d7e9a0b14 \
  -H "X-API-Key: vai_your_api_key"

List inspections, newest first. Filter by reference_id, stage, status or outcome; page with limit (up to 200, default 50) and before (a created_at timestamp):

bash
curl "https://verdictcapture.com/api/v1/inspection-sessions?reference_id=LOAN-48213" \
  -H "X-API-Key: vai_your_api_key"
json
{
  "object": "list",
  "data": [
    {
      "id": "9c2e7b41-0f3a-4d58-8e6b-1a2b3c4d5e6f",
      "object": "inspection",
      "status": "submitted",
      "stage": "pickup",
      "reference_id": "LOAN-48213",
      "vehicle": { "vin": "4T1C11AK5RU123456", "plate": "8ABC123" },
      "outcome": "review",
      "comparison_status": "complete",
      "review_status": "pending",
      "created_at": "2026-10-08T14:20:00.000Z",
      "submitted_at": "2026-10-08T15:00:47.000Z",
      "expires_at": "2026-10-15T14:20:00.000Z"
    }
  ]
}

Record the staff decision on a submitted inspection. It fires inspection.reviewed with the updated record, and returns it:

bash
curl -X POST https://verdictcapture.com/api/v1/inspection-sessions/9c2e7b41-0f3a-4d58-8e6b-1a2b3c4d5e6f/review \
  -H "X-API-Key: vai_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{ "decision": "damage_confirmed", "note": "Scratch confirmed with the driver.", "reviewer": "ops@fleet.example.com" }'

decision is approved or damage_confirmed. Reviewing an inspection that isn't submitted returns 409 not_submitted; while a pickup is still being compared or the report is being prepared it returns 409 processing (retry shortly). A recorded decision is never changed by the automated checks. The same decision can be recorded from the dashboard.

People lose links. Resend issues a new token for the same inspection:

bash
curl -X POST https://verdictcapture.com/api/v1/inspection-sessions/6a1f3c52-8d0e-4b7a-9f21-3c5d7e9a0b14/resend \
  -H "X-API-Key: vai_your_api_key"

The previous link stops working immediately, resend_count goes up, and you get back {id, url, token, expires_at, resend_count}. Resend keeps the original expires_at; it doesn't extend the deadline. It answers 409 for a submitted or revoked inspection (including one submitted while the resend was in progress) and 410 once the link has expired. There is no separate revoke endpoint.

Guided 13 and 15 view inspections

Guided inspections check every photo as it is taken and ask for a retake before the person moves on. If you request the passenger door and the photo shows a wheel, the step does not advance. They use the same endpoints as above with an additive contract, inspection_contract_version: 2. Existing vehicle_handoff inspections keep their behaviour.

Guided inspections are enabled per account. Until yours is enabled, create answers 503 with v2_admission_disabled. Each vehicle class, steering side and cab layout is enabled only after it passes our photo evaluation; a combination that has not answers 503 with evaluation_required and no inspection is created.

bash
curl -X POST https://verdictcapture.com/api/v1/inspection-sessions \
  -H "X-API-Key: vai_your_api_key" \
  -H "Idempotency-Key: 7d6c1f0e-loan-48213" \
  -H "Content-Type: application/json" \
  -d '{
    "template": "vehicle_condition_truck",
    "profile": "door15",
    "steering_side": "right",
    "cab": "single",
    "reference_id": "loan-48213",
    "allowed_embed_origins": ["https://app.example.com"]
  }'
FieldValues
templatevehicle_condition_sedan, vehicle_condition_suv, vehicle_condition_truck, or vehicle_condition_custom with required_shots.
profilebaseline13 (13 required views plus up to two optional damage close-ups) or door15 (15 required views). custom for the custom template.
steering_sideleft, right or unknown (default). Driver and passenger wording is shown only when it is known.
cabstandard (default) or single. A single-cab truck replaces the rear-seats view with the cargo bed.
allowed_embed_originsExact HTTPS origins allowed to embed the capture page.
allow_flagged_submissionDefault false. When true, a person who cannot complete a view after three rejected photos can submit for your review with a reason.
accepted_retention_days, rejected_retention_daysDefaults 90 and 7.

The Idempotency-Key header is required. Sending the same body with the same key returns the same inspection and link; a different body answers 409.

The 13 views are the front, all four corners, both sides, the dashboard, front seats, rear seats (or cargo bed), odometer and VIN plate. door15 adds the whole left-front and right-front doors. Left and right always mean the vehicle's own sides, facing forward.

What each photo returns

POST /api/v1/inspection-sessions/{token}/submit returns 200 with a decision, or 202 while the check is still running:

decisionMeaning
acceptedCorrect view, usable photo. Visible damage is kept for your review, not retaken.
retake_requiredWrong component or view, or an unusable photo. The step does not advance.
review_requiredThe view could not be confirmed. The photo is kept and a retake is offered; it never counts as verified coverage.

The response also carries attempt_id, reason_codes, missing_zones, retry_allowed and remaining_content_attempts. If a response is lost, GET /api/v1/inspection-sessions/{token}/attempts/{attempt_id} returns the same result without checking the photo again.

finalize answers 422 and lists the blocking views until every required view has an accepted photo, and 409 while a check is still running. Submission is not completed analysis or staff approval: read the inspection with your API key, or wait for the signed webhook. Every guided inspection is routed to your staff for condition review.

Any-order photos, tire close-ups and walk-around video

Three options extend the same required views. Each is available only for vehicle configurations whose checks have passed evaluation; others are refused with 503 evaluation_required rather than run unchecked.

OptionRequestBehavior
Any-order photos"capture_mode": "freeform"The person photographs the vehicle in any order (or picks several photos). Send slot: "auto": each photo is classified, then checked under the matched view's own rules. A photo that matches no remaining view returns unassigned: true with plain guidance and is never evidence. Any order never replaces an accepted photo; a named-slot retake can. Named slots keep working, so older SDK builds still complete these inspections.
Tire close-upsvehicle_condition_custom views tire_front_left, tire_front_right, tire_rear_left, tire_rear_rightAlways photographed one tire at a time. Each view returns visual_tread_condition (appears_adequate, appears_worn, appears_bald or unknown): a visual estimate from one photo, not a tread-depth measurement. The wheel position is the one requested; a photo cannot verify it.
Walk-around video"capture_mode": "video" with optional "video": {"interior": "required", "still_fallback": true}The person records a slow exterior walk-around and an interior clip (no sound is recorded; audio in uploaded files is removed before anything is kept). Up to 15 of the clearest distinct frames from each clip are checked like any-order photos and fill the views they show; the odometer and VIN are separate photos. Clips upload in resumable chunks through POST {token}/clips. A processed upload alone never completes an inspection. Video inspections are not yet priced or generally available.

Inspections returned by GET /api/v1/inspection-sessions/{id} include capture_mode, a clips list for video inspections (with a short-lived link to the audio-free recording), source on views filled from a video frame (clip and time) and tire on tire views.

Damage-adjusted valuation

POST /api/v1/inspection-sessions/{id}/valuation applies your reference value and your methodology to a submitted inspection's current findings. Verify does not supply vehicle values or depreciation rates.

bash
curl -X POST https://verdictcapture.com/api/v1/inspection-sessions/$INSPECTION_ID/valuation \
  -H "X-API-Key: vai_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "idempotency_key": "valuation-loan-48213-1",
    "reference": { "amount_cents": 1450000, "currency": "USD", "source": "Your book value", "as_of": "2026-10-19" },
    "methodology": {
      "methodology_id": "standard-auto", "version": "2026-10", "label": "Standard auto collateral",
      "currency": "USD", "findings_basis": "staff_reviewed",
      "repair_matrix": [
        { "id": "light", "panel": "*", "damage_type": "*", "severity": "light", "amount_cents": 15000, "repair_method": "Spot repair", "charge_basis": "per_panel" }
      ],
      "diminution": { "mode": "percent_of_repair", "percent": 10 },
      "collateral_discount_percent": 5
    }
  }'

Repair cost, market-value diminution and your collateral discount are separate lines. Matrix rules work like the appraisal cost matrix: the most specific rule wins, per panel or per finding. Staff dismissals, corrections and merged duplicates are applied; structural concerns are never priced automatically. With findings_basis: "staff_reviewed" totals stay empty until a staff decision exists. Each result lists unpriced findings and its limitations, and is stored unchanged; GET returns the latest one, marked stale once the inspection changes. Valuations are never sent in webhooks.

You can give the returned url directly to the person doing the inspection. It opens in a desktop, laptop or mobile browser without an app installation or borrower dashboard login. On a computer, choose saved photo files; on a phone, use the camera or photo library. The same required-view and retake checks apply.

Set send_immediately: false to deliver the link through your own application. Optionally set return_url to your HTTPS application page. After the server acknowledges submission, the standalone page offers a return button and redirects after three seconds. The return URL includes inspection_id and inspection_status=submitted, plus reference_id and stage when supplied. An unsuccessful submission does not redirect. With no return URL, the person can close the completed page.

An embedded inspection stays inside its frame. Handle its submitted event in your own page and choose when to continue or navigate; the capture does not automatically navigate the parent. A returned user or callback indicates saved submission. Your backend still reads the API or verified webhook for analysis, report and staff-review state.

Embedding with the browser package

Download verify-ai-capture-web.tgz and install it into your frontend. Your backend creates the inspection with its API key; the browser receives only the capture link.

js
import { start } from '@switchlabs/verify-ai-capture-web';
 
const capture = start({
  sessionUrl: inspection.url,
  container: document.querySelector('#capture'),
  allowedOrigin: 'https://verify.switchlabs.dev',
  onEvent(event) {
    if (event.event === 'submitted') {
      // Ask your backend to read the current inspection.
    }
  },
});
// capture.cancel() closes the frame and keeps the inspection resumable.
// capture.resume() mounts it again; capture.destroy() removes listeners.

Events are ready, step_changed, validation_result, progress, submitted, error and cancelled. They carry IDs and state only, never photos or links. Your page must allow the frame to use the camera (allow="camera" is set for you, plus any upstream Permissions-Policy). Without camera permission the capture falls back to the phone's photo picker.

The condition report

Every submitted inspection gets a PDF: the vehicle and reference, the outcome and its reasons, the odometer, every view's photo with its result, a damage table, and the changes since delivery for a pickup. Download it from pdf_url, or with the link token from GET /api/v1/inspection-sessions/{token}/pdf. See Generating PDF condition reports.

Token security cheatsheet

  • Links carry a signed token prefixed vai_lnk_. Only a hash of it is stored, so a token can't be recovered from our side, only replaced.
  • Default lifetime is 7 days, maximum 30 days (ttl_seconds).
  • Expired, replaced, or tampered tokens are refused: the page shows a plain, branded message instead of the capture flow, and the endpoints answer 404 or 410.
  • The token authorizes capturing photos for one inspection and downloading its PDF, nothing else. Reading the full inspection, listing, reviewing and resending all need your API key.

What's next

What do you need to verify?

Tell us about the evidence, the decision and the workflow around it.

Discuss your workflow