Kézikönyv

A Fittle integrálása portálba – lépésről lépésre.

Portálok, láncok és saját rendszerek fejlesztőinek: a konfigurátor beágyazása, ajánlatkérések fogadása webhookkal, munka az API-val és a 3D előnézet megmutatása a mesternek. Node.js, PHP és curl példákkal. Az alapintegráció körülbelül egy órát vesz igénybe.

1. Hogyan működik

Semmit sem kell hosztolni vagy telepíteni. A teljes folyamat négy lépés:

  1. A konfigurátor az Ön webhelyén fut iframe-ben (egyetlen <script>). Az ügyfél 3D-ben tervez konyhát, szekrényt, nappali falat, bútort vagy fürdőszobát.
  2. Ajánlatkérést küld (név, e-mail, telefon, kérések, fotók). A Fittle az Ön kulcsa alatt tárolja.
  3. Ön háromféleképpen kapja meg: e-mailben (3D linkkel), webhookkal (aláírt POST az Ön szerverére – azonnal) és az API-n keresztül (bármikor később, fotókkal együtt).
  4. Kiosztja a mestereknek – minden ajánlatkéréshez tartozik egy 3D előnézeti link, amely fiók nélkül megnyílik.

Amire szüksége van: asztalosfiók az app.getfittle.com oldalon ajánlatkérésenkénti elszámolással (a portálok és láncok nem csomagot fizetnek, hanem minden beérkezett ajánlatkérés után) – az átállítás után a fiókban megjelenik az Integráció kártya a webhookkal és az API-kulccsal. Írjon nekünk, átállítjuk.

Ajánlatkérésenkénti elszámolás nélkül nincs Integráció kártya, és az API-hívások 403 integration_off választ adnak. Maga a konfigurátor beágyazása (1. lépés) normál csomaggal is működik.

1. lépés – A konfigurátor beágyazása (5 perc)

A legegyszerűbb mód: egy szkript azon az oldalon, ahol a konfigurátornak lennie kell. A FITT-… kulcs a fiókban található; az Ön domainjeihez van kötve (a fiókban állítható), így más oldalról nem használható vissza.

<div id="configurator-hu"></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. A konfigurátor nyelve a fiók alapértelmezett nyelve; az ügyfél a fejlécben átválthatja.

Portál bejelentkezett felhasználókkal: data-target helyett hívja a HNL.mount()-ot – kitölti a nevet, e-mailt és telefont az ajánlatkérő űrlapon, és értesül, amikor az ajánlatkérés elment:

<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>

HNL.on események: ready {product}, quote {id, product, design}, design (válasz a HNL.getDesign(cb)-re), height {height}. A HNL.prefill({…}) később is hívható.

Tipp. A quote esemény az ügyfél böngészőjében érkezik – köszönőoldalhoz vagy átirányításhoz jó. A szerveroldali feldolgozáshoz használja a webhookot (2. lépés), az alá van írva.

2. lépés – Webhook (20 perc)

A fiókban → Integráció állítsa be a Webhook URL-t (https) és a titkos kulcsot (bármilyen hosszú karakterlánc, pl. 32 véletlen karakter). Minden ajánlatkérésnél ezt küldjük:

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": { … } }
    }
  }
}

Kézbesítési szabályok:

  • Válaszoljon 2xx-szel 8 másodpercen belül. A feldolgozást a válasz után végezze (sor, worker) – különben időtúllépés fenyeget.
  • Hiba vagy időtúllépés esetén 10 mp, 1 perc és 5 perc után újrapróbáljuk, majd feladjuk – az ajánlatkérést bármikor letöltheti az API-n keresztül. Átirányításokat (3xx) nem követünk.
  • Ugyanaz az X-Furniconf-Delivery = ugyanaz a kézbesítés. Tárolja, és a duplikátumokat hagyja figyelmen kívül (idempotencia).
  • Az ügyfél fotói nincsenek a webhookban (csak photoCount) – az API-n keresztül töltheti le őket (3. lépés).

Aláírás ellenőrzése

Az aláírás HMAC-SHA256 a titkos kulccsal a timestamp + "." + nyers törzs karakterlánc felett. Állandó idejű összehasonlítást használjon:

// 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);

Ugyanez PHP-ban:

<?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']
}

Teszt: a fiókban a Teszt webhook küldése gomb webhook.test eseményt küld ugyanazokkal a fejlécekkel; az eredményt (HTTP státusz, próbálkozások) Ön a fiókban, mi a logban látjuk.

Leggyakoribb hiba: az aláírás nem egyezik, mert a keretrendszer előbb feldolgozta és újra szerializálta a törzset (más szóközök, kulcssorrend). A nyers törzs van aláírva – Expressben express.raw() az express.json() előtt, PHP-ban php://input, Djangóban request.body, Laravelben $request->getContent().

3. lépés – API (15 perc)

