Example Flows
Common API usage patterns and integration workflows
Last updated: August 5, 2026
- Base URL:
https://app.nx.vet- Auth:
Authorization: Bearer nxvet_sk_YOUR_API_KEY- Get org ID:
GET /api/auth/me→organizationId- List labels:
GET /api/organizations/{orgId}/labels?types=...&limit=20- Label details:
GET /api/labels/{labelId}- List conversations:
GET /api/nxhub/conversations(uses the API key's organization)- Create conversation:
POST /api/nxhub/conversations/create- Download audio:
GET /api/labels/{labelId}/audio- List devices:
GET /api/devices?organizationId={orgId}
Overview
This page demonstrates common integration flows using the NxVET API. Each example shows the sequence of API calls, request parameters, and how to work with the response data.
All examples use the following base configuration. Use GET /api/auth/me to discover your organization ID:
# Set your API key
API_KEY="nxvet_sk_YOUR_API_KEY"
BASE_URL="https://app.nx.vet/api"
# Discover your organization ID
ORG_ID=$(curl -s -H "Authorization: Bearer $API_KEY" \
"$BASE_URL/auth/me" | python3 -c "import sys,json; print(json.load(sys.stdin)['organizationId'])")
const API_KEY = process.env.NXVET_API_KEY;
const BASE_URL = 'https://app.nx.vet/api';
const headers = {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
};
// Discover your organization ID
const me = await fetch(`${BASE_URL}/auth/me`, { headers }).then(r => r.json());
const ORG_ID = me.organizationId;
import os
import requests
API_KEY = os.environ['NXVET_API_KEY']
BASE_URL = 'https://app.nx.vet/api'
headers = {
'Authorization': f'Bearer {API_KEY}',
'Content-Type': 'application/json'
}
# Discover your organization ID
me = requests.get(f'{BASE_URL}/auth/me', headers=headers).json()
ORG_ID = me['organizationId']
List & Filter Labels
Fetch labels for your organization with pagination and filtering. Use this flow only when maintaining an existing label-based integration.
Step 1 — List devices (for filtering)
Optionally fetch the list of devices to enable device-based filtering:
curl -H "Authorization: Bearer $API_KEY" \
"$BASE_URL/devices?organizationId=$ORG_ID&limit=200"
const res = await fetch(
`${BASE_URL}/devices?organizationId=${ORG_ID}&limit=200`,
{ headers }
);
const devices = await res.json();
// devices = [{ value: "device-id", displayName: "Room 1", serial: "NX-1234" }, ...]
res = requests.get(
f'{BASE_URL}/devices',
params={'organizationId': ORG_ID, 'limit': 200},
headers=headers
)
devices = res.json()
# devices = [{"value": "device-id", "displayName": "Room 1", "serial": "NX-1234"}, ...]
Step 2 — Fetch labels with pagination
List labels sorted by date, with optional filters for label type, patient search, and user.
# Paginated list with filters (repeat types= for each record type)
curl -H "Authorization: Bearer $API_KEY" \
"$BASE_URL/organizations/$ORG_ID/labels?\
limit=10&\
offset=0&\
sortingProperty=FromTime&\
isSortingDescending=true&\
patientSearch=Luna&\
types=ClinicConversation&types=NxHubBatch"
# Server-side date range (ISO 8601; filters on record start time, toTime exclusive)
curl -H "Authorization: Bearer $API_KEY" \
"$BASE_URL/organizations/$ORG_ID/labels?\
fromTime=2026-07-30T00:00:00Z&\
toTime=2026-08-04T00:00:00Z&\
types=NxHubBatch&types=NxMIC"
const params = new URLSearchParams({
limit: 10,
offset: 0,
sortingProperty: 'FromTime',
isSortingDescending: true,
patientSearch: 'Luna',
// Optional server-side date range (ISO 8601, filters on record start time)
fromTime: '2026-07-30T00:00:00Z',
toTime: '2026-08-04T00:00:00Z'
});
// Repeat types= for each record type
['ClinicConversation', 'NxHubBatch'].forEach(t => params.append('types', t));
const res = await fetch(
`${BASE_URL}/organizations/${ORG_ID}/labels?${params}`,
{ headers }
);
// Total count is in the response header
const totalCount = res.headers.get('X-Total-Count');
const labels = await res.json();
console.log(`Showing ${labels.length} of ${totalCount} labels`);
res = requests.get(
f'{BASE_URL}/organizations/{ORG_ID}/labels',
params={
'limit': 10,
'offset': 0,
'sortingProperty': 'FromTime',
'isSortingDescending': True,
'patientSearch': 'Luna',
# Optional server-side date range (ISO 8601, filters on record start time)
'fromTime': '2026-07-30T00:00:00Z',
'toTime': '2026-08-04T00:00:00Z',
# A list makes requests repeat types= for each record type
'types': ['ClinicConversation', 'NxHubBatch']
},
headers=headers
)
total_count = res.headers.get('X-Total-Count')
labels = res.json()
print(f'Showing {len(labels)} of {total_count} labels')
Available sorting properties: FromTime, Type, Patient
Available record types:
ClinicConversation— Live clinic recordingNxHubBatch— NxHUB manual recording (started with the device button)NxMIC— NxHUB auto-mode (ambient) recordingDictationAudio— DictationPhoneCallAudio— Phone call recordingButtonRecording,AudioButtonRecording— Button-triggered recordingsAggregateLabel— Combined medical record
Get Label Details
Retrieve full details for a specific label, including its transcript and patient data.
LABEL_ID="019e1234-5678-7000-abcd-123456789abc"
curl -H "Authorization: Bearer $API_KEY" \
"$BASE_URL/labels/$LABEL_ID"
const labelId = '019e1234-5678-7000-abcd-123456789abc';
const res = await fetch(`${BASE_URL}/labels/${labelId}`, { headers });
const label = await res.json();
// Access patient info
console.log(`Patient: ${label.patient?.name}`);
console.log(`Type: ${label.type}`);
console.log(`Duration: ${label.fromTime} — ${label.toTime}`);
// Access the transcript. Notes are append-only version history:
// regeneration adds a NEW note of the same type, so take the newest.
const newestOfType = (notes, type) =>
notes.filter(n => n.type === type)
.sort((a, b) => new Date(b.createdAt ?? b.time) - new Date(a.createdAt ?? a.time))[0];
const transcript = newestOfType(label.ownedPatientNotes, 'Transcript');
console.log('Transcript:', transcript?.content);
label_id = '019e1234-5678-7000-abcd-123456789abc'
res = requests.get(f'{BASE_URL}/labels/{label_id}', headers=headers)
label = res.json()
# Access patient info
print(f"Patient: {label.get('patient', {}).get('name')}")
print(f"Type: {label['type']}")
# Access the transcript. Notes are append-only version history:
# regeneration adds a NEW note of the same type, so take the newest.
def newest_of_type(notes, note_type):
matches = [n for n in notes if n['type'] == note_type]
return max(matches, key=lambda n: n.get('createdAt') or n['time'], default=None)
transcript = newest_of_type(label.get('ownedPatientNotes', []), 'Transcript')
if transcript:
print(f"Transcript: {transcript['content']}")
Response structure
The label detail response includes:
id,type,fromTime,toTime— Basic label metadatapatient— Assigned patient object (id,name)ownedPatientNotes[]— Transcript and SOAP notes. Append-only version history: regeneration adds a new note of the sametyperather than replacing the old one, and array order is not guaranteed — filter bytypeand take the greatestcreatedAt. Some records store the transcript under typeNotesTranscriptinstead ofTranscript; robust readers accept the newest across both typesfriendlyName,deviceSerial— Device info
Assign Patient to Label
Associate a patient with a label in an existing label-based workflow.
LABEL_ID="019e1234-5678-7000-abcd-123456789abc"
PATIENT_ID="019e5678-abcd-7000-1234-abcdef012345"
curl -X POST -H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d "{\"patientId\": \"$PATIENT_ID\"}" \
"$BASE_URL/labels/$LABEL_ID/assign"
const labelId = '019e1234-5678-7000-abcd-123456789abc';
const patientId = '019e5678-abcd-7000-1234-abcdef012345';
const res = await fetch(`${BASE_URL}/labels/${labelId}/assign`, {
method: 'POST',
headers,
body: JSON.stringify({ patientId })
});
if (res.ok) {
console.log('Patient assigned successfully');
}
label_id = '019e1234-5678-7000-abcd-123456789abc'
patient_id = '019e5678-abcd-7000-1234-abcdef012345'
res = requests.post(
f'{BASE_URL}/labels/{label_id}/assign',
json={'patientId': patient_id},
headers=headers
)
if res.ok:
print('Patient assigned successfully')
Download Audio Recording
Download the audio file for a label. The server resolves the storage location for you, so you only need the labelId.
audio/wav or audio/mp4 depending on the stored source file.
Step 1 — Download the audio file
LABEL_ID="019c78ab-389e-7001-adf2-a7399b7a7d22"
# Download the audio file (binary response, not JSON).
# -O uses the filename from the server's Content-Disposition header.
curl -H "Authorization: Bearer $API_KEY" \
-OJ \
"$BASE_URL/labels/${LABEL_ID}/audio"
# Verify the file
file ./*
# → audio file saved using the server-provided filename
import { writeFile } from 'fs/promises';
const labelId = '019c78ab-389e-7001-adf2-a7399b7a7d22';
const res = await fetch(
`${BASE_URL}/labels/${labelId}/audio`,
{ headers }
);
if (!res.ok) {
throw new Error(`Download failed: ${res.status}`);
}
const contentType = res.headers.get('content-type') || 'application/octet-stream';
const extension = contentType === 'audio/mp4' ? 'm4a' : contentType === 'audio/wav' ? 'wav' : 'bin';
const buffer = Buffer.from(await res.arrayBuffer());
await writeFile(`${labelId}.${extension}`, buffer);
console.log(`Saved ${buffer.length} bytes to ${labelId}.${extension}`);
label_id = '019c78ab-389e-7001-adf2-a7399b7a7d22'
res = requests.get(
f'{BASE_URL}/labels/{label_id}/audio',
headers=headers
)
res.raise_for_status()
content_type = res.headers.get('Content-Type', 'application/octet-stream')
extension = 'm4a' if content_type == 'audio/mp4' else 'wav' if content_type == 'audio/wav' else 'bin'
with open(f'{label_id}.{extension}', 'wb') as f:
f.write(res.content)
print(f'Saved {len(res.content)} bytes to {label_id}.{extension}')
Key details
- You only need the
labelId; the server resolves the storage location - Response is binary audio, not JSON
Content-Typeis typicallyaudio/wavoraudio/mp4- Returns
404if the label has no audio recording
Manage Conversations
List and inspect NxHub ambient recording conversations with token-based pagination. With API-key authentication, the unfiltered endpoint returns records across every device in the organization bound to the key.
organizationId. If you include it, it must match the key's organization. Add deviceId only when you intentionally want one device; organization and device filters combine.
Step 1 — List conversations
# List all conversations for the API key's organization
curl -H "Authorization: Bearer $API_KEY" \
"$BASE_URL/nxhub/conversations?pageSize=50"
# Fetch next page using the token from the previous response
curl -H "Authorization: Bearer $API_KEY" \
"$BASE_URL/nxhub/conversations?\
pageSize=50&\
pageToken=NEXT_PAGE_TOKEN"
async function listConversations({ status = null, deviceId = null, pageToken = null } = {}) {
const params = new URLSearchParams({
pageSize: 50
});
if (status) params.set('status', status);
if (deviceId) params.set('deviceId', deviceId);
if (pageToken) params.set('pageToken', pageToken);
const res = await fetch(
`${BASE_URL}/nxhub/conversations?${params}`,
{ headers }
);
return res.json();
}
// All devices and statuses in the API key's organization
const result = await listConversations();
console.log(`Found ${result.items.length} conversations`);
// Paginate if more results exist
if (result.hasMore) {
const nextPage = await listConversations({ pageToken: result.nextPageToken });
}
def list_conversations(status=None, device_id=None, page_token=None):
params = {'pageSize': 50}
if status:
params['status'] = status
if device_id:
params['deviceId'] = device_id
if page_token:
params['pageToken'] = page_token
res = requests.get(
f'{BASE_URL}/nxhub/conversations',
params=params,
headers=headers
)
return res.json()
# All devices and statuses in the API key's organization
result = list_conversations()
print(f"Found {len(result['items'])} conversations")
# Paginate
if result.get('hasMore'):
next_page = list_conversations(page_token=result['nextPageToken'])
Conversation statuses: ACTIVE, COMPLETED, PROCESSING, UPLOADED, FAILED, FILTERED
UPLOADED is the successful terminal state. FILTERED means the recording was intentionally excluded (for example, because it was too short or lacked sufficient speech); it is not an API error.
Step 2 — Get conversation details
DEVICE_ID="device-id-here"
CONVERSATION_ID="conversation-id-here"
curl -H "Authorization: Bearer $API_KEY" \
"$BASE_URL/nxhub/conversations/$DEVICE_ID/$CONVERSATION_ID"
const deviceId = 'device-id-here';
const conversationId = 'conversation-id-here';
const res = await fetch(
`${BASE_URL}/nxhub/conversations/${deviceId}/${conversationId}`,
{ headers }
);
const conversation = await res.json();
console.log(`Status: ${conversation.status}`);
console.log(`Speech: ${conversation.speechMinuteCount} min`);
console.log(`Transcript state: ${conversation.transcriptState}`);
console.log(`Importable: ${conversation.isImportable ? 'yes' : 'no'}`);
if (!conversation.isImportable) {
console.log(`Reason: ${conversation.notImportableReason}`);
}
if (conversation.isImportable) {
console.log(`Transcript: ${conversation.transcriptText}`);
}
Troubleshooting an empty list
- Call
GET /api/auth/meand confirm the expectedorganizationId, API-key scopes, andentitlements.nxHub. - Call
GET /api/devices?organizationId={orgId}and confirm the expected NxHub devices appear. - Call bare
GET /api/nxhub/conversationsbefore adding device, status, or time filters.
A 401 points to an invalid, expired, or revoked key. A 403 points to a key scope, organization, or subscription mismatch. A successful empty response means no conversation matched the current scope and filters.
Create a Conversation
Create a new NxHub conversation from a time range on a device. This flow involves previewing available audio, creating the conversation, and completing it.
Step 1 — View the device timeline
Fetch the timeline to see when speech was detected (max 6-hour window):
DEVICE_ID="device-id-here"
# 3-hour window: now minus 3 hours to now (in milliseconds)
END_MS=$(date +%s000)
START_MS=$((END_MS - 10800000))
curl -H "Authorization: Bearer $API_KEY" \
"$BASE_URL/nxhub/conversations/timeline/$DEVICE_ID?\
startMs=$START_MS&\
endMs=$END_MS"
const deviceId = 'device-id-here';
const endMs = Date.now();
const startMs = endMs - (3 * 60 * 60 * 1000); // 3 hours ago
const res = await fetch(
`${BASE_URL}/nxhub/conversations/timeline/${deviceId}?startMs=${startMs}&endMs=${endMs}`,
{ headers }
);
const timeline = await res.json();
// timeline.minutes[] contains per-minute VAD (voice activity detection) data
for (const minute of timeline.minutes) {
if (!minute.vadIsSilent) {
console.log(`Speech at ${new Date(minute.minuteStartMs).toLocaleTimeString()}`);
console.log(` Duration: ${minute.vadSpeechDurationMs}ms`);
const text = minute.diarizedTranscriptText || minute.transcriptText;
if (text) {
console.log(` Text: ${text}`);
}
}
}
Step 2 — Preview the time range
Check the audio content before creating (max 90-minute range):
# Select a 30-minute window with speech activity
curl -H "Authorization: Bearer $API_KEY" \
"$BASE_URL/nxhub/conversations/create/preview?\
deviceId=$DEVICE_ID&\
startMs=$START_MS&\
endMs=$END_MS"
const previewRes = await fetch(
`${BASE_URL}/nxhub/conversations/create/preview?deviceId=${deviceId}&startMs=${startMs}&endMs=${endMs}`,
{ headers }
);
const preview = await previewRes.json();
console.log(`Total: ${preview.totalMinutes} min`);
console.log(`Speech: ${preview.speechMinutes} min`);
console.log(`Silent: ${preview.silentMinutes} min`);
Step 3 — Create the conversation
curl -X POST -H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"deviceId\": \"$DEVICE_ID\",
\"startMs\": $START_MS,
\"endMs\": $END_MS
}" \
"$BASE_URL/nxhub/conversations/create"
const res = await fetch(`${BASE_URL}/nxhub/conversations/create`, {
method: 'POST',
headers,
body: JSON.stringify({ deviceId, startMs, endMs })
});
const result = await res.json();
if (result.success) {
console.log(`Created conversation: ${result.conversation.conversationId}`);
} else {
console.log(`Error: ${result.message}`);
}
Step 4 — Complete the conversation
When the conversation is ready, mark it as complete to trigger processing:
curl -X POST -H "Authorization: Bearer $API_KEY" \
"$BASE_URL/nxhub/conversations/$DEVICE_ID/$CONVERSATION_ID/complete"
const res = await fetch(
`${BASE_URL}/nxhub/conversations/${deviceId}/${conversationId}/complete`,
{ method: 'POST', headers }
);
const result = await res.json();
if (result.success) {
console.log('Conversation completed — processing will begin');
// Subscribe to conversation_completed webhook to know when it's done
}
conversation_completed webhook events to get notified when processing finishes and the label is created.
Task Tracker (Follow-Up Boards)
Trello/Jira-style boards with user-definable statuses and custom fields. The typical setup: an AI agent bulk-creates follow-up items from each day's visits via API, and clinic staff work them in the portal at app.nx.vet/tasks — editing fields, changing statuses, and adding comments.
Step 1: Create a board (once)
Omitting statuses and fields gives the default follow-up-tracker template: Not addressed / In progress / Completed / No Action Needed statuses, plus Patient, Doctor, Invoiced items, and Initials fields.
curl -X POST -H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "Follow-up Tracker"}' \
"$BASE_URL/task-boards"
const res = await fetch(`${BASE_URL}/task-boards`, {
method: 'POST',
headers,
body: JSON.stringify({ name: 'Follow-up Tracker' })
});
const board = await res.json();
console.log(board.id, board.statuses.map(s => s.name));
res = requests.post(
f'{BASE_URL}/task-boards',
json={'name': 'Follow-up Tracker'},
headers=headers
)
board = res.json()
print(board['id'], [s['name'] for s in board['statuses']])
Step 2: Bulk-create follow-up items (daily, per visit)
Share a groupKey per patient+visit so the portal groups the rows, and set linkUrl to the visit's record page so staff see the context in one click. The batch is all-or-nothing — one invalid item fails the whole request.
BOARD_ID="019f0001-0000-7000-a000-000000000001"
curl -X POST -H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"items": [
{
"title": "Doctor check-in call promised for Friday or Saturday",
"groupKey": "whiskey-2026-08-18",
"fields": { "patient": "Whiskey", "doctor": "Dr. John Trahan" },
"linkUrl": "https://app.nx.vet/audio-recordings?label=LABEL_ID"
},
{
"title": "Referral for rhinoscopy / CT if the Temaril-P does not work",
"groupKey": "whiskey-2026-08-18",
"fields": { "patient": "Whiskey", "doctor": "Dr. John Trahan" }
}
]
}' \
"$BASE_URL/task-boards/$BOARD_ID/tasks/bulk"
const boardId = '019f0001-0000-7000-a000-000000000001';
const res = await fetch(`${BASE_URL}/task-boards/${boardId}/tasks/bulk`, {
method: 'POST',
headers,
body: JSON.stringify({
items: [
{
title: 'Doctor check-in call promised for Friday or Saturday',
groupKey: 'whiskey-2026-08-18',
fields: { patient: 'Whiskey', doctor: 'Dr. John Trahan' },
linkUrl: 'https://app.nx.vet/audio-recordings?label=LABEL_ID'
},
{
title: 'Referral for rhinoscopy / CT if the Temaril-P does not work',
groupKey: 'whiskey-2026-08-18',
fields: { patient: 'Whiskey', doctor: 'Dr. John Trahan' }
}
]
})
});
const { items } = await res.json();
console.log(`Created ${items.length} tasks`);
board_id = '019f0001-0000-7000-a000-000000000001'
res = requests.post(
f'{BASE_URL}/task-boards/{board_id}/tasks/bulk',
json={'items': [
{
'title': 'Doctor check-in call promised for Friday or Saturday',
'groupKey': 'whiskey-2026-08-18',
'fields': {'patient': 'Whiskey', 'doctor': 'Dr. John Trahan'},
'linkUrl': 'https://app.nx.vet/audio-recordings?label=LABEL_ID'
},
{
'title': 'Referral for rhinoscopy / CT if the Temaril-P does not work',
'groupKey': 'whiskey-2026-08-18',
'fields': {'patient': 'Whiskey', 'doctor': 'Dr. John Trahan'}
}
]},
headers=headers
)
print(f"Created {len(res.json()['items'])} tasks")
Step 3: List what is still open
curl -H "Authorization: Bearer $API_KEY" \
"$BASE_URL/task-boards/$BOARD_ID/tasks?category=open"
const res = await fetch(
`${BASE_URL}/task-boards/${boardId}/tasks?category=open`, { headers }
);
const { items, total } = await res.json();
console.log(`${total} open follow-ups`);
res = requests.get(
f'{BASE_URL}/task-boards/{board_id}/tasks',
params={'category': 'open'},
headers=headers
)
print(f"{res.json()['total']} open follow-ups")
Step 4: Update a status and add a comment
On task updates, fields is a merge-patch — only the keys you send change (an explicit JSON null deletes a key). Top-level text props (description, groupKey, assignee, dueDate, linkUrl, completedAt) use a different convention: null/absent = unchanged, empty string "" = clear. Moving into a done-category status stamps completedAtIso automatically; completedAt is also directly editable ("date done").
TASK_ID="019f0002-0000-7000-a000-000000000042"
curl -X PATCH -H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"statusId": "completed", "fields": {"initials": "mu"}}' \
"$BASE_URL/tasks/$TASK_ID"
curl -X POST -H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"body": "Comm put in — owner will be notified if concerns."}' \
"$BASE_URL/tasks/$TASK_ID/comments"
const taskId = '019f0002-0000-7000-a000-000000000042';
await fetch(`${BASE_URL}/tasks/${taskId}`, {
method: 'PATCH',
headers,
body: JSON.stringify({ statusId: 'completed', fields: { initials: 'mu' } })
});
await fetch(`${BASE_URL}/tasks/${taskId}/comments`, {
method: 'POST',
headers,
body: JSON.stringify({ body: 'Comm put in — owner will be notified if concerns.' })
});
task_id = '019f0002-0000-7000-a000-000000000042'
requests.patch(
f'{BASE_URL}/tasks/{task_id}',
json={'statusId': 'completed', 'fields': {'initials': 'mu'}},
headers=headers
)
requests.post(
f'{BASE_URL}/tasks/{task_id}/comments',
json={'body': 'Comm put in — owner will be notified if concerns.'},
headers=headers
)
PATCH /api/task-boards/{boardId} replaces the whole statuses/fields array — GET the board, modify the array, send it back complete. Removing a status that tasks still use returns 409 unless ?migrateTo=<statusId> names a replacement, and replacing fields in a way that invalidates stored task values returns 409 unless ?dropInvalidFieldValues=true removes them in the same save. Subscribe to task_created/task_updated/task_comment_added webhook events to close the loop without polling.
Webhook-Driven Sync
Use webhooks to keep your system in sync with NxVET in real-time. When an event occurs, fetch the relevant data from the API.
Example: Sync webhook events to your system
const express = require('express');
const crypto = require('crypto');
const app = express();
const WEBHOOK_SECRET = process.env.NXVET_WEBHOOK_SECRET; // whsec_...
// Parse raw body for signature verification
app.post('/webhooks/nxvet', express.raw({ type: '*/*' }), async (req, res) => {
// 1. Verify signature
const signature = req.headers['x-nervex-signature'];
const expected = 'sha256=' + crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
if (signature !== expected) {
return res.status(401).send('Invalid signature');
}
// 2. Process the event
const event = JSON.parse(req.body);
const deliveryId = req.headers['x-nervex-delivery'];
switch (event.type) {
case 'new_label': {
// Fetch the full label details
const label = await fetch(
`${BASE_URL}/labels/${event.data.labelId}`,
{ headers }
).then(r => r.json());
console.log(`New label: ${label.id}, patient: ${label.patient?.name}`);
// Save to your database...
break;
}
case 'label_updated': {
// Re-fetch the label to get updated content
const label = await fetch(
`${BASE_URL}/labels/${event.data.labelId}`,
{ headers }
).then(r => r.json());
console.log(`Label updated: ${label.id}`);
// Update in your database...
break;
}
case 'conversation_completed': {
// Fetch conversation details
const conv = await fetch(
`${BASE_URL}/nxhub/conversations/${event.data.deviceId}/${event.data.conversationId}`,
{ headers }
).then(r => r.json());
console.log(`Conversation completed: ${conv.conversationId}`);
break;
}
}
// 3. Return 200 to acknowledge receipt
res.status(200).send('OK');
});
app.listen(3000);
import hmac
import hashlib
from flask import Flask, request
app = Flask(__name__)
WEBHOOK_SECRET = os.environ['NXVET_WEBHOOK_SECRET'] # whsec_...
@app.route('/webhooks/nxvet', methods=['POST'])
def handle_webhook():
# 1. Verify signature
signature = request.headers.get('X-NerveX-Signature', '')
expected = 'sha256=' + hmac.new(
WEBHOOK_SECRET.encode(),
request.data,
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(signature, expected):
return 'Invalid signature', 401
# 2. Process the event
event = request.get_json()
delivery_id = request.headers.get('X-NerveX-Delivery')
if event['type'] == 'new_label':
label = requests.get(
f"{BASE_URL}/labels/{event['data']['labelId']}",
headers=headers
).json()
print(f"New label: {label['id']}, patient: {label.get('patient', {}).get('name')}")
# Save to your database...
elif event['type'] == 'conversation_completed':
conv = requests.get(
f"{BASE_URL}/nxhub/conversations/{event['data']['deviceId']}/{event['data']['conversationId']}",
headers=headers
).json()
print(f"Conversation completed: {conv['conversationId']}")
# 3. Return 200 to acknowledge receipt
return 'OK', 200
Webhook + API pattern summary
| Webhook Event | Follow-up API Call | Use Case |
|---|---|---|
new_label |
GET /api/labels/{labelId} |
Import a new label for an existing integration |
label_updated |
GET /api/labels/{labelId} |
Sync an updated transcript |
label_deleted |
— | Remove a label from your system |
conversation_created |
GET /api/nxhub/conversations/{deviceId}/{conversationId} |
Track new ambient recording |
conversation_completed |
GET /api/nxhub/conversations/{deviceId}/{conversationId} |
Retrieve completed conversation with transcript |