Emitir una Factura
Guía paso a paso para emitir tu primera factura electrónica FACT con QAPI.
Emitir una Factura
Esta guía cubre el flujo completo: construir el request, manejar el 202 PROCESSING,
hacer polling hasta CERTIFIED y obtener el PDF final.
QAPI usa un flujo asíncrono. El POST /v1/invoices responde 202 inmediatamente
— la certificación ocurre en segundo plano. Nunca esperes el 202 para confirmar emisión.
Flujo completo
Construye el request
El body mínimo para una factura FACT en entorno live:
{
"tipo_dte": "FACT",
"moneda": "GTQ",
"fecha_emision": "2026-06-08T10:00:00-06:00",
"emisor": {
"nit": "12345678",
"nombre": "Mi Empresa S.A.",
"nombre_comercial": "Mi Empresa",
"codigo_establecimiento": 1,
"afiliacion_iva": "GEN",
"direccion": {
"calle": "6a Avenida 1-36 Zona 1",
"municipio": "Guatemala",
"departamento": "Guatemala",
"codigo_postal": "01001",
"pais": "GT"
},
"correo": "facturacion@miempresa.com"
},
"receptor": {
"id": "CF",
"tipo_id": "NIT",
"nombre": "Consumidor Final"
},
"frases": [
{ "tipo_frase": 1, "codigo_escenario": 1 }
],
"items": [
{
"tipo": "S",
"cantidad": 1,
"unidad_medida": "UND",
"descripcion": "Servicio de consultoría",
"precio_unitario": 100.00,
"descuento": 0,
"impuestos": [
{
"nombre": "IVA",
"codigo_unidad_gravable": 1,
"monto_gravable": 100.00,
"monto_impuesto": 12.00
}
]
}
],
"metadata": {
"referencia_interna": "ORD-2026-001"
}
}precio_unitario siempre SIN IVA
El precio_unitario es el precio antes de IVA. QAPI valida que
monto_impuesto = monto_gravable × 0.12. Si envías el precio con IVA incluido,
recibirás 422 VALIDATION_ERROR.
Envía el request
curl -X POST https://api.dtevia.com.gt/v1/invoices \
-H "Authorization: Bearer qapi_live_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-d @factura.jsonconst res = await fetch('https://api.dtevia.com.gt/v1/invoices', {
method: 'POST',
headers: {
Authorization: 'Bearer qapi_live_xxxxxxxxxxxx',
'Content-Type': 'application/json',
},
body: JSON.stringify(invoicePayload),
});
const { id, status } = await res.json();
// status === "PROCESSING"import axios from 'axios';
const { data } = await axios.post(
'https://api.dtevia.com.gt/v1/invoices',
invoicePayload,
{ headers: { Authorization: 'Bearer qapi_live_xxxxxxxxxxxx' } }
);
// data.status === "PROCESSING"import httpx
res = httpx.post(
"https://api.dtevia.com.gt/v1/invoices",
json=invoice_payload,
headers={"Authorization": "Bearer qapi_live_xxxxxxxxxxxx"},
)
data = res.json()
# data["status"] == "PROCESSING"La respuesta 202 incluye el id de la factura y el estado PROCESSING:
{
"id": "inv_V1StGXR8_Z5jdHi6",
"status": "PROCESSING",
"tipo_dte": "FACT",
"gran_total": 112.00,
"moneda": "GTQ",
"numero_acceso": 543219876,
"created_at": "2026-06-08T16:00:00.000Z",
"links": { "self": "/v1/invoices/inv_V1StGXR8_Z5jdHi6" }
}Espera la certificación
Puedes usar polling con backoff o configurar un webhook para
recibir invoice.certified automáticamente.
async function waitForCertification(id: string, apiKey: string) {
const delays = [1000, 2000, 2000, 3000, 5000, 5000, 10000];
for (const delay of delays) {
await new Promise(r => setTimeout(r, delay));
const res = await fetch(`https://api.dtevia.com.gt/v1/invoices/${id}`, {
headers: { Authorization: `Bearer ${apiKey}` },
});
const invoice = await res.json();
if (invoice.status === 'CERTIFIED') return invoice;
if (invoice.status === 'FAILED') throw new Error(invoice.error_message);
}
throw new Error('Timeout esperando certificación');
}async function waitForCertification(id: string, apiKey: string) {
const delays = [1000, 2000, 2000, 3000, 5000, 5000, 10000];
for (const delay of delays) {
await new Promise(r => setTimeout(r, delay));
const { data } = await axios.get(
`https://api.dtevia.com.gt/v1/invoices/${id}`,
{ headers: { Authorization: `Bearer ${apiKey}` } }
);
if (data.status === 'CERTIFIED') return data;
if (data.status === 'FAILED') throw new Error(data.error_message);
}
throw new Error('Timeout esperando certificación');
}import time
def wait_for_certification(invoice_id: str, api_key: str) -> dict:
delays = [1, 2, 2, 3, 5, 5, 10]
for delay in delays:
time.sleep(delay)
res = httpx.get(
f"https://api.dtevia.com.gt/v1/invoices/{invoice_id}",
headers={"Authorization": f"Bearer {api_key}"},
)
invoice = res.json()
if invoice["status"] == "CERTIFIED":
return invoice
if invoice["status"] == "FAILED":
raise Exception(invoice["error_message"])
raise TimeoutError("Timeout esperando certificación")Accede al PDF certificado
Cuando el estado es CERTIFIED, el campo pdf_url contiene el enlace al PDF con sello SAT:
{
"id": "inv_V1StGXR8_Z5jdHi6",
"status": "CERTIFIED",
"uuid_sat": "550E8400-E29B-41D4-A716-446655440000",
"serie": "A",
"numero_dte": 42,
"pdf_url": "https://print.totaldoc.io/pdf?uuid=550E8400-...",
"certified_at": "2026-06-08T16:00:02.000Z"
}El pdf_url es la URL de la representación gráfica provista por el certificador.
Es un enlace estable — no expira. También puedes usar GET /v1/invoices/:id/pdf,
que redirige (302) al mismo enlace.
Consideraciones importantes
Zona horaria
Guatemala usa UTC-6. Usa siempre -06:00 en el offset de fecha_emision:
"fecha_emision": "2026-06-08T10:00:00-06:00"Consumidor Final (CF)
Para ventas sin NIT específico, usa id: "CF" con tipo_id: "NIT":
"receptor": { "id": "CF", "tipo_id": "NIT", "nombre": "Consumidor Final" }Múltiples items
Puedes incluir varios items en un mismo DTE. Cada uno con sus propios impuestos:
"items": [
{
"tipo": "S",
"cantidad": 2,
"unidad_medida": "HRS",
"descripcion": "Horas de desarrollo",
"precio_unitario": 150.00,
"descuento": 0,
"impuestos": [{ "nombre": "IVA", "codigo_unidad_gravable": 1, "monto_gravable": 300.00, "monto_impuesto": 36.00 }]
},
{
"tipo": "B",
"cantidad": 1,
"unidad_medida": "UND",
"descripcion": "Licencia anual",
"precio_unitario": 500.00,
"descuento": 50.00,
"impuestos": [{ "nombre": "IVA", "codigo_unidad_gravable": 1, "monto_gravable": 450.00, "monto_impuesto": 54.00 }]
}
]