Skip to main content

WooCommerce

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 WordPress site with WooCommerce installed and REST API access (Settings, Advanced, REST API). Get WooCommerce, it is free.

Use Case 1: Compliant E-Invoice on a Completed Order​

Turn a completed WooCommerce order 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.
WooCommerce (order updated) → your handler → PolyDoc API → email / order Note

Register a webhook for Order updated​

In wp-admin, go to WooCommerce → Settings → Advanced → Webhooks → Add webhook. Set:

  • Topic: Order updated
  • Delivery URL: your handler URL (must be HTTPS)
  • Secret: a long random string (or let WooCommerce generate one). You need this in Step 2 to verify the HMAC.
  • API Version: WP REST API Integration v3
  • Status: Active
wp-admin

Verify the webhook HMAC​

Your handler receives the POST. Before trusting it, verify the X-WC-Webhook-Signature header: compute an HMAC-SHA256 of the raw request body with your webhook secret, base64-encode it, and compare in constant time. Then guard on the order status so you only invoice completed orders:

import crypto from "node:crypto";

// rawBody MUST be the raw request bytes, not the parsed JSON.
function verifyWooCommerceWebhook(rawBody, signatureHeader) {
const digest = crypto
.createHmac("sha256", process.env.WC_WEBHOOK_SECRET)
.update(rawBody, "utf8")
.digest("base64");
return crypto.timingSafeEqual(
Buffer.from(digest),
Buffer.from(signatureHeader),
);
}

if (!verifyWooCommerceWebhook(rawBody, request.headers["x-wc-webhook-signature"])) {
return new Response("invalid signature", { status: 401 });
}

const order = JSON.parse(rawBody);
if (order.status !== "completed") return; // or "processing" if you invoice on payment

Read the order from the payload​

WooCommerce webhook v3 delivers the full order object, so there is no second fetch in Use Case 1. The fields the next step maps from:

Map the order and call PolyDoc​

Map the order to PolyDoc's eInvoice block. The seller is your own shop (your legal entity: set it once). Everything else comes from the order:

PolyDoc eInvoice.invoiceWooCommerce sourceTransform
numberorder numberdirect (string; merchant-facing)
issueDatedate_completed (else date_created).slice(0,10) (ISO 8601)
currencyCodecurrency.toUpperCase()
buyer.namebilling.company else first_name + last_nameprefer company for B2B
buyer.emailbilling.emaildirect
buyer.addressbilling.{address_1,city,postcode,country}postcode→postalCode, country→countryCode (already ISO-2)
lines[]line_items[].map() (below)
lines[].unitPriceline priceparseFloat; if prices_include_tax, divide out tax
lines[].lineTotalline subtotal (pre-discount) or totalparseFloat, tax-adjusted
lines[].vatRatefrom line.taxes[] + order.tax_lines[], else total_tax / subtotal x 100direct percent
totalNet / Tax / Grossorder subtotal / total_tax / totalparseFloat

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:

const order = JSON.parse(rawBody); // the verified order from Step 2
const inclusive = order.prices_include_tax === true;

// Order-level rate from tax_lines (most stores have one); fall back to a compute.
const orderRate =
parseFloat(order.tax_lines?.[0]?.rate_percent ?? 0) ||
(parseFloat(order.total_tax) / parseFloat(order.subtotal)) * 100 ||
0;

const lines = order.line_items.map((l) => {
const lineRate =
parseFloat(l.taxes?.[0]?.rate_id && order.tax_lines?.find((t) => t.rate_id === l.taxes[0].rate_id)?.rate_percent) ||
orderRate;
const listed = parseFloat(l.price);
const unitPrice = inclusive ? listed / (1 + lineRate / 100) : listed;
return {
description: l.name,
quantity: l.quantity,
unitCode: "C62", // "HUR" for hours, "DAY" for days, etc.
unitPrice,
lineTotal: unitPrice * l.quantity,
vatRate: lineRate,
vatCategoryCode: lineRate > 0 ? "S" : "Z",
};
});

