SDKs

SDKs

  • Una clase, seis métodos

    Un cliente Invoice con issue(), consult(), cancel(), reissue(), correct() e invalidate() — nada más que instanciar.

  • Modelos tipados

    Cada ítem es un Product tipado, con campos fiscales de Brasil validados en el cliente antes de que salga la solicitud.

  • El mismo contrato en cada lenguaje

    Python, Go, Node y PHP siguen el mismo contrato de API, verificado en CI en cada cambio — métodos, errores y recursos se comportan igual, así que cambiar de lenguaje no significa reaprender stackin.

¿Recién empezando?

Cree una cuenta gratuita, genere una SDK key y emita su primer documento en homologation — gratis e ilimitado, sin tarjeta.

Instalación
Copy
pip install stackin-python-sdk

Una dependencia, sin SDK de terceros debajo.

Autenticación

El SDK autentica con una SDK key — pásela una vez al crear el client, la librería se encarga del resto. Nunca construye una URL ni define un header manualmente.

Cómo obtener una SDK key

  1. 1
    Regístrese e inicie sesión en .
  2. 2
    Cree una empresa — obligatorio antes de la key.
  3. 3
    En el dashboard, abra la empresa y vaya a Empresa → API keys, dejando el contexto en sdk. No hace falta certificado en este paso — la key puede crearse y usarse enseguida; el certificado solo se exige después, en la emisión.
  4. 4
    Nombre la key y elija el entorno — homologation o production. Ambos son obligatorios en la creación, y el entorno es fijo por el resto de la vida de la key.
  5. 5
    Copie la key de inmediato — se muestra , al crearla, y no se puede recuperar después. Si la pierde, revóquela y cree otra.
Copy
from stackin import Invoice

client = Invoice(api_key="$STACKIN_API_KEY")

Una empresa mantiene hasta 3 keys a la vez, en cualquier combinación de contexto (sdk/api) y entorno (homologation/production) — revocar una libera el cupo.

Un token ausente o inválido devuelve 401 — ver abajo.

Entornos

Cada SDK key se crea para exactamente un entorno — o or — elegido al generarla en el dashboard, fijo durante toda la vida de la key. No hay campo de entorno en las llamadas del SDK: la key con la que se autentica decide a dónde va el documento.

HomologationEntorno de prueba

Gratis e ilimitado — es el entorno de prueba real de SEFAZ/ADN, no un mock — así que integra y prueba todo antes de cambiar a production.

ProductionEntorno real

Para documentos reales. Asegúrate de que todo funcione en homologation antes de cambiar.

Una empresa puede tener keys de ambos entornos a la vez (hasta 3 en total).

Idempotencia

Emitir es la única llamada que no debes repetir a ciegas. Si la respuesta se pierde — timeout, conexión caída — el documento bien puede haber sido autorizado, y un segundo intento emite una segunda factura: otro crédito, otro número de serie quemado, y deshacerlo exige cancelar, que tiene plazo.

Pasa idempotency_key (con el nombre que corresponda a cada lenguaje) para que reintentar sea seguro. Al repetir con la misma clave y el mismo cuerpo recibes de vuelta la primera respuesta — sin segundo documento, sin consumir crédito. Disponible en issue y reissue.

Genera la clave tú mismo — un UUID por evento de negocio, no por petición HTTP — y consérvala mientras todavía puedas reintentar. Las claves viven 24 horas; después de eso la misma clave se trata como nueva.

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 | optionaltu clave, hasta 255 caracteres. El SDK solo la reenvía en el header — nunca genera ni guarda nada.
replay
same key, same bodymisma clave, mismo cuerpo: devuelve la respuesta guardada, sin reprocesar.
APIError 422
same key, other bodymisma clave, cuerpo distinto. Reutilizar una clave en otra petición es un error del cliente, no un reintento.
APIError 409
still runningla primera llamada sigue en curso. Consulta antes de volver a intentar.

Límites de uso

Los límites son por clave de API. Cada empresa tiene su propio contador — el volumen de un cliente nunca gasta la cuota de otro.

Emitir tiene un límite más estricto que consultar, y consultar más estricto que listar el historial. Cuanto más depende una operación de la SEFAZ, menos llamadas por minuto acepta.

