Guida

Integrare Fittle in un portale – passo dopo passo.

Per sviluppatori di portali, catene e sistemi personalizzati: incorporare il configuratore, ricevere le richieste via webhook, usare l’API e mostrare l’anteprima 3D all’artigiano. Con esempi in Node.js, PHP e curl. Un’integrazione di base richiede circa un’ora.

1. Come funziona

Non ospitate e non installate nulla. L’intero flusso ha quattro passi:

  1. Il configuratore gira sul vostro sito in un iframe (un solo <script>). Il cliente progetta in 3D cucina, armadio, parete soggiorno, mobili o bagno.
  2. Invia una richiesta (nome, e-mail, telefono, esigenze, foto). Fittle la salva sotto la vostra chiave.
  3. La ricevete in tre modi: via e-mail (con link 3D), via webhook (POST firmato al vostro server – subito) e tramite API (in qualsiasi momento, foto comprese).
  4. La passate ai vostri artigiani – ogni richiesta ha un link all’anteprima 3D che si apre senza account.

Cosa serve: un account falegname su app.getfittle.com con fatturazione a richiesta (portali e catene non pagano un piano ma ogni richiesta ricevuta) – dopo il passaggio, nell’account compare la scheda Integrazione con webhook e chiave API. Scriveteci e vi attiviamo.

Senza fatturazione a richiesta la scheda Integrazione non c’è e le chiamate API restituiscono 403 integration_off. Il solo inserimento del configuratore (passo 1) funziona anche con un piano normale.

Passo 1 – Incorporare il configuratore (5 minuti)

Il modo più semplice: uno script nella pagina dove deve comparire il configuratore. La chiave FITT-… è nell’account; è legata ai vostri domini (impostati nell’account), quindi non può essere usata da un altro sito.

<div id="configurator-it"></div>
<script src="https://app.getfittle.com/embed.js"
        data-key="FITT-XXXX-XXXX-XXXX"
        data-product="kitchen"
        data-target="#configurator"></script>

data-product: kitchen · wardrobe · living · furniture · bathroom. La lingua del configuratore è la lingua predefinita del vostro account; il cliente può cambiarla nell’intestazione.

Portale con utenti registrati: invece di data-target chiamate HNL.mount() – precompilate nome, e-mail e telefono nel modulo di richiesta e venite avvisati quando una richiesta è stata inviata:

<script src="https://app.getfittle.com/embed.js" data-key="FITT-XXXX-XXXX-XXXX"></script>
<script>
  HNL.mount('#configurator', {
    product: 'wardrobe',                 // kitchen | wardrobe | living | furniture | bathroom
    fill: true,                          // fill the parent's height (default 820 px)
    prefill: {                           // logged-in user of your portal (editable in the form)
      name: 'Jane Doe', email: 'jane@example.com', phone: '+421 900 000 000'
    }
  });
  HNL.on('ready', function (e) { console.log('configurator running:', e.product); });
  HNL.on('quote', function (e) {
    // request sent – e.id is the request id (DM…), e.design the design JSON
    location.href = '/thank-you?request=' + encodeURIComponent(e.id);
  });
</script>

Eventi HNL.on: ready {product}, quote {id, product, design}, design (risposta a HNL.getDesign(cb)), height {height}. HNL.prefill({…}) si può chiamare anche più tardi.

Suggerimento. L’evento quote arriva nel browser del cliente – utile per una pagina di ringraziamento o un redirect. Per l’elaborazione lato server usate il webhook (passo 2), che è firmato.

Passo 2 – Webhook (20 minuti)

Nell’account → Integrazione impostate l’URL del webhook (https) e un segreto (una stringa lunga qualsiasi, es. 32 caratteri casuali). A ogni richiesta inviamo:

POST https://portal.example.com/furniconf/webhook
Content-Type: application/json
X-Furniconf-Event: quote.created
X-Furniconf-Delivery: WMTZ7Q2K9A1B
X-Furniconf-Timestamp: 1789300000
X-Furniconf-Signature: sha256=3f1a…9c

