Realtime notifications for key events in your USVN account.
Respond with any 2xx within ~10s to acknowledge.
Each request is signed (HMAC‑SHA256) with your endpoint secret. Verify both the signature and timestamp.
Content-Type: application/jsonX-Event-Id, X-Event-Type, X-Event-Timestamp, X-Event-AttemptX-Signature: v1=<hex> (HMAC_SHA256 of "timestamp.rawBody")X-Signature-Alg: HMAC-SHA256X-Webhook-Version: v1, X-Api-Version: v3<?php
$secret = "YOUR_ENDPOINT_SECRET";
$ts = $_SERVER["HTTP_X_EVENT_TIMESTAMP"] ?? "";
$sig = $_SERVER["HTTP_X_SIGNATURE"] ?? "";
$raw = file_get_contents("php://input");
if (!preg_match("/^v1=([a-f0-9]{64})$/i", $sig, $m)) { http_response_code(400); exit; }
$hex = $m[1];
if (abs(time() - (int)$ts) > 300) { http_response_code(400); exit; }
$mac = hash_hmac("sha256", $ts . "." . $raw, $secret);
if (!hash_equals($mac, $hex)) { http_response_code(400); exit; }
http_response_code(200); echo "ok";
const express = require("express");
const crypto = require("crypto");
const app = express();
app.post("/webhooks", express.raw({ type: "application/json" }), (req, res) => {
const secret = process.env.WEBHOOK_SECRET;
const ts = req.header("X-Event-Timestamp");
const sig = (req.header("X-Signature") || "").replace(/^v1=/i,"");
if (!ts || !/^[a-f0-9]{64}$/i.test(sig)) return res.sendStatus(400);
if (Math.abs(Date.now()/1000 - Number(ts)) > 300) return res.sendStatus(400);
const mac = crypto.createHmac("sha256", secret)
.update(ts + "." + req.body)
.digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(mac,"hex"), Buffer.from(sig,"hex"))) {
return res.sendStatus(400);
}
const payload = JSON.parse(req.body.toString("utf8"));
// handle payload...
res.send("ok");
});
from django.http import HttpResponse, HttpResponseBadRequest
import hmac, hashlib, time, json
def webhook(request):
secret = b"YOUR_ENDPOINT_SECRET"
ts = request.META.get("HTTP_X_EVENT_TIMESTAMP")
sig = (request.META.get("HTTP_X_SIGNATURE") or "").replace("v1=", "")
if not ts or len(sig) != 64: return HttpResponseBadRequest()
if abs(int(time.time()) - int(ts)) > 300: return HttpResponseBadRequest()
mac = hmac.new(secret, (ts + ".").encode() + request.body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(mac, sig): return HttpResponseBadRequest()
payload = json.loads(request.body.decode("utf-8"))
return HttpResponse("ok")
All events share a common top‑level envelope. The "data" field contains the event‑specific payload.
{
"id": "evt_123",
"type": "appointment-complete",
"occurred_at": "2024-06-21T12:34:56Z",
"webhook_version": "v1",
"api_version": "v3",
"attempt": 1,
"data": {
"object": "appointment",
"id": 123456,
"status": "completed"
},
"meta": {
"tenant_id": "example",
"source": "usvn.wp",
"request_id": "req_abc123"
}
}
Deduplicate using the envelope id (also sent as X-Event-Id). Multiple deliveries may occur—treat them as the same event.
We avoid breaking changes in v1. If necessary, a new webhook version (v2) will be introduced with a migration path.
appointment-canceled — Appointment Canceled appointment-complete — Appointment Complete appointment-created — Appointment Created Fires when a new appointment is scheduled.
{
"object": "appointment",
"id": 123456,
"status": "scheduled",
"service": "enotary",
"signing_method": "enotary",
"signer_count": 1,
"quantity": 1,
"timezone": -4,
"timestamp": 1680135300,
"local_timestamp": 1680135300,
"lang": "en",
"signers": [
{
"first_name": "Marty",
"last_name": "McFly",
"email": "marty@example.com"
}
],
"documents": [],
"videos": []
}
- Triggered immediately after appointment creation.
- id is the appointment’s unique identifier.
- signers, documents, and videos arrays may be empty at creation time.
- Use status to determine initial state (usually scheduled).
appointment-updated — Appointment Updated Fires when an existing appointment’s details are updated (e.g., status change, reschedule).
{
"object": "appointment",
"id": 123456,
"status": "rescheduled",
"timestamp": 1680138900,
"local_timestamp": 1680138900,
"changes": {
"status": {
"from": "scheduled",
"to": "rescheduled"
},
"timestamp": {
"from": 1680135300,
"to": 1680138900
}
}
}
- Fires for any significant update: status, schedule time, or service type.
- The changes object shows “before” and “after” values for modified fields.
- Not all fields are guaranteed in every update; check what’s present.
- Use id to reconcile the appointment record in your system.
document-deleted — Document Deleted document-uploaded — Document Uploaded reservation-canceled — Reservation Canceled Fires when a reservation is canceled before or during processing.
- Triggered if a reservation is explicitly canceled by the user or system.
- Once canceled, the reservation cannot be submitted or processed further.
- Check `canceled_at` timestamp to reconcile in your system.
- Use `id` or `public_key` to identify the reservation.
- Associated `document_ids` may still exist but are no longer active for this reservation.
reservation-created — Reservation Created reservation-submitted — Reservation Submitted signer-added — Signer Added Fires when a new signer is added to an appointment.
{
"id": "evt_91ab23cd",
"type": "signer-added",
"occurred_at": "2024-06-21T17:00:00+00:00",
"webhook_version": "v1",
"api_version": "v3",
"attempt": 1,
"data": {
"object": "signer",
"id": 45231,
"first_name": "Liam",
"last_name": "Notario",
"email": "liam.notario@example.com",
"id_approval": {
"auto": "approved",
"human": "approved"
},
"created_at": "2024-06-21T16:59:58+00:00"
},
"meta": {
"tenant_id": "acme-co",
"source": "usvn",
"request_id": "req_S1GN3RADD"
}
}
- Triggered immediately after a signer record is created.
- Includes signer details (`first_name`, `last_name`, `email`, `id_approval`).
- Use the `id` to reference this signer in future webhook events or via the REST API.
- Not all fields are guaranteed; consumers should handle missing optional attributes.
signer-removed — Signer Removed video-deleted — Video Deleted video-uploaded — Video Uploaded