SDKs oficiales

SDKs para PHP, Node.js, Python, Java y C#

Los cinco se usan igual: mismos nombres de métodos (en español, como los campos del API), mismo comportamiento y mismos errores. Si cambias de lenguaje, no reaprendes nada. Los datos que mandas y recibes son exactamente los del API, sin traducir.

PHP, Node.js y Python: publicados y de código abierto (MIT)

PHP 0.2.0, Node.js 1.1.0 y Python 0.2.0 ya están en sus registros oficiales.

Node.js: npm · GitHub — PHP: Packagist · GitHub — Python: PyPI · GitHub

Java y C#: se instalan desde GitHub (pronto en Maven Central / NuGet)

Ya están listos y publicados como descargable en GitHub. Java 11+ (sin dependencias): factusmart-0.1.0.jar · repo. C# (netstandard2.0 y net8.0): FactuSmart.0.1.0.nupkg · repo. Descarga el archivo y sigue los pasos de la pestaña Java o C# de abajo.

Instalar

npm install factusmart

Requisitos: Node.js 18+ (sin dependencias), PHP 8.1+ con ext-curl y ext-json, Python 3.8+ (solo librería estándar), Java 11+ (sin dependencias), .NET Framework 4.6.1+ / .NET Core 2.0+ / .NET 8 (netstandard2.0 y net8.0).

Tu primera factura en 10 líneas

Reemplaza TU_API_KEY y TU_URL_BASE por los de tu panel (pestaña Configuración). No hay un dominio único: cada cuenta vive en su servidor.

import { FactuSmart } from 'factusmart'

const fs = new FactuSmart({ apiKey: 'TU_API_KEY', baseUrl: 'https://TU_URL_BASE/api/v1/issuer', ruc: '20100070970' })

const { items, totales } = FactuSmart.calcularComprobante([
  { descripcion: 'Servicio de consultoría', cantidad: 1, valor_unitario: 100, afectacion: '10' },
])

const r = await fs.comprobantes.emitir({
  codigo_tipo_documento: '01',
  datos_del_cliente_o_receptor: { codigo_tipo_documento_identidad: '6', numero_documento: '20131312955' },
  items, totales,
})

console.log(r.aceptado_por_sunat, r.serie_numero)
  • calcularComprobante (calcular_comprobante en Python) arma items y totales con IGV 18% y redondeo a 2 decimales. Afectación 10 gravado, 20 exonerado, 30 inafecto, 40 exportación.
  • Si no los mandas, el SDK pone numero_documento: "#" (correlativo automático), la fecha y hora de Perú, codigo_tipo_operacion: "0101" y codigo_tipo_moneda: "PEN".
  • El ruc del constructor es el emisor por defecto; lo puedes cambiar en cada llamada con la opción ruc.
  • En C# todos los métodos son async (terminan en Async) y devuelven un JsonElement; en Java devuelven un Map. Los datos que mandas usan las mismas claves del API.

Métodos y endpoints

Un método por cada endpoint del API. Node.js, PHP y Java usan camelCase; Python, snake_case; C#, PascalCase con Async al final. El detalle de cada campo está en la Referencia del API.

