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
| Event | When it fires |
|---|---|
verification.created | A new verification session is created |
verification.updated | Any field on the verification changes |
verification.completed | All workflow steps have finished executing |
verification.approved | Verification status set to APPROVED |
verification.rejected | Verification status set to REJECTED |
verification.manual_review | Verification flagged for human review |
step.completed | A single workflow step finishes successfully |
step.failed | A 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:
| Field | Type | Description |
|---|---|---|
id | string | Unique delivery ID (evt_ prefix). Use for deduplication. |
event | string | Event type |
createdAt | string | ISO 8601 timestamp when the event was generated |
data | object | Event-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:
| Header | Description |
|---|---|
Content-Type | application/json |
X-VerifyMX-Event | Event type (e.g. verification.approved) |
X-VerifyMX-Signature | HMAC-SHA256 signature — see below |
X-VerifyMX-Delivery | Delivery attempt ID (for logging) |
User-Agent | VerifyMX-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:
- Read the raw request body as a UTF-8 string (do not parse it first)
- Compute
HMAC-SHA256(body, secret) - Compare with
X-VerifyMX-Signatureusing 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:
| Attempt | Delay |
|---|---|
| 1 | Immediate |
| 2 | 10 seconds |
| 3 | 1 minute |
| 4 | 5 minutes |
| 5 | 30 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.