Generate Invoice PDF with PHP & AJAX

Generate a professional invoice PDF directly from a dynamic web form using PHP, Dompdf, and AJAX for seamless downloads without page reloads. This tutorial leverages your attached scripts for a production-ready implementation with CSRF protection, logo uploads, and real-time calculations.

This tutorial explains how to generate a professional Invoice PDF using PHP and AJAX without page reload. The invoice is created dynamically from a web form, supports multiple line items, tax calculation, optional logo upload, and automatically downloads the generated PDF. Let’s dive into the implementation of the invoice PDF generator using PHP and AJAX!

πŸ”₯ What You’ll Build

  • Dynamic invoice creation form (add/remove items)
  • Real-time subtotal, tax, and total calculation
  • Logo upload with preview (PNG/JPG/GIF)
  • AJAX-based PDF generation (no page reload)
  • Automatic PDF download
  • Secure & optimized PHP backend

πŸ“‹ Prerequisites

Install Dompdf via Composer by running (in your project root):

composer require dompdf/dompdf

Note that: You don’t need to download the Dompdf library separately, all the required library files are included in our source code package.

πŸ“ Project Structure

Organize files as follows for clean deployment:

generate_invoice_pdf/
β”œβ”€β”€ index.php
β”œβ”€β”€ generate_pdf.php
β”œβ”€β”€ assets/
β”‚   β”œβ”€β”€ css/
β”‚   β”‚   └── style.css
β”‚   └── js/
β”‚       └── app.js
└── vendor/
    └── autoload.php

πŸ’» Code Overview

  • index.php: Main HTML form for invoice input and JavaScript for AJAX handling.
  • generate_pdf.php: PHP script that processes form data, generates the PDF using Dompdf, and returns it for download.
  • assets/js/app.js: JavaScript for dynamic form behavior and AJAX submission.
  • assets/css/style.css: Basic styling for the form.

πŸš€ Implementation Steps

  1. Setup HTML Form (index.php): Create a form with fields for customer details, invoice items, tax rate, and logo upload. Include buttons to add/remove items dynamically.
  2. JavaScript for Dynamic Behavior (assets/js/app.js): Write JS to handle adding/removing invoice items, calculating totals in real-time, and submitting the form via AJAX.
  3. PHP Backend (generate_pdf.php): Implement PHP to validate input, process the logo upload, generate the invoice HTML, and convert it to PDF using Dompdf.
  4. AJAX Submission: Use AJAX to send form data to the PHP script and handle the PDF download without reloading the page.
  5. Testing: Test the entire flow to ensure the invoice is generated correctly and downloads as expected.

Create the Invoice Form (index.php)

Create a file named index.php and define HTML elements to build an invoice form with organized sections for customer/seller details, dynamic items table, tax summary, and notes. The invoice form collects:

  • Company & client details (including full addresses)
  • Invoice number & dates
  • Multiple invoice items
  • Tax percentage
  • Optional business logo upload (PNG/JPG/GIF, <2MB)

At the beginning, start a PHP session and generate a CSRF token for security.

<?php 
// Start session
session_start();

