Referencia de la API

Referencia de la API

  • Emite, consulta, cancela y corrige

    Emisión, consulta, cancelación, reemisión, carta de corrección e inutilización de rango — NF-e y NFS-e, cada una con lo que soporta.

  • JSON sobre HTTPS

    Solicitudes y respuestas son JSON, autenticadas con una API key Bearer.

  • Delimitada a una empresa

    Cada solicitud está delimitada a una empresa — el estado, la dirección y el certificado digital del emisor se resuelven en el servidor a partir de la API key, así que la solicitud solo necesita los campos de negocio (comprador, ítems, montos).

¿Recién empezando?

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

Base URL
https://api.stackin.io/api/v1Copiar

Todos los endpoints de la API son relativos a esta base URL.

Descargar la colección de Postman

Generada del mismo OpenAPI de esta página. Impórtela y complete la variable api_key.

Autenticación

La API autentica con una API key — mismo mecanismo que el SDK, solo que con el contexto en . Sin login, sin JWT — un header Bearer en cada solicitud.

Cómo obtener una API 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. El contexto viene en sdk por defecto — cámbielo a api.
  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.
cURL
Copy
curl https://api.stackin.io/api/v1/invoices \
  -H "Authorization: Bearer $STACKIN_API_KEY"

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

Entornos

Cada API key se crea para exactamente un entorno — o or — elegido al generarla, y fijo durante toda la vida de la key.

No hay campo de entorno en las solicitudes de la API: la key 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.

Envía el header Idempotency-Key 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. Funciona en POST /invoices y en POST /invoices/{id}/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.

cURL
Copy
curl -X POST https://api.stackin.io/api/v1/invoices \
  -H "Authorization: Bearer $STACKIN_API_KEY" \
  -H "Idempotency-Key: 3f7c1e9a-2b4d-4f6a-8c1e-9d0b5a7f2c34" \
  -H "Content-Type: application/json" \
  -d '{
  "document_type": "nfse",
  "client_name": "John Doe",
  "tax_id": "00000000000",
  "items": [
    { "description": "Technical consulting - 10 hours", "unit_price": "1500.00" }
  ]
}'
Idempotency-Key
header | optionaltu clave, hasta 255 caracteres. Está acotada a tu empresa: la clave de una cuenta nunca choca con la de otra.
201
replaymisma clave, mismo cuerpo: devuelve la respuesta guardada, sin reprocesar.
422
mismatchmisma clave, cuerpo distinto. Reutilizar una clave en otra petición es un error del cliente, no un reintento.
409
in progressla 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 leer el historial. Cuanto más depende una operación de la SEFAZ, menos llamadas por minuto acepta.

Cada respuesta trae el límite, lo que queda y cuándo se reabre la ventana. Al excederlo, la respuesta es 429 con Retry-After en segundos — espera ese tiempo antes de volver a intentar.

Headers de la respuesta
Copy
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 57
X-RateLimit-Reset: 1788556800
429
Copy
{
  "detail": "Rate limit reached: 60 requests per 60s for this operation. Retry after the window resets."
}
POST /invoices
60 / mingenera documento y consume crédito.
POST /invoices/{id}/reissue
60 / mingenera documento y consume crédito.
GET /invoices/{access_key}
120 / minllega a la SEFAZ, pero no genera documento.
POST /invoices/{access_key}/cancel
120 / minllega a la SEFAZ, pero no genera documento.
GET /invoices
600 / minno sale de stackin.

Webhooks

En vez de consultar en bucle para saber cómo terminó una emisión, registra una URL https y stackin te avisa. Un endpoint con events: [] los recibe todos; un nombre de evento desconocido se rechaza en el registro.

Cada entrega va firmada en Stackin-Signature, con la forma t=<epoch>,v1=<hmac>. El HMAC es SHA-256 sobre {timestamp}.{cuerpo} con tu secreto. Verifícalo antes de procesar: la firma es lo que distingue nuestra entrega de quien descubrió tu URL.

La entrega es al menos una vez — ante fallo reintentamos 6 veces, esperando desde 30s hasta 6h. Usa Stackin-Delivery-Id para ignorar lo que ya procesaste. El historial, incluido lo que dejamos de entregar, está en GET /webhooks/deliveries.

cURL
Copy
curl -X POST https://api.stackin.io/api/v1/webhooks \
  -H "Authorization: Bearer $STACKIN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://seu-erp.com.br/stackin", "secret": "um-segredo-longo-e-aleatorio", "events": []}'
Lo que llega a tu endpoint
Copy
{
  "event": "invoice.authorized",
  "data": {
    "invoice_id": "9f2c1e3a-4b5d-6e7f-8a9b-0c1d2e3f4a5b",
    "document_type": "nfe",
    "access_key": "35240912345678000199550010000000011000000017",
    "status": "issued",
    "amount": 1500.0
  }
}
Verificando la firma
Copy
import hashlib, hmac, time

