Guide
HTML to PDF in WordPress and WooCommerce
WordPress plugin developers and WooCommerce store owners have been reaching for PDF plugins for years, each one bundling its own renderer with its own CSS quirks and its own compatibility drama. This guide shows a different approach: call a PDF API directly from your theme or plugin using WordPress's built-in HTTP functions, skip every binary dependency, and get output that matches your browser on any host, including cheap shared hosting that will never let you install wkhtmltopdf.
Why a PDF API beats a WordPress plugin
Every PDF plugin ships a renderer that runs inside your PHP process. That renderer has to parse HTML, implement a layout engine, handle fonts, and produce a binary PDF, all on the same server that is serving your storefront. The problems stack up quickly.
| Concern | PDF plugin / wkhtmltopdf | PDFPipe API |
|---|---|---|
| Modern CSS | Flexbox and grid often ignored or broken | Current engine, matches your browser |
| Server memory | Long documents hit PHP memory_limit walls | Render happens off your server |
| Shared hosting | Binary installs often blocked entirely | Works anywhere wp_remote_post works |
| Fonts | Manual registration and subsetting required | Web fonts load like in a browser |
| Dependencies | Plugin conflicts, update breakage | One HTTP call, no plugin to maintain |
The only requirement on your end is PHP's curl extension (enabled on every host) or WordPress's wp_remote_post(), which wraps it.
Calling the API with wp_remote_post()
WordPress ships its own HTTP API that handles timeouts, redirects, and error responses consistently across hosts. Using it instead of raw cURL means your code fits naturally into any plugin or theme. The endpoint is POST https://api.pdfpipe.xyz/v1/pdf.
<?php
/**
* Generate a PDF from an HTML string and stream it to the browser.
*
* @param string $html The HTML to render.
* @param string $filename Suggested filename for the download.
*/
function pdfpipe_render_and_download( string $html, string $filename = 'document.pdf' ): void {
$api_key = get_option( 'pdfpipe_api_key', '' );
if ( empty( $api_key ) ) {
wp_die( 'PDFPipe API key is not configured.' );
}
$response = wp_remote_post(
'https://api.pdfpipe.xyz/v1/pdf',
[
'timeout' => 30,
'headers' => [
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . $api_key,
],
'body' => wp_json_encode( [
'html' => $html,
'options' => [ 'format' => 'A4' ],
] ),
]
);
if ( is_wp_error( $response ) ) {
wp_die( 'PDF request failed: ' . $response->get_error_message() );
}
$status = wp_remote_retrieve_response_code( $response );
$body = wp_remote_retrieve_body( $response );
if ( 200 !== (int) $status ) {
wp_die( 'PDF render failed (HTTP ' . $status . ').' );
}
header( 'Content-Type: application/pdf' );
header( 'Content-Disposition: attachment; filename="' . sanitize_file_name( $filename ) . '"' );
header( 'Content-Length: ' . strlen( $body ) );
// Flush WordPress output buffers before echoing raw bytes.
while ( ob_get_level() ) {
ob_end_clean();
}
echo $body;
exit;
}Call this from any request handler, action callback, or shortcode that needs to hand the visitor a PDF. The API key is read from WordPress options, which you can manage from the admin settings page described later in this guide.
WooCommerce order invoice on payment
The woocommerce_payment_complete action fires after a payment is confirmed. Hook into it to generate a PDF invoice for the order, store it via the API, and save the document URL as order meta so you can surface it later.
<?php
add_action( 'woocommerce_payment_complete', 'pdfpipe_generate_order_invoice' );
function pdfpipe_generate_order_invoice( int $order_id ): void {
$order = wc_get_order( $order_id );
if ( ! $order ) {
return;
}
// Build a simple HTML invoice from order data.
$items_html = '';
foreach ( $order->get_items() as $item ) {
$items_html .= sprintf(
'<tr><td>%s</td><td>%d</td><td>%s</td></tr>',
esc_html( $item->get_name() ),
$item->get_quantity(),
wc_price( $item->get_total() )
);
}
$billing = $order->get_formatted_billing_address();
$html = <<<HTML
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
body { font-family: sans-serif; padding: 48px; color: #1d1812; }
h1 { font-size: 28px; margin-bottom: 4px; }
p { margin: 0 0 8px; color: #555; }
table { width: 100%; border-collapse: collapse; margin-top: 24px; }
th, td { text-align: left; padding: 8px 12px; border-bottom: 1px solid #e5e5e5; }
th { font-size: 12px; text-transform: uppercase; color: #888; }
.total { font-weight: bold; font-size: 18px; margin-top: 24px; }
</style>
</head>
<body>
<h1>Invoice #{$order->get_order_number()}</h1>
<p>Date: {$order->get_date_created()->date('F j, Y')}</p>
<p>Bill to:</p>
<address style="font-style:normal; margin-bottom:24px">{$billing}</address>
<table>
<thead><tr><th>Item</th><th>Qty</th><th>Total</th></tr></thead>
<tbody>{$items_html}</tbody>
</table>
<p class="total">Order total: {$order->get_formatted_order_total()}</p>
</body>
</html>
HTML;
$api_key = get_option( 'pdfpipe_api_key', '' );
if ( empty( $api_key ) ) {
return; // Key not configured — skip silently.
}
$response = wp_remote_post(
'https://api.pdfpipe.xyz/v1/pdf',
[
'timeout' => 30,
'headers' => [
'Content-Type' => 'application/json',
'Authorization' => 'Bearer ' . $api_key,
],
'body' => wp_json_encode( [
'html' => $html,
'options' => [ 'format' => 'A4' ],
'store' => true,
] ),
]
);
if ( is_wp_error( $response ) || 200 !== (int) wp_remote_retrieve_response_code( $response ) ) {
return; // Log to your error tracker in production.
}
$data = json_decode( wp_remote_retrieve_body( $response ), true );
if ( ! empty( $data['url'] ) ) {
$order->update_meta_data( '_pdf_invoice_url', esc_url_raw( $data['url'] ) );
$order->save();
}
}Passing "store": true asks the API to keep the rendered PDF and return a permanent URL instead of raw bytes. That URL is saved as _pdf_invoice_url on the order so downstream code can link to it without re-rendering.
Download link in My Account
Use the woocommerce_my_account_my_orders_actions filter to append a "Download Invoice" button to each order row in the customer's account.
<?php
add_filter( 'woocommerce_my_account_my_orders_actions', 'pdfpipe_add_invoice_download_action', 10, 2 );
function pdfpipe_add_invoice_download_action( array $actions, WC_Order $order ): array {
$invoice_url = $order->get_meta( '_pdf_invoice_url' );
if ( ! empty( $invoice_url ) ) {
$actions['pdf_invoice'] = [
'url' => esc_url( $invoice_url ),
'name' => __( 'Download Invoice', 'your-textdomain' ),
];
}
return $actions;
}WooCommerce renders these action links as anchor tags in the orders table. The URL points directly to the stored PDF, so the customer gets an instant download with no server-side processing at click time.
Storing the API key in WordPress admin
The snippets above read the key from get_option('pdfpipe_api_key'). The cleanest way to let a site owner enter that key is a settings field registered under an existing settings section, or a dedicated settings page in your plugin.
<?php
add_action( 'admin_init', 'pdfpipe_register_settings' );
function pdfpipe_register_settings(): void {
register_setting(
'general', // Settings group (appears on Settings > General).
'pdfpipe_api_key',
[
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'default' => '',
]
);
add_settings_section(
'pdfpipe_section',
'PDF Generation',
'__return_false',
'general'
);
add_settings_field(
'pdfpipe_api_key_field',
'PDFPipe API Key',
'pdfpipe_render_api_key_field',
'general',
'pdfpipe_section'
);
}
function pdfpipe_render_api_key_field(): void {
$value = get_option( 'pdfpipe_api_key', '' );
printf(
'<input type="password" id="pdfpipe_api_key" name="pdfpipe_api_key"
value="%s" class="regular-text" autocomplete="off">',
esc_attr( $value )
);
echo '<p class="description">Your PDFPipe API key. Keep this secret.</p>';
}This wires the field into Settings › General. To create a dedicated plugin settings page instead, register a custom menu with add_options_page() and pass your own group name to register_setting().
Alternative: define the key in wp-config.php
For managed or version-controlled sites where the settings screen is inconvenient, define the key as a PHP constant in wp-config.php and read it with defined(). This is also useful on staging environments where the database is regularly overwritten from production.
// In wp-config.php (above "That's all, stop editing!"):
define( 'PDFPIPE_API_KEY', 'your-api-key-here' );// In your plugin or functions.php, prefer the constant when present:
function pdfpipe_get_api_key(): string {
if ( defined( 'PDFPIPE_API_KEY' ) && PDFPIPE_API_KEY ) {
return PDFPIPE_API_KEY;
}
return (string) get_option( 'pdfpipe_api_key', '' );
}The constant approach keeps the secret out of the database entirely. The pdfpipe_get_api_key() helper lets you use whichever storage method suits the environment without changing call sites.
Paste your own HTML into the live playground on the home page to see the PDF render instantly, then read the docs for every option including page size, margins, headers, footers, and landscape orientation.
See pricing →