Skip to main content

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

TypeWhen it fires
checkout.session.completedPayment confirmed (live ingest, admin approve, or sandbox confirmed)
payment.confirmedSame confirmation path as above
payment.pending_reviewAuto-match failed or sandbox requires_manual_review
payment.failedSandbox failed or admin reject path
payment.refundedFull 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' }) so req.body is the raw Buffer — HMAC must be computed over the exact bytes received, not a re-serialized object.
  • Compare signatures with crypto.timingSafeEqual only after both values are Buffer instances of equal length.
  • Respond 200 OK immediately 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.