FactuSmart — SDK de Python
SDK oficial del API de facturación electrónica de FactuSmart (SUNAT, Perú): facturas, boletas, notas de crédito/débito, guías de remisión (GRE 2.0) y anulaciones.
- Python 3.8 o superior.
- Cero dependencias: solo librería estándar (
urllib). - Con type hints (
py.typed). - Documentación completa del API: https://factusmart.pe/api-facturacion-electronica/docs
Instalar
pip install factusmart
Uso en 10 líneas
import factusmart
fs = factusmart.Cliente(api_key="TU_API_KEY", base_url="https://TU_URL_BASE/api/v1/issuer", ruc="20123456789")
calc = factusmart.calcular_comprobante([
{"descripcion": "Servicio de consultoría", "cantidad": 1, "valor_unitario": 100, "afectacion": "10"},
])
r = fs.comprobantes.emitir({
"codigo_tipo_documento": "01",
"datos_del_cliente_o_receptor": {"codigo_tipo_documento_identidad": "6", "numero_documento": "20555555555"},
"items": calc["items"], "totales": calc["totales"],
})
print(r["aceptado_por_sunat"], r["serie_numero"])
base_url no es igual para todas las cuentas: cada cuenta vive en un
servidor distinto. Cópiala de tu panel de integrador (pestaña Configuración,
"URL base de la API").
Opciones del cliente
| Opción | Obligatoria | Por defecto | Para qué |
|---|---|---|---|
api_key |
sí | — | Tu API key (va en el header X-API-Key) |
base_url |
sí | — | URL base de tu cuenta, sin barra final |
ruc |
no | — | RUC emisor por defecto. Se cambia en cada llamada con ruc= |
timeout |
no | 60 |
Segundos por petición (SUNAT a veces tarda) |
max_reintentos |
no | 3 |
Ver "Reintentos e idempotencia" |
verificar_ssl |
no | True |
False solo en pruebas locales con certificado propio |
Si falta api_key o base_url, el cliente lanza ValueError al crearse.
Métodos
Los datos se envían y se reciben tal cual los define el API (claves en
español con guion bajo). Todos los métodos que operan sobre un RUC aceptan
ruc= para cambiar el del constructor (en GET va como ?ruc=; en POST va en el
cuerpo, y si tú ya pusiste ruc en los datos, se respeta el tuyo).
| Método | Endpoint | Devuelve |
|---|---|---|
fs.ping(ruc=None) |
GET /ping |
dict |
fs.cuenta.saldo() |
GET /account |
dict |
fs.series.listar(ruc=None) |
GET /series |
dict |
fs.comprobantes.emitir(datos, ruc=None, idempotency_key=None) |
POST /documents |
dict |
fs.comprobantes.listar(filtros=None, ruc=None) |
GET /documents |
dict |
fs.comprobantes.consultar(external_id, ruc=None) |
GET /documents/{id} |
dict |
fs.comprobantes.descargar(external_id, tipo, ruc=None) |
GET /documents/{id}/{pdf|xml|cdr} |
bytes |
fs.comprobantes.reenviar(external_id, ruc=None) |
POST /documents/{id}/reenviar |
dict |
fs.comprobantes.consultar_sunat(external_id, ruc=None) |
POST /documents/{id}/consultar-sunat |
dict |
fs.comprobantes.historial(external_id, ruc=None) |
GET /documents/{id}/historial |
dict |
fs.anulaciones.crear(datos, ruc=None) |
POST /anulaciones |
dict |
fs.anulaciones.estado(ticket, ruc=None) |
GET /anulaciones/{ticket}/status |
dict |
fs.guias.emitir(datos, ruc=None) |
POST /guias |
dict |
fs.guias.estado(external_id, ruc=None) |
GET /guias/{id}/status |
dict |
fs.rucs.listar() |
GET /rucs |
dict |
fs.rucs.crear(datos) |
POST /rucs |
dict |
fs.rucs.activar(ruc, activo) |
PATCH /rucs/{ruc}/activation |
dict |
Filtros de comprobantes.listar: estado, desde, hasta (YYYY-MM-DD),
limite (1 a 100) y antes_de (pasa el siguiente_cursor de la página
anterior).
Guardar el PDF:
with open("comprobante.pdf", "wb") as f:
f.write(fs.comprobantes.descargar(r["external_id"], "pdf"))
Lo que comprobantes.emitir completa por ti
Solo si no lo mandas:
numero_documento:"#"(FactuSmart asigna el correlativo). Si mandas tu propio número, se respeta.fecha_de_emisionyhora_de_emision: fecha y hora de Perú (America/Lima), no la de tu servidor.codigo_tipo_moneda:"PEN".codigo_tipo_operacion:"0101"en facturas y boletas (en notas no aplica).
guias.emitir completa igual numero_documento, fecha_de_emision y
hora_de_emision.
Calcular IGV y totales
El API espera los importes ya calculados. calcular_comprobante lo hace por ti:
recibe cada ítem con descripcion, cantidad, valor_unitario (sin IGV) y
afectacion, y devuelve {"items": [...], "totales": {...}} listo para enviar.
afectacion |
Significado | IGV |
|---|---|---|
"10" |
Gravado | 18% |
"20" |
Exonerado | 0 |
"30" |
Inafecto | 0 |
"40" |
Exportación | 0 (va en total_exportacion) |
Opcionales por ítem: codigo_interno (recomendado: el SKU de tu sistema) y
unidad_de_medida (por defecto NIU; usa ZZ para servicios).
Redondea a 2 decimales por ítem con decimal.Decimal y ROUND_HALF_UP, así da
exactamente los mismos números que los SDK de PHP y Node.
La regla de oro: revisa aceptado_por_sunat
Un HTTP 200 significa que FactuSmart generó tu comprobante, no que SUNAT lo
aceptó. SUNAT puede estar caída o rechazarlo: recibirás estado: "01" con
aceptado_por_sunat: False y el motivo en sunat["descripcion"].
Eso NO es una excepción. El SDK te devuelve la respuesta tal cual y tú decides:
r = fs.comprobantes.emitir(datos)
if not r["aceptado_por_sunat"]:
print("SUNAT no lo aceptó todavía:", r.get("sunat"), r.get("accion_requerida"))
# corrige y reintenta gratis con: fs.comprobantes.reenviar(r["external_id"])
Mira también entorno ("prueba" o "produccion") y tiene_validez_fiscal.
Errores
Todas las excepciones están en factusmart.errores (y también en factusmart)
y heredan de FactuSmartError, que trae:
status: código HTTP (Nonesi fue un error de red),mensaje: el mensaje del API, en español,respuesta: el cuerpo completo de la respuesta.
| HTTP | Excepción |
|---|---|
| 401 | ErrorDeAutenticacion |
| 402 | SinCreditos |
| 403 | RucNoAutorizado |
| 404 | NoEncontrado |
| 422 | ErrorDeValidacion |
| 429 | LimiteDePeticiones (con segundos_de_espera si se pudo leer) |
| otros / red | FactuSmartError |
from factusmart import errores
try:
r = fs.comprobantes.emitir(datos)
except errores.ErrorDeValidacion as e:
print("Datos inválidos:", e.mensaje) # no se cobró el crédito
except errores.SinCreditos:
print("Recarga créditos")
except errores.FactuSmartError as e:
print("Error", e.status, e.mensaje)
Reintentos e idempotencia
comprobantes.emitirsiempre manda el headerIdempotency-Key: la tuya (idempotency_key="id-de-tu-venta") o un UUID v4 generado. La misma clave se usa en todos los reintentos de esa llamada, así un reintento nunca emite dos veces ni cobra dos veces. Si llamas dos veces con la misma clave, recibes la misma respuesta.- Se reintenta solo ante: error de red/timeout, HTTP 429, 502, 503 y 504, con
espera creciente (1 s, 2 s, 4 s…), hasta
max_reintentosveces. Ante un límite de peticiones se respeta el headerRetry-Aftero el "Espera N segundos" del mensaje. - En
emitir, el HTTP 409 ("ya hay una emisión en curso con esa Idempotency-Key") también se reintenta con la misma clave. - Se reintentan las consultas (GET) y
rucs.crear(el API es idempotente por RUC). - Nunca se reintentan solas:
anulaciones.crear,guias.emitir,comprobantes.reenviar,comprobantes.consultar_sunatyrucs.activar.
Pruebas (para quien mantiene el SDK)
python -m unittest discover -s tests -v
test_unitarios.pyytest_calculo.pyno usan red.test_contrato.pyleeopenapi.yaml(variableFACTUSMART_OPENAPI) y falla si algúnoperationIdno tiene método en el SDK.test_integracion.pyva contra el API local (FACTUSMART_TEST_BASE_URL,FACTUSMART_TEST_API_KEY,FACTUSMART_TEST_RUC;FACTUSMART_INTEGRACION=0para saltarlas). Gasta 2 créditos por corrida. Se niega a correr contrafactusmart.pe.
Licencia
MIT. Ver LICENSE.
Metadata
Release files for factusmart 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| factusmart-0.1.0.tar.gz | 24.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| factusmart-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 40.9 kB
Release files / factusmart-0.1.0.tar.gz
| Download URL | factusmart-0.1.0.tar.gz |
|---|---|
| Size | 24.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
8c8629d2ba1dd347f9d1de78dd51db48ef2e1002dd1602e9924762daafae5842
|
|
BLAKE2b-256 checksum How to use checksums |
29beec589a0358e99ffdd45b30c218c8555ee3b5237cf3f92f433b8d370cd5fb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.
Transparency logRelease files / factusmart-0.1.0-py3-none-any.whl
| Download URL | factusmart-0.1.0-py3-none-any.whl |
|---|---|
| Size | 16.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
a920f39b09bf3adcce706c7086babdf67c0d91e1f2f0ec47bbcf4a841088e935
|
|
BLAKE2b-256 checksum How to use checksums |
fadcb05d5207b784719dd6bc677b8ba5d7e7c8380350f32780cf77c0a8d24f42
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 14, 2026.
Transparency log