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.
https://api.stackin.io/api/v1CopiarTodos los endpoints de la API son relativos a esta base URL.
Descargar la colección de PostmanGenerada 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
- 1Regístrese e inicie sesión en .
- 2Cree una empresa — obligatorio antes de la key.
- 3En el dashboard, abra la empresa y vaya a Empresa → API keys. El contexto viene en
sdkpor defecto — cámbielo aapi. - 4Nombre la key y elija el entorno —
homologationoproduction. Ambos son obligatorios en la creación, y el entorno es fijo por el resto de la vida de la key. - 5Copie la key de inmediato — se muestra , al crearla, y no se puede recuperar después. Si la pierde, revóquela y cree otra.
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.
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.
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 -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.
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 57
X-RateLimit-Reset: 1788556800{
"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 -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": []}'{
"event": "invoice.authorized",
"data": {
"invoice_id": "9f2c1e3a-4b5d-6e7f-8a9b-0c1d2e3f4a5b",
"document_type": "nfe",
"access_key": "35240912345678000199550010000000011000000017",
"status": "issued",
"amount": 1500.0
}
}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.
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.
Guía:Rechazo 539 en la NF-e: qué es y cómo resolverlo · Rechazo 204 de la NF-e: NF-e duplicada, qué significa y cómo resolverlo · SEFAZ no disponible: cómo confirmarlo y emitir NF-e en contingencia
Ejemplo de respuesta
{
"detail": "This company has no state set — required for NFe. Set it via PATCH /companies/{id}."
}| HTTP | Error | Cuándo | Más información |
|---|---|---|---|
| 400 | Configuración inválida | La configuración del emisor (certificado, dirección, campos fiscales) está ausente o es inválida. | |
| 401 | No autorizado | El 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 |
| 402 | Límite alcanzado | La cuota del plan de la empresa para notas de production fue alcanzada y el plan no tiene precio de excedente (trial). | |
| 404 | No encontrado | Ningún documento con esa clave de acceso o invoice_id pertenece a la empresa de tu key. | |
| 409 | Conflicto de operación | La operación solicitada no aplica a ese document_type (ej.: una operación exclusiva de nfse en una nfe). | |
| 422 | Error de validación | El cuerpo de la solicitud falló la validación — campo obligatorio faltante o malformado. | |
| 429 | Límite de peticiones | Demasiadas llamadas para esta key en la ventana actual. El header Retry-After dice cuántos segundos esperar. | Ver detalles |
| 501 | No implementado | La firma para ese tipo de documento/país aún no está implementada en el servidor. | |
| 502 | El autorizador rechazóRechazo del autorizador | El 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 |
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.
# 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:
{
"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.
{
"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
}
}
}
}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
Guía:Carta de corrección de la NF-e (CC-e): qué se corrige, plazo y cómo hacerla
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
Guía:Inutilizar numeración de la NF-e en la SEFAZ: cuándo y cómo
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
Guía:Manifestación del Destinatario de la NF-e: eventos, plazos y descarga del XML
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.