Príručka

Integrácia Fittle do portálu – krok za krokom.

Pre vývojárov portálov, reťazcov a vlastných systémov: vložiť konfigurátor, prijímať dopyty webhookom, pracovať s API a ukázať 3D náhľad majstrovi. S ukážkami v Node.js, PHP a curl. Základná integrácia trvá približne hodinu.

1. Ako to funguje

Nič nehostujete a nič neinštalujete. Celý tok má štyri kroky:

  1. Konfigurátor beží na vašom webe v iframe (jeden <script>). Zákazník si navrhne kuchyňu, skriňu, obývaciu stenu, nábytok alebo kúpeľňu v 3D.
  2. Odošle dopyt (meno, e-mail, telefón, požiadavky, fotky). Fittle ho uloží pod vaším kľúčom.
  3. Vy ho dostanete trikrát: e-mailom (s odkazom na 3D), webhookom (podpísaný POST na váš server – hneď) a cez API (kedykoľvek neskôr, aj s fotkami).
  4. Rozdáte ho majstrom – každý dopyt má odkaz na 3D náhľad, ktorý sa otvorí bez účtu.

Čo potrebujete: účet stolára na app.getfittle.com s účtovaním za dopyt (portály a reťazce neplatia balík, ale za každý prijatý dopyt) – po prepnutí sa v účte objaví karta Integrácia s webhookom a API kľúčom. Napíšte nám, prepneme vás.

Bez účtovania za dopyt karta Integrácia nie je a volania API vrátia 403 integration_off. Samotné vloženie konfigurátora (krok 1) funguje aj na bežnom balíku.

Krok 1 – Vloženie konfigurátora (5 minút)

Najjednoduchší spôsob: jeden skript na stránke, kde má byť konfigurátor. Kľúč FITT-… nájdete v účte; je zviazaný s vašimi doménami (nastavíte ich v účte), takže sa z inej stránky nedá zneužiť.

<div id="configurator"></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. Jazyk konfigurátora je predvolený jazyk vášho účtu; zákazník si ho môže prepnúť v hlavičke.

Portál s prihlásenými používateľmi: namiesto data-target zavolajte HNL.mount() – doplníte meno, e-mail a telefón do formulára dopytu a dozviete sa, keď bol dopyt odoslaný:

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

Udalosti HNL.on: ready {product}, quote {id, product, design}, design (odpoveď na HNL.getDesign(cb)), height {height}. HNL.prefill({…}) ide zavolať aj neskôr.

Tip. Udalosť quote príde v prehliadači zákazníka – hodí sa na poďakovanie alebo presmerovanie. Na spracovanie dopytu na serveri použite webhook (krok 2), ten je podpísaný.

Krok 2 – Webhook (20 minút)

V účte → Integrácia nastavte Webhook URL (https) a tajný kľúč (ľubovoľný dlhý reťazec, napr. 32 náhodných znakov). Pri každom dopyte vám pošleme:

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

Pravidlá doručovania:

  • Odpovedzte 2xx do 8 sekúnd. Spracovanie robte až po odpovedi (fronta, ďalšie vlákno) – inak hrozí timeout.
  • Pri chybe alebo timeoute opakujeme po 10 s, 1 min a 5 min, potom to vzdáme – dopyt si vždy viete stiahnuť cez API. Presmerovania (3xx) nenasledujeme.
  • Rovnaké X-Furniconf-Delivery = rovnaké doručenie. Ukladajte si ho a duplicitu ignorujte (idempotencia).
  • Fotky zákazníka vo webhooku nie sú (len photoCount) – stiahnete ich cez API (krok 3).

Overenie podpisu

Podpis je HMAC-SHA256 tajným kľúčom nad reťazcom timestamp + "." + surové telo. Porovnávajte v konštantnom čase:

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

To isté v 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: tlačidlo Otestovať webhook v účte pošle udalosť webhook.test s rovnakými hlavičkami; výsledok (HTTP stav, pokusy) vidíte v účte aj my v logu.

Najčastejšia chyba: podpis nesedí, lebo framework telo najprv rozparsoval a znova serializoval (iné medzery, poradie kľúčov). Podpisuje sa surové telo – v Expresse express.raw() pred express.json(), v PHP php://input, v Django request.body, v Laravel $request->getContent().

Krok 3 – API (15 minút)

V účte → Integrácia → Vygenerovať API kľúč. Kľúč fak_… sa zobrazí len raz – uložte ho do tajomstiev servera, nikdy do frontendu. Každé volanie má hlavičku Authorization: Bearer fak_…, telá sú JSON, limit 600 volaní za 10 minút.

