Un formulario de contacto que manda un email a alguien que está de vacaciones es el cementerio de los leads. Con Diil, cada formulario de tu web llega como una tarjeta al CRM: con teléfono o email, un estado y alguien responsable de devolver la llamada. Basta con un atributo en un <form>; si necesitas más control, tienes JavaScript y el servidor.
Tres formas de enviar un lead; elige la que mejor te encaje:
- Un formulario con
data-crm-lead: cero JavaScript. El widget intercepta el envío y se encarga del resto. window.crmLead(): para React, Vue y cualquier formulario que controles tú.POST /marketing/leaddesde tu servidor: pedidos por teléfono, formularios procesados en el backend, integraciones con otros sistemas.
Cómo llega un lead de tu formulario al CRM
En Diil un lead es un tipo especial de evento. Cada evento tiene un nombre (por ejemplo, contact_form) y ese nombre hay que declararlo antes en el CRM. Marca en él “Esto es un lead” y cada envío con ese nombre se convertirá en una tarjeta en CRM → Leads.
- Un visitante rellena el formulario y pulsa “Enviar”.
- El widget recoge los campos, averigua quién es la persona (nombre, teléfono, email, mensaje) y lo envía.
- El servidor comprueba que el nombre está declarado, que el lead trae teléfono o email y que no tiene pinta de bot.
- Aparece una tarjeta de lead en el CRM y, si lo has configurado, una notificación en Telegram.
Dos reglas que conviene tener claras desde el principio:
- Un nombre no declarado se rechaza. El CRM solo acepta lo que conoce: nada de basura sorpresa por culpa de una errata.
- ¿Declarado, pero sin marcar como lead? Entonces se guarda como un evento normal y no se crea ninguna tarjeta de lead.
Configura un tipo de lead en el CRM
Dos minutos de clics, una vez por cada tipo de formulario:
Abre Marketing → Eventos
Aquí se describen todos los eventos que puede enviar tu web.Crea un evento
Ponle el nombre que usará tu código: letras latinas, dígitos y guiones bajos, empezando por una letra:contact_form,callback_request,quote_request.Marca “Esto es un lead”
A partir de ahora los envíos se convierten en tarjetas de lead. Como dice la pista del CRM, el formulario debe enviar una vía de respuesta: un teléfono o un email.Opcional: marca “Este evento genera ingresos”
Solo así se guardan el importe y la moneda del lead. Sin esta casilla se ignoran sin decir nada.
Opción A: un formulario con un atributo
Añade data-crm-lead="nombre_de_tu_evento" a cualquier formulario de una página que tenga el script del widget. Esa es toda la integración. Los formularios que aparecen más tarde (en un modal, tras un cambio de ruta en una SPA) se detectan solos.
Ejemplo completo
Un formulario de “pide presupuesto” con nombre, teléfono, email y mensaje, un importe para el informe de ingresos, un mensaje de “gracias” y una validación propia. Primero HTML puro; las versiones de React y Vue llaman a crmLead() desde su propio manejador de envío.
<form id="quote" data-crm-lead="quote_request" data-crm-value="149900" data-crm-currency="EUR">
<label>Tu nombre <input name="name" autocomplete="name" required></label>
<label>Teléfono <input name="phone" type="tel" autocomplete="tel"></label>
<label>Email <input name="email" type="email" autocomplete="email"></label>
<label>¿Qué necesitas? <textarea name="message" rows="4"></textarea></label>
<!-- No es uno de los campos conocidos: aparece tal cual en la tarjeta del lead -->
<label>Tamaño del equipo
<select name="team_size">
<option>1–5</option>
<option>6–20</option>
<option>20+</option>
</select>
</label>
<button type="submit">Enviar solicitud</button>
<p class="form-status" role="status" hidden></p>
</form>
<script>
const form = document.getElementById('quote');
const status = form.querySelector('.form-status');
const button = form.querySelector('button');
function say(text) {
status.textContent = text;
status.hidden = false;
}
// 1. Antes de enviar: nuestra propia validación. preventDefault() = no se envía nada
form.addEventListener('crm:lead-before', (event) => {
const phone = form.elements.phone.value.trim();
const email = form.elements.email.value.trim();
if (!phone && !email) {
event.preventDefault();
say('Déjanos un teléfono o un email para poder responderte.');
return;
}
button.disabled = true;
});
// 2. Después de enviar: el widget no muestra nada, el "gracias" es cosa nuestra
form.addEventListener('crm:lead', (event) => {
button.disabled = false;
say(event.detail.ok
? '¡Gracias! Te responderemos en un día laborable.'
: 'Algo ha fallado. Inténtalo de nuevo o llámanos.');
});
</script>
<script src="https://widget.sitecog.com/widget.js" defer></script>'use client';
import { useState, type FormEvent } from 'react';
const EMPTY = { name: '', phone: '', email: '', message: '' };
export function QuoteForm() {
const [form, setForm] = useState(EMPTY);
const [status, setStatus] = useState<'idle' | 'sending' | 'done' | 'error'>('idle');
const set = (key: keyof typeof EMPTY) => (e: { target: { value: string } }) =>
setForm((prev) => ({ ...prev, [key]: e.target.value }));
async function onSubmit(e: FormEvent) {
e.preventDefault();
if (!form.phone.trim() && !form.email.trim()) {
setStatus('error');
return;
}
setStatus('sending');
// crmLead nunca lanza excepciones; solo es undefined si widget.js no está en la página
const res = await window.crmLead?.('quote_request', form, { value: 149900, currency: 'EUR' });
if (res?.ok) {
setForm(EMPTY);
setStatus('done');
} else {
setStatus('error');
}
}
if (status === 'done') return <p>¡Gracias! Te responderemos en un día laborable.</p>;
// Aquí no hay data-crm-lead: este formulario envía con crmLead por su cuenta
return (
<form onSubmit={onSubmit}>
<input value={form.name} onChange={set('name')} placeholder="Tu nombre" required />
<input value={form.phone} onChange={set('phone')} type="tel" placeholder="Teléfono" />
<input value={form.email} onChange={set('email')} type="email" placeholder="Email" />
<textarea value={form.message} onChange={set('message')} placeholder="¿Qué necesitas?" />
<button disabled={status === 'sending'}>Enviar solicitud</button>
{status === 'error' && <p role="alert">Déjanos un teléfono o un email y vuelve a intentarlo.</p>}
</form>
);
}<script setup lang="ts">
import { reactive, ref } from 'vue';
const empty = () => ({ name: '', phone: '', email: '', message: '' });
const form = reactive(empty());
const status = ref<'idle' | 'sending' | 'done' | 'error'>('idle');
async function submit() {
if (!form.phone.trim() && !form.email.trim()) {
status.value = 'error';
return;
}
status.value = 'sending';
// crmLead nunca lanza excepciones; solo es undefined si widget.js no está en la página
const res = await window.crmLead?.('quote_request', { ...form }, { value: 149900, currency: 'EUR' });
if (res?.ok) {
Object.assign(form, empty());
status.value = 'done';
} else {
status.value = 'error';
}
}
</script>
<template>
<p v-if="status === 'done'">¡Gracias! Te responderemos en un día laborable.</p>
<!-- Aquí no hay data-crm-lead: este formulario envía con crmLead por su cuenta -->
<form v-else @submit.prevent="submit">
<input v-model="form.name" placeholder="Tu nombre" required />
<input v-model="form.phone" type="tel" placeholder="Teléfono" />
<input v-model="form.email" type="email" placeholder="Email" />
<textarea v-model="form.message" placeholder="¿Qué necesitas?" />
<button :disabled="status === 'sending'">Enviar solicitud</button>
<p v-if="status === 'error'" role="alert">Déjanos un teléfono o un email y vuelve a intentarlo.</p>
</form>
</template>Qué pasa cuando se envía el formulario
- El widget siempre cancela el envío nativo: la página no se recarga y el
actiondel formulario no se usa. - Lanza
crm:lead-beforesobre el formulario. Si algún listener llama aevent.preventDefault(), la historia acaba aquí y no se envía nada. - Recoge los campos con
FormData, salvo los campos excluidos (contraseñas, datos de tarjeta y compañía). Solo cuentan los valores de texto: los campos de archivo se ignoran. Varios campos con el mismo nombre (un grupo de checkboxes) se unen con", ". - Envía el lead, protegido con un pase de formulario de un solo uso (más sobre esto en protección contra spam).
- Lanza
crm:leadcon{ ok: true }o{ ok: false }enevent.detail. - Si
okes true, llama aform.reset(). Si falla, el formulario se queda como está para que el visitante no pierda lo que ha escrito.
El widget no muestra ninguna interfaz propia: ni toast, ni spinner, ni “gracias”. Tu web, tu diseño, tus palabras: escucha crm:lead y enseña lo que quieras.
Atributos del formulario
| Atributo | Ejemplo | Qué hace |
|---|---|---|
data-crm-lead | "quote_request" | Convierte el formulario en un formulario de leads. El valor es el nombre del evento declarado en el CRM. Un atributo vacío equivale al nombre lead_submit, que también hay que declarar. |
data-crm-value | "149900" | Importe en unidades menores, un entero: 149900 son 1.499,00. Solo se guarda si el evento tiene “Este evento genera ingresos”. |
data-crm-currency | "EUR" | Moneda del importe: EUR, USD, UAH… |
data-crm-ignore | sin valor | Va en un campo o en un contenedor (fieldset, div…), no en el propio formulario. Ese campo, o todo lo que haya dentro del contenedor, nunca se envía. Mira Campos que nunca se envían. |
Eventos del formulario
Los dos eventos hacen bubbling, así que puedes escucharlos en el propio formulario o una sola vez en document para todos los formularios de la página.
| Evento | Cuándo | event.detail | Cancelable |
|---|---|---|---|
crm:lead-before | Justo después del submit, antes de enviar nada | { name }: el nombre del evento | Sí: preventDefault() detiene el envío |
crm:lead | Cuando el servidor ha respondido | { ok }: true si el lead se ha aceptado | No |
document.addEventListener('crm:lead', (event) => {
if (event.detail.ok) {
event.target.closest('.modal')?.classList.add('is-thanks');
}
});Nombres de campo que entiende el CRM
Una tarjeta de lead tiene cuatro huecos principales: nombre, teléfono, email y mensaje. El widget los rellena fijándose en los nombres de los campos. Los nombres se recortan y se pasan a minúsculas, y luego se comparan de forma exacta con esta lista (así que Phone funciona y your-phone no):
| Hueco | Nombres de campo que lo rellenan | Longitud máx. |
|---|---|---|
| Nombre | name, fio, username, user_name, fullname, full_name, firstname, first_name, contact_name, client_name, имя, фио, ім'я | 200 |
| Teléfono | phone, tel, telephone, mobile, phone_number, contact_phone, телефон, тел | 40 |
email, e_mail, e-mail, mail, contact_email, почта, пошта, емейл | 320 | |
| Mensaje | message, comment, comments, text, question, note, description, task, сообщение, комментарий, вопрос, повідомлення, коментар | 4000 |
- Gana la primera coincidencia. Si un formulario tiene
phoneymobile, el hueco lo rellena el que aparezca primero. - Todo lo demás también se guarda. Los campos desconocidos y las segundas coincidencias van a “extra” y aparecen en la tarjeta del lead en el orden del formulario. Hasta 30 campos extra; claves de hasta 60 caracteres, valores de hasta 1000.
- Un teléfono solo cuenta con entre 7 y 20 dígitos. Espacios, paréntesis y guiones no molestan; un
+inicial se conserva. - Teléfono o email, sí o sí. Un lead sin ninguno de los dos se rechaza con
no_contact: no habría forma de responder.
Campos que nunca se envían
Un formulario de leads no es sitio para contraseñas ni números de tarjeta, y el widget se encarga de que no acaben en el CRM aunque alguien los meta en el mismo <form> por despiste. Estos campos se saltan siempre:
- Los
<input type="password">, se llamen como se llamen. - Todo lo marcado con
data-crm-ignore: el atributo puede ir en el propio campo o en cualquier contenedor (fieldset,div…), y entonces se salta todo lo que haya dentro. - Los campos con un nombre sensible. El nombre se trocea en palabras (por
_,-,., camelCase…) y se excluye si alguna de ellas, como palabra suelta, escard,cc,csc,cvv,cvc,iban,ssn,secretopass. Además, basta con que el nombre contenga en cualquier sitiopassword,passwd,pwd,token,csrf,xsrf,creditcard,cardnumberoccnum.
Unos ejemplos: card_number, cvv, csrf_token y user_password no se envían; discard_reason sí, porque ahí card no es una palabra suelta. Y si un campo con cierto nombre queda excluido, quedan excluidos todos los campos con ese mismo nombre.
Los campos ocultos (type="hidden") sí se envían, siempre que su nombre no sea sensible: son perfectos para pasar el producto, el plan o la página desde la que se escribió.
<form data-crm-lead="signup_request">
<input name="email" type="email"> <!-- ✓ se envía -->
<input name="plan" type="hidden" value="pro"> <!-- ✓ oculto, pero se envía -->
<input name="password" type="password"> <!-- ✗ type="password" -->
<input name="card_number"> <!-- ✗ nombre sensible -->
<fieldset data-crm-ignore> <!-- ✗ nada de lo que hay dentro -->
<input name="billing_address">
<input name="billing_zip">
</fieldset>
<button type="submit">Empezar</button>
</form>crmLead() aplica la misma regla de nombres a las claves de fields. Y el servidor vuelve a pasar el mismo filtro por su cuenta, también en POST /lead con clave secreta: un campo con nombre sensible nunca acaba en el CRM, venga de donde venga.
Opción B: envía leads desde JavaScript
Cuando el estado del formulario es tuyo (React, Vue, un asistente de varios pasos, un chatbot), llama al widget directamente. Las dos funciones aparecen en window en cuanto se carga widget.js.
crmLead(name, fields, options)
const { ok } = await window.crmLead(
'callback_request',
{ name: 'Anna', phone: '+49 30 1234567', message: 'Llamadme después de las 17:00, por favor' },
{ value: 149900, currency: 'EUR' },
);namestringobligatorio^[A-Za-z][A-Za-z0-9_]*$). Un nombre que no encaja recibe un aviso en la consola y { ok: false }.fieldsobjectobligatoriopassword, card_number…) se descartan, igual que en los formularios.options.valueintegeropcional149900 = 1.499,00. Solo se guarda en eventos con “Este evento genera ingresos”.options.currencystringopcionalEUR.Devuelve una Promise que se resuelve en:
okbooleantrue: el lead se ha aceptado. false: se ha rechazado o no ha llegado; el motivo no se indica, a propósito (mira por qué).crmLeadForm(form, name)
Hace exactamente lo mismo que el atributo data-crm-lead, pero desde código. Viene bien cuando el formulario lo renderiza una librería de terceros y no puedes añadirle atributos, o cuando el nombre se decide en tiempo de ejecución. No devuelve nada; el resultado llega por el mismo evento crm:lead.
const form = document.querySelector('#newsletter-popup form');
window.crmLeadForm(form, 'newsletter_signup');
form.addEventListener('crm:lead', (e) => {
if (e.detail.ok) form.innerHTML = '<p>¡Ya estás dentro! Revisa tu bandeja de entrada.</p>';
});formHTMLFormElementobligatorionamestringobligatoriocrmLead.Declaraciones de TypeScript
El widget es un script normal, así que TypeScript no sabe nada de él. Pega esto en cualquier archivo .d.ts:
export {};
declare global {
interface Window {
crmLead?: (
name: string,
fields: Record<string, string>,
options?: { value?: number; currency?: string },
) => Promise<{ ok: boolean }>;
crmLeadForm?: (form: HTMLFormElement, name: string) => void;
}
}Cómo funciona la protección contra spam
Un formulario público es un imán para bots. No necesitas un CAPTCHA para mantenerlos fuera: el widget y el servidor se encargan juntos, y los visitantes reales ni se enteran.
Un pase de formulario de un solo uso
Antes de enviar, el widget obtiene del servidor un “pase de formulario” firmado. Para ganar tiempo, lo pide en cuanto el visitante pone el foco en el formulario por primera vez. Un pase es:
- de un solo uso: un pase, un lead;
- ligado a tu web: un pase de un sitio no sirve en otro;
- válido durante 24 horas.
Los bots que hacen POST a ciegas contra el endpoint no tienen pase y se rechazan. Y un doble clic o un reenvío con el mismo pase devuelve { ok: true }, pero el lead se guarda una sola vez: nada de tarjetas duplicadas por culpa de dedos impacientes.
El honeypot
Es el campo oculto company_site de antes. Una persona no lo ve, así que se queda vacío. Un bot que rellena todos los campos que encuentra se delata solo, y el lead se rechaza.
Leads “sospechosos”
Algunos leads tienen algo raro pero podrían ser reales. Esos se aceptan y reciben la etiqueta Sospechoso en el CRM, para que un gestor les eche un vistazo antes de llamar:
- el formulario se rellenó en menos de 3 segundos, más rápido de lo humanamente posible;
- el formulario estuvo abierto más de 30 minutos antes de enviarse;
- llegaron más de 5 leads desde una misma IP en una hora.
La IP que ves en la tarjeta del lead está recortada (en IPv4 el último octeto pasa a cero: 203.0.113.57 → 203.0.113.0; en IPv6 se corta a /48), según las mismas reglas de qué guardamos y durante cuánto.
“Spam” es un estado que una persona pone a mano en el CRM. Nada se marca como spam automáticamente, así que ningún cliente real acaba en la papelera sin que nadie se entere.
Por qué el navegador solo recibe ok: true o false
Decirle a un bot “rechazado: honeypot relleno” es darle una clase gratis de cómo colarse. Por eso, desde el navegador, todos los rechazos se ven iguales: { ok: false }, sin motivo. ¿Depurando tu propio formulario? La lista de comprobación cubre todos los casos, y la vía del servidor sí te dice qué ha fallado, porque está protegida con una clave secreta.
Por dentro: la petición del navegador
Para curiosos: nunca tendrás que montarla tú. El widget obtiene un pase de POST https://back.sitecog.com/marketing/fk (respuesta: {"pass":"…"}) y luego envía el lead:
POST https://back.sitecog.com/marketing/f
Content-Type: application/json
{
"n": "contact_form",
"pass": "…",
"fields": {
"name": "Anna",
"email": "anna@example.com",
"message": "Hi!",
"company_site": ""
},
"val": 149900,
"cur": "EUR",
"u": "https://shop.example/contacts",
"vid": "…",
"sid": "…"
}La respuesta es { "ok": true } o { "ok": false }. vid y sid son los ids del visitante y de la sesión, para que el lead quede vinculado al canal publicitario por el que llegó el visitante: mira Eventos y analítica.
Envía leads desde tu servidor
No todos los leads nacen en un navegador. Usa la vía del servidor cuando:
- un gestor toma un pedido por teléfono y lo mete en tu back office;
- tu formulario se procesa en el backend (un handler en PHP, una server action de Next.js) y prefieres no depender del widget;
- los leads llegan de otro sistema: un marketplace, un servicio de reservas, un bot.
POST https://back.sitecog.com/marketing/lead
x-event-key: sk_…
Content-Type: application/jsonClaves secretas
La vía del servidor se autentica con una clave secreta. Créala en CRM → Marketing → Eventos → Claves secretas → Emitir una clave. Una clave tiene la forma sk_ + 48 caracteres hexadecimales; un sitio puede tener hasta 5 claves activas y cualquiera de ellas se puede revocar. Ponle a cada clave un nombre que diga dónde vive (“servidor de pagos”, “pedidos por teléfono”): tu yo del futuro te lo agradecerá cuando toque revocar una.
Ejemplo de petición
curl https://back.sitecog.com/marketing/lead \
-H "x-event-key: sk_3f9a1c7e5b2d4f6a8c0e1b3d5f7a9c2e4b6d8f0a1c3e5b7d" \
-H "Content-Type: application/json" \
-d '{
"name": "phone_order",
"event_id": "order-1024",
"fields": {
"name": "Anna Schmidt",
"phone": "+49 30 1234567",
"message": "Two pairs of Air 3, graphite. Delivery to the office."
},
"value": 149900,
"currency": "EUR",
"url": "https://shop.example/contacts"
}'// Node 18+: fetch viene de serie
const res = await fetch('https://back.sitecog.com/marketing/lead', {
method: 'POST',
headers: {
'x-event-key': process.env.CRM_EVENT_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
name: 'phone_order',
event_id: `order-${order.id}`, // un reintento no creará un segundo lead
visitor_id: order.crmVid, // guardado desde el navegador al pagar, si lo tienes
session_id: order.crmSid,
fields: {
name: order.customerName,
phone: order.phone,
message: order.comment,
delivery: order.deliveryMethod, // va a "extra"
},
value: order.totalCents, // 149900 = 1.499,00
currency: 'EUR',
}),
});
const data = await res.json();
if (!res.ok) throw new Error(`CRM lead: ${res.status} ${data.message}`);
// data → { ok: true, duplicate: false }import os
import requests
res = requests.post(
"https://back.sitecog.com/marketing/lead",
headers={"x-event-key": os.environ["CRM_EVENT_KEY"]},
json={
"name": "phone_order",
"event_id": "order-1024",
"fields": {
"name": "Anna Schmidt",
"phone": "+49 30 1234567",
"message": "Two pairs of Air 3, graphite.",
},
"value": 149900,
"currency": "EUR",
},
timeout=10,
)
res.raise_for_status()
print(res.json()) # {'ok': True, 'duplicate': False}Cuerpo de la petición
x-event-keyheaderstringobligatoriosk_… del CRM. Si falta o está revocada → 401 invalid_key.namestringobligatorion.fieldsobjectobligatorioevent_idstringopcionalid.visitor_idstringopcionalsession_idstringopcionalcrm_sid). Con él, el lead hereda el canal publicitario y las etiquetas UTM de esa sesión. Alias: sid.valueintegeropcional149900 = 1.499,00. Solo se guarda en eventos con “Este evento genera ingresos”. Alias: val.currencystringopcionalEUR. Alias: cur.urlstringopcionalu.Respuesta
{ "ok": true, "duplicate": false }okbooleanduplicatebooleantrue si ya existe un lead con este event_id. No se ha guardado nada nuevo, y no pasa nada.A diferencia del navegador, la vía del servidor no usa pases de formulario ni comprobaciones de tiempo: la clave secreta ya es prueba suficiente. El honeypot sigue valiendo: un company_site no vacío en fields recibe 400 rejected. Y el filtro de nombres sensibles también: aunque la petición venga firmada con tu clave, un campo como card_number o user_password no se guarda.
Idempotencia: reintenta sin miedo
Las redes fallan en el peor momento. ¿La petición llegó o no? Con event_id no necesitas saberlo: vuelve a enviarla. Una repetición devuelve {"ok":true,"duplicate":true} y el lead no se guarda dos veces.
{ "ok": true, "duplicate": true }Atribución: qué anuncio trajo el lead
Los leads del widget se vinculan al visitante automáticamente. Un lead del servidor no sabe nada del navegador, a menos que se lo cuentes. El widget guarda el id del visitante en localStorage con la clave crm_vid y el id de sesión con crm_sid. Envíalos a tu backend junto con el formulario o el pedido, y pásalos después:
const crm = {
vid: localStorage.getItem('crm_vid'),
sid: localStorage.getItem('crm_sid'),
};
await fetch('/api/orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...order, crm }),
});
// …y en el servidor: visitor_id: body.crm.vid, session_id: body.crm.sidCon session_id, el lead hereda el canal publicitario y las etiquetas UTM de la primera visita de esa sesión, así que aparece en los informes junto a la campaña que se lo ganó. Más en Eventos y analítica.
Errores
Desde el navegador, crmLead() y el evento crm:lead solo dicen ok: false. La vía del servidor responde con un estado y un message:
| Estado | Cuerpo | Qué ha pasado |
|---|---|---|
| 400 | {"message":"unknown_event","name":"phone_order"} | No hay ningún evento con este nombre en el CRM. Decláralo en Marketing → Eventos (revisa la ortografía y las mayúsculas). |
| 400 | {"message":"invalid_event_name"} | El nombre tiene caracteres que no son letras latinas, dígitos o guiones bajos, o no empieza por una letra. |
| 400 | {"message":"no_contact"} | No hay ni teléfono (7–20 dígitos) ni email en fields. |
| 400 | {"message":"invalid_body"} | El cuerpo no es un objeto JSON con la forma esperada. |
| 400 | {"message":"rejected"} | El campo honeypot company_site está relleno. |
| 401 | {"message":"invalid_key"} | x-event-key falta, está mal formada o revocada. |
| 413 | — | El cuerpo pesa más de 8 KB. |
| 429 | {"message":"rate_limit_exceeded"} | Demasiadas peticiones en este minuto. Espera al siguiente. |
Las peticiones del navegador también pueden recibir 400 unknown_origin o 400 unknown_domain cuando el dominio de la página no es un sitio del CRM: los verás en la pestaña Network, mientras el código sigue recibiendo ok: false.
Límites
| Qué | Límite |
|---|---|
| Peticiones por IP (todas las rutas de marketing juntas) | 120 por minuto |
| Peticiones por sitio | 6000 por minuto |
| Pases de formulario por IP | 20 por minuto |
| Cuerpo de la petición | 8 KB |
| Nombre / teléfono / email / mensaje | 200 / 40 / 320 / 4000 caracteres |
| Campos extra | hasta 30; clave de hasta 60 y valor de hasta 1000 caracteres |
| Teléfono | 7–20 dígitos para contar como contacto |
event_id | hasta 128 caracteres |
| Claves secretas | hasta 5 activas por sitio |
Las ventanas son minutos fijos: tras un 429, espera a que empiece el minuto siguiente. Un visitante real nunca se acercará; un script aporreando tu formulario, sí.
Solución de problemas
El lead no ha llegado
Repasa la lista: casi siempre es una de estas:
- El nombre no está declarado. En Marketing → Eventos tiene que haber un evento con exactamente este nombre, mayúsculas incluidas. Un
data-crm-leadvacío equivale alead_submit: declara ese. - No está marcado “Esto es un lead”. Entonces el envío se guarda como un evento normal: búscalo en las estadísticas de eventos, no en Leads.
- Ni teléfono ni email. O están, pero con nombres que el CRM no conoce (
your-phone,contact[email]) y han acabado en “extra”. Renómbralos: mira la tabla de campos. Un teléfono con menos de 7 dígitos tampoco cuenta. - Los datos estaban en un campo de archivo. Los archivos se ignoran: el widget solo envía texto.
- Un campo real se llama
company_site. Es el nombre del honeypot; el lead se trata como un bot. - Tu propio listener de
crm:lead-beforeha llamado apreventDefault(), quizá cuando no te lo esperabas. - El widget no está en esta página. Sin
widget.jsel atributo no hace nada y el formulario se envía a la antigua (o no se envía). - Lo bloquea una Content Security Policy. Si tu web tiene CSP, necesita
script-src https://widget.sitecog.comyconnect-src https://back.sitecog.com. La consola del navegador te lo dirá en rojo. - El dominio no es un sitio del CRM. El sitio se reconoce por el origen de la página (se quita el
www.). Probar enlocalhosto en un dominio de staging que no está añadido al CRM daunknown_domainen la pestaña Network. - Has hecho muchas pruebas. 20 pases de formulario por minuto y por IP sobran para personas, pero no para clics frenéticos. Espera un minuto.
Ha llegado el lead, pero falta un campo
- El nombre del campo parece sensible. Repasa la regla de nombres: un
promo_passse descarta porquepasses una palabra suelta. Renómbralo (promo_code) y listo. - El campo, o algo que lo envuelve, lleva
data-crm-ignore. Suele pasar con unfieldseto undivheredado de otra parte del formulario. - Es un
type="password": esos no se envían nunca. - Es un campo de archivo. Los archivos se ignoran de todas formas: el widget solo envía texto.
No se muestra el importe
Marca “Este evento genera ingresos” en el tipo de evento. Comprueba también que el importe es un entero en unidades menores: 1499.00 se envía como 149900.
Notificaciones en Telegram
Un lead que nadie ve es un lead perdido. En los ajustes de CRM → Leads puedes conectar un bot de Telegram: pega el token del bot, activa las notificaciones, decide si también se envían los sospechosos y fija horas de silencio para que el turno de noche de los bots no despierte a tu equipo de ventas.
Las notificaciones usan formato HTML, y todo lo que escribió el visitante se escapa antes de llegar ahí. Un “nombre” como <a href="…">Haz clic aquí</a> llega como texto plano: un visitante no puede colar un enlace en el chat de tu equipo ni romper el formato. Los campos largos se recortan para que una novela en el mensaje no se convierta en un muro de texto; el lead completo siempre está en el CRM.
Eliminar los datos de una persona a petición suya
“Por favor, borren todo lo que tengan sobre mí” es una petición normal, y atenderla no debería suponer una semana excavando en tablas. El CRM tiene dos herramientas para ello, sin código por tu parte.
Desde un lead o un chat
La ficha de un lead y la de un chat tienen “Eliminar los datos del visitante”. Antes de borrar nada, el CRM muestra exactamente qué se va a eliminar:
- el perfil del visitante y la analítica: visitas a páginas y eventos;
- chats y adjuntos: el número de chats, mensajes y archivos;
- leads: solo si marcas “Eliminar también las solicitudes de este visitante”. Desactivado por defecto: un lead suele ser un negocio en curso, y la decisión es tuya.
Un lead enviado desde tu servidor sin visitor_id no está vinculado a ningún visitante, así que ahí solo se puede eliminar el propio lead.
Por correo o teléfono
Normalmente la petición llega por correo: “Soy anna@example.com, olvídenme”. Ve a CRM → Ajustes → “Solicitudes de eliminación de datos”, introduce el correo o el teléfono que dio la persona y el CRM encontrará sus leads, sus chats y su cuenta en tu web. Un botón elimina todo lo encontrado; también puede irse la analítica de visitas de los visitantes relacionados.
- Permisos. Para eliminar se necesitan los mismos permisos que para editar esas secciones. Un gestor que solo puede ver los chats ve lo encontrado, pero no puede borrarlo.
- Registro de actividad. Cada eliminación queda en el registro de actividad, sin el correo ni el teléfono de la persona: si no, el registro guardaría justo lo que se pidió olvidar.
- Sin deshacer. Eliminado es eliminado. Revisa las cifras antes de confirmar.