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