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.
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.
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.
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+).