def verify(secret: str, body: str, header: str, tolerance: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
    if abs(int(time.time()) - int(parts["t"])) > tolerance:
        return False
    expected = hmac.new(
        secret.encode(),
        f'{parts["t"]}.{body}'.encode(),
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])
invoice.authorized
eventla SEFAZ autorizó el documento.
invoice.rejected
eventla SEFAZ lo rechazó. El motivo está en el historial de la factura.
invoice.cancelled
eventla cancelación fue aceptada.
invoice.corrected
eventse registró una carta de corrección.
Stackin-Signature
headerel timestamp forma parte de lo firmado, así que una entrega capturada no puede reenviarse después.
Stackin-Delivery-Id
headeridentifica la entrega. Dos con el mismo id son lo mismo, reenviado.

Versionado

La ruta es /api/v1. Dentro de ella podemos agregar — campo nuevo en la respuesta, valor nuevo en un enum, endpoint nuevo, parámetro opcional nuevo — sin cambiar de versión.

Tu parte del trato: trata un campo desconocido como dato, no como error. Los cuatro SDKs oficiales ya lo hacen, cada uno con una prueba que fija el comportamiento. Si llamas a la API directamente, no valides la respuesta contra un schema cerrado.

Quitar algo, cambiar un tipo, endurecer una validación o cambiar el significado de un valor exige versión nueva. Cuando exista una /v2, la /v1 sigue respondiendo por al menos 12 meses — y lo avisa en cada respuesta, con los headers de al lado.

Headers cuando la versión esté depreciada
Copy
Deprecation: Fri, 01 Jan 2027 00:00:00 GMT
Sunset: Sat, 01 Jan 2028 00:00:00 GMT
Link: <https://docs.stackin.io/api#versioning>; rel="deprecation"
aditivo
sin versión nuevacampo nuevo, valor nuevo en un enum, endpoint nuevo, parámetro opcional nuevo.
breaking
exige /v2quitar algo, cambiar un tipo, endurecer una validación, cambiar el significado de un valor, volver obligatorio lo que era opcional.
Deprecation
HTTP-datecuándo se marcó la versión para salir. Es HTTP-date, no ISO.
Sunset
HTTP-datecuándo deja de responder. Al menos 12 meses después del anuncio.

Paginación

GET /invoices está paginado con los query params y and (por defecto query params (default , máx , max ).

El envelope de la respuesta siempre incluye:

total
integertotal de filas que coinciden con los filtros.
total_pages
integer
next_page / prev_page
integer | null
data
arraylas filas de la página.

Errores

La API usa códigos HTTP estándar. En la mayoría de los casos la respuesta lleva un objeto detail. Los rechazos de la SEFAZ o ADN devuelven información adicional sobre el autorizador.

Emitir responde 201 Created, no 200. También inutilizar numeración y registrar un webhook. Eliminar un webhook responde 204, sin cuerpo.

Ejemplo de respuesta
{
  "detail": "This company has no state set — required for NFe. Set it via PATCH /companies/{id}."
}
HTTPErrorCuándoMás información
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).Ver detalles
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.Ver detalles
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.Ver detalles

Rechazos del autorizador

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

Usa detail.invoice_id para consultar el estado del intento en /invoices/{{id}}.

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: quantity x unit_price se compara con amount y el documento se rechaza antes de la transmisión si difieren. Los valores decimales pueden enviarse como texto para no perder precisión — unit_price acepta hasta 10 decimales.

Copy
# unit_price: what one unit costs
{
  "description": "Teclado",
  "product": { "quantity": 2, "unit": "UN", "br": { "ncm": "84716052", "cfop": "5102" } },
  "unit_price": "120.00"
}
# -> qCom 2.0000  vUnCom 120.0000000000  vProd 240.00

# amount: the line's gross total (the original contract)
{
  "description": "Teclado",
  "product": { "quantity": 3 },
  "amount": "50.00"
}
# -> qCom 3.0000  vUnCom 16.6666666667  vProd 50.00

# both, checked against each other before transmission
{
  "description": "Teclado",
  "product": { "quantity": 2 },
  "unit_price": "120.00",
  "amount": "240.00"
}

Cuando no coinciden la respuesta es 422 e indica la línea y ambos números:

Copy
{
  "detail": {
    "code": "ITEM_TOTAL_MISMATCH",
    "field": "items[0].amount",
    "message": "amount is 50.00, but quantity x unit_price is 150.00.",
    "expected": "150.00",
    "received": "50.00"
  }
}

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.

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
{
  "description": "Teclado",
  "unit_price": "120.00",
  "product": {
    "quantity": 2,
    "br": {
      "ncm": "84716052",
      "cfop": "5102",
      "ibs_cbs": {
        "cst": "000",
        "classification": "000001",
        "rate_state": 0.1,
        "rate_city": 0.0,
        "rate_federal": 0.9
      }
    }
  }
}
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.

Listar facturas

Lista el historial de emisiones de la empresa, paginado (vea Paginación arriba). Filtre con document_type y and (issued, rejected, cancelled).

Consultar factura

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

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.

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.

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. 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 la respuesta es 409.

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. Un número que ya llegó a la SEFAZ no puede inutilizarse: revisamos primero nuestros registros y respondemos 409 diciendo cuáles son, sin ida a la SEFAZ — y la SEFAZ vuelve a revisar lo que no se ve desde aquí. Solo NF-e, y sin clave de acceso: no hay documento al que apuntar. Usa GET /invoices/invalidations para listar los rangos ya inutilizados.

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.

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.

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.

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.

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