Webhook Signature Verification
Webhooks allow your system to receive real-time updates regarding transaction lifecycle status transitions. Because network requests can be spoofed, you must validate every incoming payload digitally using the X-Payparse-Signature HTTP header before processing.
Event Structure
{
id: string; // Immutable Event id (also used as webhook event id)
type: "checkout.session.completed" | "payment.confirmed" | "payment.failed" | "payment.pending_review" | "payment.refunded";
created_at: string; // ISO 8601 extended string format
livemode: boolean; // false for sandbox / test-key events
data: {
reference: string;
memo?: string; // Payparse Interac memo (PP-XXXXXX)
amount: number; // Value in cents
status: string;
metadata?: Record<string, any>;
}
}
Event types
| Type | When it fires |
|---|---|
checkout.session.completed | Payment confirmed (live ingest, admin approve, or sandbox confirmed) |
payment.confirmed | Same confirmation path as above |
payment.pending_review | Auto-match failed or sandbox requires_manual_review |
payment.failed | Sandbox failed or admin reject path |
payment.refunded | Full refund marked COMPLETED (exactly once; idempotent retries do not re-send) |
Platform events are immutable. Register endpoints for the types you need; inspect history via GET /v1/events or the Developers dashboard.
Signature Calculation Mechanics
Payparse uses your unique webhook endpoint secret key to calculate an HMAC-SHA256 signature of the raw inbound JSON body string. The signature is transmitted in the X-Payparse-Signature HTTP header on every dispatch.
X-Payparse-Signature: <hmac-sha256-hex(raw_body, webhook_secret)>
Verification Example (Node.js/Express)
Always use a timing-safe string comparison primitive (crypto.timingSafeEqual) to verify signatures, ensuring your verification routes are fully protected against side-channel timing analysis vectors.
const crypto = require('crypto');
const express = require('express');
const app = express();
app.post('/webhooks/payparse', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-payparse-signature'];
const secret = process.env.PAYPARSE_WEBHOOK_SECRET;
// Calculate the expected signature hash from the raw body context
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(req.body)
.digest('hex');
// Perform a timing-safe evaluation loop
const isValid = crypto.timingSafeEqual(
Buffer.from(signature, 'utf8'),
Buffer.from(expectedSignature, 'utf8')
);
if (!isValid) {
return res.status(401).json({ error: 'Invalid webhook signature verification' });
}
// Parse payload and update internal databases safely
const event = JSON.parse(req.body);
if (event.type === 'payment.confirmed') {
// Reconcile corporate accounting ledgers or fulfill invoices asynchronously
console.log(`Payment confirmed for reference: ${event.data.reference}`);
}
if (event.type === 'payment.refunded') {
// Reverse fulfillment — original Interac deposit was not cancelled in-place
console.log(`Refund completed for reference: ${event.data.reference}`);
}
res.status(200).json({ received: true });
});
Key Requirements
- Use
express.raw({ type: 'application/json' })soreq.bodyis the rawBuffer— HMAC must be computed over the exact bytes received, not a re-serialized object. - Compare signatures with
crypto.timingSafeEqualonly after both values areBufferinstances of equal length. - Respond
200 OKimmediately after verification to acknowledge receipt; perform heavier reconciliation work asynchronously.
Retry Behavior
If your endpoint does not respond 2xx, Payparse retries delivery with exponential backoff (up to 5 attempts). Ensure your handler is idempotent so duplicate deliveries do not double-fulfill orders.