KicketKicket
Mi cuenta
Guía · Webhooks▾
  • Inicio
  • Primeros pasos
  • Autenticación
  • Paginación y sincronización
  • Errores
  • Límites de uso
  • Webhooks
  • Conciliación
  • Modelo de datos
  • Referencia
  • Descargas
  • Cambios

API de Kicket · v1

  • Inicio
  • Primeros pasos
  • Autenticación
  • Paginación y sincronización
  • Errores
  • Límites de uso
  • Webhooks
  • Conciliación
  • Modelo de datos
  • Referencia
  • Descargas
  • Cambios

Webhooks

Kicket le avisa a tu sistema cuando una venta o un producto cambia, con un POST firmado. Así no tenés que consultar a cada rato.

Configurar un endpoint

  1. En Integraciones › API de Kicket, en la parte de Webhooks, tocá Nuevo endpoint.
  2. Poné la dirección de tu sistema (tiene que ser https://, pública y en el puerto 443 o 8443), elegí el entorno y los eventos que querés recibir.
  3. Copiá el secreto (whsec_…): con él verificás que cada aviso viene de Kicket.
  4. Tocá Enviar prueba: te llega un test.ping. En Ver entregas ves cada intento, con el status que contestó tu sistema, y podés reintentar uno a mano.

Los eventos

EventoQué esCuándo sale
sale.closedVenta cobradaLa primera vez que una venta queda cobrada (CLOSED). Una ronda de mesa abierta (OPEN) no avisa.
sale.updatedVenta modificada después del cobroUna venta ya avisada cambió: una edición después del cobro, una devolución parcial, el medio de pago corregido.
sale.canceledVenta anuladaUna venta se anuló sin devolver plata.
sale.refundedVenta devueltaUna venta se anuló y se devolvió la plata.
sale.deletedVenta eliminadaUna venta se borró. `data` es la lápida.
product.updatedProducto o combo modificadoUn producto o combo se creó o cambió (no hay `product.created`). Agotarse no cuenta: es stock.
product.deletedProducto o combo eliminadoUn producto o combo se borró. `data` es la lápida.

Además, test.ping llega sólo cuando tocás «Enviar prueba». Un aviso sale, en general, uno o dos minutos después del cambio.

En el entorno de prueba sólo llega test.ping: la cuenta de prueba es una cadena de ficción con su historia ya cargada, y no tiene ventas nuevas. Para probar tu receptor con el cuerpo de una venta, armalo con el ejemplo de esta página o con un GET /sales/{id} de la cuenta de prueba, firmalo con tu secreto y mandátelo vos.

El cuerpo

Un JSON con el tipo, el momento y el objeto tal cual lo devuelve la API (una venta, un producto o una lápida):

POST a tu endpoint
{
  "type": "sale.closed",
  "timestamp": "2026-10-27T13:03:51.204Z",
  "data": {
    "id": "6720a3f1c9e2b4d7a1f3e8c2",
    "object": "sale",
    "status": "CLOSED",
    "total": 9810,
    "…": "la venta entera, como en GET /sales/{id}"
  }
}
Tomalo como un aviso con la foto del momento. Los avisos pueden llegar repetidos o en otro orden (un reintento de hace una hora puede llegar después de uno nuevo): antes de pisar lo que tenés, compará updated_at, o pedí el objeto por id.

La firma

Cada aviso sigue la especificación Standard Webhooks y trae tres headers:

HeaderQué es
webhook-idEl id del mensaje (msg_…). Es el mismo en cada reintento: deduplicá por él.
webhook-timestampLos segundos desde 1970 de este intento.
webhook-signaturev1, + el HMAC-SHA256 en base64 de <webhook-id>.<webhook-timestamp>.<cuerpo>, con la clave que sale de decodificar en base64 lo que va después de whsec_. Puede traer varias separadas por espacio.

Cómo se verifica

  1. Leé el cuerpo CRUDO, sin parsear: se firma byte por byte, y un JSON re-armado no coincide.
  2. Rechazá si webhook-timestamp está a más de 5 minutos de tu reloj.
  3. Calculá la firma esperada y comparala en tiempo constante con cada firma v1 del header.
  4. Si alguna coincide, es de Kicket. Si no, contestá 400 y no hagas nada.
Verificar la firma
import { createHmac, timingSafeEqual } from 'node:crypto'

// secreto: el whsec_… del endpoint. headers: los del pedido (en minúscula, como los da Node).
// cuerpo: el cuerpo CRUDO, tal cual llegó (antes de JSON.parse): se firma byte por byte.
export function verificarWebhook(secreto, headers, cuerpo) {
  const id = headers['webhook-id']
  const ts = headers['webhook-timestamp']
  const firmas = headers['webhook-signature']
  if (!id || !ts || !firmas || !/^\d+$/.test(ts)) return false
  // Más de 5 minutos de diferencia con tu reloj: se rechaza (frena que alguien reenvíe uno viejo).
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false
  const clave = Buffer.from(secreto.replace(/^whsec_/, ''), 'base64')
  const esperada = createHmac('sha256', clave).update(`${id}.${ts}.${cuerpo}`).digest()
  // Puede haber varias firmas separadas por espacio (mientras rotás el secreto): alcanza con que una coincida.
  return firmas.split(' ').some((parte) => {
    const [version, valor] = parte.split(',')
    if (version !== 'v1' || !valor) return false
    const recibida = Buffer.from(valor, 'base64')
    return recibida.length === esperada.length && timingSafeEqual(recibida, esperada)
  })
}

Un receptor completo:

Un receptor de webhooks
import { createServer } from 'node:http'
import { verificarWebhook } from './verificar.mjs'

const SECRETO = process.env.KICKET_WEBHOOK_SECRET // el whsec_… que te mostró el panel

createServer((req, res) => {
  const partes = []
  req.on('data', (p) => partes.push(p))
  req.on('end', () => {
    const cuerpo = Buffer.concat(partes).toString('utf8')
    if (req.method !== 'POST' || !verificarWebhook(SECRETO, req.headers, cuerpo)) {
      res.writeHead(400).end()
      return
    }
    // Contestá enseguida (menos de 10 s) y procesá después: si tardás, se cuenta como fallido y se reintenta.
    res.writeHead(200).end()
    const evento = JSON.parse(cuerpo)
    // Deduplicá por webhook-id: un reintento trae el mismo id.
    encolar(req.headers['webhook-id'], evento)
  })
}).listen(3000)
Si usás una librería de Standard Webhooks (por ejemplo standardwebhooks en npm o PyPI), también sirve: pasale el secreto tal cual, con su whsec_.

Rotar el secreto

Al rotar el secreto en el panel, durante 24 horas cada aviso va firmado con los dos (dos firmas en el header): tenés ese tiempo para cargar el nuevo sin perder ninguno.

Respuestas y reintentos

  • Contestá cualquier 2xx en menos de 10 segundos. Recibí, guardá y contestá; el trabajo pesado, después.
  • Cualquier otra cosa (otro status, una redirección, un timeout, un error de red) es un intento fallido. Se reintenta con estas esperas: 5 s, 5 min, 30 min, 2 h, 5 h, 10 h, 10 h (8 intentos en total, en poco más de un día). Si contestás 429 o 503 con Retry-After, se respeta si pide esperar más.
  • Después del último intento la entrega queda fallida; la podés reintentar a mano desde el panel.
  • Si contestás 410, el endpoint se desactiva (tu sistema dice que esa dirección ya no existe). Si falla 5 días seguidos, también se desactiva solo. Se reactiva desde el panel.
  • Como un aviso puede llegar más de una vez, deduplicá por webhook-id.
  • Un endpoint que falla o que tarda más de 5 segundos en contestar se frena hasta la vuelta siguiente (un minuto): lo suyo queda en la cola sin gastar intentos, y los avisos de los demás siguen saliendo. Contestar rápido es lo que hace que te lleguen todos al toque.
  • «Enviar prueba» y «Reintentar» tienen un tope de 10 por minuto por cuenta.

Webhooks y sincronización, juntos

Lo más robusto es usar los dos: el webhook te avisa al toque y la sincronización incremental cada tanto (por ejemplo, cada 15 minutos) levanta lo que un aviso perdido no trajo. Un webhook no reemplaza a la sincronización: si tu sistema estuvo caído más de un día, lo que falte lo trae updated_since.

AnteriorLímites de usoSiguienteConciliación