Skip to main content

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 source field (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.

localhost/admin

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.invoicePrestaShop sourceTransform
number$order->referencedirect (string; merchant-facing)
issueDate$order->date_addsubstr(…, 0, 10) (ISO 8601)
currencyCode$currency->iso_codedirect
buyer.name$customer->firstname + lastnameconcatenate
buyer.email$customer->emaildirect
buyer.address$invoiceAddress->{address1,city,postcode}id_country → ISO-2 via Country::getIsoById()
lines[]$order->getProducts()foreach (below)
lines[].unitPricerow unit_price_tax_exclnet, direct
lines[].lineTotalrow total_price_tax_exclnet, direct
lines[].vatRaterow tax_ratedirect percent
totalNet / Tax / Grosstotal_paid_tax_excl / (gross − net) / total_paid_tax_incldirect

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:

localhost/admin

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:

localhost/admin

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_referencearchive_urlissuedretain_until
XKBKNABJKs3://invoices/XKBKNABJK.pdf2026-02-152036-02-15
QTPMABCDEs3://invoices/QTPMABCDE.pdf2026-02-182036-02-18