Skip to content

PRO REST API

REST API in the polski-pro/v1 namespace for managing invoices, settings and legal documents. Requires authentication and manage_woocommerce capability.

The API requires two authorization elements:

  1. WordPress nonce - the X-WP-Nonce header with a value generated by wp_create_nonce('wp_rest')
  2. Capability - the logged-in user must have the manage_woocommerce capability
const response = await fetch('/wp-json/polski-pro/v1/invoices', {
headers: {
'X-WP-Nonce': wpApiSettings.nonce,
'Content-Type': 'application/json',
},
});
$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.

GET /wp-json/polski-pro/v1/invoices

Query 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": []
}
]
POST /wp-json/polski-pro/v1/invoices

Body 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": []
}
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."
}
POST /wp-json/polski-pro/v1/invoices/{id}/pdf

Regenerates 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.

POST /wp-json/polski-pro/v1/invoices/{id}/ksef

Queues 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."
}
POST /wp-json/polski-pro/v1/invoices/{id}/correction

Creates 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.

GET /wp-json/polski-pro/v1/invoices/stats

Query 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.

GET /wp-json/polski-pro/v1/settings
PUT /wp-json/polski-pro/v1/settings

One 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.

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.

GET /wp-json/polski-pro/v1/legal/variables

Returns 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_email": "[email protected]",
"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"
}
}
POST /wp-json/polski-pro/v1/legal/generate

Renders 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_email": "[email protected]",
"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_email": "[email protected]",
"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.

POST /wp-json/polski-pro/v1/legal/publish

Renders 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.

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).

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);
This page is for informational purposes only and does not constitute legal advice. Consult a lawyer before implementation. Polski for WooCommerce is open source software (GPLv2) provided without warranty.