Webhooks

Vindex tells your server when something happens: a customer submitted, Vindex needs more from them, a decision was made, a report is ready. Every delivery is signed, so you know it came from Vindex.

Set up an endpoint

  1. 1

    Build a route on your server that accepts POST requests with a JSON body, for example https://acme.example/webhooks/vindex.

  2. 2

    In the Developers console, under Webhooks, add the URL and choose the events it should receive (or all of them). Each mode has its own endpoints.

  3. 3

    Copy the endpoint's signing secret (whsec_…) into your server's secrets, then press Send test event to see a delivery arrive.

Live endpoints
https:// on the standard port (443), to a host name, never an IP address.
Test endpoints
https:// on any port. To reach your laptop while you build, use a tunnel such as ngrok.
Never allowed
Private, internal and cloud-metadata addresses, and URLs with a user name or password in them.

Verify the signature

Vindex signs with Standard Webhooks, so a ready-made library in your language does the checking. Each delivery has three headers:

A delivery
POST /webhooks/vindex HTTP/1.1
Content-Type: application/json
webhook-id: evt_0vZ3kq9XfJ1mB7c2Lr8sTy
webhook-timestamp: 1762511719
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=

{"id":"evt_0vZ3kq9XfJ1mB7c2Lr8sTy","object":"event","type":"verification.decided",...}
webhook-id
The event's id. The same on every retry of that event, so use it to skip duplicates.
webhook-timestamp
When this attempt was signed, in Unix seconds. Refuse anything more than 5 minutes off.
webhook-signature
v1, and a base64 HMAC-SHA256 of {id}.{timestamp}.{body} made with your secret. While you rotate the secret there are two, separated by a space; accept the delivery if either matches.
# pip install standardwebhooks
import os
from standardwebhooks.webhooks import Webhook

webhook = Webhook(os.environ["VINDEX_WEBHOOK_SECRET"].removeprefix("whsec_"))

# Flask shown; any framework works. Read the body as raw bytes, before any JSON parsing.
@app.post("/webhooks/vindex")
def vindex_webhook():
    try:
        event = webhook.verify(request.get_data(), request.headers)
    except Exception:
        return "", 400  # wrong signature or too old: ignore it
    queue.enqueue(handle_event, event)  # do the work later
    return "", 204  # answer quickly
› Without a library

The same check by hand. Compare signatures in constant time, and use the raw body.

import base64
import hashlib
import hmac
import time

def verify(secret: str, headers, body: bytes, tolerance: int = 300) -> None:
    """Raise unless the delivery is signed with this secret and fresh."""
    message_id = headers["webhook-id"]
    timestamp = headers["webhook-timestamp"]
    if abs(time.time() - int(timestamp)) > tolerance:
        raise ValueError("timestamp outside the tolerance")
    key = base64.b64decode(secret.removeprefix("whsec_"))
    signed = f"{message_id}.{timestamp}.".encode() + body
    expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
    for signature in headers["webhook-signature"].split(" "):
        version, _, value = signature.partition(",")
        if version == "v1" and hmac.compare_digest(value, expected):
            return
    raise ValueError("no valid signature")

Retries and failures

A delivery succeeds when your endpoint answers 2xx within 15 seconds. Anything else (an error, a timeout, a redirect) counts as a failure and is retried:

  1. 1Immediately
  2. 25 seconds later
  3. 35 minutes later
  4. 430 minutes later
  5. 52 hours later
  6. 65 hours later
  7. 710 hours later
  8. 810 hours later
  9. 924 hours later
  10. 1024 hours later

In live mode that is ten attempts over about three days. Waits vary by up to 20% so retries don't arrive in a burst. In test mode only the first four are made.

Redirects are not followed: give the final URL. If your endpoint answers 429 or 503 with Retry-After, Vindex waits at least that long. 410 Gone turns the endpoint off at once.

Failing for an hour
Vindex emails your team's owners, admins and developers, and marks the endpoint in the console.
Failing for 3 days
A second email.
No success for 5 days
The endpoint is turned off, and you get an email saying so.
Recovering
Fix the endpoint, turn it back on, and use Resend failed events in the console, or read what you missed from GET /v1/events (kept 30 days).

Handle events safely

  • Answer first, work later. Verify, queue the event, return 204. Slow handlers time out and get retried.
  • Expect duplicates. Delivery is at least once. Skip a webhook-id you have already handled.
  • Expect any order. Events can arrive out of order. Read the object with your key and act on its current status, not on the event alone.
  • No personal data. Events are thin: ids, statuses and your references. Fetch details with your key when you need them.
  • Check the mode. livemode tells test events from live ones.

Rotate the secret

Roll secret in the console gives you a new secret. For the overlap you choose (none, an hour, or up to 24 hours) Vindex signs every delivery with both, so you can update your servers without missing anything. Libraries accept a delivery if any signature matches. Every reveal and roll is written to your account's audit log.

Event catalogue

Every event has the same envelope; data describes the object. Full examples for each type are in the API reference.

verification.decided
{
  "id": "evt_0vZ3kq9XfJ1mB7c2Lr8sTy",
  "object": "event",
  "type": "verification.decided",
  "livemode": true,
  "created_at": "2026-11-05T10:24:51.204Z",
  "data": {
    "id": "ver_0vZ3kq9XfJ1mB7c2Lr8sTy",
    "object": "verification",
    "type": "individual",
    "status": "decided",
    "stage": "decided",
    "outcome": "approved",
    "risk_rating": "low",
    "external_reference": "crm-42"
  }
}

Verifications

verification.created
A verification was created and its hosted link issued.
verification.started
The customer opened the hosted link and started.
verification.submitted
The customer submitted; Vindex review has started.
verification.action_required
Vindex needs more from the customer. Send them their hosted link again.
verification.resubmitted
The customer answered Vindex's request; review continues.
verification.decided
Vindex reached its outcome: approved, approved with conditions, or rejected.
verification.expired
The hosted link expired before the customer submitted. Held credits were released.
verification.canceled
The verification was canceled. Held credits were released.
report.ready
A report PDF (customer report or outcome letter) can be downloaded.

Screenings

screening_batch.completed
Every subject in a batch screening has been screened.

Billing

billing.credits_purchased
Credits were added to your live balance after a payment.
billing.balance_low
Your live balance dropped to or below your alert threshold.
billing.credits_exhausted
Your live balance reached zero; paid calls now return 402.
billing.credits_expiring
Some credits expire within the next 30 days.
billing.auto_recharge_failed
An automatic top-up failed; auto-recharge is paused.

Account

api_key.expiring
An API key expires within 7 days.

See every delivery

The console's webhook page shows each attempt with the status your endpoint answered, how long it took and the first part of its reply, and lets you retry one now. Calling https://api.vindexgrid.com/v1/events with your key lists what was sent.

Something missing or unclear? Signed-in users can tell us from Feedback in the app.