Captar leads del sitio web
Alguien completó el formulario de contacto de su sitio. Con una llamada, ese contacto entra en Faturei Hoje como lead y ya abre una tarea para que alguien del equipo le responda.
Cómo queda el diseño
Sección titulada «Cómo queda el diseño»navegador del visitante -> su servidor -> POST /public/v1/leadsEl formulario envía a su servidor, y es su servidor el que llama a la API. La clave nunca va al navegador: cualquiera que abriera el código de la página se llevaría la clave.
La API no tiene protección contra robots para su formulario. El filtro de spam, el captcha y el límite por visitante quedan de su lado, antes de la llamada.
La clave
Sección titulada «La clave»Cree una clave solo para el sitio, con lo mínimo:
| Permiso | Para qué |
|---|---|
leads:create | Registrar el lead |
tasks:create | Abrir la tarea de contacto. Sin él, solo se puede registrar con task: null |
team:read | Opcional. Solo si va a elegir quién queda a cargo de la tarea |
El miembro que la clave representa necesita acceso a clientes en el panel y, cuando la captación abre una tarea, también a tareas. Vea Permisos.
La llamada
Sección titulada «La llamada»POST /public/v1/leads recibe el nombre y al menos un contacto, email o phone. Sin ninguno de los dos, la respuesta es 422.
# Genere la clave una vez y guárdela: en un reintento, repita con el mismo valor.LEAD_KEY=$(uuidgen)curl -X POST "https://api.fatureihoje.com/public/v1/leads" \ -H "Authorization: Bearer $FH_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $LEAD_KEY" \ -d '{ "name": "Ana Souza", "email": "ana@example.com", "phone": "+5511999990000", "source": "sitio", "notes": "Quiero un presupuesto de instalación de cámaras.", "task": { "due_in_hours": 4, "priority": "high" } }'import { randomUUID } from 'node:crypto';
// En producción, esto viene del formulario que recibió su servidor.const form = { name: 'Ana Souza', email: 'ana@example.com', phone: '+5511999990000', message: 'Quiero un presupuesto de instalación de cámaras.',};
// Una clave por envío del formulario. Guárdela con el envío para repetir igual.const idempotencyKey = randomUUID();
const res = await fetch('https://api.fatureihoje.com/public/v1/leads', { method: 'POST', headers: { Authorization: `Bearer ${process.env.FH_API_KEY}`, 'Content-Type': 'application/json', 'Idempotency-Key': idempotencyKey, }, body: JSON.stringify({ name: form.name, email: form.email, phone: form.phone, source: 'sitio', notes: form.message, task: { due_in_hours: 4, priority: 'high' }, }),});
const body = await res.json();
if (!res.ok) { throw new Error(`${res.status} ${body.error.code}: ${body.error.message}`);}
console.log(body.lead.id, body.deduplicated ? 'registro reutilizado' : 'lead nuevo');<?php
// En producción, esto viene del formulario que recibió su servidor.$form = [ 'name' => 'Ana Souza', 'email' => 'ana@example.com', 'phone' => '+5511999990000', 'message' => 'Quiero un presupuesto de instalación de cámaras.',];
// Una clave por envío del formulario. Guárdela con el envío para repetir igual.$idempotencyKey = bin2hex(random_bytes(16));
$ch = curl_init('https://api.fatureihoje.com/public/v1/leads');curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('FH_API_KEY'), 'Content-Type: application/json', 'Idempotency-Key: ' . $idempotencyKey, ], CURLOPT_POSTFIELDS => json_encode([ 'name' => $form['name'], 'email' => $form['email'], 'phone' => $form['phone'], 'source' => 'sitio', 'notes' => $form['message'], 'task' => ['due_in_hours' => 4, 'priority' => 'high'], ]),]);
$body = json_decode(curl_exec($ch), true);$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($status !== 201) { throw new RuntimeException($status . ' ' . $body['error']['code'] . ': ' . $body['error']['message']);}
echo $body['lead']['id'], ' ', $body['deduplicated'] ? 'registro reutilizado' : 'lead nuevo', PHP_EOL;import jsonimport osimport urllib.errorimport urllib.requestimport uuid
# En producción, esto viene del formulario que recibió su servidor.form = { "name": "Ana Souza", "email": "ana@example.com", "phone": "+5511999990000", "message": "Quiero un presupuesto de instalación de cámaras.",}
# Una clave por envío del formulario. Guárdela con el envío para repetir igual.idempotency_key = str(uuid.uuid4())
payload = { "name": form["name"], "email": form["email"], "phone": form["phone"], "source": "sitio", "notes": form["message"], "task": {"due_in_hours": 4, "priority": "high"},}
request = urllib.request.Request( "https://api.fatureihoje.com/public/v1/leads", data=json.dumps(payload).encode(), method="POST", headers={ "Authorization": f"Bearer {os.environ['FH_API_KEY']}", "Content-Type": "application/json", "Idempotency-Key": idempotency_key, },)
try: with urllib.request.urlopen(request) as response: body = json.load(response)except urllib.error.HTTPError as failure: error = json.load(failure)["error"] raise SystemExit(f"{failure.code} {error['code']}: {error['message']}")
print(body["lead"]["id"], "registro reutilizado" if body["deduplicated"] else "lead nuevo")Qué hace cada campo, con su tamaño máximo, está en la Referencia. Los que vale la pena comentar:
sourcedice de dónde vino el lead. Use un valor por formulario o campaña (sitio,landing-black-friday) y podrá separar los leads después.noteses el mensaje que escribió la persona. Va a las observaciones del registro.taskcontrola la tarea de contacto. Sin el campo, se abre la tarea estándar, con plazo de 24 horas. Con un objeto, usted elige título, descripción, plazo (due_in_hoursodue_at, uno u otro), prioridad y responsable. Contask: null, no se abre ninguna tarea.
Qué vuelve
Sección titulada «Qué vuelve»La respuesta es 201 con un objeto lead_capture:
lead: el registro, con elid. Ese id es el mismo del cliente y sirve enGET /public/v1/clients/{id}.task: la tarea de contacto abierta, onullcuando usted enviótask: null.deduplicated:truecuando el registro ya existía.
Un contacto que ya está en la base
Sección titulada «Un contacto que ya está en la base»Si el teléfono o el correo ya pertenece a un registro de la empresa, la API reutiliza ese registro y no sobrescribe nada: ni el nombre, ni el contacto, ni el estado. Un cliente activo que vuelve a completar el formulario sigue activo. La tarea de contacto se abre igual, porque alguien tiene que responder.
Cuando eso pasa y la clave no tiene leads:read ni clients:read, lead y task vienen solo con object e id. Es a propósito: una clave que solo registra no lee el registro de quien ya estaba en la base.
Elegir quién responde
Sección titulada «Elegir quién responde»Sin responsable, la tarea nace sin dueño. Para asignarla a alguien, busque a la persona en GET /public/v1/team y envíe su id en task.assignee_id, junto con su type en task.assignee_type. Uno sin el otro responde 422.
{ "name": "Ana Souza", "phone": "+5511999990000", "task": { "assignee_id": "3f6c2a9e-8b1d-4e5f-9a7c-2d4b6e8f0a1c", "assignee_type": "org_member" }}La lista del equipo cambia poco. Léala una vez y guárdela, en vez de consultarla en cada lead.
Cuando la llamada falla
Sección titulada «Cuando la llamada falla»Guarde el envío del formulario de su lado antes de llamar a la API. Si la llamada falla por red, tiempo agotado, 429 o 5xx, vuelva a intentar más tarde con la misma Idempotency-Key: el lead no se registra dos veces. Vea Idempotencia.
| Respuesta | Qué hacer |
|---|---|
422 validation_failed | Corrija el campo indicado en errors. Repetir igual no lo resuelve |
403 permission_missing | Le falta un permiso a la clave, normalmente tasks:create |
403 member_permission_denied | El miembro de la clave no tiene acceso a clientes en el panel, o a tareas cuando la captación abre una tarea |
429 rate_limit_exceeded | Espere el Retry-After y repita. Vea Límites de uso |
| 5xx o falla de red | Repita con la misma Idempotency-Key |
El catálogo completo está en Errores.
Próximo paso
Sección titulada «Próximo paso»- Sincronizar clientes con un CRM o ERP: llevar el registro a su sistema.
- Webhooks: el evento
client.createdavisa a su sistema cuando entra un registro nuevo, venga del sitio, del panel o de WhatsApp.