stackin-python-sdk 0.1.2__py3-none-any.whl → 0.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
stackin/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
1
  """Invoice __init__ module."""
2
2
 
3
- __version__ = "0.1.2"
3
+ __version__ = "0.3.0"
4
4
  __description__ = (
5
5
  "Python SDK for issuing, consulting and cancelling electronic invoices."
6
6
  )
stackin/core/client.py CHANGED
@@ -36,6 +36,32 @@ def _resolve_base_url(
36
36
  return DEFAULT_BASE_URL
37
37
 
38
38
 
39
+ _NFE_ADDRESS_FIELDS = (
40
+ "street",
41
+ "number",
42
+ "neighborhood",
43
+ "city",
44
+ "state",
45
+ "zip_code",
46
+ "city_code",
47
+ )
48
+
49
+
50
+ def _validate_nfe_address(address: Address | None) -> None:
51
+ """NFE needs the buyer's full address — a partial one is a SEFAZ rejection."""
52
+ if address is None:
53
+ raise ValueError("recipient_address is required for NFE")
54
+
55
+ missing = [
56
+ field for field in _NFE_ADDRESS_FIELDS if not getattr(address, field)
57
+ ]
58
+ if missing:
59
+ raise ValueError(
60
+ "recipient_address is missing required fields for NFE: "
61
+ + ", ".join(missing)
62
+ )
63
+
64
+
39
65
  class Invoice:
40
66
  """Client for issuing, consulting, and cancelling fiscal documents."""
41
67
 
@@ -61,6 +87,7 @@ class Invoice:
61
87
  recipient_address: Address | None = None,
62
88
  series: str | None = None,
63
89
  number: str | None = None,
90
+ idempotency_key: str | None = None,
64
91
  ) -> dict:
65
92
  """Issues a fiscal document."""
66
93
  if not items:
@@ -74,6 +101,7 @@ class Invoice:
74
101
  raise ValueError(
75
102
  f"items[{index}].cfop is required for NFE"
76
103
  )
104
+ _validate_nfe_address(recipient_address)
77
105
 