Al excederlo, el SDK lanza APIError con estado 429. No expone los headers de la respuesta, así que espera unos segundos antes de volver a intentar — y reutiliza la misma clave de idempotencia, para que el reintento no genere un 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 / mingenera documento y consume crédito.
consult / cancel
120 / minllega a la SEFAZ, pero no genera documento.
APIError 429
over the limitespera y repite con la misma clave de idempotencia.

Errores

Los SDKs traducen cada falla en un tipo distinto: APIError para lo que respondió la API (con status y detail), ConnectionFailedError para una falla de red antes de cualquier respuesta, y el error de validación del propio lenguaje para lo que se detecta en el cliente, antes de que salga la solicitud.

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}")
HTTPErrorCuándo
400Configuración inválidaLa configuración del emisor (certificado, dirección, campos fiscales) está ausente o es inválida.
401No autorizadoEl Bearer token está ausente, expirado, es inválido o no fue creado para el host que estás llamando (api.stackin.io vs sdk.stackin.io).
402Límite alcanzadoLa cuota del plan de la empresa para notas de production fue alcanzada y el plan no tiene precio de excedente (trial).
404No encontradoNingún documento con esa clave de acceso o invoice_id pertenece a la empresa de tu key.
409Conflicto de operaciónLa operación solicitada no aplica a ese document_type (ej.: una operación exclusiva de nfse en una nfe).
422Error de validaciónEl cuerpo de la solicitud falló la validación — campo obligatorio faltante o malformado.
429Límite de peticionesDemasiadas llamadas para esta key en la ventana actual. El header Retry-After dice cuántos segundos esperar.
501No implementadoLa firma para ese tipo de documento/país aún no está implementada en el servidor.
502El autorizador rechazóRechazo del autorizadorEl autorizador (SEFAZ/estado o ADN) rechazó la solicitud. detail.message trae el error del propio autorizador, y detail.invoice_id (solo en la emisión) permite consultar el intento rechazado.

Rechazos del autorizador

Cuando la SEFAZ o ADN rechaza una operación, la respuesta incluye información adicional.

El detail del APIError lleva estos campos cuando el autorizador rechaza — usa detail.invoice_id para consultar el intento después.

Copy
{
  "detail": {
    "message": "IE do emitente inválida",
    "invoice_id": "inv_8f3c9b2e6a7d"
  }
}
detail.message
Mensaje devuelto por el autorizador.
detail.invoice_id
Identifica el intento de emisión.

Valores del ítem

unit_price es el precio de una unidad. amount es el valor bruto total de los productos de la línea, antes de descuento, flete, seguro y otros gastos.

Envíe uno de los dos. Enviar ambos afirma que coinciden: la API compara quantity x unit_price con amount y rechaza el documento antes de la transmisión si difieren. En los SDK los decimales viajan como texto, así que un precio con 10 decimales llega a la API exactamente como se escribió.

También se rechazan: ninguno de los dos campos (ITEM_AMOUNT_MISSING), valores iguales o menores que cero, un descuento mayor que el valor bruto (ITEM_DISCOUNT_EXCEEDS_GROSS) y precisión que el XML no admite (ITEM_PRECISION_UNSUPPORTED). Nada debe migrar: amount conserva el significado que siempre tuvo.

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 tributaria

El grupo de la reforma tributaria, por ítem. Obligatorio desde 2026 para el régimen normal. cst y classification (cClassTrib) son su decisión fiscal; la API deriva los importes de las alícuotas.

Las tres alícuotas son porcentajes: rate_state (pIBSUF), rate_city (pIBSMun) y rate_federal (pCBS). base es opcional — sin ella, la base es el valor del ítem. Un ítem sin el grupo no emite nada, que es como era la NF-e antes de la 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 impuesto del ítem

ICMS, IPI, PIS y COFINS por ítem, a través de Tax. Cada grupo tiene su clase por CST/CSOSN — IcmsSn102 para Simples Nacional sin crédito, PisNt y CofinsNt para no gravado, y así.

stackin nunca infiere tributación: qué grupo aplica es decisión fiscal del emisor, y equivocarse produce impuesto equivocado, no un error de esquema. Sin tax, ningún grupo de impuesto llega al 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

Facturas

Emitir factura

