Empezar
La API de LeadFlow te da acceso a los leads, tareas y equipo de tu cuenta, y los webhooks te avisan en cuanto pasa algo. Todo va sobre HTTPS y JSON. Dirección base:
https://api-leads.vred.es/v1
Primera llamada: crea una clave en Configuración › API e integraciones y pide la raíz.
curl https://api-leads.vred.es/v1 \
-H "Authorization: Bearer lf_live_TU_CLAVE"
{ "name": "LeadFlow API", "version": "v1", "tenant": { "id": 28, "name": "Tu empresa" }, "scope": "read", "endpoints": [ "GET /v1/leads", "..." ] }
Autenticación
Cada petición lleva la clave en la cabecera Authorization: Bearer lf_live_… (también vale x-api-key). La clave identifica a tu cuenta: nunca hay que enviar un identificador de empresa.
- Solo lectura: consultar leads, tareas, equipo y etiquetas.
- Lectura y escritura: además crear o modificar leads, notas y tareas, y enviar mensajes.
Límites y errores
120 peticiones por minuto por clave. Cada respuesta lleva X-RateLimit-Limit y X-RateLimit-Remaining; al superarlo recibes 429 con Retry-After en segundos.
Los errores devuelven siempre la misma forma:
{ "error": { "code": "validation_error", "message": "limit: Number must be less than or equal to 100" } }
| HTTP | code | Cuándo |
|---|---|---|
| 400 | validation_error | Falta un campo o tiene un formato incorrecto |
| 401 | missing_api_key / invalid_api_key | Sin clave, clave incorrecta o revocada |
| 403 | read_only_key / account_paused | La clave no puede escribir, o la cuenta está en pausa |
| 404 | not_found | El recurso no existe o no es de tu cuenta |
| 429 | rate_limited | Demasiadas peticiones |
Leads
GET/leads
Lista paginada. Filtros: status (uno o varios separados por coma), assigned_to (id de usuario), tag (nombre o slug), q (busca en nombre, teléfono y email), created_since y updated_since (ISO 8601), sort (updated, created, last_interaction), limit (máx. 100) y offset.
curl "https://api-leads.vred.es/v1/leads?status=cualificado,cita_agendada&updated_since=2026-09-01T00:00:00Z&limit=50" \
-H "Authorization: Bearer lf_live_TU_CLAVE"
{ "data": [ {
"id": 14072,
"name": "Calería Maychi, S.L.",
"phone": "34698911195",
"email": "info@caleriamaychi.es",
"status": "en_cualificacion",
"source": "prospector",
"channel": "whatsapp_waha",
"tags": [{ "id": 42, "name": "BdB", "slug": "bdb" }],
"assignedTo": { "id": 6, "name": "Enrique", "email": "enrique@empresa.com" },
"conversationOwner": "ia",
"outreachPaused": false,
"deal": { "value": 2500, "probability": 25, "expectedCloseAt": "2026-10-15", "wonAt": null, "lostAt": null, "lostReason": null },
"appointment": { "at": null, "outcome": null },
"data": { "provincia": "Cádiz", "empresa": "Calería Maychi" },
"lastInteractionAt": "2026-09-04T11:34:22.000Z",
"createdAt": "2026-09-03T10:55:29.000Z",
"updatedAt": "2026-09-04T11:53:21.000Z"
} ], "total": 1, "limit": 50, "offset": 0 }
Estados posibles: nuevo, en_cualificacion, cualificado, cita_agendada, ganado, perdido. conversationOwner dice si la conversación la lleva la IA (ia) o una persona (humano). data son los datos que la IA o el equipo han ido recogiendo.
GET/leads/{id}
La ficha completa: lo anterior más notes, tasks, messages (los últimos 100, en orden) y events (el historial).
POST/leads
Crea un lead o, si ya existe uno con ese teléfono o email, lo actualiza con lo que envíes: las etiquetas se añaden, los datos se fusionan. Responde 201 al crear y 200 al actualizar, y la cabecera X-Lead-Created lo confirma. Hace falta phone o email.
curl -X POST https://api-leads.vred.es/v1/leads \
-H "Authorization: Bearer lf_live_TU_CLAVE" -H "Content-Type: application/json" \
-d '{
"name": "Ferretería García",
"phone": "600 123 456",
"email": "info@ferreteriagarcia.es",
"source": "web",
"tags": ["Web", "Cadena88"],
"data": { "provincia": "Murcia", "interes": "Control de stock" },
"deal": { "value": 3000, "probability": 20 },
"note": "Pidió información desde el formulario de la web",
"outreach": true
}'
"outreach": true si quieres que el seguimiento con IA le escriba. Las respuestas que llegan por WhatsApp o email siempre se atienden.Campos: name, phone, email, status, source, tags (se crean si no existen), data (texto por clave), deal (value en euros, probability 0-100, expectedCloseAt AAAA-MM-DD), assignedTo (id de usuario o null), outreach, note.
PATCH/leads/{id}
Modifica solo lo que envíes. Además de los campos anteriores: lostReason, addTags, removeTags (o tags para reemplazarlas todas). Poner null en una clave de data la borra.
curl -X PATCH https://api-leads.vred.es/v1/leads/14072 \
-H "Authorization: Bearer lf_live_TU_CLAVE" -H "Content-Type: application/json" \
-d '{ "status": "ganado", "deal": { "value": 4200 }, "assignedTo": 6 }'
Ganar o perder por API hace lo mismo que desde el panel: cierra las tareas de la cita, apunta la fecha y lo atribuye a la campaña que lo originó.
Notas
POST/leads/{id}/notes
curl -X POST https://api-leads.vred.es/v1/leads/14072/notes \
-H "Authorization: Bearer lf_live_TU_CLAVE" -H "Content-Type: application/json" \
-d '{ "body": "Presupuesto enviado por email, pendiente de respuesta", "pinned": true }'
La nota aparece en la ficha firmada como API · nombre de la clave.
Tareas
GET/tasks
Por defecto las pendientes, ordenadas por vencimiento. Filtros: status (pending, done, all), assigned_to, lead_id, due_before.
POST/leads/{id}/tasks
curl -X POST https://api-leads.vred.es/v1/leads/14072/tasks \
-H "Authorization: Bearer lf_live_TU_CLAVE" -H "Content-Type: application/json" \
-d '{ "title": "Llamar para cerrar la demo", "kind": "llamar", "dueAt": "2026-09-08T09:00:00+02:00", "assignedTo": 6 }'
kind: llamar, email, whatsapp, reunion, otro. Sin assignedTo, la tarea va al asignado del lead.
PATCH/tasks/{id}
curl -X PATCH https://api-leads.vred.es/v1/tasks/9 \
-H "Authorization: Bearer lf_live_TU_CLAVE" -H "Content-Type: application/json" \
-d '{ "done": true }'
Mensajes
POST/leads/{id}/messages
Envía un mensaje al lead por whatsapp o email (con subject). Sin channel, usa el canal por el que llegó el lead. Funciona como un envío manual desde el panel: la IA se aparta de esa conversación y el mensaje queda en el hilo.
curl -X POST https://api-leads.vred.es/v1/leads/14072/messages \
-H "Authorization: Bearer lf_live_TU_CLAVE" -H "Content-Type: application/json" \
-d '{ "channel": "whatsapp", "body": "Hola, te confirmo la demo del lunes a las 10:00." }'
Responde 201 si el canal aceptó el envío y 502 con el motivo si no.
Equipo y etiquetas
GET/team devuelve los usuarios (id, nombre, email, rol, activo) para usar en assignedTo. GET/tags devuelve las etiquetas (id, nombre, slug) para filtrar.
Webhooks
Un webhook es una URL tuya a la que LeadFlow envía un POST cuando ocurre un evento. Se crean en Configuración › Webhooks salientes, eligiendo los eventos. Al crearlo recibes un secreto con el que se firma cada envío.
Cada aviso es un JSON con esta forma:
{
"id": "evt_dzILQu_aa6gpWSmN",
"event": "lead.status_changed",
"createdAt": "2026-09-04T14:32:36.000Z",
"tenant": { "id": 28, "name": "Tu empresa" },
"data": {
"lead": { ...el lead, igual que en GET /leads/{id}... },
"change": { "from": "cualificado", "to": "ganado" },
"actor": "Enrique"
}
}
Cabeceras: X-LeadFlow-Event, X-LeadFlow-Event-Id (el mismo para todos los webhooks que reciben ese evento; sirve para no procesarlo dos veces), X-LeadFlow-Delivery, X-LeadFlow-Timestamp y X-LeadFlow-Signature.
Tu servidor debe responder con un código 2xx en menos de 10 segundos; haz el trabajo pesado después de responder. Si falla, reintentamos a 1 min, 10 min, 1 h, 6 h y 16 h. Tras tres días fallando, el webhook se desactiva y el email de alertas de la cuenta recibe un aviso; se puede reactivar desde el panel. El registro de entregas, con reintento manual, está en el panel.
Eventos
| Evento | Cuándo se envía |
|---|---|
lead.created | Se crea un lead: por WhatsApp o email entrante, por el prospector, por una landing, a mano en el panel o por esta API. Las importaciones masivas no avisan. |
lead.status_changed | Cambia el estado del lead (nuevo, en_cualificacion, cualificado, cita_agendada, ganado, perdido). `data.change` trae `from` y `to`. |
lead.won | El lead pasa a ganado. Se envía además de lead.status_changed. |
lead.lost | El lead pasa a perdido. `data.change.lostReason` si se indicó. |
lead.assigned | Se asigna el lead a alguien del equipo (a mano o automáticamente). |
appointment.booked | Se agenda una cita. `data.change.appointmentAt` en ISO 8601. |
appointment.outcome | Se marca el resultado de la cita: `realizada` o `no_asistio`. |
message.received | Llega un mensaje del lead por cualquier canal. `data.message` trae id, canal, texto y fecha. |
ai.handoff | La IA se aparta de la conversación y hace falta una persona. `data.change` trae el motivo. |
task.created | Se crea una tarea sobre el lead. |
task.done | Se marca una tarea como hecha. |
Verificar la firma
X-LeadFlow-Signature es sha256= seguido del HMAC SHA-256, en hexadecimal, calculado con tu secreto sobre la cadena timestamp + "." + cuerpo, donde el cuerpo son los bytes exactos recibidos. Rechaza avisos con más de 5 minutos de antigüedad.
Node.js (Express)
import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";
const app = express();
app.post("/leadflow", express.raw({ type: "application/json" }), (req, res) => {
const ts = req.header("x-leadflow-timestamp");
const sig = req.header("x-leadflow-signature") ?? "";
const expected = "sha256=" + createHmac("sha256", process.env.LEADFLOW_SECRET).update(ts + "." + req.body).digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(ts)) < 300;
if (!fresh || sig.length !== expected.length || !timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) return res.sendStatus(401);
const event = JSON.parse(req.body);
res.sendStatus(200); // responde primero
procesa(event); // trabaja después
});
PHP
$body = file_get_contents('php://input');
$ts = $_SERVER['HTTP_X_LEADFLOW_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_LEADFLOW_SIGNATURE'] ?? '';
$expected = 'sha256=' . hash_hmac('sha256', $ts . '.' . $body, LEADFLOW_SECRET);
if (abs(time() - (int)$ts) > 300 || !hash_equals($expected, $sig)) { http_response_code(401); exit; }
$event = json_decode($body, true);
http_response_code(200);
Python
import hmac, hashlib, time
def verify(secret: str, ts: str, body: bytes, signature: str) -> bool:
expected = "sha256=" + hmac.new(secret.encode(), f"{ts}.".encode() + body, hashlib.sha256).hexdigest()
return abs(time.time() - int(ts)) < 300 and hmac.compare_digest(expected, signature)
Receta: Zapier
Lead ganado → fila en Google Sheets
- En Zapier, crea un Zap con el disparador Webhooks by Zapier › Catch Hook. Copia la URL que te da.
- En LeadFlow, Configuración › Webhooks salientes › Nuevo webhook: pega la URL y marca Lead ganado. Pulsa Probar para que Zapier reciba una muestra.
- Añade la acción Google Sheets › Create Spreadsheet Row y mapea
data__lead__name,data__lead__phone,data__lead__deal__valueydata__lead__assignedTo__name.
Nuevo contacto en otra herramienta → lead en LeadFlow
Acción Webhooks by Zapier › Custom Request: método POST, URL https://api-leads.vred.es/v1/leads, cabecera Authorization: Bearer lf_live_…, Content-Type: application/json, y en Data el JSON con name, phone, email, source y tags.
Receta: Make
- Módulo Webhooks › Custom webhook: crea uno y copia la URL.
- Crea el webhook en LeadFlow con esa URL y los eventos que quieras; pulsa Probar para que Make determine la estructura.
- Para escribir en LeadFlow usa HTTP › Make a request con la cabecera de autorización y el JSON del recurso. Para consultar,
GET https://api-leads.vred.es/v1/leads?updated_since=…con un módulo programado.
Receta: formulario de tu web
Desde tu servidor (nunca desde el navegador, la clave quedaría expuesta), al recibir el formulario:
await fetch("https://api-leads.vred.es/v1/leads", {
method: "POST",
headers: { "Authorization": "Bearer " + process.env.LEADFLOW_KEY, "Content-Type": "application/json" },
body: JSON.stringify({
name: form.nombre, phone: form.telefono, email: form.email,
source: "web", tags: ["Web"],
data: { interes: form.interes, mensaje: form.mensaje },
outreach: true, // la IA le escribe para cualificarlo
}),
});
Si la misma persona vuelve a rellenar el formulario, se actualiza su ficha en vez de duplicarla.
Especificación completa: openapi.json (importable en Postman, Insomnia o Make).