EntrarComeçar
SDKs

SDKs

  • Uma classe, seis métodos

    Um client Invoice com issue(), consult(), cancel(), reissue(), correct() e invalidate() — nada mais pra instanciar.

  • Modelos tipados

    Cada item é um Product tipado, com campos fiscais do Brasil validados no cliente antes da requisição sair.

  • Mesmo contrato em toda linguagem

    Python, Go, Node e PHP seguem o mesmo contrato de API, verificado no CI a cada mudança — métodos, erros e recursos se comportam igual, então trocar de linguagem não significa reaprender a stackin.

Começando agora?

Crie uma conta gratuita, gere uma SDK key e emita seu primeiro documento em homologation — é grátis e ilimitado, sem cartão.

Instalação
Copy
pip install stackin-python-sdk

Uma dependência, sem SDK de terceiros por baixo.

Autenticação

O SDK autentica com uma SDK key — passe uma vez ao criar o client, a biblioteca cuida do resto. Você nunca monta URL nem define header manualmente.

Conseguindo uma SDK key

  1. 1
    Cadastre-se e faça login em .
  2. 2
    Crie uma empresa — obrigatório antes da key.
  3. 3
    No dashboard, abra a empresa e vá em Empresa → API keys, deixando o contexto em sdk. Nenhum certificado é necessário nesta etapa — a key pode ser criada e usada na hora; o certificado só é exigido depois, na emissão de fato.
  4. 4
    Nomeie a key e escolha o ambiente — homologation ou production. Os dois são obrigatórios na criação, e o ambiente é fixo pelo resto da vida da key.
  5. 5
    Copie a key na hora — ela aparece , na criação, e não pode ser recuperada depois. Perdeu, revoga e cria outra.
Copy
from stackin import Invoice

client = Invoice(api_key="$STACKIN_API_KEY")

Uma empresa mantém até 3 keys ao mesmo tempo, em qualquer combinação de contexto (sdk/api) e ambiente (homologation/production) — revogar uma libera a vaga.

Token ausente ou inválido retorna 401 — veja abaixo.

Ambientes

Toda SDK key é criada para exatamente um ambiente — ou or — escolhido ao gerar no dashboard, fixo pelo resto da vida da key. Não existe campo de ambiente nas chamadas do SDK: a key usada decide pra onde vai o documento.

HomologationAmbiente de teste

Grátis e ilimitado — é o ambiente de teste real do SEFAZ/ADN, não é mock — então integre e teste tudo antes de trocar pra production.

ProductionAmbiente real

Para documentos reais. Garanta que tudo funciona em homologation antes de trocar.

Uma empresa pode ter keys dos dois ambientes ao mesmo tempo (até 3 no total).

Idempotência

Emitir é a única chamada que você não deve repetir às cegas. Se a resposta se perde — timeout, conexão caída — o documento pode muito bem ter sido autorizado, e uma segunda tentativa emite uma segunda nota fiscal: outro crédito, outro número da série queimado, e desfazer exige cancelamento, que tem prazo.

Passe idempotency_key (o nome segue a convenção de cada linguagem) para tornar a repetição segura. Repetindo com a mesma chave e o mesmo corpo, você recebe de volta a primeira resposta — sem segunda nota, sem consumir crédito. Disponível em issue e reissue.

Gere a chave você mesmo — um UUID por evento de negócio, não por requisição HTTP — e guarde enquanto ainda puder tentar de novo. As chaves valem 24 horas; depois disso a mesma chave é tratada como nova.

Copy
result = client.issue(
    document_type=DocumentType.NFSE,
    client_name="Maria Silva",
    tax_id="12345678909",
    items=[Product(description="Consultoria", unit_price=Decimal("1500.00"))],
    idempotency_key=key,
)
idempotency_key
string | optionalsua chave, no máximo 255 caracteres. O SDK só a repassa no header — nunca gera nem guarda nada.
replay
same key, same bodymesma chave, mesmo corpo: devolve a resposta gravada, sem reprocessar.
APIError 422
same key, other bodymesma chave, corpo diferente. Reusar uma chave em outra requisição é erro do cliente, não repetição.
APIError 409
still runninga primeira chamada ainda está rodando. Consulte antes de tentar de novo.

Limites de uso

Os limites são por chave de API. Cada empresa tem a sua própria contagem — o volume de um cliente nunca gasta a cota de outro.

