Digifact FEL Guatemala — SDK para Python
SDK en Python para la API de facturación electrónica en línea (FEL) de Guatemala de Digifact.
Instalación
pip install digifact-sdk
O desde el código fuente:
pip install -e python/
Configuración del cliente (DigifactClient(...))
Ordenados de más usados a menos usados.
| Parámetro | Tipo | Por defecto | Descripción |
|---|---|---|---|
taxid |
str |
requerido | NIT del emisor. Acepta dígitos o con separadores ("12345678", "1234567-8"); los no-dígitos se eliminan. Se rellena internamente a 12 caracteres. |
username |
str |
requerido | Usuario corto de Digifact (la parte después de GT.<NIT>., p. ej. "FELUSER"). |
password |
str |
"" |
Contraseña de la cuenta. Requerido si no se provee token. |
token |
str |
"" |
Bearer token preobtenido. Si se provee, se omite el login. |
environment |
str |
"test" |
"test" o "production". |
seller_name |
str |
"" |
Nombre del emisor. Para NIT individual es el nombre de la persona; para S.A. / S.E. es la razón social de la entidad. Si está vacío, se consulta en SAT vía lookup_nit(). |
seller_address |
str |
"" |
Dirección del emisor. Si está vacía, se consulta en SAT. |
branch_code |
str |
"1" |
Código del establecimiento del RTU. Un NIT puede tener varios establecimientos (1, 2, 3…). Se escribe en Seller.BranchInfo.Code. |
branch_name |
str |
"ESTABLECIMIENTO PRINCIPAL" |
Nombre comercial de la sucursal — el mismo que aparece en la patente de comercio. Se escribe en Seller.BranchInfo.Name. |
afiliacion_iva |
str |
"GEN" |
Afiliación IVA del RTU: "GEN", "PEQ" o "EXE". |
tipo_frase |
str | None |
None |
Sobreescritura global de TipoFrase (legacy). Mutuamente exclusivo con frases. Ver frases. |
escenario |
str | None |
None |
Sobreescritura global de CodigoEscenario (legacy). Mutuamente exclusivo con frases. |
frases |
list[dict] | None |
None |
Lista de frases {"tipo_frase": ..., "escenario": ...}. Reemplaza a tipo_frase/escenario. Mutuamente exclusivo con ellos. Ver frases. |
petroleo_rates |
dict[str, float] | None |
None |
Mapa código PETROLEO → tarifa por unidad. Usado sólo por fuel_invoice() (gasolineras). |
timeout |
int |
120 |
Timeout HTTP en segundos. |
session |
requests.Session | None |
None |
Sesión HTTP personalizada (útil para tests). |
tipo_personeria |
str |
"1" |
Código de TipoPersoneria del RTU. Sólo aplica a RDON (Recibo por Donación); ignóralo en los demás documentos. |
Inicio rápido
from digifact_sdk import DigifactClient
client = DigifactClient(
taxid="12345678",
username="FELUSER",
password="secret",
environment="test", # o "production"
)
# FACT CF — consumidor final, el IVA se calcula automáticamente
result = client.invoice(
buyer="CF",
items=[{"description": "Consultoría", "qty": 1, "price": 100.00}]
)
print(result.auth_number)
# FACT a NIT — el nombre del receptor se consulta automáticamente en SAT
result = client.invoice(
buyer="12345678",
items=[
{"description": "Laptop", "qty": 1, "price": 5000.00, "type": "Bien"},
{"description": "Soporte anual", "qty": 1, "price": 500.00},
]
)
# FACT a receptor con CUI (DPI) — el nombre se consulta en SAT automáticamente
result = client.invoice(
buyer={"taxid": "1234567890123", "type": "CUI"},
items=[{"description": "Producto", "qty": 2, "price": 50.00}]
)
# Equivalente: un string de 13 dígitos se resuelve como CUI
result = client.invoice(
buyer="1234567890123",
items=[{"description": "Producto", "qty": 2, "price": 50.00}]
)
# Con nombre explícito no se consulta SAT
result = client.invoice(
buyer={"taxid": "3730617490101", "type": "CUI", "name": "Juan Pérez"},
items=[{"description": "Producto", "qty": 2, "price": 50.00}]
)
# Receptor NIT con datos explícitos (sin consulta automática)
result = client.invoice(
buyer={
"taxid": "12345678",
"name": "EMPRESA EJEMPLO S.A.",
"address": "6 AV 6-48 ZONA 9",
"city": "01009",
"district": "GUATEMALA",
"state": "GUATEMALA",
"country": "GT",
"email": "facturacion@empresa.com", # opcional
},
items=[{"description": "Producto", "qty": 1, "price": 100.00}]
)
# FCAM (Factura Cambiaria)
result = client.invoice(
buyer="12345678",
items=[{"description": "Servicio", "qty": 1, "price": 500.00}],
doc_type="FCAM",
payment_terms=[{"date": "2026-04-18", "amount": 500.00}]
)
# Nota de crédito (NCRE)
result = client.credit_note(
buyer="12345678",
items=[{"description": "Devolución", "qty": 1, "price": 100.00}],
origin={
"auth_number": "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
"date": "2026-03-18",
"series": "XXXXXXXX",
"number": "123456",
},
reason="Producto defectuoso"
)
# Nota de débito (NDEB)
result = client.debit_note(
buyer="12345678",
items=[{"description": "Cargo adicional", "qty": 1, "price": 50.00}],
origin={...},
reason="Cargo por entrega express"
)
# Anular un DTE
result = client.cancel(
auth_number="XXXXXXXX-...",
receiver_id="CF",
issue_datetime="2026-03-18 21:40:14",
reason="Error en monto"
)
# Nota de crédito total
result = client.credit_note_total(
auth_number="XXXXXXXX-...",
issue_datetime="2026-03-18 21:40:14",
reason="Nota de crédito total"
)
# Consulta de NIT
info = client.lookup_nit("12345678")
print(info["name"])
# Consulta de CUI (DPI) — SAT devuelve el nombre como "APELLIDOS, NOMBRES"
info = client.lookup_cui("1234567890123")
print(info["name"]) # "PEREZ LOPEZ, JUAN CARLOS"
# Obtener DTE
doc = client.get_dte("XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX")
Tipos de documento
| Tipo | Descripción | IVA |
|---|---|---|
FACT |
Factura estándar | Sí |
FCAM |
Factura Cambiaria (con abonos) | Sí |
NDEB |
Nota de débito | Sí |
NCRE |
Nota de crédito | Sí |
NABN |
Nota de abono | No |
FESP |
Factura especial | Sí |
RDON |
Recibo por donación | No |
FPEQ |
Factura pequeño contribuyente | No |
RECI |
Recibo universitario | No |
CCA |
Cobro por cuenta ajena | Sí |
Cálculo del IVA
Los precios incluyen IVA (es lo que paga el cliente):
total_linea = qty × price
base_imponible = total_linea / 1.12
monto_iva = total_linea − base_imponible
Todos los montos se formatean como cadenas con 6 decimales.
Campos del ítem
{
"description": str, # requerido
"price": float | Decimal, # requerido — precio unitario, incluye IVA
"qty": float | Decimal, # opcional, por defecto 1
"type": str, # opcional: "Servicio" (por defecto) | "Bien"
"unit_of_measure": str, # opcional, por defecto "UNI"
"discount": float | None, # opcional — descuento de la línea
}
Facturas de combustible (FACT Combustible)
Las facturas de combustible emiten IVA y un impuesto PETROLEO según la
especificación de SAT. Los ítems sin petroleo_amount se tratan como ítems
regulares (sólo IVA) y pueden coexistir en la misma factura.
Opción A — tarifas fijadas al inicializar el cliente (recomendada para gasolineras)
# Fijar tarifas PETROLEO una sola vez por tipo de combustible (Q/galón, según MEM o factura del proveedor)
client = DigifactClient(
taxid="12345678",
username="FELUSER",
password="secret",
petroleo_rates={"1": 4.70, "2": 4.60, "4": 1.30}, # SUPER / REGULAR / DIESEL
)
# Sólo hace falta petroleo_code — petroleo_amount se completa automáticamente
result = client.fuel_invoice(
buyer="CF",
items=[
{"description": "GASOLINA SUPER", "qty": 30, "price": 35.00, "petroleo_code": "1", "type": "Bien"},
{"description": "GASOLINA REGULAR", "qty": 20, "price": 34.00, "petroleo_code": "2", "type": "Bien"},
{"description": "GASOLINA DIESEL", "qty": 50, "price": 32.00, "petroleo_code": "4", "type": "Bien"},
# Ítems regulares (sin petroleo_code): sólo IVA, pueden coexistir
{"description": "FILTRO DE ACEITE", "qty": 1, "price": 45.00, "type": "Bien"},
{"description": "SET DE CANDELAS NGK", "qty": 1, "price": 400.00, "type": "Bien"},
],
)
print(result.auth_number)
Opción B — monto explícito por ítem
result = client.fuel_invoice(
buyer="CF",
items=[
{"description": "GASOLINA SUPER", "qty": 1, "price": 35.00, "petroleo_amount": 4.70, "petroleo_code": "1", "type": "Bien"},
{"description": "GASOLINA DIESEL", "qty": 1, "price": 32.00, "petroleo_amount": 1.30, "petroleo_code": "4", "type": "Bien"},
],
)
Campos del ítem de combustible
| Campo | Tipo | Por defecto | Descripción |
|---|---|---|---|
description |
str |
requerido | Descripción de la línea |
price |
float|Decimal |
requerido | Precio unitario completo al consumidor (incluye PETROLEO + IVA). Es lo que paga el cliente en la bomba. Si la factura del proveedor muestra un precio unitario sin PETROLEO/IDP (p. ej. 37.99), suma la tarifa IDP por unidad: price = 37.99 + 4.70 = 42.69. |
qty |
float|Decimal |
1 |
Cantidad |
type |
str |
"Servicio" |
"Bien" o "Servicio" |
unit_of_measure |
str |
"UNI" |
Código de unidad de SAT |
petroleo_amount |
float|Decimal |
— | Impuesto PETROLEO por unidad; omitir para ítems sólo-IVA |
petroleo_code |
str |
"1" |
"1"=SUPER, "2"=REGULAR, "4"=DIESEL. Obligatorio cuando se omite petroleo_amount y petroleo_rates está configurado; lanza DigifactValidationError si el código no está en el diccionario de tarifas. |
Exención temporal (Decreto 22-2026)
Del 1 de octubre al 31 de diciembre de 2026, y después hasta agotar el inventario comprado exento, la
gasolina superior, la regular y el diésel se facturan sin IVA ni IDP. Se activa por ítem con el código de
unidad gravable exento: "18" superior, "21" regular con etanol, "20" diésel y "19" regular sin
etanol (sólo importadores). price es el precio de bomba, ya sin impuestos, y qty va en galones. El SDK
envía ambos impuestos en cero y agrega las frases 9/23 y 4/38; no hace falta petroleo_amount ni petroleo_rates.
from digifact_sdk import fuel_exemption
items = [
{"description": "GASOLINA SUPER", "qty": 10, "price": 30.00, "petroleo_code": "18", "unit_of_measure": "GAL"},
{"description": "GASOLINA REGULAR", "qty": 5, "price": 28.00, "petroleo_code": "21", "unit_of_measure": "GAL"},
{"description": "DIESEL", "qty": 20, "price": 27.00, "petroleo_code": "20", "unit_of_measure": "GAL"},
]
result = client.fuel_invoice("CF", items)
# Montos y leyendas obligatorias para un ticket propio (el PDF de Digifact ya las imprime)
exencion = fuel_exemption(items)
print(exencion.idp, exencion.iva) # 93.70 117.60
print(exencion.leyendas)
Los detalles y las advertencias están en el README principal.
Subsidio combustible
El subsidio a la gasolina y al diésel finalizó el jueves 2 de julio de 2026 a las 24:00, antes de lo
previsto: el presupuesto de Q2 mil millones (Decreto 11-2026, reglamentado por el Acuerdo Gubernativo
64-2026) se agotó por la demanda. Por eso el SDK nunca envía las frases TipoFrase=9, Escenario=18 ni
TipoFrase=9, Escenario=19 por su cuenta — no hay fecha de corte que valga para todos.
Las estaciones con inventario adquirido bajo el subsidio deben mantener el precio rebajado hasta agotar ese
producto, sujeto a verificación. Mientras te quede ese inventario, manda las frases explícitamente con
frases; al agotarlo, deja de mandarlas:
result = client.fuel_invoice("CF", items, frases=[
{"tipo_frase": "1", "escenario": "1"}, # frase base
{"tipo_frase": "9", "escenario": "18"},
{"tipo_frase": "9", "escenario": "19"},
])
Nota: la leyenda del subsidio en la representación gráfica la genera Digifact a partir de las frases del XML certificado, no este SDK. Al dejar de mandar las frases, deja de imprimirse sola.
Nota:
frasesytipo_frase/escenarioson mutuamente exclusivos — pasarlos juntos lanzaDigifactValidationError.
Configuración de frases (TipoFrase / CodigoEscenario)
Todo DTE (excepto FESP) debe llevar un par TipoFrase + CodigoEscenario. El
SDK elige valores por defecto adecuados, por lo que no hace falta
configurar nada en el caso común.
Orden de precedencia: argumentos por llamada → globales del constructor → tabla de valores por defecto.
Tabla de valores por defecto:
| DTE | Afiliación | TipoFrase | CodigoEscenario | Notas |
|---|---|---|---|---|
| FESP | — | — | — | Sin bloque AdditionlInfo |
| FPEQ | PEQ | 2 |
1 |
Pequeño contribuyente |
| RDON | cualquiera | 4 |
4 |
Donaciones |
| RECI | cualquiera | 4 |
5 |
Recibos (universidades) |
| NABN | cualquiera | 9 |
17 |
Abonos |
| FACT / FCAM / NCRE / NDEB | GEN | 1 |
1 |
Por defecto: ISR régimen sobre utilidades trimestrales |
| FACT / FCAM / NCRE / NDEB | PEQ | 2 |
1 |
|
| FACT / FCAM / NCRE / NDEB | EXE | 4 |
1 |
Exento |
Nueva API: frases[] (múltiples frases)
Usa frases cuando necesitas enviar más de un par TipoFrase/Escenario (p. ej. listados en resoluciones SAT). Es mutuamente exclusivo con tipo_frase/escenario.
# Lista explícita de frases (reemplaza a tipo_frase/escenario)
client.fuel_invoice("CF", items, frases=[
{"tipo_frase": "1", "escenario": "1"},
{"tipo_frase": "9", "escenario": "18"},
{"tipo_frase": "9", "escenario": "19"},
])
# Globalmente en el constructor
client = DigifactClient(
taxid="12345678", username="FELUSER", password="...",
frases=[{"tipo_frase": "1", "escenario": "2"}], # ISR régimen opcional simplificado
)
Regla:
frasesytipo_frase/escenarioson mutuamente exclusivos en cada nivel (por llamada y en el constructor). Combinarlos lanzaDigifactValidationError.
API legacy: tipo_frase / escenario
Ambos parámetros siguen funcionando exactamente como antes para integraciones existentes.
# Sobreescritura por llamada (uno o ambos)
client.invoice("CF", items, escenario="1")
client.invoice("CF", items, tipo_frase="2", escenario="1")
# Funciona igual en los demás métodos de DTE
client.credit_note("12345678", items, origin, "...", tipo_frase="2", escenario="1")
client.fuel_invoice("CF", items, tipo_frase="2", escenario="1")
# O globalmente al construir el cliente (p. ej. GEN + ISR régimen opcional simplificado)
client = DigifactClient(
taxid="12345678", username="FELUSER", password="...",
afiliacion_iva="GEN",
tipo_frase="1", # opcional — la tabla ya devuelve "1" para GEN
escenario="2", # ISR régimen opcional simplificado (sobreescribe el "1" por defecto)
)
Para descubrir el par correcto en un caso particular, revisa las afiliaciones del RTU en el portal de SAT.
Referencia de métodos
Todos los métodos de emisión devuelven DteResult con result.auth_number, series, number, issue_datetime, raw.
| Método | Firma | Descripción |
|---|---|---|
invoice() |
invoice(buyer, items, *, doc_type="FACT", payment_terms=None, amount_str="", observaciones="-", tipo_personeria=None, tipo_frase=None, escenario=None) |
Emite FACT, FCAM, FESP, FPEQ, NABN, RDON o RECI según doc_type. |
cca_invoice() |
cca_invoice(buyer, items, cobros, *, tipo_frase=None, escenario=None) |
FACT con complemento CCA. |
fuel_invoice() |
fuel_invoice(buyer, items, *, tipo_frase=None, escenario=None, frases=None) |
FACT con complemento combustible (IVA + PETROLEO). |
credit_note() |
credit_note(buyer, items, origin, reason, *, tipo_frase=None, escenario=None) |
Nota de crédito (NCRE). |
debit_note() |
debit_note(buyer, items, origin, reason, *, tipo_frase=None, escenario=None) |
Nota de débito (NDEB). |
credit_note_total() |
credit_note_total(auth_number, issue_datetime, reason="...", reference="") |
Nota de crédito total. Devuelve dict. |
cancel() |
cancel(auth_number, receiver_id, issue_datetime, reason="Anulación") |
Anula un DTE. Devuelve dict. |
lookup_nit() |
lookup_nit(nit) |
Consulta SAT. Devuelve {"nit","name","address","city","district","state"}. |
lookup_cui() |
lookup_cui(cui) |
Consulta SAT por CUI (DPI). Devuelve {"cui","name","status"}. El nombre viene como "APELLIDOS, NOMBRES", tal como lo registra SAT. |
get_dte() |
get_dte(auth_number, fmt="JSON") |
Recupera el DTE ("JSON", "XML", "HTML", "PDF"). |
get_dte_info() |
get_dte_info(auth_number) |
Metadatos del DTE. |
Parámetros comunes
buyer:"CF"(consumidor final), un NIT string ("12345678"— se consulta el nombre), un CUI string de 13 dígitos ("1234567890123"— se consulta el nombre), un dict CUI ({"type":"CUI","taxid":...}— se consulta el nombre; con"name"explícito no se consulta) o un dict NIT explícito ({"taxid","name","address","city","district","state","country","email"}).items: lista de dicts condescription(str, req),price(float/Decimal, req),qty(1),type("Servicio"/"Bien"),unit_of_measure("UNI"),discount(opcional).origin(NCRE/NDEB):{"auth_number": ..., "date": "YYYY-MM-DD", "series": ..., "number": ...}.
Establecimiento (sucursal)
Cada NIT puede tener varios establecimientos registrados en el RTU. Configúralos al crear el cliente:
client = DigifactClient(
taxid="12345678",
username="FELUSER",
password="secret",
branch_code="2",
branch_name="SUCURSAL ZONA 10",
)
Aplican a todos los DTE emitidos por ese cliente. Si se omiten, se usan los defaults "1" / "ESTABLECIMIENTO PRINCIPAL".
Ejecutar las pruebas
# Pruebas unitarias (no requieren credenciales)
python -m pytest tests/ -v
# Pruebas de integración
export DIGIFACT_TAXID=12345678
export DIGIFACT_USERNAME=FELUSER
export DIGIFACT_PASSWORD=tu_contraseña
python -m pytest tests/ -v
Variables de entorno
| Variable | Descripción |
|---|---|
DIGIFACT_TAXID |
NIT del emisor (p. ej. 12345678) |
DIGIFACT_USERNAME |
Usuario (p. ej. FELUSER) |
DIGIFACT_PASSWORD |
Contraseña de la cuenta |
DIGIFACT_ENVIRONMENT |
test (default) o production |
Manejo de errores
from digifact_sdk import (
DigifactError, # base
DigifactAuthError, # fallo de autenticación
DigifactApiError, # error HTTP / de API
DigifactValidationError, # rechazo de SAT
DigifactNitNotFoundError, # NIT no encontrado
)
try:
result = client.invoice("CF", [...])
except DigifactValidationError as exc:
print(f"SAT rechazó: {exc}")
print(f"Código: {exc.code}")
print(f"Respuesta: {exc.raw}")
except DigifactError as exc:
print(f"Error del SDK: {exc}")
Metadata
Release files for digifact-sdk 3.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 | |
|---|---|---|---|
| digifact_sdk-3.1.0.tar.gz | 40.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| digifact_sdk-3.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 68.2 kB
Release files / digifact_sdk-3.1.0.tar.gz
| Download URL | digifact_sdk-3.1.0.tar.gz |
|---|---|
| Size | 40.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a629d5ee47eb624107f20b06c125560e430c183ec481544a6bfe1166d1ebe19c
|
|
BLAKE2b-256 checksum How to use checksums |
49a34099ac084ee32bfb2c6825e86e804b89f109d15e554a7ba2507459df1f0a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 7, 2026.
Transparency logRelease files / digifact_sdk-3.1.0-py3-none-any.whl
| Download URL | digifact_sdk-3.1.0-py3-none-any.whl |
|---|---|
| Size | 28.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
30156e5d745c94343a914381049e227d12e2384b2f8012475d0830f6b5d3aae4
|
|
BLAKE2b-256 checksum How to use checksums |
887a4d90d9698d95834464ad9d4cbf6d1f06c44cc2775bf57de6118fbed63388
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 7, 2026.
Transparency log