Vadovas

Fittle integravimas į portalą – žingsnis po žingsnio.

Portalų, tinklų ir savų sistemų kūrėjams: įterpti konfigūratorių, gauti užklausas webhook’u, dirbti su API ir parodyti 3D peržiūrą meistrui. Su pavyzdžiais Node.js, PHP ir curl. Bazinė integracija trunka apie valandą.

1. Kaip tai veikia

Nieko netalpinate ir nieko nediegiate. Visą eigą sudaro keturi žingsniai:

  1. Konfigūratorius veikia jūsų svetainėje iframe (vienas <script>). Klientas 3D projektuoja virtuvę, spintą, svetainės sekciją, baldus ar vonios kambarį.
  2. Jis išsiunčia užklausą (vardas, el. paštas, telefonas, pageidavimai, nuotraukos). Fittle ją išsaugo po jūsų raktu.
  3. Gaunate ją trimis būdais: el. paštu (su 3D nuoroda), webhook’u (pasirašytas POST į jūsų serverį – iš karto) ir per API (bet kada vėliau, su nuotraukomis).
  4. Perduodate ją meistrams – kiekviena užklausa turi 3D peržiūros nuorodą, kuri atsidaro be paskyros.

Ko reikia: staliaus paskyros app.getfittle.com su apmokestinimu už užklausą (portalai ir tinklai moka ne už paketą, o už kiekvieną gautą užklausą) – perjungus paskyroje atsiranda kortelė Integracija su webhook ir API raktu. Parašykite mums, perjungsime.

Be apmokestinimo už užklausą Integracijos kortelės nėra, o API kvietimai grąžina 403 integration_off. Pats konfigūratoriaus įterpimas (1 žingsnis) veikia ir su įprastu paketu.

1 žingsnis – Konfigūratoriaus įterpimas (5 minutės)

Paprasčiausias būdas: vienas skriptas puslapyje, kuriame turi būti konfigūratorius. Raktą FITT-… rasite paskyroje; jis susietas su jūsų domenais (nustatomi paskyroje), todėl iš kito puslapio jo negalima piktnaudžiauti.

<div id="configurator-lt"></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. Konfigūratoriaus kalba – numatytoji jūsų paskyros kalba; klientas gali ją perjungti antraštėje.

Portalas su prisijungusiais naudotojais: vietoj data-target iškvieskite HNL.mount() – iš anksto užpildote vardą, el. paštą ir telefoną užklausos formoje ir sužinote, kada užklausa išsiųsta:

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

Įvykiai HNL.on: ready {product}, quote {id, product, design}, design (atsakymas į HNL.getDesign(cb)), height {height}. HNL.prefill({…}) galima iškviesti ir vėliau.

Patarimas. Įvykis quote ateina kliento naršyklėje – tinka padėkos puslapiui ar peradresavimui. Apdorojimui serveryje naudokite webhook (2 žingsnis), kuris yra pasirašytas.

2 žingsnis – Webhook (20 minučių)

Paskyroje → Integracija nustatykite Webhook URL (https) ir paslaptį (bet kokią ilgą eilutę, pvz., 32 atsitiktinius simbolius). Su kiekviena užklausa siunčiame:

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

Pristatymo taisyklės:

  • Atsakykite 2xx per 8 sekundes. Apdorokite po atsakymo (eilė, worker) – kitaip gresia laiko limitas.
  • Įvykus klaidai ar laiko limitui kartojame po 10 s, 1 min ir 5 min, tada pasiduodame – užklausą visada galite atsisiųsti per API. Peradresavimų (3xx) nesekame.
  • Tas pats X-Furniconf-Delivery = tas pats pristatymas. Saugokite jį ir ignoruokite dublikatus (idempotencija).
  • Kliento nuotraukų webhook’e nėra (tik photoCount) – atsisiųskite jas per API (3 žingsnis).

Parašo tikrinimas

Parašas yra HMAC-SHA256 su jūsų paslaptimi virš eilutės timestamp + "." + neapdorotas turinys. Lyginkite pastoviu laiku:

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

Tas pats 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']
}

Testas: mygtukas Siųsti testinį webhook paskyroje išsiunčia įvykį webhook.test su tomis pačiomis antraštėmis; rezultatą (HTTP būseną, bandymus) matote paskyroje, o mes – žurnale.

Dažniausia klaida: parašas nesutampa, nes karkasas pirmiausia išanalizavo ir iš naujo serializavo turinį (kiti tarpai, raktų tvarka). Pasirašomas neapdorotas turinys – Express express.raw() prieš express.json(), PHP php://input, Django request.body, Laravel $request->getContent().

3 žingsnis – API (15 minučių)

