Webhooks

This page is the reference for what VerifyAI sends to your webhook endpoints: the request, its headers, each event's payload, and how failed deliveries are retried.

For a walkthrough — adding an endpoint, verifying signatures in code, a production handler — see Setting up webhooks.

Managing endpoints

Endpoints are managed in the dashboard under Settings → Webhooks. There is no public REST API for creating, listing or deleting them. From the dashboard you can:

  • Add an endpoint: an HTTPS URL (not a private or loopback address) and the events it receives. The signing secret (whsec_…) is shown once, when the endpoint is added.
  • Edit events on an existing endpoint.
  • Send test event: deliver a signed webhook.test event to one endpoint and see its response.
  • Read the delivery log: each endpoint's 20 most recent deliveries, with status, attempts, and the HTTP status and body your endpoint returned.
  • Redeliver a failed or retrying delivery immediately.
  • Use the VerifyAI test receiver: a built-in endpoint that accepts every delivery and reports whether the signature verified, so you can see real deliveries before you have a server.
  • Delete an endpoint. Deliveries to it stop.

The request

Every delivery is an HTTPS POST with a JSON body:

HeaderValue
Content-Typeapplication/json
X-VerifyAI-Signaturet=<unix seconds>,v1=<hex>. v1 is HMAC-SHA256 of `${t}.${rawBody}`, keyed by the endpoint's signing secret.
X-VerifyAI-EventThe event name.
X-VerifyAI-DeliveryThe delivery id. The same on every retry and redelivery of that delivery; use it to deduplicate.

The body is { "event": "<name>", "data": { … } }. Events other than verification.completed also carry a top-level created_at.

Verify the signature over the raw body before parsing it, reject a t more than a few minutes from your clock, and compare in constant time. See Verify the signature.

Responses and retries

Your endpoint returnsWe do
Any 2xx within 5 secondsMark the delivery delivered.
Anything else, a timeout, or a network errorRetry after 1 minute, then after 5 more minutes (three attempts in total); then mark it failed.

Retries are processed on a schedule, so one can arrive shortly after it is due. A failed delivery can be redelivered from the dashboard at any time. Each attempt is signed with a fresh t.

Events

EventFires when
verification.completedA photo check finishes, compliant or not, including each photo taken through an inspection link.
inspection.submittedAn inspection link is submitted and processed (Pickup comparison, PDF and outcome included).
inspection.reviewedA decision is recorded on a submitted inspection, from the dashboard or POST /api/v1/inspection-sessions/{id}/review.
inspection.decisionA lending inspection gets its automatic decision, or a new one after a correction or a late reference value.
webhook.testSend test event is clicked for an endpoint. Sent to that endpoint only.

verification.completed

json
{
  "event": "verification.completed",
  "data": {
    "id": "ver_3aF92k1m",
    "created_at": "2026-10-08T14:55:02Z",
    "status": "success",
    "is_compliant": false,
    "confidence": 0.91,
    "policy": "pol_handoff_exterior",
    "category": "damage_noted",
    "violation_reasons": ["no_visible_damage"],
    "feedback": "Scratch on the rear bumper, left of the plate.",
    "warning_violation_reasons": [],
    "warning_feedback": null,
    "damage_findings": [
      {
        "finding_id": "f1",
        "panel": "car_rear_bumper",
        "damage_type": "scratch",
        "severity": "light",
        "severity_score": 0.3,
        "bbox": [0.41, 0.62, 0.55, 0.68],
        "area_pct": 0.04,
        "confidence": 0.86
      }
    ],
    "overall_severity": "light",
    "metadata": {
      "reference_id": "LOAN-48213",
      "stage": "pickup",
      "inspection_session_id": "9c2e7b41-0f3a-4d58-8e6b-1a2b3c4d5e6f",
      "inspection_slot": "rear",
      "channel": "web"
    },
    "image_url": "<signed URL, valid for 1 hour>"
  }
}
FieldDescription
id, created_atThe verification.
statusProcessing status.
is_compliant, confidenceThe verdict and its confidence (0–1).
policyThe policy id the photo was checked against.
categoryThe policy category the photo was classified into.
violation_reasons, feedbackFailed criteria and the plain-language feedback.
warning_violation_reasons, warning_feedbackNon-blocking criteria that failed.
damage_findings, overall_severityOnly when the policy reports damage and found some. bbox is [x1, y1, x2, y2] as fractions of the image size; area_pct is the fraction (0–1) of the panel covered.
readingsOnly when the policy reads values off the photo, e.g. { "odometer": 18766, "odometer_unit": "mi", "fuel_level": "1/2" }.
metadataWhat you sent. Inspection photos add inspection_session_id, inspection_slot, and the inspection's context.
image_urlSigned link to the photo, valid for 1 hour from the delivery attempt.

inspection.submitted and inspection.reviewed

data is the inspection, the same object GET /api/v1/inspection-sessions/{id} returns. Photo and PDF links in a webhook are signed for 7 days. A complete example is in Sending an inspection link.

FieldDescription
id, objectThe inspection id and "inspection".
statussubmitted for these events.
stagedelivery, pickup, or null.
reference_idYour reference, if you set one.
templatee.g. vehicle_handoff, or null.
vehicle, contextWhat you sent when creating the inspection (context also gets reference_id, stage and the VIN).
created_at, opened_at, submitted_at, expires_atTimestamps.
outcomeclear or review.
outcome_reasonsWhy, in plain language. Empty when clear.
views_required, views_capturedRequired views and how many were captured.
views[]One per view: slot, base_slot, label, required, status (accepted, flagged, override, retake_needed, missing, skipped), category, category_label, feedback, warning, damage[] (type, panel, severity), readings, verification_id, captured_at, attempts, image_url.
odometer{ reading, unit, fuel_level } from the odometer photo, or null.
comparisonnull unless the inspection is a pickup. Then { status }, where status is pending, complete, no_baseline or failed; when complete it also has baseline_inspection_id, baseline_submitted_at, views[] (slot, label, comparable, new_damage[] with description, location, severity of minor, moderate or severe, notes), new_damage_count, odometer (baseline, current, unit, distance), fuel (baseline, current), and computed_at.
review{ status, note, reviewed_by, reviewed_at }. status is pending, approved, damage_confirmed, or null.
pdf_urlSigned link to the condition-report PDF, or null if it could not be rendered.
signatureWhether a signature was captured.

inspection.decision

Sent for inspections created with lending. data is the same decision GET /api/v1/inspection-sessions/{id}/decision returns, plus the inspection's identifiers. See Vehicle inspection integration for every field.

FieldDescription
inspection_contract_version2.
inspection_id, reference_id, correlation_idThe inspection and your identifiers for it.
event_id, decision_idThe decision's id (the two are equal). The same decision has the same id at every endpoint.
sequenceIncreases with each new decision for the inspection. Keep the highest; never let an older one replace a newer one.
currentWhether this is the decision for the inspection's current result.
lending_eligibility_decisionaccepted, discounted, declined, needs_recapture or unable_to_assess, with reason_codes and reasons.
condition_assessment, identity, valuation, policy, input_hashCondition findings, VIN check, deduction and lending value, the policy version, and the evidence hash.

webhook.test

json
{
  "event": "webhook.test",
  "created_at": "2026-10-08T12:00:00.000Z",
  "data": { "message": "Test event from VerifyAI", "webhook_id": "3f0c9a7e-…" }
}

What's next

Get in Touch

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