[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


appointment-complete — Appointment Complete


appointment-created — Appointment Created

Fires when a new appointment is scheduled.

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

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

Sample Payload
{
    "object": "appointment",
    "id": 123456,
    "status": "rescheduled",
    "timestamp": 1680138900,
    "local_timestamp": 1680138900,
    "changes": {
        "status": {
            "from": "scheduled",
            "to": "rescheduled"
        },
        "timestamp": {
            "from": 1680135300,
            "to": 1680138900
        }
    }
}
Notes

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

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


reservation-submitted — Reservation Submitted


signer-added — Signer Added

Fires when a new signer is added to an appointment.

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

- 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