📄 PHP Invoice Payment Stamping Engine Architecture: FPDI, Batch Processing, and Original Document Preservation
title: "📄 PHP Invoice Payment Stamping Engine Architecture: FPDI, Batch Processing, and Original Document Preservation"
published: true
description: "A technical overview of a PHP invoice payment stamping engine using FPDI, FPDF, and JSZip, with individual and batch processing, local archiving, and original document preservation."
tags: php, architecture, pdf, automation
cover_image: https://direct_url_to_image.jpg
Use a ratio of 100:42 for best results.
published_at: 2026-11-15 20:41 +0000
A PDF invoice generally represents a document that has already been generated, transmitted, and potentially archived.
When a payment is received, it may be necessary to produce a paid version of the invoice, including a payment confirmation stamp and the corresponding payment date.
One approach is to modify the existing document directly. Another is to generate a new PDF from the original and overlay the payment information.
The second approach preserves the original document while producing a separate visual payment confirmation.
This article presents the architecture of a PHP invoice payment stamping engine using FPDI and FPDF, with support for individual PDF files, ZIP batch processing, and optional local archiving without a database.
1. Separating the Original Invoice From Its Payment Confirmation
The first architectural principle is to never modify the original file.
An invoice may already be stored in an accounting system, associated with a reference, or integrated into an electronic invoicing workflow.
Directly modifying that document can introduce several problems:
- Loss of the original version
- Modification of an already archived document
- Potential alteration of signatures or metadata
- Loss of embedded data
The engine therefore separates the source document from the generated document.
Original invoice
│
â–¼
PDF import
│
â–¼
Generate a copy
│
â–¼
Overlay payment stamp
│
â–¼
Paid invoice
The original file remains unchanged.
The generated document contains a visual representation of the invoice, supplemented by a red payment confirmation stamp and a payment date.
This separation is particularly important when a PDF contains structured data or other elements that are not visible in its graphical representation.
2. General Engine Architecture
The engine is designed to be deployed directly within the client's environment.
It relies on a protected web interface, a PHP engine, and specialized PDF processing libraries.
No database is required to store documents.
Browser
│
â–¼
Protected web interface
│
â–¼
PHP engine
│
├── Authentication
├── File processing
├── ZIP extraction
├── PDF generation
└── Optional archiving
│
â–¼
Download generated result
Documents are processed temporarily and made available for immediate download.
Depending on the deployment configuration, a stamped copy can also be retained in a dedicated local archive directory.
The engine was designed as an add-on to a Factur-X EN16931 batch invoicing system, but it can operate independently of the main service after installation.
3. Project Structure
The documented project structure separates the web interface, internal engine, dependencies, and storage directories.
invoice-stamper/
│
├── temp_runtime/
│ └── .htaccess
│
├── dependencies/
├── paid_archives/
├── direct_archives/
├── logs/
│
├── access.php
├── stamper-engine.php
├── .htaccess
├── LICENCE.md
│
└── docs/
└── README_FR.md
Each component has a distinct responsibility.
| Component | Responsibility |
|---|---|
access.php |
Public web interface entry point |
stamper-engine.php |
Authentication and PDF processing |
temp_runtime/ |
Temporary processing files |
dependencies/ |
Composer libraries |
paid_archives/ |
Archived invoices from batch mode |
direct_archives/ |
Archived PDFs from direct mode |
logs/ |
Error logs |
.htaccess |
Protection of internal resources |
This organization separates the files required to execute the engine from the documents handled during processing.
It also distinguishes temporary files from documents intended for long-term retention.
The public documentation describes this architecture but does not distribute the production files.
4. FPDI and FPDF: Importing and Reconstructing a Document
PDF processing relies on two complementary libraries.
| Library | Responsibility |
|---|---|
| FPDI | Import pages from an existing PDF |
| FPDF | Generate a new PDF document |
FPDI allows pages from an existing PDF to be imported and used as templates within a new document.
FPDF then provides the functionality required to add text, shapes, and other graphical elements.
The processing principle is:
Original PDF
│
â–¼
FPDI: import pages
│
â–¼
FPDF: create new document
│
â–¼
Add payment confirmation
│
â–¼
Generated PDF
The operation is not simply a matter of opening a PDF and modifying a few bytes.
The engine reconstructs a new document from the imported page content.
This distinction explains why certain properties of the original PDF may not be preserved in the generated result.
5. Educational Example: Overlaying a Payment Stamp
A simplified example illustrates how FPDI and FPDF can be used together.
<?php
require 'vendor/autoload.php';
use setasign\Fpdi\Fpdi;
$pdf = new Fpdi();
$pageCount = $pdf->setSourceFile('invoice.pdf');
for ($page = 1; $page <= $pageCount; $page++) {
$template = $pdf->importPage($page);
$size = $pdf->getTemplateSize($template);
$pdf->AddPage(
$size['orientation'],
[$size['width'], $size['height']]
);
$pdf->useTemplate($template);
if ($page === 1) {
$pdf->SetFont('Helvetica', 'B', 16);
$pdf->SetTextColor(200, 0, 0);
$pdf->SetXY(15, 15);
$pdf->Cell(0, 8, 'PAID');
$pdf->SetFont('Helvetica', '', 10);
$pdf->SetXY(15, 25);
$pdf->Cell(0, 6, 'Payment date: 2026-11-15');
}
}
$pdf->Output('F', 'invoice_paid.pdf');
This is an educational example. It does not reproduce the production engine, its internal paths, or the exact positioning of its payment stamp.
It illustrates several important operations:
- Importing each page of the source document
- Retrieving page dimensions and orientation
- Reproducing the page in a new document
- Adding graphical information
- Saving the result as a separate file
In this example, the payment stamp is applied only to the first page.
A real implementation must consider the document layout to avoid placing the stamp over important invoice information.
The invoice.pdf file is never rewritten. Only invoice_paid.pdf is generated.
6. Preserving Page Dimensions and Orientation
Not every PDF uses an A4 portrait layout.
An invoice may contain multiple pages, different page dimensions, or landscape-oriented content.
It is therefore preferable to retrieve the characteristics of each imported page.
$template = $pdf->importPage($page);
$size = $pdf->getTemplateSize($template);
$pdf->AddPage(
$size['orientation'],
[$size['width'], $size['height']]
);
This approach avoids imposing an arbitrary page format on the generated document.
Stamp positioning introduces another consideration.
A fixed position may work well for an invoice generated by a known invoicing engine but become problematic when processing a document from another system.
An engine that accepts external PDF files must consider several constraints.
| Constraint | Potential issue |
|---|---|
| Page format | Incorrect positioning or scaling |
| Orientation | Incorrect stamp placement |
| Existing content | Overlaying important information |
| Page count | Stamping an inappropriate page |
These considerations illustrate general PDF processing challenges. They do not imply that the documented engine includes an automatic free-space detection algorithm.
7. Two Processing Modes: Direct and Batch
The engine provides two processing modes.
The first handles an individual PDF document.
The second processes a monthly ZIP archive containing multiple invoices.
Payment stamping engine
│
├── Direct mode
│ └── Individual PDF
│
└── Batch mode
├── Monthly ZIP
├── Invoice list
├── Individual stamping
└── Group stamping
Both modes use the same PDF generation principle, but they handle documents differently.
Direct Mode
The user uploads a PDF, enters a payment date, and downloads the stamped document.
Upload PDF
│
â–¼
Enter payment date
│
â–¼
Validation
│
â–¼
Generate paid PDF
│
â–¼
Download
This mode is not limited to invoices generated by the batch invoicing system.
It accepts PDFs from other systems, provided their characteristics are compatible with the PDF processing libraries.
Batch Mode
Batch mode begins with the upload of a monthly ZIP archive.
The engine detects the invoices and presents a list containing information such as the client, invoice reference, and billing period.
The user can then stamp an individual invoice or select multiple documents.
This distinction provides a simple workflow for occasional processing while supporting larger document batches.
8. Processing a Monthly ZIP Archive
Batch processing adds an orchestration layer around the PDF engine.
Monthly ZIP archive
│
â–¼
Temporary extraction
│
â–¼
Invoice detection
│
â–¼
Display invoice list
│
â–¼
Document selection
│
â–¼
Generate paid PDFs
│
â–¼
Build output archive
The input archive is not necessarily identical to the output archive.
The result contains the selected and stamped documents, organized by client reference.
The documented output structure is:
factures_acquittees.zip
│
└── clientRef/
├── F-2025-001_ACQUITTEE.pdf
└── F-2025-002_ACQUITTEE.pdf
This organization facilitates the transmission of documents to the batch invoicing system.
It also preserves a readable association between each invoice and its client without requiring an additional database.
9. ZIP Extraction and File Security
Extracting a user-supplied archive is a sensitive operation.
An archive can contain unexpected files, relative paths, or very large amounts of compressed data.
For example, a ZIP entry could contain a path such as:
../../outside.php
Insufficiently controlled extraction could allow a file to be written outside the intended directory. This vulnerability category is commonly known as Zip Slip.
One possible strategy is to validate each entry before extraction.
function isSafeZipEntry(string $name): bool
{
$name = str_replace('\\', '/', $name);
if (
str_starts_with($name, '/') ||
preg_match('/^[a-zA-Z]:/', $name)
) {
return false;
}
$parts = explode('/', $name);
return !in_array('..', $parts, true);
}
This example illustrates basic path validation.
It is not a complete security solution and does not reproduce the internal controls of the engine.
A robust implementation must also consider entry types, symbolic links, file size limits, file counts, and total decompressed size.
The documented engine uses a dedicated temporary directory for archive extraction and PDF generation, with protection against direct web access.
10. Individual Stamping and Multiple Selection
Batch mode supports stamping one invoice at a time or processing several documents with a shared payment date.
From an architectural perspective, individual document processing can be separated from batch orchestration.
Selected invoices
│
â–¼
Validate payment date
│
â–¼
For each invoice
│
├── Import PDF
├── Generate payment stamp
└── Create output document
│
â–¼
Prepare result
A conceptual selection model could look like this:
{
"payment_date": "2026-11-15",
"invoices": [
{
"reference": "F-2026-001",
"client": "CLIENT-A"
},
{
"reference": "F-2026-002",
"client": "CLIENT-A"
}
]
}
This JSON is illustrative. It does not represent a public data exchange format used by the engine.
The shared payment date is applied to the selected documents.
In a real application, invoice references received from the browser must be validated on the server to ensure that they correspond to documents accessible within the current batch.
A reference supplied by the client should not be interpreted directly as a filesystem path.
11. Generating ZIP Archives in the Browser
The project also uses JSZip to generate ZIP archives on the client side.
This library allows an archive to be assembled directly in the browser from files that are already available.
An educational example could look like this:
const zip = new JSZip();
const clientFolder = zip.folder("CLIENT-A");
clientFolder.file(
"F-2026-001_ACQUITTEE.pdf",
firstPdfBlob
);
clientFolder.file(
"F-2026-002_ACQUITTEE.pdf",
secondPdfBlob
);
const archive = await zip.generateAsync({
type: "blob"
});
The resulting archive can then be offered for download:
const url = URL.createObjectURL(archive);
const link = document.createElement("a");
link.href = url;
link.download = "factures_acquittees.zip";
document.body.appendChild(link);
link.click();
link.remove();
setTimeout(() => {
URL.revokeObjectURL(url);
}, 1000);
These examples assume that the generated PDF documents are already available as Blob objects.
They illustrate client-side ZIP generation without reproducing the protocol used by the engine to transfer the documents.
This approach separates two responsibilities:
PHP server
└── Generate paid PDF documents
Browser
└── Assemble ZIP archive
Client-side archive generation does not mean that all PDF processing occurs locally in the browser. The PDF documents are processed by the PHP engine within the client's environment.
JSZip is loaded from a CDN in the documented architecture. This dependency may therefore require network access unless it is hosted locally in a different deployment configuration.
12. Factur-X: Preserving the Original Does Not Mean Preserving XML in the Copy
The engine was originally designed as an add-on to a Factur-X EN16931 batch invoicing system.
This introduces an important technical constraint.
A Factur-X invoice combines two representations:
Original Factur-X invoice
│
├── PDF/A-3
│ └── Human-readable document
│
└── Embedded XML
└── Structured invoice data
When an engine imports PDF pages to generate a new document, it may reproduce their appearance without preserving every internal element of the original file.
Original Factur-X invoice
│
â–¼
Import PDF pages
│
â–¼
Graphical reconstruction
│
â–¼
Paid PDF
The generated document may therefore preserve the invoice's visual appearance without preserving its embedded XML.
It may also no longer satisfy the PDF/A-3 requirements of the source document.
The engine does not guarantee the preservation of embedded Factur-X XML data in reprocessed PDF files.
This limitation must remain explicit.
| Document | Role |
|---|---|
| Original Factur-X | Invoice and structured data |
| Generated paid PDF | Visual payment confirmation |
The stamped PDF should therefore not automatically be presented as a new compliant Factur-X invoice.
Producing a genuinely modified Factur-X document would require preserving or reconstructing the required components, verifying data consistency, and validating the result against the applicable specifications.
That is not the objective of the payment stamping engine presented here.
13. Temporary Files and Local Archiving
The engine distinguishes between two storage categories.
Temporary files are required during ZIP extraction and document generation.
Paid archives contain copies retained when archiving is enabled.
Local storage
│
├── temp_runtime/
│ ├── ZIP extraction
│ └── PDF generation
│
├── paid_archives/
│ └── Batch mode documents
│
└── direct_archives/
└── Direct mode documents
Temporary files are deleted after processing.
Optional archives remain separate from this working directory.
This separation prevents documents required for an active operation from being confused with documents intended for long-term retention.
Isolating Processing Jobs
An engine handling multiple documents can benefit from a separate working directory for each processing job.
For example:
$jobId = bin2hex(random_bytes(16));
$workDir = sys_get_temp_dir()
. DIRECTORY_SEPARATOR
. 'invoice-' . $jobId;
if (!mkdir($workDir, 0700)) {
throw new RuntimeException(
'Unable to create temporary directory.'
);
}
This example illustrates a generic job isolation strategy.
It does not describe the exact mechanism used within temp_runtime/.
Isolation can reduce filename collisions between concurrent operations and simplify the removal of files associated with a specific job.
Reliable cleanup must also account for processing errors and interrupted executions.
14. Operating Without a Database
The engine does not use a database to store documents.
PDF files and ZIP archives are the primary resources handled by the system.
Inputs
├── Individual PDF
└── ZIP archive
Processing
├── Temporary files
├── Payment information
└── PDF generation
Outputs
├── Paid PDF
├── Structured ZIP
└── Optional local archive
This architecture is suitable for a tool whose primary responsibility is transforming existing documents.
It avoids introducing a relational storage layer solely to retain files.
However, operating without a database does not mean operating without state.
Authentication, sessions, temporary files, and active processing jobs still require appropriate management mechanisms.
If the system were extended to provide comprehensive payment tracking, complex searches, or shared transaction history, its storage requirements could change.
The architectural choice therefore depends on the actual responsibility of the engine.
15. Authentication and Internal Resource Protection
The web interface is password-protected.
The documented engine includes several security mechanisms:
- Protection against brute-force attempts
- Secure session management
- Protection against unauthorized file access
- Payment data validation
- Temporary file cleanup
- HTTP response security controls
Separating the public entry point from the internal engine provides another architectural boundary.
Browser
│
â–¼
access.php
│
â–¼
Internal engine
│
├── Dependencies
├── Temporary files
└── Archives
The .htaccess rules help prevent direct access to internal resources in a correctly configured Apache environment.
Password protection alone is insufficient if processed files remain directly accessible through their URLs.
Security controls must therefore apply to both the interface and the resources it handles.
Payment Date Validation
A date received from a form must be validated on the server.
A generic PHP example could look like this:
function validatePaymentDate(string $value): bool
{
$date = DateTimeImmutable::createFromFormat(
'!Y-m-d',
$value
);
return $date !== false
&& $date->format('Y-m-d') === $value;
}
This validation checks the format and calendar validity of the date.
It does not establish that a payment has actually occurred.
Business rules, such as whether future payment dates are permitted, must be defined separately according to the system's requirements.
16. A Payment Stamp Is Not a Bank Confirmation
The engine generates a document containing a payment confirmation stamp based on a date supplied by the user.
This operation must be distinguished from automated payment verification.
Payment completed
│
â–¼
User enters payment date
│
â–¼
Generate visual confirmation
The engine does not independently establish the existence of a bank transaction.
The stamping operation alone does not perform accounting reconciliation or provide independent confirmation that a payment has been received.
| Operation | Responsibility |
|---|---|
| Bank verification | Establish whether payment occurred |
| Reconciliation | Associate payment with an invoice |
| Visual stamping | Generate a paid document |
| Archiving | Retain the generated document |
This distinction defines the scope of the tool.
The engine transforms a document using supplied payment information. It is not, by itself, a complete financial management system.
17. Dependencies and Runtime Environment
The engine requires PHP 8.0 or later.
The documented requirements include:
| Component | Purpose |
|---|---|
| PHP 8.0+ | Engine execution |
zip |
ZIP archive processing |
mbstring |
Multibyte string handling |
| Composer | PHP dependency management |
| FPDI | PDF page import |
| FPDF | PDF document generation |
| JSZip | Browser-side ZIP generation |
The engine is designed to be installed within the client's environment.
It does not require a database or a permanent connection to the main invoicing service after installation.
PHP dependencies are stored in a dedicated directory.
The public documentation describes the components and operating principles without providing a public installation procedure or production files.
Conclusion
An invoice payment stamping engine involves more than adding red text to a PDF.
It must distinguish the original document from the generated copy, correctly import pages, process individual documents and archives, validate incoming data, and manage the lifecycle of temporary files.
FPDI and FPDF provide a relatively lightweight foundation for PDF reconstruction. JSZip complements batch processing by allowing archives to be assembled in the browser.
Operating without a database remains consistent with an engine specialized in document transformation and optional local archiving.
The Factur-X limitation remains essential: preserving the original invoice does not mean that the stamped copy automatically retains its embedded XML or PDF/A-3 compliance.
The responsibility of the engine is therefore clearly defined: generate a separate visual payment confirmation without modifying the source document.
Technical Documentation
The project architecture, dependencies, processing workflows, and security considerations are documented in the GitHub repository.
The repository provides technical documentation rather than downloadable source code or production files.
View the technical documentation on GitHub

Top comments (0)