Emitir tem limite mais apertado que consultar, e consultar mais apertado que listar o histórico. Quanto mais uma operação depende da SEFAZ, menos chamadas por minuto ela aceita.

Ao estourar, o SDK levanta APIError com status 429. Ele não expõe os headers da resposta, então espere alguns segundos antes de tentar de novo — e reutilize a mesma chave de idempotência, para a repetição não gerar um segundo documento.

Copy
try:
    result = client.issue(...)
except APIError as error:
    if error.status_code == 429:
        # Wait before calling again — reuse the same idempotency_key.
        raise
issue / reissue
60 / mingera documento e consome crédito.
consult / cancel
120 / minfala com a SEFAZ, mas não gera documento.
APIError 429
over the limitespere e repita com a mesma chave de idempotência.

Erros

Os SDKs traduzem cada falha em um tipo distinto: APIError pro que a API respondeu (com status e detail), ConnectionFailedError pra falha de rede antes de qualquer resposta, e o erro de validação da própria linguagem pro que é pego no cliente, antes da requisição sair.

Copy
from stackin import APIError, ConnectionFailedError

try:
    result = client.issue(...)
except ValueError as error:
    # Caught before the request left — empty items, missing ncm/cfop,
    # incomplete recipient_address on an NFE.
    print(f"Invalid request: {error}")
except APIError as error:
    # 4xx/5xx from the API. 502 carries the authorizer's own message.
    print(f"[{error.status_code}] {error.detail}")
except ConnectionFailedError as error:
    print(f"Could not reach the API: {error}")
HTTPErroQuando
400Configuração inválidaA configuração do emissor (certificado, endereço, campos fiscais) está ausente ou inválida.
401Não autorizadoO Bearer token está ausente, expirado, inválido ou não foi criado para o host que você está chamando (api.stackin.io vs sdk.stackin.io).
402Limite atingidoA cota do plano da empresa para notas de production foi atingida e o plano não tem preço de excedente (trial).
404Não encontradoNenhum documento com essa chave de acesso ou invoice_id pertence à empresa da sua key.
409Conflito de operaçãoA operação pedida não se aplica a esse document_type (ex.: operação exclusiva de nfse numa nfe).
422Erro de validaçãoO corpo da requisição falhou na validação — campo obrigatório faltando ou malformado.
429Limite de requisiçõesChamadas demais para essa key na janela atual. O header Retry-After diz quantos segundos esperar.
501Não implementadoAssinatura para esse tipo de documento/país ainda não implementada no servidor.
502Autorizador rejeitouRejeição do autorizadorO autorizador (SEFAZ/UF ou ADN) rejeitou a requisição. detail.message traz o erro do próprio autorizador, e detail.invoice_id (só na emissão) permite consultar a tentativa rejeitada.

Rejeições do autorizador

Quando a SEFAZ ou ADN rejeita uma operação, a resposta inclui informações adicionais.

O detail do APIError carrega esses campos quando o autorizador rejeita — use detail.invoice_id pra consultar a tentativa depois.

Copy
{
  "detail": {
    "message": "IE do emitente inválida",
    "invoice_id": "inv_8f3c9b2e6a7d"
  }
}
detail.message
Mensagem retornada pelo autorizador.
detail.invoice_id
Identifica a tentativa de emissão.

Valores do item

unit_price é o preço de uma unidade. amount é o valor bruto total dos produtos da linha, antes de desconto, frete, seguro e outras despesas.

Envie um dos dois. Enviar os dois é afirmar que fecham: a API compara quantity x unit_price com amount e recusa o documento antes da transmissão se divergirem. No SDK os decimais viajam como texto, então um preço com 10 casas chega à API exatamente como foi escrito.

Também são recusados: nenhum dos dois campos (ITEM_AMOUNT_MISSING), valores menores ou iguais a zero, desconto maior que o valor bruto (ITEM_DISCOUNT_EXCEEDS_GROSS) e precisão que o XML não carrega (ITEM_PRECISION_UNSUPPORTED). Nada precisa migrar: amount mantém o significado que sempre teve.

Copy
from decimal import Decimal

# unit_price: what one unit costs — the line is 2 x 120.00 = 240.00
Product(description="Teclado", quantity=Decimal("2"), unit_price=Decimal("120.00"))

