Unofficial, minimal Python SDK for the wFirma.pl API (API Key & OAuth2).
Status: prototype for integrations. Before production, verify scopes, payloads and responses with official docs.
This SDK focuses on a pragmatic subset: contractors, invoices (get/add/download/send), and company accounts. It exposes a low-level
call()escape hatch for everything else.
pip install wfirma-sdk-python # when published
poetry add wfirma-sdk-pythonPython >= 3.9 recommended.
from wfirma_sdk import WFirmaAPIClient
client = WFirmaAPIClient(
base_url="https://api2.wfirma.pl",
company_id="YOUR_COMPANY_ID", # optional but recommended
# ---- Auth option A: API Key headers ----
access_key="...",
secret_key="...",
app_key="...",
# ---- OR Auth option B: OAuth2 ----
# oauth2_token="eyJhbGciOi..." # Bearer token
)
# Add contractor
resp = client.contractors.add({
"name": "Nazwa kontrahenta",
"zip": "12-345",
"country": "PL",
"tax_id_type": "custom",
"nip": "1111111111",
})
print(resp)
# Get invoice
inv = client.invoices.get(12345678)
print(inv)
# Download invoice PDF link (valid for a short time on backend)
link = client.invoices.download(
12345678,
page="all",
address=0,
leaflet=0,
duplicate=0,
payment_cashbox_documents=0,
warehouse_documents=0,
)
print(link["status"]["code"], link.get("response"))
# Send invoice by email
client.invoices.send(
12345678,
email="odbiorca@adres.pl",
subject="Otrzymałeś fakturę",
page="invoice",
leaflet=0,
duplicate=0,
body="Przesyłam fakturę",
)To run the demo example:
# Clone the repository
git clone https://github.com/musictechlab/mtl-wfirma-python-sdk.git
cd mtl-wfirma-python-sdk
# Install dependencies with poetry
poetry install
# Set up your environment variables
cp .env.example .env # if available, or create .env manually
# Edit .env with your credentials:
# WFIRMA_OAUTH_TOKEN=your_oauth_token
# WFIRMA_COMPANY_ID=your_company_id
# Run the demo
poetry run python examples/demo.pyThe demo will fetch the 20 most recent invoices from your wFirma account.
Place keys in headers (client does it for you):
accessKeysecretKeyappKey
client = WFirmaAPIClient(
company_id="...",
access_key="...",
secret_key="...",
app_key="...",
)Use Authorization Code flow in your app to obtain an access_token. Pass it to the client:
client = WFirmaAPIClient(
company_id="...",
oauth2_token="ACCESS_TOKEN_VALUE", # adds Authorization: Bearer ... and ?oauth_version=2
)Tip: Regardless of auth method, remember
company_idif your account has multiple companies.
contractors—add,get,edit,findinvoices—get,add,download,send,find(custom XML)company_accounts—find,getcall(path, ...)— low-level escape hatch
# add
client.contractors.add({
"name": "ACME Sp. z o.o.",
"zip": "00-000",
"city": "Warszawa",
"country": "PL",
"tax_id_type": "nip",
"nip": "1234567890",
})
# get
client.contractors.get(12345)
# edit
client.contractors.edit(12345, {"name": "ACME SA", "zip": "00-001"})
# find (basic paging/fields)
client.contractors.find(page=1, limit=50, fields=["Contractor.id", "Contractor.name"])Advanced find — pass your own <parameters> as XML:
parameters_xml = b"""<?xml version="1.0" encoding="UTF-8"?>
<api>
<contractors>
<parameters>
<page>1</page>
<limit>50</limit>
<fields>
<field>Contractor.id</field>
<field>Contractor.name</field>
</fields>
<conditions>
<condition>
<field>name</field>
<operator>like</operator>
<value>ACME</value>
</condition>
</conditions>
<order>
<asc>name</asc>
</order>
</parameters>
</contractors>
</api>"""
client.contractors.find(conditions_xml=parameters_xml)# get
client.invoices.get(12345678)add — for complex nested structures use raw XML from wFirma docs:
xml_body = b"""<api>
<invoices>
<invoice>
<contractor>
<name>Testowy kontrahent</name>
<zip>10-100</zip>
<city>Wrocław</city>
<street>Prosta</street>
</contractor>
<type>correction</type>
<parent_id>16679047</parent_id>
<invoicecontents>
<invoicecontent>
<parent_id>19630727</parent_id>
<name>produkt1</name>
<count>1.0000</count>
<price>11.00</price>
</invoicecontent>
<invoicecontent>
<parent_id>19630791</parent_id>
<name>produkt2</name>
<count>1.0000</count>
<price>11.00</price>
</invoicecontent>
<invoicecontent>
<name>nowy - produkt3</name>
<count>1.0000</count>
<price>11.00</price>
</invoicecontent>
</invoicecontents>
</invoice>
</invoices>
</api>"""
client.invoices.add(invoice_xml_body=xml_body)download — build <parameters> via helper:
client.invoices.download(
12345678,
page="all", address=0, leaflet=0, duplicate=0,
payment_cashbox_documents=0, warehouse_documents=0,
)send
client.invoices.send(
12345678,
email="odbiorca@adresmailowy123.pl",
subject="Otrzymałeś fakturę",
page="invoice",
leaflet=0,
duplicate=0,
body="Przesyłam fakturę",
)find — pass your own <parameters> XML for full control:
invoices_find_xml = b"""<?xml version="1.0" encoding="UTF-8"?>
<api>
<invoices>
<parameters>
<page>1</page>
<limit>20</limit>
<fields>
<field>Invoice.id</field>
<field>Invoice.fullnumber</field>
<field>Invoice.date</field>
<field>InvoiceContent.name</field>
<field>InvoiceContent.price</field>
</fields>
<conditions>
<condition>
<field>Invoice.remaining</field>
<operator>gt</operator>
<value>0</value>
</condition>
</conditions>
<order>
<desc>date</desc>
</order>
</parameters>
</invoices>
</api>"""
client.invoices.find(parameters_xml=invoices_find_xml)client.company_accounts.find()
client.company_accounts.get(999)- Network/HTTP/XML parsing errors ⇒ raises
WFirmaAPIError. - API-level status checking: if
<status><code>...</code></status>is notOK(orNO_CONTENT),WFirmaAPIErroris raised with the decoded response for inspection.
from wfirma_sdk import WFirmaAPIError
try:
client.invoices.get("bad-id")
except WFirmaAPIError as e:
print(e, e.http_status, e.response)- Format: SDK defaults to
inputFormat=xml&outputFormat=xmland parses XML to Python dicts. - Multiple companies: set
company_idin the client to avoid accidental requests to the wrong company. - Temporary links:
/invoices/downloadreturns a link that is valid only briefly (per backend rules). - Rate limits: avoid bursts; consider backoff/retries on
TOTAL REQUESTS LIMIT EXCEEDEDorTOTAL EXECUTION TIME LIMIT EXCEEDED. - Escape hatch:
client.call(path, method=..., params=..., body_xml=...)to hit any endpoint not yet wrapped.
This SDK is intentionally small. Breaking changes may occur until 1.0. Pin versions in production.
Contributions are welcome! Please read CONTRIBUTING.md before submitting a PR.
To report a vulnerability, please see SECURITY.md.
MIT License — © 2025 MusicTech Lab — see LICENSE for details.
Website | LinkedIn | Let's talk
Crafted by musictechlab.io