Whether you need to generate order invoices in WooCommerce, create downloadable product catalogs, or automatically send PDF confirmation emails — PDF generation is a common requirement for WordPress sites. Yet existing plugins and PHP libraries often fall short when it comes to layout quality, server resource usage, and long-term maintainability.
This guide walks you through integrating FUNBREW PDF with WordPress using wp_remote_post(), shortcodes, custom post types, WooCommerce, REST API endpoints, and proper security practices (nonce verification and capability checks).
For basic PHP API usage, see the language quickstart guide. For error handling patterns, check the error handling guide.
When WordPress Sites Need PDF Generation
WordPress is used far beyond blogging — it powers e-commerce stores, membership platforms, corporate portals, and custom business applications. PDF generation comes up in many scenarios:
| Use Case | Examples |
|---|---|
| E-commerce / WooCommerce | Order confirmations, invoices, packing slips |
| Membership sites | Member cards, certificates, credentials |
| Business / B2B | Quotes, contracts, reports |
| Content distribution | Product catalogs, whitepapers |
| Form integration | Contact form confirmations, application receipts |
Most developers start by searching for a plugin. Let's look at why existing solutions often aren't enough.
The Limits of Existing Plugins (TCPDF-based etc.)
The most common WordPress PDF solutions rely on PHP libraries like TCPDF, mPDF, or dompdf. These run entirely on your server and generate PDFs in PHP.
In practice, these approaches hit several walls:
Poor CSS Support
PHP-based PDF libraries have limited CSS support. flexbox, grid, and many modern CSS properties are either partially supported or ignored entirely. Japanese and other CJK fonts are notoriously difficult to handle. Even if your HTML looks great in a browser, the PDF output often has broken layouts.
// A common mPDF problem — flexbox is simply ignored
$mpdf = new \Mpdf\Mpdf();
$mpdf->WriteHTML('<div style="display:flex;">...</div>'); // flex doesn't work
Server Resource Consumption
TCPDF and mPDF run on PHP, consuming significant CPU and memory for large documents or under concurrent load. On shared hosting, this can cause timeouts or out-of-memory errors.
Maintenance and Compatibility
Every WordPress major version and PHP version upgrade risks breaking your PDF plugin. If the plugin stops receiving updates, you're left running vulnerable code or scrambling to find an alternative.
Why the API Approach Is Better
Using a cloud PDF generation API like FUNBREW PDF addresses all of these problems:
Zero Server Load
PDF rendering happens on FUNBREW PDF's servers, not yours. WordPress only needs to make an HTTP request. Traffic spikes don't affect your PDF generation capability.
Full CSS Support
FUNBREW PDF uses a Chromium-based rendering engine with complete support for modern CSS — flexbox, grid, custom fonts, gradients, and shadows all work exactly as they do in a browser. For Japanese font handling specifics, see the Japanese font guide.
Simple Integration
The entire integration is one HTTP request. WordPress provides wp_remote_post() for exactly this purpose — no extra plugins required, just a few dozen lines of code.
No Maintenance Required
FUNBREW PDF handles all server maintenance. WordPress and PHP version upgrades don't affect your PDF functionality.
Setup: Getting and Configuring Your API Key
Create a free account and generate an API key from the dashboard.
The WordPress-idiomatic way to store your API key is as a constant in wp-config.php:
// wp-config.php
define( 'FUNBREW_PDF_API_KEY', 'sk-your-api-key' );
define( 'FUNBREW_PDF_API_URL', 'https://api.pdf.funbrew.cloud/v1/pdf/from-html' );
Alternatively, you can store it as a WordPress option and let administrators configure it through an admin settings page (covered later in this guide):
// Retrieve from plugin settings
$api_key = get_option( 'funbrew_pdf_api_key' );
For comprehensive API key security practices, see the security guide.
Making API Calls with wp_remote_post()
WordPress provides wp_remote_post() as the standard way to make outbound HTTP requests — no direct curl required. It automatically selects the best transport (cURL, fsockopen, or streams) for the current environment.
/**
* Generate a PDF from HTML using FUNBREW PDF API.
*
* @param string $html The HTML content to convert.
* @param array $options API options (format, margin, etc.)
* @return string|WP_Error PDF binary string on success, WP_Error on failure.
*/
function funbrew_generate_pdf( $html, $options = array() ) {
$api_key = defined( 'FUNBREW_PDF_API_KEY' ) ? FUNBREW_PDF_API_KEY : get_option( 'funbrew_pdf_api_key' );
$api_url = defined( 'FUNBREW_PDF_API_URL' ) ? FUNBREW_PDF_API_URL : 'https://api.pdf.funbrew.cloud/v1/pdf/from-html';
if ( empty( $api_key ) ) {
return new WP_Error( 'missing_api_key', 'FUNBREW PDF API key is not configured.' );
}
$body = array_merge(
array(
'html' => $html,
'format' => 'A4',
'engine' => 'quality',
),
$options
);
$response = wp_remote_post(
$api_url,
array(
'method' => 'POST',
'timeout' => 60,
'headers' => array(
'Authorization' => 'Bearer ' . $api_key,
'Content-Type' => 'application/json',
),
'body' => wp_json_encode( $body ),
)
);
if ( is_wp_error( $response ) ) {
return $response;
}
$status_code = wp_remote_retrieve_response_code( $response );
if ( 200 !== $status_code ) {
$body_text = wp_remote_retrieve_body( $response );
return new WP_Error(
'pdf_generation_failed',
sprintf( 'PDF generation failed (status: %d, message: %s)', $status_code, $body_text )
);
}
return wp_remote_retrieve_body( $response );
}
This function returns the raw PDF binary string on success or a WP_Error on failure. Callers check with is_wp_error() — a standard WordPress pattern.
Sending the PDF to the Browser
/**
* Output a PDF binary to the browser as a download.
*
* @param string $pdf_data The PDF binary string.
* @param string $filename The download filename.
*/
function funbrew_output_pdf( $pdf_data, $filename = 'document.pdf' ) {
// Clear any output buffering WordPress may have started
if ( ob_get_length() ) {
ob_end_clean();
}
header( 'Content-Type: application/pdf' );
header( 'Content-Disposition: attachment; filename="' . sanitize_file_name( $filename ) . '"' );
header( 'Content-Length: ' . strlen( $pdf_data ) );
header( 'Cache-Control: private, max-age=0, must-revalidate' );
echo $pdf_data; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
exit;
}
Shortcode Implementation ([funbrew_pdf])
Shortcodes let editors add PDF download buttons to any post or page without touching code.
Registering the Shortcode
/**
* [funbrew_pdf] shortcode — adds a PDF download link.
*
* Usage: [funbrew_pdf post_id="123" label="Download PDF"]
*/
function funbrew_pdf_shortcode( $atts ) {
$atts = shortcode_atts(
array(
'post_id' => get_the_ID(),
'label' => 'Download PDF',
'format' => 'A4',
),
$atts,
'funbrew_pdf'
);
$post_id = absint( $atts['post_id'] );
$label = esc_html( $atts['label'] );
if ( ! $post_id ) {
return '';
}
$nonce = wp_create_nonce( 'funbrew_pdf_download_' . $post_id );
$url = add_query_arg(
array(
'action' => 'funbrew_pdf_download',
'post_id' => $post_id,
'format' => esc_attr( $atts['format'] ),
'_wpnonce' => $nonce,
),
admin_url( 'admin-ajax.php' )
);
return sprintf(
'<a href="%s" class="funbrew-pdf-download-btn" target="_blank">%s</a>',
esc_url( $url ),
$label
);
}
add_shortcode( 'funbrew_pdf', 'funbrew_pdf_shortcode' );
The AJAX Handler
/**
* Handle the AJAX request, generate the PDF, and output it.
*/
function funbrew_pdf_ajax_handler() {
// 1. Verify nonce
$post_id = isset( $_GET['post_id'] ) ? absint( $_GET['post_id'] ) : 0;
if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ?? '' ) ), 'funbrew_pdf_download_' . $post_id ) ) {
wp_die( 'Security check failed.', 403 );
}
// 2. Fetch the post
$post = get_post( $post_id );
if ( ! $post || 'publish' !== $post->post_status ) {
wp_die( 'Post not found.', 404 );
}
// 3. Build HTML
$format = isset( $_GET['format'] ) ? sanitize_text_field( wp_unslash( $_GET['format'] ) ) : 'A4';
$html = funbrew_build_post_html( $post );
// 4. Generate PDF
$pdf_data = funbrew_generate_pdf( $html, array( 'format' => $format ) );
if ( is_wp_error( $pdf_data ) ) {
wp_die( esc_html( $pdf_data->get_error_message() ), 500 );
}
// 5. Output to browser
$filename = sanitize_file_name( $post->post_name . '.pdf' );
funbrew_output_pdf( $pdf_data, $filename );
}
// For logged-in users
add_action( 'wp_ajax_funbrew_pdf_download', 'funbrew_pdf_ajax_handler' );
// For logged-out users (allow public posts to be downloaded)
add_action( 'wp_ajax_nopriv_funbrew_pdf_download', 'funbrew_pdf_ajax_handler' );
Building the HTML from Post Content
/**
* Generate PDF-ready HTML from a WP_Post object.
*
* @param WP_Post $post The post to render.
* @return string HTML string.
*/
function funbrew_build_post_html( $post ) {
$title = get_the_title( $post );
$content = apply_filters( 'the_content', $post->post_content );
$date = get_the_date( 'F j, Y', $post );
return '<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
font-size: 14px;
line-height: 1.7;
color: #1a1a1a;
padding: 40px 48px;
max-width: 800px;
margin: 0 auto;
}
h1 {
font-size: 22px;
font-weight: 700;
border-bottom: 3px solid #1a56db;
padding-bottom: 12px;
margin-bottom: 8px;
}
.meta { color: #6b7280; font-size: 12px; margin-bottom: 32px; }
h2 { font-size: 18px; margin-top: 32px; border-left: 4px solid #1a56db; padding-left: 10px; }
h3 { font-size: 15px; margin-top: 24px; }
img { max-width: 100%; height: auto; }
table { width: 100%; border-collapse: collapse; margin: 16px 0; }
th, td { border: 1px solid #e5e7eb; padding: 8px 12px; }
th { background: #f9fafb; font-weight: 600; }
pre { background: #f4f4f5; padding: 16px; border-radius: 4px; overflow-x: auto; font-size: 12px; }
code { background: #f4f4f5; padding: 2px 5px; border-radius: 3px; font-size: 12px; }
blockquote { border-left: 4px solid #e5e7eb; margin: 0; padding-left: 16px; color: #6b7280; }
.footer {
margin-top: 48px;
padding-top: 16px;
border-top: 1px solid #e5e7eb;
font-size: 11px;
color: #9ca3af;
text-align: center;
}
</style>
</head>
<body>
<h1>' . esc_html( $title ) . '</h1>
<p class="meta">Published: ' . esc_html( $date ) . '</p>
<div class="content">' . $content . '</div>
<div class="footer">Generated by FUNBREW PDF</div>
</body>
</html>';
}
Using the Shortcode
Paste any of these into the WordPress block editor or classic editor:
[funbrew_pdf]
[funbrew_pdf label="Save this article as PDF"]
[funbrew_pdf post_id="123" label="Download Catalog" format="A4"]
Custom Post Types
Custom post types like product catalogs, case studies, or job listings can be PDFed with the same approach. Here's an example using a product_catalog CPT.
Registering the CPT
function funbrew_register_product_catalog_cpt() {
register_post_type(
'product_catalog',
array(
'labels' => array(
'name' => 'Product Catalogs',
'singular_name' => 'Product Catalog',
),
'public' => true,
'has_archive' => true,
'supports' => array( 'title', 'editor', 'thumbnail', 'custom-fields' ),
'show_in_rest' => true,
)
);
}
add_action( 'init', 'funbrew_register_product_catalog_cpt' );
Building a Rich HTML Template with Custom Fields
/**
* Generate a product catalog PDF template with custom fields.
*
* @param WP_Post $post The product catalog post.
* @return string HTML string.
*/
function funbrew_build_catalog_html( $post ) {
$product_code = get_post_meta( $post->ID, 'product_code', true );
$price = get_post_meta( $post->ID, 'price', true );
$specifications = get_post_meta( $post->ID, 'specifications', true );
$specs = $specifications ? json_decode( $specifications, true ) : array();
$thumbnail_url = get_the_post_thumbnail_url( $post, 'large' );
$content = apply_filters( 'the_content', $post->post_content );
$title = get_the_title( $post );
ob_start();
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; font-size: 13px; color: #1a1a1a; padding: 40px; }
.header { display: flex; justify-content: space-between; align-items: flex-start; border-bottom: 3px solid #1a56db; padding-bottom: 20px; margin-bottom: 28px; }
.header-left h1 { font-size: 22px; margin: 0 0 4px; }
.product-code { color: #6b7280; font-size: 12px; }
.product-image { max-width: 200px; max-height: 150px; object-fit: contain; }
.specs-table { width: 100%; border-collapse: collapse; margin-top: 20px; }
.specs-table th, .specs-table td { border: 1px solid #e5e7eb; padding: 8px 12px; }
.specs-table th { background: #f9fafb; width: 35%; font-weight: 600; }
.price-block { background: #eff6ff; border: 1px solid #bfdbfe; border-radius: 6px; padding: 16px 20px; margin: 24px 0; font-size: 20px; font-weight: 700; color: #1d4ed8; }
</style>
</head>
<body>
<div class="header">
<div class="header-left">
<h1><?php echo esc_html( $title ); ?></h1>
<?php if ( $product_code ) : ?>
<div class="product-code">Product Code: <?php echo esc_html( $product_code ); ?></div>
<?php endif; ?>
</div>
<?php if ( $thumbnail_url ) : ?>
<img src="<?php echo esc_url( $thumbnail_url ); ?>" alt="<?php echo esc_attr( $title ); ?>" class="product-image">
<?php endif; ?>
</div>
<?php if ( $price ) : ?>
<div class="price-block">List Price: $<?php echo number_format( floatval( $price ), 2 ); ?></div>
<?php endif; ?>
<?php if ( ! empty( $specs ) ) : ?>
<h2>Specifications</h2>
<table class="specs-table">
<tbody>
<?php foreach ( $specs as $key => $value ) : ?>
<tr>
<th><?php echo esc_html( $key ); ?></th>
<td><?php echo esc_html( $value ); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
<div class="description"><?php echo $content; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></div>
</body>
</html>
<?php
return ob_get_clean();
}
Adding a PDF Download Button to the Edit Screen
function funbrew_add_catalog_pdf_metabox() {
add_meta_box(
'funbrew_pdf_metabox',
'PDF Export',
'funbrew_catalog_pdf_metabox_html',
'product_catalog',
'side',
'high'
);
}
add_action( 'add_meta_boxes', 'funbrew_add_catalog_pdf_metabox' );
function funbrew_catalog_pdf_metabox_html( $post ) {
$nonce = wp_create_nonce( 'funbrew_pdf_download_' . $post->ID );
$url = add_query_arg(
array(
'action' => 'funbrew_pdf_download',
'post_id' => $post->ID,
'_wpnonce' => $nonce,
),
admin_url( 'admin-ajax.php' )
);
echo '<p><a href="' . esc_url( $url ) . '" target="_blank" class="button button-primary">Download PDF</a></p>';
echo '<p class="description">Generates a PDF from the currently saved content.</p>';
}
WooCommerce Integration (Order Invoices)
Automatically attaching a PDF invoice to WooCommerce order completion emails is one of the most requested features for B2B stores.
Building the Invoice HTML
/**
* Build an invoice PDF from a WooCommerce order.
*
* @param WC_Order $order The WooCommerce order object.
* @return string HTML string.
*/
function funbrew_build_order_pdf_html( $order ) {
$order_id = $order->get_id();
$order_date = wc_format_datetime( $order->get_date_created() );
$billing_name = $order->get_formatted_billing_full_name();
$billing_addr = $order->get_formatted_billing_address();
$items = $order->get_items();
$subtotal = $order->get_subtotal();
$total_tax = $order->get_total_tax();
$total = $order->get_total();
$payment_method = $order->get_payment_method_title();
ob_start();
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<style>
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; font-size: 13px; color: #1a1a1a; padding: 48px; }
.invoice-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 40px; }
.invoice-title { font-size: 28px; font-weight: 700; color: #1a56db; margin: 0; }
.invoice-meta { text-align: right; font-size: 12px; color: #6b7280; }
.billing-address { background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 6px; padding: 16px 20px; margin-bottom: 32px; }
.billing-address strong { font-size: 15px; display: block; margin-bottom: 4px; }
table { width: 100%; border-collapse: collapse; margin-bottom: 24px; }
thead th { background: #1a56db; color: #fff; padding: 10px 12px; text-align: left; font-size: 12px; }
tbody td { padding: 10px 12px; border-bottom: 1px solid #e5e7eb; }
.text-right { text-align: right; }
.totals-table { width: 280px; margin-left: auto; border-collapse: collapse; }
.totals-table td { padding: 6px 12px; border-bottom: 1px solid #e5e7eb; }
.totals-table .grand-total td { font-size: 16px; font-weight: 700; color: #1a56db; border-top: 2px solid #1a56db; }
.footer { margin-top: 48px; font-size: 11px; color: #9ca3af; text-align: center; border-top: 1px solid #e5e7eb; padding-top: 16px; }
</style>
</head>
<body>
<div class="invoice-header">
<h1 class="invoice-title">Invoice</h1>
<div class="invoice-meta">
<p>Order #<?php echo esc_html( $order_id ); ?></p>
<p>Date: <?php echo esc_html( $order_date ); ?></p>
<p>Payment: <?php echo esc_html( $payment_method ); ?></p>
</div>
</div>
<div class="billing-address">
<strong><?php echo esc_html( $billing_name ); ?></strong>
<?php echo wp_kses_post( nl2br( $billing_addr ) ); ?>
</div>
<table>
<thead>
<tr>
<th>Item</th>
<th class="text-right">Qty</th>
<th class="text-right">Unit Price</th>
<th class="text-right">Subtotal</th>
</tr>
</thead>
<tbody>
<?php foreach ( $items as $item ) : ?>
<?php
$quantity = $item->get_quantity();
$subtotal_item = $item->get_subtotal();
$unit_price = ( $quantity > 0 ) ? $subtotal_item / $quantity : 0;
?>
<tr>
<td><?php echo esc_html( $item->get_name() ); ?></td>
<td class="text-right"><?php echo esc_html( $quantity ); ?></td>
<td class="text-right">$<?php echo number_format( $unit_price, 2 ); ?></td>
<td class="text-right">$<?php echo number_format( $subtotal_item, 2 ); ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<table class="totals-table">
<tr>
<td>Subtotal</td>
<td class="text-right">$<?php echo number_format( $subtotal, 2 ); ?></td>
</tr>
<tr>
<td>Tax</td>
<td class="text-right">$<?php echo number_format( $total_tax, 2 ); ?></td>
</tr>
<tr class="grand-total">
<td>Total</td>
<td class="text-right">$<?php echo number_format( $total, 2 ); ?></td>
</tr>
</table>
<div class="footer">Generated by FUNBREW PDF</div>
</body>
</html>
<?php
return ob_get_clean();
}
Attaching the PDF to Order Completion Emails
/**
* Attach a PDF invoice to WooCommerce order completion emails.
*
* @param array $attachments Current attachment file paths.
* @param string $email_id The email ID.
* @param WC_Order $order The order object.
* @return array Updated attachment paths.
*/
function funbrew_attach_invoice_to_order_email( $attachments, $email_id, $order ) {
// Only attach to the order completion email
if ( 'customer_completed_order' !== $email_id ) {
return $attachments;
}
if ( ! ( $order instanceof WC_Order ) ) {
return $attachments;
}
$html = funbrew_build_order_pdf_html( $order );
$pdf_data = funbrew_generate_pdf( $html, array( 'format' => 'A4' ) );
if ( is_wp_error( $pdf_data ) ) {
// Log the error but don't block the email
error_log( 'FUNBREW PDF Error: ' . $pdf_data->get_error_message() );
return $attachments;
}
// Save to a temp file (WordPress cleans these up)
$tmp_file = wp_tempnam( 'invoice-' . $order->get_id() . '.pdf' );
file_put_contents( $tmp_file, $pdf_data ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
$attachments[] = $tmp_file;
return $attachments;
}
add_filter( 'woocommerce_email_attachments', 'funbrew_attach_invoice_to_order_email', 10, 3 );
Adding a PDF Column to the Orders List
/**
* Add a PDF invoice column to the WooCommerce orders table.
*/
function funbrew_add_orders_pdf_column( $columns ) {
$new_columns = array();
foreach ( $columns as $key => $value ) {
$new_columns[ $key ] = $value;
if ( 'order_total' === $key ) {
$new_columns['pdf_invoice'] = 'PDF Invoice';
}
}
return $new_columns;
}
add_filter( 'manage_woocommerce_page_wc-orders_columns', 'funbrew_add_orders_pdf_column' );
function funbrew_orders_pdf_column_content( $column, $order ) {
if ( 'pdf_invoice' !== $column ) {
return;
}
$nonce = wp_create_nonce( 'funbrew_pdf_download_order_' . $order->get_id() );
$url = add_query_arg(
array(
'action' => 'funbrew_order_pdf_download',
'order_id' => $order->get_id(),
'_wpnonce' => $nonce,
),
admin_url( 'admin-ajax.php' )
);
echo '<a href="' . esc_url( $url ) . '" target="_blank" class="button">PDF</a>';
}
add_action( 'manage_woocommerce_page_wc-orders_custom_column', 'funbrew_orders_pdf_column_content', 10, 2 );
/**
* AJAX handler for admin order PDF download.
*/
function funbrew_order_pdf_ajax_handler() {
// Capability check — shop managers and above only
if ( ! current_user_can( 'manage_woocommerce' ) ) {
wp_die( 'You do not have permission to do this.', 403 );
}
$order_id = isset( $_GET['order_id'] ) ? absint( $_GET['order_id'] ) : 0;
if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ?? '' ) ), 'funbrew_pdf_download_order_' . $order_id ) ) {
wp_die( 'Security check failed.', 403 );
}
$order = wc_get_order( $order_id );
if ( ! $order ) {
wp_die( 'Order not found.', 404 );
}
$html = funbrew_build_order_pdf_html( $order );
$pdf_data = funbrew_generate_pdf( $html );
if ( is_wp_error( $pdf_data ) ) {
wp_die( esc_html( $pdf_data->get_error_message() ), 500 );
}
funbrew_output_pdf( $pdf_data, 'order-' . $order_id . '.pdf' );
}
add_action( 'wp_ajax_funbrew_order_pdf_download', 'funbrew_order_pdf_ajax_handler' );
Adding a REST API Endpoint
For headless WordPress setups or external application integration, registering a custom REST API route is the cleanest approach.
/**
* Register FUNBREW PDF REST API routes.
*/
function funbrew_register_pdf_rest_routes() {
// Generate PDF from a post (requires authentication)
register_rest_route(
'funbrew/v1',
'/pdf/post/(?P<id>\d+)',
array(
'methods' => 'GET',
'callback' => 'funbrew_rest_generate_post_pdf',
'permission_callback' => 'funbrew_rest_permission_check',
'args' => array(
'id' => array(
'required' => true,
'type' => 'integer',
'sanitize_callback' => 'absint',
),
'format' => array(
'default' => 'A4',
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'enum' => array( 'A4', 'A3', 'Letter', 'Legal' ),
),
),
)
);
// Generate PDF from custom HTML (admins only)
register_rest_route(
'funbrew/v1',
'/pdf/html',
array(
'methods' => 'POST',
'callback' => 'funbrew_rest_generate_html_pdf',
'permission_callback' => function () {
return current_user_can( 'manage_options' );
},
'args' => array(
'html' => array( 'required' => true, 'type' => 'string' ),
'filename' => array( 'default' => 'document.pdf', 'type' => 'string' ),
'format' => array( 'default' => 'A4', 'type' => 'string' ),
),
)
);
}
add_action( 'rest_api_init', 'funbrew_register_pdf_rest_routes' );
/**
* REST handler: generate PDF from a post.
*/
function funbrew_rest_generate_post_pdf( $request ) {
$post_id = $request->get_param( 'id' );
$format = $request->get_param( 'format' );
$post = get_post( $post_id );
if ( ! $post || 'publish' !== $post->post_status ) {
return new WP_Error( 'not_found', 'Post not found.', array( 'status' => 404 ) );
}
$html = funbrew_build_post_html( $post );
$pdf_data = funbrew_generate_pdf( $html, array( 'format' => $format ) );
if ( is_wp_error( $pdf_data ) ) {
return new WP_Error( 'pdf_failed', $pdf_data->get_error_message(), array( 'status' => 502 ) );
}
return rest_ensure_response(
array(
'post_id' => $post_id,
'filename' => $post->post_name . '.pdf',
'pdf' => base64_encode( $pdf_data ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
'size' => strlen( $pdf_data ),
)
);
}
/**
* REST handler: generate PDF from custom HTML.
*/
function funbrew_rest_generate_html_pdf( $request ) {
$html = $request->get_param( 'html' );
$filename = sanitize_file_name( $request->get_param( 'filename' ) );
$format = $request->get_param( 'format' );
$pdf_data = funbrew_generate_pdf( $html, array( 'format' => $format ) );
if ( is_wp_error( $pdf_data ) ) {
return new WP_Error( 'pdf_failed', $pdf_data->get_error_message(), array( 'status' => 502 ) );
}
return rest_ensure_response(
array(
'filename' => $filename,
'pdf' => base64_encode( $pdf_data ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
'size' => strlen( $pdf_data ),
)
);
}
function funbrew_rest_permission_check() {
return true; // Restrict with `is_user_logged_in()` if needed
}
Calling the REST API
# Generate PDF for post ID 123
curl -H "Authorization: Bearer YOUR_WP_JWT_TOKEN" \
"https://your-site.com/wp-json/funbrew/v1/pdf/post/123?format=A4"
# Generate PDF from custom HTML (admin only)
curl -X POST \
-H "Authorization: Bearer YOUR_WP_JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"html":"<h1>Hello</h1>","filename":"hello.pdf"}' \
"https://your-site.com/wp-json/funbrew/v1/pdf/html"
Security: Nonce Verification and Capability Checks
Security is critical for any WordPress plugin that makes external API calls. Here's a clear breakdown of what to implement and why.
Why Nonces Matter
A nonce is a time-limited, action-bound token that prevents CSRF (Cross-Site Request Forgery) attacks. Without nonce verification, an attacker could trick a logged-in user into making a request to your AJAX endpoint — potentially exhausting your API key quota or generating unwanted PDFs.
// Create a nonce tied to an action and a specific post
$nonce = wp_create_nonce( 'funbrew_pdf_action_' . $post_id );
// Verify the nonce at the start of every AJAX handler
if ( ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ?? '' ) ), 'funbrew_pdf_action_' . $post_id ) ) {
wp_die( 'Security check failed.', 403 );
}
Capability Checks
Use WordPress's capability system to control who can generate PDFs:
// Admins only
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( 'You do not have permission to do this.', 403 );
}
// Only users who can edit the specific post
if ( ! current_user_can( 'edit_post', $post_id ) ) {
wp_die( 'You do not have permission to do this.', 403 );
}
// WooCommerce shop managers
if ( ! current_user_can( 'manage_woocommerce' ) ) {
wp_die( 'You do not have permission to do this.', 403 );
}
// Allow logged-out users to access only published posts
$post = get_post( $post_id );
if ( 'publish' !== $post->post_status && ! current_user_can( 'read_private_posts' ) ) {
wp_die( 'This content is not publicly available.', 403 );
}
Input Sanitization Reference
| Input | Function to Use |
|---|---|
| Integer IDs | absint( $_GET['post_id'] ) |
| Text strings | sanitize_text_field() |
| URLs | esc_url_raw() |
| HTML output | esc_html(), esc_attr() |
| URL output | esc_url() |
| Nonce fields | wp_verify_nonce() |
| Filenames | sanitize_file_name() |
For detailed security guidance, see the security guide.
Storing API Keys Safely
Provide an admin settings page so site owners can enter their API key through the WordPress UI rather than editing code directly:
function funbrew_register_settings_page() {
add_options_page(
'FUNBREW PDF Settings',
'FUNBREW PDF',
'manage_options',
'funbrew-pdf-settings',
'funbrew_settings_page_html'
);
}
add_action( 'admin_menu', 'funbrew_register_settings_page' );
function funbrew_register_settings() {
register_setting(
'funbrew_pdf_settings',
'funbrew_pdf_api_key',
array(
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'default' => '',
)
);
}
add_action( 'admin_init', 'funbrew_register_settings' );
function funbrew_settings_page_html() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
?>
<div class="wrap">
<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
<form method="post" action="options.php">
<?php settings_fields( 'funbrew_pdf_settings' ); ?>
<table class="form-table">
<tr>
<th scope="row">
<label for="funbrew_pdf_api_key">API Key</label>
</th>
<td>
<input
type="password"
id="funbrew_pdf_api_key"
name="funbrew_pdf_api_key"
value="<?php echo esc_attr( get_option( 'funbrew_pdf_api_key' ) ); ?>"
class="regular-text"
/>
<p class="description">
Get your API key from the <a href="https://pdf.funbrew.cloud/dashboard" target="_blank">FUNBREW PDF dashboard</a>.
</p>
</td>
</tr>
</table>
<?php submit_button(); ?>
</form>
</div>
<?php
}
Structuring This as a Plugin
Here's the recommended file structure for packaging this as a proper WordPress plugin:
wp-content/plugins/funbrew-pdf/
├── funbrew-pdf.php # Main plugin file
├── includes/
│ ├── class-pdf-generator.php # Core PDF generation
│ ├── class-shortcode.php # Shortcode handler
│ ├── class-woocommerce.php # WooCommerce integration
│ ├── class-rest-api.php # REST endpoints
│ └── class-admin-settings.php # Settings page
└── assets/
└── css/
└── download-btn.css
<?php
/**
* Plugin Name: FUNBREW PDF Generator
* Description: PDF generation for WordPress using FUNBREW PDF API.
* Version: 1.0.0
* Author: Your Name
* License: GPL v2 or later
* Text Domain: funbrew-pdf
*/
if ( ! defined( 'ABSPATH' ) ) {
exit; // Prevent direct file access
}
define( 'FUNBREW_PDF_PLUGIN_VERSION', '1.0.0' );
define( 'FUNBREW_PDF_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
define( 'FUNBREW_PDF_API_ENDPOINT', 'https://api.pdf.funbrew.cloud/v1/pdf/from-html' );
require_once FUNBREW_PDF_PLUGIN_DIR . 'includes/class-pdf-generator.php';
require_once FUNBREW_PDF_PLUGIN_DIR . 'includes/class-shortcode.php';
require_once FUNBREW_PDF_PLUGIN_DIR . 'includes/class-rest-api.php';
require_once FUNBREW_PDF_PLUGIN_DIR . 'includes/class-admin-settings.php';
// Load WooCommerce integration only when WooCommerce is active
if ( class_exists( 'WooCommerce' ) ) {
require_once FUNBREW_PDF_PLUGIN_DIR . 'includes/class-woocommerce.php';
}
Performance: Transient Caching and Retry Logic
Transient Cache to Avoid Redundant API Calls
If the same post is downloaded as PDF frequently, use the WordPress Transients API to cache the binary and avoid repeated API calls:
/**
* Get a cached PDF or generate a new one.
*
* @param WP_Post $post The post to PDF-ify.
* @param bool $force Force regeneration even if cached.
* @return string|WP_Error
*/
function funbrew_get_cached_pdf( $post, $force = false ) {
$cache_key = 'funbrew_pdf_' . $post->ID . '_' . $post->post_modified_gmt;
if ( ! $force ) {
$cached = get_transient( $cache_key );
if ( false !== $cached ) {
return $cached;
}
}
$html = funbrew_build_post_html( $post );
$pdf_data = funbrew_generate_pdf( $html );
if ( ! is_wp_error( $pdf_data ) ) {
// Cache for 12 hours, or until the post is updated
set_transient( $cache_key, $pdf_data, 12 * HOUR_IN_SECONDS );
}
return $pdf_data;
}
/**
* Invalidate PDF cache when a post is updated.
*/
function funbrew_clear_pdf_cache_on_update( $post_id ) {
global $wpdb;
$wpdb->query(
$wpdb->prepare(
"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s",
'_transient_funbrew_pdf_' . $post_id . '_%'
)
);
}
add_action( 'save_post', 'funbrew_clear_pdf_cache_on_update' );
Retry Logic with Exponential Backoff
/**
* Generate a PDF with automatic retries on failure.
*
* @param string $html HTML to convert.
* @param array $options API options.
* @param int $max_retry Maximum number of attempts.
* @return string|WP_Error
*/
function funbrew_generate_pdf_with_retry( $html, $options = array(), $max_retry = 3 ) {
$last_error = null;
for ( $attempt = 1; $attempt <= $max_retry; $attempt++ ) {
$result = funbrew_generate_pdf( $html, $options );
if ( ! is_wp_error( $result ) ) {
return $result;
}
$last_error = $result;
if ( $attempt < $max_retry ) {
sleep( $attempt ); // 1s, 2s, 3s backoff
}
}
return $last_error;
}
For production deployment guidelines and monitoring strategies, see the production operations guide. For generating PDFs at scale, see the batch processing guide.
Summary
Here's the complete picture of integrating FUNBREW PDF API with WordPress:
- Use
wp_remote_post()— WordPress's built-in HTTP API makes the integration clean and environment-agnostic - Verify nonces on every AJAX handler — protects against CSRF and quota abuse
- Use
current_user_can()for every privileged action — WordPress's capability system is the right tool for access control - Leverage WooCommerce hooks —
woocommerce_email_attachmentsmakes order invoice PDFs a one-filter implementation - Expose a REST API route — enables headless WordPress and native app integrations
- Cache with transients — reduce API calls for frequently requested PDFs
Try your HTML template in the Playground before building, then check the documentation for full API options. See the pricing page for available plans. For invoice-specific patterns, see the invoice automation guide.
Related Guides
- HTML to PDF: Complete Guide — Full landscape of conversion approaches
- PHP Laravel PDF API Guide — Full-featured Laravel integration
- Certificate PDF Automation — Auto-issue completion certificates
- PDF Webhook Integration — Async PDF generation patterns
- HTML to PDF CSS Optimization — Print CSS best practices