PDFPipe

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.

ConcernPDF plugin / wkhtmltopdfPDFPipe API
Modern CSSFlexbox and grid often ignored or brokenCurrent engine, matches your browser
Server memoryLong documents hit PHP memory_limit wallsRender happens off your server
Shared hostingBinary installs often blocked entirelyWorks anywhere wp_remote_post works
FontsManual registration and subsetting requiredWeb fonts load like in a browser
DependenciesPlugin conflicts, update breakageOne 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
<?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
<?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
<?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
<?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.

php
// In wp-config.php (above "That's all, stop editing!"):
define( 'PDFPIPE_API_KEY', 'your-api-key-here' );
php
// 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 →