View Categories

REST API: PDF Invoices and packing slips

6 min read

The REST API for PDF Invoices, Pick Lists & Packing Slips: list the documents an order has, issue its invoice, and fetch an issued PDF. It is built for a back-office tool that issues and files invoices, and for a headless store that shows its customers a link to their invoice. The setup and the conventions shared by every endpoint (keys, errors, pagination) are in <a href=”/docs/rest-api/”>REST API overview and authentication</a>.

Base URL and sign-in #

NamespaceWhat lives thereNeeds
/wp-json/sidocs/v1/orders/{id}/documents (list, issue) and documents/{id}/pdf (fetch)PDF Invoices, Pick Lists & Packing Slips, with WooCommerce
  • sidocs/v1 does not start with wc-, so a WooCommerce REST API key does not work here. A call with one answers 401 (invalid_username). Use a WordPress Application Password (Users > Profile > Application Passwords) as HTTP Basic (username:application password), or, inside wp-admin, the logged-in cookie with an X-WP-Nonce header.
  • The routes have been in PDF Invoices since 1.0.0.
  • Send bodies as JSON (Content-Type: application/json).
  • Errors use the WordPress shape {"code", "message", "data": {"status"}}. The PDF Invoices codes start with sidocs_.
STORE="https://example.com/wp-json"
curl -u "username:YOUR_APP_PASSWORD" "$STORE/sidocs/v1/orders/123/documents"

Who may do what #

AbilityWho has itOpens
edit_shop_orders or manage_woocommerceShop managers and administrators, and any role given one of themAll three routes, for every order
The order’s key (below)Anyone who holds itGET /orders/{id}/documents, for that order, and only the documents a customer may have

A request that is not allowed answers 401 (not signed in) or 403, code rest_forbidden.

The order key for customers and headless stores #

A store front end can read an order’s documents without a staff login by sending the WooCommerce order key as key: the value that starts with wc_order_, which is the order_key field of the order in the WooCommerce API and the key in the order’s thank-you and order-view links. Send it in the query string (?key=wc_order_AbCdEf123456). The key works like this:

  • It has to match the order in the URL. A wrong or missing key answers 401 rest_forbidden.
  • A guest order (no customer account) opens with the key alone, as long as Guests is ticked in the plugin’s Settings (the box is ticked by default).
  • An order that belongs to a customer account opens only when the request is also signed in as that customer (an Application Password or the logged-in cookie of that user). The key alone is refused. The key is not a login.
  • The key never opens the other two routes. Issuing and GET /documents/{id}/pdf are staff only.

What a key holder sees is limited by the Settings tab, My Account shows: by default only the invoice, and only once it has been issued. A customer never makes a number.

Documents of an order #

GET /sidocs/v1/orders/{id}/documents #

Lists the document types this order gets, with the number and issue time of those already issued.

ParameterNotes
idIn the path. The WooCommerce order id
keyOptional for staff. For a customer or a headless store: the order key (above)

Staff get every type that applies to the order. A key holder gets only the types a customer may open. Each entry:

FieldNotes
typeThe document type: invoice, packing-slip, and, on a store with rentals, return-slip (rental orders) and deposit-receipt (when a deposit was refunded). Credit notes and pick lists are not listed here: a credit note is made by a refund, and a pick list covers many orders
labelThe name shown to people (Invoice)
numberThe document number (INV-0082) for a numbered type that has been issued; empty otherwise
issuedWhen it was issued, in UTC (Y-m-d H:i:s); empty otherwise
urlA link the customer can open to get the PDF, built with the order key. Empty when a customer may not open this type yet, for example an invoice that has not been issued
[
  {
    "type": "invoice",
    "label": "Invoice",
    "number": "INV-0082",
    "issued": "2026-11-11 01:29:19",
    "url": "https://example.com/?sidocs_doc=invoice&order=123&key=wc_order_AbCdEf123456"
  },
  {
    "type": "packing-slip",
    "label": "Packing slip",
    "number": "",
    "issued": "",
    "url": ""
  }
]
ErrorWhen
404 sidocs_orderThe order does not exist (staff only: a key holder for a missing order gets 401)
401 or 403 rest_forbiddenNot staff, and the key is missing, wrong, or the order belongs to an account you are not signed in as

