Rokasgrāmata

Fittle integrācija portālā – soli pa solim.

Portālu, ķēžu un savu sistēmu izstrādātājiem: iegult konfiguratoru, saņemt pieprasījumus ar webhook, strādāt ar API un parādīt 3D priekšskatījumu meistaram. Ar piemēriem Node.js, PHP un curl. Pamata integrācija aizņem aptuveni stundu.

1. Kā tas darbojas

Jūs neko nemitināt un neko neinstalējat. Visai plūsmai ir četri soļi:

  1. Konfigurators darbojas jūsu vietnē iframe (viens <script>). Klients 3D projektē virtuvi, skapi, viesistabas sienu, mēbeles vai vannas istabu.
  2. Viņš nosūta pieprasījumu (vārds, e-pasts, tālrunis, prasības, fotogrāfijas). Fittle to saglabā zem jūsu atslēgas.
  3. Jūs to saņemat trīs veidos: pa e-pastu (ar 3D saiti), ar webhook (parakstīts POST uz jūsu serveri – uzreiz) un caur API (jebkurā laikā vēlāk, ieskaitot fotogrāfijas).
  4. Jūs to izdalāt meistariem – katram pieprasījumam ir 3D priekšskatījuma saite, kas atveras bez konta.

Kas jums nepieciešams: galdnieka konts vietnē app.getfittle.com ar norēķiniem par pieprasījumu (portāli un ķēdes nemaksā par paketi, bet par katru saņemto pieprasījumu) – pēc pārslēgšanas kontā parādās karte Integrācija ar webhook un API atslēgu. Rakstiet mums, mēs pārslēgsim.

Bez norēķiniem par pieprasījumu Integrācijas kartes nav, un API izsaukumi atgriež 403 integration_off. Pati konfiguratora iegulšana (1. solis) darbojas arī ar parasto paketi.

1. solis – Konfiguratora iegulšana (5 minūtes)

Vienkāršākais veids: viens skripts lapā, kur jābūt konfiguratoram. Atslēgu FITT-… atradīsiet kontā; tā ir piesaistīta jūsu domēniem (iestata kontā), tāpēc no citas lapas to nevar ļaunprātīgi izmantot.

<div id="configurator-lv"></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. Konfiguratora valoda ir jūsu konta noklusējuma valoda; klients to var pārslēgt galvenē.

Portāls ar pieteikušamies lietotājiem: data-target vietā izsauciet HNL.mount() – iepriekš aizpildāt vārdu, e-pastu un tālruni pieprasījuma formā un uzzināt, kad pieprasījums nosūtīts:

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

Notikumi HNL.on: ready {product}, quote {id, product, design}, design (atbilde uz HNL.getDesign(cb)), height {height}. HNL.prefill({…}) var izsaukt arī vēlāk.

Padoms. Notikums quote pienāk klienta pārlūkā – noder pateicības lapai vai novirzīšanai. Apstrādei serverī izmantojiet webhook (2. solis), kas ir parakstīts.

2. solis – Webhook (20 minūtes)

Kontā → Integrācija iestatiet Webhook URL (https) un noslēpumu (jebkura gara virkne, piem., 32 nejaušas rakstzīmes). Katrā pieprasījumā mēs sūtām:

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

Piegādes noteikumi:

  • Atbildiet ar 2xx 8 sekunžu laikā. Apstrādājiet pēc atbildes (rinda, worker) – citādi draud taimauts.
  • Kļūdas vai taimauta gadījumā atkārtojam pēc 10 s, 1 min un 5 min, tad atsakāmies – pieprasījumu vienmēr var lejupielādēt caur API. Novirzīšanām (3xx) nesekojam.
  • Tas pats X-Furniconf-Delivery = tā pati piegāde. Saglabājiet to un ignorējiet dublikātus (idempotence).
  • Klienta fotogrāfiju webhook nav (tikai photoCount) – lejupielādējiet tās caur API (3. solis).

Paraksta pārbaude

Paraksts ir HMAC-SHA256 ar jūsu noslēpumu pār virkni timestamp + "." + neapstrādāts ķermenis. Salīdziniet konstantā laikā:

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

Tests: poga Sūtīt testa webhook kontā nosūta notikumu webhook.test ar tām pašām galvenēm; rezultātu (HTTP statuss, mēģinājumi) redzat kontā, mēs – žurnālā.

Biežākā kļūda: paraksts nesakrīt, jo ietvars vispirms parsēja un no jauna serializēja ķermeni (citas atstarpes, atslēgu secība). Paraksta neapstrādāto ķermeni – Express express.raw() pirms express.json(), PHP php://input, Django request.body, Laravel $request->getContent().

3. solis – API (15 minūtes)

Kontā → Integrācija → Ģenerēt API atslēgu. Atslēga fak_… tiek parādīta tikai vienreiz – glabājiet to servera noslēpumos, nekad frontendā. Katrs izsaukums nes Authorization: Bearer fak_…, ķermeņi ir JSON, ierobežojums 600 izsaukumi 10 minūtēs.

