NxVET Partner Webhook Integration

HMAC-Signed Events for NxHUB (Ambient Conversations) & NxSCOPE (TPR + Audio)

Last updated: December 15, 2025

Overview

This guide explains how NxVET delivers webhook events to your system, how to verify each request using HMAC signing, and what payloads to expect from NxHUB and NxSCOPE devices.

1) Partner Onboarding and Setup

1.1 What the Partner Provides to NxVET

To enable webhook delivery, the partner provides:

  • Webhook URL (webhook_url)
    Your HTTPS endpoint where NxVET will POST events.
    Example: https://partner.example.com/webhooks/nxvet
  • (Optional) Requested event types
    Example: record.created, measurement.created
Note: NxVET supports a limited set of allowlisted partners. Partner setup is coordinated with NxVET (not self-serve).

1.2 What NxVET Provides to the Partner (One Time)

NxVET provisions the integration and shares securely:

  • Partner ID (partner_id)
    Example: prt_12345
  • HMAC Secret (hmac_secret)
    Shared secret used to verify webhook authenticity.
Important: NxVET generates this secret and provides it one time only. If it's lost, NxVET will rotate and issue a new one.

2) How NxVET Identifies the End User

Each NxVET customer links their NxVET account to your system using a Partner Identifier:

  • Partner Identifier (partner_user_key): An identifier you provide to the user that uniquely maps to their account in your system.
Security Requirement: partner_user_key must be opaque and unguessable (e.g., random string / UUID). Do not use an email address, clinic name, or any predictable identifier.

The user enters it in NxVET → Settings → Integrations → Your Partner.

NxVET includes partner_user_key in every webhook so you can route events to the correct customer/account.

Handling Invalid Identifiers

If NxVET sends an event with a partner_user_key that your system does not recognize (or is not authorized), your webhook endpoint should return an appropriate non-2xx response (e.g., 400 or 404) and include a short error message. NxVET will record the failure and retry according to policy.

3) Device Overview

NxHUB — Continuous Ambient Recording

  • Always-on ambient recording in the clinic environment
  • Sends an event when NxVET detects a conversation is complete
  • Typically results in a "record created" workflow
  • May include optional transcript and SOAP content

NxSCOPE — TPR Measurement Device + Audio

  • Captures Temperature / Pulse / Respiration (TPR)
  • May produce two WAV audio artifacts:
    • stethoscope_wav (stethoscope channel)
    • voice_wav (voice channel)
  • Sends an event when a measurement is captured and uploaded

4) Webhook Request Format

NxVET sends webhook events as HTTP POST requests to your webhook_url.

4.1 Headers

Each request includes:

X-Nxvet-Partner-Id: <partner_id>
X-Nxvet-Timestamp: <unix_seconds>
X-Nxvet-Signature: <hex_hmac_sha256>

4.2 Body (JSON)

The request body is JSON with the following required top-level fields:

  • event_id (string) — unique identifier; use for deduplication
  • event_type (string) — e.g., record.created, measurement.created
  • occurred_at (string) — ISO 8601 UTC timestamp
  • partner_id (string)
  • partner_user_key (string)
  • device_id (string)
  • device_friendly_name (string)
  • signed_urls (array) — one or more signed URLs, each valid for 1 hour
  • data (object) — event-specific content

Optional top-level fields (based on vendor agreement):

  • transcript (string, optional)
  • soap (string, optional)

5) Field Definitions

5.1 Top-Level Fields

Field Type Required Constraints / Notes
event_id string Unique per event. Max 128 chars. Deduplicate on this.
event_type string Max 64 chars. Examples: record.created, measurement.created
occurred_at string ISO 8601 UTC (e.g., 2025-12-15T14:03:22Z)
partner_id string Provided by NxVET
partner_user_key string Partner-provided, opaque/unguessable identifier. Max 256 chars.
device_id string NxVET device identifier. Max 128 chars.
device_friendly_name string Human-readable label. Max 128 chars.
signed_urls array One or more artifacts. Each URL is valid for 1 hour.
transcript string ⚠️ Optional (agreement). May be omitted or present as "".
soap string ⚠️ Optional (agreement). May be omitted or present as "".
data object Event-specific payload

5.2 signed_urls[] Schema

Each item in signed_urls:

Field Type Required Notes
type string Describes the artifact (see recommended types below)
url string HTTPS signed URL. Typically expires in 3600 seconds.
content_type string e.g., audio/wav, application/pdf
expires_in_seconds number Typically 3600

Recommended type values:

  • NxHUB: hub_audio_wav
  • NxSCOPE: stethoscope_wav, voice_wav

Partners should ignore unknown type values gracefully.

6) Event Types

NxVET will confirm which events are enabled for your integration. Typical events include:

  • record.created (usually from NxHUB conversation completion)
  • record.updated (optional/future if enabled)
  • measurement.created (usually from NxSCOPE TPR capture)

9) HMAC Signature Specification

Required: You must verify every request to ensure authenticity.

9.1 Message to Sign

Compute signature over:

<timestamp>.<raw_body>

Where:

  • <timestamp> is X-Nxvet-Timestamp
  • <raw_body> is the exact raw request body bytes (before parsing)

9.2 Signature

Compute:

signature = HEX( HMAC_SHA256(hmac_secret, message) )

Compare to X-Nxvet-Signature using constant-time compare.

9.3 Replay Protection (Required)

Reject requests outside a time window (recommended ±5 minutes).

Code Examples

const crypto = require('crypto');

