API SUNAT con Python: factura electrónica con requests

Publicado el

Con Python solo necesitas requests para integrar API SUNAT. En esta guía emites una factura en SUNAT beta y verificas el webhook que te avisa cuando SUNAT responde, con Flask.

Lo que necesitas

Python 3 con la librería requests.

Una cuenta de API SUNAT y una clave de prueba asu_test_: emite en SUNAT beta, con numeración propia y sin consumir tu plan.

Un RUC de prueba registrado con su certificado digital. La guía de inicio rápido de la documentación explica cómo hacerlo desde el panel.

La clave guardada en la variable de entorno APISUNAT_KEY de tu servidor, nunca en el código ni en el navegador.

Emite una factura

Envía la venta a POST /documents. Con un RUC en customer.documentNumber puedes omitir el nombre: la API lo busca.

PYemitir_factura.py
import os, uuid, requests

response = requests.post(
    "https://api.apisunat.net/api/v1/issuer/documents",
    headers={
        "X-API-Key": os.environ["APISUNAT_KEY"],
        "Idempotency-Key": str(uuid.uuid4()),
    },
    json={
        "documentType": "01",
        "currency": "PEN",
        "issueDate": "2026-10-27",
        "customer": {"documentType": "6", "documentNumber": "20100066603"},
        "items": [{"description": "Consultoría", "quantity": 1, "unitPrice": 1180, "unitCode": "ZZ"}],
    },
    timeout=60,
)
body = response.json()
if not response.ok:
    raise RuntimeError(f"{body['error']['code']}: {body['error']['message']}")
print(body["fullNumber"], body["status"])

Reintenta sin emitir dos veces

POST /documents exige el header Idempotency-Key. Guarda esa clave junto a tu pedido y reúsala si reintentas: la misma clave con el mismo cuerpo devuelve el mismo resultado y no emite un comprobante duplicado.

Si SUNAT rechaza el comprobante, la API no responde con un error HTTP: responde 202 con status "rejected" y el motivo en sunat.errorCode y sunat.errorMessage.

Recibe la respuesta de SUNAT por webhook

La emisión responde de inmediato y SUNAT puede seguir procesando. Registra un webhook para recibir document.accepted, document.rejected o document.failed.

Verifica la firma del header X-Apisunat-Signature (HMAC-SHA256 de "<t>.<cuerpo crudo>" con tu secreto whsec_) y rechaza las entregas con más de 5 minutos de antigüedad.

PYwebhook.py (Flask)
import hashlib, hmac, os, time
from flask import Flask, request, abort

app = Flask(__name__)
SECRET = os.environ["APISUNAT_WEBHOOK_SECRET"].encode()  # whsec_...

@app.post("/webhooks/apisunat")
def apisunat_webhook():
    parts = dict(p.split("=", 1) for p in request.headers.get("X-Apisunat-Signature", "").split(",") if "=" in p)
    body = request.get_data()
    expected = hmac.new(SECRET, parts.get("t", "").encode() + b"." + body, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, parts.get("v1", "")) or abs(time.time() - int(parts.get("t", 0))) > 300:
        abort(400)
    event = request.get_json()
    # Guarda event["id"] para ignorar duplicados.
    return "", 200

Siguientes pasos

Para emitir boletas cambia documentType a "03"; para notas de crédito, a "07". Todos los campos y códigos de error están en la documentación de la API.

Cuando todo funcione en beta, cambia a una clave asu_live_ y habilita producción en tu RUC.

Otras guías