TochiHubDesarrolladores
Ir a TochiHub

Webhooks

Recibe publicaciones y leads en tu sistema.

Configura URL y eventos en Configuración → Integraciones. Solo OWNER/ADMIN de la corredora pueden gestionarlos. Copia el secreto al crear o rotar. El listado solo muestra una vista enmascarada. Al rotar, las entregas incluyen firmas del secreto nuevo y del anterior durante 24 horas. Headers: X-TochiHub-Signature: v1=<hex HMAC-SHA256>, X-TochiHub-Timestamp: <epoch segundos>, X-TochiHub-Event y X-TochiHub-Delivery. Firma exactamente timestamp + "." + body crudo en UTF-8. Nunca reserialices JSON antes de verificar. Durante la rotación puede haber varias firmas v1 separadas por coma: basta una válida. Rechaza timestamps fuera de una tolerancia de 300 segundos y compara en tiempo constante. Body: { id, event, livemode, createdAt, data }. id es el identificador estable de entrega; el secreto nunca viaja en el body. Entrega al menos una vez: deduplica por id. Persiste el evento antes de responder 2xx y procesa tu lógica después. Timeout 10 segundos, hasta 5 intentos totales, backoff exponencial desde 30 segundos (30, 60, 120, 240). Un timeout o cualquier respuesta fuera de 2xx cuenta como fallo. Cada intento firma el mismo evento con un timestamp nuevo. El historial de entregas está en el admin y en get_integration_status. Enviar prueba usa datos ficticios y livemode: false; también puedes probar publicaciones completas con una key TEST.

Verificar en Node.js

Ejemplo con Express. Define persistOnce(id, event) en tu almacenamiento: una inserción durable con id único debe ignorar duplicados. Si falla la persistencia, responde 5xx. Configura TOCHIHUB_WEBHOOK_SECRET en tu receptor, sin imprimirlo.

javascript
import express from 'express';
import { createHmac, timingSafeEqual } from 'node:crypto';

const app = express();
const secret = process.env.TOCHIHUB_WEBHOOK_SECRET;
if (!secret) throw new Error('Falta TOCHIHUB_WEBHOOK_SECRET');

app.post('/webhooks/tochihub', express.raw({ type: 'application/json' }), async (req, res) => {
  const timestampText = req.get('X-TochiHub-Timestamp') ?? '';
  const timestamp = Number(timestampText);
  const signatures = (req.get('X-TochiHub-Signature') ?? '').split(',');
  const fresh = /^\d+$/.test(timestampText) &&
    Number.isSafeInteger(timestamp) && Math.abs(Date.now() / 1000 - timestamp) <= 300;
  const expected = createHmac('sha256', secret)
    .update(timestampText + '.').update(req.body).digest();
  const valid = fresh && signatures.some((part) => {
    const match = /^v1=([a-f0-9]{64})$/.exec(part.trim());
    return match !== null && timingSafeEqual(Buffer.from(match[1], 'hex'), expected);
  });
  if (!valid) return res.sendStatus(401);
  const event = JSON.parse(req.body.toString('utf8'));
  await persistOnce(event.id, event);
  res.sendStatus(204);
});

app.listen(8080);

LEAD_CREATED

Llegó un lead nuevo (desde un portal, el sitio o cargado a mano) asociado a una propiedad.

json
{
  "id": "delivery-ejemplo",
  "event": "LEAD_CREATED",
  "livemode": false,
  "createdAt": "2026-10-05T14:03:00.000Z",
  "data": {
    "leadId": "cm1lead0000000000000000",
    "propertyId": "cm1prop0000000000000000",
    "channel": "PORTAL_CONTACT",
    "contactName": "Camila Rojas",
    "contactPhone": "+56912345678",
    "contactEmail": "camila@example.com",
    "message": "Hola, ¿sigue disponible el departamento?",
    "createdAt": "2026-10-05T14:03:00.000Z"
  }
}

PROPERTY_CREATED

Se creó una propiedad (en el panel, por WhatsApp o por la API).

json
{
  "id": "delivery-ejemplo",
  "event": "PROPERTY_CREATED",
  "livemode": false,
  "createdAt": "2026-10-05T14:03:00.000Z",
  "data": {
    "propertyId": "cm1prop0000000000000000",
    "externalId": "crm-prop-12345",
    "title": "Departamento 2D2B en Providencia",
    "operationType": "SALE",
    "status": "DRAFT",
    "createdAt": "2026-10-05T14:03:00.000Z"
  }
}

PROPERTY_PUBLICATION_COMPLETED

Terminó una publicación, actualización o baja en un portal (con éxito o con error definitivo; nunca en un reintento).

json
{
  "id": "delivery-ejemplo",
  "event": "PROPERTY_PUBLICATION_COMPLETED",
  "livemode": false,
  "createdAt": "2026-10-05T14:03:00.000Z",
  "data": {
    "propertyId": "cm1prop0000000000000000",
    "externalId": "crm-prop-12345",
    "portalCode": "YAPO",
    "action": "PUBLISH",
    "succeeded": false,
    "errorMessage": "Yapo requires the built area (square, m²) on the property.",
    "errorDescription": "Falta la superficie construida (m²)."
  }
}

PROPERTY_UPDATED

Se editó una propiedad o su galería (en el panel o por la API). Trae el estado actual; consulta GET /v1/properties/{id} para el detalle.

json
{
  "id": "delivery-ejemplo",
  "event": "PROPERTY_UPDATED",
  "livemode": false,
  "createdAt": "2026-10-05T14:03:00.000Z",
  "data": {
    "propertyId": "cm1prop0000000000000000",
    "externalId": "crm-prop-12345",
    "status": "PUBLISHED",
    "updatedAt": "2026-10-05T14:03:00.000Z"
  }
}

PROPERTY_STATUS_CHANGED

Una propiedad se pausó, se reanudó o se archivó (en el panel o por la API).

json
{
  "id": "delivery-ejemplo",
  "event": "PROPERTY_STATUS_CHANGED",
  "livemode": false,
  "createdAt": "2026-10-05T14:03:00.000Z",
  "data": {
    "propertyId": "cm1prop0000000000000000",
    "externalId": "crm-prop-12345",
    "previousStatus": "PUBLISHED",
    "status": "PAUSED",
    "changedAt": "2026-10-05T14:03:00.000Z"
  }
}

INBOX_MESSAGE_RECEIVED

Entró un mensaje nuevo a la bandeja de entrada.

json
{
  "id": "delivery-ejemplo",
  "event": "INBOX_MESSAGE_RECEIVED",
  "livemode": false,
  "createdAt": "2026-10-05T14:03:00.000Z",
  "data": {
    "conversationId": "cm1conv0000000000000000",
    "messageId": "cm1msg00000000000000000",
    "channel": "WHATSAPP",
    "contactName": "Camila Rojas",
    "contactPhone": "+56912345678",
    "body": "¿Puedo visitar el depto el sábado?"
  }
}

TICKET_CREATED

Se creó un ticket de seguimiento (manual o porque el asistente derivó la conversación).

json
{
  "id": "delivery-ejemplo",
  "event": "TICKET_CREATED",
  "livemode": false,
  "createdAt": "2026-10-05T14:03:00.000Z",
  "data": {
    "ticketId": "cm1tick0000000000000000",
    "title": "Cliente pide visita el sábado",
    "priority": "MEDIUM",
    "source": "BOT_FALLBACK",
    "conversationId": "cm1conv0000000000000000"
  }
}

Buscar en la documentación