Vehicle inspection integration
Use the same inspection session with a native mobile SDK, a browser link, a website embed, or your own API-driven screens. The dashboard shows the same saved evidence, condition review and PDF report for each approach. A borrower using a capture link does not need a dashboard account.
Capture through the API, review in the portal
Your integration can stop at collecting and submitting the complete photo set. Create the inspection on your backend, let the customer capture each configured view, show the server's photo-check feedback, and finalize once every required view is accepted and all uploads/checks have settled. Your staff then use Dashboard → Inspections for the saved photos, damage assessment and report.
This capture workflow does not require your application to fetch damage findings,
component grades, deductions or a lending decision. Those result endpoints are
optional integrations. An inspection.submitted webhook is also optional if your
application only needs the final submission acknowledgment and staff use the
portal. Keep the inspection ID and your reference_id to associate the portal
record with the customer's application.
Photo acceptance means that view satisfies its configured capture checks; it does not mean the vehicle is undamaged or approved for a loan. Required angles come from the session's configuration. The server enforces completeness even when you build your own screens. The processing policy still applies normally; choosing portal-only results does not disable analysis or change lending rules.
1. Create on your backend
Store the tenant key in your server's secret manager. Authenticate your own user and application before creating a session. Never put the tenant key in a mobile app, web page, capture URL or borrower-visible log.
curl https://verify.switchlabs.dev/api/v1/inspection-sessions \
-H "X-API-Key: $VERIFY_AI_API_KEY" \
-H 'Idempotency-Key: application-123-inspection-1' \
-H 'Content-Type: application/json' \
-d '{
"template": "vehicle_condition_sedan",
"profile": "door15",
"vehicle_class": "sedan",
"steering_side": "left",
"reference_id": "APPLICATION-123",
"recipient": {"name": "Demo tester"},
"require_signature": true,
"send_immediately": false
}'The response includes id, signed capture url, credential_expires_at and
session_expires_at. Retrying the same request with the same idempotency key
returns the same session. A different request requires a new key. Save the
inspection ID against your application; do not use the capture credential as
your permanent record identifier.
For SUVs or trucks use the matching vehicle_condition_suv or
vehicle_condition_truck template and vehicle class. baseline13 collects
13 required views; door15 also collects separate front-door views. Steering
and cab configuration must match the vehicle. Account enablement controls which
configurations can be created. Expiring evaluation access uses its assigned
policy and sandbox terms; it does not activate production lending decisions or
automatic billing. 503 evaluation_required indicates an unqualified
configuration, not a retryable image-upload problem.
2. Choose the capture interface
- Browser link: open the response's
urlon desktop, laptop or mobile. Add an HTTPSreturn_urlat creation to return to your application after acknowledged submission. See self-inspection links. - Website embed: use the browser capture package and explicitly allow your
website origin with
allowed_embed_originsin the create request. The host handles the submitted callback and navigation. See the browser example in self-inspection links. - Native app: pass
urltoInspectionClientandVerifyAIInspectionin the React Native or Flutter SDK. TestFlight and our tester app are not prerequisites for this integration. - Custom screens: call the signed-token capture endpoints yourself. Preserve slot IDs, request IDs, shot versions and the server's accepted/retake/processing states. Use the OpenAPI contract linked below; do not implement acceptance rules locally.
3. Configure different workflows
Your backend chooses a configuration for each new inspection. Your application can use different configurations for different products or application stages; it does not have to copy the dashboard's default checklist.
| Choice | API configuration | Boundary |
|---|---|---|
| Your own photo order and names | vehicle_condition_custom, profile: custom, ordered required_shots with your slot, label and supported view | Views use the existing inspection contracts; arbitrary new analysis tasks need a separate policy/qualification |
| Required versus optional photos | required: true/false; optional repeatable groups; total capacity up to 30 | Repeatable groups are optional and upload as slot_1 through slot_N |
| Camera or imported photos | capture_sources: ["camera"], ["library"], or both | Each upload must declare its source_mode when sources are configured |
| Continue capturing during checks | background_analysis: true | Guided photos only; wait for every queued upload/check before finalizing |
| Signature and return navigation | require_signature, HTTPS return_url | The hosted client supports signatures; the custom reference page is no-signature only |
| Hosted, embedded or your own capture UI | Same signed capture credential; allowed_embed_origins for iframes | Permanent tenant keys stay on your backend |
| Your application identifiers | reference_id and context | Context is tenant-side metadata, not instructions that change analysis or lending rules |
| Your own decision logic | Read findings through the tenant API and calculate in your backend | A capture acknowledgment is not a finished result; use the decision endpoint for lending sessions |
| VerifyAI's automatic lending calculation | lending plus optional book reference | Guided photos only; uses the account's saved policy, not arbitrary per-request rules |
| Collection, free-order or video capture | processing_workflow or capture_mode, as documented in OpenAPI | Separate qualified configurations; cannot be combined with automatic lending |
Custom slot names must be unique after repeatable expansion. auto, unsorted,
image, image_N, video_frame and video_frame_N are reserved. Extra capacity
adds a damage group, so your slots must not collide with that group or its
expanded names; conflicting requests return 400 capture_slot_conflict.
Every expanded name must be at most 48 characters, including its _N suffix;
overlong names return the same error. Source restrictions apply in both directions:
a library-only session rejects camera photos. The existing 422 camera_required
code means the supplied source is outside capture_sources; read that field in
the error response rather than assuming camera is always required.
Configurations, guides, vehicle identity and lending policy are pinned when the inspection is created. They cannot be changed halfway through a live session. If your application branches into a different checklist, create a new inspection with a new idempotency key and associate it with the same application reference; cancel the superseded open inspection when appropriate. Credential renewal keeps the existing configuration and evidence.
Lending-policy authoring currently uses Settings → Lending policy.
lending.policy_id selects the account's available policy; it does not create a
policy, select an arbitrary historical version, or override rules per request.
If you need several simultaneous rule sets, keep that decision logic in your
backend or arrange explicit product support before relying on that behavior.
4. Read progress and results
Your backend reads GET /api/v1/inspection-sessions/{id} with X-API-Key.
Use GET /api/v1/inspection-sessions?reference_id=APPLICATION-123 to find the
associated records. Configure signed inspection.submitted and
inspection.reviewed webhooks in dashboard settings, verify their signatures,
and deduplicate by the X-VerifyAI-Delivery header, which every retry reuses.
The API remains available if you prefer polling.
See webhooks.
Submission saves evidence. It does not mean staff have approved its condition. Reports may still be preparing; poll the saved inspection until its current PDF is available. Retrieve fresh signed image/report URLs when an old one expires. Staff can review in the dashboard or through the revision-bound review endpoint.
Lending inspections: automatic decision and value
Accounts enabled for a lending policy can create an inspection that is decided automatically, with no staff review step. Send the photo set, the VIN you hold and, when you have it, your reference (book) value:
curl https://verify.switchlabs.dev/api/v1/inspection-sessions \
-H "X-API-Key: $VERIFY_AI_API_KEY" \
-H 'Idempotency-Key: application-123-inspection-1' \
-H 'Content-Type: application/json' \
-d '{
"template": "vehicle_condition_custom",
"profile": "custom",
"vehicle_class": "from_vin",
"steering_side": "left",
"vehicle": {"vin": "1HGBH41JXMN109186"},
"reference_id": "APPLICATION-123",
"required_shots": [
{"slot": "front", "label": "Front", "view": "front"},
{"slot": "rear", "label": "Rear", "view": "rear"},
{"slot": "right_side", "label": "Right side", "view": "right_side"},
{"slot": "left_side", "label": "Left side", "view": "left_side"},
{"slot": "front_right", "label": "Front-right 45°", "view": "front_right"},
{"slot": "front_left", "label": "Front-left 45°", "view": "front_left"},
{"slot": "rear_right", "label": "Rear-right 45°", "view": "rear_right"},
{"slot": "rear_left", "label": "Rear-left 45°", "view": "rear_left"},
{"slot": "odometer", "label": "Odometer", "view": "odometer"},
{"slot": "front_cabin", "label": "Interior (front cabin)", "view": "front_seats"},
{"slot": "tire_front_left", "label": "Tyre, front left", "view": "tire_front_left"},
{"slot": "tire_front_right", "label": "Tyre, front right", "view": "tire_front_right"},
{"slot": "tire_rear_right", "label": "Tyre, rear right", "view": "tire_rear_right"},
{"slot": "tire_rear_left", "label": "Tyre, rear left", "view": "tire_rear_left"},
{"slot": "windscreen", "label": "Windscreen", "view": "windscreen"},
{"slot": "vin_plate", "label": "VIN", "view": "vin_plate"},
{"slot": "engine_bay", "label": "Engine compartment (under the bonnet)", "view": "engine_bay"}
],
"require_signature": false,
"capture_sources": ["camera"],
"background_analysis": true,
"lending": {
"reference": {"amount_cents": 1200000, "currency": "USD", "source": "Black Book", "as_of": "2026-10-21"}
},
"send_immediately": false
}'- Your lending policy (decline rules, repair amounts, deduction limit, lending
percentage, currency and VIN-mismatch handling) is edited in the dashboard under
Settings → Lending policy. Each save is a new version used by inspections
created afterwards; an inspection is always decided by the version it was
created under, and the settings page can preview any completed inspection under
unsaved changes. Omit
lending.policy_idto use your current policy. The reference value must be in the policy currency (422 reference_currency_mismatch), andsteering_sidemust beleftorright(400 steering_side_required). vehicle_class: "from_vin"decodes your VIN when the inspection is created and chooses the sedan, SUV or truck guides from its body type. An invalid VIN returns400 invalid_vin; a body type the guides do not cover returns422 vehicle_class_unsupported; an unavailable decoder returns503 vin_decoder_unavailable, and you can retry or sendvehicle_classyourself. Year, make and model you leave empty are filled from the decode.- The photographed VIN is compared with yours and never replaces it. The result
reports
vin_status:match,mismatch,not_read,invalid_formatornot_supplied. capture_sources: ["camera"]hides photo-library import in the official capture page and SDKs, and the server refuses photos a client reports as imported (422 camera_required). This restricts the input path; it is not proof that a photo was taken live.background_analysis: truelets the borrower keep photographing while earlier photos are checked in the hosted capture page; a photo that needs a retake is offered again after the current shot. Use the hosted page or custom web example below for this integration.
When the inspection completes, the decision is recorded once and is the same in
the API, the dashboard, the lender PDF and the signed inspection.decision
webhook (subscribe to it in dashboard settings). Read it with
GET /api/v1/inspection-sessions/{id}/decision or from the decision field of
the inspection record:
lending_eligibility_decision:accepted,discounted,declined,needs_recapture(a required photo was not accepted) orunable_to_assess(for example a VIN mismatch or incomplete analysis), withreason_codesand plain-languagereasons.condition_assessment: one defect per panel and damage type, at its highest severity, however many photos show it; visible engine-compartment concerns; visual tyre estimates.valuation: yourreference, the deductionline_items,totals(total_deductions_cents,adjusted_collateral_value_cents) andlending_value_cents. A declined vehicle returns0under your rule (value_state: "declined_zero"); the adjusted collateral value is reported when it can be calculated. With an incomplete component percentage grid,total_deductions_centsand adjusted collateral value arenull, even on decline. Gridrepair_cost_centsis alsonull: condition-loss percentages are not repair estimates. Never treat these nulls as zero deductions. Without a reference value,lending_value_centsisnullandvalue_stateisreference_missing; send the value later withPOST /api/v1/inspection-sessions/{id}/decision({"reference": {...}, "idempotency_key": "..."}), which records a new decision and sends a new webhook.policy(id, version, andstatus:draftfor the starting policy,confirmedonce you save your own),input_hash,decision_idandsequence. Deduplicate webhook deliveries by theX-VerifyAI-Deliveryheader, and keep the highestsequencefor an inspection; an older event never replaces a newer one.
The decision is a visible-condition result under your policy. Severity is a visual estimate from photos; hidden, mechanical and unphotographed damage is not assessed, engine-compartment photos show visible condition only, and tyre condition is a visual tread estimate. Staff may still correct findings in the dashboard; a correction records a new decision with a new sequence.
5. Recover or cancel
POST /api/v1/inspection-sessions/{id}/resend with {"send":false} renews
capture credentials within the inspection's lifetime. Give the newly returned
URL to the app; never ask the SDK to create its own tenant key.
Cancel an unfinished inspection from your backend:
curl -X POST "https://verify.switchlabs.dev/api/v1/inspection-sessions/$INSPECTION_ID/revoke" \
-H "X-API-Key: $VERIFY_AI_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"reason":"Application withdrawn by applicant"}'Cancellation invalidates all capture credentials, blocks further capture and
keeps evidence/audit history. Repeating it returns the saved cancellation
receipt. Submitted or expired inspections return 409 not_open; a missing or
different-account ID returns 404 not_found.
The machine-readable contract is maintained in
vehicle-inspections-v2.openapi.json.
Before testing with real borrowers, verify your own app's authentication,
return screen, webhook receiver, permission prompts and chosen device builds.
Custom web capture reference
Download the browser client, working camera page and server signature verifier. Host the page and client together over HTTPS on your domain. For capture with assessment in the portal, your backend can create this guided, no-signature inspection. The reference page does not require a lending-policy setup:
{
"template": "vehicle_condition_sedan",
"profile": "baseline13",
"vehicle_class": "sedan",
"steering_side": "left",
"reference_id": "APPLICATION-123",
"require_signature": false,
"capture_sources": ["camera"],
"send_immediately": false
}POST this body to /api/v1/inspection-sessions from your backend with its tenant
key and a new Idempotency-Key, then open the example with
#token=SIGNED_CAPTURE_TOKEN from the response. Account qualification still
applies. Add lending only when you separately want an automatic lending decision;
the 17-view request above is one such optional configuration.
Use a no-referrer policy and keep credentials out of logs.
The fragment supports reload; the client saves prepared photos and original request
IDs in IndexedDB. If device storage is unavailable, it reports durable: false.
The client calls:
GET /{token}for authoritative slots, guidance and current state.POST /{token}/submit, multipartimage,slot,client_request_id,source_mode: camera. The reference page waits for an accepted check before advancing. The shared uploader also supports background operation with at most three concurrent uploads and respects the server's active-attempt limit.GET /{token}/attempts?client_request_id=...before retrying a lost response;GET /{token}/attempts/{attempt_id}after a 202 receipt. It preserves IDs on transport retries and makes at most one technical recovery with a new ID.POST /{token}/finalizewith{"submission_mode":"normal"}. Required retakes or unfinished checks block submission. A successful replay is safe. Staff can now open the saved inspection in the portal. Only integrations consuming an automatic lending result need to readGET /{inspection_id}/decisionwith their tenant key.
The reference page uses connectCapture({ sequence: "sequential", ... }).
onProgress and client.progress() expose current_slot, accepted/required counts,
per-slot states, checking, can_capture and can_submit. In sequential mode,
add refuses another view until the current required view is accepted. A 202
upload acknowledgment does not advance the sequence. Rejected photos stay on
the current view; retry exhaustion stops further captures. After required photos
are accepted, the caller may take optional photos or submit. Submission requires
all required views accepted and every local photo settled. Omit sequence or use
"background" for application-controlled background capture. These options control
the client, not the pinned server assessment or its latency.
For fictional value tests, supply lending.reference.reference_kind: "nominal_test"
with the amount, currency, source and date. Such decisions include
qualification.test_only: true and visible test labels in the portal and PDF.
Your backend must exclude these results from real funding and production billing.
For an actual customer-supplied value use "customer_book_value"; the optional field
preserves compatibility with existing requests. A reference pinned at creation
cannot be silently replaced. Start a new inspection with the actual value.
The additive condition_assessment.component_grades contract describes individual
component damage grades from 1 (lowest) to 5 (highest), with evidence IDs. Clear,
uncertain, not-visible and not-applicable states have no numeric grade. This is a
candidate capability awaiting severity qualification; current inspections continue
to use their existing severity contract. Never synthesize intermediate grades from
three severity labels. percentage_loss_status: "awaiting_matrix" explicitly means
no component percentage-loss calculation was applied. Existing monetary lines retain
the methodology shown in their result.
A rejected view gets a new photo and request ID. Expired, revoked or rotated
credentials stop the queue, including when finalization returns 404 or 410;
obtain a replacement link from your backend. Credential rotation returns
404 not_found, while expiration or revocation returns 410. A
revision/shot-version conflict requires a fresh configuration read. The example
is for guided, no-signature sessions; use the hosted client for other workflows.
Never treat a browser callback or an upload acknowledgment as a lending decision.
GET /api/v1/inspection-sessions/{token} returns CaptureConfiguration without
a tenant key. The same URL with an inspection UUID and X-API-Key returns the
tenant's Session result. OpenAPI describes both response types on that shared
path. Both responses are private and must not be cached.
The reference client exposes slots with optional repeatable groups expanded.
add(slot, photo, source) defaults to camera; pass library only when the
session permits it. The supplied page uses the camera. finalize() refuses
while any local photo is queued, uploading or awaiting a check, including optional
photos. A server rejection preserves the queue so the user can resolve retakes
and retry. Capture closes only after acknowledged submission or an explicit
close(). The reference client rejects legacy, collection/video and signature
flows at connection time; use the hosted client or implement the corresponding
OpenAPI lifecycle for those flows.
If a required view exhausts its retries, the interface cannot submit normally.
An authorized backend can grant recovery with POST /{id}/recover after resolving
the incident, then ask the user to reload the capture link. A fresh server recovery
grant takes precedence over the device's exhausted receipt. The hosted flow also
supports flagged submission when the session policy permits it.
Read client.durable for current storage status and provide onDegraded to warn
if browser storage fails during capture. Keep the page open until uploads finish
when durability is unavailable. Revoked or submitted sessions clear their device
queue when detected; expired or rotated links retain photos for a replacement
credential. UI callbacks are isolated from queue transitions.
Direct capture requests permit cross-origin GET/POST/OPTIONS with Content-Type,
Idempotency-Key and X-API-Key headers. Browsers can read Retry-After, X-Request-Id
and Server-Timing. The tenant key stays on the backend even though CORS permits
its header. Iframe origins are controlled separately by allowed_embed_origins.
| Limit | Scope |
|---|---|
| Evaluation key: 20/minute, 200/hour | Dollar Loans' current sandbox key |
| Signed capture token: 600/minute, 6,000/hour | One token; includes status reads |
| 3 active photo attempts | One inspection |
| 12 active photo attempts | One tenant |
| 3.5 MiB image / 4,000,000-byte request | JPEG, PNG or WebP upload |
| 10 new tests/day | Dollar Loans' tester account, separate from API requests |
On 429, wait for Retry-After before retrying. Network errors, 409 busy/processing and temporary server failures use bounded exponential backoff and reconciliation. The request counters are not a concurrent-analysis allowance.
Deduction arithmetic and reason facts
Optional repair_matrix[].reference_percent_bps is an integer from 0 to 10,000.
A rule charges the greater of its amount_cents or its percentage of the reference
value, rounded half-up in cents. For example, 200 basis points means 2%:
a $500 floor yields $500 on $10,000 and $1,000 on $50,000. Missing reference values
produce pricing_state: reference_missing; fixed floors shown in that state are
provisional, not a completed percentage calculation. Existing fixed-only policies
and pinned inspections retain their original calculation.
Each damage line includes its component, damage types, severity, contributing defects, evidence IDs, rule IDs and calculations. Multiple findings sharing a panel and repair method use the largest resolved amount once. Tyre and engine lines have their own typed component fields. The OpenAPI contract defines all three line types.
reason_catalog_version: lending-reasons-v1 and reason_facts identify the rule,
policy version/hash, threshold and affected defect/evidence IDs. Existing codes
keep their meanings. Download the complete versioned reason catalog
for every code, category, policy field and definition. Preserve unknown codes when
adding a client integration so additive codes do not break result handling.
In particular, severe_damage means the severity threshold
selected by the lender, which can be medium. Codes cover severity, damage type,
panel count, engine concerns, deduction share, VIN mismatch, missing evidence,
incomplete analysis, uncertain condition, unpriced findings and advisory outcomes.
Map these facts to your own customer notices with your compliance team.
Verify the webhook signature over the original timestamp and raw body before
parsing. Read event.event, claim the X-VerifyAI-Delivery ID durably, keep the
highest decision sequence atomically and fetch the current decision before acting.
A failed handler must release/retry its delivery claim; do not mark it done early.
Framing guides and qualified capture checks
capture_guides accepts a versioned pack of static SVG assets keyed by
vehicle_class, cab and view, including exterior, cabin, readings, individual
tire positions, windscreen and engine-compartment views. Each SVG needs a positive viewBox.
Scripts, event handlers, external references, embedded documents and unsafe markup
are rejected. The pack is hashed and pinned to the inspection. Missing mappings
use built-in guides. Maximum: 128 assets, 16 KiB per SVG, 512 KiB total SVG content. A complete
17-view pack for four body/cab variants fits. Use the configured view name
(front_seats for the front-cabin slot); each tire position has its own mapping.
exterior_quality requests the separately qualified exterior-quality-v1 profile,
with landscape, min_vehicle_width and margin. Admission requires the exact
quality-policy hash and vehicle/class/steering/cab evaluation; an unqualified
configuration returns 503 evaluation_required. The initial candidate uses a
70% width target and 2% margin. Production activation depends on qualification.
The server enforces normalized image orientation and observed framing, obstruction
and cut-off checks. No crop or stretch is used to manufacture acceptance.
Single-cab pickups use vehicle_class: truck, cab: single, and the correct
steering side with the same lending 17-view set. No rear-seat shot is requested.
Configuration support does not bypass per-configuration qualification.
For a single-cab configuration admitted with the exterior-quality profile, merge these fields into the lending request above (choose the actual steering side):
{
"vehicle_class": "truck",
"cab": "single",
"steering_side": "left",
"exterior_quality": {
"version": "exterior-quality-v1",
"landscape": true,
"min_vehicle_width": 0.7,
"margin": 0.02
}
}The dashboard's lending form exposes Single cab (one row of seats) for trucks and applies these requirements automatically for admitted configurations, including single cab resolved from the VIN. Existing inspections keep their original capture requirements and policy version.
Optional component corrections and outcome records
The portal supports evidence-linked component corrections on completed lending inspections. Use a grade from 1 (lowest damage) through 5 (highest damage); clear, uncertain, not visible and not applicable have no numeric grade. The original observations remain in the audit history. Staff corrections appear in the report. Automatic five-grade availability depends on the account's qualified profile.
Authenticated POST /api/v1/inspection-sessions/{id}/review accepts optional
component_grade_edits with component, status, grade, accepted evidence_ids and
rationale. Evidence IDs are returned on accepted inspection views. Preserve the
existing review rationale, revision and idempotency fields. A capture token cannot
perform this operation. The OpenAPI Review schema defines the complete body.
Optional lending_outcome records an approved deduction, actual repair cost or
final adjusted vehicle value in integer minor units, with currency, observation
time, rationale and the exact decision_sequence. These records support later
calibration; they do not change lending rules or authorize payment. Mark label
provenance as personally confirmed only after actually checking the supporting
photos and amount sources. Saving does not automatically train a model.
For applications that separately choose to store lending results, the downloadable receiver store pairs with the signed webhook example. It persists duplicate protection, leases and the newest decision sequence on a private SQLite file. Use Node 22.13 or newer on one persistent host, or implement the same transactions in your shared database. This is optional; capture plus portal results remains a complete integration. The receiver records decisions and never funds a loan.
Nominal values supplied after capture also keep the inspection test-only. You may
adjust the nominal test amount, but supplying a production book value to that same
inspection returns reference_pinned. Create a new inspection for a real valuation.
When calling the reference capture client, await each add() before calling it
again; background mode still allows capture while earlier photo checks continue.