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— missingmanage_woocommercecapability.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/pdfContent-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 ofinvoice,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— invalidforce_type.422— order is in a state that doesn’t generate documents (cancelled, failed). Override withforce_typeif 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.
Related#
- Customer Downloads — the public download endpoint.
- Admin Tools — bulk operations from the admin UI.
- Developer Guide — full hook and filter reference.