1. Kako deluje
Ničesar ne gostite in ničesar ne nameščate. Celoten potek ima štiri korake:
- Konfigurator teče na vaši strani v iframu (en
<script>). Stranka v 3D oblikuje kuhinjo, omaro, dnevno steno, pohištvo ali kopalnico. - Pošlje povpraševanje (ime, e-pošta, telefon, zahteve, fotografije). Fittle ga shrani pod vašim ključem.
- Prejmete ga na tri načine: po e-pošti (s 3D povezavo), prek webhooka (podpisan POST na vaš strežnik – takoj) in prek API-ja (kadar koli pozneje, tudi s fotografijami).
- Razdelite ga mojstrom – vsako povpraševanje ima povezavo do 3D predogleda, ki se odpre brez računa.
Kaj potrebujete: račun mizarja na app.getfittle.com z obračunom na povpraševanje (portali in verige ne plačujejo paketa, temveč vsako prejeto povpraševanje) – po preklopu se v računu pojavi kartica Integracija z webhookom in API ključem. Pišite nam, preklopili vas bomo.
403 integration_off. Sama vgradnja konfiguratorja (korak 1) deluje tudi na običajnem paketu.Korak 1 – Vgradnja konfiguratorja (5 minut)
Najenostavnejši način: ena skripta na strani, kjer naj bo konfigurator. Ključ FITT-… najdete v računu; vezan je na vaše domene (nastavite jih v računu), zato ga z druge strani ni mogoče zlorabiti.
<div id="configurator-sl"></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 konfiguratorja je privzeti jezik vašega računa; stranka ga lahko preklopi v glavi.
Portal s prijavljenimi uporabniki: namesto data-target pokličite HNL.mount() – vnaprej izpolnite ime, e-pošto in telefon v obrazcu povpraševanja in izveste, kdaj je bilo povpraševanje poslano:
<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>
Dogodki HNL.on: ready {product}, quote {id, product, design}, design (odgovor na HNL.getDesign(cb)), height {height}. HNL.prefill({…}) lahko pokličete tudi pozneje.
quote pride v strankin brskalnik – primeren za stran z zahvalo ali preusmeritev. Za obdelavo na strežniku uporabite webhook (korak 2), ki je podpisan.Korak 2 – Webhook (20 minut)
V računu → Integracija nastavite Webhook URL (https) in skrivni ključ (poljuben dolg niz, npr. 32 naključnih znakov). Ob vsakem povpraševanju pošljemo:
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 v 8 sekundah. Obdelujte šele po odgovoru (vrsta, worker) – sicer grozi timeout.
- Ob napaki ali timeoutu ponovimo po 10 s, 1 min in 5 min, nato odnehamo – povpraševanje lahko vedno prenesete prek API-ja. Preusmeritvam (3xx) ne sledimo.
- Isti
X-Furniconf-Delivery= ista dostava. Shranjujte ga in podvojitve prezrite (idempotenca). - Strankinih fotografij v webhooku ni (samo
photoCount) – prenesite jih prek API-ja (korak 3).
Preverjanje podpisa
Podpis je HMAC-SHA256 z vašim skrivnim ključem nad nizom timestamp + "." + surovo telo. Primerjajte v konstantnem času:
// 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 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: gumb Pošlji testni webhook v računu pošlje dogodek webhook.test z istimi glavami; rezultat (HTTP status, poskusi) vidite v računu, mi pa v dnevniku.
express.raw() pred express.json(), v PHP php://input, v Djangu request.body, v Laravelu $request->getContent().Korak 3 – API (15 minut)
V računu → Integracija → Ustvari API ključ. Ključ fak_… se prikaže samo enkrat – shranite ga med skrivnosti strežnika, nikoli v frontend. Vsak klic nosi Authorization: Bearer fak_…, telesa so JSON, omejitev 600 klicev na 10 minut.
Nova povpraševanja (najnovejša najprej; fotografije samo kot število):
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": { … } }
] }
Eno povpraševanje s seznamom fotografij in prenos 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
Sprememba stanja – da vi in mi vidimo, kje je povpraševanje:
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", … } }
Povezava do 3D predogleda za mojstra (brez računa, podpisana, velja navedeno število dni):
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" }
Brez webhooka: periodično prevzemanje (npr. vsakih 5 minut):
# 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_…"
Stanja povpraševanja: new → seen → contacted → quoted → won / lost. Druge poti: GET /api/portal/me (vaš profil in nastavitve), POST /api/portal/webhook/test.
Odgovori z napako imajo obliko { "error": "…", "reason": "…", "reqId": "…" } – ob prijavi težave navedite reqId.
Korak 4 – 3D predogled za mojstra in v portalu
Vsako povpraševanje ima povezavo iz view-link: mojster jo odpre v brskalniku (tudi na mobilnem) in vidi natanko to, kar je stranka oblikovala – vrtenje, kote, odpiranje vrat, sprehod, brez prijave. Pošljite jo po e-pošti ali SMS ali jo pokažite v podrobnostih povpraševanja v svojem 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>
Povezava je podpisana in časovno omejena (days, privzeto 30). Po poteku ustvarite novo – povpraševanje ostane shranjeno.
Korak 5 – Kaj je v JSON-u načrta
design ima dve plasti: berljiv opis za ljudi in 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
}
- Berljive vrednosti (oblika, mere, moduli in njihova vsebina, dekorji s kodami, aparati, prostor z odprtinami) so vedno v privzetem jeziku vašega računa – ne glede na to, v katerem jeziku je stranka klikala.
stateje surovo stanje konfiguratorja (kode, ne besedila). Omogoča, da načrt kadar koli znova naložite v 3D; ne spreminjajte ga.room.cart(samo v cenovnem načinu): košarica s kodami izdelkov, količinami in zneski, če je stranka v prostor postavila vaše 3D objekte ali ploščice.- Format je stabilen – nova polja se samo dodajajo, obstoječa se nikoli ne preimenujejo.
Obračun in omejitve
- Plačate za vsako prejeto povpraševanje dogovorjeno ceno brez DDV; mesečni račun prejmete po e-pošti (PDF), izpis povpraševanj vidite v računu.
- API: 600 klicev / 10 minut na ključ; webhook: 8 s za odgovor, 4 poskusi.
- Strankino povpraševanje je omejeno po velikosti (načrt do 350 kB, opomba 4 000 znakov, fotografije do 2 MB) – večje brskalnik zavrne že pred pošiljanjem.
Kontrolni seznam pred zagonom
- Domene vaše strani so v računu (zaklep ključa) in konfigurator se naloži na produkcijski strani.
- Webhook: podpis preverjen nad surovim telesom, 2xx v 8 s, podvojitve po
X-Furniconf-Deliveryprezrte, test opravljen. - API ključ je med skrivnostmi strežnika; ob uhajanju v računu ustvarite novega (stari takoj preneha veljati).
- Stanja povpraševanj pošiljate nazaj (
status) – vidna so tudi v računu. - Mojster dobi
view-link, ne JSON-a. - Nastavljen je obračun na povpraševanje (sicer kartica Integracija manjka).
Kode napak (reason)
| Koda | Pomen in rešitev |
|---|---|
api_key (401) | Ključ fak_… je neveljaven ali je bil ustvarjen nov. Preverite glavo Authorization. |
integration_off (403) | Račun nima obračuna na povpraševanje – webhook/API nista aktivna. Pišite nam. |
suspended (403) | Račun je začasno onemogočen. |
rate_limit (429) | Preveč klicev (600 / 10 min). Upočasnite, uporabite since. |
license, quota, bad_email… (/api/quote) | Konfigurator je zavrnil povpraševanje: neveljavna licenca/domena (license), presežena mesečna omejitev (quota), neveljaven e-naslov/telefon/dolžina (bad_email, bad_phone, too_long), načrt nad 350 kB (too_big). |
Podpora
Klepet neposredno v računu (kartica Podpora) ali e-pošta; pri tehnični težavi priložite reqId iz odgovora in X-Furniconf-Delivery iz webhooka. Popolna referenca: PORTAL.md.