Skip to main content

Webhook Signatures

Webhook signatures can be used to verify the authenticity and integrity of a received webhook notification. By verifying the webhook signature, merchants can protect themselves from malicious parties sending tampered webhook data to their webhook endpoint.

Configuration

When configuring a webhook subscription, you can generate a secret that will be used to sign the webhook content. Go to the Webhook subscriptions dashboard, select the subscription, open the menu, and select Generate secret. Once created, the secret can be copied from the table and used to verify webhooks.

Note: It may take up to 30 seconds for the signature to appear in webhook notifications after generating or rotating a secret.

Signature Verification

When a secret is enabled for a webhook, the following HTTP headers are sent with every webhook message:

  • timestamp: UNIX timestamp in seconds used to generate the signature.
  • timezone: Timezone.
  • signature: Comma-separated list of signatures for each of the active secrets.

To verify the webhook content:

  1. Compute the RSA value using the webhook subscription secret.
  2. Ensure the computed value matches at least one of the signature header values.
  3. (Optional) Check that the timestamp is not too old to prevent replay attacks.

Important: When extracting the payload, use the exact string as received, including all formatting, spaces, and line breaks. If using a JSON library, use the original value received.

SDK Support

EFundFlow SDKs support verifying webhook signatures. Example usage:

JAVA# example

import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
import java.math.BigDecimal;

public class WebhookSignatureVerifier {

/**
* Verify Webhook signature using the same method as SignUtil.checkSign
* @param payload The original request body string
* @param signature The 'signature' field from header (Base64 string)
* @param publicKey The platform public key (Base64 string, PEM header/footer removed)
* @return true if signature is valid, false otherwise
*/
public static boolean verifySignature(String payload, String signature, String publicKey) {
try {
// 1. Generate signature string using the same method as SignUtil
String signContent = generateSignatureString(payload);

// 2. Restore public key
byte[] keyBytes = Base64.getDecoder().decode(publicKey);
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey pubKey = keyFactory.generatePublic(keySpec);

// 3. Build Signature instance using SHA1withRSA (same as SignUtil)
Signature sig = Signature.getInstance("SHA1withRSA");
sig.initVerify(pubKey);
sig.update(signContent.getBytes(StandardCharsets.UTF_8));

// 4. Verify signature
byte[] signatureBytes = Base64.getDecoder().decode(signature);
return sig.verify(signatureBytes);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}

public static String generateSignatureString(String jsonStr) throws Exception {
Map<String, Object> payment = JSON.parseObject(jsonStr);
StringBuffer content = new StringBuffer();
append(content, payment, "&", "=");
return content.toString();
}

private static void append(StringBuffer content, Map<String, Object> sourceObj, String kvPairSeparator, String kvSeparator) {
if (sourceObj == null) {
return;
}
Map<String, Object> obj = sourceObj;
if (obj.keySet().size() == 0) {
return;
}
List<String> keyList = new ArrayList<String>(obj.keySet().size());
for (String key : obj.keySet()) {
keyList.add(key);
}
Collections.sort(keyList);
for (String key : keyList) {
Object value = obj.get(key);
if (value instanceof List) {
for (int i = 0; i < ((List<?>) value).size(); i++) {
Object item = ((List<?>) value).get(i);
if (item instanceof Map) {
append(content, (Map<String, Object>) item, kvPairSeparator, kvSeparator);
}
}
} else if (value instanceof Map) {
append(content, (Map<String, Object>) value, kvPairSeparator, kvSeparator);
} else if (value instanceof String
|| value instanceof Float
|| value instanceof Double
|| value instanceof Integer
|| value instanceof Long
|| value instanceof BigDecimal
|| value instanceof Boolean) {
if (content.length() > 0) {
content.append(kvPairSeparator);
}
content.append(key);
content.append(kvSeparator);
content.append(value);
}
}
}


public static void main(String[] args) {
String payload = "{...}";
String signature = "Base64SignatureStringFromHeader";
String publicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...";

boolean valid = verifySignature(payload, signature, publicKey);
System.out.println("verify result: " + valid);
}
}

SDKs are available for C#, Go, Java, PHP, Python, and TypeScript.

Secret Rotation

Secrets can be rotated in the webhook subscriptions page. Secret rotation allows you to safely change the secret used to sign webhooks. During the rotation period, signatures for both the old and new secrets are sent. After this period, only the new secret is used.

To rotate a secret:

  1. Generate a new secret using the Rotate secret… menu in the webhook subscriptions page.
  2. Update your webhook endpoint to use the new secret.
  3. Verify that webhooks are processed using the new secret.
  4. Wait for the old secret to expire after the rotation period.

Note: It may take up to 30 seconds for new signatures to appear in webhook notifications after rotation.

Powered by Docusaurus