{
  "id": "WMTZ7Q2K9A1B",
  "event": "quote.created",
  "createdAt": "2026-09-16T09:12:33.000Z",
  "shopKey": "FITT-XXXX-XXXX-XXXX",
  "data": {
    "design": {
      "id": "DMU3VCTB63BB7",
      "product": "kitchen",
      "status": "new",
      "createdAt": "2026-09-16T09:12:33.000Z",
      "customer": { "name": "Jane Doe", "email": "jane@example.com", "phone": "+421 900 000 000",
                    "note": "Handles in black, please.", "photoCount": 2 },
      "design": { "type": "kitchen", "layout": "…", "dims": { … }, "runs": [ … ], "room": { … }, "state": { … } }
    }
  }
}

Regole di consegna:

  • Rispondete 2xx entro 8 secondi. Elaborate dopo aver risposto (coda, worker) – altrimenti rischiate il timeout.
  • In caso di errore o timeout riproviamo dopo 10 s, 1 min e 5 min, poi rinunciamo – la richiesta si può sempre scaricare via API. I redirect (3xx) non vengono seguiti.
  • Stesso X-Furniconf-Delivery = stessa consegna. Salvatelo e ignorate i duplicati (idempotenza).
  • Le foto del cliente non sono nel webhook (solo photoCount) – scaricatele via API (passo 3).

Verifica della firma

La firma è HMAC-SHA256 con il vostro segreto sulla stringa timestamp + "." + corpo grezzo. Confrontate a tempo costante:

// Node.js / Express – the signature is computed over the RAW body, so read it before any JSON parser
const express = require('express');
const crypto = require('crypto');
const app = express();
const SECRET = process.env.FURNICONF_WEBHOOK_SECRET;

app.post('/furniconf/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const ts  = req.header('X-Furniconf-Timestamp') || '';
  const got = req.header('X-Furniconf-Signature') || '';
  const want = 'sha256=' + crypto.createHmac('sha256', SECRET).update(ts + '.' + req.body).digest('hex');
  if (got.length !== want.length || !crypto.timingSafeEqual(Buffer.from(got), Buffer.from(want))) {
    return res.status(401).end();                       // not from Fittle
  }
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return res.status(401).end();   // replay protection (5 min)

  const evt = JSON.parse(req.body);
  res.status(200).end();                                // answer first (within 8 s) …
  if (evt.event === 'quote.created') {
    const d = evt.data.design;
    // … then process: idempotency by delivery id, save, assign to a craftsman
    saveRequest({ deliveryId: evt.id, requestId: d.id, product: d.product, customer: d.customer, design: d.design });
  }
});
app.listen(3000);

Lo stesso in PHP:

<?php  // PHP – same check with hash_hmac + hash_equals
$secret = getenv('FURNICONF_WEBHOOK_SECRET');
$raw    = file_get_contents('php://input');
$ts     = $_SERVER['HTTP_X_FURNICONF_TIMESTAMP'] ?? '';
$got    = $_SERVER['HTTP_X_FURNICONF_SIGNATURE'] ?? '';
$want   = 'sha256=' . hash_hmac('sha256', $ts . '.' . $raw, $secret);
if (!hash_equals($want, $got) || abs(time() - (int)$ts) > 300) { http_response_code(401); exit; }

http_response_code(200);
$evt = json_decode($raw, true);
if ($evt['event'] === 'quote.created') {
  $d = $evt['data']['design'];
  // $evt['id'] = delivery id (idempotency), $d['id'] = request id, $d['customer'], $d['design']
}

Test: il pulsante Invia webhook di prova nell’account invia un evento webhook.test con le stesse intestazioni; il risultato (stato HTTP, tentativi) è visibile nell’account e nel nostro log.

