Saltar a contenido

Recetas

Migrar desde la API cruda

Si ya te integraste a mano contra /api/v2, esto es lo que reemplaza el SDK.

import uuid, requests

headers = {
    "Authorization": f"Bearer {API_KEY}",
    "Idempotency-Key": str(uuid.uuid4()),
    "Content-Type": "application/json",
}

contribuyente = requests.get(f"{BASE}/taxpayer", headers=headers).json()["data"]
timbrados = requests.get(f"{BASE}/stamps?enabled=true", headers=headers).json()["data"]
timbrado = timbrados[0]

total = 1100000
iva = round(total / 11)

cuerpo = {
    "taxPayerRuc": contribuyente["ruc"],
    "taxPayerSocialName": contribuyente["socialName"],
    "taxPayerFantasyName": contribuyente["fantasyName"],
    "stampIdentifier": timbrado["identifier"],
    "stampBeginDate": timbrado["beginDate"][:10],
    "establishmentCode": "001",
    "dispatchPointIdentifier": "001",
    "invoiceDate": "2026-06-15T10:00:00",
    "saleType": "cash",
    "transactionTypeCode": 1,
    "currency": "PYG",
    "clientType": "entity",
    "clientRuc": "80012345-1",
    "clientSocialName": "Cliente S.A.",
    "clientIsTaxPayer": True,
    "clientCountryCode": "PRY",
    "clientHasAddress": False,
    "amount": total,
    "amount10Percent": total,
    "taxed10Percent": total - iva,
    "iva10Percent": iva,
    "items": [{
        "description": "Consultoría",
        "code": "1",
        "measureUnit": 77,
        "quantity": 1,
        "unitAmount": total,
        "totalAmount": total,
        "ivaType": "10_percent",
        "amount10Percent": total,
        "taxed10Percent": total - iva,
        "iva10Percent": iva,
    }],
}

r = requests.post(f"{BASE}/invoices", json=cuerpo, headers=headers)
r.raise_for_status()
factura = r.json()["data"]
import smartdoc

sd = smartdoc.Client(establishment="001", dispatch_point="001")

factura = sd.invoices.create(
    recipient=smartdoc.Recipient.entity(
        ruc="80012345-1", social_name="Cliente S.A."
    ),
    items=[smartdoc.Item("Consultoría", quantity=1, unit_amount=1100000)],
    transaction_type=smartdoc.TransactionType.MERCHANDISE_SALE,
    invoice_date="2026-06-15T10:00:00",
)

Mandar campos que el SDK no modela

Para un campo de la API que no tiene parámetro propio, pasalo como argumento con su nombre tal cual: va al cuerpo sin tocar.

sd.invoices.create(
    recipient=...,
    items=[...],
    dncpContractCode="ABC-123",
    dncpContractYear="2026",
)

Leer un campo de la respuesta cruda

Para un campo que el modelo no expone como propiedad:

factura.raw["invoiceNumber"]
factura["invoiceNumber"]        # equivalente

Emitir por lotes

Para emitir muchos documentos seguidos. El SDK regula el ritmo solo; hay que decidir qué hacer con los que fallan.

def emitir_lote(sd, ventas):
    emitidas, fallidas = [], []

    for venta in ventas:
        try:
            factura = sd.invoices.create(
                recipient=recipient_for(venta.cliente),
                items=items_de(venta),
            )
            emitidas.append((venta.id, factura.id))
        except smartdoc.ValidationError as e:
            fallidas.append((venta.id, str(e)))
        except (smartdoc.RateLimitError, smartdoc.ServerError) as e:
            cola.reintentar(venta.id, motivo=str(e))

    return emitidas, fallidas

Con el cliente asíncrono, en paralelo:

import asyncio

async def emitir_lote_async(ventas):
    async with smartdoc.AsyncClient(
        establishment="001", dispatch_point="001"
    ) as sd:
        tareas = [
            sd.invoices.create(
                recipient=recipient_for(v.cliente), items=items_de(v)
            )
            for v in ventas
        ]
        return await asyncio.gather(*tareas, return_exceptions=True)

Exportar un mes de facturación

Para bajar todas las facturas de un período a un archivo.

import csv
from datetime import date

facturas = sd.invoices.list(
    status="approved_by_set",
    date_from=date(2026, 7, 1),
    date_to=date(2026, 7, 31),
).auto_paging_iter()

with open("julio.csv", "w", newline="") as f:
    escritor = csv.writer(f)
    escritor.writerow(["numero", "fecha", "cliente", "monto", "cdc"])
    for factura in facturas:
        escritor.writerow([
            factura["invoiceNumber"],
            factura["invoiceDate"][:10],
            factura["clientSocialName"],
            factura.amount,
            factura.cdc,
        ])

Emitir desde varias sucursales

Si el contribuyente tiene más de un establecimiento, no lo fijes en el cliente: pasalo en cada llamada.

sd = smartdoc.Client()      # sin establishment ni dispatch_point

sd.invoices.create(
    recipient=..., items=[...],
    establishment=sucursal.codigo_establecimiento,
    dispatch_point=caja.codigo_punto,
)

Sin indicarlo y con más de uno, la llamada levanta ConfigurationError con las opciones disponibles.

Verificar una factura de proveedor

Para consultar en la DNIT un documento que recibiste, antes de cargarlo. No lo registra.

documento = sd.received_documents.lookup_by_cdc("01800695631001...")
print(documento["status"], documento["taxPayerRuc"])

Requiere el permiso write_received_documents. Si el documento no existe en la DNIT, la llamada levanta ValidationError con código SIFEN_REJECTED.

Usar tu propia clave de idempotencia

Cada create() ya va con una clave nueva que genera el SDK, así que esto es opcional. Sirve si preferís que la clave venga de tu sistema.

factura = sd.invoices.create(
    recipient=recipient_for(venta.cliente),
    items=items_de(venta),
    idempotency_key=f"venta-{venta.id}",
)

Repetir la llamada con la misma clave devuelve el documento ya creado.

Conciliar contra tu base

Guardá el id de SmartDoc, que es lo que llega en el webhook:

factura = sd.invoices.create(...)
venta.smartdoc_id = factura.id
venta.save()

Para el camino inverso, poné tu identificador en code, que queda impreso en el documento. Tiene que medir entre 3 y 15 caracteres:

smartdoc.Recipient.entity(
    ruc="80012345-1",
    social_name="Cliente S.A.",
    code=f"cli-{cliente_local.id}",
)

Ver qué manda el SDK

Para inspeccionar las solicitudes HTTP:

import logging

logging.basicConfig(level=logging.DEBUG)
logging.getLogger("httpx").setLevel(logging.DEBUG)

Para ver el cuerpo sin mandarlo:

import json
from smartdoc.resources.bodies import invoice_body

cuerpo = invoice_body(
    {"taxPayerRuc": "80012345-1"},      # base del emisor, a mano
    recipient=smartdoc.Recipient.entity(ruc="8-1", social_name="C"),
    items=[smartdoc.Item("X", quantity=1, unit_amount=1100000)],
)
print(json.dumps(cuerpo, indent=2, ensure_ascii=False))