ComprobadoDocumentaciónIngresar

Webhooks

Un webhook es un aviso que Comprobado le manda a tu servidor cuando algo cambia, en lugar de que tengas que consultar una y otra vez. Sirve, por ejemplo, para enterarte de que un comprobante incierto se resolvió o de que un lote terminó.

Los webhooks son una función del plan: si el tuyo no los incluye, la consola la muestra con un candado, la API responde 403 con AS-2027 y no se envían avisos. Ver Plan, cupo y pagos.

Cada aviso trae solo ids y estados. Nunca datos del receptor ni importes: con el id consultás el detalle por la API.

Crear un webhook

En Integraciones → Webhooks → Nuevo webhook (lo puede hacer el Titular o un Administrador), o por la API con una clave «completa»:

curl -X POST https://TU-DOMINIO/v1/webhooks \
  -H "Authorization: Bearer $COMPROBADO_KEY" -H "Content-Type: application/json" \
  -d '{ "url": "https://tu-servidor.com/webhooks/comprobado", "eventos": ["invoice.authorized", "invoice.reconciled"] }'

La respuesta trae el secreto de firma (whsec_…). Se muestra una sola vez: guardalo. Sin eventos (o con ["*"]) recibís todos. Podés tener hasta 5 webhooks.

La dirección tiene que ser https y pública. Por seguridad no se aceptan localhost, redes internas (10.x, 192.168.x…), direcciones con usuario y contraseña, ni se siguen redirecciones. Para probar en tu máquina, exponela con un túnel (ngrok, cloudflared).

Eventos

Evento Cuándo
invoice.authorized ARCA autorizó un comprobante
invoice.rejected Un comprobante fue rechazado (por ARCA o antes de enviarlo)
invoice.uncertain Quedó incierto: no se sabe si ARCA lo registró. No lo reemitas: esperá el siguiente aviso
invoice.deferred ARCA no respondía y el comprobante quedó en espera (?diferir=true): todavía no tiene número ni CAE. Después llega invoice.authorized o invoice.rejected
invoice.reconciled Un incierto se resolvió (estado: autorizado o rechazado)
batch.finished Un lote terminó
batch.paused Un lote se pausó (motivo: cuota, cuenta, ambiente, incierto o error)
certificate.expiring Tu certificado de ARCA vence en 30, 7 o 1 días (una vez por umbral)
subscription.generated Un abono generó su lote del período
payment.registered Se registró un cobro (comprobante emitido) o un pago (recibido), parcial o total, o se marcó como saldado
payment.voided Se anuló un cobro o pago registrado
ping La prueba que mandás con el botón Probar

El aviso

POST /webhooks/comprobado HTTP/1.1
Content-Type: application/json
X-Comprobado-Event: invoice.reconciled
X-Comprobado-Delivery: evt_3de669e1c5944de68b5251102947c65b
X-Comprobado-Signature: t=1761256637,v1=5b0c…e41f
User-Agent: Comprobado-Webhooks/1.0
{
  "id": "evt_3de669e1c5944de68b5251102947c65b",
  "tipo": "invoice.reconciled",
  "creado": "2026-10-23T21:57:14Z",
  "data": {
    "id": "inv_a780f155…",
    "estado": "autorizado",
    "tipoCodigo": 11,
    "puntoVenta": 1,
    "numero": 14,
    "cae": "86431019635161",
    "ambiente": "produccion",
    "origen": "suite"
  }
}

Los avisos de cobros y pagos llevan solo ids y estado, nunca importes: data es { "objeto": "comprobante" | "recibido", "id": "inv_…" | "rcv_…", "pagoId": "pag_…", "estado": "adeudada" | "parcial" | "saldada" }. Para el detalle (importe, medio, saldo) consultá GET /v1/invoices/{id}/pagos o GET /v1/purchase-invoices/{id}/pagos.

Respondé cualquier 2xx dentro de los 10 segundos. Hacé el trabajo pesado después de responder.

Verificar la firma (obligatorio)

Cualquiera que conozca tu URL podría mandarte un aviso falso. La firma prueba que lo mandó Comprobado: es un HMAC-SHA256 de "<t>.<cuerpo exacto>" con tu secreto.

// Node.js (Express): usá el cuerpo CRUDO, sin parsear
import crypto from 'node:crypto';
function verificar(cuerpoCrudo, cabecera, secreto) {
  const t = cabecera.split(',')[0].slice(2);
  const v1 = cabecera.split('v1=')[1];
  const esperado = crypto.createHmac('sha256', secreto).update(`${t}.${cuerpoCrudo}`).digest('hex');
  const ok =
    v1.length === esperado.length && crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(esperado));
  return ok && Math.abs(Date.now() / 1000 - Number(t)) < 300; // y que no sea viejo (anti-replay)
}
# Python
import hmac, hashlib, time
def verificar(cuerpo_crudo: bytes, cabecera: str, secreto: str) -> bool:
    t = cabecera.split(",")[0][2:]
    v1 = cabecera.split("v1=")[1]
    esperado = hmac.new(secreto.encode(), f"{t}.".encode() + cuerpo_crudo, hashlib.sha256).hexdigest()
    return hmac.compare_digest(v1, esperado) and abs(time.time() - int(t)) < 300
// C#
static bool Verificar(string cuerpoCrudo, string cabecera, string secreto)
{
    var t = cabecera.Split(',')[0][2..];
    var v1 = cabecera.Split("v1=")[1];
    var mac = HMACSHA256.HashData(Encoding.UTF8.GetBytes(secreto), Encoding.UTF8.GetBytes($"{t}.{cuerpoCrudo}"));
    return CryptographicOperations.FixedTimeEquals(Encoding.ASCII.GetBytes(v1), Encoding.ASCII.GetBytes(Convert.ToHexString(mac).ToLowerInvariant()))
        && Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - long.Parse(t)) < 300;
}

Reintentos: no se pierde nada

El aviso se guarda antes de enviarse. Si tu servidor está caído o responde con error, reintentamos con espera creciente:

Intento 2.º 3.º 4.º 5.º 6.º 7.º 8.º en adelante
Espera 30 s 2 min 10 min 30 min 1 h 3 h 6–12 h hasta el 12.º intento (~3 días)

Pasado eso el aviso queda fallido, pero no se borra: en Entregas lo ves y lo podés reenviar cuando tu servidor vuelva (también si ya se entregó, por ejemplo para volver a procesarlo). Si desactivás un webhook, los avisos esperan hasta que lo reactives.

Buenas prácticas

  • Idempotencia: un mismo aviso puede llegar más de una vez (reintentos, reenvíos). Guardá el X-Comprobado-Delivery y descartá los repetidos.
  • Sin orden garantizado: un invoice.reconciled puede llegar antes que el invoice.uncertain si este último tuvo que reintentarse. Usá el estado del aviso y, ante la duda, consultá el comprobante.
  • No confíes solo en el aviso: es una señal para actuar; el estado oficial es GET /v1/invoices/{id}.
  • Rotá el secreto si se filtra: Cambiar secreto lo reemplaza al instante (el anterior deja de valer).
  • Los avisos de una cuenta en el simulador funcionan igual: sirve para probar todo el ciclo sin ARCA.

Si algo falla

Respuesta Qué pasó
400 AS-6001 La dirección no es válida (hace falta https público)
400 AS-6002 Un evento no existe (ver GET /v1/webhooks/events)
409 AS-6003 Llegaste al máximo de 5 webhooks
404 AS-6004 El webhook no existe o ya lo borraron
403 AS-2014 La clave no tiene el permiso de configuración (hace falta una clave «completa»)