Custom Integration (Webhook)
Send every automation run to your own system as a signed event - n8n, Make, Zapier or your own code.
Last updated: 2026-09-02
fluss.ai runs the built-in automations itself — emails, SMS, valuations. If you need something we don't do — a CRM without a native integration, a notification in your channel, your own lead scoring — you point a custom integration at your own system and we send it a signed event on every run.
You host the logic. We hold none of your credentials.
It works with anything that can receive an HTTP POST: n8n, Make, Zapier, a Lambda or your own backend.
Setup
- Create an automation of type CRM integration
- Choose Custom webhook as the provider
- Enter your webhook URL — this is where we POST
- Click generate next to Signing secret and copy the value
- Click Send test event to check delivery
- Save the automation and attach it to your flow
Copy the signing secret straight away. We store it encrypted and will not show it again. If you lose it, generate a new one and set it in both places.
The secret is what lets your endpoint prove a request really came from fluss.ai. Without it, anyone who learns your URL can post fake leads into your CRM.
The other fields
| Field | Meaning |
|---|---|
| Authorization token | Optional. Sent as a Bearer token if your endpoint requires its own authentication. |
| Include template variables | Adds the full variable map. Larger requests, and rarely needed. |
The event
POST to your URL, Content-Type: application/json.
Headers
| Header | Meaning |
|---|---|
x-fluss-signature | t=<unix seconds>,v1=<hex hmac> — see below |
x-fluss-timestamp | The same timestamp, for convenience |
x-fluss-event | Event name, currently always lead.automation |
x-fluss-delivery | Unique id for this delivery — use it to dedupe |
Authorization | Bearer <your token>, only if you configured one |
Body
{
"version": 1,
"event": "lead.automation",
"executionId": 84213,
"attempt": 1,
"account": { "userId": "user_2ab…", "orgId": null },
"lead": {
"hashId": "K9mQx2",
"locale": "de",
"firstName": "Ada",
"lastName": "Lovelace",
"salutation": "ms",
"email": "ada@example.com",
"phone": "+4915112345678",
"street": "Unter den Linden",
"streetNumber": "1",
"postalCode": "10117",
"city": "Berlin",
"country": "DE",
"latitude": "52.5170",
"longitude": "13.3889",
"segment": "WHG_K",
"spaceLiving": 84,
"spacePlot": null,
"yearOfConstruction": 1998,
"propRooms": 3,
"value": 512000,
"rentMonthly": 1706,
"currency": "EUR",
"valuationUrl": "https://fluss.ai/f/…",
"status": "new",
"source": "website",
"createdAt": "2026-08-18T09:14:22.000Z"
},
"template": { "id": 12, "name": "Push to CRM" }
}
hashId is the lead's public id — use it to call back into fluss.ai.
value is null when the property couldn't be valued automatically — an
unusual segment, or an address outside our coverage. That is normal; handle
the case.
Stability
version is bumped only for a breaking change. New fields can appear at any time — parse defensively and ignore what you don't recognise.
Verifying the signature
Compute HMAC-SHA256 over <timestamp>.<raw body> using your signing secret and compare it against v1 from the header.
Sign the raw body bytes, exactly as received. Re-serialising parsed JSON can reorder keys, and then the signature won't match.
const crypto = require('crypto');
function verify(rawBody, header, secret) {
const parts = Object.fromEntries(
header.split(',').map((p) => {
const [k, ...rest] = p.trim().split('=');
return [k, rest.join('=')];
})
);
const timestamp = Number(parts.t);
if (!Number.isFinite(timestamp)) return false;
// Reject replays. 5 minutes is what we recommend.
if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
const a = Buffer.from(expected, 'hex');
const b = Buffer.from(parts.v1 ?? '', 'hex');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Delivery behaviour
- Timeout 20 seconds. Answer fast and do slow work asynchronously.
- Retries on
5xx,429and network errors: 1m → 5m → 25m, four attempts in total.4xxis treated as permanent and is not retried. - Redirects are not followed. Point us at the final URL.
- Private addresses are refused. We won't call loopback, internal networks or cloud-metadata addresses. Your endpoint must be reachable from the public internet.
- At-least-once. A retry after a timeout can deliver something you already processed. Dedupe on
x-fluss-deliveryor onexecutionId.
Every attempt is visible in fluss.ai on the lead's timeline, with the response status and a snippet of the body.
Testing it
Send test event delivers a sample lead to your URL immediately, signed exactly the way a real event is. Nothing is written to your account.
The body is shape-identical to a real one, with test: true added and obviously fake data (Ada Lovelace, an example.com address).
Use the flag to avoid writing a test contact into a real CRM — but don't take a different code path on it, or a passing test tells you nothing about the real event.
The failure message comes straight from the attempt, so a wrong URL, a bad TLS certificate or a rejected private address all say so directly.
Writing back into fluss.ai
Create a key under Account → API keys, choosing only the scopes you need. API keys require a Pro plan. It's shown once — store it immediately. A key is bound to your account and can be revoked at any time.
Send it as x-fluss-api-key: flk_… or as Authorization: Bearer flk_….
| Endpoint | Scope | Does |
|---|---|---|
external.getLead | leads:read | Fetch the full lead |
external.updateLead | leads:write | Enrich or correct fields |
external.addComment | comments:write | Leave a note on the lead's timeline |
external.attachDocument | documents:write | Attach a file to the lead |
external.whoami | leads:read | Check a key and see its scopes |
curl https://fluss.ai/api/trpc/external.addComment \
-H "x-fluss-api-key: $FLUSS_API_KEY" \
-H "x-raw-output: 1" \
-H "content-type: application/json" \
-d '{"json":{"leadHashId":"K9mQx2","text":"Pushed to HubSpot as #4711"}}'
x-raw-output: 1 gives you plain JSON instead of our internal encoding.
updateLead accepts contact details, address, value, rentMonthly, status, priority, source and valuationUrl. Omitted fields are left alone, so a partial update won't wipe anything.
n8n starter workflow
Download fluss-lead-starter.n8n.json and import it into your own n8n (Workflows → Import from File).
It arrives wired up: webhook trigger → signature verification → field mapping → a placeholder CRM node → a comment back into fluss.ai.
Then:
- Copy the Production URL from the Fluss Webhook node into fluss.ai
- Set
FLUSS_WEBHOOK_SECRETon your n8n to the signing secret - Set
FLUSS_API_KEYif you want the write-back node - Replace the Your CRM node with your actual CRM
Works on n8n Cloud and self-hosted alike — it's your instance either way.
Migrating off our n8n
Customers who wanted custom automation used to get an account on our n8n, and we built and maintained the workflow. That is going away: fluss.ai now runs its automations itself.
| Before | Now |
|---|---|
| Your workflow lived on our n8n | It lives on your n8n (or Make, Zapier …) |
| We operated it | You operate it — and can change it whenever |
| Wrote back with an admin token from us | Writes back with your own API key |
| Our support built it for you | Import the starter workflow — or still ask us |
The scoped key is the real improvement: the old token was far broader than any integration needs. Yours is limited to your account and to the scopes you pick.
Requirements
- fluss.ai Pro or higher (CRM integrations are available from the Pro plan)
- An HTTPS endpoint reachable from the public internet
If you'd rather we built it, the contact link is right on the integration screen. Or write to us at support@fluss.ai.