Recibir los leads en tu CRM
Una suscripción, la firma verificada y los dos eventos que mueven un lead.
AutomotorasSolo en los workspaces de automotoras.
Un lead que entra por el sitio tiene que aparecer en el CRM sin que nadie copie y pegue. Esta receta lo resuelve con una suscripción a eventos y un endpoint tuyo. Nada de consultar cada cinco minutos ni de exportar planillas.
1. Una key para la integración
La suscripción tiene dueño, y el dueño decide qué datos trae cada entrega. Un evento llega con el recurso completo solo si su dueño puede leerlo. Así que la key necesita, además de los permisos de webhooks, el de lectura del recurso que vas a escuchar:
curl -X POST https://api.vitrinadev.com/api/v1/api-keys \
-H "Authorization: Bearer $VITRINA_ROOT_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "CRM leads del sitio",
"scopes": ["webhooks:read", "webhooks:write", "leads:read", "contacts:read"]
}'Sin leads:read, la suscripción se crea igual y las entregas llegan sin data, con "data_omitted": "missing_scope:leads:read". Es un modo de fallar silencioso: la integración parece andar y no trae nada.
2. La suscripción
curl -X POST https://api.vitrinadev.com/api/v1/webhooks \
-H "Authorization: Bearer $VITRINA_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://crm.tu-dominio.cl/vitrina",
"events": ["lead.created", "lead.stage_changed", "lead.won", "lead.lost"],
"description": "leads al CRM",
"include_data": true
}'La respuesta trae un secret que empieza con whsec_, una sola vez. Guárdalo: es con lo que vas a verificar que un POST que dice venir de Vitrina viene de Vitrina. Si lo pierdes, borra la suscripción y crea otra.
La url tiene que ser pública. Una dirección privada o localhost se rechaza al crear la suscripción, antes de cualquier entrega. Mientras desarrollas, expón tu receptor con un túnel.
3. Verifica la firma antes de escribir nada
Cada entrega trae X-Webhook-Signature: t=<unix>,v1=<hex>, un HMAC-SHA256 sobre ${timestamp}.${cuerpo crudo}. El verificador completo, en JavaScript y en Python, con un ejemplo resuelto que puedes reproducir, está en Empezar.
Dos reglas que valen más que el código:
- Firma los bytes que recibiste, nunca una reserialización. Si tu framework te entrega el cuerpo ya parseado, los bytes originales se perdieron y toda firma va a fallar.
- Revisa el timestamp. La ventana es de 300 segundos. Sin ella, quien haya visto una entrega puede reenviarla cuando quiera.
4. El evento que crea el lead
Una consulta del sitio dispara lead.created y el POST sale de inmediato. El registro de entregas guarda el body exacto que salió, así que sirve para ver qué recibe tu endpoint:
curl "https://api.vitrinadev.com/api/v1/webhooks/02ee9a15-a743-41ff-a343-cfea200bcfa1/deliveries?limit=1" \
-H "Authorization: Bearer $VITRINA_KEY"{
"data": [
{
"id": 846,
"subscription_id": "02ee9a15-a743-41ff-a343-cfea200bcfa1",
"event": "lead.created",
"event_id": "c5cbbd3d-8b39-4d00-aa35-fe9daf98399c",
"attempt": 1,
"status_code": 200,
"error": null,
"response_excerpt": "",
"request_payload": {
"id": "c5cbbd3d-8b39-4d00-aa35-fe9daf98399c",
"data": {
"title": "Suzuki Swift 2021, consulta del sitio",
"intent": "buy",
"source": "website",
"lead_id": "9f52c753-a6cb-4d03-9837-6a1601fd8874",
"team_id": null,
"stage_id": "00000000-0000-4000-8000-000000004101",
"contact_id": "01a0cbfe-0e3a-7960-9901-ad153b1db135",
"pipeline_id": "00000000-0000-4000-8000-000000004000",
"value_amount": null,
"owner_user_id": null,
"value_currency": "CLP"
},
"type": "lead.created",
"author": {
"id": "0eb8e217-8a6f-4cb8-8216-b077194b3df7",
"kind": "api_key",
"name": "CRM leads del sitio"
},
"version": 1,
"resource": {
"id": "9f52c753-a6cb-4d03-9837-6a1601fd8874",
"url": "https://api.vitrinadev.com/api/v1/leads/9f52c753-a6cb-4d03-9837-6a1601fd8874",
"type": "lead"
},
"tenant_id": "00000000-0000-4000-8000-000000000001",
"created_at": "2026-09-23T02:00:19.841Z"
},
"latency_ms": 613,
"created_at": "2026-09-23T02:00:20.739063+00:00",
"redelivery_of": null
}
],
"meta": {
"pagination": {
"limit": 1
},
"offset": 0
}
}request_payload es el body completo, tal como viajó. source dice por qué puerta entró el lead: website, chileautos, yapo, mercadolibre, conversation. Con ese campo la automotora responde «¿de dónde vienen los leads que cierran?». intent distingue a quien quiere comprar de quien quiere vender o financiar.
El evento no trae los datos del contacto, solo su contact_id. Cada recurso viaja en su propio evento, detrás de su propio permiso. Para el nombre y el teléfono, lee el contacto con tu key:
curl https://api.vitrinadev.com/api/v1/contacts/01a0cbfe-0e3a-7960-9901-ad153b1db135 \
-H "Authorization: Bearer $VITRINA_KEY"Trampa
Trata el id del evento como llave de idempotencia
Reintentamos hasta cinco veces con espera creciente, y un reenvío manual repite
la entrega. Tu endpoint va a ver el mismo evento más de una vez. id es único
por evento: guárdalo y descarta el que ya procesaste. Responder 200 rápido y
procesar después es la otra mitad. Si tardas, cuenta como falla y vuelve a
llegar.
5. El evento que lo mueve
Cuando alguien mueve el lead de etapa en la aplicación:
{
"data": [
{
"id": 855,
"subscription_id": "02ee9a15-a743-41ff-a343-cfea200bcfa1",
"event": "lead.stage_changed",
"event_id": "11ff3b38-4465-47d5-844d-c5ab2ecb30e0",
"attempt": 1,
"status_code": 200,
"error": null,
"response_excerpt": "",
"request_payload": {
"id": "11ff3b38-4465-47d5-844d-c5ab2ecb30e0",
"data": {
"reason": null,
"lead_id": "9f52c753-a6cb-4d03-9837-6a1601fd8874",
"to_stage_id": "00000000-0000-4000-8000-000000004102",
"from_stage_id": "00000000-0000-4000-8000-000000004101",
"to_stage_slug": "contacted"
},
"type": "lead.stage_changed",
"author": {
"id": "0eb8e217-8a6f-4cb8-8216-b077194b3df7",
"kind": "api_key",
"name": "CRM leads del sitio"
},
"changes": {
"stage": {
"to": "00000000-0000-4000-8000-000000004102",
"from": "00000000-0000-4000-8000-000000004101"
}
},
"version": 1,
"resource": {
"id": "9f52c753-a6cb-4d03-9837-6a1601fd8874",
"url": "https://api.vitrinadev.com/api/v1/leads/9f52c753-a6cb-4d03-9837-6a1601fd8874",
"type": "lead"
},
"tenant_id": "00000000-0000-4000-8000-000000000001",
"created_at": "2026-09-23T02:00:36.146Z"
},
"latency_ms": 622,
"created_at": "2026-09-23T02:00:40.783945+00:00",
"redelivery_of": null
}
],
"meta": {
"pagination": {
"limit": 1
},
"offset": 0
}
}changes trae el from y el to en el sobre, antes de data, así que puedes decidir qué hacer sin leer nada más. Y to_stage_slug es el nombre estable de la etapa: la automotora puede renombrar «Contactado» en la aplicación sin romperte el mapeo.
lead.won y lead.lost cierran el ciclo, y son los que tu CRM quiere para reportar.
6. Cuando «no llegan los webhooks»
Antes de escribirnos, mira el mismo registro de entregas. Cada intento deja una fila, incluidos los que fallaron, con su status_code, su error y su attempt.
- Hay filas con
status_codeen 500 o ennull→ el problema está en tu endpoint, y nosotros reintentamos.erroryresponse_excerptdicen qué contestó. - No hay ninguna fila → el evento nunca se disparó, o tu suscripción no lo incluye. Revisa
events. - La suscripción tiene
paused_at→ fallaron demasiadas entregas seguidas y la pausamos sola para no seguir golpeando un endpoint caído.
Los dos modos de entrega, los reintentos, la pausa automática y el reenvío están en Webhooks. Cada evento con sus campos exactos está en el catálogo, y el lote que produjo esta consulta, en Mostrar tu stock en tu sitio web.