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.

bash
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 url on desktop, laptop or mobile. Add an HTTPS return_url at 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_origins in the create request. The host handles the submitted callback and navigation. See the browser example in self-inspection links.
  • Native app: pass url to InspectionClient and VerifyAIInspection in 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.

ChoiceAPI configurationBoundary
Your own photo order and namesvehicle_condition_custom, profile: custom, ordered required_shots with your slot, label and supported viewViews use the existing inspection contracts; arbitrary new analysis tasks need a separate policy/qualification
Required versus optional photosrequired: true/false; optional repeatable groups; total capacity up to 30Repeatable groups are optional and upload as slot_1 through slot_N
Camera or imported photoscapture_sources: ["camera"], ["library"], or bothEach upload must declare its source_mode when sources are configured
Continue capturing during checksbackground_analysis: trueGuided photos only; wait for every queued upload/check before finalizing
Signature and return navigationrequire_signature, HTTPS return_urlThe hosted client supports signatures; the custom reference page is no-signature only
Hosted, embedded or your own capture UISame signed capture credential; allowed_embed_origins for iframesPermanent tenant keys stay on your backend
Your application identifiersreference_id and contextContext is tenant-side metadata, not instructions that change analysis or lending rules
Your own decision logicRead findings through the tenant API and calculate in your backendA capture acknowledgment is not a finished result; use the decision endpoint for lending sessions
VerifyAI's automatic lending calculationlending plus optional book referenceGuided photos only; uses the account's saved policy, not arbitrary per-request rules
Collection, free-order or video captureprocessing_workflow or capture_mode, as documented in OpenAPISeparate 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:

bash
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_id to use your current policy. The reference value must be in the policy currency (422 reference_currency_mismatch), and steering_side must be left or right (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 returns 400 invalid_vin; a body type the guides do not cover returns 422 vehicle_class_unsupported; an unavailable decoder returns 503 vin_decoder_unavailable, and you can retry or send vehicle_class yourself. 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_format or not_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: true lets 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) or unable_to_assess (for example a VIN mismatch or incomplete analysis), with reason_codes and plain-language reasons.
  • 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: your reference, the deduction line_items, totals (total_deductions_cents, adjusted_collateral_value_cents) and lending_value_cents. A declined vehicle returns 0 under 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_cents and adjusted collateral value are null, even on decline. Grid repair_cost_cents is also null: condition-loss percentages are not repair estimates. Never treat these nulls as zero deductions. Without a reference value, lending_value_cents is null and value_state is reference_missing; send the value later with POST /api/v1/inspection-sessions/{id}/decision ({"reference": {...}, "idempotency_key": "..."}), which records a new decision and sends a new webhook.
  • policy (id, version, and status: draft for the starting policy, confirmed once you save your own), input_hash, decision_id and sequence. Deduplicate webhook deliveries by the X-VerifyAI-Delivery header, and keep the highest sequence for 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:

bash
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:

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

  1. GET /{token} for authoritative slots, guidance and current state.
  2. POST /{token}/submit, multipart image, 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.
  3. 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.
  4. POST /{token}/finalize with {"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 read GET /{inspection_id}/decision with 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.

LimitScope
Evaluation key: 20/minute, 200/hourDollar Loans' current sandbox key
Signed capture token: 600/minute, 6,000/hourOne token; includes status reads
3 active photo attemptsOne inspection
12 active photo attemptsOne tenant
3.5 MiB image / 4,000,000-byte requestJPEG, PNG or WebP upload
10 new tests/dayDollar 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):

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

Get in Touch

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