Webhooks & Signature Verification
Webhooks allow your application to receive real-time notifications when an asynchronous extraction or audit job completes.
Supported Events
job.completed: Dispatched when receipt extraction or submission audit completes successfully.job.failed: Dispatched if an unrecoverable failure occurs after exhausting retry attempts.
Webhook Payload Example
{
"event": "job.completed",
"jobId": "job_01HXYZ1234",
"tenantId": "stepup",
"type": "receipt_extraction",
"timestamp": 1726084205,
"result": {
"vendor": "Kumon Math & Reading Center",
"amount": 180.00,
"transactionDate": "2026-09-02",
"invoiceType": "service",
"items": [
{
"description": "Monthly Math Tutoring - September 2026",
"price": 180.00,
"quantity": 1
}
],
"paymentDetails": {
"isPaid": true,
"paymentMethod": "Visa ending in 4128",
"cardLast4": "4128",
"proofOfPaymentType": "card"
}
}
}
Verifying Signatures
To ensure that webhook payloads genuinely originated from HeyPeppy and have not been tampered with, every webhook request includes an HMAC-SHA256 signature in the X-HeyPeppy-Signature header.
Header Format
X-HeyPeppy-Signature: t=1726084205,v1=9b3a1c0d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b
Where:
tis the Unix timestamp of when the webhook was generated.v1is the hexadecimal HMAC-SHA256 signature calculated over${t}.${rawBody}using your webhook secret.
Node.js Verification Example
import crypto from "crypto";
export function verifyHeyPeppyWebhook(
rawBody: string,
signatureHeader: string,
secret: string,
toleranceSeconds: number = 300,
): boolean {
const parts = signatureHeader.split(",");
const timestampStr = parts.find((p) => p.startsWith("t="))?.slice(2);
const signature = parts.find((p) => p.startsWith("v1="))?.slice(3);
if (!timestampStr || !signature) return false;
const timestamp = parseInt(timestampStr, 10);
const now = Math.floor(Date.now() / 1000);
// Prevent replay attacks
if (Math.abs(now - timestamp) > toleranceSeconds) {
return false;
}
const payloadToSign = `${timestamp}.${rawBody}`;
const expectedSignature = crypto
.createHmac("sha256", secret)
.update(payloadToSign, "utf8")
.digest("hex");
return crypto.timingSafeEqual(
Buffer.from(signature, "hex"),
Buffer.from(expectedSignature, "hex"),
);
}