// Generate CSRF token
if (empty($_SESSION['csrf_token'])) {
    
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}
$csrf $_SESSION['csrf_token'];
?> <form id="invoice-form" enctype="multipart/form-data" novalidate> <input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf) ?>"> <section class="cols"> <div class="card"> <h2>Customer Information</h2> <div class="row"> <input name="customer_first" placeholder="First name" required> </div> <div class="row"> <input name="customer_last" placeholder="Last name" required> </div> <div class="row"> <input name="customer_business" placeholder="Business name"> </div> <div class="row"> <input name="customer_phone" placeholder="Phone"> </div> <div class="row"> <input name="customer_address" placeholder="Address"> </div> <div class="row"> <input name="customer_city" placeholder="City"> </div> <div class="row"> <input name="customer_state" placeholder="State"> </div> <div class="row"> <input name="customer_zip" placeholder="Zip Code"> </div> <div class="row"> <input name="customer_country" placeholder="Country"> </div> </div> <div class="card"> <h2>Seller Information</h2> <div class="row"> <input name="seller_business" placeholder="Business name" required> </div> <div class="row"> <input name="seller_email" placeholder="Email" type="email" required> </div> <div class="row"> <input name="seller_phone" placeholder="Phone"> </div> <div class="row"> <input name="seller_website" placeholder="Website"> </div> <div class="row"> <input name="seller_address" placeholder="Address"> </div> <div class="row"> <input name="seller_city" placeholder="City"> </div> <div class="row"> <input name="seller_state" placeholder="State"> </div> <div class="row"> <input name="seller_zip" placeholder="Zip Code"> </div> <div class="row"> <input name="seller_country" placeholder="Country"> </div> <div class="row logo-row"> <label>Logo (optional)</label> <div class="logo-input-wrap"> <input id="logo-input" name="logo" type="file" accept="image/*"> <div id="logo-preview" class="logo-preview" style="display:none"> <img id="logo-preview-img" src="" alt="Logo preview"> <button type="button" id="remove-logo" class="remove-logo">Remove</button> </div> </div> </div> </div> </section> <section class="card"> <h2>Invoice Details</h2> <div class="row field-inline"> <label> Invoice # <input name="invoice_number" placeholder="Invoice #" value="<?= htmlspecialchars('INV-001') ?>"> </label> <label> Invoice Date <input name="issue_date" type="date" required value="<?= date('Y-m-d') ?>"> </label> <label> Due Date <input name="due_date" type="date" required> </label> </div> </section> <section class="card"> <h2>Items</h2> <table id="items-table"> <thead> <tr> <th>Item</th> <th>Description</th> <th>Qty</th> <th>Price</th> <th></th> </tr> </thead> <tbody></tbody> </table> <button type="button" class="btn-info" id="add-item">+ Add Item</button> </section> <section class="card"> <div class="summary"> <div> <label>Tax (%)</label> <input id="tax" name="tax" type="number" value="0" min="0" step="0.01"> </div> <div> <label>Subtotal:</label> <span id="subtotal">0.00</span> </div> <div> <label>Tax:</label> <span id="tax-amount">0.00</span> </div> <div class="total"> <label>Total:</label> <span id="total">0.00</span> </div> </div> </section> <section class="card"> <h2>Notes</h2> <textarea name="notes" rows="4" placeholder="Add any notes or terms for this invoice..."></textarea> </section> <div class="actions"> <a href="index.php" class="btn btn-secondary">Reset</a> <button type="submit" class="btn-primary" id="create-btn">Create Invoice (Download PDF)</button> </div> </form>

Include JavaScript handler script (app.js) for dynamic invoice items, real-time subtotal/tax/total updates, and logo preview.

<script src="assets/js/app.js"></script>

Handle Dynamic Invoice Items & Totals with JavaScript (assets/js/app.js)

The app.js file contains JavaScript code to manage dynamic invoice items, calculate subtotals, taxes, and totals in real-time, and handle logo preview functionality. This file manages all client-side interactions.
Features Implemented:

  • Add/remove invoice items: Clone table rows, update arrays like items_name[], items_qty[]
  • Live subtotal, tax, and total calculations: On input change, compute subtotal = sum(qty * price), tax_amount = subtotal * (tax/100), total = subtotal + tax_amount
  • Preview uploaded logo: Use FileReader API to display base64 image in #logo-preview-img
  • AJAX form submission using fetch(): Serialize form data including files via FormData, POST to generate_pdf.php
  • Success handler: Create blob URL from response and trigger saveAs(blob, filename) for direct PDF download

Include error handling for validation failures (e.g., missing items) with user-friendly alerts.

(function(){
    const addItemBtn = document.getElementById('add-item');
    const itemsTbody = document.querySelector('#items-table tbody');
    const taxInput = document.getElementById('tax');
    const subtotalEl = document.getElementById('subtotal');
    const taxAmountEl = document.getElementById('tax-amount');
    const totalEl = document.getElementById('total');
    const form = document.getElementById('invoice-form');
    const logoInput = document.getElementById('logo-input');
    const logoPreviewWrap = document.getElementById('logo-preview');
    const logoPreviewImg = document.getElementById('logo-preview-img');
    const removeLogoBtn = document.getElementById('remove-logo');

    // Function for dynamic item row creation
    function createRow(item={name:'',desc:'',qty:1,price:0}){
        const tr = document.createElement('tr');
        tr.innerHTML = `
            <td><input name="items_name[]" required value="${escapeHtml(item.name)}"></td>
            <td><input name="items_desc[]" value="${escapeHtml(item.desc)}"></td>
            <td><input name="items_qty[]" class="small" type="number" min="0" step="1" required value="${item.qty}"></td>
            <td><input name="items_price[]" class="small" type="number" min="0" step="0.01" required value="${item.price}"></td>
            <td><button type="button" class="remove-item">Remove</button></td>
        `;
        itemsTbody.appendChild(tr);
        tr.querySelector('.remove-item').addEventListener('click', ()=>{tr.remove(); computeTotals();});
        tr.querySelectorAll('input').forEach(i=>i.addEventListener('input', computeTotals));
    }

    function escapeHtml(s){ return String(s).replace(/[&<>\"]/g, function(c){return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c];}); }

    addItemBtn.addEventListener('click', ()=>createRow());
    taxInput.addEventListener('input', computeTotals);

    // Function for real-time total calculation
    function computeTotals(){
        let subtotal=0;
        const qtys = Array.from(document.getElementsByName('items_qty[]'));
        const prices = Array.from(document.getElementsByName('items_price[]'));
        for(let i=0;i<qtys.length;i++){
            const q = parseFloat(qtys[i].value)||0;
            const p = parseFloat(prices[i].value)||0;
            subtotal += q*p;
        }
        const taxPerc = parseFloat(taxInput.value)||0;
        const taxAmt = subtotal * (taxPerc/100);
        const total = subtotal + taxAmt;
        subtotalEl.textContent = '$'+subtotal.toFixed(2);
        taxAmountEl.textContent = '$'+taxAmt.toFixed(2);
        totalEl.textContent = '$'+total.toFixed(2);
    }

    // initial one item
    createRow({name:'',desc:'',qty:1,price:0});
    computeTotals();

    // Logo preview handling
    if(logoInput){
        logoInput.addEventListener('change', function(){
            const f = this.files && this.files[0];
            if(!f){ logoPreviewWrap.style.display='none'; logoPreviewImg.src=''; return; }
            if(f.size > 2*1024*1024){ alert('Logo must be under 2 MB'); this.value=''; return; }
            const allowed = ['image/png','image/jpeg','image/gif'];
            if(!allowed.includes(f.type)) { alert('Only PNG/JPEG/GIF allowed'); this.value=''; return; }
            const reader = new FileReader();
            reader.onload = function(ev){ logoPreviewImg.src = ev.target.result; logoPreviewWrap.style.display='flex'; };
            reader.readAsDataURL(f);
        });
        removeLogoBtn.addEventListener('click', function(){
            logoInput.value = '';
            logoPreviewImg.src = '';
            logoPreviewWrap.style.display = 'none';
        });
    }

    // Form submission and PDF generation
    form.addEventListener('submit', async function(e){
        e.preventDefault();
        const btn = document.getElementById('create-btn');
        if(!form.checkValidity()){
            form.reportValidity();
            return;
        }
        btn.disabled = true; btn.textContent = 'Generating...';
        const data = new FormData(form);
        try{
            const resp = await fetch('generate_pdf.php', {method:'POST', body:data});
            if(!resp.ok){
                const text = await resp.text();
                throw new Error(text || 'Server error');
            }
            const blob = await resp.blob();
            const filename = getFileNameFromDisposition(resp.headers.get('Content-Disposition')) || 'invoice.pdf';
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); a.remove();
            URL.revokeObjectURL(url);
        }catch(err){
            alert('Error: '+err.message);
        }finally{ btn.disabled=false; btn.textContent='Create Invoice (Download PDF)'; }
    });

    // Helper to extract filename from Content-Disposition header
    function getFileNameFromDisposition(disposition){
        if(!disposition) return null;
        const match = /filename\*?=([^;]+)/i.exec(disposition);
        if(!match) return null;
        let fn = match[1].trim();
        fn = fn.replace(/UTF-8''/i,'');
        fn = fn.replace(/"/g,'');
        return decodeURIComponent(fn);
    }
})();

Generate Invoice PDF in PHP (generate_pdf.php)

The generate_pdf.php file processes the submitted form data, generates a PDF invoice using the Dompdf library, and returns it for download using PHP. It includes CSRF protection, input sanitization, and process POST data securely and output binary PDF.

  • Session/CSRF validation: hash_equals($_SESSION['csrftoken'], $_POST['csrftoken'])
  • Input sanitization: Custom clean() for HTML escape, floatval_safe() for numerics
  • Logo handling: Validate MIME/size, convert to base64 data URI for embedding
  • HTML template: Flexbox header with seller logo/invoice details, bordered items table, summary with subtotal/tax/total
  • Dompdf rendering: $dompdf->loadHtml($html); $dompdf->setPaper('A4', 'portrait'); $pdf = $dompdf->output();
  • Binary headers: Clean output buffers, set Content-Type: application/pdf, Content-Disposition: attachment

Use number_format($value, 2) for currency formatting and nl2br() for notes.

<?php 
// Start session
session_start();

// Load Dompdf library
require __DIR__ '/vendor/autoload.php';
use 
Dompdf\Dompdf;
use 
Dompdf\Options;

// Only accept POST
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    
http_response_code(405);
    echo 
'Method not allowed';
    exit;
}

// Validate CSRF token
$csrf $_POST['csrf_token'] ?? '';
if (empty(
$_SESSION['csrf_token']) || !hash_equals($_SESSION['csrf_token'], $csrf)) {
    
http_response_code(400);
    echo 
'Invalid CSRF token';
    exit;
}

// Helper sanitizers
function clean($s){ return htmlspecialchars(trim($s), ENT_QUOTES'UTF-8'); }
function 
floatval_safe($v){ return is_numeric($v) ? (float)$v 0.0; }

// Collect and sanitize fields
$customer = [
    
'first'=>clean($_POST['customer_first'] ?? ''),
    
'last'=>clean($_POST['customer_last'] ?? ''),
    
'business'=>clean($_POST['customer_business'] ?? ''),
    
'phone'=>clean($_POST['customer_phone'] ?? ''),
    
'address'=>clean($_POST['customer_address'] ?? ''),
    
'city'=>clean($_POST['customer_city'] ?? ''),
    
'state'=>clean($_POST['customer_state'] ?? ''),
    
'zip'=>clean($_POST['customer_zip'] ?? ''),
    
'country'=>clean($_POST['customer_country'] ?? ''),
];
$seller = [
    
'business'=>clean($_POST['seller_business'] ?? ''),
    
'email'=>filter_var($_POST['seller_email'] ?? ''FILTER_SANITIZE_EMAIL),
    
'phone'=>clean($_POST['seller_phone'] ?? ''),
    
'website'=>filter_var($_POST['seller_website'] ?? ''FILTER_SANITIZE_URL),
    
'address'=>clean($_POST['seller_address'] ?? ''),
    
'city'=>clean($_POST['seller_city'] ?? ''),
    
'state'=>clean($_POST['seller_state'] ?? ''),
    
'zip'=>clean($_POST['seller_zip'] ?? ''),
    
'country'=>clean($_POST['seller_country'] ?? ''),
];
$invoice = [
    
'number'=>clean($_POST['invoice_number'] ?? ''),
    
'issue'=>clean($_POST['issue_date'] ?? ''),
    
'due'=>clean($_POST['due_date'] ?? ''),
];
$notes clean($_POST['notes'] ?? '');
$tax floatval_safe($_POST['tax'] ?? 0);

// Collect invoice items
$items = [];
$names $_POST['items_name'] ?? [];
$descs $_POST['items_desc'] ?? [];
$qtys $_POST['items_qty'] ?? [];
$prices $_POST['items_price'] ?? [];
for(
$i=0;$i<count($names);$i++){
    
$n clean($names[$i] ?? '');
    
$d clean($descs[$i] ?? '');
    
$q intval($qtys[$i] ?? 0);
    
$p floatval_safe($prices[$i] ?? 0);
    if(
$n === '' && $d === '' && $q<=&& $p==0) continue;
    
$items[] = ['name'=>$n,'desc'=>$d,'qty'=>$q,'price'=>$p];
}
if(empty(
$items)){
    
http_response_code(400);
    echo 
'At least one invoice item is required.';
    exit;
}

// Handle logo upload (optional).
$tempLogo '';
if (!empty(
$_FILES['logo']) && $_FILES['logo']['error'] !== UPLOAD_ERR_NO_FILE) {
    
// Check for upload errors
    
if ($_FILES['logo']['error'] !== UPLOAD_ERR_OK) {
        
http_response_code(400);
        exit(
'Logo upload error');
    }

    
// File type validation
    
$allowed = ['image/png''image/jpeg''image/gif'];

    
$finfo finfo_open(FILEINFO_MIME_TYPE);
    
$mime  finfo_file($finfo$_FILES['logo']['tmp_name']);
    
finfo_close($finfo);

    if (!
in_array($mime$allowed)) {
        
http_response_code(400);
        exit(
'Invalid logo type');
    }

    
// File size limit (2MB)
    
if ($_FILES['logo']['size'] > 1024 1024) {
        
http_response_code(400);
        exit(
'Logo too large');
    }

    
// Temp file path
    
$tmpPath $_FILES['logo']['tmp_name'];

    
// Build base64 data URI
    
$imageData file_get_contents($tmpPath);
    
$base64    'data:' $mime ';base64,' base64_encode($imageData);
    
$tempLogo    str_replace(' ''+'$base64);
}

// Build HTML for PDF
function money($v){ return number_format((float)$v,2); }
$subtotal 0;
$itemsHtml '';
foreach(
$items as $it){
    
$line $it['qty'] * $it['price'];
    
$subtotal += $line;
    
$itemsHtml .= "<tr>\n".
        
"<td style=\"border:1px solid #ccc;padding:8px;\">"$it['name'] ."</td>\n".
        
"<td style=\"border:1px solid #ccc;padding:8px;\">"$it['desc'] ."</td>\n".
        
"<td style=\"border:1px solid #ccc;padding:8px;text-align:right;\">"$it['qty'] ."</td>\n".
        
"<td style=\"border:1px solid #ccc;padding:8px;text-align:right;\">"money($it['price']) ."</td>\n".
        
"<td style=\"border:1px solid #ccc;padding:8px;text-align:right;\">"money($line) ."</td>\n".
        
"</tr>\n";
}
$taxAmt $subtotal * ($tax/100);
$total $subtotal $taxAmt;

$html '<!doctype html><html><head><meta charset="utf-8"><style>' .
    
'body{font-family:Arial,Helvetica,sans-serif;font-size:12px;color:#222} .header{display:flex;justify-content:space-between;align-items:center} .seller{font-weight:700}' .
    
'table{width:100%;border-collapse:collapse;margin-top:12px}' .
    
'</style></head><body>';
$html .= '<div class="header">';
$html .= '<div class="left">';

if (!empty(
$tempLogo)) {
    
$html .= '<img src="' $tempLogo '" height="80" style="margin-bottom:10px;">';
}

$html .= '<div class="seller">'$seller['business'] .'</div>';
$html .= '<div>'$seller['address'] .'<br>'$seller['city'].' '$seller['state'].' '$seller['zip'].'<br>'$seller['country'] .'</div>';
$html .= '</div>';
$html .= '<div class="right">';
$html .= '<h2>Invoice</h2>';
$html .= '<div><strong>Invoice #:</strong> '$invoice['number'] .'</div>';
$html .= '<div><strong>Issue:</strong> '$invoice['issue'] .'</div>';
$html .= '<div><strong>Due:</strong> '$invoice['due'] .'</div>';
$html .= '</div></div>';

$html .= '<hr>';
$html .= '<div style="display:flex;justify-content:space-between">';
$html .= '<div><strong>Bill To:</strong><br>'$customer['first'].' '$customer['last'] .'<br>'$customer['business'] .'<br>'$customer['address'] .'<br>'$customer['city'].' '$customer['state'].' '$customer['zip'] .'<br>'$customer['country'] .'</div>';
$html .= '</div>';

$html .= '<table><thead><tr><th style="border:1px solid #ccc;padding:8px">Item</th><th style="border:1px solid #ccc;padding:8px">Description</th><th style="border:1px solid #ccc;padding:8px">Qty</th><th style="border:1px solid #ccc;padding:8px">Price</th><th style="border:1px solid #ccc;padding:8px">Line Total</th></tr></thead><tbody>';
$html .= $itemsHtml;
$html .= '</tbody></table>';

$html .= '<div style="width:300px;margin-left:auto;margin-top:12px">';
$html .= '<table style="width:100%">';
$html .= '<tr><td style="padding:6px">Subtotal:</td><td style="padding:6px;text-align:right">$'.money($subtotal).'</td></tr>';
$html .= '<tr><td style="padding:6px">Tax ('.htmlspecialchars($tax).'%):</td><td style="padding:6px;text-align:right">$'.money($taxAmt).'</td></tr>';
$html .= '<tr><td style="padding:6px;font-weight:700">Total:</td><td style="padding:6px;text-align:right;font-weight:700">$'.money($total).'</td></tr>';
$html .= '</table></div>';

if(
$notes){ $html .= '<h3>Notes</h3><p>'nl2br($notes) .'</p>'; }

$html .= '</body></html>';

// Generate PDF via Dompdf
$options = new Options();
$options->set('isRemoteEnabled'true);

$dompdf = new Dompdf($options);
$dompdf->loadHtml($html);
$dompdf->setPaper('A4','portrait');
$dompdf->render();

// Render PDF to string (avoid direct streaming which can be corrupted by prior output)
$pdf $dompdf->output();

// Ensure no accidental output (warnings/notices) remain β€” clean all output buffers
while (ob_get_level() > 0) {
    @
ob_end_clean();
}

// Turn off display_errors so PHP won't inject warnings into the binary stream
ini_set('display_errors''0');

// Send proper binary headers
$filename 'invoice_'.time().'.pdf';
header('Content-Type: application/pdf');
header('Content-Disposition: attachment; filename="' rawurlencode($filename) . '"; filename*=UTF-8\'\'' rawurlencode($filename));
header('Content-Transfer-Encoding: binary');
header('Accept-Ranges: bytes');
header('Content-Length: ' strlen($pdf));

echo 
$pdf;
exit;
?>

πŸ” Security & Optimization Tips

  • Escape user input before output
  • Limit upload size (2MB recommended)
  • Remove uploaded logo after PDF generation
  • Use HTTPS for production
  • Cache fonts in Dompdf for faster rendering

🎯 Final Output

βœ” Professional invoice PDF
βœ” Dynamic & reusable
βœ” Client-ready
βœ” Perfect for SaaS, billing systems, freelancers

πŸš€ Conclusion

With this guide, you can create dynamic, professional invoices in PDF format using PHP and Dompdf. Customize the template to fit your brand and business needs. This Generate Invoice PDF with PHP (AJAX) solution is:

  • Beginner-friendly
  • Production-ready
  • Easily customizable
  • Ideal for real-world billing applications

You can extend this Invoice PDF Generator script further by adding Database storage, Invoice numbers, Currency selection, Email PDF to client, and Payment gateway integration. Happy coding!

If you want complete project with all features, check out our PHP Invoice Management System Script.

πŸ”— Related Resources

– Convert HTML to PDF in PHP with Dompdf: https://www.codexworld.com/convert-html-to-pdf-php-dompdf/
– Export MySQL Data to Excel using PHP: https://www.codexworld.com/export-data-to-excel-in-php/

Looking for expert assistance to implement or extend this script’s functionality? Submit a Service Request

Leave a reply

construction Need this implemented in your project? Request Implementation Help β†’ keyboard_double_arrow_up