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
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.
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.
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 deduplicationevent_type(string) — e.g.,record.created,measurement.createdoccurred_at(string) — ISO 8601 UTC timestamppartner_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 hourdata(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
9.1 Message to Sign
Compute signature over:
<timestamp>.<raw_body>
Where:
<timestamp>isX-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 retries on:
- Network errors/timeouts
- HTTP 5xx
- HTTP 429
Any 2xx response indicates success.
Recommended Handling Pattern
- Verify timestamp freshness
- Verify HMAC signature
- Deduplicate by event_id
- Enqueue async processing
- 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)