Webhook Integration
Receive real-time HMAC-signed HTTP notifications when events occur in your organization
Last updated: February 20, 2026
Overview
Webhooks allow your application to receive real-time HTTP POST notifications when events occur in your NxVET organization. Each webhook is HMAC-SHA256 signed so you can verify the request's authenticity.
Key Features
- Organization-scoped: Webhooks are tied to your organization
- HMAC-SHA256 signed: Every request includes a cryptographic signature
- Self-service: Create and manage webhooks via the NxVET Integrations page or API
- Automatic retries: 5 retries with exponential backoff on failure
- Delivery logs: Inspect the last 50 delivery attempts
- Up to 10 webhooks per organization
Webhook Management
Manage webhooks through the NxVET Integrations page (Webhooks tab) or via the API.
API Endpoints
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/organizations/{orgId}/webhooks |
Create a new webhook |
| GET | /api/organizations/{orgId}/webhooks |
List all webhooks |
| DEL | /api/organizations/{orgId}/webhooks/{id} |
Delete a webhook |
| POST | /api/organizations/{orgId}/webhooks/{id}/test |
Send a test ping |
| POST | /api/organizations/{orgId}/webhooks/{id}/enable |
Re-enable a disabled webhook |
| GET | /api/organizations/{orgId}/webhooks/{id}/deliveries |
View delivery logs (last 50) |
See the API Reference for full request/response details.
Creating a Webhook
curl -X POST "https://app.nx.vet/api/organizations/YOUR_ORG_ID/webhooks" \
-H "Authorization: Bearer nxvet_sk_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://my-app.example.com/webhooks/nxvet",
"description": "Production webhook",
"eventTypes": ["conversation_created", "conversation_completed"]
}'
Response:
{
"id": "019e1234-5678-7000-abcd-123456789abc",
"url": "https://my-app.example.com/webhooks/nxvet",
"description": "Production webhook",
"eventTypes": ["conversation_created", "conversation_completed"],
"isActive": true,
"consecutiveFailures": 0,
"createdAt": "2026-02-20T23:00:00Z",
"secret": "whsec_A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6Q7R8S9T0U1V2"
}
secret is shown only once when the webhook is created. Store it securely for signature verification. If lost, delete the webhook and create a new one.
Event Types
| Event | Description | Data Fields |
|---|---|---|
new_label |
A new consultation record is created | { labelId } |
label_updated |
An existing record is updated | { labelId } |
label_deleted |
A record is deleted | { labelId } |
conversation_created |
A NxHub conversation has started | { conversationId, deviceId } |
conversation_completed |
A NxHub conversation has finished | { conversationId, deviceId } |
ping |
Test event (sent via the test endpoint) | { test: true } |
When creating a webhook, you can specify which event types to receive. If no event types are specified, the webhook receives all events.
Payload Format
NxVET sends webhook events as HTTP POST requests with a JSON body:
Example: conversation_completed
{
"id": "evt_019e5678-abcd-7000-1234-56789abcdef0",
"type": "conversation_completed",
"organizationId": "0185644d-9383-7000-b35d-6e31e1158b43",
"timestamp": "2026-02-20T23:10:00+00:00",
"data": {
"conversationId": "conv-abc123",
"deviceId": "hub_00112233"
}
}
Example: new_label
{
"id": "evt_019e6789-bcde-7000-2345-6789abcdef01",
"type": "new_label",
"organizationId": "0185644d-9383-7000-b35d-6e31e1158b43",
"timestamp": "2026-02-20T23:15:30+00:00",
"data": {
"labelId": "019b0f73-9ab4-7000-bd9c-33a07f1352d2"
}
}
Top-Level Fields
| Field | Type | Description |
|---|---|---|
id |
string | Unique event ID (use for deduplication) |
type |
string | Event type (see table above) |
organizationId |
string | Organization UUID |
timestamp |
string | ISO 8601 UTC timestamp |
data |
object | Event-specific payload |
Request Headers
Each webhook request includes these headers:
| Header | Example | Purpose |
|---|---|---|
X-NerveX-Signature |
sha256=a1b2c3d4e5f6... |
HMAC-SHA256 signature of the request body |
X-NerveX-Event |
conversation_completed |
Event type |
X-NerveX-Delivery |
evt_019e5678-abcd-... |
Unique delivery ID (use for idempotency) |
Content-Type |
application/json |
Always JSON |
Signature Verification
The X-NerveX-Signature header contains a HMAC-SHA256 hex digest of the raw request body, computed using your webhook secret:
signature = "sha256=" + HEX(HMAC-SHA256(secret, rawBody))
Verification Code Examples
const crypto = require('crypto');
function verifyWebhook(req, secret) {
const signature = req.headers['x-nervex-signature'];
const rawBody = req.rawBody; // Get raw body bytes
// Compute expected signature
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
// Constant-time compare
if (!crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
)) {
throw new Error('Invalid signature');
}
return true;
}
// Express.js example
app.post('/webhooks/nxvet', express.raw({ type: 'application/json' }), (req, res) => {
try {
verifyWebhook(req, process.env.NXVET_WEBHOOK_SECRET);
const event = JSON.parse(req.body);
// Process event based on type
switch (event.type) {
case 'conversation_completed':
handleConversationCompleted(event.data);
break;
case 'new_label':
handleNewLabel(event.data);
break;
}
res.status(200).send('OK');
} catch (err) {
res.status(401).send('Invalid signature');
}
});
import hmac
import hashlib
def verify_webhook(request, secret):
signature = request.headers.get('X-NerveX-Signature')
raw_body = request.body # Get raw body bytes
# Compute expected signature
expected = 'sha256=' + hmac.new(
secret.encode('utf-8'),
raw_body,
hashlib.sha256
).hexdigest()
# Constant-time compare
if not hmac.compare_digest(signature, expected):
raise ValueError('Invalid signature')
return True
# Flask example
@app.route('/webhooks/nxvet', methods=['POST'])
def handle_webhook():
try:
verify_webhook(request, os.environ['NXVET_WEBHOOK_SECRET'])
event = request.get_json()
# Process event based on type
if event['type'] == 'conversation_completed':
handle_conversation_completed(event['data'])
elif event['type'] == 'new_label':
handle_new_label(event['data'])
return 'OK', 200
except ValueError:
return 'Invalid signature', 401
using System.Security.Cryptography;
using System.Text;
public bool VerifyWebhook(HttpRequest request, string secret)
{
var signature = request.Headers["X-NerveX-Signature"].ToString();
var rawBody = ReadRawBody(request);
// Compute expected signature
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(rawBody));
var expected = "sha256=" + BitConverter.ToString(hash)
.Replace("-", "").ToLower();
// Constant-time compare
return CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(signature),
Encoding.UTF8.GetBytes(expected));
}
// ASP.NET Core example
[HttpPost("/webhooks/nxvet")]
public async Task<IActionResult> HandleWebhook()
{
var rawBody = await new StreamReader(Request.Body).ReadToEndAsync();
if (!VerifyWebhook(Request, _configuration["NxVET:WebhookSecret"]))
return Unauthorized("Invalid signature");
var webhookEvent = JsonSerializer.Deserialize<WebhookEvent>(rawBody);
switch (webhookEvent.Type)
{
case "conversation_completed":
await HandleConversationCompleted(webhookEvent.Data);
break;
case "new_label":
await HandleNewLabel(webhookEvent.Data);
break;
}
return Ok();
}
Delivery & Retries
Delivery Guarantees
- At-least-once delivery: Events may be delivered more than once. Deduplicate by the
X-NerveX-Deliveryheader oridfield. - Ordering: Events may arrive out of order. Use the
timestampfield for ordering. - Any 2xx response is considered successful delivery.
Retry Schedule
Failed deliveries are retried up to 5 times with exponential backoff:
| Attempt | Delay |
|---|---|
| 1st retry | ~1 second |
| 2nd retry | ~16 seconds |
| 3rd retry | ~81 seconds |
| 4th retry | ~4 minutes |
| 5th retry | ~10 minutes |
What Counts as a Failure
- Network errors or connection timeouts
- HTTP
5xxserver errors - HTTP
429too many requests - No response within timeout
200 OK as quickly as possible. Process the event asynchronously to avoid timeouts.
Testing Webhooks
Send a test ping event to verify your endpoint is working:
curl -X POST "https://app.nx.vet/api/organizations/YOUR_ORG_ID/webhooks/WEBHOOK_ID/test" \
-H "Authorization: Bearer nxvet_sk_YOUR_API_KEY"
This sends a webhook with:
{
"id": "evt_test_...",
"type": "ping",
"organizationId": "0185644d-9383-7000-b35d-6e31e1158b43",
"timestamp": "2026-02-20T23:00:00+00:00",
"data": {
"test": true
}
}
You can also send test pings from the NxVET Integrations page (Webhooks tab → Test button).
Delivery History
Inspect the last 50 delivery attempts for a webhook to debug integration issues:
curl -H "Authorization: Bearer nxvet_sk_YOUR_API_KEY" \
"https://app.nx.vet/api/organizations/YOUR_ORG_ID/webhooks/WEBHOOK_ID/deliveries"
Response:
[
{
"id": "019e9999-0000-7000-abcd-000000000001",
"eventId": "evt_019e5678-abcd-7000-1234-56789abcdef0",
"eventType": "conversation_completed",
"attemptNumber": 1,
"httpStatusCode": 200,
"responseTimeMs": 142,
"errorMessage": null,
"success": true,
"createdAt": "2026-02-20T23:10:00Z"
},
{
"id": "019e9999-0000-7000-abcd-000000000002",
"eventId": "evt_019e6789-bcde-7000-2345-6789abcdef01",
"eventType": "new_label",
"attemptNumber": 1,
"httpStatusCode": 500,
"responseTimeMs": 2034,
"errorMessage": "Internal Server Error",
"success": false,
"createdAt": "2026-02-20T23:15:30Z"
}
]
Delivery logs are also visible in the NxVET UI by expanding a webhook row in the Integrations page.