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.

You don't need this for onOffice or Propstack — those are built in. See onOffice and Propstack.

Setup

  1. Create an automation of type CRM integration
  2. Choose Custom webhook as the provider
  3. Enter your webhook URL — this is where we POST
  4. Click generate next to Signing secret and copy the value
  5. Click Send test event to check delivery
  6. 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

FieldMeaning
Authorization tokenOptional. Sent as a Bearer token if your endpoint requires its own authentication.
Include template variablesAdds the full variable map. Larger requests, and rarely needed.

The event

POST to your URL, Content-Type: application/json.

Headers

HeaderMeaning
x-fluss-signaturet=<unix seconds>,v1=<hex hmac> — see below
x-fluss-timestampThe same timestamp, for convenience
x-fluss-eventEvent name, currently always lead.automation
x-fluss-deliveryUnique id for this delivery — use it to dedupe
AuthorizationBearer <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, 429 and network errors: 1m → 5m → 25m, four attempts in total. 4xx is 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-delivery or on executionId.

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_….

EndpointScopeDoes
external.getLeadleads:readFetch the full lead
external.updateLeadleads:writeEnrich or correct fields
external.addCommentcomments:writeLeave a note on the lead's timeline
external.attachDocumentdocuments:writeAttach a file to the lead
external.whoamileads:readCheck 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:

  1. Copy the Production URL from the Fluss Webhook node into fluss.ai
  2. Set FLUSS_WEBHOOK_SECRET on your n8n to the signing secret
  3. Set FLUSS_API_KEY if you want the write-back node
  4. 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.

BeforeNow
Your workflow lived on our n8nIt lives on your n8n (or Make, Zapier …)
We operated itYou operate it — and can change it whenever
Wrote back with an admin token from usWrites back with your own API key
Our support built it for youImport 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.

Related Articles