Gwida

L-integrazzjoni ta’ Fittle f’portal – pass pass.

Għall-iżviluppaturi ta’ portali, ktajjen u sistemi tagħhom: daħħal il-konfiguratur, irċievi t-talbiet b’webhook, aħdem mal-API u uri l-previżjoni 3D lill-mastrudaxxa. B’eżempji f’Node.js, PHP u curl. Integrazzjoni bażika tieħu madwar siegħa.

1. Kif jaħdem

Ma tospita xejn u ma tinstalla xejn. Il-fluss kollu għandu erba’ passi:

  1. Il-konfiguratur jaħdem fuq is-sit tiegħek f’iframe (<script> wieħed). Il-klijent jiddisinja fi 3D kċina, gwardarobba, ħajt tas-salott, għamara jew kamra tal-banju.
  2. Jibgħat talba (isem, email, telefown, rekwiżiti, ritratti). Fittle jaħżinha taħt iċ-ċavetta tiegħek.
  3. Tirċeviha bi tliet modi: bl-email (b’link 3D), b’webhook (POST iffirmat lis-server tiegħek – minnufih) u permezz tal-API (kull ħin wara, bir-ritratti).
  4. Tqassamha lill-mastrudaxxi tiegħek – kull talba għandha link ta’ previżjoni 3D li tinfetaħ mingħajr kont.

X’għandek bżonn: kont ta’ mastrudaxxa fuq app.getfittle.com b’ħlas għal kull talba (il-portali u l-ktajjen ma jħallsux pakkett iżda kull talba li tasal) – wara l-bidla, fil-kont tidher il-kard Integrazzjoni bil-webhook u ċ-ċavetta API. Iktbilna u naqilbuk.

Mingħajr ħlas għal kull talba m’hemmx kard Integrazzjoni u s-sejħiet API jirritornaw 403 integration_off. Id-dħul tal-konfiguratur waħdu (pass 1) jaħdem ukoll b’pakkett normali.

Pass 1 – Dħul tal-konfiguratur (5 minuti)

L-aktar mod sempliċi: script wieħed fuq il-paġna fejn għandu jkun il-konfiguratur. Iċ-ċavetta FITT-… issibha fil-kont; hija marbuta mad-dominji tiegħek (issettjahom fil-kont), għalhekk ma tistax tintuża ħażin minn sit ieħor.

<div id="configurator-mt"></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. Il-lingwa tal-konfiguratur hija l-lingwa awtomatika tal-kont tiegħek; il-klijent jista’ jibdilha fl-intestatura.

Portal b’utenti li daħlu: minflok data-target sejjaħ HNL.mount() – timla minn qabel l-isem, l-email u t-telefown fil-formola tat-talba u tkun taf meta ntbagħtet talba:

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

Avvenimenti HNL.on: ready {product}, quote {id, product, design}, design (tweġiba għal HNL.getDesign(cb)), height {height}. HNL.prefill({…}) jista’ jissejjaħ ukoll aktar tard.

Suġġeriment. L-avveniment quote jasal fil-browser tal-klijent – tajjeb għal paġna ta’ ringrazzjament jew ridirezzjoni. Għall-ipproċessar fuq is-server uża l-webhook (pass 2), li huwa ffirmat.

Pass 2 – Webhook (20 minuta)

Fil-kont → Integrazzjoni issettja l-URL tal-webhook (https) u sigriet (kwalunkwe sekwenza twila, eż. 32 karattru każwali). Ma’ kull talba nibagħtu:

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

Regoli ta’ konsenja:

  • Wieġeb 2xx fi żmien 8 sekondi. Ipproċessa wara t-tweġiba (kju, worker) – inkella hemm riskju ta’ timeout.
  • F’każ ta’ żball jew timeout nerġgħu nippruvaw wara 10 s, 1 min u 5 min, imbagħad naqtgħu qalbna – it-talba dejjem tista’ tniżżilha permezz tal-API. Ridirezzjonijiet (3xx) ma nsegwuhomx.
  • L-istess X-Furniconf-Delivery = l-istess konsenja. Aħżnu u injora d-duplikati (idempotenza).
  • Ir-ritratti tal-klijent mhumiex fil-webhook (biss photoCount) – niżżilhom permezz tal-API (pass 3).

Verifika tal-firma

Il-firma hija HMAC-SHA256 bis-sigriet tiegħek fuq is-sekwenza timestamp + "." + korp mhux ipproċessat. Qabbel f’ħin kostanti:

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

L-istess f’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: il-buttuna Ibgħat webhook ta’ prova fil-kont tibgħat avveniment webhook.test bl-istess headers; ir-riżultat (status HTTP, tentattivi) jidher fil-kont tiegħek u fil-log tagħna.

L-aktar żball komuni: il-firma ma taqbilx għax il-framework l-ewwel ipparsja u reġa’ sserjalizza l-korp (spazji differenti, ordni taċ-ċwievet). Jiġi ffirmat il-korp mhux ipproċessat – f’Express express.raw() qabel express.json(), f’PHP php://input, f’Django request.body, f’Laravel $request->getContent().

Pass 3 – API (15-il minuta)

Fil-kont → Integrazzjoni → Iġġenera ċavetta API. Iċ-ċavetta fak_… tintwera darba biss – aħżinha fis-sigrieti tas-server, qatt fil-frontend. Kull sejħa ġġorr Authorization: Bearer fak_…, il-korpi huma JSON, limitu ta’ 600 sejħa kull 10 minuti.

