Integrity and Source Verification
Every payload from a payment processor carries two risks: tampering (someone modified the data in transit) and impersonation (someone forged the sender's identity). The Prism provides the tools to eliminate both risks:
| Verification Type | What It Checks | Attack Prevented |
|---|---|---|
| Integrity | Amount, currency, and transaction ID match your records | Data tampering |
| Source | Cryptographic signature using shared secrets | Impersonation, forged webhooks |
Before trusting any webhook or redirect response, the Prism helps you verifying both.
Webhook Signature Verification
Typically Payment processors sign webhooks with a shared secret. You verify the signature before trusting the payload. Prism handles this for every supported processor.
// Incoming webhook from Stripe
const payload = req.body;
const signature = req.headers['stripe-signature'];
// Prism verifies the signature
const result = await client.events.handle({
payload: payload,
signature: signature,
connector: 'stripe',
webhookSecrets: {
secret: process.env.STRIPE_WEBHOOK_SECRET
}
});
// result.source_verified tells you if the webhook is authentic
console.log(result.sourceVerified); // true or false
Supported Algorithms:
| Algorithm | Processors Using It | Security Level |
|---|---|---|
| HMAC-SHA256 | Stripe, Adyen, Checkout.com | Recommended |
| HMAC-SHA1 | Legacy systems | Acceptable |
| HMAC-SHA512 | High-security connectors | Maximum |
The Verification Flow
// 1. Receive webhook
app.post('/webhooks/stripe', async (req, res) => {
const payload = req.body;
const signature = req.headers['stripe-signature'];
// 2. Verify before processing
const result = await client.events.handle({
payload: payload,
signature: signature,
connector: 'stripe',
webhookSecrets: { secret: process.env.STRIPE_WEBHOOK_SECRET }
});
// 3. Check verification result
if (!result.sourceVerified) {
// Reject forged webhooks immediately
return res.status(401).json({ error: 'Invalid signature' });
}
// 4. Process verified event
await processPaymentEvent(result.eventResponse);
res.json({ received: true });
});
Never process unverified webhooks. A forged webhook could mark a failed payment as successful, causing you to ship product for unpaid orders.
Amount and Currency Verification
Webhooks include amounts. Prism verifies these match your records.
{
"event_type": "PAYMENT_INTENT_SUCCESS",
"amount": {
"minor_amount": 5999,
"currency": "USD"
},
"expected_amount": {
"minor_amount": 5999,
"currency": "USD"
}
}
If amounts mismatch, Prism flags the discrepancy:
{
"error": {
"code": "AMOUNT_MISMATCH",
"message": "Webhook amount does not match expected amount",
"webhook_amount": 4999,
"expected_amount": 5999,
"currency": "USD"
}
}
This prevents attacks where an attacker modifies the webhook payload to show a lower amount than actually charged.
Transaction ID Verification
Prism tracks transaction IDs across the lifecycle. When a webhook arrives, it verifies the ID matches an in-flight transaction.
| Check | Failure Mode |
|---|---|
| ID exists in system | Reject unknown transaction IDs |
| Status transition valid | Reject invalid state changes (e.g., SUCCEEDED → PENDING) |
| Amount matches | Reject amount tampering |
| Currency matches | Reject currency switching attacks |
Error: Signature Verification Failed
{
"error": {
"code": "SIGNATURE_VERIFICATION_FAILED",
"message": "Webhook signature does not match payload",
"connector": "stripe",
"suggestion": "Check your webhook secret is correct and the payload was not modified"
}
}
Common causes:
- Wrong webhook secret configured
- Payload modified in transit (proxy, middleware)
- Signature header missing or malformed
Error: Replay Attack Detected
{
"error": {
"code": "DUPLICATE_EVENT",
"message": "Event ID already processed",
"event_id": "evt_1234567890",
"processed_at": "2026-03-19T10:30:00Z"
}
}
Prism tracks event IDs to prevent replay attacks. An attacker cannot resend an old webhook to trigger duplicate actions.
Webhook Secrets Configuration
Configure secrets per connector:
const client = new ConnectorServiceClient({
connector: 'stripe',
webhookSecrets: {
secret: process.env.STRIPE_WEBHOOK_SECRET,
// Some connectors use additional secrets
additionalSecret: process.env.STRIPE_ADDITIONAL_SECRET
}
});
Secret Rotation:
- Generate new secret in processor dashboard
- Update Prism configuration
- Old secret continues working during transition
- Remove old secret after 24 hours
Testing Webhook Verification
Test with forged signatures to verify your rejection logic:
# Send webhook with invalid signature
curl -X POST http://localhost:8080/webhooks/stripe \
-H "Content-Type: application/json" \
-H "Stripe-Signature: forged_signature" \
-d '{"type":"payment_intent.succeeded","id":"evt_test"}'
# Expected: 401 Unauthorized
Best Practices
- Verify before processing — Never trust webhooks without signature verification
- Use HTTPS — Webhooks over HTTP expose secrets to interception
- Log verification failures — Repeated failures indicate attack attempts
- Implement idempotency — Handle duplicate webhooks gracefully even with verification
- Rotate secrets quarterly — Limit exposure window if a secret leaks
Next Steps
- Source Verification — Verify redirect responses and 3DS callbacks
- Error Handling — Handle verification failures in your application