Integrar con n8n, Make y Zapier
Faturei Hoje no tiene app ni integración nativa en n8n, Make o Zapier. Lo que funciona, y lo que muestra esta guía, es el camino genérico de las tres herramientas:
- para llamar a la API, el módulo de solicitud HTTP, con la clave en la cabecera
Authorization; - para recibir eventos, el disparador de webhook de la herramienta, que recibe el
POSTde cada entrega.
Lo que ofrece cada herramienta cambia con su versión y su plan. La información de abajo viene de la documentación oficial de cada una, revisada en septiembre de 2026. Donde no pudimos confirmar algo, el texto lo dice.
La clave
Sección titulada «La clave»Cree una clave solo para la herramienta, con los permisos de lo que hace el flujo y nada más. Guárdela en el almacén de credenciales de la herramienta, no en un campo de texto suelto del flujo: quien abre el flujo no necesita ver la clave. Si la herramienta corre en un servidor con IP fija, use la lista de IPs permitidas.
Llamar a la API
Sección titulada «Llamar a la API»La solicitud es siempre la misma, en cualquier herramienta:
| Qué | Valor |
|---|---|
| Dirección | https://api.fatureihoje.com/public/v1/... |
| Cabecera | Authorization: Bearer fh_live_... |
| Cuerpo, cuando lo hay | JSON, con Content-Type: application/json |
En todo POST | Idempotency-Key, con un valor estable (vea abajo) |
n8n. El nodo HTTP Request. En la autenticación, elija la credencial genérica Header Auth, con nombre Authorization y valor Bearer seguido de la clave. Para el cuerpo, active Send Body y elija JSON; para la Idempotency-Key, active Send Headers. En las listas, la paginación del nodo tiene el modo Update a Parameter in Each Request, que sirve para enviar el next_cursor de vuelta en el parámetro cursor hasta que has_more venga false.
Make. La app HTTP, módulo de solicitud (Make a request). Complete la URL, el método y la cabecera Authorization, y envíe el cuerpo como JSON crudo, con el Content-Type application/json.
Zapier. Webhooks by Zapier, acción Custom Request. Es la que el propio Zapier indica para PATCH y DELETE, JSON anidado y cabeceras personalizadas. Webhooks by Zapier no está en el plan gratuito de Zapier.
La Idempotency-Key en una herramienta de automatización
Sección titulada «La Idempotency-Key en una herramienta de automatización»Una herramienta de automatización repite ejecuciones: el usuario hace clic en “ejecutar de nuevo”, la propia herramienta reintenta después de una falla. Si la clave de idempotencia se genera en el momento, cada repetición vuelve a crear el registro.
Use como clave un valor que ya viene del disparador y no cambia entre repeticiones: el id de la respuesta del formulario, el id del pedido de la tienda, el webhook-id del evento que disparó el flujo. Si un mismo disparador hace más de un POST, agregue un sufijo por llamada (<id>-cliente, <id>-venta). Vea Idempotencia.
Recibir eventos
Sección titulada «Recibir eventos»n8n. El nodo Webhook, método POST. La URL de prueba solo funciona mientras el editor está escuchando; registre en Faturei Hoje la URL de producción, que pasa a valer cuando el flujo se publica. En Respond, use Immediately.
Make. El módulo Custom webhook. En sus opciones se pueden capturar las cabeceras de la solicitud y activar el JSON pass-through, que entrega el cuerpo como texto en vez de interpretarlo.
Zapier. El disparador Catch Raw Hook, de Webhooks by Zapier. Entrega el cuerpo sin interpretar e incluye las cabeceras, hasta 2 MB. El Catch Hook común entrega el cuerpo ya interpretado. Zapier responde 200 a quien envió.
En cualquiera de ellas, registre la URL del disparador como endpoint: vea Crear un endpoint. Dos cuidados valen para las tres:
- Responda rápido. La entrega tiene 15 segundos. Un flujo que solo responde después de terminar todo puede pasarse de eso, y la entrega vuelve, aunque el trabajo esté hecho.
- Ignore duplicados. La misma entrega puede llegar más de una vez, con el mismo
webhook-id. Guarde los ids procesados en el almacenamiento de la herramienta y salte los repetidos. Vea Buenas prácticas.
La firma, herramienta por herramienta
Sección titulada «La firma, herramienta por herramienta»La firma es lo que prueba que la entrega vino de Faturei Hoje. Verificarla exige cuatro cosas: el cuerpo crudo, exactamente como llegó; un HMAC-SHA256 con el secreto decodificado de base64; una comparación en tiempo constante; y rechazar el webhook-timestamp con más de 5 minutos de diferencia con su reloj. No todas las herramientas tienen las cuatro.
| Herramienta | Lo que se puede | Lo que no confirmamos |
|---|---|---|
| n8n | El nodo Webhook tiene la opción Raw Body, y el nodo Code en JavaScript tiene el módulo crypto de Node.js en n8n Cloud. Con los dos, la receta de la página de firma funciona. En una instalación propia, n8n permite importar módulos de Node.js en el nodo Code | No probamos el flujo en n8n. La forma exacta de leer el cuerpo crudo dentro del nodo Code cambia según la versión; revise la documentación de su versión |
| Make | La función de hash SHA256 acepta clave de HMAC, con la codificación de la clave en Base64 y la salida en Base64: es el cálculo de la firma. El JSON pass-through entrega el cuerpo como texto | No confirmamos que el texto del pass-through sea idéntico, byte a byte, al cuerpo enviado. No encontramos una comparación en tiempo constante entre las funciones de Make: la comparación queda como una igualdad común. Tampoco revisamos las funciones de fecha de Make para rechazar el webhook-timestamp fuera de la ventana |
| Zapier | El Catch Raw Hook entrega el cuerpo crudo y las cabeceras, y Code by Zapier en JavaScript corre Node.js con la biblioteca estándar, que incluye crypto | No probamos el flujo en Zapier. Los nombres de los campos en los que el cuerpo crudo y las cabeceras llegan al paso de código no están en la documentación que revisamos |
Ante la duda, o cuando la herramienta no puede verificar, siga el patrón de la próxima sección.
El patrón seguro: el webhook avisa, la API confirma
Sección titulada «El patrón seguro: el webhook avisa, la API confirma»Trate el evento como un aviso de que algo cambió, y no como la fuente del dato. Al recibirlo, tome el tipo y el id del objeto y lea el objeto por la API, con su clave. Un evento falso, enviado por alguien que descubrió la URL de su disparador, como mucho hace que usted lea un registro que ya existe; no puede meter datos inventados en su flujo, porque el dato viene de la API.
Esto no reemplaza la firma cuando la acción del flujo depende solo de que el evento haya llegado (enviar un correo de “venta pagada”, por ejemplo): en ese caso, verifique la firma, o confirme por la API que la venta realmente está pagada antes de actuar.
# El evento llegó con data.object = { "object": "sale", "id": "3c5e..." }.# Lea el objeto por la API antes de usarlo.curl "https://api.fatureihoje.com/public/v1/sales/3c5e7a9b-1d3f-4b5d-8f1a-3c5e7a9b1d3f" \ -H "Authorization: Bearer $FH_API_KEY"// El cuerpo que recibió el disparador de webhook.const event = { id: 'evt_9b2f4c7e1a3d4f6b8c0e2a4d6f8b1c3e', type: 'sale.paid', data: { object: { object: 'sale', id: '3c5e7a9b-1d3f-4b5d-8f1a-3c5e7a9b1d3f' } },};
// Tipo de objeto -> ruta de la API. Agregue los que usa su flujo.const ROUTES = { client: 'clients', sale: 'sales', service_order: 'service_orders', quote: 'quotes', task: 'tasks' };
const route = ROUTES[event.data.object.object];if (!route) throw new Error(`tipo no contemplado: ${event.data.object.object}`);
const res = await fetch(`https://api.fatureihoje.com/public/v1/${route}/${event.data.object.id}`, { headers: { Authorization: `Bearer ${process.env.FH_API_KEY}` },});
if (res.status === 404) { console.log('El registro ya no existe (o no es de esta empresa).');} else { const object = await res.json(); if (!res.ok) throw new Error(`${res.status} ${object.error.code}: ${object.error.message}`); // De aquí en adelante, use `object`, no el cuerpo del evento. if (event.type === 'sale.paid' && object.payment_status !== 'paid') { console.log('La venta no está pagada ahora: no actúe según el evento.'); } else { console.log(object.id, object.updated_at); }}<?php
// El cuerpo que recibió el disparador de webhook.$event = [ 'id' => 'evt_9b2f4c7e1a3d4f6b8c0e2a4d6f8b1c3e', 'type' => 'sale.paid', 'data' => ['object' => ['object' => 'sale', 'id' => '3c5e7a9b-1d3f-4b5d-8f1a-3c5e7a9b1d3f']],];
// Tipo de objeto -> ruta de la API. Agregue los que usa su flujo.$routes = ['client' => 'clients', 'sale' => 'sales', 'service_order' => 'service_orders', 'quote' => 'quotes', 'task' => 'tasks'];
$type = $event['data']['object']['object'];if (!isset($routes[$type])) { throw new RuntimeException('tipo no contemplado: ' . $type);}
$ch = curl_init('https://api.fatureihoje.com/public/v1/' . $routes[$type] . '/' . $event['data']['object']['id']);curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('FH_API_KEY')],]);$object = json_decode(curl_exec($ch), true);$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($status === 404) { echo 'El registro ya no existe (o no es de esta empresa).', PHP_EOL;} elseif ($status !== 200) { throw new RuntimeException($status . ' ' . $object['error']['code'] . ': ' . $object['error']['message']);} else { // De aquí en adelante, use $object, no el cuerpo del evento. if ($event['type'] === 'sale.paid' && $object['payment_status'] !== 'paid') { echo 'La venta no está pagada ahora: no actúe según el evento.', PHP_EOL; } else { echo $object['id'], ' ', $object['updated_at'], PHP_EOL; }}import jsonimport osimport urllib.errorimport urllib.request
# El cuerpo que recibió el disparador de webhook.event = { "id": "evt_9b2f4c7e1a3d4f6b8c0e2a4d6f8b1c3e", "type": "sale.paid", "data": {"object": {"object": "sale", "id": "3c5e7a9b-1d3f-4b5d-8f1a-3c5e7a9b1d3f"}},}
# Tipo de objeto -> ruta de la API. Agregue los que usa su flujo.ROUTES = {"client": "clients", "sale": "sales", "service_order": "service_orders", "quote": "quotes", "task": "tasks"}
kind = event["data"]["object"]["object"]if kind not in ROUTES: raise SystemExit(f"tipo no contemplado: {kind}")
request = urllib.request.Request( f"https://api.fatureihoje.com/public/v1/{ROUTES[kind]}/{event['data']['object']['id']}", headers={"Authorization": f"Bearer {os.environ['FH_API_KEY']}"},)try: with urllib.request.urlopen(request) as response: obj = json.load(response) # De aquí en adelante, use obj, no el cuerpo del evento. if event["type"] == "sale.paid" and obj["payment_status"] != "paid": print("La venta no está pagada ahora: no actúe según el evento.") else: print(obj["id"], obj["updated_at"])except urllib.error.HTTPError as failure: if failure.code == 404: print("El registro ya no existe (o no es de esta empresa).") else: error = json.load(failure)["error"] raise SystemExit(f"{failure.code} {error['code']}: {error['message']}")En un evento .deleted, el 404 es la respuesta esperada: el registro se eliminó. En los demás, la lectura trae el objeto como está ahora, que puede ser más nuevo que el evento. Es lo que usted quiere cuando el objetivo es reflejar el estado actual.
Sin webhook: consultar cada cierto tiempo
Sección titulada «Sin webhook: consultar cada cierto tiempo»Las tres herramientas tienen un disparador por horario. Un flujo que corre cada hora y lee la lista con updated_after trae lo que cambió, sin depender de recibir nada. Para eso, el flujo tiene que guardar el punto de partida entre una ejecución y otra, en el almacenamiento que ofrece la herramienta. La receta está en Sincronización incremental con updated_after, y valen las mismas reglas: los mismos filtros en todas las páginas, ventana de superposición y escritura por id.
Cada llamada de la herramienta consume del presupuesto de llamadas de la empresa. Vea Límites de uso.
Próximo paso
Sección titulada «Próximo paso»- Crear un endpoint: registrar la URL del disparador.
- Verificar la firma: la receta completa, con código.