VerifyMX Docs

Webhooks

Event types, payload format, signature verification, retry schedule.

VerifyMX delivers real-time events to your HTTPS endpoint via HTTP POST. Webhooks eliminate the need to poll the API for verification results.


Event Types

EventWhen it fires
verification.createdA new verification session is created
verification.updatedAny field on the verification changes
verification.completedAll workflow steps have finished executing
verification.approvedVerification status set to APPROVED
verification.rejectedVerification status set to REJECTED
verification.manual_reviewVerification flagged for human review
step.completedA single workflow step finishes successfully
step.failedA single workflow step fails

Payload Format

Every event is delivered as a JSON object with a consistent envelope:

{
  "id": "evt_m5k2j9a_x4b8c",
  "event": "verification.approved",
  "createdAt": "2026-04-08T12:01:00.000Z",
  "data": {
    "verificationId": "ver_01HXYZ...",
    "previousStatus": "IN_PROGRESS",
    "newStatus": "APPROVED"
  }
}

Envelope fields:

FieldTypeDescription
idstringUnique delivery ID (evt_ prefix). Use for deduplication.
eventstringEvent type
createdAtstringISO 8601 timestamp when the event was generated
dataobjectEvent-specific payload (see per-event shapes below)

Per-Event Data Shapes

verification.created

{
  "verificationId": "ver_01HXYZ...",
  "workflowId": "wf_01HXYZ...",
  "status": "PENDING"
}

verification.approved / verification.rejected / verification.manual_review

{
  "verificationId": "ver_01HXYZ...",
  "previousStatus": "IN_PROGRESS",
  "newStatus": "APPROVED"
}

step.completed / step.failed

{
  "verificationId": "ver_01HXYZ...",
  "stepId": "vs_01...",
  "stepType": "DOCUMENT_OCR",
  "status": "PASSED"
}

Request Headers

Every webhook POST includes these headers:

HeaderDescription
Content-Typeapplication/json
X-VerifyMX-EventEvent type (e.g. verification.approved)
X-VerifyMX-SignatureHMAC-SHA256 signature — see below
X-VerifyMX-DeliveryDelivery attempt ID (for logging)
User-AgentVerifyMX-Webhooks/1.0

Signature Verification

Every payload is signed with HMAC-SHA256 using the signing secret you received when creating the webhook endpoint.

Signature format:

X-VerifyMX-Signature: sha256=<hex_digest>

Algorithm:

  1. Read the raw request body as a UTF-8 string (do not parse it first)
  2. Compute HMAC-SHA256(body, secret)
  3. Compare with X-VerifyMX-Signature using a constant-time comparison

Node.js

const crypto = require('crypto');

function verifyWebhookSignature(rawBody, secret, signatureHeader) {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(rawBody, 'utf8')
    .digest('hex');

  const expectedBuf = Buffer.from(expected, 'utf8');
  const actualBuf   = Buffer.from(signatureHeader, 'utf8');

  if (expectedBuf.length !== actualBuf.length) return false;
  return crypto.timingSafeEqual(expectedBuf, actualBuf);
}

// Express example
app.post('/webhooks/verifymx', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['x-verifymx-signature'];
  if (!verifyWebhookSignature(req.body, process.env.VERIFYMX_WEBHOOK_SECRET, sig)) {
    return res.status(401).send('Invalid signature');
  }

  const event = JSON.parse(req.body);
  console.log('Event:', event.event);

  res.status(200).send('ok');
});

Python

import hashlib
import hmac

def verify_webhook_signature(raw_body: bytes, secret: str, signature_header: str) -> bool:
    expected = 'sha256=' + hmac.new(
        secret.encode('utf-8'),
        raw_body,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature_header)

# FastAPI example
from fastapi import Request, HTTPException

@app.post('/webhooks/verifymx')
async def webhook_handler(request: Request):
    raw_body = await request.body()
    sig = request.headers.get('x-verifymx-signature', '')

    if not verify_webhook_signature(raw_body, WEBHOOK_SECRET, sig):
        raise HTTPException(status_code=401, detail='Invalid signature')

    event = await request.json()
    print(f"Event: {event['event']}")
    return {'ok': True}

Go

package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "fmt"
    "io"
    "net/http"
)

func verifyWebhookSignature(body []byte, secret, signatureHeader string) bool {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(body)
    expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(expected), []byte(signatureHeader))
}

func webhookHandler(w http.ResponseWriter, r *http.Request) {
    body, err := io.ReadAll(r.Body)
    if err != nil {
        http.Error(w, "Bad request", http.StatusBadRequest)
        return
    }

    sig := r.Header.Get("X-VerifyMX-Signature")
    if !verifyWebhookSignature(body, webhookSecret, sig) {
        http.Error(w, "Invalid signature", http.StatusUnauthorized)
        return
    }

    fmt.Println("Verified webhook:", r.Header.Get("X-VerifyMX-Event"))
    w.WriteHeader(http.StatusOK)
}

Retry Schedule

If your endpoint returns a non-2xx response or times out (10 second timeout), the delivery is retried with exponential backoff:

AttemptDelay
1Immediate
210 seconds
31 minute
45 minutes
530 minutes

After 5 failed attempts, the delivery is marked permanently failed. No further retries occur.

Best practice: Return 200 OK immediately after signature validation, then process the event asynchronously. This prevents timeouts from causing spurious retries.


Deduplication

Use the id field in the envelope (evt_...) to deduplicate deliveries. The same event may be delivered more than once if your endpoint returns a non-2xx status or times out on attempt N but the delivery actually succeeded.

Store the id in a short-lived cache (Redis SET NX EX 86400) and skip processing if the key already exists.


Testing

Use POST /v1/webhooks/:id/test to send a synthetic verification.created event to your endpoint:

curl -X POST "https://api.verifymx.xyz/v1/webhooks/whe_01HXYZ.../test" \
  -H "Authorization: Bearer $TOKEN"

The test payload includes "test": true in the data field so your handler can identify and skip it in production logic.


Registering a Webhook

curl -X POST "https://api.verifymx.xyz/v1/webhooks" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://myapp.mx/hooks/verifymx",
    "events": [
      "verification.approved",
      "verification.rejected",
      "verification.manual_review"
    ]
  }'

Store the returned secret securely. It is not retrievable after creation — you must delete and recreate the endpoint to rotate the secret.

On this page