Idempotencia e Integridad
Cómo usar Idempotency-Key para evitar facturas duplicadas y verificar integridad con checksums.
Idempotencia e Integridad
QAPI ofrece dos mecanismos para garantizar confiabilidad en tu integración: idempotencia para evitar duplicados y checksums para verificar integridad.
Idempotencia
Cuando tu servidor envía un request pero la red falla antes de recibir respuesta, ¿emitiste la factura o no? Sin idempotencia, reenviar el request podría crear un duplicado con validez tributaria.
Cómo usar Idempotency-Key
Envía un identificador único en el header Idempotency-Key con cada request de creación
(POST /v1/invoices y POST /v1/sandbox/invoices). Puede ser un UUID o el ID de tu orden —
cualquier string ASCII sin espacios de 1 a 200 caracteres:
curl -X POST https://api.dtevia.com.gt/v1/invoices \
-H "Authorization: Bearer qapi_live_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: ORD-2026-001" \
-d @factura.jsonSi envías el mismo Idempotency-Key dos veces con el mismo body, QAPI no crea una
segunda factura: devuelve exactamente la misma respuesta que el request original
(mismo id) con el header Idempotency-Replayed: true. La ventana de idempotencia
es de 24 horas y aplica por tenant.
Por qué esto importa en facturación
QAPI genera un numero_acceso aleatorio por request, así que dos requests idénticos
sin Idempotency-Key producen dos DTEs distintos y válidos — el anti-duplicados
del certificador no puede detectarlos. La única protección real contra duplicados por
reintentos de red es este header.
Genera la key en tu código
import { randomUUID } from 'crypto';
async function emitirFacturaIdempotente(payload: InvoicePayload, orderId: string) {
// Usa un deterministic ID basado en tu orden para sobrevivir reinicios del servidor
const idempotencyKey = orderId; // o randomUUID() si no tienes un ID propio
const res = await fetch('https://api.dtevia.com.gt/v1/invoices', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.QAPI_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey,
},
body: JSON.stringify(payload),
});
return res.json();
}import uuid
def emitir_factura_idempotente(payload: dict, order_id: str) -> dict:
idempotency_key = order_id # o str(uuid.uuid4())
res = httpx.post(
"https://api.dtevia.com.gt/v1/invoices",
json=payload,
headers={
"Authorization": f"Bearer {QAPI_API_KEY}",
"Idempotency-Key": idempotency_key,
},
)
return res.json()Comportamiento en reintentos
| Escenario | Comportamiento |
|---|---|
| Timeout antes de recibir respuesta | Reenvía con la misma key → obtienes la respuesta original (header Idempotency-Replayed: true) |
| El request original dio error (422, 5xx) | La key queda libre: corrige el payload y reintenta con la misma key (solo se guardan respuestas exitosas) |
| Misma key, body distinto | 409 IDEMPOTENCY_CONFLICT — usa una key nueva para un request nuevo |
| Dos requests simultáneos con la misma key | El segundo recibe 409 IDEMPOTENCY_CONFLICT con Retry-After |
| Key diferente, mismo payload | QAPI crea una segunda factura distinta |
| Key expirada (>24h) | QAPI trata el request como nuevo |
Integridad de respuestas
QAPI firma todas las respuestas con un checksum SHA-256 para que puedas detectar manipulación en tránsito.
Headers de integridad
Prop
Type
Asertar el checksum del request (opcional)
Si querés que QAPI verifique que tu request llegó intacto, calculá el SHA-256 (hex) del
body que enviás y mandalo en el header X-Content-SHA256. QAPI compara ese valor con el
hash del cuerpo recibido y, si no coincide, rechaza el request con 400 INTEGRITY_CHECK_FAILED
antes de procesarlo.
BODY=$(cat factura.json)
HASH=$(printf '%s' "$BODY" | sha256sum | cut -d' ' -f1)
curl -X POST https://api.dtevia.com.gt/v1/invoices \
-H "Authorization: Bearer qapi_live_xxxxxxxxxxxx" \
-H "Content-Type: application/json" \
-H "X-Content-SHA256: $HASH" \
-d "$BODY"El header es opcional: si no lo envías, QAPI no valida el hash del request (igual responde
con X-Request-Checksum, el SHA-256 del body que recibió). La comparación es sobre el hash en
minúsculas, calculado sobre los bytes exactos del body que mandás.
Verificar el checksum de respuesta
import crypto from 'crypto';
async function fetchWithIntegrityCheck(url: string, options: RequestInit) {
const res = await fetch(url, options);
const rawBody = await res.text();
const receivedChecksum = res.headers.get('x-response-checksum');
if (receivedChecksum) {
const calculatedChecksum = crypto
.createHash('sha256')
.update(rawBody)
.digest('hex');
if (calculatedChecksum !== receivedChecksum) {
throw new Error('Integridad comprometida: checksum no coincide');
}
}
return JSON.parse(rawBody);
}import hashlib
def fetch_with_integrity_check(url: str, **kwargs) -> dict:
res = httpx.get(url, **kwargs)
raw_body = res.text
received_checksum = res.headers.get("x-response-checksum")
if received_checksum:
calculated = hashlib.sha256(raw_body.encode()).hexdigest()
if calculated != received_checksum:
raise ValueError("Integridad comprometida: checksum no coincide")
return res.json()integrity_checksum en la factura
Además de los headers HTTP, cada factura certificada incluye un campo
integrity_checksum en el objeto de respuesta:
{
"id": "inv_V1StGXR8_Z5jdHi6",
"status": "CERTIFIED",
"integrity_checksum": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
"uuid_sat": "550E8400-E29B-41D4-A716-446655440000"
}Es el SHA-256 (hex, sin prefijo) del body JSON original con el que creaste la factura. Puedes recalcularlo sobre el payload que enviaste para demostrar que lo que QAPI registró es exactamente lo que tu sistema emitió.
Resumen de buenas prácticas
- Siempre usa
Idempotency-KeyenPOST /v1/invoices— usa el ID de tu orden como key - Guarda el
idQAPI junto a tu orden inmediatamente al recibir el202 - Verifica
X-Response-Checksumen entornos de alta seguridad - Almacena
integrity_checksumde cada factura certificada como evidencia de integridad