Generating PDFs with Node.js

This guide will show you how to integrate docpenny into your Node.js application. We’ll walk through a complete workflow: submitting a generation job, polling for its completion, and downloading the final document.

What do I need before starting?

Before you begin, ensure you have:

  1. Node.js 18+ installed.
  2. An API Key (get one from Dashboard Settings).
  3. A Template ID (upload one in the Templates dashboard).

1. Project Setup

Create a new directory for your project and initialize it:

mkdir pdf-gen-app
cd pdf-gen-app
npm init -y

We’ll use the native fetch API available in modern Node.js environments.

2. The Implementation

Create a file named generate.js and add the following code. This script demonstrates the full lifecycle of a PDF generation job.

const API_KEY = 'your_api_key_here';
const TEMPLATE_ID = 'your_template_id_here';
const API_BASE_URL = 'https://api.docpenny.com/api';

async function generatePDF() {
    // 1. Submit the Job
    console.log('Submitting PDF generation job...');
    const submitResponse = await fetch(`${API_BASE_URL}/jobs`, {
        method: 'POST',
        headers: {
            'x-api-key': API_KEY,
            'Content-Type': 'application/json',
        },
        body: JSON.stringify({
            templateId: TEMPLATE_ID,
            data: JSON.stringify({
                name: 'Satoshi Nakamoto',
                date: new Date().toLocaleDateString(),
                items: [
                    { desc: 'Technical Whitepaper', qty: 1, price: 0 },
                    { desc: 'Genesis Block', qty: 1, price: 50 }
                ]
            })
        }),
    });

    if (!submitResponse.ok) {
        const error = await submitResponse.json();
        console.error('Failed to submit job:', error);
        return;
    }

    const { jobId } = await submitResponse.json();
    console.log(`Job submitted successfully. Job ID: ${jobId}`);

    // 2. Poll for Status
    let status = 'pending';
    const terminalStates = ['ready_full_zip', 'ready_partial_zip', 'all_tasks_downloaded', 'failed', 'expired'];
    while (!terminalStates.includes(status)) {
        console.log(`Checking status... (${status})`);
        await new Promise(resolve => setTimeout(resolve, 2000));

        const statusResponse = await fetch(`${API_BASE_URL}/jobs/${jobId}`, {
            headers: { 'x-api-key': API_KEY }
        });
        const jobDetails = await statusResponse.json();
        status = jobDetails.status;
    }

    if (status === 'failed' || status === 'expired') {
        console.error(`Job ${status}.`);
        return;
    }

    console.log('Job completed!');

    // 3. Download the Result
    // For bulk jobs, you can download a ZIP of all PDFs
    console.log('Downloading ZIP archive...');
    const downloadResponse = await fetch(`${API_BASE_URL}/download/zip/${jobId}`, {
        headers: { 'x-api-key': API_KEY }
    });

    if (downloadResponse.ok) {
        const fs = require('fs');
        const buffer = await downloadResponse.arrayBuffer();
        fs.writeFileSync(`job_${jobId}.zip`, Buffer.from(buffer));
        console.log(`Saved result to job_${jobId}.zip`);
    } else {
        console.error('Failed to download result.');
    }
}

generatePDF().catch(console.error);

3. Running the Script

Replace the placeholders with your actual credentials and run the script:

node generate.js

What are the best practices?

Handling Webhooks

Polling is great for simplicity, but for high-volume applications, we recommend using Webhooks. You can provide a webhookUrl in the initial POST request, and we’ll notify your server as soon as the job is finished.

One-Time Download

The /api/download/zip/{jobId} endpoint only allows one download per job. After the first successful download, subsequent requests will return a 404. If you need to download the same content again, re-submit the generation job.

How do I verify webhook signatures?

To ensure that a webhook request actually came from docpenny, every request includes an X-Webhook-Signature header. You should verify this signature using your Webhook Secret found in your dashboard.

The signature is an HMAC-SHA256 hash of the raw request body, prefixed with sha256=.

Node.js (Express) Verification Example

const crypto = require('crypto');

function verifyWebhook(req, res, next) {
    const signature = req.headers['x-webhook-signature'];
    const secret = process.env.WEBHOOK_SECRET;

    // Get raw body (requires express.raw() or similar)
    const body = req.body.toString();

    const hmac = crypto.createHmac('sha256', secret);
    const digest = 'sha256=' + hmac.update(body).digest('hex');

    if (signature === digest) {
        next();
    } else {
        res.status(401).send('Invalid signature');
    }
}

Error Handling

Always check the status and error fields in the response. Common errors include insufficient_credits or validation_error (if your data doesn’t match the template requirements).

Secure your Keys

Never commit your API keys to version control. Use environment variables (e.g., via dotenv) to manage sensitive configuration.

en