function verifyWebhook(req, hmacSecret) {
    const timestamp = req.headers['x-nxvet-timestamp'];
    const signature = req.headers['x-nxvet-signature'];
    const rawBody = req.rawBody; // Get raw body bytes

    // 1. Verify timestamp freshness (±5 minutes)
    const currentTime = Math.floor(Date.now() / 1000);
    if (Math.abs(currentTime - parseInt(timestamp)) > 300) {
        throw new Error('Timestamp too old or too far in future');
    }

    // 2. Compute HMAC
    const message = `${timestamp}.${rawBody}`;
    const computedSignature = crypto
        .createHmac('sha256', hmacSecret)
        .update(message)
        .digest('hex');

    // 3. Constant-time compare
    if (!crypto.timingSafeEqual(
        Buffer.from(signature),
        Buffer.from(computedSignature)
    )) {
        throw new Error('Invalid signature');
    }

    return true;
}
import hmac
import hashlib
import time

def verify_webhook(request, hmac_secret):
    timestamp = request.headers.get('X-Nxvet-Timestamp')
    signature = request.headers.get('X-Nxvet-Signature')
    raw_body = request.body  # Get raw body bytes

    # 1. Verify timestamp freshness (±5 minutes)
    current_time = int(time.time())
    if abs(current_time - int(timestamp)) > 300:
        raise ValueError('Timestamp too old or too far in future')

    # 2. Compute HMAC
    message = f"{timestamp}.{raw_body.decode('utf-8')}"
    computed_signature = hmac.new(
        hmac_secret.encode('utf-8'),
        message.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()

    # 3. Constant-time compare
    if not hmac.compare_digest(signature, computed_signature):
        raise ValueError('Invalid signature')

    return True
using System;
using System.Security.Cryptography;
using System.Text;

public bool VerifyWebhook(HttpRequest request, string hmacSecret)
{
    var timestamp = request.Headers["X-Nxvet-Timestamp"];
    var signature = request.Headers["X-Nxvet-Signature"];
    var rawBody = ReadRawBody(request);

    // 1. Verify timestamp freshness (±5 minutes)
    var currentTime = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
    if (Math.Abs(currentTime - long.Parse(timestamp)) > 300)
    {
        throw new Exception("Timestamp too old or too far in future");
    }

    // 2. Compute HMAC
    var message = $"{timestamp}.{rawBody}";
    using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(hmacSecret));
    var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(message));
    var computedSignature = BitConverter.ToString(hash)
        .Replace("-", "").ToLower();

    // 3. Constant-time compare
    if (!CryptographicOperations.FixedTimeEquals(
        Encoding.UTF8.GetBytes(signature),
        Encoding.UTF8.GetBytes(computedSignature)))
    {
        throw new Exception("Invalid signature");
    }

    return true;
}

7) Workflow Examples

7.1 NxHUB Workflow: Conversation Complete → record.created

{
  "event_id": "evt_20251215_0001001",
  "event_type": "record.created",
  "occurred_at": "2025-12-15T15:10:02Z",
  "partner_id": "prt_12345",
  "partner_user_key": "c9f6b9d8-7f1b-4cfb-9f83-2bf2f1a2c3d4",
  "device_id": "hub_00112233",
  "device_friendly_name": "NxHUB - Front Desk",
  "signed_urls": [
    {
      "type": "hub_audio_wav",
      "url": "https://storage.example.com/signed?...",
      "content_type": "audio/wav",
      "expires_in_seconds": 3600
    }
  ],
  "transcript": "Optional transcript text...",
  "soap": "Optional SOAP note text...",
  "data": {
    "source_device_type": "NxHUB",
    "record_id": "rec_123",
    "patient_id": "pat_456"
  }
}

7.2 NxSCOPE Workflow: TPR Captured (+ Audio) → measurement.created

{
  "event_id": "evt_20251215_0002009",
  "event_type": "measurement.created",
  "occurred_at": "2025-12-15T15:18:41Z",
  "partner_id": "prt_12345",
  "partner_user_key": "c9f6b9d8-7f1b-4cfb-9f83-2bf2f1a2c3d4",
  "device_id": "scope_778899",
  "device_friendly_name": "NxSCOPE - Exam Room 1",
  "signed_urls": [
    {
      "type": "stethoscope_wav",
      "url": "https://storage.example.com/signed?...",
      "content_type": "audio/wav",
      "expires_in_seconds": 3600
    },
    {
      "type": "voice_wav",
      "url": "https://storage.example.com/signed?...",
      "content_type": "audio/wav",
      "expires_in_seconds": 3600
    }
  ],
  "data": {
    "source_device_type": "NxSCOPE",
    "measurement_type": "TPR",
    "patient_id": "pat_456",
    "temperature_c": 38.6,
    "pulse_bpm": 104,
    "respiration_rpm": 26,
    "measured_at": "2025-12-15T15:18:20Z"
  }
}

10) Partner Processing Requirements

NxVET delivery is at-least-once: Duplicates may occur → dedupe by event_id.

NxVET retries on:

  • Network errors/timeouts
  • HTTP 5xx
  • HTTP 429

Any 2xx response indicates success.

Recommended Handling Pattern

  1. Verify timestamp freshness
  2. Verify HMAC signature
  3. Deduplicate by event_id
  4. Enqueue async processing
  5. Return 2xx quickly

11) Error Responses

Recommended partner responses:

  • 401 Unauthorized: Timestamp or signature verification fails
  • 400 Bad Request / 404 Not Found: partner_user_key is unknown/invalid
  • 429 Too Many Requests: Rate limited (NxVET will retry)
  • 5xx: Temporary server errors (NxVET will retry)

13) Partner Implementation Checklist