npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@stackin-io/stackin-node-sdk

v0.11.0

Published

Node/TypeScript SDK for stackin — issue, consult and cancel Brazilian fiscal documents (NF-e, NFS-e) via a single Invoice client. No certificates, XML or SOAP.

Readme

Integrate once. Issue everywhere.

Node npm License

API Reference · Node SDK guide


stackin

Node/TypeScript 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 apiKey.

One class, Invoiceissue()/consult()/cancel()/reissue()/correct()/invalidate()/pdf()/received()/manifest(), nothing else to instantiate. Each line item is a br.Productdescription/amount are universal, everything else (ncm/cfop/cest/tax groups...) is Brazil-specific and only required for NFE; NFSE ignores it.

What a line item is worth

unitPrice is the price of one unit. amount is the gross total of the line's products, before discount, freight, insurance and other expenses. Send either; sending both asserts that they agree.

// More than one unit — the note's line is 2 x 120.00 = 240.00
new br.Product({ description: "Teclado", quantity: 2, unitPrice: 120.0, unit: "UN" });

// Legacy: amount alone still means the line's gross total
new br.Product({ description: "Servico", quantity: 3, amount: 150.0 });

Amounts that do not add up are refused before the authorizer sees them, with a 422 naming the line and both numbers (ITEM_TOTAL_MISMATCH).

Migrating

Before:  quantity: 3, amount: 150.0
After:   quantity: 3, unitPrice: 50.0

Nothing has to migrate. amount keeps the meaning it always had and is not deprecated in this release.

Install

npm install @stackin-io/stackin-node-sdk

Usage

Get an apiKey from the stackin dashboard — 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. Defaults to https://sdk.stackin.io.

import {
  Invoice,
  DocumentType,
  Address,
  br,
} from "@stackin-io/stackin-node-sdk";

const client = new Invoice({ apiKey: "COMPANY_API_KEY" });

const invoice = await client.issue({
  documentType: DocumentType.NFSE,
  clientName: "John Doe",
  taxId: "00000000000",
  items: [new br.Product({ description: "Software development", unitPrice: 5000.0 })],
});

const status = await client.consult("ACCESS_KEY...", {
  documentType: DocumentType.NFSE,
});

await client.cancel("ACCESS_KEY...", {
  documentType: DocumentType.NFSE,
  reason: "Typo",
});

// The authorizer's PDF, as bytes. NFS-e only; the XML stays the legally valid
// document, and a 502 here means the authorizer is down.
const document = await client.pdf("ACCESS_KEY...", {
  documentType: DocumentType.NFSE,
});
await writeFile("nota.pdf", document);

// Retries a submission that never reached the authorizer, or was rejected by
// it. Takes the invoice's local id — not an access key, since a failed
// submission never got one. Consumes quota exactly like a fresh issue().
await client.reissue("9f2c1e3a-4b5d-6e7f-8a9b-0c1d2e3f4a5b");

// NFE requires ncm/cfop on every item, plus the buyer's full recipientAddress:
await client.issue({
  documentType: DocumentType.NFE,
  clientName: "Buyer Company Ltd",
  taxId: "11111111111111",
  items: [
    new br.Product({
      description: "Test product",
      unitPrice: 100.0,
      ncm: "84713012",
      cfop: "5102",
    }),
  ],
  recipientAddress: new Address({
    street: "Avenida Atlantica",
    number: "500",
    neighborhood: "Copacabana",
    city: "Rio de Janeiro",
    state: "RJ",
    zipCode: "22010000",
    cityCode: "3304557",
  }),
});

recipientAddress is an Address — the buyer's address, required for NFE and ignored for NFSE. Every field is required, cityCode (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 ValidationError locally, before the request goes out.

items is an array of br.Productdescription/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).

Retrying safely

Issuing is the one call you must not repeat blindly. If the response is lost — a timeout, a dropped connection — the document may well have been authorized, and a second attempt issues a second fiscal document: another credit, another number burned, and undoing it means cancelling, which has a deadline.

Pass an idempotency key to make the retry safe:

const key = randomUUID();

const result = await invoice.issue({
  documentType: DocumentType.NFSE,
  clientName: "Maria Silva",
  taxId: "12345678909",
  items: [new br.Product({ description: "Consultoria", unitPrice: 1500.0 })],
  idempotencyKey: key,
});