POST /sidocs/v1/orders/{id}/documents #

Issues a numbered document for the order. Staff only. This is how a back-office tool issues an invoice.

ParameterNotes
typeRequired. A numbered, kept type. Use invoice. A type that is not numbered (packing-slip, return-slip) cannot be issued: it is drawn fresh each time it is opened

It answers 200:

FieldNotes
idThe document id, the one GET /documents/{id}/pdf takes
numberThe number taken from the series (INV-0082)

What issuing does:

  • The call takes the next number in the type’s series (a prefix, no gaps, no duplicates, yearly reset if you set one) and freezes what the document says and how it looks. Later changes to the order, the brand or the template do not change it.
  • It is safe to repeat. If the order already has an issued invoice, the call returns that document’s id and number and takes no new number. This is also how you look up the id of an invoice that was issued earlier.
  • Issuing the invoice by this call is the same as printing it on the order screen. The store can also issue it by itself when the order reaches a status, set in Settings under Issue the invoice when an order becomes.
  • The PDF is drawn when the number is taken and stored. Stored PDF files are deleted after the retention period you set (30 days by default) and drawn again, identical in content, when someone opens the document.
ErrorWhen
404 sidocs_orderThe order does not exist, type is missing, or the type is not a numbered one
401 or 403 rest_forbiddenNot staff
500 sidocs_busyThe number series was busy, even after retries. Send the call again
500 sidocs_seriesThe number series could not be read

The PDF #

GET /sidocs/v1/documents/{id}/pdf #

Returns one issued document’s PDF inside JSON, as base64. Staff only. The response is application/json, not a PDF, so a client has to decode the pdf field.

ParameterNotes
idIn the path. The document id from POST /orders/{id}/documents
FieldNotes
numberThe document number
pdfThe file, base64 encoded

It checks the stored file against the record when it reads it. If the file was removed by the retention clean-up, it is drawn again from the frozen data. Save it from the shell like this:

curl -s -u "username:YOUR_APP_PASSWORD" \
  "https://example.com/wp-json/sidocs/v1/documents/456/pdf" \
  | python3 -c "import sys, json, base64; d = json.load(sys.stdin); open(d['number'] + '.pdf', 'wb').write(base64.b64decode(d['pdf']))"
ErrorWhen
404 sidocs_documentNo document with that id
401 or 403 rest_forbiddenNot staff
500 sidocs_tamperedThe stored file does not match its record, so it was not served
500 sidocs_drawThe file was gone and could not be drawn again. See WooCommerce > Status > Logs

Getting the PDF as a file, for a customer #

A customer or a headless store does not use the REST route for the file. It opens the url from GET /orders/{id}/documents. That link is a normal page of the store, not a REST route, and it answers the PDF itself:

  • Content-Type: application/pdf, Content-Disposition: attachment; filename="INV-0082.pdf".
  • Access is the same as the list: the key has to match the order, a guest order needs Guests ticked, and an order of a customer account needs that customer signed in (the cookie). Without those it answers 403 with an HTML message, not JSON.
  • Only the types a customer may have are served, and a numbered one only once it has been issued.

Save it with curl’s -o:

curl -s -o INV-0082.pdf \
  "https://example.com/?sidocs_doc=invoice&order=123&key=wc_order_AbCdEf123456"

Check what you got with file INV-0082.pdf (it should say PDF document). A 403 page saves as HTML under the same name, so check the status first with -w "%{http_code}" if a script depends on it.

Examples #

A headless store: show the customer the documents of their guest order, then fetch the invoice.

curl "https://example.com/wp-json/sidocs/v1/orders/123/documents?key=wc_order_AbCdEf123456"

A back-office tool: issue the invoice, then store the PDF.

curl -u "username:YOUR_APP_PASSWORD" -X POST \
  -H "Content-Type: application/json" \
  -d '{"type": "invoice"}' \
  "https://example.com/wp-json/sidocs/v1/orders/123/documents"
{
  "id": 456,
  "number": "INV-0082"
}

Then call GET /documents/456/pdf as shown above.

Hooks #

The plugin has filters for the file name (sidocs_file_name) and for adding document types (sidocs_documents). Neither is needed to use the API.