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"
}
Important: The 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

Required: Always verify the signature to ensure the webhook came from NxVET.

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-Delivery header or id field.
  • Ordering: Events may arrive out of order. Use the timestamp field 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 5xx server errors
  • HTTP 429 too many requests
  • No response within timeout
Tip: Return 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.

Implementation Checklist