Priručnik

Integracija Fittlea u portal – korak po korak.

Za programere portala, lanaca i vlastitih sustava: ugradite konfigurator, primajte upite webhookom, radite s API-jem i pokažite 3D pregled majstoru. S primjerima u Node.js-u, PHP-u i curlu. Osnovna integracija traje otprilike sat vremena.

1. Kako funkcionira

Ništa ne hostate i ništa ne instalirate. Cijeli tijek ima četiri koraka:

  1. Konfigurator radi na vašoj stranici u iframeu (jedan <script>). Kupac u 3D-u dizajnira kuhinju, ormar, regal za dnevni boravak, namještaj ili kupaonicu.
  2. Šalje upit (ime, e-mail, telefon, zahtjevi, fotografije). Fittle ga sprema pod vašim ključem.
  3. Primate ga na tri načina: e-mailom (s 3D poveznicom), webhookom (potpisani POST na vaš poslužitelj – odmah) i putem API-ja (bilo kada kasnije, uključujući fotografije).
  4. Dijelite ga majstorima – svaki upit ima poveznicu na 3D pregled koja se otvara bez računa.

Što trebate: račun stolara na app.getfittle.com s naplatom po upitu (portali i lanci ne plaćaju paket nego svaki primljeni upit) – nakon prebacivanja u računu se pojavljuje kartica Integracija s webhookom i API ključem. Pišite nam, prebacit ćemo vas.

Bez naplate po upitu nema kartice Integracija, a API pozivi vraćaju 403 integration_off. Samo ugrađivanje konfiguratora (korak 1) radi i na običnom paketu.

Korak 1 – Ugradnja konfiguratora (5 minuta)

Najjednostavniji način: jedna skripta na stranici gdje konfigurator treba biti. Ključ FITT-… nalazi se u računu; vezan je uz vaše domene (postavljaju se u računu), pa se ne može zloupotrijebiti s druge stranice.

<div id="configurator-hr"></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. Jezik konfiguratora je zadani jezik vašeg računa; kupac ga može promijeniti u zaglavlju.

Portal s prijavljenim korisnicima: umjesto data-target pozovite HNL.mount() – unaprijed popunite ime, e-mail i telefon u obrascu upita i saznajte kada je upit poslan:

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

Događaji HNL.on: ready {product}, quote {id, product, design}, design (odgovor na HNL.getDesign(cb)), height {height}. HNL.prefill({…}) može se pozvati i kasnije.

Savjet. Događaj quote stiže u kupčev preglednik – prikladno za stranicu zahvale ili preusmjeravanje. Za obradu na poslužitelju koristite webhook (korak 2), koji je potpisan.

Korak 2 – Webhook (20 minuta)

U računu → Integracija postavite Webhook URL (https) i tajni ključ (bilo koji dugi niz, npr. 32 nasumična znaka). Pri svakom upitu šaljemo:

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

Pravila dostave:

  • Odgovorite 2xx unutar 8 sekundi. Obrađujte tek nakon odgovora (red, worker) – inače prijeti timeout.
  • Kod greške ili timeouta ponavljamo nakon 10 s, 1 min i 5 min, zatim odustajemo – upit uvijek možete dohvatiti putem API-ja. Preusmjeravanja (3xx) ne slijedimo.
  • Isti X-Furniconf-Delivery = ista dostava. Spremajte ga i ignorirajte duplikate (idempotentnost).
  • Fotografije kupca nisu u webhooku (samo photoCount) – preuzmite ih putem API-ja (korak 3).

Provjera potpisa

Potpis je HMAC-SHA256 vašim tajnim ključem nad nizom timestamp + "." + sirovo tijelo. Uspoređujte u konstantnom vremenu:

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

Isto u PHP-u:

<?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: gumb Pošalji testni webhook u računu šalje događaj webhook.test s istim zaglavljima; rezultat (HTTP status, pokušaji) vidite u računu, a mi u logu.

Najčešća greška: potpis se ne podudara jer je framework prvo parsirao i ponovno serijalizirao tijelo (drugi razmaci, redoslijed ključeva). Potpisuje se sirovo tijelo – u Expressu express.raw() prije express.json(), u PHP-u php://input, u Djangu request.body, u Laravelu $request->getContent().

Korak 3 – API (15 minuta)