78
106
  payload = {
79
107
  "document_type": document_type.value,
@@ -88,7 +116,12 @@ class Invoice:
88
116
  if number:
89
117
  payload["number"] = number
90
118
 
91
- return self._request("POST", "/invoices", json=payload)
119
+ return self._request(
120
+ "POST",
121
+ "/invoices",
122
+ json=payload,
123
+ idempotency_key=idempotency_key,
124
+ )
92
125
 
93
126
  def consult(
94
127
  self,
@@ -118,10 +151,86 @@ class Invoice:
118
151
  "POST", f"/invoices/{access_key}/cancel", json=payload
119
152
  )
120
153
 
121
- def _headers(self) -> dict:
154
+ def invalidate(
155
+ self,
156
+ *,
157
+ series: str,
158
+ number_start: int,
159
+ number_end: int,
160
+ reason: str,
161
+ ) -> dict:
162
+ """Reports a reserved but never used NFE numbering range."""
163
+ if not 15 <= len(reason) <= 255:
164
+ raise ValueError("reason must be 15 to 255 characters")
165
+ if number_end < number_start:
166
+ raise ValueError("number_end can't be below number_start")
167
+
168
+ payload = {
169
+ "series": series,
170
+ "number_start": number_start,
171
+ "number_end": number_end,
172
+ "reason": reason,
173
+ }
174
+
175
+ return self._request("POST", "/invoices/invalidations", json=payload)
176
+
177
+ def correct(
178
+ self,
179
+ access_key: str,
180
+ *,
181
+ document_type: DocumentType,
182
+ correction: str,
183
+ ) -> dict:
184
+ """Files a correction letter (CC-e) against an issued document."""
185
+ if not 15 <= len(correction) <= 1000:
186
+ raise ValueError("correction must be 15 to 1000 characters")
187
+
188
+ payload = {
189
+ "document_type": document_type.value,
190
+ "correction": correction,
191
+ }
192
+
193
+ return self._request(
194
+ "POST", f"/invoices/{access_key}/correction", json=payload
195
+ )
196
+
197
+ def reissue(
198
+ self, invoice_id: str, *, idempotency_key: str | None = None
199
+ ) -> dict:
200
+ """Retries a previous invoice submission by its local id."""
201
+ return self._request(
202
+ "POST",
203
+ f"/invoices/{invoice_id}/reissue",
204
+ idempotency_key=idempotency_key,
205
+ )
206
+
207
+ def pdf(
208
+ self,
209
+ access_key: str,
210
+ *,
211
+ document_type: DocumentType,
212
+ ) -> bytes:
213
+ """The authorizer's rendering of an authorized document.
214
+
215
+ Returns raw bytes — the only method that does not return a
216
+ parsed object. The XML is the legally valid document; this is a
217
+ convenience, and the authorizer's endpoint for it is unstable,
218
+ so an APIError with status 502 means the authorizer is
219
+ unavailable, not that the invoice is wrong. NFS-e only.
220
+ """
221
+ params = {"document_type": document_type.value}
222
+ response = self._send(
223
+ "GET", f"/invoices/{access_key}/pdf", params=params
224
+ )
225
+ return response.content
226
+
227
+ def _headers(self, idempotency_key: str | None = None) -> dict:
228
+ headers = {}
122
229
  if self.api_key:
123
- return {"Authorization": f"Bearer {self.api_key}"}
124
- return {}
230
+ headers["Authorization"] = f"Bearer {self.api_key}"
231
+ if idempotency_key:
232
+ headers["Idempotency-Key"] = idempotency_key
233
+ return headers
125
234
 
126
235
  def _request(
127
236
  self,
@@ -130,7 +239,30 @@ class Invoice:
130
239
  *,
131
240
  json: dict | None = None,
132
241
  params: dict | None = None,
242
+ idempotency_key: str | None = None,
133
243
  ) -> dict:
244
+ response = self._send(
245
+ method,
246
+ path,
247
+ json=json,
248
+ params=params,
249
+ idempotency_key=idempotency_key,
250
+ )
251
+ try:
252
+ body = response.json() if response.content else {}
253
+ except ValueError:
254
+ body = {}
255
+ return body.get("result", body)
256
+
257
+ def _send(
258
+ self,
259
+ method: str,
260
+ path: str,
261
+ *,
262
+ json: dict | None = None,
263
+ params: dict | None = None,
264
+ idempotency_key: str | None = None,
265
+ ) -> requests.Response:
134
266
  url = f"{self.base_url}/api/v1{path}"
135
267
 
136
268
  try:
@@ -139,21 +271,20 @@ class Invoice:
139
271
  url,
140
272
  json=json,
141
273
  params=params,
142
- headers=self._headers(),
274
+ headers=self._headers(idempotency_key),
143
275
  timeout=self.timeout,
144
276
  )
145
277
  except requests.RequestException as error:
146
278
  raise ConnectionFailedError(str(error)) from error
147
279
 
148
- try:
149
- body = response.json() if response.content else {}
150
- except ValueError:
151
- body = {}
152
-
153
280
  if not response.ok:
281
+ try:
282
+ body = response.json() if response.content else {}
283
+ except ValueError:
284
+ body = {}
154
285
  raise APIError(
155
286
  status_code=response.status_code,
156
287
  detail=body.get("detail", response.text),
157
288
  )
158
289
 
