Deprecation notice: NxVET is deprecating consultation record support in the near future. New integrations should use transcripts, conversations, webhooks, and other NxVET ecosystem data.

Example Flows

Common API usage patterns and integration workflows

Last updated: February 20, 2026

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.

Prerequisites: All examples assume you have an API key. See Authentication for setup instructions.
Deprecation notice: Label-based consultation record workflows below are retained for existing integrations, but NxVET is deprecating consultation record support in the near future.

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
curl -H "Authorization: Bearer $API_KEY" \
  "$BASE_URL/organizations/$ORG_ID/labels?\
limit=10&\
offset=0&\
sortingProperty=FromTime&\
isSortingDescending=true&\
patientSearch=Luna&\
types=ClinicConversation,NxHubBatch"
const params = new URLSearchParams({
  limit: 10,
  offset: 0,
  sortingProperty: 'FromTime',
  isSortingDescending: true,
  patientSearch: 'Luna',
  types: 'ClinicConversation,NxHubBatch'
});

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',
        '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 recording
  • NxHubBatch — NxHub ambient recording
  • DictationAudio — Dictation
  • DocumentSoap — Document upload
  • PhoneCallAudio — Phone call recording
  • ButtonRecording, AudioButtonRecording — Button-triggered recordings
  • AggregateLabel — Combined medical record

Get Label Details

Retrieve full details for a specific label, including transcript, notes, 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 transcript and notes
for (const note of label.ownedPatientNotes) {
  const content = JSON.parse(note.content);

  // Transcript text
  if (content.english_soap) {
    console.log('Transcript:', content.english_soap);
  }

  // Clinical note
  if (content.clinical_note) {
    console.log('Clinical Note:', content.clinical_note);
  }
}
import json

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 transcript and notes
for note in label.get('ownedPatientNotes', []):
    content = json.loads(note['content'])

    if 'english_soap' in content:
        print(f"Transcript: {content['english_soap']}")

    if 'clinical_note' in content:
        print(f"Clinical Note: {content['clinical_note']}")

Response structure

The label detail response includes:

  • id, type, fromTime, toTime — Basic label metadata
  • patient — Assigned patient object (id, name)
  • ownedPatientNotes[] — Array of notes. Each note has a content field (JSON string) containing transcript, clinical notes, and metadata
  • friendlyName, 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.

What to expect: The response is binary audio, not JSON. The server may return 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-Type is typically audio/wav or audio/mp4
  • Returns 404 if the label has no audio recording

Generate Medical Record

Combine multiple labels into a single generated record using a template. This is useful for consolidating several sessions into one output.

Step 1 — Fetch available templates

curl -H "Authorization: Bearer $API_KEY" \
  "$BASE_URL/organizations/$ORG_ID/medical-templates"
const res = await fetch(
  `${BASE_URL}/organizations/${ORG_ID}/medical-templates`,
  { headers }
);
const templates = await res.json();
// templates = [{ id: "...", name: "Standard SOAP", content: "..." }, ...]

Step 2 — Aggregate labels

Combine up to 5 labels into a medical record:

curl -X POST -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "LabelIds": [
      "019e1111-1111-7000-aaaa-111111111111",
      "019e2222-2222-7000-bbbb-222222222222"
    ],
    "PatientId": "019e5678-abcd-7000-1234-abcdef012345",
    "MedicalTemplateId": "template-id-here"
  }' \
  "$BASE_URL/labels/aggregate"
const res = await fetch(`${BASE_URL}/labels/aggregate`, {
  method: 'POST',
  headers,
  body: JSON.stringify({
    LabelIds: [
      '019e1111-1111-7000-aaaa-111111111111',
      '019e2222-2222-7000-bbbb-222222222222'
    ],
    PatientId: '019e5678-abcd-7000-1234-abcdef012345',
    MedicalTemplateId: 'template-id-here'
  })
});

const newLabel = await res.json();
console.log(`Created aggregate label: ${newLabel.id}`);

Step 3 — Trigger report regeneration (optional)

Reprocess a label with a different template. This is a fire-and-forget operation — both 200 and 504 responses indicate the job was triggered successfully.

LABEL_ID="019e1234-5678-7000-abcd-123456789abc"
TEMPLATE_ID="template-id-here"

curl -X POST -H "Authorization: Bearer $API_KEY" \
  "$BASE_URL/labels/$LABEL_ID/lambda?medicalTemplateId=$TEMPLATE_ID"

# Note: 504 Gateway Timeout is expected — the job runs asynchronously
const labelId = '019e1234-5678-7000-abcd-123456789abc';
const templateId = 'template-id-here';

try {
  await fetch(
    `${BASE_URL}/labels/${labelId}/lambda?medicalTemplateId=${templateId}`,
    { method: 'POST', headers }
  );
} catch (e) {
  // 504 is expected — the job is processing asynchronously
}

// Use webhooks to know when the label is updated (label_updated event)
Tip: Subscribe to label_updated webhook events to get notified when the regenerated report is ready.

Manage Conversations

List and inspect NxHub ambient recording conversations with token-based pagination.

Step 1 — List conversations

# List active conversations
curl -H "Authorization: Bearer $API_KEY" \
  "$BASE_URL/nxhub/conversations?\
organizationId=$ORG_ID&\
status=ACTIVE&\
pageSize=20"

# Fetch next page using the token from the previous response
curl -H "Authorization: Bearer $API_KEY" \
  "$BASE_URL/nxhub/conversations?\
organizationId=$ORG_ID&\
pageSize=20&\
pageToken=NEXT_PAGE_TOKEN"
async function listConversations(status = null, pageToken = null) {
  const params = new URLSearchParams({
    organizationId: ORG_ID,
    pageSize: 20
  });
  if (status) params.set('status', status);
  if (pageToken) params.set('pageToken', pageToken);

  const res = await fetch(
    `${BASE_URL}/nxhub/conversations?${params}`,
    { headers }
  );
  return res.json();
}

// List active conversations
const result = await listConversations('ACTIVE');
console.log(`Found ${result.items.length} conversations`);

// Paginate if more results exist
if (result.hasMore) {
  const nextPage = await listConversations('ACTIVE', result.nextPageToken);
}
def list_conversations(status=None, page_token=None):
    params = {'organizationId': ORG_ID, 'pageSize': 20}
    if status:
        params['status'] = status
    if page_token:
        params['pageToken'] = page_token

    res = requests.get(
        f'{BASE_URL}/nxhub/conversations',
        params=params,
        headers=headers
    )
    return res.json()

# List active conversations
result = list_conversations(status='ACTIVE')
print(f"Found {len(result['items'])} conversations")

# Paginate
if result.get('hasMore'):
    next_page = list_conversations('ACTIVE', result['nextPageToken'])

Conversation statuses: ACTIVE, COMPLETED, PROCESSING, UPLOADED, FAILED, FILTERED

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(`Has transcript: ${conversation.hasTranscript ? 'yes' : 'no'}`);
if (conversation.transcriptText) {
  console.log(`Transcript: ${conversation.transcriptText}`);
}

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`);
    if (minute.transcriptText) {
      console.log(`  Text: ${minute.transcriptText}`);
    }
  }
}

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
}
Tip: Subscribe to conversation_completed webhook events to get notified when processing finishes and the label is created.

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 updated transcript or notes
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