MétodoEndpointPara qué
ping()C#: PingAsync()GET/pingProbar la API key y ver el saldo
cuenta.saldo()C#: Cuenta.SaldoAsync()GET/accountSaldo, plan y RUCs de la cuenta
series.listar()C#: Series.ListarAsync()GET/seriesSeries del RUC y siguiente número
establecimientos.listar()C#: Establecimientos.ListarAsync()GET/establecimientosLocales (anexos) del RUC con sus series
establecimientos.crear(datos)C#: Establecimientos.CrearAsync(datos)POST/establecimientosCrear un local (anexo) y sus series
catalogos.listar()C#: Catalogos.ListarAsync()GET/catalogosCatálogos SUNAT disponibles (gratis)
catalogos.consultar(nombre, opciones?)Python: catalogos.consultar(nombre, q=None)C#: Catalogos.ConsultarAsync(nombre)GET/catalogos/{nombre}Filas de un catálogo: unidades, afectación IGV, motivos… (gratis)
catalogos.buscarProductoSunat(q, opciones?)Python: catalogos.buscar_producto_sunat(q, limite=None)C#: Catalogos.BuscarProductoSunatAsync(q)GET/catalogos/productos-sunat?q=Buscar el código de producto SUNAT (UNSPSC)
comprobantes.calcular(items, opciones?)Python: comprobantes.calcular(items)C#: Comprobantes.CalcularAsync(items)POST/calcularFactuSmart calcula items y totales listos para emitir (gratis)
comprobantes.emitir(datos)C#: Comprobantes.EmitirAsync(datos)POST/documentsEmitir factura, boleta o nota (1 crédito)
comprobantes.listar(filtros)C#: Comprobantes.ListarAsync(filtros)GET/documentsListar comprobantes con filtros
comprobantes.consultar(id)C#: Comprobantes.ConsultarAsync(id)GET/documents/{external_id}Estado guardado y links
comprobantes.descargar(id, tipo)C#: Comprobantes.DescargarAsync(id, tipo)GET/documents/{external_id}/{type}PDF, XML o CDR (devuelve bytes)
comprobantes.descargar(id, 'pdf', { formato: 'ticket' })Python: comprobantes.descargar(id, 'pdf', formato='ticket')C#: Comprobantes.DescargarAsync(id, "pdf", formato: "ticket")GET/documents/{external_id}/pdf?formato=PDF en ticket 80/58/50 mm o A5
comprobantes.reenviar(id)C#: Comprobantes.ReenviarAsync(id)POST/documents/{external_id}/reenviarVolver a enviar a SUNAT
comprobantes.consultarSunat(id)Python: comprobantes.consultar_sunat(id)C#: Comprobantes.ConsultarSunatAsync(id)POST/documents/{external_id}/consultar-sunatPreguntar a SUNAT en vivo
comprobantes.historial(id)C#: Comprobantes.HistorialAsync(id)GET/documents/{external_id}/historialEventos del comprobante
anulaciones.crear(datos)C#: Anulaciones.CrearAsync(datos)POST/anulacionesComunicación de baja (1 crédito)
anulaciones.estado(ticket)C#: Anulaciones.EstadoAsync(ticket)GET/anulaciones/{ticket}/statusResultado de la baja
guias.emitir(datos)C#: Guias.EmitirAsync(datos)POST/guiasGuía de remisión GRE (1 crédito)
guias.estado(id)C#: Guias.EstadoAsync(id)GET/guias/{external_id}/statusResultado de la guía
rucs.listar()C#: Rucs.ListarAsync()GET/rucsRUCs de la cuenta
rucs.crear(datos)C#: Rucs.CrearAsync(datos)POST/rucsDar de alta un RUC (nace en prueba)
rucs.activar(ruc, activo)C#: Rucs.ActivarAsync(ruc, activo)PATCH/rucs/{ruc}/activationActivar o desactivar un RUC

En PHP se llaman con flecha: $fs->comprobantes->emitir(...). formato solo aplica al PDF: a4, a5, ticket (80 mm), ticket58, ticket50; sin él, el A4 de siempre. Ver Descargar PDF y Varios locales (anexos).

Errores

Todos heredan de FactuSmartError, que trae status (HTTP), mensaje (el message del API) y respuesta (el cuerpo completo). En PHP viven en FactuSmart\Excepciones; en C# la base se llama FactuSmartException.

HTTPExcepciónCuándo
401ErrorDeAutenticacionAPI key faltante, inválida o de otro servidor
402SinCreditosTe quedaste sin créditos: no se emitió nada
403RucNoAutorizadoEl RUC no es de tu cuenta o está desactivado
404NoEncontradoNo existe ese comprobante, ticket o archivo
422ErrorDeValidacionDatos inválidos o falta indicar el RUC (el crédito no se cobra)
429LimiteDePeticionesSuperaste el límite de peticiones; trae segundosDeEspera si se pudo leer
otros / redFactuSmartErrorBase de todas: status, mensaje y respuesta completa

Regla de oro: aceptado_por_sunat = false NO es una excepción

SUNAT puede rechazar un comprobante o no responder a tiempo. Eso es un resultado normal: el API responde 200 y el SDK te devuelve la respuesta tal cual, sin lanzar error. Revisa siempre aceptado_por_sunat, guarda el external_id y sigue Si no sale "Aceptado".

import { FactuSmart, SinCreditos, ErrorDeValidacion, FactuSmartError } from 'factusmart'

