Skip to content

Releases: stackin-io/stackin-go-sdk

v0.10.0

Choose a tag to compare

@FernandoCelmer FernandoCelmer released this 10 Sep 23:34
b8802c0

What's Changed

  • FiscalReference — the published classification tables, with an accessor per kind
  • Taxpayer — 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

Choose a tag to compare

@FernandoCelmer FernandoCelmer released this 08 Sep 01:20

What's Changed

  • IbsCbs on 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

Choose a tag to compare

@FernandoCelmer FernandoCelmer released this 07 Sep 23:39

What's Changed

  • UnitPrice on a product — the price of one unit, beside Amount, 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

Choose a tag to compare

@FernandoCelmer FernandoCelmer released this 06 Sep 16:06

What's Changed

  • Cancel takes RequestOptions, 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

Choose a tag to compare

@FernandoCelmer FernandoCelmer released this 06 Sep 07:51

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

Choose a tag to compare

@FernandoCelmer FernandoCelmer released this 06 Sep 07:24

What's Changed

  • history — lists what this company issued, the counterpart of received
  • 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

Choose a tag to compare

@FernandoCelmer FernandoCelmer released this 06 Sep 02:53

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

Choose a tag to compare

@FernandoCelmer FernandoCelmer released this 06 Sep 02:48

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

Choose a tag to compare

@FernandoCelmer FernandoCelmer released this 06 Sep 00:09

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

Choose a tag to compare

@FernandoCelmer FernandoCelmer released this 05 Sep 17:49

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.