Webhooks
Eventos, formato de payload y verificación HMAC-SHA256 para webhooks de Tikra.
Tikra puede enviar eventos a tu sistema vía HTTP POST cuando algo cambia (pedidos, remitos, pagos). Reduce la necesidad de polling.
Setup
- Andá a
/portal/dashboard/api/webhookso usáPOST /api/v1/portal/webhooks. - Especificá:
- URL — debe ser HTTPS, accesible desde Internet, no IPs privadas.
- Eventos — qué eventos querés recibir (lista abajo).
- Vas a recibir un signing_secret UNA SOLA VEZ. Guardalo en un vault.
Eventos disponibles
| Evento | Cuándo |
|---|---|
order.created | Cliente crea pedido |
order.approved | Admin aprueba pedido |
order.rejected | Admin rechaza pedido |
order.in_preparation | Status pasa a en preparación |
order.in_transit | Status pasa a en tránsito |
order.delivered | Status pasa a entregado |
order.failed_delivery | Intento de entrega fallido |
order.cancelled | Cancelación |
remito.created | Nuevo remito (ingreso o egreso) |
remito.voided | Remito anulado |
payment.received | Pago registrado |
credit_note.issued | Nota de crédito emitida |
stock.changed | Stock de distribución de un producto cambia (cualquier causa) |
lot.created | Lote nuevo creado para distribución |
lot.depleted | Lote queda en 0 (transición positivo → 0) |
lot.expiring | Lote con expiry en próximos 30 días (cron diario 5am AR) |
Eventos de stock y lots (v3.36.0)
A partir de v3.36.0 el API emite 4 events adicionales para el lifecycle de stock y lotes de distribución.
stock.changed
Disparado cuando el stock total de distribución de un producto-cliente cambia (cualquier causa: pedido aprobado, remito ingreso/egreso, ajuste manual, CSV import).
{
"id": "evt_abc123...",
"type": "stock.changed",
"created_at": "2026-05-21T14:30:00Z",
"api_version": "v1",
"data": {
"object": {
"product_id": "uuid",
"old_quantity": 100,
"new_quantity": 87,
"delta": -13,
"source": null,
"updated_at": "2026-05-21T14:30:00Z"
}
}
}Notas: source queda null en v1; futuras versiones lo derivarán del stock_movement más reciente. Para obtener el detalle de qué lotes cambiaron, hacé GET /stock/lots?product_id={id} tras recibir el event.
lot.created
Disparado cuando se crea un lote nuevo en context distribución (remito ingreso, transferencia, ajuste).
{
"id": "evt_abc124...",
"type": "lot.created",
"created_at": "2026-05-21T14:30:00Z",
"api_version": "v1",
"data": {
"object": {
"lot_id": "uuid",
"product_id": "uuid",
"lot_number": "L-2026-001",
"quantity": 50,
"expiry_date": "2027-12-31",
"vintage_year": 2024,
"source_type": "remito_ingreso",
"created_at": "2026-05-21T14:30:00Z"
}
}
}lot.depleted
Disparado cuando un lote de distribución transiciona de quantity > 0 a quantity = 0.
{
"id": "evt_abc125...",
"type": "lot.depleted",
"created_at": "2026-05-21T14:30:00Z",
"api_version": "v1",
"data": {
"object": {
"lot_id": "uuid",
"product_id": "uuid",
"lot_number": "L-2026-001",
"expiry_date": "2027-12-31",
"vintage_year": 2024,
"depleted_at": "2026-05-21T14:30:00Z"
}
}
}Notas: Si el lote oscila (qty=0 → qty=5 → qty=0 por ajuste/void), se emite múltiples veces — refleja cada transición real.
lot.expiring
Cron diario (5:00 AM AR / 8:00 AM UTC) emite un event por cada lote de distribución con quantity > 0 y expiry_date en los próximos 30 días.
{
"id": "evt_abc126...",
"type": "lot.expiring",
"created_at": "2026-05-21T08:00:00Z",
"api_version": "v1",
"data": {
"object": {
"lot_id": "uuid",
"product_id": "uuid",
"lot_number": "L-2026-001",
"quantity": 30,
"expiry_date": "2026-06-15",
"vintage_year": 2024,
"days_until_expiry": 25
}
}
}Notas: Re-emite diariamente sin dedupe — usá lot_id + date (del campo created_at del envelope) para deduplicar si querés actuar 1 vez por lote por día.
Formato del payload
{
"id": "evt_01HX...",
"type": "order.approved",
"created_at": "2026-05-09T15:30:00Z",
"api_version": "v1",
"data": {
"object": { }
}
}Verificar la firma (HMAC-SHA256)
Cada request incluye el header Tikra-Signature: t=<unix_ts>,v1=<hmac_hex>. Verificá ANTES de procesar.
Node.js / TypeScript
import crypto from "crypto";
function verifyWebhook(body, signature, secret) {
const [tPart, v1Part] = signature.split(",");
const t = parseInt(tPart.split("=")[1], 10);
const expectedSig = v1Part.split("=")[1];
// Replay protection: tolerá ±5 min de skew
if (Math.abs(Date.now() / 1000 - t) > 300) {
throw new Error("Timestamp fuera de tolerancia (replay attack?)");
}
const computed = crypto.createHmac("sha256", secret)
.update(`${t}.${body}`).digest("hex");
if (!crypto.timingSafeEqual(
Buffer.from(computed),
Buffer.from(expectedSig),
)) {
throw new Error("Firma inválida");
}
}
// Express:
app.post("/webhooks/tikra", express.raw({ type: "application/json" }), (req, res) => {
const signature = req.headers["tikra-signature"];
const body = req.body.toString("utf8");
try {
verifyWebhook(body, signature, process.env.TIKRA_WEBHOOK_SECRET);
} catch (err) {
return res.status(401).send(err.message);
}
const event = JSON.parse(body);
console.log("Recibido:", event.type, event.id);
res.status(200).send("ok");
});Python
import hmac, hashlib, time
def verify_webhook(body: bytes, signature: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in signature.split(","))
t = int(parts["t"])
if abs(time.time() - t) > 300:
return False
expected = hmac.new(
secret.encode(),
f"{t}.{body.decode()}".encode(),
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(expected, parts["v1"])Comportamiento de retry
Si tu endpoint devuelve un código fuera de 2xx (o hace timeout >10s), Tikra reintenta.
| Intento | Tiempo desde el anterior | Total elapsed |
|---|---|---|
| 1 | 0s | 0s |
| 2 | 30s | 30s |
| 3 | 5m | ~5.5m |
| 4 | 30m | ~35m |
| 5 | 2h | ~2.5h |
| 6 | 6h | ~8.5h |
| 7 | 24h | ~33h |
Después del 7° intento sin éxito, el delivery queda como abandoned. Podés reintentarlo manualmente desde el dashboard.
Auto-pausa: después de 10 fallas consecutivas en un endpoint, Tikra lo pone en estado paused y te manda un email. Activalo de nuevo desde el dashboard cuando esté arreglado.
Idempotencia
El campo id (ej. evt_01HX...) es único por evento. Si lo recibís dos veces (porque tu endpoint timeouteó pero la entrega sí llegó), procesalo una sola vez:
const seen = new Set(); // o tu DB
if (seen.has(event.id)) return res.status(200).send("ya procesado");
seen.add(event.id);
// ... procesáTest desde el dashboard
En /portal/dashboard/api/webhooks/{id} hay un botón Test ping que dispara un evento mock con tipo webhook.test. El payload tiene la misma forma que un evento real:
{
"id": "evt_test_<uuid>",
"type": "webhook.test",
"created_at": "2026-05-09T15:30:00Z",
"api_version": "v1",
"data": {
"object": {
"message": "Test ping desde el dashboard de Tikra Portal API",
"endpoint_id": "<endpoint id>"
}
}
}Tu endpoint debería:
- Verificar la firma HMAC (igual que con eventos reales).
- Detectar
type === "webhook.test"y responder 200 OK sin procesar el payload como evento de negocio.
El dashboard muestra el HTTP status, la duración, y el body de la respuesta — útil para depurar.
Nota:
webhook.testno aparece en la lista de eventos subscribables porque no corresponde a ningún evento de negocio. Solo se puede disparar manualmente desde el dashboard.