Docs Emboss - PDF Invoices and Packing Slips for WooCommerce

REST API

REST API#

Emboss - PDF Invoices and Packing Slips for WooCommerce exposes three REST endpoints under the epdi/v1 namespace for headless WooCommerce setups, integrations with accounting tools, and custom dashboards. The API is disabled by default and must be enabled in settings.

Enabling the API#

Go to WooCommerce > Settings > Invoices > Advanced and toggle Enable REST API.

This was a deliberate design choice: most stores don’t need the REST API and exposing additional endpoints unnecessarily increases the attack surface. Enable it only when you actually plan to call it.

Authentication#

All endpoints require the manage_woocommerce capability.

For programmatic access, use WooCommerce REST API authentication:

  • Application passwords (WordPress 5.6+, recommended for headless setups).
  • Basic Auth over HTTPS with WooCommerce REST API consumer key + secret.
  • Cookie authentication with a nonce for browser-based JavaScript clients.

Anonymous requests return 401 Unauthorized.

Endpoints#

GET /wp-json/epdi/v1/documents/{order_id}#

Returns metadata for the document associated with an order.

Path parameters:

  • order_id (integer, required) — the WooCommerce order ID.

Response (200 OK):

{
  "order_id": 123,
  "document_type": "receipt",
  "invoice_number": 42,
  "display_number": "INV-2026-000042",
  "generated_date": "2026-04-09T14:23:11+00:00",
  "file_size": 23847,
  "pdf_url": "https://yourstore.com/?epdi_download=123&key=wc_order_abc123",
  "regenerate_url": "https://yourstore.com/wp-json/epdi/v1/documents/123/regenerate"
}

Errors:

  • 401 — not authenticated.
  • 403 — missing manage_woocommerce capability.
  • 404 — order doesn’t exist.
  • 404 — order has no PDF yet (call regenerate to create one).

Example with cURL:

curl https://yourstore.com/wp-json/epdi/v1/documents/123 \
  -u "ck_xxx:cs_yyy"

GET /wp-json/epdi/v1/documents/{order_id}/pdf#

Streams the PDF binary for an order.

Path parameters:

  • order_id (integer, required).

Response (200 OK):

  • Content-Type: application/pdf
  • Content-Disposition: attachment; filename="invoice-INV-2026-000042.pdf"
  • Content-Length: 23847
  • Binary PDF body.

Errors:

  • 401, 403, 404 — same as the metadata endpoint.

Example with cURL:

curl https://yourstore.com/wp-json/epdi/v1/documents/123/pdf \
  -u "ck_xxx:cs_yyy" \
  --output invoice-123.pdf

POST /wp-json/epdi/v1/documents/{order_id}/regenerate#

Forces regeneration of the PDF for an order.

Path parameters:

  • order_id (integer, required).

Body parameters (optional, JSON):

{
  "force_type": "invoice"
}
  • force_type (string, optional) — one of invoice, receipt, credit-note, packing-slip. Overrides the auto-resolved type.

Response (200 OK):

Same structure as GET /documents/{order_id} with the new metadata.

Errors:

  • 401, 403, 404 — same as above.
  • 400 — invalid force_type.
  • 422 — order is in a state that doesn’t generate documents (cancelled, failed). Override with force_type if you really want to.

Example with cURL:

curl -X POST https://yourstore.com/wp-json/epdi/v1/documents/123/regenerate \
  -u "ck_xxx:cs_yyy" \
  -H "Content-Type: application/json" \
  -d '{"force_type": "receipt"}'

Common patterns#

Sync invoices to your accounting tool#

After every order completion, fetch the receipt and forward it:

async function syncInvoice(orderId) {
  const auth = btoa(`${process.env.WC_CK}:${process.env.WC_CS}`);
  const headers = { Authorization: `Basic ${auth}` };

  const meta = await fetch(
    `https://yourstore.com/wp-json/epdi/v1/documents/${orderId}`,
    { headers }
  ).then(r => r.json());

  const pdf = await fetch(
    `https://yourstore.com/wp-json/epdi/v1/documents/${orderId}/pdf`,
    { headers }
  ).then(r => r.arrayBuffer());

  await uploadToQuickBooks({
    invoiceNumber: meta.display_number,
    orderId: meta.order_id,
    generatedDate: meta.generated_date,
    pdf: Buffer.from(pdf),
  });
}

Hook it to woocommerce_order_status_completed via a webhook or scheduled job.

Bulk regenerate from a script#

After updating branding or switching template variants, regenerate the last 30 days of orders:

#!/bin/bash
# requires: jq, curl, WooCommerce REST API credentials in env

CK="$WC_CONSUMER_KEY"
CS="$WC_CONSUMER_SECRET"
SITE="https://yourstore.com"

# Get orders from the last 30 days
SINCE=$(date -u -v-30d +"%Y-%m-%dT%H:%M:%S")
ORDERS=$(curl -s -u "$CK:$CS" \
  "$SITE/wp-json/wc/v3/orders?after=$SINCE&per_page=100&status=completed" \
  | jq -r '.[].id')

for ID in $ORDERS; do
  echo "Regenerating order $ID..."
  curl -s -X POST -u "$CK:$CS" \
    -H "Content-Type: application/json" \
    "$SITE/wp-json/epdi/v1/documents/$ID/regenerate" \
    > /dev/null
done

For larger batches, prefer the bulk regenerate action on the order list — it uses Action Scheduler and won’t time out.

Headless storefront (Next.js / Remix)#

After completing an order in your headless storefront, fetch the receipt and present it inline:

// app/orders/[id]/receipt/route.ts
import { NextRequest } from 'next/server';

export async function GET(
  _request: NextRequest,
  { params }: { params: { id: string } }
) {
  const auth = Buffer.from(
    `${process.env.WC_CK}:${process.env.WC_CS}`
  ).toString('base64');

  const upstream = await fetch(
    `${process.env.WC_URL}/wp-json/epdi/v1/documents/${params.id}/pdf`,
    { headers: { Authorization: `Basic ${auth}` } }
  );

  return new Response(upstream.body, {
    headers: {
      'Content-Type': 'application/pdf',
      'Content-Disposition': 'inline; filename="receipt.pdf"',
    },
  });
}

The customer-facing route stays clean (/orders/123/receipt) while the auth headers stay server-side.

Rate limiting#

The REST endpoints don’t have a built-in rate limit. Use a server-level rate limiter (Cloudflare, Nginx, fail2ban) if you’re exposing the API to less-trusted callers.

The frontend download endpoint (?epdi_download=...) does have rate limiting — see Customer Downloads.

CORS#

The plugin doesn’t add custom CORS headers. WooCommerce and WordPress core handle CORS via standard hooks. If you need to call the API from a browser on a different origin, configure CORS at your web server level or via a plugin.

Schema#

If you’re generating client SDKs, the OpenAPI-style schema lives in the response of:

curl https://yourstore.com/wp-json/epdi/v1/ \
  -u "ck_xxx:cs_yyy"

Each route declares its arguments, response shape, and permission callback in the standard WordPress REST schema format.

Extending the API#

To add custom endpoints under the same epdi/v1 namespace:

add_action( 'rest_api_init', function() {
    register_rest_route( 'epdi/v1', '/documents/(?P<id>\d+)/email', array(
        'methods' => 'POST',
        'callback' => 'my_email_invoice_handler',
        'permission_callback' => function() {
            return current_user_can( 'manage_woocommerce' );
        },
    ) );
} );

The plugin’s existing endpoints all use the \EasyPDFInvoices\REST\InvoiceController class — read its source if you want to follow the same conventions for argument validation, error responses, and permission checks.