# amount: the line's gross total (the original contract)
Product(description="Teclado", quantity=Decimal("3"), amount=Decimal("50.00"))

# both, checked against each other before transmission
Product(
    description="Teclado",
    quantity=Decimal("2"),
    unit_price=Decimal("120.00"),
    amount=Decimal("240.00"),
)

IBS/CBS — Reforma Tributária

O grupo da Reforma Tributária, por item. Obrigatório a partir de 2026 para o regime normal. cst e classification (cClassTrib) são decisão fiscal sua; a API deriva os valores das alíquotas.

As três alíquotas são percentuais: rate_state (pIBSUF), rate_city (pIBSMun) e rate_federal (pCBS). base é opcional — sem ela, a base é o valor do item. Item sem o grupo não emite nada, que é como a NF-e era antes da reforma.

Copy
from stackin.br import IbsCbs, Product

Product(
    description="Teclado",
    quantity=Decimal("2"),
    unit_price=Decimal("120.00"),
    ncm="84716052",
    cfop="5102",
    ibs_cbs=IbsCbs(
        cst="000",
        classification="000001",
        rate_state=0.1,
        rate_city=0.0,
        rate_federal=0.9,
    ),
)

Grupos de imposto do item

ICMS, IPI, PIS e COFINS por item, através de Tax. Cada grupo tem sua classe por CST/CSOSN — IcmsSn102 para Simples Nacional sem crédito, PisNt e CofinsNt para não tributado, e assim por diante.

A stackin não infere tributação: qual grupo se aplica é decisão fiscal do emitente, e errar aqui gera imposto errado, não erro de schema. Sem tax, nenhum grupo de imposto vai no XML.

Copy
from stackin.br import CofinsNt, IcmsSn102, PisNt, Product, Tax

Product(
    description="Teclado",
    quantity=Decimal("2"),
    unit_price=Decimal("120.00"),
    ncm="84716052",
    cfop="5102",
    tax=Tax(icms=IcmsSn102(csosn="102"), pis=PisNt(cst="07"), cofins=CofinsNt(cst="07")),
)
Endpoints

Notas fiscais

Emitir nota

Emite um novo documento. items[].product.br (ncm, cfop) é obrigatório para , ignorado em , ignored for — serviço não tem código de classificação fiscal, então nfse só lê description e amount de cada item.

A mesma chamada funciona para nfse — basta trocar document_type.

Copy
from decimal import Decimal

from stackin import Address, DocumentType, Invoice
from stackin.br import Product

client = Invoice(api_key="$STACKIN_API_KEY")

result = client.issue(
    document_type=DocumentType.NFE,
    client_name="Buyer Company Ltd",
    tax_id="11222333000181",
    items=[
        Product(
            description="Rosa Holambra Vermelha",
            unit_price=Decimal("112.44"),
            ncm="06031100",
            cfop="5102",
        )
    ],
    recipient_address=Address(
        street="Rua das Flores",
        number="1200",
        neighborhood="Centro",
        city="Joinville",
        state="SC",
        zip_code="89201100",
        city_code="4209102",
    ),
)

Consultar nota

Consulta o status atual de um documento direto no autorizador pela access key. Requer como query param.

Copy
result = client.consult(
    document_type=DocumentType.NFE,
    access_key="42260831112223330001815500000012341123456789",
)

Cancelar nota

Cancela um documento previamente autorizado. precisa ter pelo menos 15 caracteres para nfe (o campo xJust do autorizador exige). Cancelar não devolve cota do plano — a capacidade do autorizador já foi gasta na emissão.

Copy
result = client.cancel(
    document_type=DocumentType.NFE,
    access_key="42260831112223330001815500000012341123456789",
    reason="Duplicate order, cancelled by the buyer within 24h",
)

Reemitir nota

Reenvia uma emissão que falhou, pelo `id` local do documento (não pela chave de acesso — uma nota rejeitada não tem uma). Útil pra retentar depois de corrigir um problema de configuração ou uma indisponibilidade do autorizador. Consome crédito como uma emissão nova, já que é uma nova transmissão.

Copy
result = client.reissue(
    invoice_id="9f2c1e3a-4b5d-6e7f-8a9b-0c1d2e3f4a5b",
)

Corrigir nota

Corrige uma nota já autorizada com uma carta de correção, sem cancelar nem reemitir — não consome crédito nem queima número da série. Serve para erro que não muda o imposto: nome de produto, dados de transporte, informações complementares.