U računu → Integracija → Generiraj API ključ. Ključ fak_… prikazuje se samo jednom – spremite ga u tajne poslužitelja, nikad u frontend. Svaki poziv nosi Authorization: Bearer fak_…, tijela su JSON, ograničenje 600 poziva na 10 minuta.

Novi upiti (najnoviji prvi; fotografije samo kao broj):

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

Jedan upit s popisom fotografija i preuzimanje fotografije:

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

Promjena statusa – da vi i mi vidimo gdje je upit:

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

Poveznica na 3D pregled za majstora (bez računa, potpisana, vrijedi zadani broj dana):

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 webhooka: periodično dohvaćanje (npr. svakih 5 minuta):

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

Statusi upita: newseencontactedquotedwon / lost. Ostale putanje: GET /api/portal/me (vaš profil i postavke), POST /api/portal/webhook/test.

Odgovori s greškom imaju oblik { "error": "…", "reason": "…", "reqId": "…" } – pri prijavi problema navedite reqId.

Korak 4 – 3D pregled za majstora i u portalu

Svaki upit ima poveznicu iz view-link: majstor je otvara u pregledniku (i na mobitelu) i vidi točno ono što je kupac dizajnirao – rotacija, kote, otvaranje vrata, šetnja, bez prijave. Pošaljite je e-mailom ili SMS-om ili je prikažite u detalju upita u svom portalu:

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

Poveznica je potpisana i vremenski ograničena (days, zadano 30). Nakon isteka generirate novu – upit ostaje spremljen.

Korak 5 – Što je u JSON-u dizajna

design ima dva sloja: čitljiv opis za ljude i state za 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
}
  • Čitljive vrijednosti (oblik, dimenzije, moduli i njihov sadržaj, dekori s kodovima, uređaji, prostorija s otvorima) uvijek su na zadanom jeziku vašeg računa – bez obzira na kojem je jeziku kupac klikao.
  • state je sirovo stanje konfiguratora (kodovi, ne tekstovi). Zahvaljujući njemu dizajn se bilo kada može učitati natrag u 3D; ne mijenjajte ga.
  • room.cart (samo u cjenovnom načinu): košarica s kodovima proizvoda, količinama i iznosima ako je kupac u prostoriju stavio vaše 3D objekte ili pločice.
  • Format je stabilan – nova polja se samo dodaju, postojeća se nikad ne preimenuju.

Naplata i ograničenja

  • Plaćate za svaki primljeni upit dogovorenu cijenu bez PDV-a; mjesečni račun stiže e-mailom (PDF), izvještaj o upitima vidite u računu.
  • API: 600 poziva / 10 minuta po ključu; webhook: 8 s za odgovor, 4 pokušaja.
  • Upit kupca ograničen je veličinom (dizajn do 350 kB, napomena 4 000 znakova, fotografije do 2 MB) – veće preglednik odbija još prije slanja.

Kontrolni popis prije pokretanja

  • Domene vaše stranice su u računu (zaključavanje ključa) i konfigurator se učitava na produkcijskoj stranici.
  • Webhook: potpis provjeren nad sirovim tijelom, 2xx unutar 8 s, duplikati prema X-Furniconf-Delivery se ignoriraju, test prošao.
  • API ključ je u tajnama poslužitelja; kod curenja generirate novi u računu (stari odmah prestaje vrijediti).
  • Vraćate statuse upita (status) – vidljivi su i u računu.
  • Majstor dobiva view-link, ne JSON.
  • Postavljena je naplata po upitu (inače nedostaje kartica Integracija).

Kodovi grešaka (reason)

KodZnačenje i rješenje
api_key (401)Ključ fak_… je nevažeći ili je generiran novi. Provjerite zaglavlje Authorization.
integration_off (403)Račun nema naplatu po upitu – webhook/API nisu aktivni. Pišite nam.
suspended (403)Račun je suspendiran.
rate_limit (429)Previše poziva (600 / 10 min). Usporite, koristite since.
license, quota, bad_email… (/api/quote)Konfigurator je odbio upit: nevažeća licenca/domena (license), prekoračen mjesečni limit (quota), nevaljan e-mail/telefon/duljina (bad_email, bad_phone, too_long), dizajn veći od 350 kB (too_big).

Podrška

Chat izravno u računu (kartica Podrška) ili e-mail; kod tehničkog problema priložite reqId iz odgovora i X-Furniconf-Delivery iz webhooka. Potpuna referenca: PORTAL.md.