Webhooks


Webhooks allow your application to receive real-time HTTP notifications when specific events occur in the Invox Medical platform. Instead of polling for updates, your server will be called automatically with the relevant event data.

Credentials and authentication

Each organization can be assigned a pair of keys used to sign and verify webhook payloads:

FieldDescription
apiKeyPublic key. Included in the string to sign.
secretKeyPrivate key. Used as the HMAC key. Never transmitted in the payload.

Common HTTP headers

All webhook events (except OnInvoxAuraAudioCreated) are sent as JSON with the following headers:

Content-Type: application/json
User-Agent: Invox-Medical-Webhook/1.0

The default delivery timeout is 30 seconds. The OnInvoxAuraAudioCreated event uses a 60-second timeout and is sent as multipart/form-data.

Signature algorithm (requestSignature)

Every webhook payload includes a requestSignature field (when the organization has credentials configured). This signature allows you to verify that the request was genuinely sent by Invox Medical and has not been tampered with.

How the signature is computed

  1. Take all fields from the payload except eventName and requestSignature.
  2. For each value:
    • If it is an object (and not null): JSON.stringify(value)
    • If it is null or undefined: empty string ""
    • Any other type (string, number, boolean): String(value)
  3. Append the apiKey as the last element.
  4. Join all values with the | separator.
  5. Compute HMAC-SHA256 using secretKey as the key.
  6. Encode the result in Base64.

Implementation

TypeScript
import * as CryptoJS from "crypto-js";

function generateSignature(
  payload: Record<string, unknown>,
  apiKey: string,
  secretKey: string,
): string {
  const excludedKeys = ["eventName", "requestSignature"];

  const values = Object.entries(payload)
    .filter(([key]) => !excludedKeys.includes(key))
    .map(([, value]) =>
      typeof value === "object" && value !== null
        ? JSON.stringify(value)
        : String(value ?? ""),
    );

  values.push(apiKey);

  const dataToSign = values.join("|");
  const hmac = CryptoJS.HmacSHA256(dataToSign, secretKey);
  return CryptoJS.enc.Base64.stringify(hmac);
}
C#
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;

public static string GenerateSignature(
    Dictionary<string, object?> payload,
    string apiKey,
    string secretKey)
{
    var excludedKeys = new HashSet<string> { "eventName", "requestSignature" };

    var values = payload
        .Where(kvp => !excludedKeys.Contains(kvp.Key))
        .Select(kvp =>
        {
            if (kvp.Value is null) return "";
            if (kvp.Value is JsonElement el) return el.ToString();
            if (kvp.Value is IDictionary<string, object?> || kvp.Value is IList<object?>)
                return JsonSerializer.Serialize(kvp.Value);
            return kvp.Value.ToString() ?? "";
        })
        .ToList();

    values.Add(apiKey);

    var dataToSign = string.Join("|", values);
    using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secretKey));
    var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(dataToSign));
    return Convert.ToBase64String(hash);
}
Java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.nio.charset.StandardCharsets;
import java.util.*;

public class WebhookSignature {

    private static final Set<String> EXCLUDED_KEYS =
        Set.of("eventName", "requestSignature");
    private static final ObjectMapper mapper = new ObjectMapper();

    public static String generateSignature(
            LinkedHashMap<String, Object> payload,
            String apiKey,
            String secretKey) throws Exception {

        List<String> values = new ArrayList<>();
        for (Map.Entry<String, Object> entry : payload.entrySet()) {
            if (EXCLUDED_KEYS.contains(entry.getKey())) continue;
            Object value = entry.getValue();
            if (value == null) {
                values.add("");
            } else if (value instanceof Map || value instanceof List) {
                values.add(mapper.writeValueAsString(value));
            } else {
                values.add(String.valueOf(value));
            }
        }
        values.add(apiKey);

        String dataToSign = String.join("|", values);
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(
            secretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
        byte[] hash = mac.doFinal(
            dataToSign.getBytes(StandardCharsets.UTF_8));
        return Base64.getEncoder().encodeToString(hash);
    }
}
Python
import hmac
import hashlib
import base64
import json

def generate_signature(
    payload: dict,
    api_key: str,
    secret_key: str,
) -> str:
    excluded_keys = {"eventName", "requestSignature"}

    values = []
    for key, value in payload.items():
        if key in excluded_keys:
            continue
        if value is None:
            values.append("")
        elif isinstance(value, (dict, list)):
            values.append(json.dumps(value, separators=(",", ":")))
        else:
            values.append(str(value))

    values.append(api_key)