159
- return body.get("result", body)
290
+ return response
@@ -0,0 +1,214 @@
1
+ Metadata-Version: 2.4
2
+ Name: stackin-python-sdk
3
+ Version: 0.3.0
4
+ Summary: Python SDK for issuing, consulting and cancelling electronic invoices.
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Keywords: nfe,nfse,nota-fiscal,nota-fiscal-eletronica,invoice,fiscal-document,brazil,sefaz,e-invoicing
8
+ Author: Fernando Celmer
9
+ Author-email: email@fernandocelmer.com
10
+ Requires-Python: >=3.10
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Office/Business :: Financial :: Accounting
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Classifier: Natural Language :: Portuguese (Brazilian)
21
+ Requires-Dist: pydantic
22
+ Requires-Dist: requests
23
+ Project-URL: Changelog, https://github.com/stackin-io/stackin-python-sdk/releases
24
+ Project-URL: Documentation, https://docs.stackin.io/blog/python-sdk
25
+ Project-URL: Homepage, https://docs.stackin.io
26
+ Project-URL: Issues, https://github.com/stackin-io/stackin-python-sdk/issues
27
+ Project-URL: Repository, https://github.com/stackin-io/stackin-python-sdk
28
+ Description-Content-Type: text/markdown
29
+
30
+ <div align="center">
31
+
32
+ <img src="https://raw.githubusercontent.com/stackin-io/stackin-python-sdk/master/docs/assets/stackin.png" width="120" />
33
+
34
+ **Integrate once. Issue everywhere.**
35
+
36
+ [![Python](https://img.shields.io/badge/python-3.10%2B-blue?style=flat-square)](pyproject.toml)
37
+ [![PyPI](https://img.shields.io/pypi/v/stackin-python-sdk?style=flat-square)](https://pypi.org/project/stackin-python-sdk/)
38
+ [![License](https://img.shields.io/badge/license-MIT-informational?style=flat-square)](https://github.com/stackin-io/stackin-python-sdk)
39
+
40
+ [API Reference](https://docs.stackin.io) · [Python SDK guide](https://docs.stackin.io/blog/python-sdk)
41
+
42
+ </div>
43
+
44
+ ---
45
+
46
+ # stackin
47
+
48
+ Python SDK for fiscal document issuance — a handful of business fields, nothing about certificates, XML, XSD, signing or SOAP. The API resolves all of that from the issuer's own configuration, identified by `api_key`.
49
+
50
+ **One class, `Invoice`** — `issue()`/`consult()`/`cancel()`/`reissue()`/`correct()`/`invalidate()`/`pdf()`, nothing else to instantiate. Each line item is a `Product` (`stackin.br`) — `description`/`amount` are universal, everything else (`ncm`/`cfop`/`cest`/tax groups...) is Brazil-specific and only required for NFE; NFSE ignores it.
51
+
52
+ ## Install
53
+
54
+ ```bash
55
+ pip install stackin-python-sdk
56
+ ```
57
+
58
+ ## Usage
59
+
60
+ Get an `api_key` from the [stackin dashboard](https://app.stackin.io) — select the issuing company, then Settings → API key (context `sdk`). One key per issuing company, shown once at creation. The API resolves the issuer (CNPJ, state, address, certificate, environment) entirely from it; nothing about the issuer is ever passed on a call.
61
+
62
+ ```python
63
+ from stackin import Invoice, DocumentType, Address
64
+ from stackin.br import Product # Brazil-specific line item — NCM/CFOP
65
+
66
+ client = Invoice(api_key="COMPANY_API_KEY") # defaults to https://sdk.stackin.io
67
+
68
+ invoice = client.issue(
69
+ document_type=DocumentType.NFSE,
70
+ client_name="John Doe",
71
+ tax_id="00000000000",
72
+ items=[Product(description="Software development", amount=5000.00)],
73
+ )
74
+
75
+ status = client.consult("ACCESS_KEY...", document_type=DocumentType.NFSE)
76
+ client.cancel(
77
+ "ACCESS_KEY...",
78
+ document_type=DocumentType.NFSE,
79
+ reason="Typo",
80
+ )
81
+ client.reissue(invoice["id"]) # retries a rejected/failed submission
82
+
83
+ # The authorizer's PDF, as raw bytes. NFS-e only; the XML stays the
84
+ # legally valid document, and a 502 here means the authorizer is down.
85
+ with open("nota.pdf", "wb") as file:
86
+ file.write(client.pdf(access_key, document_type=DocumentType.NFSE))
87
+
88
+ # NFE requires ncm/cfop on every item, plus the buyer's full recipient_address:
89
+ client.issue(
90
+ document_type=DocumentType.NFE,
91
+ client_name="Buyer Company Ltd",
92
+ tax_id="11111111111111",
93
+ items=[Product(description="Test product", amount=100.00, ncm="84713012", cfop="5102")],
94
+ recipient_address=Address(
95
+ street="Avenida Atlantica",
96
+ number="500",
97
+ neighborhood="Copacabana",
98
+ city="Rio de Janeiro",
99
+ state="RJ",
100
+ zip_code="22010000",
101
+ city_code="3304557",
102
+ ),
103
+ )
104
+ ```
105
+
106
+ `recipient_address` is an `Address` — the buyer's address, **required for NFE** and ignored for NFSE. Every field is required, `city_code` (the 7-digit IBGE municipality code) included: it becomes `enderDest` on the wire and the SEFAZ rejects a partial one. `state` is also what resolves `idDest` — a buyer in another state is emitted as an interstate operation automatically. A missing or incomplete address raises a `ValueError` locally, before the request goes out.
107
+
108
+ `items` is a list of `Product` (`stackin.br`) — `description`/`amount` apply to any document type; `ncm`/`cfop` (plus everything else on `Product`: `cest`, tax groups, presumed credits...) are Brazil-specific and required per item for NFE, ignored for NFSE (a service isn't a physical good).
109
+
110
+ ## Retrying safely
111
+
112
+ Issuing is the one call you must not repeat blindly. If the response is lost — a
113
+ timeout, a dropped connection — the document may well have been authorized, and a
114
+ second attempt issues a **second** fiscal document: another credit, another number
115
+ burned, and undoing it means cancelling, which has a deadline.
116
+
117
+ Pass `idempotency_key` to make the retry safe:
118
+
119
+ ```python
120
+ key = str(uuid.uuid4())
121
+
122
+ result = invoice.issue(
123
+ document_type=DocumentType.NFSE,
124
+ client_name="Maria Silva",
125
+ tax_id="12345678909",
126
+ items=[Product(description="Consultoria", amount=1500.00)],
127
+ idempotency_key=key,
128
+ )
129
+ ```
130
+
131
+ Retry with the **same key and the same body** and you get the first response back,
132
+ replayed — no second document, no credit consumed. `reissue()` takes the same
133
+ argument.
134
+
135
+ | Situation | What the API does |
136
+ |---|---|
137
+ | New key | issues normally, records the response |
138
+ | Same key, same body | replays the recorded response |
139
+ | Same key, different body | `APIError` 422 |
140
+ | Same key, first call still running | `APIError` 409 |
141
+ | Previous attempt failed | key is released — the retry issues |
142
+ | Key older than 24 hours | treated as new |
143
+
144
+ Generate the key yourself and keep it for as long as you might retry — a `uuid4` per
145
+ business event, not per HTTP call. The SDK never generates one, because a key minted
146
+ per call would protect nothing, and because two genuinely separate invoices for the
147
+ same customer and amount on the same day are a normal thing to issue.
148
+
149
+ ## Correcting a document
150
+
151
+ Some mistakes don't need a cancellation. A wrong product name, wrong
152
+ transport details, a typo in the extra information — a **CC-e** (carta de
153
+ correção) fixes those, and it is free: no new credit, no burned series
154
+ number, no reissue.
155
+
156
+ ```python
157
+ result = invoice.correct(
158
+ "35240912345678000199550010000000011000000017",
159
+ document_type=DocumentType.NFE,
160
+ correction="Transportadora corrigida para Rapido Ltda",
161
+ )
162
+ ```
163
+
164
+ The correction text is 15 to 1000 characters, checked locally before the call.
165
+
166
+ What a CC-e **cannot** fix: anything that changes the tax owed (base, rate,
167
+ price, quantity, totals), the buyer or the seller, or the issue date. Those
168
+ still mean cancelling and reissuing. The API sends the legally fixed wording
169
+ that says exactly this, attached to every correction.
170
+
171
+ The original document does not change — the CC-e is an event attached to it, and
172
+ the authorized XML stays as it was. A document accepts at most 20 of them, and
173
+ they are numbered for you.
174
+
175
+ **NF-e only.** NFS-e has no correction letter, and asking for one returns
176
+ a `409`.
177
+
178
+ ## Invalidating unused numbers
179
+
180
+ NF-e numbering is sequential and the SEFAZ expects it to have no gaps. A number
181
+ gets reserved the moment issuing starts, so a submission that fails afterwards —
182
+ a rejection, a timeout — leaves a hole in the series. Reporting that range is how
183
+ you close it.
184
+
185
+ ```python
186
+ result = invoice.invalidate(
187
+ series="1",
188
+ number_start=10,
189
+ number_end=12,
190
+ reason="Numeracao reservada e nao utilizada por falha no ERP",
191
+ )
192
+ ```
193
+
194
+ The reason is 15 to 255 characters and the range is inclusive; both are checked
195
+ locally, as is `number_end` not being below `number_start`.
196
+
197
+ A number that already reached the authorizer can't be invalidated. The API checks
198
+ its own records first and answers `409` naming the offending numbers, without a
199
+ round trip — and the authorizer checks again for what we can't see from here.
200
+
201
+ **NF-e only**, and it takes no access key: there is no document to point at.
202
+
203
+ ## Errors
204
+
205
+ - `stackin.APIError` — the API responded with a non-2xx status (`status_code`, `detail`) — a 401 here means `api_key` is missing, wrong, or was rotated.
206
+ - `stackin.ConnectionFailedError` — the API didn't respond (network/DNS/timeout).
207
+ - `ValueError` — `issue()`'s `items` is empty, missing `ncm`/`cfop` on an item for NFE, or a missing/incomplete `recipient_address` on NFE.
208
+
209
+ Building the full fiscal document (issuer data, service code, tax groups, schema-accurate XML) is the API's job — configured once per company, not passed on every call.
210
+
211
+ ## Examples
212
+
213
+ Runnable end-to-end scripts in [`examples/nfe/`](examples/nfe/) and [`examples/nfse/`](examples/nfse/) — one file per field/variant, from the bare minimum to every field filled. `examples/consult_invoice.py`, `examples/cancel_invoice.py`, and `examples/reissue_invoice.py` cover the operations that act on an already-issued document.
214
+
@@ -1,13 +1,13 @@
1
- stackin/__init__.py,sha256=zY-ivw5Pckpw-tXwUmn4hwJzU4TZ6MJrW8RLEpPDG_c,495
1
+ stackin/__init__.py,sha256=rsEjE3iiFCRB4EySvHJiK8B-OTrtB35teC6LHX9uo-Y,495
2
2
  stackin/br/__init__.py,sha256=Nw5VyeoFFJPqweZI60uADtBrxilXqkpzpkYtR5WZ630,656
3
3
  stackin/br/product.py,sha256=nuHQkWMz0eRmWYWPoduwmAfibBQvrcajEMxerwHGa9E,4004
4
4
  stackin/br/tax.py,sha256=PdUkg5JdNKy7AHlt5ZpM9svvilZA93FPMr2QNi0SRlw,7716
5
5
  stackin/core/__init__.py,sha256=dxtOP1DopMSNFYGh3Hf12RVcs58R9xwLn-gqfKFo-HE,19
6
6
  stackin/core/address.py,sha256=-w5R3nzfU5T3VoxIeygu5Dq4DRb_lJHAGc0lmSMwEMU,616
7
- stackin/core/client.py,sha256=abXZ71nz-skXM2afUOFNwY1j0_zlJRssihOQT4hwrXg,4591
7
+ stackin/core/client.py,sha256=u8Q14sF844Pd0oMWy8AA0F4ry78p_s17mYprE2E8NNY,8486
8
8
  stackin/core/exceptions.py,sha256=enl8GS_WlDV6PyH1M_KTxzLG9Qdr-9MJrMXB_AlCtYg,521
9
9
  stackin/core/types.py,sha256=SYpLS2EnqKcudm7wFwCTOVAQe1IYT5T0TGSD0E6z8bo,416
10
- stackin_python_sdk-0.1.2.dist-info/METADATA,sha256=PRoF8vxBg3HZFqW3exHsmqeYT6JNLUuTnElpnyUJhwQ,5200
11
- stackin_python_sdk-0.1.2.dist-info/WHEEL,sha256=eY7nduwzv-ldUxpzbRlxwvC693Hg6PX8bWDjEHjZ_dk,88
12
- stackin_python_sdk-0.1.2.dist-info/licenses/LICENSE,sha256=D-XHDcKFFhzEo9C5Dp4QPvt00pb_u__hi4Bc08zpxyU,1072
13
- stackin_python_sdk-0.1.2.dist-info/RECORD,,
10
+ stackin_python_sdk-0.3.0.dist-info/METADATA,sha256=pvDErFOVR0AphHcoXLuIU-cYwsHSgWvgEZLwwbctohI,9628
11
+ stackin_python_sdk-0.3.0.dist-info/WHEEL,sha256=eY7nduwzv-ldUxpzbRlxwvC693Hg6PX8bWDjEHjZ_dk,88
12
+ stackin_python_sdk-0.3.0.dist-info/licenses/LICENSE,sha256=D-XHDcKFFhzEo9C5Dp4QPvt00pb_u__hi4Bc08zpxyU,1072
13
+ stackin_python_sdk-0.3.0.dist-info/RECORD,,
@@ -1,108 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: stackin-python-sdk
3
- Version: 0.1.2
4
- Summary: Python SDK for issuing, consulting and cancelling electronic invoices.
5
- License-Expression: MIT
6
- License-File: LICENSE
7
- Keywords: nfe,nfse,nota-fiscal,nota-fiscal-eletronica,invoice,fiscal-document,brazil,sefaz,e-invoicing
8
- Author: Fernando Celmer
9
- Author-email: email@fernandocelmer.com
10
- Requires-Python: >=3.10
11
- Classifier: Development Status :: 3 - Alpha
12
- Classifier: Intended Audience :: Developers
13
- Classifier: Programming Language :: Python :: 3
14
- Classifier: Programming Language :: Python :: 3.10
15
- Classifier: Programming Language :: Python :: 3.11
16
- Classifier: Programming Language :: Python :: 3.12
17
- Classifier: Programming Language :: Python :: 3.13
18
- Classifier: Topic :: Office/Business :: Financial :: Accounting
19
- Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
- Classifier: Natural Language :: Portuguese (Brazilian)
21
- Requires-Dist: pydantic
22
- Requires-Dist: requests
23
- Project-URL: Changelog, https://github.com/stackin-io/stackin-python-sdk/releases
24
- Project-URL: Documentation, https://docs.stackin.io/blog/python-sdk
25
- Project-URL: Homepage, https://docs.stackin.io
26
- Project-URL: Issues, https://github.com/stackin-io/stackin-python-sdk/issues
27
- Project-URL: Repository, https://github.com/stackin-io/stackin-python-sdk
28
- Description-Content-Type: text/markdown
29
-
30
- <div align="center">
31
-
32
- <img src="https://raw.githubusercontent.com/stackin-io/stackin-python-sdk/master/docs/assets/stackin.png" width="120" />
33
-
34
- **Integrate once. Issue everywhere.**
35
-
36
- [![Python](https://img.shields.io/badge/python-3.10%2B-blue?style=flat-square)](pyproject.toml)
37
- [![PyPI](https://img.shields.io/pypi/v/stackin-python-sdk?style=flat-square)](https://pypi.org/project/stackin-python-sdk/)
38
- [![License](https://img.shields.io/badge/license-MIT-informational?style=flat-square)](https://github.com/stackin-io/stackin-python-sdk)
39
-
40
- [API Reference](https://docs.stackin.io) · [Python SDK guide](https://docs.stackin.io/blog/python-sdk)
41
-
42
- </div>
43
-
44
- ---
45
-
46
- # stackin
47
-
48
- Python SDK for fiscal document issuance — a handful of business fields, nothing about certificates, XML, XSD, signing or SOAP. The API resolves all of that from the issuer's own configuration, identified by `api_key`.
49
-
50
- **One class, `Invoice`** — `issue()`/`consult()`/`cancel()`, nothing else to instantiate. Each line item is a `Product` (`stackin.br`) — `description`/`amount` are universal, everything else (`ncm`/`cfop`/`cest`/tax groups...) is Brazil-specific and only required for NFE; NFSE ignores it.
51
-
52
- ## Install
53
-
54
- ```bash
55
- pip install stackin-python-sdk
56
- ```
57
-
58
- ## Usage
59
-
60
- Get an `api_key` from the [stackin dashboard](https://app.stackin.io) — select the issuing company, then Settings → API key (context `sdk`). One key per issuing company, shown once at creation. The API resolves the issuer (CNPJ, state, address, certificate, environment) entirely from it; nothing about the issuer is ever passed on a call.
61
-
62
- ```python
63
- from stackin import Invoice, DocumentType, Address
64
- from stackin.br import Product # Brazil-specific line item — NCM/CFOP
65
-
66
- client = Invoice(api_key="COMPANY_API_KEY") # defaults to https://sdk.stackin.io
67
-
68
- invoice = client.issue(
69
- document_type=DocumentType.NFSE,
70
- client_name="John Doe",
71
- tax_id="00000000000",
72
- items=[Product(description="Software development", amount=5000.00)],
73
- )
74
-
75
- status = client.consult("ACCESS_KEY...", document_type=DocumentType.NFSE)
76
- client.cancel(
77
- "ACCESS_KEY...",
78
- document_type=DocumentType.NFSE,
79
- reason="Typo",
80
- )
81
-
82
- # NFE requires ncm/cfop on every item, and optionally recipient_address.state
83
- # to get idDest right on interstate sales:
84
- client.issue(
85
- document_type=DocumentType.NFE,
86
- client_name="Buyer Company Ltd",
87
- tax_id="11111111111111",
88
- items=[Product(description="Test product", amount=100.00, ncm="84713012", cfop="5102")],
89
- recipient_address=Address(state="RJ"),
90
- )
91
- ```
92
-
93
- `recipient_address` is an `Address`, but despite the name only `.state` is read — the rest of the fields aren't sent anywhere yet. It's the actual customer's state, used only to set `idDest` (interstate vs internal) on NFE — optional, omitting it always produces `idDest=1` (internal).
94
-
95
- `items` is a list of `Product` (`stackin.br`) — `description`/`amount` apply to any document type; `ncm`/`cfop` (plus everything else on `Product`: `cest`, tax groups, presumed credits...) are Brazil-specific and required per item for NFE, ignored for NFSE (a service isn't a physical good).
96
-
97
- ## Errors
98
-
99
- - `stackin.APIError` — the API responded with a non-2xx status (`status_code`, `detail`) — a 401 here means `api_key` is missing, wrong, or was rotated.
100
- - `stackin.ConnectionFailedError` — the API didn't respond (network/DNS/timeout).
101
- - `ValueError` — `issue()`'s `items` is empty, or missing `ncm`/`cfop` on an item for NFE.
102
-
103
- Building the full fiscal document (issuer data, service code, tax groups, schema-accurate XML) is the API's job — configured once per company, not passed on every call.
104
-
105
- ## Examples
106
-
107
- Runnable end-to-end scripts in [`examples/nfe/`](examples/nfe/) and [`examples/nfse/`](examples/nfse/) — one file per field/variant, from the bare minimum to every field filled.
108
-