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.testevent 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:
| Header | Value |
|---|---|
Content-Type | application/json |
X-VerifyAI-Signature | t=<unix seconds>,v1=<hex>. v1 is HMAC-SHA256 of `${t}.${rawBody}`, keyed by the endpoint's signing secret. |
X-VerifyAI-Event | The event name. |
X-VerifyAI-Delivery | The 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 returns | We do |
|---|---|
Any 2xx within 5 seconds | Mark the delivery delivered. |
| Anything else, a timeout, or a network error | Retry 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
| Event | Fires when |
|---|---|
verification.completed | A photo check finishes, compliant or not, including each photo taken through an inspection link. |
inspection.submitted | An inspection link is submitted and processed (Pickup comparison, PDF and outcome included). |
inspection.reviewed | A decision is recorded on a submitted inspection, from the dashboard or POST /api/v1/inspection-sessions/{id}/review. |
inspection.decision | A lending inspection gets its automatic decision, or a new one after a correction or a late reference value. |
webhook.test | Send test event is clicked for an endpoint. Sent to that endpoint only. |
verification.completed
{
"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>"
}
}| Field | Description |
|---|---|
id, created_at | The verification. |
status | Processing status. |
is_compliant, confidence | The verdict and its confidence (0–1). |
policy | The policy id the photo was checked against. |
category | The policy category the photo was classified into. |
violation_reasons, feedback | Failed criteria and the plain-language feedback. |
warning_violation_reasons, warning_feedback | Non-blocking criteria that failed. |
damage_findings, overall_severity | Only 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. |
readings | Only when the policy reads values off the photo, e.g. { "odometer": 18766, "odometer_unit": "mi", "fuel_level": "1/2" }. |
metadata | What you sent. Inspection photos add inspection_session_id, inspection_slot, and the inspection's context. |
image_url | Signed 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.
| Field | Description |
|---|---|
id, object | The inspection id and "inspection". |
status | submitted for these events. |
stage | delivery, pickup, or null. |
reference_id | Your reference, if you set one. |
template | e.g. vehicle_handoff, or null. |
vehicle, context | What you sent when creating the inspection (context also gets reference_id, stage and the VIN). |
created_at, opened_at, submitted_at, expires_at | Timestamps. |
outcome | clear or review. |
outcome_reasons | Why, in plain language. Empty when clear. |
views_required, views_captured | Required 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. |
comparison | null 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_url | Signed link to the condition-report PDF, or null if it could not be rendered. |
signature | Whether 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.
| Field | Description |
|---|---|
inspection_contract_version | 2. |
inspection_id, reference_id, correlation_id | The inspection and your identifiers for it. |
event_id, decision_id | The decision's id (the two are equal). The same decision has the same id at every endpoint. |
sequence | Increases with each new decision for the inspection. Keep the highest; never let an older one replace a newer one. |
current | Whether this is the decision for the inspection's current result. |
lending_eligibility_decision | accepted, discounted, declined, needs_recapture or unable_to_assess, with reason_codes and reasons. |
condition_assessment, identity, valuation, policy, input_hash | Condition findings, VIN check, deduction and lending value, the policy version, and the evidence hash. |
webhook.test
{
"event": "webhook.test",
"created_at": "2026-10-08T12:00:00.000Z",
"data": { "message": "Test event from VerifyAI", "webhook_id": "3f0c9a7e-…" }
}What's next
- Setting up webhooks — verify signatures, handle retries, and ship a production handler.
- Sending an inspection link —
the flow behind the
inspection.*events. - Concepts: Verifications — the object
behind
verification.completed.