try {
  const r = await fs.comprobantes.emitir(datos, { idempotencyKey: 'venta-000123' })
  if (!r.aceptado_por_sunat) {
    // No es excepción: el comprobante existe. Revisa r.estado, r.sunat y r.accion_requerida.
  }
} catch (e) {
  if (e instanceof SinCreditos) { /* sube de plan o pide créditos extra */ }
  else if (e instanceof ErrorDeValidacion) { console.error(e.mensaje) }
  else if (e instanceof FactuSmartError) { console.error(e.status, e.mensaje, e.respuesta) }
  else throw e
}

Decide por codigo y reintentable

Toda excepción trae también codigo (el código estable del API, ej. SIN_CREDITOS, DATOS_INVALIDOS) y reintentable (true si repetir la misma petición más tarde puede funcionar). Úsalos en vez de comparar mensaje, que puede cambiar de redacción. Si te llega un codigo que no conoces, guíate por reintentable. Catálogo completo en Códigos de error.

import { FactuSmartError } from 'factusmart'

try {
  await fs.comprobantes.emitir(datos, { idempotencyKey: 'venta-000123' })
} catch (e) {
  if (!(e instanceof FactuSmartError)) throw e
  if (e.codigo === 'SIN_CREDITOS') { /* sube de plan o pide créditos extra */ }
  else if (e.reintentable) { /* encola y vuelve a intentar más tarde */ }
  else { console.error(e.codigo, e.mensaje) /* corrige los datos */ }
}

Verificar webhooks

Si configuraste un webhook, cada aviso llega firmado en el header X-FactuSmart-Firma (HMAC-SHA256 del cuerpo con tu secreto whsec_…). Los SDK lo verifican con una sola función que devuelve true/false:

  • Node.js: FactuSmart.verificarFirmaWebhook(cuerpoCrudo, firma, secreto)
  • PHP: \FactuSmart\Cliente::verificarFirmaWebhook($cuerpoCrudo, $firma, $secreto)
  • Python: factusmart.verificar_firma_webhook(cuerpo_crudo, firma, secreto)
  • Java: FactuSmart.verificarFirmaWebhook(cuerpoCrudo, firma, secreto)
  • C#: FactuSmartClient.VerificarFirmaWebhook(cuerpoCrudo, firma, secreto)
import express from 'express'
import { FactuSmart } from 'factusmart'

app.post('/webhooks/factusmart', express.raw({ type: 'application/json' }), (req, res) => {
  const firma = req.get('X-FactuSmart-Firma') || ''
  if (!FactuSmart.verificarFirmaWebhook(req.body, firma, process.env.FACTUSMART_WEBHOOK_SECRET)) {
    return res.sendStatus(401)
  }
  const aviso = JSON.parse(req.body.toString('utf8'))
  res.sendStatus(200)
})

Pásale el cuerpo crudo, no el JSON ya convertido

Si vuelves a serializar el JSON, la firma no coincide. Si no es válida, responde 401. Y deduplica por el id del aviso: es el mismo en todos los reintentos.

Reintentos e idempotencia

  • comprobantes.emitir siempre manda Idempotency-Key: la tuya si la pasas (idempotencyKey / idempotency_key, recomendado: el id de tu venta) o una generada. La misma clave se reusa en todos los reintentos, así un reintento nunca emite dos veces ni te cobra dos veces.
  • Se reintenta solo ante error de red o timeout, límite de peticiones y respuestas 502, 503, 504, con espera creciente (1 s, 2 s, 4 s…). Por defecto hasta 3 veces (maxReintentos / max_reintentos).
  • Un 409 al emitir («ya hay una emisión en curso con esa Idempotency-Key») también se reintenta con la misma clave.
  • Nunca se reintentan solos: anulaciones.crear, guias.emitir, comprobantes.reenviar, comprobantes.consultarSunat y rucs.activar. Las consultas (GET) sí.
  • Timeout por defecto: 60 segundos (SUNAT a veces tarda).

Más sobre el comportamiento del API en Evitar duplicados y Códigos de error.

¿Listo para integrar?

Crea tu cuenta gratis y recibe tu API key al instante, con 50 créditos de regalo para empezar a emitir en producción. El modo prueba es gratis y no los consume.

Crear cuenta gratis