Talbiet ġodda (l-aktar reċenti l-ewwel; ritratti biss bħala numru):

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

Talba waħda bil-lista tar-ritratti u tniżżil ta’ ritratt:

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

Bidla fl-istatus – biex int u aħna naraw fejn tinsab it-talba:

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

Link ta’ previżjoni 3D għall-mastrudaxxa (mingħajr kont, iffirmata, valida għan-numru ta’ jiem mogħti):

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

Mingħajr webhook: ġbir perjodiku (eż. kull 5 minuti):

# 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 tat-talba: newseencontactedquotedwon / lost. Toroq oħra: GET /api/portal/me (il-profil u s-settings tiegħek), POST /api/portal/webhook/test.

It-tweġibiet ta’ żball għandhom il-forma { "error": "…", "reason": "…", "reqId": "…" } – semmi r-reqId meta tirrapporta problema.

Pass 4 – Previżjoni 3D għall-mastrudaxxa u fil-portal

Kull talba għandha link minn view-link: il-mastrudaxxa jiftaħha fil-browser (anki fuq il-mowbajl) u jara eżatt dak li ddisinja l-klijent – rotazzjoni, qisien, ftuħ tal-bibien, mixja, mingħajr login. Ibgħatha bl-email jew SMS, jew uriha fid-dettall tat-talba fil-portal tiegħek:

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

Il-link hija ffirmata u limitata fiż-żmien (days, awtomatikament 30). Wara li tiskadi ġġenera waħda ġdida – it-talba tibqa’ maħżuna.

Pass 5 – X’hemm fil-JSON tad-disinn

design għandu żewġ saffi: deskrizzjoni li tinqara għan-nies u state għall-magna:

{
  "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
}
  • Il-valuri li jinqraw (forma, qisien, moduli u l-kontenut tagħhom, dekorazzjonijiet b’kodiċijiet, apparat, kamra b’fetħiet) huma dejjem fil-lingwa awtomatika tal-kont tiegħek – tkun xi tkun il-lingwa li uża l-klijent.
  • state huwa l-istat mhux ipproċessat tal-konfiguratur (kodiċijiet, mhux testi). Jippermetti li d-disinn jerġa’ jitgħabba fi 3D kull ħin; tibdlux.
  • room.cart (biss fil-modalità tal-prezzijiet): karrettun b’kodiċijiet tal-prodotti, kwantitajiet u ammonti jekk il-klijent poġġa fil-kamra l-oġġetti 3D jew il-madum tiegħek.
  • Il-format huwa stabbli – oqsma ġodda jiżdiedu biss, dawk eżistenti qatt ma jinbidlilhom isimhom.

Ħlas u limiti

  • Tħallas għal kull talba li tasal il-prezz miftiehem mingħajr VAT; il-fattura ta’ kull xahar tasal bl-email (PDF), ir-rendikont tat-talbiet jinsab fil-kont.
  • API: 600 sejħa / 10 minuti għal kull ċavetta; webhook: 8 s biex twieġeb, 4 tentattivi.
  • It-talba tal-klijent hija limitata fid-daqs (disinn sa 350 kB, nota 4,000 karattru, ritratti sa 2 MB) – akbar minn hekk il-browser jirrifjuta qabel ma jibgħat.

Lista ta’ kontroll qabel it-tnedija

  • Id-dominji tas-sit tiegħek jinsabu fil-kont (lock taċ-ċavetta) u l-konfiguratur jitgħabba fuq il-paġna live.
  • Webhook: firma vverifikata fuq il-korp mhux ipproċessat, 2xx fi żmien 8 s, duplikati skont X-Furniconf-Delivery injorati, test għadda.
  • Iċ-ċavetta API tinsab fis-sigrieti tas-server; f’każ ta’ tnixxija ġġenera waħda ġdida fil-kont (il-qadima tieqaf minnufih).
  • Tibgħat lura l-istatusi tat-talbiet (status) – jidhru wkoll fil-kont.
  • Il-mastrudaxxa jirċievi l-view-link, mhux JSON.
  • Il-ħlas għal kull talba huwa ssettjat (inkella l-kard Integrazzjoni tkun nieqsa).

Kodiċijiet ta’ żball (reason)

KodiċiTifsira u soluzzjoni
api_key (401)Iċ-ċavetta fak_… mhijiex valida jew ġiet iġġenerata waħda ġdida. Iċċekkja l-header Authorization.
integration_off (403)Il-kont m’għandux ħlas għal kull talba – webhook/API mhumiex attivi. Iktbilna.
suspended (403)Il-kont huwa sospiż.
rate_limit (429)Wisq sejħiet (600 / 10 min). Naqqas ir-ritmu, uża since.
license, quota, bad_email… (/api/quote)Il-konfiguratur irrifjuta t-talba: liċenzja/dominju invalidu (license), limitu ta’ kull xahar maqbuż (quota), email/telefown/tul invalidu (bad_email, bad_phone, too_long), disinn akbar minn 350 kB (too_big).

Appoġġ

Chat direttament fil-kont (kard Appoġġ) jew email; għal problema teknika ehmeż ir-reqId mit-tweġiba u l-X-Furniconf-Delivery mill-webhook. Referenza sħiħa: PORTAL.md.