Nové dopyty (najnovšie prvé; fotky len ako počet):

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

Jeden dopyt aj so zoznamom fotiek a stiahnutie fotky:

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

Zmena stavu – aby ste v účte aj u nás videli, kde dopyt je:

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

Odkaz na 3D náhľad pre majstra (bez účtu, podpísaný, platí zadaný počet dní):

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

Bez webhooku: pravidelné sťahovanie (napr. každých 5 minút):

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

Stavy dopytu: newseencontactedquotedwon / lost. Ďalšie cesty: GET /api/portal/me (váš profil a nastavenia), POST /api/portal/webhook/test.

Chybové odpovede majú tvar { "error": "…", "reason": "…", "reqId": "…" } – pri hlásení problému nám pošlite reqId.

Krok 4 – 3D náhľad pre majstra a v portáli

Každý dopyt má odkaz z view-link: majster ho otvorí v prehliadači (aj na mobile) a vidí presne to, čo zákazník navrhol – otáčanie, kóty, otváranie dvierok, prechádzka, bez prihlásenia. Odkaz pošlite e-mailom, SMS-kou alebo ho ukážte v detaile dopytu vo vašom portáli:

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

Odkaz je podpísaný a časovo obmedzený (days, predvolene 30). Po vypršaní si vygenerujete nový – dopyt zostáva uložený.

Krok 5 – Čo je v JSON návrhu

design má dve vrstvy: čitateľný popis pre ľudí a state pre stroj:

{
  "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
}
  • Čitateľné hodnoty (tvar, rozmery, moduly a ich obsah, dekory s kódmi, spotrebiče, miestnosť s otvormi) sú vždy v predvolenom jazyku vášho účtu – bez ohľadu na to, v akom jazyku zákazník klikal.
  • state je surový stav konfigurátora (kódy, nie texty). Vďaka nemu sa návrh dá kedykoľvek načítať späť do 3D; nemeňte ho.
  • room.cart (len v cenovom režime): košík s kódmi produktov, množstvami a sumami, ak zákazník vložil do izby vaše 3D objekty alebo dlažbu.
  • Formát je stabilný – nové polia sa len pridávajú, existujúce sa neprejmenúvajú.

Účtovanie a limity

  • Platíte za každý prijatý dopyt dohodnutú cenu bez DPH; mesačnú faktúru dostanete e-mailom (PDF), výkaz dopytov vidíte v účte.
  • API: 600 volaní / 10 minút na kľúč; webhook: 8 s na odpoveď, 4 pokusy.
  • Dopyt zákazníka je limitovaný veľkosťou (návrh do 350 kB, poznámka 4 000 znakov, fotky do 2 MB) – väčšie prehliadač odmietne ešte pred odoslaním.

Kontrolný zoznam pred spustením

  • Domény vášho webu sú v účte (zámok kľúča) a konfigurátor sa na ostrej stránke načíta.
  • Webhook: podpis overený nad surovým telom, odpoveď 2xx do 8 s, duplicity podľa X-Furniconf-Delivery sa ignorujú, test prešiel.
  • API kľúč je v tajomstvách servera; pri úniku ho v účte vygenerujete nanovo (starý prestane platiť okamžite).
  • Stavy dopytov posielate späť (status) – vidíte ich aj v účte.
  • Majster dostane view-link, nie JSON.
  • Máte nastavené účtovanie za dopyt (inak karta Integrácia chýba).

Chybové kódy (reason)

KódVýznam a riešenie
api_key (401)Kľúč fak_… je neplatný alebo bol vygenerovaný nový. Skontrolujte hlavičku Authorization.
integration_off (403)Účet nemá účtovanie za dopyt – webhook/API nie sú aktívne. Napíšte nám.
suspended (403)Účet je pozastavený.
rate_limit (429)Príliš veľa volaní (600 / 10 min). Spomaľte, použite since.
license, quota, bad_email… (/api/quote)Konfigurátor odmietol dopyt: neplatná licencia/doména (license), prekročený mesačný strop (quota), nevalidný e-mail/telefón/dĺžka (bad_email, bad_phone, too_long), návrh nad 350 kB (too_big).

Podpora

Chat priamo v účte (karta Podpora) alebo e-mail; pri technickom probléme priložte reqId z odpovede a X-Furniconf-Delivery z webhooku. Kompletný referenčný popis: PORTAL.sk.md.