PRO REST API
REST API in the polski-pro/v1 namespace for managing invoices, settings and legal documents. Requires authentication and manage_woocommerce capability.
Authentication
Section titled “Authentication”The API requires two authorization elements:
- WordPress nonce - the
X-WP-Nonceheader with a value generated bywp_create_nonce('wp_rest') - Capability - the logged-in user must have the
manage_woocommercecapability
Authentication example (JavaScript)
Section titled “Authentication example (JavaScript)”const response = await fetch('/wp-json/polski-pro/v1/invoices', { headers: { 'X-WP-Nonce': wpApiSettings.nonce, 'Content-Type': 'application/json', },});Authentication example (PHP / cURL)
Section titled “Authentication example (PHP / cURL)”$nonce = wp_create_nonce('wp_rest');
$response = wp_remote_get( rest_url('polski-pro/v1/invoices'), [ 'headers' => [ 'X-WP-Nonce' => $nonce, ], ]);A request without a valid nonce is rejected by WordPress itself, before any handler runs. A logged-in user without the capability is rejected by the endpoint: the invoice routes return polski_pro_forbidden with status 403, while the settings and legal routes fall back to the WordPress default, rest_forbidden with 401 when nobody is logged in and 403 otherwise. The GUS lookup never refuses on capability at all: it lets every caller through and only rejects the ones over the per IP limit, with polski_pro_rate_limited and status 429.
Invoice endpoints
Section titled “Invoice endpoints”Retrieve invoice list
Section titled “Retrieve invoice list”GET /wp-json/polski-pro/v1/invoicesQuery parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
order_id |
int |
none | Filter by WooCommerce order ID |
type |
string |
none | Type filter: faktura_vat, proforma, korygujaca, paragon, packing_slip |
status |
string |
none | Status filter: draft, issued, sent, paid, cancelled |
page |
int |
1 |
Page number, clamped to a minimum of 1 |
per_page |
int |
20 |
Results per page, clamped to 1 to 100 |
These five are the only registered arguments. There is no date range filter and no free-text search: filtering is by order, type and status, and each filter is an exact match.
Response (200 OK):
The body is a plain array of invoice objects. Counts come back in the X-WP-Total and X-WP-TotalPages headers, not in the body. List entries carry no line items: items is filled in only by the single-invoice endpoint.
[ { "id": 1, "order_id": 567, "original_invoice_id": null, "source_refund_id": null, "type": "faktura_vat", "type_label": "Faktura VAT", "number": "FV/2026/04/001", "status": "issued", "status_label": "Issued", "nip_seller": "9876543210", "nip_buyer": "1234567890", "buyer_name": "Firma Testowa Sp. z o.o.", "buyer_address": "ul. Testowa 1", "buyer_city": "Warszawa", "buyer_postcode": "00-001", "correction_reason": null, "net_total": 1000.00, "vat_total": 230.00, "gross_total": 1230.00, "currency": "PLN", "pdf_path": "/wp-content/uploads/polski-pro/invoices/FV-2026-04-001.pdf", "ksef_reference": null, "ksef_status": null, "issued_at": "2026-04-01T10:30:00+02:00", "created_at": "2026-04-01T10:30:00+02:00", "items": [] }]Create invoice from order
Section titled “Create invoice from order”POST /wp-json/polski-pro/v1/invoicesBody parameters (JSON):
| Parameter | Type | Required | Description |
|---|---|---|---|
order_id |
int |
Yes | WooCommerce order ID |
type |
string |
No | Type: faktura_vat (default), proforma, korygujaca, paragon, packing_slip |
Request:
{ "order_id": 567, "type": "faktura_vat"}Response (201 Created):
The created invoice, in the same shape as the list entries above. An unknown type, a missing order or a failed creation returns 400 with a message.
{ "id": 43, "order_id": 567, "original_invoice_id": null, "source_refund_id": null, "type": "faktura_vat", "type_label": "Faktura VAT", "number": "FV/2026/04/002", "status": "issued", "status_label": "Issued", "nip_seller": "9876543210", "nip_buyer": "1234567890", "buyer_name": "Firma Testowa Sp. z o.o.", "buyer_address": "ul. Testowa 1", "buyer_city": "Warszawa", "buyer_postcode": "00-001", "correction_reason": null, "net_total": 500.00, "vat_total": 115.00, "gross_total": 615.00, "currency": "PLN", "pdf_path": null, "ksef_reference": null, "ksef_status": null, "issued_at": "2026-04-05T14:00:00+02:00", "created_at": "2026-04-05T14:00:00+02:00", "items": []}Retrieve invoice details
Section titled “Retrieve invoice details”GET /wp-json/polski-pro/v1/invoices/{id}Returns complete invoice data including line items.
Response (200 OK):
{ "id": 43, "order_id": 567, "original_invoice_id": null, "source_refund_id": null, "type": "faktura_vat", "type_label": "Faktura VAT", "number": "FV/2026/04/002", "status": "issued", "status_label": "Issued", "nip_seller": "9876543210", "nip_buyer": "1234567890", "buyer_name": "Firma Testowa Sp. z o.o.", "buyer_address": "ul. Testowa 1", "buyer_city": "Warszawa", "buyer_postcode": "00-001", "correction_reason": null, "net_total": 212.20, "vat_total": 48.80, "gross_total": 261.00, "currency": "PLN", "pdf_path": "/wp-content/uploads/polski-pro/invoices/FV-2026-04-002.pdf", "ksef_reference": null, "ksef_status": null, "issued_at": "2026-04-05T14:00:00+02:00", "created_at": "2026-04-05T14:00:00+02:00", "items": [ { "id": 91, "product_id": 120, "name": "Produkt testowy", "quantity": 2, "unit": "szt.", "net_price": 100.00, "vat_rate": 23, "vat_amount": 46.00, "gross_price": 246.00 }, { "id": 92, "product_id": null, "name": "Dostawa - InPost Paczkomat", "quantity": 1, "unit": "szt.", "net_price": 12.20, "vat_rate": 23, "vat_amount": 2.80, "gross_price": 15.00 } ]}The seller name and address are not part of this payload. They come from the seller data in the invoice settings; only nip_seller is stored on the invoice.
Response (404 Not Found):
{ "message": "Invoice not found."}Regenerate invoice PDF
Section titled “Regenerate invoice PDF”POST /wp-json/polski-pro/v1/invoices/{id}/pdfRegenerates the invoice PDF file. The endpoint takes no body parameters. There is no template or language to choose: the layout is fixed, and the logo, seller data and footer notes are read from the invoices settings group.
Response (200 OK):
The refreshed invoice object, with pdf_path pointing at the new file. A missing invoice returns 404, a missing order 400, and a failed render 500, each with a message.
Submit invoice to KSeF
Section titled “Submit invoice to KSeF”POST /wp-json/polski-pro/v1/invoices/{id}/ksefQueues the invoice for submission to the National e-Invoice System (KSeF) through Action Scheduler. The endpoint takes no body parameters, and nothing in wp-admin calls it: a REST client is the only way to reach it.
The call returns as soon as the job is queued. It never returns a KSeF reference number, a KSeF status or a UPO link inline, because none of those exist yet at that point. Read them later from ksef_reference and ksef_status on the invoice.
Before you rely on this, read KSeF integration: the API client is pinned to the KSeF test environment, and its authentication flow is not proven end to end.
Response (202 Accepted):
{ "invoice": { "id": 43, "number": "FV/2026/04/002", "status": "issued", "ksef_reference": null, "ksef_status": null }, "message": "Invoice queued for KSeF submission."}The invoice value is the full invoice object shown above, trimmed here to the fields that matter for this call.
Other responses:
| Code | When |
|---|---|
404 |
No invoice with that ID |
422 |
The document is not an accounting document. Only faktura_vat and korygujaca are eligible, so Paragon, Proforma and Packing Slip are rejected here |
409 |
The invoice already carries a KSeF reference, and a submitted invoice can only be corrected, never resubmitted |
503 |
Action Scheduler is unavailable, so nothing can be queued |
{ "message": "Paragon documents are not eligible for KSeF submission."}Create correction invoice
Section titled “Create correction invoice”POST /wp-json/polski-pro/v1/invoices/{id}/correctionCreates a correction invoice linked to the source invoice. The corrected amounts are derived from the order and, when given, from the WooCommerce refund. There is no way to hand-write the corrected lines over this endpoint.
Body parameters (JSON):
| Parameter | Type | Required | Description |
|---|---|---|---|
refund_id |
int |
No | WooCommerce refund the correction is based on |
Request:
{ "refund_id": 981}Response (201 Created):
The correction invoice, in the same shape as every other invoice object. type is korygujaca, original_invoice_id points at the source invoice, and the totals are negative.
{ "id": 44, "order_id": 567, "original_invoice_id": 43, "source_refund_id": 981, "type": "korygujaca", "type_label": "Faktura korygujaca", "number": "FK/2026/04/001", "status": "issued", "status_label": "Issued", "correction_reason": "Zwrot 1 sztuki produktu", "net_total": -100.00, "vat_total": -23.00, "gross_total": -123.00, "currency": "PLN", "ksef_reference": null, "ksef_status": null, "issued_at": "2026-04-05T15:00:00+02:00", "created_at": "2026-04-05T15:00:00+02:00", "items": []}A missing source invoice returns 404, and a correction the service cannot build returns 400.
Statistics endpoint
Section titled “Statistics endpoint”Retrieve invoice statistics
Section titled “Retrieve invoice statistics”GET /wp-json/polski-pro/v1/invoices/statsQuery parameters:
| Parameter | Type | Default | Description |
|---|---|---|---|
days |
int |
30 |
Number of days back, clamped to 1 to 365 |
days is the only argument. There is no grouping parameter: the shape below is fixed.
Response (200 OK):
| Field | Type | Description |
|---|---|---|
period_days |
int |
The window that was counted, echoed back |
total |
int |
Invoices created in the window, every status |
total_revenue |
float |
Sum of gross_total, cancelled invoices excluded |
ksef_sent |
int |
Invoices in the window that carry a KSeF reference |
by_type |
array |
One entry per type: type, count, revenue |
by_status |
array |
One entry per status: status, count |
Revenue is a number, not a formatted string, and it is gross. There is no net or tax breakdown, no per tax rate split and no day by day timeline.
{ "period_days": 30, "total": 156, "total_revenue": 149358.9, "ksef_sent": 0, "by_type": [ { "type": "faktura_vat", "count": 150, "revenue": 152430.0 }, { "type": "korygujaca", "count": 3, "revenue": -1230.0 }, { "type": "proforma", "count": 3, "revenue": 3078.9 } ], "by_status": [ { "status": "issued", "count": 120 }, { "status": "sent", "count": 25 }, { "status": "paid", "count": 8 }, { "status": "cancelled", "count": 3 } ]}The two revenue figures do not match on purpose. by_type adds up to 154278.9, while total_revenue is 149358.9, because the three cancelled invoices here carry 4920.0 in gross: they are counted in their type’s revenue and left out of total_revenue. Expect total_revenue to be lower than the by_type sum whenever the window contains a cancelled invoice, and never higher.
Settings endpoint
Section titled “Settings endpoint”Read and update PRO settings
Section titled “Read and update PRO settings”GET /wp-json/polski-pro/v1/settingsPUT /wp-json/polski-pro/v1/settingsOne route with a readable and an editable method, so GET reads and PUT, PATCH or POST writes. Both require manage_woocommerce.
Body parameters (JSON):
The body is made of top-level setting groups. There is no section parameter and no settings wrapper. The groups the handler recognises:
| Group | Stored option |
|---|---|
invoices |
polski_pro_invoices |
ksef |
polski_pro_ksef |
nip |
polski_pro_nip |
shipping |
polski_pro_shipping |
delivery_date |
polski_delivery_date |
checkout |
polski_pro_checkout |
abandoned_carts |
polski_abandoned_carts |
conditional_payments |
polski_pro_conditional_payments |
product_feed |
polski_pro_product_feed |
loyalty |
polski_pro_loyalty |
sms |
polski_pro_sms |
accounting |
polski_pro_accounting |
Each group you send replaces its stored option wholesale, and keys outside that group’s allow-list are dropped. Groups you leave out of the body are untouched. Anything sent under a name that is not in the list above is ignored.
Request:
{ "invoices": { "auto_generate": true, "auto_generate_statuses": "completed", "numbering_format": "FV/{YYYY}/{MM}/{NR}", "payment_deadline_days": 14 }, "ksef": { "enabled": false, "environment": "test" }}Response (200 OK):
The complete settings object, every group, read back from the options after the write. Trimmed here to the two groups the request touched.
{ "invoices": { "auto_generate": true, "auto_generate_statuses": "completed", "numbering_format": "FV/{YYYY}/{MM}/{NR}", "payment_deadline_days": 14 }, "ksef": { "enabled": false, "environment": "test" }}Note what the replace-wholesale rule costs, and that the response above shows it: the ksef group now holds two keys, so the stored api_token and auto_send are gone, and the invoices group lost the seller data, bank account and email settings that the request left out. Read the group with GET first, change what you need, send it back whole.
Legal document generation endpoint
Section titled “Legal document generation endpoint”Three routes, all requiring manage_woocommerce: one reads the variables the templates accept, one renders a preview, one saves the rendered content as a WordPress page.
Read the available variables
Section titled “Read the available variables”GET /wp-json/polski-pro/v1/legal/variablesReturns the variable names the templates recognise, the values currently derived from the store, and the document types. The route takes no parameters.
Response (200 OK):
| Field | Type | Description |
|---|---|---|
required |
object |
Variable name mapped to a human readable label |
defaults |
object |
The same variable names mapped to the values read from the site and WooCommerce right now |
types |
object |
Document type slug mapped to its label |
defaults is what the renderer merges under whatever you send in variables, so a key you leave out is not empty, it is whatever this route reports. Keys with no source in WooCommerce, company_regon, company_phone, bank_name and bank_account, come back as empty strings until you pass them yourself.
{ "required": { "company_name": "Full company name", "company_address": "Company address", "company_nip": "NIP (tax ID)", "company_regon": "REGON number", "company_email": "Contact email", "company_phone": "Contact phone", "shop_url": "Shop URL", "shop_name": "Shop name", "bank_name": "Bank name", "bank_account": "Bank account number" }, "defaults": { "company_name": "Mój Sklep", "company_address": "ul. Sklepowa 5, 02-222 Warszawa", "company_nip": "9876543210", "company_regon": "", "company_phone": "", "shop_url": "https://mojsklep.pl", "shop_name": "Mój Sklep", "bank_name": "", "bank_account": "" }, "types": { "regulamin": "Regulamin sklepu", "polityka-prywatnosci": "Polityka prywatnosci", "polityka-zwrotow": "Prawo odstapienia / Polityka zwrotow" }}Generate legal document
Section titled “Generate legal document”POST /wp-json/polski-pro/v1/legal/generateRenders a legal document from the bundled Polish template and the store data. This is a preview: nothing is saved.
Body parameters (JSON):
| Parameter | Type | Required | Description |
|---|---|---|---|
type |
string |
Yes | Document type: regulamin, polityka-prywatnosci, polityka-zwrotow |
variables |
object |
No | Placeholder values, defaults to an empty object |
The templates are Polish and there is no format or language parameter. Output is always the rendered template.
Recognised keys inside variables: company_name, company_address, company_nip, company_regon, company_email, company_phone, shop_url, shop_name, bank_name, bank_account. Every one is optional. Anything you leave out is filled from the store: the site name, the WooCommerce store address, the WooCommerce sender email and the site URL.
Request:
{ "type": "regulamin", "variables": { "company_name": "Mój Sklep Sp. z o.o.", "company_nip": "9876543210", "company_address": "ul. Sklepowa 5, 02-222 Warszawa", "company_phone": "+48 123 456 789" }}Response (200 OK):
{ "type": "regulamin", "content": "<h1>Regulamin sklepu internetowego...</h1>...", "variables_used": { "company_name": "Mój Sklep Sp. z o.o.", "company_nip": "9876543210", "company_address": "ul. Sklepowa 5, 02-222 Warszawa", "company_phone": "+48 123 456 789" }}variables_used echoes back what you sent, after sanitising, not the merged set. There is no word count and no section list. An unknown type returns 400, and a missing or empty template returns 500, each with a message.
Publish a document as a page
Section titled “Publish a document as a page”POST /wp-json/polski-pro/v1/legal/publishRenders the same content as generate and saves it as a WordPress page. The page is always created with status draft, so nothing goes live until you publish it yourself. The resulting page ID is written to the matching Polski option, polski_terms_page_id for regulamin, polski_privacy_page_id for polityka-prywatnosci, polski_returns_page_id for polityka-zwrotow.
Body parameters (JSON):
| Parameter | Type | Required | Description |
|---|---|---|---|
type |
string |
Yes | Document type: regulamin, polityka-prywatnosci, polityka-zwrotow |
variables |
object |
No | Placeholder values, defaults to an empty object, same keys as generate |
page_id |
int |
No | Existing page to overwrite, defaults to 0, passed through absint |
Leave page_id at 0 and a new page is inserted. Pass an ID that exists and that page is updated in place, title, content and status included, so regenerating a document does not leave a second copy behind. Pass an ID that no longer exists and the handler falls back to inserting a new page rather than failing.
Request:
{ "type": "regulamin", "page_id": 312, "variables": { "company_name": "Mój Sklep Sp. z o.o.", "company_nip": "9876543210" }}Response (201 Created):
| Field | Type | Description |
|---|---|---|
page_id |
int |
ID of the page that was created or updated |
edit_url |
string |
Raw wp-admin edit link for that page |
view_url |
string |
Permalink of the page |
status |
string |
Always draft |
message |
string |
Confirmation text |
{ "page_id": 312, "edit_url": "https://mojsklep.pl/wp-admin/post.php?post=312&action=edit", "view_url": "https://mojsklep.pl/?page_id=312", "status": "draft", "message": "Page created as draft. Review and publish when ready."}The rendered HTML is not echoed back here. Call generate first if you want to see the content before it lands in a page. An unknown type returns 400, a missing or empty template 500, and a page WordPress refused to save also 500, each with a message.
Error codes
Section titled “Error codes”The invoice endpoints do not use named error codes. A failure is a plain message string plus an HTTP status, so branch on the status, never on a code:
{ "message": "Invoice not found."}| HTTP code | Message | When |
|---|---|---|
| 400 | Invalid invoice type. |
type is outside the invoice type list |
| 400 | Failed to create invoice. Check order exists. |
The order is missing or creation failed |
| 400 | Associated order not found. |
PDF regeneration when the order is gone |
| 400 | Failed to create correction invoice. |
The correction could not be built |
| 404 | Invoice not found. |
No invoice with that ID |
| 404 | Original invoice not found. |
Correction against a source invoice that does not exist |
| 409 | This invoice has already been submitted to KSeF. |
The invoice already carries a KSeF reference |
| 422 | ... documents are not eligible for KSeF submission. |
The document is not an accounting document |
| 500 | PDF generation failed. |
The renderer returned nothing, TCPDF missing included |
| 503 | Action Scheduler is unavailable, so the invoice cannot be queued. |
Nothing can be queued for KSeF |
The one named code in the invoice controller is polski_pro_forbidden, status 403, from the permission check when the user lacks manage_woocommerce. On the settings and legal routes the equivalent rejection is the WordPress default, rest_forbidden. The GUS lookup adds polski_pro_invalid_nip (400), polski_pro_gus_unavailable (502) and polski_pro_rate_limited (429).
Limits and throttling
Section titled “Limits and throttling”There is no global rate limit. Requests to the invoice, statistics, settings and legal endpoints are not counted or throttled.
The one limiter in the PRO REST layer is on the GUS company lookup, POST /wp-json/polski-pro/v1/gus/lookup, because that endpoint is reachable from the checkout by users who are not logged in. Anyone with manage_woocommerce skips it entirely. Everyone else is counted per client IP, by default 10 lookups per hour, and gets polski_pro_rate_limited with status 429 once over. No Retry-After header is sent.
Both numbers are filterable, so the window can be widened or the limiter switched off by returning 0:
add_filter('polski-pro/gus/rate_limit_window_seconds', fn () => HOUR_IN_SECONDS);add_filter('polski-pro/gus/rate_limit_max_attempts', fn () => 10);Next steps
Section titled “Next steps”- Report issues: GitHub Issues
- Related: Accounting integrations