Polling vs webhook
Hacienda (Costa Rica’s tax authority) takes seconds or minutes to resolve a document. There are two ways to learn the result: querying repeatedly (polling) or receiving a notification (webhook). The first is simple, but expensive and slow; the second is the recommended option whenever you can expose a public endpoint.
What gets retried and what doesn’t
It’s worth distinguishing three elements that tend to get conflated into one:
| What | Retry |
|---|---|
| Sending the document to Hacienda | 5 attempts, backoff from 10 s to 5 min |
| Querying Hacienda for the final status | every 10 min, no time limit, until it responds |
| Delivering the webhook to the integrator | 3 in-process attempts (0 s / 2 s / 8 s) + 2 by cron every 10 min |
The first two correspond to LX CloudPos talking to Hacienda; the third is delivery to the integrator. Even so, GET /api/v1/facturas/{clave} is always the source of truth for the status: it’s fed by the second row’s sweep, regardless of whether the webhook was delivered.
Polling pattern (not recommended)
Query the document’s detail in a loop until its status changes. This is useful for local testing or when you can’t expose a public endpoint.
async function esperarHacienda(clave, maxIntentos = 30) {
for (let i = 0; i < maxIntentos; i++) {
const { data } = await axios.get(`/api/v1/facturas/${clave}`);
if (data.estadoHacienda === 'aceptado') return data;
if (data.estadoHacienda === 'rechazado') throw new Error('Rechazado');
await sleep(30000); // 30s
}
throw new Error('Timeout esperando Hacienda');
}Webhook pattern (recommended)
The document is issued and requires no active follow-up. LX CloudPos notifies you when Hacienda responds, and the integrator only reads the result from its own database.
// 1. Issuer: issue and forget
await emitirFactura(data);
// 2. Webhook receiver: record the result
app.post('/webhooks/lxs', verificarHmac, async (req, res) => {
const evento = parsearEvento(req.body);
await db.facturas.update({
where: { clave: evento.clave },
data: { estadoFinal: evento.ind_estado }
});
res.status(200).end();
});
// 3. Frontend: query your own DB's status, without touching the API