Jauni pieprasījumi (jaunākie vispirms; fotogrāfijas tikai kā skaits):

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

Viens pieprasījums ar fotogrāfiju sarakstu un fotogrāfijas lejupielāde:

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

Statusa maiņa – lai jūs un mēs redzētu, kur pieprasījums atrodas:

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 priekšskatījuma saite meistaram (bez konta, parakstīta, derīga norādīto dienu skaitu):

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 webhook: periodiska izgūšana (piem., ik pēc 5 minūtēm):

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

Pieprasījuma statusi: newseencontactedquotedwon / lost. Citi ceļi: GET /api/portal/me (jūsu profils un iestatījumi), POST /api/portal/webhook/test.

Kļūdu atbildes ir formā { "error": "…", "reason": "…", "reqId": "…" } – ziņojot par problēmu, norādiet reqId.

4. solis – 3D priekšskatījums meistaram un portālā

Katram pieprasījumam ir saite no view-link: meistars to atver pārlūkā (arī mobilajā) un redz tieši to, ko klients projektēja – griešana, izmēri, durvju atvēršana, pastaiga, bez pieteikšanās. Nosūtiet to pa e-pastu vai SMS vai rādiet sava portāla pieprasījuma detaļās:

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

Saite ir parakstīta un laikā ierobežota (days, noklusējums 30). Pēc termiņa beigām ģenerējiet jaunu – pieprasījums paliek saglabāts.

5. solis – Kas ir projekta JSON

design ir divi slāņi: lasāms apraksts cilvēkiem un state mašīnai:

{
  "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
}
  • Lasāmās vērtības (forma, izmēri, moduļi un to saturs, dekori ar kodiem, ierīces, telpa ar ailēm) vienmēr ir jūsu konta noklusējuma valodā – neatkarīgi no tā, kādā valodā klients klikšķināja.
  • state ir konfiguratora neapstrādātais stāvoklis (kodi, ne teksti). Pateicoties tam, projektu jebkurā laikā var ielādēt atpakaļ 3D; nemainiet to.
  • room.cart (tikai cenu režīmā): grozs ar produktu kodiem, daudzumiem un summām, ja klients telpā ievietoja jūsu 3D objektus vai flīzes.
  • Formāts ir stabils – jauni lauki tikai tiek pievienoti, esošie nekad netiek pārdēvēti.

Norēķini un ierobežojumi

  • Maksājat par katru saņemto pieprasījumu nolīgto cenu bez PVN; mēneša rēķins pienāk pa e-pastu (PDF), pieprasījumu pārskats ir kontā.
  • API: 600 izsaukumi / 10 minūtes uz atslēgu; webhook: 8 s atbildei, 4 mēģinājumi.
  • Klienta pieprasījums ir ierobežots pēc izmēra (projekts līdz 350 kB, piezīme 4 000 rakstzīmes, fotogrāfijas līdz 2 MB) – lielākus pārlūks noraida jau pirms nosūtīšanas.

Kontrolsaraksts pirms palaišanas

  • Jūsu vietnes domēni ir kontā (atslēgas slēdzene) un konfigurators ielādējas reālajā lapā.
  • Webhook: paraksts pārbaudīts pār neapstrādāto ķermeni, 2xx 8 s laikā, dublikāti pēc X-Furniconf-Delivery ignorēti, tests izturēts.
  • API atslēga ir servera noslēpumos; noplūdes gadījumā kontā ģenerējiet jaunu (vecā uzreiz zaudē spēku).
  • Nosūtāt atpakaļ pieprasījumu statusus (status) – tie redzami arī kontā.
  • Meistars saņem view-link, nevis JSON.
  • Ir iestatīti norēķini par pieprasījumu (citādi Integrācijas karte trūkst).

Kļūdu kodi (reason)

KodsNozīme un risinājums
api_key (401)Atslēga fak_… nav derīga vai ir ģenerēta jauna. Pārbaudiet galveni Authorization.
integration_off (403)Kontam nav norēķinu par pieprasījumu – webhook/API nav aktīvi. Rakstiet mums.
suspended (403)Konts ir apturēts.
rate_limit (429)Pārāk daudz izsaukumu (600 / 10 min). Palēniniet, izmantojiet since.
license, quota, bad_email… (/api/quote)Konfigurators noraidīja pieprasījumu: nederīga licence/domēns (license), pārsniegts mēneša limits (quota), nederīgs e-pasts/tālrunis/garums (bad_email, bad_phone, too_long), projekts virs 350 kB (too_big).

Atbalsts

Tērzēšana tieši kontā (Atbalsta karte) vai e-pasts; tehniskas problēmas gadījumā pievienojiet reqId no atbildes un X-Furniconf-Delivery no webhook. Pilna atsauce: PORTAL.md.