O texto vai de 15 a 1000 caracteres, conferido pelo SDK antes da chamada. Uma CC-e não corrige nada que mude o valor do imposto, o emitente, o destinatário ou a data de emissão — nesses casos ainda é cancelar e reemitir. A nota original não muda: a correção fica anexada a ela. São no máximo 20 por nota, numeradas automaticamente. Só NF-e; para NFS-e vem APIError 409.

Copy
result = client.correct(
    "35240912345678000199550010000000011000000017",
    document_type=DocumentType.NFE,
    correction="Transportadora corrigida para Rapido Ltda",
)

Inutilizar faixa

A numeração da NF-e é sequencial e a SEFAZ cobra que não tenha buraco. O número é reservado quando a emissão começa, então uma emissão que falha depois disso — rejeição, timeout — deixa um vão na série. Inutilizar a faixa é como se fecha esse vão.

O motivo vai de 15 a 255 caracteres e a faixa é inclusiva — o SDK confere os dois, e que o fim não seja menor que o início, antes da chamada. Número que já chegou na SEFAZ não pode ser inutilizado: vem APIError 409 dizendo quais são. Só NF-e, e sem chave de acesso: não há documento para apontar.

Copy
result = client.invalidate(
    series="1",
    number_start=10,
    number_end=12,
    reason="Numeracao reservada e nao utilizada por falha no ERP",
)

PDF do documento

A representação impressa que a autorizadora gera — DANFE para NF-e, DANFSe para NFS-e. Retorna bytes crus, o único método que não devolve objeto: grave em arquivo ou sirva direto.

O XML continua sendo o documento com valor jurídico; o PDF é conveniência. O endpoint da autorizadora para isso é instável, então um erro 502 aqui significa autorizadora indisponível, não que a nota está errada.

Copy
pdf = client.pdf(access_key, document_type=DocumentType.NFSE)

with open("nota.pdf", "wb") as file:
    file.write(pdf)

Histórico

O que esta empresa emitiu, mais recente primeiro. É a contrapartida de received: aquele lista o que emitiram contra você, este o que você emitiu.

Filtra por document_type e status (issued, rejected, cancelled) e pagina com limit e offset. A resposta traz o envelope de paginação padrão.

Copy
page = client.history(
    document_type=DocumentType.NFE,
    status="issued",
    limit=20,
    offset=0,
)

for row in page["data"]:
    print(row["access_key"], row["client_name"], row["amount"])

Tentativas

Toda tentativa feita para uma nota e o que voltou da autorizadora. consult dá o status; este dá o motivo — o código da rejeição e o XML que foi enviado.

Recebe o invoice_id, não a chave de acesso: um documento rejeitado nunca teve chave. É por aqui que se descobre, por exemplo, que a rejeição foi 972.

Copy
attempts = client.submissions(invoice["id"])

for attempt in attempts:
    print(attempt["status"], attempt["detail"])

Notas recebidas

Documentos que outras empresas emitiram contra o seu CNPJ. Lê o que a API já coletou — não chama a autorizadora na hora.

A coleta roda em agenda do lado da API porque a SEFAZ limita quantas vezes um CNPJ pode consultar por dia. Consultar aqui não gasta essa cota.

Copy
page = client.received(limit=20)

for document in page["data"]:
    print(document["access_key"], document["issuer_name"], document["amount"])

Manifestação

A resposta formal de quem recebeu: confirmação, ciência, desconhecimento ou operação não realizada.

Só operação não realizada aceita justificativa — e exige uma. O SDK verifica isso antes de sair da sua máquina, em vez de gastar uma ida à API para ser informado.

Copy
from stackin import Manifestation

client.manifest(access_key, manifestation=Manifestation.CONFIRMACAO)

client.manifest(
    access_key,
    manifestation=Manifestation.OPERACAO_NAO_REALIZADA,
    reason="Mercadoria nao recebida",
)
Confiável

Mais de 99,9% de uptime

Seguro

Dados criptografados e LGPD ready

Rápido

Infraestrutura escalável e de baixa latência

IA em primeiro lugar

Servidor MCP oficial — emita por agente

© 2026 stackin. Todos os direitos reservados.Feito com código, propósito e café.Política de PrivacidadeTermos de UsoContato