Errore più comune: la firma non corrisponde perché il framework ha analizzato e riserializzato il corpo (spazi, ordine delle chiavi diversi). Viene firmato il corpo grezzo – in Express express.raw() prima di express.json(), in PHP php://input, in Django request.body, in Laravel $request->getContent().

Passo 3 – API (15 minuti)

Nell’account → Integrazione → Genera chiave API. La chiave fak_… viene mostrata una sola volta – conservatela nei segreti del server, mai nel frontend. Ogni chiamata porta Authorization: Bearer fak_…, i corpi sono JSON, limite 600 chiamate ogni 10 minuti.

Nuove richieste (le più recenti prima; foto solo come numero):

curl -s "https://app.getfittle.com/api/portal/designs?status=new&limit=50" \
  -H "Authorization: Bearer fak_XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"

{ "designs": [
    { "id": "DMU3VCTB63BB7", "product": "kitchen", "status": "new", "createdAt": "2026-09-16T09:12:33.000Z",
      "customer": { "name": "Jane Doe", "email": "jane@example.com", "phone": "+421 900 000 000", "note": "…", "photoCount": 2 },
      "design": { … } }
] }

Una richiesta con l’elenco foto e download di una foto:

curl -s "https://app.getfittle.com/api/portal/designs/DMU3VCTB63BB7?photos=1" \
  -H "Authorization: Bearer fak_…"

{ "design": { "id": "DMU3VCTB63BB7", …,
    "customer": { …, "photoCount": 2,
      "photos": [ { "n": 1, "name": "kitchen-now.jpg", "type": "image/jpeg", "size": 812345,
                    "url": "/api/portal/designs/DMU3VCTB63BB7/photos/1" }, … ] } } }

# photo (binary) – the API key is required here as well
curl -s "https://app.getfittle.com/api/portal/designs/DMU3VCTB63BB7/photos/1" \
  -H "Authorization: Bearer fak_…" -o kitchen-now.jpg

Cambio di stato – così voi e noi vediamo a che punto è la richiesta:

curl -s -X POST "https://app.getfittle.com/api/portal/designs/DMU3VCTB63BB7/status" \
  -H "Authorization: Bearer fak_…" -H "Content-Type: application/json" \
  -d '{ "status": "contacted" }'

{ "ok": true, "design": { "id": "DMU3VCTB63BB7", "status": "contacted", … } }

Link all’anteprima 3D per l’artigiano (senza account, firmato, valido per i giorni indicati):

curl -s -X POST "https://app.getfittle.com/api/portal/designs/DMU3VCTB63BB7/view-link" \
  -H "Authorization: Bearer fak_…" -H "Content-Type: application/json" \
  -d '{ "days": 30 }'

{ "ok": true,
  "url": "https://app.getfittle.com/c/kitchen?k=FITT-XXXX-XXXX-XXXX&load=DMU3VCTB63BB7&vt=eyJ0…&view=1" }

Senza webhook: polling periodico (es. ogni 5 minuti):

# polling instead of a webhook: everything since the last check
curl -s "https://app.getfittle.com/api/portal/designs?since=2026-09-16T00:00:00Z" -H "Authorization: Bearer fak_…"

Stati della richiesta: newseencontactedquotedwon / lost. Altri percorsi: GET /api/portal/me (profilo e impostazioni), POST /api/portal/webhook/test.

Le risposte di errore hanno la forma { "error": "…", "reason": "…", "reqId": "…" } – citate il reqId quando segnalate un problema.

Passo 4 – Anteprima 3D per l’artigiano e nel portale

Ogni richiesta ha un link da view-link: l’artigiano lo apre nel browser (anche da mobile) e vede esattamente ciò che il cliente ha progettato – rotazione, quote, apertura ante, passeggiata, senza login. Inviatelo via e-mail o SMS, o mostratelo nel dettaglio richiesta del vostro portale:

<!-- the request inside your portal: a 3D preview in an iframe (URL from view-link) -->
<iframe src="https://app.getfittle.com/c/kitchen?k=FITT-…&load=DMU3VCTB63BB7&vt=eyJ0…&view=1"
        style="width:100%;height:720px;border:0;border-radius:12px" allow="xr-spatial-tracking"></iframe>

