Releases: stackin-io/stackin-go-sdk
Release list
v0.10.0
What's Changed
FiscalReference— the published classification tables, with an accessor per kindTaxpayer— one lookup, by exact tax id- Path segments are escaped — a value the caller types can no longer rewrite the request's URL
Why
An issuer filling a document needs CFOP, NCM, CEST and the rest, and until now the SDK had no way to ask. Both clients are read-only and take the same api key.
ref := stackin.NewFiscalReference(stackin.WithAPIKey("..."))
ref.NCM.Get("84716052")
ref.NCM.Search(stackin.SearchQuery{Term: "teclado", Limit: 5})
ref.Kinds()
stackin.NewTaxpayer(stackin.WithAPIKey("...")).Get("00000000000191")
The eight named accessors are ergonomics. Kind(name) reaches anything else, including a classification published after this release — ask Kinds() rather than trusting the list, because the two answers diverge the moment the source data grows one.
metadata is passed through as the API sends it and differs per kind: utrib on an NCM, ncm_code on a CEST, tax_type on a CST, empty on an ISS service.
Three things worth knowing
These share the invoice read allowance — 600 calls a minute per key, the same bucket Consult, History, Received and Pdf draw from. One page of Search beats N single lookups.
Ordering is fixed — kind, then code, ascending. Unlike History, the search takes no sort or order argument: the route accepts them and discards them.
A 404 from Taxpayer does not mean the company does not exist. That registry reloads monthly from the RFB dump, so a recently registered CNPJ is simply not in it yet. It is not a validation rule.
Fixed
A tax id or code containing a / — which is how a CNPJ is normally written — used to land in the URL raw and address a different path, surfacing as a 404 that read like a registry miss. Segments are now escaped, and the ones that would leave the path are refused before the call.
0.9.0
What's Changed
IbsCbson a product — the Reforma Tributária group, per item
Why
The API has taken ibs_cbs since the tax-reform groups landed, but no SDK exposed it: a caller who needed IBS/CBS had to fall back to ExtraGroups and hand-build the payload. It is required from 2026 for the regime normal, so that gap had a deadline attached.
price := 120.00
br.Product{
Description: "Teclado",
Quantity: 2,
UnitPrice: &price,
NCM: ptr("84716052"),
CFOP: ptr("5102"),
IbsCbs: &br.IbsCbs{
CST: "000",
Classification: "000001",
RateState: 0.1,
RateCity: 0.0,
RateFederal: 0.9,
},
}CST (3 digits) and Classification (cClassTrib, 6 digits) are your fiscal decision; the API derives the amounts from the three rates. Base is a pointer and is omitted when nil — without it the base is the item amount.
An item without the group emits nothing, which is what an NF-e looked like before the reform. IbsCbs is a new optional field, so no existing call site changes.
0.8.0
What's Changed
UnitPriceon a product — the price of one unit, besideAmount, the line's gross total
Why
Amount has always been the line's gross total, but a reader sees {Quantity: 2, Amount: 120.00} and expects R$ 240,00. The API answered R$ 120,00, and the mistake only surfaced on an authorized document. There was no field that meant "what one unit costs", so the API derived it by dividing — in float, at the tenth decimal place the SEFAZ reads.
price := 120.00
br.Product{
Description: "Teclado",
Quantity: 2,
UnitPrice: &price,
Unit: "UN",
NCM: ptr("84716052"),
CFOP: ptr("5102"),
}Send UnitPrice or Amount. Sending both asserts they agree: the API compares quantity x unit_price against amount and refuses the document before transmission if they differ, naming the line and both numbers.
Both now travel as strings on the wire, so a ten-place unit price reaches the API exactly as typed rather than in exponent form. Amount stays a plain float64 and is omitted when it is zero, so no existing call site changes.
Amount keeps the meaning it always had and is not deprecated — nothing has to migrate.
v0.7.0
What's Changed
CanceltakesRequestOptions, so a cancellation can carry an idempotency key
Why
Issue and Reissue already accepted the key; Cancel — the one irreversible call, inside a legal window — did not. A dropped connection or a retry sent a second cancellation.
result, err := invoice.Cancel(
accessKey,
stackin.NFSE,
"Pedido cancelado pelo comprador",
stackin.WithIdempotencyKey("cancel-8f21"),
)The same key with the same body replays the stored answer. A different body under the same key is refused, not silently accepted. Nothing generates a key for you: two calls without one are two cancellations, deliberately.
The variadic parameter is additive — existing three-argument calls still compile.
v0.6.0
What's Changed
submissions— every attempt made for one invoice, and what the authorizer answered to each
submissions — why a document was rejected
consult gives the status. This gives the reason.
rows, err := inv.Submissions("e4a1b2c3-0000-4000-8000-000000000001")
Takes the invoice id, like reissue and unlike everything else: a rejected document has no access key to look it up by.
Each row carries status, status_code (the authorizer's own code — 209, 539, and so on), detail (its message), protocol when one was issued, environment, endpoint, http_status, duration_ms, and the raw_request/raw_response exactly as they went over the wire.
The rows are attempts, not documents: a reissued invoice has more than one, oldest first, and only the last one describes the current status. raw_response keeps the authorizer's own shape and is deliberately not normalized — normalizing would mean guessing, and a wrong guess about a rejection is worse than the raw payload.
Nothing was removed and no signature changed.
Full Changelog: v0.5.0...v0.6.0
v0.5.0
What's Changed
history— lists what this company issued, the counterpart ofreceived- The contract grew from nine methods to ten, in every language at once
history — the company's own issuance list
received lists what other companies issued against this one. This is the other side: what this company issued, newest first.
result, err := inv.History(stackin.HistoryQuery{DocumentType: stackin.NFE, Status: "rejected", Limit: 10})
Returns the usual paginated envelope. Each row carries both identifiers the other methods need: id, which reissue takes, and access_key, which consult, cancel, correct and pdf take. A rejected row has an id and no access key — the authorizer never assigned one.
Filters: document type, status (pending, authorized, rejected, cancelled), plus limit, offset and ordering.
It is named history rather than list because list is a builtin in Python and a language construct in PHP, and the API itself names the operation after the issuance history.
Nothing was removed and no signature changed.
Full Changelog: v0.4.1...v0.5.0
v0.4.1
Green pipeline for the tax module shipped in v0.4.0 — misspell was rejecting substituto, the SEFAZ's own Portuguese field name, once the JSON tag became snake_case. No API change from v0.4.0.
v0.4.0
Every tax group the official NF-e leiaute defines: 21 ICMS groups (was 5), PIS/COFINS by quantity, PISST, COFINSST, and the IPI stamp fields.
The tax payload now goes out in snake_case, matching the rest of the API. The XSD spelling is still accepted server-side, so existing code keeps working.
Generated from leiauteNFe_v4.00.xsd — see specs/tax/ in stackin-sdk-template.
v0.3.1
Standalone examples, every one of them reading STACKIN_API_KEY and failing with log.Fatal. Received(), Manifest() and Pdf() documented in the README. The release now issues a real NFS-e before publishing.
v0.3.0
Adds Pdf(accessKey, documentType), the seventh method on Invoice and the only one returning []byte.
The authorizer's own rendering of an authorized document. The XML remains the legally valid document; this is a convenience, and the authorizer's endpoint for it is unstable by their own documentation — an APIError with status 502 means the authorizer is unavailable, not that the invoice is wrong. NFS-e only; NF-e answers 501.
Internal: the HTTP layer was split into send (returns raw bytes) and request (parses JSON on top), because the previous path decoded JSON off every response and would have corrupted a PDF. Behaviour-neutral for the six existing methods.