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:
- A Delivery inspection for a vehicle, created with the
vehicle_handofftemplate and opened inside your own app. - A Pickup inspection for the same vehicle that is compared with the Delivery photos, view by view.
- A handler that reads the
inspection.submittedwebhook and gates on the inspection'soutcome.
1. Create a Delivery inspection
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):
{
"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
| Field | Type | Notes |
|---|---|---|
template | string | "vehicle_handoff" sets the views, the policy for each view, and require_signature: false. |
policy | string | Required when there is no template. The policy for views that don't name their own. |
required_shots | array | Your 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. |
stage | string | "delivery" or "pickup". A pickup needs a reference_id. Leave it out for a standalone inspection with no comparison (see Standalone inspections). |
reference_id | string | Your loan, rental or activity id, up to 120 characters. Pairs a Pickup with its Delivery and is how you look inspections up later. |
vehicle | object | vin, plate, year, make, model, description. Shown on the capture page and the PDF. |
recipient | object | name, email, phone, locale (en, es, fr, de). |
context | object | Anything 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_url | string | Where the person goes when they finish. Must be https. |
require_signature | boolean | Ask for a signature before submitting. Defaults to true, or false with vehicle_handoff. |
send_immediately | boolean | Defaults to true: email the link when recipient.email is set. Set false to deliver the link yourself. |
channel | string | web (default), sms or whatsapp. SMS and WhatsApp invites are not sent yet; the returned url always works. |
ttl_seconds | integer | Link 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:
| Slot | View | Checked by | Required |
|---|---|---|---|
front | Front of vehicle | pol_handoff_exterior | Yes |
driver_side | Driver side | pol_handoff_exterior | Yes |
rear | Rear of vehicle | pol_handoff_exterior | Yes |
passenger_side | Passenger side | pol_handoff_exterior | Yes |
cabin_front | Front cabin | pol_handoff_interior | Yes |
cabin_rear | Rear cabin | pol_handoff_interior | Yes |
odometer | Odometer | pol_handoff_odometer | Yes |
damage | Damage close-ups | pol_handoff_damage_closeup | No, 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).
3. Open the link in your app
With send_immediately: false nothing is sent; you put url in front
of the person yourself. Three ways that work:
- The device browser. Open
urlwith the system browser. - An in-app browser.
SFSafariViewControlleron 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, implementWebChromeClient.onShowFileChooserand hold the camera permission; on iOS, includeNSCameraUsageDescriptionin yourInfo.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:
https://fleet.example.com/loans/LOAN-48213/delivered
?inspection_id=6a1f3c52-8d0e-4b7a-9f21-3c5d7e9a0b14
&inspection_status=submitted
&stage=delivery
&reference_id=LOAN-48213In 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:
{
"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:
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:
| Endpoint | Body | What 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}/submit | multipart/form-data: image, slot | Check 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:
{
"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:
| Outcome | When |
|---|---|
clear | Every required view was captured and accepted, and nothing needs a person to look at it. |
review | Any 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":
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:
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:
{
"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:
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:
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):
curl "https://verdictcapture.com/api/v1/inspection-sessions?reference_id=LOAN-48213" \
-H "X-API-Key: vai_your_api_key"{
"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:
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.
8. Resending a link
People lose links. Resend issues a new token for the same inspection:
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.
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"]
}'| Field | Values |
|---|---|
template | vehicle_condition_sedan, vehicle_condition_suv, vehicle_condition_truck, or vehicle_condition_custom with required_shots. |
profile | baseline13 (13 required views plus up to two optional damage close-ups) or door15 (15 required views). custom for the custom template. |
steering_side | left, right or unknown (default). Driver and passenger wording is shown only when it is known. |
cab | standard (default) or single. A single-cab truck replaces the rear-seats view with the cargo bed. |
allowed_embed_origins | Exact HTTPS origins allowed to embed the capture page. |
allow_flagged_submission | Default 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_days | Defaults 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:
decision | Meaning |
|---|---|
accepted | Correct view, usable photo. Visible damage is kept for your review, not retaken. |
retake_required | Wrong component or view, or an unusable photo. The step does not advance. |
review_required | The 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.
| Option | Request | Behavior |
|---|---|---|
| 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-ups | vehicle_condition_custom views tire_front_left, tire_front_right, tire_rear_left, tire_rear_right | Always 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.
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.
A standalone link and return to your application
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.
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
404or410. - 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
- Setting up webhooks — receive
inspection.submittedandinspection.reviewedand verify their signatures. - Generating PDF condition reports — what the report contains.
- Before/after rental inspection delta — how the Delivery-vs-Pickup comparison works.