Integrate with n8n, Make and Zapier
Faturei Hoje has no app and no native integration in n8n, Make or Zapier. What works, and what this guide shows, is the generic path all three tools offer:
- to call the API, the HTTP request module, with the key in the
Authorizationheader; - to receive events, the tool’s webhook trigger, which receives the
POSTof each delivery.
What each tool offers changes with its version and plan. The information below comes from each tool’s official documentation, checked in September 2026. Where we could not confirm something, the text says so.
The key
Section titled “The key”Create a key just for the tool, with the permissions the flow needs and nothing else. Keep it in the tool’s credential store, not in a loose text field in the flow: whoever opens the flow does not need to see the key. If the tool runs on a server with a fixed IP, use the allowed IPs list.
Calling the API
Section titled “Calling the API”The request is always the same, in any tool:
| What | Value |
|---|---|
| Address | https://api.fatureihoje.com/public/v1/... |
| Header | Authorization: Bearer fh_live_... |
| Body, when there is one | JSON, with Content-Type: application/json |
On every POST | Idempotency-Key, with a stable value (see below) |
n8n. The HTTP Request node. Under authentication, pick the generic Header Auth credential, with name Authorization and value Bearer followed by the key. For the body, turn on Send Body and choose JSON; for the Idempotency-Key, turn on Send Headers. For lists, the node’s pagination has an Update a Parameter in Each Request mode, which you can use to send next_cursor back in the cursor parameter until has_more is false.
Make. The HTTP app, request module (Make a request). Fill in the URL, the method and the Authorization header, and send the body as raw JSON, with Content-Type application/json.
Zapier. Webhooks by Zapier, Custom Request action. It is the one Zapier itself points to for PATCH and DELETE, nested JSON and custom headers. Webhooks by Zapier is not on Zapier’s free plan.
The Idempotency-Key in an automation tool
Section titled “The Idempotency-Key in an automation tool”Automation tools replay runs: the user clicks “run again”, the tool itself retries after a failure. If the idempotency key is generated on the spot, every replay creates the record again.
Use as the key a value that already comes from the trigger and does not change between replays: the form response id, the store’s order id, the webhook-id of the event that fired the flow. If one trigger makes more than one POST, add a suffix per call (<id>-client, <id>-sale). See Idempotency.
Receiving events
Section titled “Receiving events”n8n. The Webhook node, method POST. The test URL only works while the editor is listening; register the production URL in Faturei Hoje, which becomes active when the workflow is published. Under Respond, use Immediately.
Make. The Custom webhook module. Its options let you capture the request headers and turn on JSON pass-through, which delivers the body as text instead of parsing it.
Zapier. The Catch Raw Hook trigger, from Webhooks by Zapier. It delivers the body unparsed and includes the headers, up to 2 MB. The regular Catch Hook delivers the body already parsed. Zapier answers 200 to the sender.
In any of them, register the trigger URL as an endpoint: see Create an endpoint. Two things apply to all three:
- Answer fast. A delivery has 15 seconds. A flow that only answers after finishing everything can go past that, and the delivery comes back, even with the work done.
- Ignore duplicates. The same delivery can arrive more than once, with the same
webhook-id. Keep the processed ids in the tool’s storage and skip repeats. See Best practices.
The signature, tool by tool
Section titled “The signature, tool by tool”The signature is what proves the delivery came from Faturei Hoje. Checking it takes four things: the raw body, exactly as it arrived; an HMAC-SHA256 with the base64-decoded secret; a constant-time comparison; and rejecting a webhook-timestamp more than 5 minutes off your clock. Not every tool has all four.
| Tool | What works | What we did not confirm |
|---|---|---|
| n8n | The Webhook node has a Raw Body option, and the JavaScript Code node has the Node.js crypto module on n8n Cloud. With both, the recipe from the signature page works. On a self-hosted install, n8n lets you import Node.js modules in the Code node | We did not test the flow in n8n. The exact way to read the raw body inside the Code node changes between versions; check the documentation for your version |
| Make | The SHA256 hash function accepts an HMAC key, with the key encoding set to Base64 and the output in Base64: that is the signature calculation. JSON pass-through delivers the body as text | We did not confirm that the pass-through text is byte-for-byte identical to the body sent. We found no constant-time comparison among Make’s functions: the comparison becomes a plain equality. We also did not check Make’s date functions for rejecting a webhook-timestamp outside the window |
| Zapier | Catch Raw Hook delivers the raw body and the headers, and JavaScript Code by Zapier runs Node.js with the standard library, which includes crypto | We did not test the flow in Zapier. The names of the fields in which the raw body and the headers reach the code step are not in the documentation we checked |
When in doubt, or when the tool cannot check the signature, follow the pattern in the next section.
The safe pattern: the webhook notifies, the API confirms
Section titled “The safe pattern: the webhook notifies, the API confirms”Treat the event as a notice that something changed, not as the source of the data. When it arrives, take the object’s type and id and read the object through the API, with your key. A fake event, sent by someone who found your trigger URL, at most makes you read a record that already exists; it cannot put made-up data into your flow, because the data comes from the API.
That does not replace the signature when the flow’s action depends only on the event having arrived (sending a “sale paid” email, for example): in that case, check the signature, or confirm through the API that the sale really is paid before acting.
# The event arrived with data.object = { "object": "sale", "id": "3c5e..." }.# Read the object through the API before using it.curl "https://api.fatureihoje.com/public/v1/sales/3c5e7a9b-1d3f-4b5d-8f1a-3c5e7a9b1d3f" \ -H "Authorization: Bearer $FH_API_KEY"// The body the webhook trigger received.const event = { id: 'evt_9b2f4c7e1a3d4f6b8c0e2a4d6f8b1c3e', type: 'sale.paid', data: { object: { object: 'sale', id: '3c5e7a9b-1d3f-4b5d-8f1a-3c5e7a9b1d3f' } },};
// Object type -> API route. Add the ones your flow uses.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(`unhandled type: ${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('The record no longer exists (or does not belong to this company).');} else { const object = await res.json(); if (!res.ok) throw new Error(`${res.status} ${object.error.code}: ${object.error.message}`); // From here on, use `object`, not the event body. if (event.type === 'sale.paid' && object.payment_status !== 'paid') { console.log('The sale is not paid right now: do not act on the event.'); } else { console.log(object.id, object.updated_at); }}<?php
// The body the webhook trigger received.$event = [ 'id' => 'evt_9b2f4c7e1a3d4f6b8c0e2a4d6f8b1c3e', 'type' => 'sale.paid', 'data' => ['object' => ['object' => 'sale', 'id' => '3c5e7a9b-1d3f-4b5d-8f1a-3c5e7a9b1d3f']],];
// Object type -> API route. Add the ones your flow uses.$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('unhandled type: ' . $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 'The record no longer exists (or does not belong to this company).', PHP_EOL;} elseif ($status !== 200) { throw new RuntimeException($status . ' ' . $object['error']['code'] . ': ' . $object['error']['message']);} else { // From here on, use $object, not the event body. if ($event['type'] === 'sale.paid' && $object['payment_status'] !== 'paid') { echo 'The sale is not paid right now: do not act on the event.', PHP_EOL; } else { echo $object['id'], ' ', $object['updated_at'], PHP_EOL; }}import jsonimport osimport urllib.errorimport urllib.request
# The body the webhook trigger received.event = { "id": "evt_9b2f4c7e1a3d4f6b8c0e2a4d6f8b1c3e", "type": "sale.paid", "data": {"object": {"object": "sale", "id": "3c5e7a9b-1d3f-4b5d-8f1a-3c5e7a9b1d3f"}},}
# Object type -> API route. Add the ones your flow uses.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"unhandled type: {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) # From here on, use obj, not the event body. if event["type"] == "sale.paid" and obj["payment_status"] != "paid": print("The sale is not paid right now: do not act on the event.") else: print(obj["id"], obj["updated_at"])except urllib.error.HTTPError as failure: if failure.code == 404: print("The record no longer exists (or does not belong to this company).") else: error = json.load(failure)["error"] raise SystemExit(f"{failure.code} {error['code']}: {error['message']}")On a .deleted event, the 404 is the expected answer: the record was deleted. On the others, the read returns the object as it is now, which may be newer than the event. That is what you want when the goal is to mirror the current state.
Without webhooks: polling on a schedule
Section titled “Without webhooks: polling on a schedule”All three tools have a schedule trigger. A flow that runs every hour and reads the list with updated_after brings in what changed, without depending on receiving anything. For that, the flow has to keep the starting point between one run and the next, in the storage the tool offers. The recipe is in Incremental sync with updated_after, and the same rules apply: the same filters on every page, an overlap window, and writes by id.
Every call the tool makes uses the company’s request budget. See Rate limits.
Next step
Section titled “Next step”- Create an endpoint: register the trigger URL.
- Verify the signature: the full recipe, with code.