A fiókban → Integráció → API-kulcs generálása. A fak_… kulcs csak egyszer jelenik meg – tárolja a szerver titkai között, soha ne a frontendben. Minden hívás Authorization: Bearer fak_… fejlécet visel, a törzsek JSON-ok, a limit 600 hívás 10 percenként.

Új ajánlatkérések (legújabb elöl; fotók csak számként):

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": { … } }
] }

Egy ajánlatkérés a fotólistával és egy fotó letöltése:

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

Státuszváltás – hogy Ön és mi is lássuk, hol tart az ajánlatkérés:

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", … } }

3D előnézeti link a mesternek (fiók nélkül, aláírva, a megadott napig érvényes):

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" }

Webhook nélkül: rendszeres lekérdezés (pl. 5 percenként):

# 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_…"

Ajánlatkérés státuszai: newseencontactedquotedwon / lost. További útvonalak: GET /api/portal/me (profil és beállítások), POST /api/portal/webhook/test.

A hibaválaszok formája { "error": "…", "reason": "…", "reqId": "…" } – probléma bejelentésekor adja meg a reqId-t.

4. lépés – 3D előnézet a mesternek és a portálban

Minden ajánlatkéréshez tartozik egy view-link link: a mester böngészőben (mobilon is) megnyitja, és pontosan azt látja, amit az ügyfél tervezett – forgatás, méretek, ajtók nyitása, bejárás, bejelentkezés nélkül. Küldje el e-mailben, SMS-ben, vagy mutassa a portál ajánlatkérés-részleteiben:

<!-- 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>

A link aláírt és időben korlátozott (days, alapértelmezés 30). Lejárat után újat generál – az ajánlatkérés tárolva marad.

5. lépés – Mi van a terv JSON-jában

A design két rétegű: olvasható leírás embereknek és state a gépnek:

{
  "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
}
  • Az olvasható értékek (forma, méretek, modulok és tartalmuk, dekorok kódokkal, készülékek, helyiség nyílásokkal) mindig a fiók alapértelmezett nyelvén vannak – függetlenül attól, milyen nyelven kattintott az ügyfél.
  • A state a konfigurátor nyers állapota (kódok, nem szövegek). Ennek köszönhetően a terv bármikor visszatölthető 3D-be; ne módosítsa.
  • A room.cart (csak ármódban): kosár termékkódokkal, mennyiségekkel és összegekkel, ha az ügyfél az Ön 3D objektumait vagy csempéit helyezte a helyiségbe.
  • A formátum stabil – új mezők csak hozzáadódnak, a meglévők nem kapnak új nevet.

Elszámolás és limitek

  • Minden beérkezett ajánlatkérés után a megállapodott nettó árat fizeti; a havi számla e-mailben érkezik (PDF), az ajánlatkérések kimutatása a fiókban látható.
  • API: 600 hívás / 10 perc kulcsonként; webhook: 8 mp a válaszra, 4 próbálkozás.
  • Az ügyfél ajánlatkérése méretkorlátos (terv legfeljebb 350 kB, megjegyzés 4 000 karakter, fotók legfeljebb 2 MB) – a nagyobbat a böngésző már küldés előtt elutasítja.

Ellenőrzőlista indulás előtt

  • A webhely domainjei a fiókban vannak (kulcszár), és a konfigurátor betölt az éles oldalon.
  • Webhook: aláírás a nyers törzs felett ellenőrizve, 2xx válasz 8 mp-en belül, duplikátumok X-Furniconf-Delivery szerint figyelmen kívül hagyva, teszt sikeres.
  • Az API-kulcs a szerver titkai között van; kiszivárgás esetén a fiókban újat generál (a régi azonnal érvénytelen).
  • Visszaküldi az ajánlatkérés státuszait (status) – a fiókban is látszanak.
  • A mester a view-link-et kapja, nem JSON-t.
  • Be van állítva az ajánlatkérésenkénti elszámolás (különben hiányzik az Integráció kártya).

Hibakódok (reason)

KódJelentés és megoldás
api_key (401)A fak_… kulcs érvénytelen, vagy új lett generálva. Ellenőrizze az Authorization fejlécet.
integration_off (403)A fióknak nincs ajánlatkérésenkénti elszámolása – a webhook/API inaktív. Írjon nekünk.
suspended (403)A fiók fel van függesztve.
rate_limit (429)Túl sok hívás (600 / 10 perc). Lassítson, használja a since-t.
license, quota, bad_email… (/api/quote)A konfigurátor elutasította az ajánlatkérést: érvénytelen licenc/domain (license), havi limit túllépve (quota), érvénytelen e-mail/telefon/hossz (bad_email, bad_phone, too_long), 350 kB feletti terv (too_big).

Támogatás

Csevegés közvetlenül a fiókban (Támogatás kártya) vagy e-mail; technikai problémánál csatolja a válasz reqId-jét és a webhook X-Furniconf-Delivery értékét. Teljes referencia: PORTAL.md.