Il link è firmato e a tempo (days, predefinito 30). Alla scadenza generatene uno nuovo – la richiesta resta salvata.

Passo 5 – Cosa c’è nel JSON del progetto

design ha due livelli: una descrizione leggibile per le persone e state per la macchina:

{
  "type": "kitchen",                     // kitchen | wardrobe | living | furniture | bathroom
  "layout": "Rohová (L)",                // readable values – in the shop's default language
  "dims":  { "baseHeight": 72, "wallHeight": 72, "totalHeight": 218, … },
  "runs":  [ { "id": "A", "length": 365, "columns": [ { "width": 80, "base": "Drez", "wall": "Otvorená polica", … } ] } ],
  "decors": { "base": "Dekor 3017", "baseCode": "D3017", … },
  "appliances": { "fridge": 1, "oven": 1, "hob": 1, "sink": 1, … },
  "room":  { "width": 460, "depth": 360, "height": 270, "openings": [ … ], "cart": { … } },
  "state": { … }                         // raw configurator state (codes) – for loading back into 3D
}
  • I valori leggibili (forma, dimensioni, moduli e contenuto, decori con codici, elettrodomestici, stanza con aperture) sono sempre nella lingua predefinita del vostro account – qualunque lingua abbia usato il cliente.
  • state è lo stato grezzo del configuratore (codici, non testi). Permette di ricaricare il progetto in 3D in qualsiasi momento; non modificatelo.
  • room.cart (solo in modalità prezzi): carrello con codici prodotto, quantità e importi se il cliente ha inserito nella stanza i vostri oggetti 3D o piastrelle.
  • Il formato è stabile – i nuovi campi vengono solo aggiunti, quelli esistenti non vengono mai rinominati.

Fatturazione e limiti

  • Pagate per ogni richiesta ricevuta il prezzo concordato IVA esclusa; la fattura mensile arriva via e-mail (PDF), il riepilogo richieste è nell’account.
  • API: 600 chiamate / 10 minuti per chiave; webhook: 8 s per rispondere, 4 tentativi.
  • La richiesta del cliente ha limiti di dimensione (progetto fino a 350 kB, note 4.000 caratteri, foto fino a 2 MB) – oltre, il browser rifiuta prima dell’invio.

Checklist prima del go-live

  • I domini del vostro sito sono nell’account (blocco chiave) e il configuratore si carica sulla pagina di produzione.
  • Webhook: firma verificata sul corpo grezzo, 2xx entro 8 s, duplicati per X-Furniconf-Delivery ignorati, test superato.
  • La chiave API è nei segreti del server; in caso di fuga generatene una nuova nell’account (la vecchia smette subito).
  • Rimandate gli stati delle richieste (status) – sono visibili anche nell’account.
  • L’artigiano riceve il view-link, non il JSON.
  • La fatturazione a richiesta è attiva (altrimenti manca la scheda Integrazione).

Codici di errore (reason)

CodiceSignificato e soluzione
api_key (401)La chiave fak_… non è valida o ne è stata generata una nuova. Controllate l’intestazione Authorization.
integration_off (403)L’account non ha la fatturazione a richiesta – webhook/API inattivi. Scriveteci.
suspended (403)L’account è sospeso.
rate_limit (429)Troppe chiamate (600 / 10 min). Rallentate, usate since.
license, quota, bad_email… (/api/quote)Il configuratore ha rifiutato la richiesta: licenza/dominio non valido (license), tetto mensile superato (quota), e-mail/telefono/lunghezza non validi (bad_email, bad_phone, too_long), progetto oltre 350 kB (too_big).

Supporto

Chat direttamente nell’account (scheda Supporto) o e-mail; per un problema tecnico allegate il reqId della risposta e l’X-Furniconf-Delivery del webhook. Riferimento completo: PORTAL.md.