const issueDate = (order.date_completed ?? order.date_created).slice(0, 10);
const dueDate = order.date_paid
? order.date_paid.slice(0, 10)
: new Date(new Date(issueDate).getTime() + 30 * 864e5).toISOString().slice(0, 10);

const body = {
source: "[template:YOUR_TEMPLATE_ID]",
templateData: {
invoice_number: order.number,
invoice_date: issueDate,
invoice_total: parseFloat(order.total),
customer_name: order.billing.company || `${order.billing.first_name} ${order.billing.last_name}`,
customer_email: order.billing.email,
items: order.line_items.map((l) => ({
quantity: l.quantity,
name: l.name,
description: l.name,
price: parseFloat(l.price),
})),
},
eInvoice: {
standard: "facturx", // or "zugferd"
profile: "en16931",
verify: true,
invoice: {
number: String(order.number),
issueDate,
dueDate,
paymentTerms: "Net 30 days", // BT-9/BT-20: see note
currencyCode: order.currency.toUpperCase(),
seller: YOUR_BUSINESS, // { name, address, taxId }: your own, set once
buyer: {
name: order.billing.company || `${order.billing.first_name} ${order.billing.last_name}`,
email: order.billing.email,
address: {
line1: order.billing.address_1,
line2: order.billing.address_2 || undefined,
city: order.billing.city,
postalCode: order.billing.postcode,
countryCode: order.billing.country, // ISO-2 already
},
},
lines,
totalNetAmount: parseFloat(order.subtotal),
totalTaxAmount: parseFloat(order.total_tax),
totalGrossAmount: parseFloat(order.total),
},
},
};

const res = await fetch("https://api.polydoc.tech/pdf/convert", {
method: "POST",
headers: {
Authorization: "Bearer YOUR_API_KEY",
"Content-Type": "application/json",
"X-Sandbox": "true", // drop in production
},
body: JSON.stringify(body),
});
const pdf = Buffer.from(await res.arrayBuffer()); // the PDF/A-3 hybrid

Deliver the hybrid PDF​

Deliver the hybrid PDF. Two common options:

a) Email it to the customer. Attach the PDF buffer from Step 4 to your transactional email (SES, Resend, Postmark, …). The single file is both the readable invoice and the compliant e-invoice, so one attachment covers both.

b) Attach it to the order as a Note. Upload the PDF to your own storage (or the WordPress Media library), then create a private Order Note linking to the archive so the document lives next to the record. With the Consumer Key + Secret from Step 1:

curl -u "$WC_CONSUMER_KEY:$WC_CONSUMER_SECRET" \
-X POST "https://your-shop.example/wp-json/wc/v3/orders/$ORDER_ID/notes" \
-H "Content-Type: application/json" \
-d '{"note":"E-invoice: https://.../WC-1042.pdf","customer_note":false}'

Use Case 2: Bulk Historical PDF/A-3 Archive​

Back-fill a compliant PDF/A-3 archive for orders you've already completed (useful for the EU's multi-year invoice-retention requirements), writing each archive straight to your own cloud bucket.
List completed orders → PolyDoc API (+ cloudStorage) → PDF/A-3b in your bucket

List the orders to archive​

Decide the window to archive (e.g. the last fiscal year) and list the completed orders. Use the WooCommerce REST API with HTTP Basic Auth (your Consumer Key + Consumer Secret from Step 1):

curl -u "$WC_CONSUMER_KEY:$WC_CONSUMER_SECRET" -G \
"https://your-shop.example/wp-json/wc/v3/orders" \
-d "status=completed" \
-d "after=2025-01-01T00:00:00" \
-d "per_page=100" \
-d "page=1"

Page through with the page param (and the X-WP-TotalPages response header) until you've collected every order in range. The response already includes inline line_items[], so there is no second call per order.

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 completedOrders) {
const body = mapToPolydoc(order); // Step 4 mapping
body.cloudStorage = {
presignedUrl: await presignPut(`invoices/${order.number}.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 number to its archive location and retention deadline:

order_numberarchive_urlissuedretain_until
WC-1042s3://invoices/WC-1042.pdf2026-02-152036-02-15
WC-1043s3://invoices/WC-1043.pdf2026-02-182036-02-18

Next steps​