Retry with the same key and the same body and you get the first response back, replayed — no second document, no credit consumed. Reissue takes the same argument.

| Situation | What the API does | |---|---| | New key | issues normally, records the response | | Same key, same body | replays the recorded response | | Same key, different body | API error 422 | | Same key, first call still running | API error 409 | | Previous attempt failed | key is released — the retry issues | | Key older than 24 hours | treated as new |

Generate the key yourself and keep it for as long as you might retry — one UUID per business event, not per HTTP call. The SDK never generates one, because a key minted per call would protect nothing, and because two genuinely separate invoices for the same customer and amount on the same day are a normal thing to issue.

Correcting a document

Some mistakes don't need a cancellation. A wrong product name, wrong transport details, a typo in the extra information — a CC-e (carta de correção) fixes those, and it is free: no new credit, no burned series number, no reissue.

const result = await invoice.correct(
  "35240912345678000199550010000000011000000017",
  {
    documentType: DocumentType.NFE,
    correction: "Transportadora corrigida para Rapido Ltda",
  }
);

The correction text is 15 to 1000 characters, checked locally before the call.

What a CC-e cannot fix: anything that changes the tax owed (base, rate, price, quantity, totals), the buyer or the seller, or the issue date. Those still mean cancelling and reissuing. The API sends the legally fixed wording that says exactly this, attached to every correction.

The original document does not change — the CC-e is an event attached to it, and the authorized XML stays as it was. A document accepts at most 20 of them, and they are numbered for you.

NF-e only. NFS-e has no correction letter, and asking for one returns a 409.

Invalidating unused numbers

NF-e numbering is sequential and the SEFAZ expects it to have no gaps. A number gets reserved the moment issuing starts, so a submission that fails afterwards — a rejection, a timeout — leaves a hole in the series. Reporting that range is how you close it.

const result = await invoice.invalidate({
  series: "1",
  numberStart: 10,
  numberEnd: 12,
  reason: "Numeracao reservada e nao utilizada por falha no ERP",
});

The reason is 15 to 255 characters and the range is inclusive; both are checked locally, as is number_end not being below number_start.

A number that already reached the authorizer can't be invalidated. The API checks its own records first and answers 409 naming the offending numbers, without a round trip — and the authorizer checks again for what we can't see from here.

NF-e only, and it takes no access key: there is no document to point at.

Documents issued against you

Everything above serves the issuer. These two serve the recipient: what suppliers billed to this CNPJ, and the formal answer to it.

Reading the list never calls the SEFAZ. The authorizer caps how many times a CNPJ may ask for its distribution per day, so collecting runs on a schedule on the API side and a page refresh cannot spend that allowance.

import { Manifestation } from "@stackin-io/stackin-node-sdk";

const page = await client.received({ limit: 20 });
console.log(page.total);

await client.manifest(accessKey, { manifestation: Manifestation.CIENCIA });
await client.manifest(accessKey, {
  manifestation: Manifestation.OPERACAO_NAO_REALIZADA,
  reason: "Mercadoria nunca chegou ao endereco",
});

Before you answer a document the SEFAZ sends only a summary (resNFe): access key, issuer, amount, date. The full document (nfeProc) arrives after a manifestation, and the schema field on each row says which one you hold.

The four answers are 210200 Confirmação da Operação, 210210 Ciência da Operação, 210220 Desconhecimento da Operação and 210240 Operação não Realizada. Only the last one takes a reason, and it requires one — both rules are checked locally, before the request goes out, because a round trip to be told a fixed rule is a round trip wasted.

Errors

  • APIError — the API responded with a non-2xx status (statusCode, detail) — a 401 here means apiKey is missing, wrong, or was rotated.
  • ConnectionFailedError — the API didn't respond (network/DNS/timeout).
  • ValidationErrorissue()'s items is empty, missing ncm/cfop on an item for NFE, or a missing/incomplete recipientAddress on NFE.

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.

Examples

Runnable end-to-end scripts in examples/nfe/ and examples/nfse/ — one file per field variant, from the bare minimum to every field filled. examples/consult_invoice.ts, examples/cancel_invoice.ts, and examples/reissue_invoice.ts cover the operations that act on an already-issued document.

Commit convention lives in CONTRIBUTING.md, not here.