Cómo integrar¶
El código que vas a escribir. Ejemplos completos de cada operación, para copiar y adaptar.
Cuando llegues a una decisión que no sea obvia —cómo traducir tu modelo al documento, qué tratamiento de IVA lleva cada producto, qué revisar antes de producción— eso está en Adaptar tu sistema.
Qué tenés que implementar¶
Tres piezas. Con eso ya facturás electrónicamente:
| # | Qué construís | Cuándo corre |
|---|---|---|
| 1 | Una llamada de creación | Cuando se genera la venta en tu sistema |
| 2 | Un endpoint HTTP que reciba los webhooks, con dos handlers | Cuando SmartDoc avisa cómo salió |
| 3 | Una acción de corrección | Solo si el documento quedó en error |
Emitir es asíncrono: la llamada de creación vuelve enseguida y la aprobación llega después, por webhook. Ese es el flujo:
1. Se genera la venta
└─> sd.invoices.create(...) guardás id + tipo, estado "pendiente"
2. Llega el webhook
├─ invoice.approved guardás el CDC, estado "aprobada" ✔ fin
└─ invoice.error guardás el motivo, estado "error"
│
3. Corregís los datos │
└─> sd.invoices.update(...) <─────────┘
└─> vuelve al paso 2
En detalle, lo mínimo de cada pieza:
- Al crear, guardá el
iddel documento y su tipo ("invoice","receipt"…). Es lo que va a llegar en el webhook y sin eso no podés vincular el evento con tu venta. - El endpoint de webhooks es una ruta
POSTde tu aplicación, expuesta en internet conhttps://, cuya URL registrás en el panel de SmartDoc. Ahí dentro, dos handlers:- Con el evento de aprobación, guardá el CDC y marcá la venta como aprobada. El CDC solo llega acá, y es obligatorio si más adelante necesitás emitir un recibo o una nota de crédito o débito sobre esa factura.
- Con el evento de error, guardá
error_codeyerror_message, y mostralo donde tu equipo lo vea. Un documento en error no se resuelve solo.
- Para corregir, una acción en tu sistema que vuelva a mandar el documento
con los datos arreglados usando
update. SmartDoc lo reprocesa y vuelve a avisarte por webhook.
De ahí en adelante es tuyo: cobrar con recibos, corregir con notas, anular. Los ejemplos de abajo cubren cada caso.
Antes de empezar¶
Dos variables en el entorno:
Y las dos piezas del SDK que se usan en todos los ejemplos:
import os
import smartdoc
sd = smartdoc.Client()
webhooks = smartdoc.Webhooks(secret=os.environ["SMARTDOC_WEBHOOK_SECRET"])
Qué es del SDK y qué es tuyo
Todo lo que empieza con smartdoc. viene del paquete. El resto es de tu
aplicación y lo vas a renombrar por lo que uses:
| En los ejemplos | Qué representa |
|---|---|
venta |
El registro de la operación en tu base de datos |
Venta.objects |
Cómo consultás esa tabla |
venta.smartdoc_entity, venta.smartdoc_id, venta.cdc, venta.estado_fiscal |
Campos que agregás a tu tabla para guardar lo que devuelve SmartDoc |
Los ejemplos son sincrónicos
Con smartdoc.Client() las llamadas no llevan await: devuelven el
documento directamente.
Si tu aplicación es asíncrona, usá smartdoc.AsyncClient(), que tiene la
misma superficie pero con await:
Montar el endpoint de webhooks¶
Los @webhooks.on(...) de los ejemplos registran handlers, pero por sí solos
no reciben nada. Hace falta exponer una ruta POST en tu aplicación y
conectarla al SDK, que se encarga de verificar la firma y llamar al handler que
corresponda.
Abajo están los tres frameworks más usados, que el SDK trae resueltos. No estás limitado a ellos: sirve cualquiera, y no hace falta tener ninguno de los tres instalado.
Con cualquier framework, pasale a dispatch el cuerpo crudo y los
encabezados. Verifica la firma, descarta las entregas repetidas y llama al
handler que corresponda:
@app.route("/webhooks/smartdoc", methods=["POST"])
def recibir():
try:
webhooks.dispatch(cuerpo_crudo, encabezados)
except smartdoc.SignatureError:
return "", 400
return "", 200
Respondé 200 cuando lo procesaste y 400 si la firma no valida.
El cuerpo tiene que ser el crudo, tal como llegó. Si el framework parsea el JSON y lo volvés a serializar, la firma deja de coincidir.
Después, en el panel de SmartDoc: Conectividad → Webhooks → Agregar, con la
URL pública de esa ruta —https://tu-servidor.com/webhooks/smartdoc— y los
tipos de evento a los que te suscribís. Al guardar, el panel muestra una sola
vez el secreto: ese es el que va en smartdoc.Webhooks(secret=...).
Para probar en tu máquina
La URL registrada tiene que ser pública y https://, así que localhost no
sirve. Levantá un túnel y registrá la URL que devuelva:
Cada endpoint tiene un botón Enviar evento de prueba que verifica la conectividad y la firma sin emitir nada.
El detalle de la firma, los reintentos y la deduplicación está en Webhooks.
Emitir una factura¶
factura = sd.invoices.create(
recipient=smartdoc.Recipient.entity(
ruc="80012345-1",
social_name="Cliente Ejemplo S.A.",
email="facturas@cliente.com.py",
),
items=[
smartdoc.Item("Consultoría de agosto", quantity=1, unit_amount=1100000),
],
)
venta.smartdoc_entity = "invoice"
venta.smartdoc_id = factura.id
venta.estado_fiscal = str(factura.status) # pending o generated
venta.save()
La aprobación llega al webhook:
@webhooks.on(smartdoc.Event.INVOICE_APPROVED)
def factura_aprobada(evento):
entidad, numero = evento.document_key # ("invoice", 1234)
venta = Venta.objects.get(smartdoc_entity=entidad, smartdoc_id=numero)
venta.cdc = evento.cdc # ← guardalo
venta.estado_fiscal = "aprobada"
venta.save()
Guardá el CDC. Solo llega acá, y es obligatorio si más adelante necesitás emitir un recibo, una nota de crédito o una nota de débito sobre esta factura: esos documentos la referencian por su CDC. Sin él no los podés emitir.
Guardá también el par (entity_type, entity_id), no solo el número: el id se
numera por tipo de documento, así que el recibo 20 y la nota de débito 20
existen a la vez.
Emitir un recibo de esa factura¶
El recibo se emite sobre una factura ya aprobada, y la referencia por su CDC.
recibo = sd.receipts.create(
recipient=smartdoc.Recipient.entity(
ruc="80012345-1",
social_name="Cliente Ejemplo S.A.",
email="facturas@cliente.com.py",
),
amount=1100000,
payment_method=smartdoc.ReceiptPaymentMethod.CASH,
associated_document=smartdoc.AssociatedDocument.electronic(venta.cdc),
invoice_id=venta.smartdoc_id,
)
@webhooks.on(smartdoc.Event.RECEIPT_APPROVED)
def recibo_generado(evento):
entidad, numero = evento.document_key
Cobro.objects.filter(
smartdoc_entity=entidad, smartdoc_id=numero
).update(estado_fiscal="generado")
El recibo no se envía a la DNIT: su estado final de éxito es generated y
evento.cdc viene en None.
Con cheque hacen falta dos datos más:
sd.receipts.create(
...,
payment_method=smartdoc.ReceiptPaymentMethod.CHECK,
check_bank="Banco Continental",
check_number="12345678",
)
Para un recibo suelto, sin factura asociada:
Emitir una nota de crédito¶
Se emite sobre una factura ya aprobada para disminuir su monto: una devolución, un descuento posterior. Lleva documento asociado obligatorio, y también la referencia por CDC.
nota = sd.credit_notes.create(
recipient=smartdoc.Recipient.entity(
ruc="80012345-1",
social_name="Cliente Ejemplo S.A.",
email="facturas@cliente.com.py",
),
items=[
smartdoc.Item("Devolución de mercadería", quantity=1, unit_amount=110000),
],
associated_document=smartdoc.AssociatedDocument.electronic(venta.cdc),
invoice_id=venta.smartdoc_id,
)
@webhooks.on(smartdoc.Event.CREDIT_NOTE_APPROVED)
def nota_aprobada(evento):
entidad, numero = evento.document_key
nota = NotaCredito.objects.get(smartdoc_entity=entidad, smartdoc_id=numero)
nota.cdc = evento.cdc
nota.save()
La nota de débito se emite igual, con sd.debit_notes, y aumenta el monto de la
factura —intereses, gastos—.
Anular una factura¶
Solo se anula una factura aprobada, y el motivo es obligatorio.
sd.invoices.cancel(venta.smartdoc_id, reason="Error en el monto facturado")
venta.estado_fiscal = "anulación solicitada"
venta.save()
@webhooks.on(smartdoc.Event.INVOICE_CANCELLED)
def factura_anulada(evento):
entidad, numero = evento.document_key
Venta.objects.filter(
smartdoc_entity=entidad, smartdoc_id=numero
).update(estado_fiscal="anulada")
cancel no devuelve el documento: la anulación es asíncrona y se confirma por
el evento.
Si la factura todavía no está aprobada, la llamada levanta un ValidationError
con código ACTION_NOT_ALLOWED.
Corregir una factura que quedó en error¶
Si la DNIT la rechaza o falla por cualquier motivo, el documento queda en
error y llega un evento de error. Se corrige editándolo: SmartDoc lo
vuelve a procesar solo, sin reenviarlo ni reemitirlo.
@webhooks.on(smartdoc.Event.INVOICE_ERROR)
def factura_en_error(evento):
entidad, numero = evento.document_key
venta = Venta.objects.get(smartdoc_entity=entidad, smartdoc_id=numero)
venta.estado_fiscal = "error"
venta.error_fiscal = f"{evento.error_code}: {evento.error_message}"
venta.save()
avisar_al_equipo(venta)
Y una vez corregido el dato que corresponda:
sd.invoices.update(
venta.smartdoc_id,
recipient=smartdoc.Recipient.entity(
ruc="80012345-1", # el dato corregido
social_name="Cliente Ejemplo S.A.",
email="facturas@cliente.com.py",
),
items=[
smartdoc.Item("Consultoría de agosto", quantity=1, unit_amount=1100000),
],
)
Vuelve a procesarse y llega invoice.approved como en el camino normal.
update no es un parche: hay que pasar el documento entero, igual que al
crearlo. Lo que no mandes, se pierde.
Se puede editar en cualquier estado menos uploaded_to_set y approved_by_set.
Ahí la API responde EDIT_NOT_ALLOWED y la vía es anular y reemitir.
Identificar al receptor: por datos o por id¶
Los datos del receptor van siempre, porque son los que se imprimen en el documento:
smartdoc.Recipient.entity(
ruc="80012345-1",
social_name="Cliente Ejemplo S.A.",
email="facturas@cliente.com.py",
)
Si además tenés el cliente cargado en SmartDoc, agregá su client_id para que
el documento quede asociado a esa ficha en el panel:
smartdoc.Recipient.from_client_id(
196, # id del cliente en SmartDoc
client_type=smartdoc.ClientType.ENTITY,
ruc="80012345-1",
social_name="Cliente Ejemplo S.A.",
is_taxpayer=True,
)
El client_id no reemplaza los datos: los complementa.
Elegir establecimiento y punto de expedición¶
Hace falta solo si el contribuyente tiene más de uno. Se aceptan las dos formas, y son equivalentes:
# Por código, el que se ve en el documento
sd = smartdoc.Client(establishment="001", dispatch_point="001")
# Por id interno de SmartDoc
sd = smartdoc.Client(establishment=8, dispatch_point=14)
Si emitís desde varias sucursales, pasalos en cada llamada en vez de fijarlos:
Sin indicarlos y con más de uno, la llamada levanta ConfigurationError con las
opciones disponibles.
Si todavía no tenés webhooks montados¶
Para una prueba rápida o un script de una sola corrida, wait_until_final
consulta hasta que el documento llegue a un estado final:
Sirve para probar. En producción usá webhooks: el polling consume cuota del límite de solicitudes por segundo, y una espera puede llevar minutos.
Y después¶
- Adaptar tu sistema — traducir tu modelo de datos al del documento, el criterio del IVA por producto y el checklist de producción.
- Webhooks — la firma, los reintentos y la deduplicación.
- Errores — qué excepción conviene reintentar y cuál no.
- Documentos — qué pide cada tipo y qué acciones acepta.