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/json
X-Event-Id
, X-Event-Type
, X-Event-Timestamp
, X-Event-Attempt
X-Signature: v1=<hex>
(HMAC_SHA256 of "timestamp.rawBody")X-Signature-Alg: HMAC-SHA256
X-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 Fires when an appointment is canceled by a user or the system.
{
"id": "evt_3a92d4ef",
"type": "appointment-canceled",
"occurred_at": "2024-06-22T12:15:30+00:00",
"webhook_version": "v1",
"api_version": "v3",
"attempt": 1,
"data": {
"object": "appointment",
"id": 823451
},
"meta": {
"tenant_id": "acme-co",
"source": "usvn",
"request_id": "req_APPTCANC1"
}
}
- Provides only the appointment `id` and object type.
- Downstream systems should reconcile and mark the appointment as canceled.
- Full appointment details are not included; query your system if more context is needed.
appointment-complete
— Appointment Complete Fires when an appointment has been successfully completed.
{
"id": "evt_cmpl12345",
"type": "appointment-complete",
"occurred_at": "2024-06-23T15:30:00+00:00",
"webhook_version": "v1",
"api_version": "v3",
"attempt": 1,
"data": {
"object": "appointment",
"id": 823451,
"status": "completed"
},
"meta": {
"tenant_id": "acme-co",
"source": "usvn",
"request_id": "req_APPTDONE1"
}
}
- Indicates the appointment is finalized — notarization or signing has been completed.
- Includes appointment `id` and final `status`.
- Downstream systems should reconcile and update their own records to reflect completion.
- Additional details (documents, signers, etc.) can be queried if needed via API.
appointment-created
— Appointment Created Fires whenever a new appointment is created in the system. This includes appointments created via the dashboard, API, or customer submission.
{
"id": "evt_8a7b23cd",
"type": "appointment-created",
"occurred_at": "2024-06-21T18:00:00+00:00",
"webhook_version": "v1",
"api_version": "v3",
"attempt": 1,
"data": {
"object": "appointment",
"id": 823451,
"first_name": "Emma",
"last_name": "Signwell",
"email": "esignwell@usvirtualnotary.com",
"status": "scheduled",
"timestamp": "2024-06-23T14:00:00+00:00",
"timezone": "America/New_York",
"service": "enotary",
"quantity": 823451,
"signing_method": "enotary",
"lang": "en"
},
"meta": {
"tenant_id": "acme-co",
"source": "usvn",
"request_id": "req_APPTCR8D"
}
}
- Triggered once, at the time the appointment is first scheduled.
- Provides core appointment details: ID, status, scheduled time, and associated reservation if applicable.
- Use `id` to reconcile in your system; future updates will be sent via `appointment-updated`.
- `timestamp` indicates when the appointment is set to occur (UTC+0).
- `timezone` indicates the user's preferred timezone.
appointment-updated
— Appointment Updated Fires when an existing appointment is modified (time, status, service, etc.). Receivers should compare the payload to their stored record to determine what changed.
{
"id": "evt_8a7b23cd",
"type": "appointment-updated",
"occurred_at": "2024-06-21T18:00:00+00:00",
"webhook_version": "v1",
"api_version": "v3",
"attempt": 1,
"data": {
"object": "appointment",
"id": 823451,
"first_name": "Emma",
"last_name": "Signwell",
"email": "esignwell@usvirtualnotary.com",
"status": "scheduled",
"timestamp": "2024-06-23T14:00:00+00:00",
"timezone": "America/New_York",
"service": "enotary",
"quantity": 823451,
"signing_method": "enotary",
"lang": "en"
},
"meta": {
"tenant_id": "acme-co",
"source": "usvn",
"request_id": "req_APPTCR8D"
}
}
- Includes the same fields as `appointment-created`; no explicit `changes` diff.
- May fire multiple times per appointment lifecycle.
- Use `id` as the stable primary key.
- `timestamp` is the scheduled time in UTC.
- Downstream systems should be idempotent and treat repeated deliveries as the same event.
document-deleted
— Document Deleted Fires when a document is deleted from a workspace. Includes the document ID and the parent object reference (object_type/object_id). File name, content type, size, and original uploaded_at are provided for context only—no file content is available.
{
"id": "evt_789",
"type": "document-deleted",
"occurred_at": "2024-06-21T15:22:13+00:00",
"webhook_version": "v1",
"api_version": "v3",
"attempt": 1,
"data": {
"object": "document",
"id": "45055",
"object_id": 123456,
"object_type": "reservation",
"filename": "passport_scan.pdf",
"content_type": "application/pdf",
"size_bytes": 238472,
"uploaded_at": "2024-06-21T15:22:10+00:00"
},
"meta": {
"tenant_id": "example",
"source": "usvn",
"request_id": "req_def456"
}
}
- Use data.id (document ID) as the canonical key to remove/flag the document in your system.
- The document may belong to different parent objects; check data.object_type (e.g., "reservation", "appointment") and data.object_id to reconcile relationships.
- The filename, content_type, size_bytes, and uploaded_at are informational (snapshotted metadata at the time of deletion) and may be omitted in rare cases.
- No file content is included or recoverable via this event.
- This event is idempotent: you may receive duplicates—deduplicate by the top-level "id" (X-Event-Id) or a (document ID + occurred_at) strategy.
- Ordering is not guaranteed across events; do not assume a prior "document-uploaded" will be delivered before this deletion event.
- If you soft-delete locally, consider retaining a tombstone keyed by data.id to prevent re-creation.
document-uploaded
— Document Uploaded Fires when a document is added to an appointment.
{
"id": "evt_789",
"type": "document-uploaded",
"occurred_at": "2024-06-21T15:22:13+00:00",
"webhook_version": "v1",
"api_version": "v3",
"attempt": 1,
"data": {
"object": "document",
"id": "45055",
"object_id": 123456,
"object_type": "reservation",
"filename": "passport_scan.pdf",
"mime_type": "application/pdf",
"size_bytes": 238472,
"uploaded_at": "2024-06-21T15:22:10+00:00"
},
"meta": {
"tenant_id": "example",
"source": "usvn",
"request_id": "req_def456"
}
}
- Fires whenever a participant or notary uploads a new document to an appointment.
- The payload includes metadata about the document (id, filename, size, type, uploader, uploaded_at).
- Only metadata is included in the webhook — the actual file contents are not transmitted.
- Use the id to fetch or reconcile the document in your system if you store references.
- Not all optional fields (e.g. uploader, size, type) are guaranteed; check for presence.
- Multiple documents may be uploaded over the lifecycle of an appointment; each upload triggers a separate event.
reservation-canceled
— Reservation Canceled Fires when a reservation is canceled by the user or system.
{
"id": "evt_2f9a7c1d3e",
"type": "reservation-canceled",
"occurred_at": "2024-06-21T15:22:13+00:00",
"webhook_version": "v1",
"api_version": "v3",
"attempt": 1,
"data": {
"object": "reservation",
"id": 735291,
"public_key": "xjD4hcqB1AnhOMhZd0Wiq5Dgbmha",
"status": "queued",
"created_at": "2024-06-21T15:22:10+00:00",
"service": "enotary",
"lang": "es",
"signing_method": "enotary",
"quantity": 2,
"unit_price": 25,
"total_price": 50,
"currency": "USD",
"document_ids": [
3,
4,
5,
7
],
"prepaid": false
},
"meta": {
"tenant_id": "acme-co",
"source": "usvn",
"request_id": "req_PQ7K4Z1M"
}
}
- 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 Fires when a new reservation record is first created in the system.
{
"id": "evt_2f9a7c1d3e",
"type": "reservation-created",
"occurred_at": "2024-06-21T15:22:13+00:00",
"webhook_version": "v1",
"api_version": "v3",
"attempt": 1,
"data": {
"object": "reservation",
"id": 735291,
"public_key": "xjD4hcqB1AnhOMhZd0Wiq5Dgbmha",
"status": "queued",
"created_at": "2024-06-21T15:22:10+00:00",
"service": "enotary",
"lang": "es",
"signing_method": "enotary",
"quantity": 2,
"unit_price": 25,
"total_price": 50,
"currency": "USD",
"document_ids": [
3,
4,
5,
7
],
"prepaid": false
},
"meta": {
"tenant_id": "acme-co",
"source": "usvn",
"request_id": "req_PQ7K4Z1M"
}
}
- Indicates the initial creation of a reservation before submission.
- Typically has status pending or draft.
- document_ids may be empty if documents haven’t yet been attached.
- Use id or public_key to reconcile with your system.
- Distinct from reservation-submitted, which signals the reservation is finalized and ready for processing.
reservation-submitted
— Reservation Submitted Triggered when a reservation is first submitted by a customer or agent.
{
"id": "evt_2f9a7c1d3e",
"type": "reservation-submitted",
"occurred_at": "2024-06-21T15:22:13+00:00",
"webhook_version": "v1",
"api_version": "v3",
"attempt": 1,
"data": {
"object": "reservation",
"id": 735291,
"public_key": "xjD4hcqB1AnhOMhZd0Wiq5Dgbmha",
"status": "queued",
"created_at": "2024-06-21T15:22:10+00:00",
"service": "enotary",
"lang": "es",
"signing_method": "enotary",
"quantity": 2,
"unit_price": 25,
"total_price": 50,
"currency": "USD",
"document_ids": [
3,
4,
5,
7
],
"prepaid": false
},
"meta": {
"tenant_id": "acme-co",
"source": "usvn",
"request_id": "req_PQ7K4Z1M"
}
}
- Fires once upon initial submission of a reservation.
- Contains identifiers to link the reservation in your system.
- Includes customer and reservation details if available at submission time.
- Subsequent updates (status changes, cancellations, etc.) will fire separate events.
- Use the `id` field to reconcile the reservation record in your system.
reservation-updated
— Reservation Updated Fired whenever a reservation’s fields are successfully changed (e.g., status, customer details, language, quantity). Emitted by both UI and API v3 updates.
{
"object": "reservation",
"id": 275839,
"status": "pending",
"service": "international",
"signing_method": "remote",
"quantity": 1,
"appointment_id": 123456,
"prepaid": false,
"customer": {
"first_name": "Alice",
"last_name": "Smith",
"email": "alice@example.com"
},
"lang": "en",
"keys": {
"appointment": "appt_AbCdEf123"
}
}
- Delivered when reservation meta changes via admin UI or API (POST /v3/reservations/{id}).
- Debounce: a single event is emitted per successful update call even if multiple fields changed.
- Best practice: treat updates as partial — re-fetch the full reservation if you need authoritative state.
- Idempotency: use the id from the envelope (X-Event-Id) to deduplicate events in your consumer.
signer-added
— Signer Added Fires when a new signer is added to an appointment or reservation.
{
"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,
"appointment_id": 245231,
"first_name": "Liam",
"last_name": "Notario",
"email": "liam.notario@example.com",
"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`).
- 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 Fires when an existing signer is removed from an appointment.
{
"id": "evt_92cd34ef",
"type": "signer-removed",
"occurred_at": "2024-06-21T17:10:00+00:00",
"webhook_version": "v1",
"api_version": "v3",
"attempt": 1,
"data": {
"object": "signer",
"id": 45231,
"appointment_id": 245231,
"removed_at": "2024-06-21T17:09:59+00:00"
},
"meta": {
"tenant_id": "acme-co",
"source": "usvn",
"request_id": "req_S1GN3RRMV"
}
}
Triggered immediately after the signer record is deleted or detached.
- Only minimal identifying information is included (`id` and `email`) to reconcile in your system.
- Once removed, the signer no longer has access to associated documents or signing actions.
- Use `id` to update your local records.
signer-updated
— Signer Updated Fires when a signer is updated.
{
"id": "evt_91ab23cd",
"type": "signer-updated",
"occurred_at": "2024-06-21T17:00:00+00:00",
"webhook_version": "v1",
"api_version": "v3",
"attempt": 1,
"data": {
"object": "signer",
"id": 45231,
"appointment_id": 245231,
"first_name": "Liam",
"last_name": "Notario",
"email": "liam.notario@example.com",
"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 updated.
- Includes signer details (`first_name`, `last_name`, `email`).
- 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.
video-deleted
— Video Deleted Fires when a previously uploaded video is deleted.
{
"id": "evt_e5f6g7h8",
"type": "video-deleted",
"occurred_at": "2024-06-21T16:10:00+00:00",
"webhook_version": "v1",
"api_version": "v3",
"attempt": 1,
"data": {
"object": "video",
"id": 99123,
"appointment_id": 735291,
"deleted_at": "2024-06-21T16:09:58+00:00",
"filename": "session_recording.mp4",
"mime_type": "video/mp4",
"size_bytes": 104857600
},
"meta": {
"tenant_id": "acme-co",
"source": "usvn",
"request_id": "req_ABC67890"
}
}
- Triggered if a video file is permanently removed from the system.
- Includes `id` and `object_type` to reconcile deletions.
- Clients should remove references to the video from their system.
- Deleted videos are no longer retrievable through the API.
video-uploaded
— Video Uploaded Fires when a new video recording is uploaded and linked to an appointment or reservation.
{
"id": "evt_a1b2c3d4",
"type": "video-uploaded",
"occurred_at": "2024-06-21T16:00:00+00:00",
"webhook_version": "v1",
"api_version": "v3",
"attempt": 1,
"data": {
"object": "video",
"id": 99123,
"appointment_id": 735291,
"filename": "session_recording.mp4",
"mime_type": "video/mp4",
"size_bytes": 104857600,
"uploaded_at": "2024-06-21T15:59:50+00:00"
},
"meta": {
"tenant_id": "acme-co",
"source": "usvn",
"request_id": "req_XYZ12345"
}
}
Triggered when a session video becomes available.
- Includes metadata like `filename`, `content_type`, `size_bytes`, and `uploaded_at`.
- Use `appointment_id` to link the video to its parent appointment.
- Clients can use the `id` to fetch/download the video through the API.