PrestaShop
Prerequisites
- A PolyDoc account and API key. Sign up for free, no credit card required; the free plan includes 150 PDF conversions per month.
- A PolyDoc invoice template. Note its short ID for the
sourcefield (e.g.[template:jlE-whg]). - A PrestaShop 1.7 or 8.x store you can install a module on, plus REST API access (Advanced Parameters, Webservice) for the bulk archive. Get PrestaShop, it is free and self-hosted.
Use Case 1: Compliant E-Invoice on an Order Status Change
Turn an order that reaches a paid status into a compliant e-invoice: a single PDF/A-3 that carries the human-readable invoice and the embedded Factur-X / ZUGFeRD XML, then deliver it to your customer.PrestaShop (order status change) → module hook → PolyDoc API → email / order document
Pick the trigger order status
Decide which order status should produce the invoice. In the back office, go to Shop Parameters → Order Settings → Statuses and note the status you want to invoice on. Most stores use Payment accepted (the constant PS_OS_PAYMENT); use Shipped instead if you invoice on fulfilment.
Create the module and register the hook
Create a module that registers the order-status hook. A module is a folder under modules/ whose name matches the main class file, e.g. modules/polydocinvoice/polydocinvoice.php. The install() method registers the hook and seeds the settings; the hook method gates on your chosen status before doing any work:
<?php
if (!defined('_PS_VERSION_')) {
exit;
}
class PolydocInvoice extends Module
{
public function __construct()
{
$this->name = 'polydocinvoice';
$this->tab = 'billing_invoicing';
$this->version = '1.0.0';
$this->author = 'Your Company';
$this->bootstrap = true;
parent::__construct();
$this->displayName = $this->l('PolyDoc e-invoice');
$this->description = $this->l('Generate a compliant Factur-X / ZUGFeRD PDF/A-3 e-invoice via PolyDoc.');
}
public function install()
{
return parent::install()
&& $this->registerHook('actionOrderStatusPostUpdate')
&& Configuration::updateValue('POLYDOC_API_KEY', '')
&& Configuration::updateValue('POLYDOC_TEMPLATE_ID', '')
&& Configuration::updateValue('POLYDOC_SANDBOX', '1')
&& Configuration::updateValue('POLYDOC_TRIGGER_STATE', (int) Configuration::get('PS_OS_PAYMENT'));
}
// Fires after every order-status change.
public function hookActionOrderStatusPostUpdate(array $params)
{
/** @var OrderState $newState */
$newState = $params['newOrderStatus'];
if ((int) $newState->id !== (int) Configuration::get('POLYDOC_TRIGGER_STATE')) {
return; // not the status we invoice on
}
$order = new Order((int) $params['id_order']);
$pdf = $this->generateEInvoice($order); // Steps 3-4
if ($pdf !== null) {
$this->deliver($order, $pdf); // Step 5
}
}
}
Read the order inside the hook
Inside the hook you already have the order id, so there is no external fetch. Load the related objects and the line items straight from the PrestaShop ORM:
$invoiceAddress = new Address((int) $order->id_address_invoice);
$customer = new Customer((int) $order->id_customer);
$currency = new Currency((int) $order->id_currency);
$countryIso = Country::getIsoById((int) $invoiceAddress->id_country); // ISO-2
foreach ($order->getProducts() as $p) {
// $p['product_name'], $p['product_quantity']
// $p['unit_price_tax_excl'], $p['unit_price_tax_incl']
// $p['total_price_tax_excl'], $p['tax_rate']
}
$net = (float) $order->total_paid_tax_excl;
$gross = (float) $order->total_paid_tax_incl;
Map the order and call PolyDoc
Map the order to PolyDoc's eInvoice block. The seller is your own shop (PrestaShop already stores it under Shop Parameters → Contact); everything else comes from the order:
PolyDoc eInvoice.invoice | PrestaShop source | Transform |
|---|---|---|
number | $order->reference | direct (string; merchant-facing) |
issueDate | $order->date_add | substr(…, 0, 10) (ISO 8601) |
currencyCode | $currency->iso_code | direct |
buyer.name | $customer->firstname + lastname | concatenate |
buyer.email | $customer->email | direct |
buyer.address | $invoiceAddress->{address1,city,postcode} | id_country → ISO-2 via Country::getIsoById() |
lines[] | $order->getProducts() | foreach (below) |
lines[].unitPrice | row unit_price_tax_excl | net, direct |
lines[].lineTotal | row total_price_tax_excl | net, direct |
lines[].vatRate | row tax_rate | direct percent |
totalNet / Tax / Gross | total_paid_tax_excl / (gross − net) / total_paid_tax_incl | direct |
Then POST to PolyDoc. source renders the visual invoice from your template; eInvoice generates and embeds the CII XML; verify: true runs VeraPDF and fails the request if the output isn't PDF/A-3b compliant:
private function generateEInvoice(Order $order): ?string
{
$invoiceAddress = new Address((int) $order->id_address_invoice);
$customer = new Customer((int) $order->id_customer);
$currency = new Currency((int) $order->id_currency);
$countryIso = Country::getIsoById((int) $invoiceAddress->id_country);
$lines = [];
foreach ($order->getProducts() as $p) {
$vatRate = (float) $p['tax_rate'];
$lines[] = [
'description' => $p['product_name'],
'quantity' => (int) $p['product_quantity'],
'unitCode' => 'C62', // "HUR" for hours, "DAY" for days
'unitPrice' => round((float) $p['unit_price_tax_excl'], 2),
'lineTotal' => round((float) $p['total_price_tax_excl'], 2),
'vatRate' => $vatRate,
'vatCategoryCode' => $vatRate > 0 ? 'S' : 'Z',
];
}
$net = (float) $order->total_paid_tax_excl;
$gross = (float) $order->total_paid_tax_incl;
$issueDate = substr((string) $order->date_add, 0, 10);
$body = [
'source' => '[template:' . Configuration::get('POLYDOC_TEMPLATE_ID') . ']',
'templateData' => [
'invoice_number' => $order->reference,
'invoice_date' => $issueDate,
'invoice_total' => $gross,
'customer_name' => trim($customer->firstname . ' ' . $customer->lastname),
'customer_email' => $customer->email,
'items' => array_map(fn ($p) => [
'quantity' => (int) $p['product_quantity'],
'name' => $p['product_name'],
'price' => round((float) $p['unit_price_tax_incl'], 2),
], $order->getProducts()),
],
'eInvoice' => [
'standard' => 'facturx', // or "zugferd"
'profile' => 'en16931',
'verify' => true,
'invoice' => [
'number' => (string) $order->reference,
'issueDate' => $issueDate,
'dueDate' => date('Y-m-d', strtotime($issueDate . ' +30 days')),
'paymentTerms' => 'Net 30 days', // see note
'currencyCode' => $currency->iso_code,
'seller' => [ // your own shop, set once
'name' => Configuration::get('PS_SHOP_NAME'),
'address' => [
'line1' => Configuration::get('PS_SHOP_ADDR1'),
'city' => Configuration::get('PS_SHOP_CITY'),
'postalCode' => Configuration::get('PS_SHOP_CODE'),
'countryCode' => Country::getIsoById((int) Configuration::get('PS_SHOP_COUNTRY_ID')),
],
'taxId' => Configuration::get('PS_SHOP_DETAILS'),
],
'buyer' => [
'name' => trim($customer->firstname . ' ' . $customer->lastname),
'email' => $customer->email,
'address' => [
'line1' => $invoiceAddress->address1,
'line2' => $invoiceAddress->address2 ?: null,
'city' => $invoiceAddress->city,
'postalCode' => $invoiceAddress->postcode,
'countryCode' => $countryIso,
],
],
'lines' => $lines,
'totalNetAmount' => round($net, 2),
'totalTaxAmount' => round($gross - $net, 2),
'totalGrossAmount' => round($gross, 2),
],
],
];
$headers = [
'Authorization: Bearer ' . Configuration::get('POLYDOC_API_KEY'),
'Content-Type: application/json',
];
if (Configuration::get('POLYDOC_SANDBOX')) {
$headers[] = 'X-Sandbox: true'; // remove in production
}
$ch = curl_init('https://api.polydoc.tech/pdf/convert');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_POSTFIELDS => json_encode($body),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 60,
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status !== 200) {
PrestaShopLogger::addLog('PolyDoc e-invoice failed (HTTP ' . $status . ') for order ' . $order->reference, 3);
return null;
}
return $response; // the PDF/A-3 hybrid as raw bytes
}
Install, configure, and deliver
Zip the module folder and install it from Modules → Module Manager → Upload a module, then click Configure to paste your PolyDoc API key, your template id, and the trigger status from Step 1. Keep Sandbox mode on while you test:
Then deliver the hybrid PDF returned by Step 4. Two common options:
a) Email it to the customer. Attach the PDF bytes to a transactional mail. The single file is both the readable invoice and the compliant e-invoice, so one attachment covers both.
b) Store it next to the order. Write it under the shop's download directory (or your own storage) and link it from the order:
private function deliver(Order $order, string $pdf): void
{
$dir = _PS_DOWNLOAD_DIR_ . 'polydoc/';
if (!is_dir($dir)) {
mkdir($dir, 0755, true);
}
file_put_contents($dir . $order->reference . '.pdf', $pdf);
// Optionally surface it on the order timeline:
$msg = new OrderMessage();
$msg->id_order = (int) $order->id;
$msg->message = 'Compliant e-invoice generated: ' . $order->reference . '.pdf';
$msg->add();
}
Use Case 2: Bulk Historical PDF/A-3 Archive
Back-fill a compliant PDF/A-3 archive for orders you've already shipped (useful for the EU's multi-year invoice-retention requirements), writing each archive straight to your own cloud bucket. This runs as an external script against PrestaShop's REST Webservice — no module needed.List orders via Webservice → PolyDoc API (+ cloudStorage) → PDF/A-3b in your bucket
Enable the Webservice and list the orders
Enable the REST API. Go to Advanced Parameters → Webservice, switch Enable PrestaShop's webservice to Yes, then Add new webservice key. Generate a key and grant View (GET) on orders, order_details, addresses, and customers — read access is all a back-fill needs:
The key is the HTTP Basic username, with an empty password. List the orders to archive — filter by status and date, and ask for the full representation in JSON:
curl -u "$PS_WS_KEY:" -G \
"https://your-shop.example/api/orders" \
--data-urlencode "output_format=JSON" \
--data-urlencode "display=full" \
--data-urlencode "filter[current_state]=5" \
--data-urlencode "date=1" \
--data-urlencode "filter[date_add]=[2025-01-01,2025-12-31]"
Generate and store each archive
For each order, map it exactly as in Use Case 1, then POST to PolyDoc with a cloudStorage.presignedUrl: a pre-signed PUT URL you generate for your S3 / R2 / GCS / Azure bucket. PolyDoc renders, builds the hybrid, and PUTs it there; the response is just a status:
for (const order of orders) {
const body = mapToPolydoc(order); // the Use Case 1 mapping
body.cloudStorage = {
presignedUrl: await presignPut(`invoices/${order.reference}.pdf`),
};
await fetch("https://api.polydoc.tech/pdf/convert", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
},
body: JSON.stringify(body),
});
}
See the cloud-storage guides for generating presigned URLs per provider.
Index the archive
Keep a small index so you can find any archived invoice later. Map the order reference to its archive location and retention deadline:
| order_reference | archive_url | issued | retain_until |
|---|---|---|---|
| XKBKNABJK | s3://invoices/XKBKNABJK.pdf | 2026-02-15 | 2036-02-15 |
| QTPMABCDE | s3://invoices/QTPMABCDE.pdf | 2026-02-18 | 2036-02-18 |