Security
HI Platform signs every webhook delivery with HMAC-SHA256. Your receiver must verify the signature before processing the payload — never trust an unsigned request.
Headers HI Platform sends
| Header | Description |
|---|---|
X-Webhook-Event |
The event name in dot-notation (assessment.completed or
participation.completed). Matches the envelope's type field.
|
X-Webhook-Timestamp |
Unix seconds at the time the request was signed. Used in the signing string. |
X-Webhook-Signature |
Lowercase hex HMAC-SHA256. Cover bytes: timestamp + . + raw body. |
X-Webhook-Delivery-Id |
UUID, stable across retries — your idempotency key. See Delivery. |
Content-Type |
Always application/json. |
Signing string
signing_string = <timestamp> + "." + <raw_body>
signature = HMAC-SHA256(signing_string, shared_secret)
header_value = hex_lowercase(signature)
-
<timestamp>is the unix-seconds value from theX-Webhook-Timestampheader. <raw_body>is the exact bytes of the request body.- The shared secret is the value HI Platform displayed to you (once) when you registered the webhook.
Verify against the raw body
The signature is computed over the literal bytes that HI Platform sent on the wire. Capture those bytes before any JSON parser touches them, and verify against that exact string.
Replay protection
Reject requests whose X-Webhook-Timestamp falls outside ±5 minutes of your server clock. HI
Platform retries deliveries for up to about 42 hours, but each retry is re-signed with a fresh timestamp — so
any timestamp older than your tolerance is a replay, not a late retry.
Rotating the shared secret
The shared secret is generated when you register and shown exactly once. Rotate it from the customer app — a fresh value is returned once; the receiver URL and any pending deliveries are preserved. Subsequent webhook deliveries are signed with the new secret.
URL restrictions
- Only
https://URLs are accepted. - Hostnames that resolve to internal IP ranges (loopback, private networks, cloud-metadata endpoints) are rejected.
- HTTP redirects are not followed.
Java verifier
A complete, standalone Java verifier you can copy into your service. Requires JDK 11 or newer.
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.Duration;
import java.time.Instant;
public final class WebhookVerifier {
private static final Duration TOLERANCE = Duration.ofMinutes(5);
public static boolean verify(String rawBody,
String timestampHeader,
String signatureHeader,
String secret) {
long timestamp;
try {
timestamp = Long.parseLong(timestampHeader);
} catch (NumberFormatException e) {
return false;
}
Instant sent = Instant.ofEpochSecond(timestamp);
if (Duration.between(sent, Instant.now()).abs().compareTo(TOLERANCE) > 0) {
return false;
}
String signingString = timestamp + "." + rawBody;
String expected = hmacSha256Hex(signingString, secret);
return MessageDigest.isEqual(
expected.getBytes(StandardCharsets.US_ASCII),
signatureHeader.getBytes(StandardCharsets.US_ASCII));
}
private static String hmacSha256Hex(String data, String key) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] digest = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder(digest.length * 2);
for (byte b : digest) sb.append(String.format("%02x", b));
return sb.toString();
} catch (Exception e) {
throw new IllegalStateException("HMAC computation failed", e);
}
}
}
What's next
- Delivery — retries, timeouts, and idempotency.