1. Så fungerar det
Du hostar inget och installerar inget. Hela flödet har fyra steg:
- Konfiguratorn körs på din webbplats i en iframe (ett enda
<script>). Kunden designar kök, garderob, tv-möbel, möbler eller badrum i 3D. - Kunden skickar en förfrågan (namn, e-post, telefon, önskemål, foton). Fittle sparar den under din nyckel.
- Du får den på tre sätt: via e-post (med 3D-länk), via webhook (signerad POST till din server – direkt) och via API (när som helst senare, inklusive foton).
- Du fördelar den till dina hantverkare – varje förfrågan har en 3D-länk som öppnas utan konto.
Det du behöver: ett snickarkonto på app.getfittle.com med debitering per förfrågan (portaler och kedjor betalar inget paket utan per mottagen förfrågan) – efter omställningen visas kortet Integration med webhook och API-nyckel i kontot. Skriv till oss så ställer vi om.
403 integration_off. Själva inbäddningen av konfiguratorn (steg 1) fungerar även på ett vanligt paket.Steg 1 – Bädda in konfiguratorn (5 minuter)
Enklaste sättet: ett skript på sidan där konfiguratorn ska finnas. Nyckeln FITT-… hittar du i kontot; den är knuten till dina domäner (ställs in i kontot), så den kan inte missbrukas från en annan sida.
<div id="configurator-sv"></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. Konfiguratorns språk är kontots standardspråk; kunden kan byta det i sidhuvudet.
Portal med inloggade användare: anropa HNL.mount() i stället för data-target – du förifyller namn, e-post och telefon i förfrågningsformuläret och får veta när en förfrågan har skickats:
<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>
Händelser HNL.on: ready {product}, quote {id, product, design}, design (svar på HNL.getDesign(cb)), height {height}. HNL.prefill({…}) kan även anropas senare.
quote kommer i kundens webbläsare – bra för en tacksida eller omdirigering. För behandling på servern använder du webhooken (steg 2), som är signerad.Steg 2 – Webhook (20 minuter)
I kontot → Integration anger du Webhook-URL (https) och en hemlighet (valfri lång sträng, t.ex. 32 slumpmässiga tecken). Vid varje förfrågan skickar vi:
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": { … } }
}
}
}
Leveransregler:
- Svara 2xx inom 8 sekunder. Bearbeta först efter svaret (kö, worker) – annars riskerar du timeout.
- Vid fel eller timeout försöker vi igen efter 10 s, 1 min och 5 min, sedan ger vi upp – du kan alltid hämta förfrågan via API:et. Omdirigeringar (3xx) följs inte.
- Samma
X-Furniconf-Delivery= samma leverans. Spara den och ignorera dubbletter (idempotens). - Kundens foton finns inte i webhooken (bara
photoCount) – hämta dem via API:et (steg 3).
Kontroll av signatur
Signaturen är HMAC-SHA256 med din hemlighet över strängen timestamp + "." + rå body. Jämför i konstant tid:
// 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);
Samma sak i 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: knappen Skicka test-webhook i kontot skickar en webhook.test-händelse med samma headers; resultatet (HTTP-status, försök) syns i kontot och i vår logg.
express.raw() före express.json(), i PHP php://input, i Django request.body, i Laravel $request->getContent().Steg 3 – API (15 minuter)
I kontot → Integration → Generera API-nyckel. Nyckeln fak_… visas bara en gång – spara den bland serverns hemligheter, aldrig i frontend. Varje anrop bär Authorization: Bearer fak_…, bodies är JSON, gräns 600 anrop per 10 minuter.
Nya förfrågningar (nyaste först; foton bara som antal):
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": { … } }
] }
En förfrågan med fotolista och nedladdning av ett foto:
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
Statusändring – så att du och vi ser var förfrågan står:
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-länk för hantverkaren (utan konto, signerad, giltig angivet antal dagar):
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" }
Utan webhook: periodisk hämtning (t.ex. var 5:e 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_…"
Förfrågningsstatus: new → seen → contacted → quoted → won / lost. Andra sökvägar: GET /api/portal/me (din profil och inställningar), POST /api/portal/webhook/test.
Felsvar har formen { "error": "…", "reason": "…", "reqId": "…" } – ange reqId när du rapporterar ett problem.
Steg 4 – 3D-vy för hantverkaren och i portalen
Varje förfrågan har en länk från view-link: hantverkaren öppnar den i webbläsaren (även mobil) och ser exakt det kunden har designat – rotera, mått, öppna dörrar, promenad, utan inloggning. Skicka den via e-post eller sms, eller visa den i förfrågningsdetaljen i din portal:
<!-- 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>
Länken är signerad och tidsbegränsad (days, standard 30). När den gått ut genererar du en ny – förfrågan finns kvar.
Steg 5 – Vad finns i design-JSON
design har två lager: en läsbar beskrivning för människor och state för maskinen:
{
"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
}
- Läsbara värden (form, mått, moduler med innehåll, dekorer med koder, vitvaror, rum med öppningar) är alltid på kontots standardspråk – oavsett vilket språk kunden använde.
stateär konfiguratorns råa tillstånd (koder, inte texter). Med den kan designen när som helst laddas tillbaka i 3D; ändra den inte.room.cart(endast i prisläge): varukorg med produktkoder, antal och belopp om kunden placerat dina 3D-objekt eller plattor i rummet.- Formatet är stabilt – nya fält läggs bara till, befintliga byter aldrig namn.
Debitering och gränser
- Du betalar det avtalade priset exkl. moms per mottagen förfrågan; månadsfakturan kommer via e-post (PDF), förfrågningsrapporten finns i kontot.
- API: 600 anrop / 10 minuter per nyckel; webhook: 8 s att svara, 4 försök.
- Kundens förfrågan är storleksbegränsad (design upp till 350 kB, anteckning 4 000 tecken, foton upp till 2 MB) – större avvisas av webbläsaren redan före sändning.
Checklista före driftstart
- Din webbplats domäner finns i kontot (nyckellås) och konfiguratorn laddas på den skarpa sidan.
- Webhook: signatur kontrollerad över den råa body:n, 2xx inom 8 s, dubbletter enligt
X-Furniconf-Deliveryignoreras, test godkänt. - API-nyckeln ligger bland serverns hemligheter; vid läcka genererar du en ny i kontot (den gamla slutar gälla direkt).
- Du skickar tillbaka förfrågningsstatus (
status) – de syns även i kontot. - Hantverkaren får
view-link, inte JSON. - Debitering per förfrågan är inställd (annars saknas Integration-kortet).
Felkoder (reason)
| Kod | Betydelse och åtgärd |
|---|---|
api_key (401) | Nyckeln fak_… är ogiltig eller en ny har genererats. Kontrollera headern Authorization. |
integration_off (403) | Kontot saknar debitering per förfrågan – webhook/API är inaktiva. Skriv till oss. |
suspended (403) | Kontot är avstängt. |
rate_limit (429) | För många anrop (600 / 10 min). Sakta ner, använd since. |
license, quota, bad_email… (/api/quote) | Konfiguratorn avvisade förfrågan: ogiltig licens/domän (license), månadstak överskridet (quota), ogiltig e-post/telefon/längd (bad_email, bad_phone, too_long), design över 350 kB (too_big). |
Support
Chatt direkt i kontot (kortet Support) eller e-post; vid tekniska problem bifoga reqId från svaret och X-Furniconf-Delivery från webhooken. Fullständig referens: PORTAL.md.