Integrar com n8n, Make e Zapier
O Faturei Hoje não tem app nem integração nativa no n8n, no Make ou no Zapier. O que funciona, e é o que este guia mostra, é o caminho genérico das três ferramentas:
- para chamar a API, o módulo de requisição HTTP, com a chave no cabeçalho
Authorization; - para receber eventos, o gatilho de webhook da ferramenta, que recebe o
POSTde cada entrega.
O que cada ferramenta oferece muda com a versão e com o plano dela. As informações abaixo vêm da documentação oficial de cada uma, conferida em setembro de 2026. Onde não conseguimos confirmar, o texto diz.
A chave
Seção intitulada “A chave”Crie uma chave só para a ferramenta, com as permissões do que o fluxo faz e nada mais. Guarde-a no cofre de credenciais da ferramenta, não num campo de texto solto do fluxo: quem abre o fluxo não precisa ver a chave. Se a ferramenta roda num servidor com IP fixo, use a lista de IPs permitidos.
Chamar a API
Seção intitulada “Chamar a API”A requisição é sempre a mesma, em qualquer ferramenta:
| O quê | Valor |
|---|---|
| Endereço | https://api.fatureihoje.com/public/v1/... |
| Cabeçalho | Authorization: Bearer fh_live_... |
| Corpo, quando tiver | JSON, com Content-Type: application/json |
Em todo POST | Idempotency-Key, com um valor estável (veja abaixo) |
n8n. Nó HTTP Request. Em autenticação, escolha a credencial genérica Header Auth, com nome Authorization e valor Bearer seguido da chave. Para o corpo, ligue Send Body e escolha JSON; para a Idempotency-Key, ligue Send Headers. Nas listas, a paginação do nó tem o modo Update a Parameter in Each Request, que serve para mandar o next_cursor de volta no parâmetro cursor até has_more vir false.
Make. App HTTP, módulo de requisição (Make a request). Preencha URL, método e o cabeçalho Authorization, e mande o corpo como JSON cru, com o Content-Type application/json.
Zapier. Webhooks by Zapier, ação Custom Request. É a que o próprio Zapier indica para PATCH e DELETE, JSON aninhado e cabeçalhos personalizados. O Webhooks by Zapier não está no plano gratuito do Zapier.
A Idempotency-Key numa ferramenta de automação
Seção intitulada “A Idempotency-Key numa ferramenta de automação”Ferramenta de automação repete execução: o usuário clica em “rodar de novo”, a própria ferramenta tenta outra vez depois de uma falha. Se a chave de idempotência for gerada na hora, cada repetição cria o registro de novo.
Use como chave um valor que já vem do gatilho e não muda entre repetições: o id da resposta do formulário, o id do pedido na loja, o webhook-id do evento que disparou o fluxo. Se um mesmo gatilho faz mais de um POST, acrescente um sufixo por chamada (<id>-cliente, <id>-venda). Veja Idempotência.
Receber eventos
Seção intitulada “Receber eventos”n8n. Nó Webhook, método POST. A URL de teste só funciona enquanto o editor está escutando; cadastre no Faturei Hoje a URL de produção, que passa a valer quando o fluxo é publicado. Em Respond, use Immediately.
Make. Módulo Custom webhook. Nas opções dele dá para pegar os cabeçalhos da requisição e ligar o JSON pass-through, que entrega o corpo como texto em vez de interpretá-lo.
Zapier. Gatilho Catch Raw Hook, do Webhooks by Zapier. Ele entrega o corpo sem interpretar e inclui os cabeçalhos, até 2 MB. O Catch Hook comum entrega o corpo já interpretado. O Zapier responde 200 a quem enviou.
Em qualquer uma, cadastre a URL do gatilho como endpoint: veja Criar um endpoint. Dois cuidados valem para as três:
- Responda rápido. A entrega tem 15 segundos. Um fluxo que só responde depois de terminar tudo pode passar disso, e a entrega volta, mesmo com o trabalho feito.
- Ignore duplicata. A mesma entrega pode chegar mais de uma vez, com o mesmo
webhook-id. Guarde os ids processados no armazenamento da ferramenta e pule o repetido. Veja Boas práticas.
A assinatura, ferramenta por ferramenta
Seção intitulada “A assinatura, ferramenta por ferramenta”A assinatura é o que prova que a entrega veio do Faturei Hoje. Conferir exige quatro coisas: o corpo cru, exatamente como chegou; um HMAC-SHA256 com o segredo decodificado de base64; uma comparação em tempo constante; e recusar o webhook-timestamp com mais de 5 minutos de diferença do relógio. Nem toda ferramenta tem as quatro.
| Ferramenta | O que dá | O que não confirmamos |
|---|---|---|
| n8n | O nó Webhook tem a opção Raw Body, e o nó Code em JavaScript tem o módulo crypto do Node.js no n8n Cloud. Com os dois, a receita da página de assinatura funciona. Na instalação própria, o n8n permite importar módulos do Node.js no nó Code | Não testamos o fluxo no n8n. O jeito exato de ler o corpo cru dentro do nó Code muda com a versão; confira na documentação da sua versão |
| Make | A função de hash SHA256 aceita chave de HMAC, com a codificação da chave em Base64 e a saída em Base64: é o cálculo da assinatura. O JSON pass-through entrega o corpo como texto | Não confirmamos que o texto do pass-through é idêntico, byte a byte, ao corpo enviado. Não achamos comparação em tempo constante nas funções do Make: a comparação vira uma igualdade comum. Também não conferimos as funções de data do Make para recusar o webhook-timestamp fora da janela |
| Zapier | O Catch Raw Hook entrega o corpo cru e os cabeçalhos, e o Code by Zapier em JavaScript roda Node.js com a biblioteca padrão, o que inclui crypto | Não testamos o fluxo no Zapier. Os nomes dos campos em que o corpo cru e os cabeçalhos chegam ao passo de código não estão na documentação que conferimos |
Na dúvida, ou quando a ferramenta não consegue conferir, siga o padrão da próxima seção.
O padrão seguro: o webhook avisa, a API confirma
Seção intitulada “O padrão seguro: o webhook avisa, a API confirma”Trate o evento como um aviso de que algo mudou, e não como a fonte do dado. Ao receber, pegue o tipo e o id do objeto e leia o objeto pela API, com a sua chave. Um evento falso, mandado por alguém que descobriu a URL do seu gatilho, no máximo faz você ler um registro que já existe; ele não consegue pôr dado inventado no seu fluxo, porque o dado vem da API.
Isso não substitui a assinatura quando a ação do fluxo depende só de o evento ter chegado (mandar um e-mail de “venda paga”, por exemplo): aí confira a assinatura, ou confirme pela API que a venda está mesmo paga antes de agir.
# O evento chegou com data.object = { "object": "sale", "id": "3c5e..." }.# Leia o objeto pela API antes de usar.curl "https://api.fatureihoje.com/public/v1/sales/3c5e7a9b-1d3f-4b5d-8f1a-3c5e7a9b1d3f" \ -H "Authorization: Bearer $FH_API_KEY"// O corpo que o gatilho de webhook recebeu.const event = { id: 'evt_9b2f4c7e1a3d4f6b8c0e2a4d6f8b1c3e', type: 'sale.paid', data: { object: { object: 'sale', id: '3c5e7a9b-1d3f-4b5d-8f1a-3c5e7a9b1d3f' } },};
// Tipo do objeto -> rota da API. Acrescente os que o seu fluxo usa.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 não tratado: ${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('O registro não existe mais (ou não é desta empresa).');} else { const object = await res.json(); if (!res.ok) throw new Error(`${res.status} ${object.error.code}: ${object.error.message}`); // Daqui em diante, use `object`, não o corpo do evento. if (event.type === 'sale.paid' && object.payment_status !== 'paid') { console.log('A venda não está paga agora: não aja com base no evento.'); } else { console.log(object.id, object.updated_at); }}<?php
// O corpo que o gatilho de webhook recebeu.$event = [ 'id' => 'evt_9b2f4c7e1a3d4f6b8c0e2a4d6f8b1c3e', 'type' => 'sale.paid', 'data' => ['object' => ['object' => 'sale', 'id' => '3c5e7a9b-1d3f-4b5d-8f1a-3c5e7a9b1d3f']],];
// Tipo do objeto -> rota da API. Acrescente os que o seu fluxo usa.$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 não tratado: ' . $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 'O registro não existe mais (ou não é desta empresa).', PHP_EOL;} elseif ($status !== 200) { throw new RuntimeException($status . ' ' . $object['error']['code'] . ': ' . $object['error']['message']);} else { // Daqui em diante, use $object, não o corpo do evento. if ($event['type'] === 'sale.paid' && $object['payment_status'] !== 'paid') { echo 'A venda não está paga agora: não aja com base no evento.', PHP_EOL; } else { echo $object['id'], ' ', $object['updated_at'], PHP_EOL; }}import jsonimport osimport urllib.errorimport urllib.request
# O corpo que o gatilho de webhook recebeu.event = { "id": "evt_9b2f4c7e1a3d4f6b8c0e2a4d6f8b1c3e", "type": "sale.paid", "data": {"object": {"object": "sale", "id": "3c5e7a9b-1d3f-4b5d-8f1a-3c5e7a9b1d3f"}},}
# Tipo do objeto -> rota da API. Acrescente os que o seu fluxo usa.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 não tratado: {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) # Daqui em diante, use obj, não o corpo do evento. if event["type"] == "sale.paid" and obj["payment_status"] != "paid": print("A venda não está paga agora: não aja com base no evento.") else: print(obj["id"], obj["updated_at"])except urllib.error.HTTPError as failure: if failure.code == 404: print("O registro não existe mais (ou não é desta empresa).") else: error = json.load(failure)["error"] raise SystemExit(f"{failure.code} {error['code']}: {error['message']}")Num evento .deleted, o 404 é a resposta esperada: o registro foi excluído. Nos outros, a leitura traz o objeto como ele está agora, que pode ser mais novo que o evento. É o que você quer quando o objetivo é espelhar o estado atual.
Sem webhook: consultar de tempos em tempos
Seção intitulada “Sem webhook: consultar de tempos em tempos”As três ferramentas têm gatilho por agenda. Um fluxo que roda a cada hora e lê a lista com updated_after traz o que mudou, sem depender de receber nada. Para isso, o fluxo precisa guardar o ponto de partida entre uma execução e outra, no armazenamento que a ferramenta oferece. A receita está em Sincronização incremental com updated_after, e as mesmas regras valem: os mesmos filtros em todas as páginas, janela de sobreposição e gravação pelo id.
Cada chamada da ferramenta gasta do orçamento de chamadas da empresa. Veja Limites de uso.
Próximo passo
Seção intitulada “Próximo passo”- Criar um endpoint: cadastrar a URL do gatilho.
- Verificar a assinatura: a receita completa, com código.