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

Next steps​