    data_to_sign = "|".join(values)
    signature = hmac.new(
        secret_key.encode("utf-8"),
        data_to_sign.encode("utf-8"),
        hashlib.sha256,
    ).digest()
    return base64.b64encode(signature).decode("utf-8")
Critical: field order matters. The signature string is built following the insertion order of the object properties. The backend constructs objects with a deterministic order. To verify the signature correctly, you must use that exact same order. Each event section specifies the precise signature string.

Verification example

TypeScript
import crypto from "crypto";

function verifyWebhookSignature(
  payload: Record<string, unknown>,
  apiKey: string,
  secretKey: string,
): boolean {
  const excludedKeys = ["eventName", "requestSignature"];
  const receivedSignature = payload.requestSignature as string;

  const values = Object.entries(payload)
    .filter(([key]) => !excludedKeys.includes(key))
    .map(([, value]) =>
      typeof value === "object" && value !== null
        ? JSON.stringify(value)
        : String(value ?? ""),
    );

  values.push(apiKey);

  const dataToSign = values.join("|");
  const hmac = crypto
    .createHmac("sha256", secretKey)
    .update(dataToSign)
    .digest("base64");

  return hmac === receivedSignature;
}
C#
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;

public static bool VerifyWebhookSignature(
    Dictionary<string, object?> payload,
    string apiKey,
    string secretKey)
{
    var excludedKeys = new HashSet<string> { "eventName", "requestSignature" };
    var receivedSignature = payload["requestSignature"]?.ToString() ?? "";

    var values = payload
        .Where(kvp => !excludedKeys.Contains(kvp.Key))
        .Select(kvp =>
        {
            if (kvp.Value is null) return "";
            if (kvp.Value is JsonElement el) return el.ToString();
            if (kvp.Value is IDictionary<string, object?> || kvp.Value is IList<object?>)
                return JsonSerializer.Serialize(kvp.Value);
            return kvp.Value.ToString() ?? "";
        })
        .ToList();

    values.Add(apiKey);

    var dataToSign = string.Join("|", values);
    using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secretKey));
    var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(dataToSign));
    var computedSignature = Convert.ToBase64String(hash);

    return computedSignature == receivedSignature;
}
Java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.nio.charset.StandardCharsets;
import java.util.*;

public class WebhookVerifier {

    private static final Set<String> EXCLUDED_KEYS =
        Set.of("eventName", "requestSignature");
    private static final ObjectMapper mapper = new ObjectMapper();

    public static boolean verifyWebhookSignature(
            LinkedHashMap<String, Object> payload,
            String apiKey,
            String secretKey) throws Exception {

        String receivedSignature = (String) payload.get("requestSignature");

        List<String> values = new ArrayList<>();
        for (Map.Entry<String, Object> entry : payload.entrySet()) {
            if (EXCLUDED_KEYS.contains(entry.getKey())) continue;
            Object value = entry.getValue();
            if (value == null) {
                values.add("");
            } else if (value instanceof Map || value instanceof List) {
                values.add(mapper.writeValueAsString(value));
            } else {
                values.add(String.valueOf(value));
            }
        }
        values.add(apiKey);

        String dataToSign = String.join("|", values);
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(
            secretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
        byte[] hash = mac.doFinal(
            dataToSign.getBytes(StandardCharsets.UTF_8));
        String computedSignature = Base64.getEncoder().encodeToString(hash);

        return computedSignature.equals(receivedSignature);
    }
}
Python
import hmac
import hashlib
import base64
import json

def verify_webhook_signature(
    payload: dict,
    api_key: str,
    secret_key: str,
) -> bool:
    excluded_keys = {"eventName", "requestSignature"}
    received_signature = payload.get("requestSignature", "")

    values = []
    for key, value in payload.items():
        if key in excluded_keys:
            continue
        if value is None:
            values.append("")
        elif isinstance(value, (dict, list)):
            values.append(json.dumps(value, separators=(",", ":")))
        else:
            values.append(str(value))

    values.append(api_key)

    data_to_sign = "|".join(values)
    computed = hmac.new(
        secret_key.encode("utf-8"),
        data_to_sign.encode("utf-8"),
        hashlib.sha256,
    ).digest()
    computed_signature = base64.b64encode(computed).decode("utf-8")

    return hmac.compare_digest(computed_signature, received_signature)
When reconstructing the signature string, the payload must be parsed preserving the field order from the received JSON. In most languages, use an ordered map/dictionary (e.g., LinkedHashMap in Java, OrderedDict in Python, or standard dict in Python 3.7+).

Available events

EventDescription
OnTranscriptionFinishedA classic transcription has completed.
OnMedicalReportFinishedA structured medical report has completed.
OnInvoxAuraTranscriptionFinishedAn Invox Aura transcription has completed.
OnInvoxAuraClinicalNoteCreatedA clinical note has been created in Invox Aura.
OnInvoxAuraClinicalNoteFinishedA clinical note has finished processing in Invox Aura.
OnInvoxAuraAudioCreatedAn audio file has been uploaded to Invox Aura.
OnAlertTriggeredAn alert notification has been generated.