Emite un nuevo documento. items[].product.br (ncm, cfop) es obligatorio para , se ignora en , ignored for — un servicio no tiene código de clasificación fiscal, así que nfse solo lee description y amount de cada ítem.

La misma llamada funciona para nfse — solo cambia 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 factura

Consulta el estado actual de un documento directamente en el autorizador por su access key. Requiere como query param.

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

Cancelar factura

Cancela un documento previamente autorizado. debe tener al menos 15 caracteres para nfe (el campo xJust del autorizador lo exige). Cancelar no devuelve la cuota del plan — la capacidad del autorizador ya se gastó en la emisión.

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

Reemitir nota

Reenvía una emisión fallida por el `id` local del documento (no por su clave de acceso — una nota rechazada no tiene una). Útil para reintentar después de corregir un problema de configuración o una caída del autorizador. Consume crédito como una emisión nueva, ya que es una nueva transmisión.

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

Corregir factura

Corrige un documento ya autorizado con una carta de corrección (CC-e), sin cancelar ni reemitir — no consume crédito ni quema número de serie. Sirve para errores que no cambian el impuesto: nombre de producto, datos de transporte, información complementaria.

El texto va de 15 a 1000 caracteres, verificado por el SDK antes de la llamada. Una CC-e no corrige nada que cambie el valor del impuesto, el emisor, el destinatario o la fecha de emisión — en esos casos sigue siendo cancelar y reemitir. El documento original no cambia: la corrección queda adjunta. Como máximo 20 por documento, numeradas automáticamente. Solo NF-e; para NFS-e llega APIError 409.

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

Inutilizar rango

La numeración de la NF-e es secuencial y la SEFAZ exige que no tenga huecos. El número se reserva cuando empieza la emisión, así que una emisión que falla después — rechazo, timeout — deja un vacío en la serie. Inutilizar el rango es como se cierra ese vacío.

El motivo va de 15 a 255 caracteres y el rango es inclusivo — el SDK verifica ambos, y que el fin no sea menor que el inicio, antes de la llamada. Un número que ya llegó a la SEFAZ no puede inutilizarse: llega como APIError 409 diciendo cuáles son. Solo NF-e, y sin clave de acceso: no hay documento al que apuntar.

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

PDF del documento

La representación impresa que genera la autorizadora — DANFE para NF-e, DANFSe para NFS-e. Devuelve bytes crudos, el único método que no devuelve un objeto: guárdelo en un archivo o sírvalo directamente.

El XML sigue siendo el documento con valor jurídico; el PDF es una conveniencia. El endpoint de la autorizadora es inestable, así que un 502 aquí significa autorizadora no disponible, no que la nota esté mal.

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

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

Historial

Lo que esta empresa emitió, lo más reciente primero. Es la contraparte de received: aquel lista lo que emitieron contra usted, este lo que usted emitió.

Filtre por document_type y status (issued, rejected, cancelled), y pagine con limit y offset. La respuesta trae el envelope de paginación estándar.

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"])

Intentos

Cada intento hecho para una nota y lo que volvió de la autorizadora. consult da el estado; este da el motivo — el código del rechazo y el XML que se envió.

Recibe el invoice_id, no la clave de acceso: un documento rechazado nunca tuvo una. Así se descubre que un rechazo fue, por ejemplo, 972.

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

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

Notas recibidas

Documentos que otras empresas emitieron contra su identificación fiscal. Lee lo que la API ya recolectó — no llama a la autorizadora en el momento.

La recolección corre en agenda del lado de la API porque la SEFAZ limita cuántas veces una identificación fiscal puede consultar por día. Leer aquí no gasta esa cuota.

Copy
page = client.received(limit=20)

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

Manifestación

La respuesta formal de quien recibió: confirmación, conocimiento, desconocimiento u operación no realizada.

Solo operación no realizada acepta justificación — y la exige. El SDK lo verifica antes de salir de su máquina, en vez de gastar una ida a la API para que se lo digan.

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",
)
Confiable

Más del 99,9% de uptime

Seguro

Datos cifrados y listo para LGPD

Rápido

Infraestructura escalable y de baja latencia

IA en primer lugar

Servidor MCP oficial — emita desde un agente

© 2026 stackin. Todos los derechos reservados.Hecho con código, propósito y café.Política de PrivacidadTérminos de UsoContacto