Skip to content

Webhook signature verification

We are supporting two methods of signing webhooks:

  • HMAC signature generated using a shared secret key
  • Digital signature generated based on a rotated InPost public key (deprecated)

Both methods are very similar, the only difference is in the key and algorithm used to produce the signature.


HMAC signature generated using a shared secret key

HMAC, which stands for Hash-Based Message Authentication Code or Keyed-Hash Message Authentication Code, uses a cryptographic hash function and a secret cryptographic key. Unlike asymmetric cryptography, HMAC authentication relies on a shared secret, eliminating the need for a complex public key infrastructure. This method requires that communicating parties establish a trusted channel to exchange the secret key prior to communication.

Example HMAC key: fdXbfU27DBNG6LuoHu@ThKl3

InPost uses secret keys exchanged with a client and the HMAC-SHA256 algorithm to generate a signature of the message hash. This signature is included in the x-inpost-signature header. Upon receiving a message, the receiver computes the HMAC to verify the integrity and authenticity of the message. If the computed HMAC matches the one sent by InPost, the message content is confirmed as untampered and trustworthy.

How signing works

  1. Content to sign is either:
  • a request body
  • a timestamp taken from the x-inpost-timestamp header (no reformatting), followed by a dot separator (.), followed by a request body e.g. <x-inpost-timestamp>.<body>.

The subscription configuration determines which of these two is used.

  1. That content is encoded to bytes as UTF-8.
  2. A signature is computed over those bytes — using HMAC-SHA256 + your shared secret for HMAC.
  3. The resulting bytes are Base64-encoded (standard alphabet, with padding) and sent as-is in the x-inpost-signature header.

Verifying the signature using a HMAC shared secret key

To verify a request, three things are needed:

  • <body> or <x-inpost-timestamp>.<body> (depending on your configuration), unmodified
  • a shared secret key (HMAC)
  • x-inpost-signature header, unmodified

Next:

  1. Take the raw request body exactly as received.
  2. Build the content to sign: <body> or <x-inpost-timestamp>.<body> (depending on your configuration).
  3. Compute HMAC-SHA256 over that content (UTF-8 bytes), using your shared secret (UTF-8 bytes) as the key.
  4. Base64-encode the result.
  5. Compare it to x-inpost-signature using a constant-time comparison — not == / .equals() — so the check does not leak timing information about how much of the signature matched.

Code examples

import base64
import hashlib
import hmac


def verify_signature(
    raw_body: bytes,
    signature_header: str,
    shared_secret: str
) -> bool:
    key = shared_secret.encode("utf-8")
    computed = base64.b64encode(
        hmac.new(key, raw_body, hashlib.sha256).digest()
    ).decode("utf-8")
    return hmac.compare_digest(computed, signature_header)

raw_body = b'{"customerReference":"customerReference","trackingNumber":"trackingNumber","eventId":"eventId","eventCode":"eventCode","timestamp":"2024-03-14T10:15:30.120Z","location":null,"delivery":{"recipientName":null,"deliveryNotes":null},"shipment":{"type":"OUTBOUND"},"returnToSender":null,"newDestination":null}'
print(
    verify_signature(
        raw_body,
        "8XJ/C5JpWFxeZQYFroMBS/JfoHWcVuIxDKtBv0QNP7Q=",
        "fdXbfU27DBNG6LuoHu@ThKl3",
    )
)

Each sample calls its verify_signature/VerifySignature function with InPost's published test vector (shared secret, the body and signature) and prints the result — running any of them should output true.

Example source code


Digital signature generated based on asymmetric cryptography

Digital signatures use a key pair: a private key InPost uses to sign, and a public key you use to verify. Unlike a shared secret, the public key never needs to be exchanged over a secure channel — but verification is more computationally expensive than HMAC.

InPost generates the key pair and publishes the public key as a certificate. For every webhook call, InPost signs the content described above with its private key using SHA256withRSA, and sends the result in x-inpost-signature.

1. Import the InPost public certificate into a keystore

keytool -genkeypair -alias al -keyalg RSA -keysize 2048 -dname "CN=Name" -validity 365 -storetype JKS -keystore test_keystore.jks -storepass testpasswd
keytool -delete -alias al -storepass testpasswd -keystore test_keystore.jks
openssl x509 -outform der -in sandbox-certificate.pem -out sandbox-certificate.der
keytool -import -alias your-alias -keystore test_keystore.jks -file sandbox-certificate.der

This creates an empty keystore test_keystore.jks, then imports the InPost public certificate sandbox-certificate.pem into it under alias your-alias, protected by password testpasswd.

2. Load the public key from the keystore

import java.io.FileInputStream;
import java.security.KeyStore;
import java.security.PublicKey;
import java.security.cert.Certificate;

public PublicKey loadPublicKeyFromKeystore() {
    KeyStore keyStore;
    try {
        keyStore = KeyStore.getInstance("JKS");
        keyStore.load(
                new FileInputStream("test_keystore.jks"),
                "testpasswd".toCharArray());
        Certificate
                certificate = keyStore.getCertificate("your-alias");
        return certificate.getPublicKey();
    } catch (Exception e) {
        throw new RuntimeException(e);
    }
}

3. Verify the signature

import java.nio.charset.StandardCharsets;
import java.security.PublicKey;
import java.security.Signature;
import java.util.Base64;

public boolean verifySignature(String body, PublicKey publicKey, String base64DigitalSignature) {
    try {
        Signature
                signature = Signature.getInstance("SHA256withRSA");
        signature.initVerify(publicKey);
        signature.update(body.getBytes(StandardCharsets.UTF_8));
        return signature.verify(Base64.getDecoder().decode(base64DigitalSignature));
    } catch (Exception e) {
        log.error("Exception during signature verification", e);
    }
    return false;
}

InPost public certificates and an example source code