[gtranslate]

US Virtual Notary - Developers Guide: Webhooks

Realtime notifications for key events in your USVN account.

Delivery Model & Retries

  • At‑least‑once delivery. Ordering is not guaranteed.
  • We retry on network errors, HTTP 429, and 5xx.
  • Backoff schedule (max 6 attempts): 1m → 5m → 30m → 2h → 6h → 24h.
  • First attempt may be sent synchronously (short timeout).

Respond with any 2xx within ~10s to acknowledge.

Security & Verification

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 verification example

<?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";

Node (Express) verification example

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");
});

Python (Django) verification example

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")

Envelope & Schema

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"
    }
}

Event Naming & Versioning

  • Event keys are dash‑case (e.g., "appointment-created").
  • Webhook contract is versioned independently via X-Webhook-Version (v1).
  • API version is included via X-Api-Version (v3).

Idempotency & Duplicates

Deduplicate using the envelope id (also sent as X-Event-Id). Multiple deliveries may occur—treat them as the same event.

Response Expectations

  • Return 2xx to acknowledge.
  • Return 4xx only if your endpoint will never accept the event in that form.
  • 5xx or timeout → we retry per the backoff schedule.

Limits & Timeouts

  • First attempt uses a short request timeout (~2s).
  • Retries use a request timeout of ~10s.
  • Retry delays follow the backoff schedule (1m → 5m → 30m → 2h → 6h → 24h).
  • Keep your handler quick and offload heavy work to background jobs.

Managing Endpoints & Secrets

  • Secret is shown once at creation—store it securely.
  • Rotate by creating a new endpoint, updating your consumer, then deleting the old endpoint.
  • HTTPS is required.

Breaking Change Policy

We avoid breaking changes in v1. If necessary, a new webhook version (v2) will be introduced with a migration path.

Events

appointment-canceled — Appointment Canceled

Fires when an appointment is canceled by a user or the system.

Sample Payload
{
    "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"
    }
}
Notes

- 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.

Sample Payload
{
    "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"
    }
}
Notes

- 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.

Sample Payload
{
    "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"
    }
}
Notes

- 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.

Sample Payload
{
    "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"
    }
}
Notes

- 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.

Sample Payload
{
    "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"
    }
}
Notes

- 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.

Sample Payload
{
    "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"
    }
}
Notes

- 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.

Sample Payload
{
    "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"
    }
}
Notes

- 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.

Sample Payload
{
    "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"
    }
}
Notes

- 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.

Sample Payload
{
    "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"
    }
}
Notes

- 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.

Sample Payload
{
    "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"
    }
}
Notes

- 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.

Sample Payload
{
    "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"
    }
}
Notes

- 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.

Sample Payload
{
    "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"
    }
}
Notes

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.

Sample Payload
{
    "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"
    }
}
Notes

- 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.

Sample Payload
{
    "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"
    }
}
Notes

- 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.

Sample Payload
{
    "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"
    }
}
Notes

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.