keysoft
Facturación electrónica SUNAT desde Python. Sin dependencias — solo biblioteca
estándar, para que entre también donde el pip está restringido.
pip install keysoft
De cero a una factura aceptada
import os
from keysoft import KeySoft
ks = KeySoft(os.environ["KEYSOFT_API_KEY"])
# El ambiente sale de la clave: ks_test_ es pruebas, ks_live_ es producción.
print(ks.environment) # 'DEMO'
emitido = ks.issue(company_id, {
"documentType": "FACTURA",
"series": "F001",
"customer": {
"identityType": "RUC",
"identityValue": "20601234567",
"legalName": "DISTRIBUIDORA DEL NORTE S.A.C.",
"email": "compras@cliente.pe", # le llega su factura al aceptarse
},
"lines": [
{"description": "Servicio de consultoría", "quantity": 2, "unitValue": 150},
],
}, idempotency_key=f"pedido-{pedido.id}")
resultado = ks.send(company_id, emitido["document"]["id"])
print(resultado["cdr"]["description"])
# "La Factura numero F001-00000001, ha sido aceptada"
Idempotencia
issue() manda Idempotency-Key por defecto. Si tu proceso reintenta por un
timeout, sin ella emitirías dos facturas — y una factura de más es un problema
fiscal, no un duplicado inocente.
Pásale la tuya: el id de tu pedido es la mejor opción, porque sobrevive a un reinicio de tu proceso.
Los errores dicen qué hacer
from keysoft import KeySoftError
try:
ks.issue(company_id, factura)
except KeySoftError as e:
print(e.code) # 'certificate_missing' — estable, programa contra él
print(e.action) # qué hacer para arreglarlo
if e.retryable:
reintentar()
Descargar y verificar
download() recalcula el SHA-256 y lanza si no cuadra. No te fíes de nosotros:
la comprobación es el producto.
xml = ks.download(company_id, document_id, "xml")
pdf = ks.download(company_id, document_id, "pdf")
Boletas: el reloj de los 7 días
Una boleta no se envía sola — va en el resumen diario, y caduca a los 7 días calendario sin que nadie te avise.
estado = ks.summaries(company_id)
if estado["alert"]["level"] != "ok":
print(estado["alert"]["message"])
# "Quedan 2 día(s) para informar las boletas más antiguas sin resumen."
resumen = ks.create_summary(company_id)
ks.send_summary(company_id, resumen["summary"]["id"])
# El bucle de consulta lo hacemos nosotros: un ticket NO es una aceptación.
final = ks.wait_for_summary(company_id, resumen["summary"]["id"])
print(final["propagatedTo"], "boletas informadas")
Webhooks
from keysoft import verify_webhook
@app.post("/hooks/keysoft")
def hook():
if not verify_webhook(
request.get_data(), # el cuerpo CRUDO
request.headers["keysoft-signature"],
os.environ["KEYSOFT_WEBHOOK_SECRET"],
):
return "", 400
evento = request.get_json()
return "", 200
El cuerpo tiene que ser el crudo. Si lo parseas y lo vuelves a serializar la firma deja de cuadrar: es el fallo más común al integrarlo.
La firma caduca a los 5 minutos, que es lo que impide que quien capture una petición válida te la reenvíe mañana.
Metadata
Release files for keysoft 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 | |
|---|---|---|---|
| keysoft-0.1.0.tar.gz | 6.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| keysoft-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 14.7 kB
Release files / keysoft-0.1.0.tar.gz
| Download URL | keysoft-0.1.0.tar.gz |
|---|---|
| Size | 6.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
f5346ae52c78a7824d74e41f3ce189cca65a83d0e7c144264dcdd79db267720e
|
|
BLAKE2b-256 checksum How to use checksums |
da3bdff8ffd86c60deb5268e41e8f56bffab50f74e38bbb39128a24ba897f4a1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.7
|
Release files / keysoft-0.1.0-py3-none-any.whl
| Download URL | keysoft-0.1.0-py3-none-any.whl |
|---|---|
| Size | 7.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
873672c032ecde7b68fa35f756a1a3fe288becf1a573a1de3a87caf985d7cde2
|
|
BLAKE2b-256 checksum How to use checksums |
1eba90240c3017ec32ab69b0f53bdeb4ab66c5d0a9001b7931044c99a365220d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.14.7
|