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
- Content to sign is either:
- a request body
- a timestamp taken from the
x-inpost-timestampheader (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.
- That content is encoded to bytes as UTF-8.
- A signature is computed over those bytes — using HMAC-SHA256 + your shared secret for HMAC.
- The resulting bytes are Base64-encoded (standard alphabet, with padding) and sent as-is in the
x-inpost-signatureheader.
Use the raw bytes, not a re-serialized payload. Always compute the signature over the exact bytes InPost sent, before any JSON parsing. Deserializing the body into an object and re-serializing it (even to "the same" JSON) can reorder keys or change whitespace, which silently breaks verification. Read the raw request body/stream in your framework instead of the parsed body — the language examples below show how.
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-signatureheader, unmodified
Next:
- Take the raw request body exactly as received.
- Build the content to sign:
<body>or<x-inpost-timestamp>.<body>(depending on your configuration). - Compute HMAC-SHA256 over that content (UTF-8 bytes), using your shared secret (UTF-8 bytes) as the key.
- Base64-encode the result.
- Compare it to
x-inpost-signatureusing 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;
}