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)

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.

Do not re-serialize the JSON before verifying. Any whitespace or key-order change produces a different signature and will cause verification to fail. Always read the body as raw bytes (or the string built directly from those bytes) before parsing.

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

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