Troubleshooting Webhook Signature Failures
Amwal signs every incoming webhook dispatch using RSA-PSS with SHA-256 and sends the base64-encoded signature in the X-Signature HTTP header. If your server rejects valid webhook events or signature verification returns false, follow this troubleshooting guide to resolve the failure.
1. The #1 Cause: Hashing Parsed JSON Instead of Raw Request Body
Critical Implementation Rule
Never serialize a parsed JavaScript object (JSON.stringify(req.body)) or Python dict back into JSON to verify the signature! Key ordering, whitespace formatting, and float precision differ from Amwal's original payload stream, causing signature verification to fail. You must verify the signature against the raw unparsed byte stream.
2. Common Causes & Solutions
| Failure Cause | Diagnostic Step | Fix |
|---|---|---|
| Parsing middleware ran before verification | Check if express.json() or body-parser parsed the request before your verifier ran. | Use the verify callback in express.json({ verify: (req, res, buf) => req.rawBody = buf }) or express.raw(). |
| Using wrong RSA Public Key | Check if the Public Key in your .env matches the one returned during endpoint registration. | Webhook public keys are provisioned via POST /api/create-webhook-and-apikey/. Store api_key.public_key securely as AMWAL_PUBLIC_KEY. |
| Incorrect RSA padding mode | Verify whether RSA_PKCS1_PSS_PADDING is configured with saltLength: 32 bytes. | In Node.js, pass padding: crypto.constants.RSA_PKCS1_PSS_PADDING and saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST. In Python, use padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.DIGEST_LENGTH). |
| Whitespace in header | Header string contains trailing carriage returns or spaces. | Run signature.trim() before passing it to base64 decoding. |
| Environment Mismatch | Sandbox webhook dispatch verified against Production public key. | Maintain distinct environment variables for Sandbox (AMWAL_PUBLIC_KEY_SANDBOX) and Production (AMWAL_PUBLIC_KEY_PROD). |
3. Verified Verification Implementations
Node.js / Express
import crypto from 'node:crypto';
import express from 'express';
const app = express();
// 1. Capture raw request body buffer before JSON parsing
app.use(express.json({
verify: (req, _res, buf) => {
req.rawBody = buf;
}
}));
const AMWAL_PUBLIC_KEY = process.env.AMWAL_PUBLIC_KEY;
app.post('/webhook', (req, res) => {
const signature = req.headers['x-signature'];
if (!signature || !req.rawBody) {
return res.status(400).send('Missing X-Signature header or request body');
}
// 2. Verify RSA-PSS SHA-256 signature
const verifier = crypto.createVerify('RSA-SHA256');
verifier.update(req.rawBody);
const isValid = verifier.verify(
{
key: AMWAL_PUBLIC_KEY,
padding: crypto.constants.RSA_PKCS1_PSS_PADDING,
saltLength: crypto.constants.RSA_PSS_SALTLEN_DIGEST,
},
signature,
'base64'
);
if (!isValid) {
console.warn('Rejected webhook: Invalid cryptographic signature');
return res.status(403).send('Invalid signature');
}
// 3. Process valid event
const payload = req.body;
console.log(`Verified webhook event: ${payload.event_type}`);
res.status(200).send('OK');
});Python (FastAPI / Cryptography)
import base64
import os
from fastapi import FastAPI, Request, HTTPException
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives.serialization import load_pem_public_key
app = FastAPI()
PUBLIC_KEY_PEM = os.getenv("AMWAL_PUBLIC_KEY").encode("utf-8")
public_key = load_pem_public_key(PUBLIC_KEY_PEM)
@app.post("/webhook")
async def webhook_handler(request: Request):
signature = request.headers.get("x-signature", "")
if not signature:
raise HTTPException(status_code=400, detail="Missing X-Signature header")
# Read raw body bytes
raw_body = await request.body()
try:
signature_bytes = base64.b64decode(signature)
public_key.verify(
signature_bytes,
raw_body,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.DIGEST_LENGTH
),
hashes.SHA256()
)
except Exception:
raise HTTPException(status_code=403, detail="Invalid signature")
payload = await request.json()
print(f"Processed event: {payload.get('event_type')}")
return {"status": "ok"}4. Manual Debugging with OpenSSL CLI
You can isolate code issues by testing a captured raw payload with OpenSSL:
# 1. Base64 decode the received X-Signature header
echo "BASE64_X_SIGNATURE_STRING" | base64 --decode > signature.bin
# 2. Verify against the raw saved JSON payload
openssl dgst -sha256 \
-sigopt rsa_padding_mode:pss \
-sigopt rsa_pss_saltlen:digest \
-verify amwal_public_key.pem \
-signature signature.bin \
raw_payload.jsonRelated Documentation
Troubleshooting API Authentication & Key Errors
Diagnosing 401 Unauthorized, 403 Forbidden, and Sandbox/Production credential mismatches on Amwal REST APIs.
Troubleshooting Webhook Delivery & Timeout Failures
Diagnosing webhook delivery retries, timeout thresholds, SSL certificate requirements, and firewall IP whitelisting.
