Webhook Authentication & Payload

To ensure incoming webhooks are legitimately from Fork PDF and to protect against replay attacks, we secure our payloads using HMAC-SHA256 signatures. You must configure a Webhook Secret in your dashboard to enable signing.

We include an x-webhook-signature header in every POST request. This header contains a Unix timestamp and the calculated signature, formatted as: t=1724958000,v1=9f83...

Important: Raw Payload Verification

HMAC verification requires the exact byte-for-byte string sent by our servers. If your web framework automatically parses incoming requests into JSON objects (like Express), you must capture the raw string body before parsing it to prevent spacing alterations that cause signature mismatches.

const express = require('express');
const crypto = require('crypto');
const app = express();

// 1. Capture the raw string body for accurate HMAC verification
app.use(express.json({
    verify: (req, res, buf) => {
        req.rawBody = buf.toString();
    }
}));

app.post('/webhooks/forkpdf', async (req, res) => {
    const sigHeader = req.headers['x-webhook-signature'];
    if (!sigHeader) return res.status(401).send('Missing signature');

    // 2. Parse the timestamp (t) and signature (v1)
    const parts = sigHeader.split(',').reduce((acc, part) => {
        const [key, value] = part.split('=');
        acc[key] = value;
        return acc;
    }, {});

    if (!parts.t || !parts.v1) return res.status(401).send('Invalid format');

    // 3. Prevent replay attacks (reject requests older than 5 minutes)
    const currentUnix = Math.floor(Date.now() / 1000);
    if (currentUnix - parseInt(parts.t, 10) > 300) {
        return res.status(401).send('Timestamp expired');
    }

    // 4. Compute expected HMAC using the raw string body
    const expectedSignature = crypto
        .createHmac('sha256', process.env.FORKPDF_WEBHOOK_SECRET)
        .update(`${parts.t}.${req.rawBody}`)
        .digest('hex');

    // 5. Compare signatures safely
    if (expectedSignature !== parts.v1) {
        return res.status(401).send('Signature mismatch');
    }

    // --- Signature is valid, acknowledge receipt ---
    res.status(200).send('OK');

    // Proceed with business logic using the safely parsed JSON body
    const pdfResult = req.body;
    
    if (pdfResult.success) {
        console.log(`PDF ${pdfResult.id} ready: ${pdfResult.downloadUrl}`);
    } else {
        console.error(`PDF ${pdfResult.id} failed.`);
    }
});