Saltar a contenido

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:

  1. Al crear, guardá el id del 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.
  2. El endpoint de webhooks es una ruta POST de tu aplicación, expuesta en internet con https://, 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_code y error_message, y mostralo donde tu equipo lo vea. Un documento en error no se resuelve solo.
  3. 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:

export SMARTDOC_API_KEY="pk_..."
export SMARTDOC_WEBHOOK_SECRET="..."

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.

factura = sd.invoices.create(...)

Si tu aplicación es asíncrona, usá smartdoc.AsyncClient(), que tiene la misma superficie pero con await:

sd = smartdoc.AsyncClient()
factura = await sd.invoices.create(...)

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.

from fastapi import FastAPI, Request

app = FastAPI()

@app.post("/webhooks/smartdoc")
async def recibir(request: Request):
    return await webhooks.fastapi_route()(request)
from flask import Flask

app = Flask(__name__)
app.add_url_rule(
    "/webhooks/smartdoc",
    view_func=webhooks.flask_view(),
    methods=["POST"],
)
# urls.py
from django.urls import path

urlpatterns = [
    path("webhooks/smartdoc", webhooks.django_view()),
]

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:

ngrok http 8000

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:

associated_document=smartdoc.AssociatedDocument.none()

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:

sd.invoices.create(..., establishment="002", dispatch_point="001")

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:

factura = sd.invoices.wait_until_final(factura.id)
print(factura.status, factura.cdc)

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.