Paskyroje → Integracija → Generuoti API raktą. Raktas fak_… rodomas tik kartą – išsaugokite jį serverio paslaptyse, niekada frontende. Kiekvienas kvietimas turi Authorization: Bearer fak_…, turiniai yra JSON, limitas 600 kvietimų per 10 minučių.

Naujos užklausos (naujausios pirmos; nuotraukos tik kaip skaičius):

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

Viena užklausa su nuotraukų sąrašu ir nuotraukos atsisiuntimas:

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

Būsenos keitimas – kad jūs ir mes matytume, kur užklausa yra:

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 peržiūros nuoroda meistrui (be paskyros, pasirašyta, galioja nurodytą dienų skaičių):

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

Be webhook: periodinis atsisiuntimas (pvz., kas 5 minutes):

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

Užklausos būsenos: newseencontactedquotedwon / lost. Kiti keliai: GET /api/portal/me (jūsų profilis ir nustatymai), POST /api/portal/webhook/test.

Klaidų atsakymai yra formos { "error": "…", "reason": "…", "reqId": "…" } – pranešdami apie problemą nurodykite reqId.

4 žingsnis – 3D peržiūra meistrui ir portale

Kiekviena užklausa turi nuorodą iš view-link: meistras ją atidaro naršyklėje (ir mobiliajame) ir mato tiksliai tai, ką klientas suprojektavo – sukimas, matmenys, durelių atidarymas, pasivaikščiojimas, be prisijungimo. Siųskite ją el. paštu ar SMS arba rodykite savo portalo užklausos detalėse:

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

Nuoroda pasirašyta ir ribota laike (days, numatyta 30). Pasibaigus galiojimui sugeneruokite naują – užklausa lieka išsaugota.

5 žingsnis – Kas yra projekto JSON

design turi du sluoksnius: skaitomą aprašą žmonėms ir state mašinai:

{
  "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
}
  • Skaitomos reikšmės (forma, matmenys, moduliai ir jų turinys, dekorai su kodais, prietaisai, patalpa su angomis) visada yra numatytąja jūsų paskyros kalba – nepriklausomai nuo to, kokia kalba klientas spaudė.
  • state yra neapdorota konfigūratoriaus būsena (kodai, ne tekstai). Jos dėka projektą bet kada galima įkelti atgal į 3D; nekeiskite jos.
  • room.cart (tik kainų režimu): krepšelis su produktų kodais, kiekiais ir sumomis, jei klientas į patalpą įdėjo jūsų 3D objektus ar plyteles.
  • Formatas stabilus – nauji laukai tik pridedami, esami niekada nepervadinami.

Apmokestinimas ir limitai

  • Mokate už kiekvieną gautą užklausą sutartą kainą be PVM; mėnesinė sąskaita ateina el. paštu (PDF), užklausų ataskaita yra paskyroje.
  • API: 600 kvietimų / 10 minučių raktui; webhook: 8 s atsakymui, 4 bandymai.
  • Kliento užklausa ribojama dydžiu (projektas iki 350 kB, pastaba 4 000 simbolių, nuotraukos iki 2 MB) – didesnes naršyklė atmeta dar prieš siunčiant.

Kontrolinis sąrašas prieš paleidimą

  • Jūsų svetainės domenai yra paskyroje (rakto užraktas) ir konfigūratorius įkeliamas realiame puslapyje.
  • Webhook: parašas patikrintas virš neapdoroto turinio, 2xx per 8 s, dublikatai pagal X-Furniconf-Delivery ignoruojami, testas praeitas.
  • API raktas yra serverio paslaptyse; nutekėjus paskyroje sugeneruojate naują (senas iš karto nustoja galioti).
  • Siunčiate atgal užklausų būsenas (status) – jos matomos ir paskyroje.
  • Meistras gauna view-link, ne JSON.
  • Nustatytas apmokestinimas už užklausą (kitaip Integracijos kortelės nėra).

Klaidų kodai (reason)

KodasReikšmė ir sprendimas
api_key (401)Raktas fak_… negalioja arba sugeneruotas naujas. Patikrinkite antraštę Authorization.
integration_off (403)Paskyra neturi apmokestinimo už užklausą – webhook/API neaktyvūs. Parašykite mums.
suspended (403)Paskyra sustabdyta.
rate_limit (429)Per daug kvietimų (600 / 10 min). Sulėtinkite, naudokite since.
license, quota, bad_email… (/api/quote)Konfigūratorius atmetė užklausą: negaliojanti licencija/domenas (license), viršytas mėnesio limitas (quota), netinkamas el. paštas/telefonas/ilgis (bad_email, bad_phone, too_long), projektas virš 350 kB (too_big).

Pagalba

Pokalbis tiesiai paskyroje (Pagalbos kortelė) arba el. paštas; esant techninei problemai pridėkite reqId iš atsakymo ir X-Furniconf-Delivery iš webhook. Visa nuoroda: PORTAL.md.