1. So funktioniert es
Sie hosten nichts und installieren nichts. Der ganze Ablauf hat vier Schritte:
- Der Konfigurator läuft auf Ihrer Website in einem iframe (ein
<script>). Der Kunde entwirft Küche, Schrank, Wohnwand, Möbel oder Bad in 3D. - Er sendet eine Anfrage (Name, E-Mail, Telefon, Wünsche, Fotos). Fittle speichert sie unter Ihrem Schlüssel.
- Sie erhalten sie dreifach: per E-Mail (mit 3D-Link), per Webhook (signierter POST an Ihren Server – sofort) und über die API (jederzeit später, inklusive Fotos).
- Sie geben sie an Ihre Handwerker weiter – jede Anfrage hat einen 3D-Vorschau-Link, der ohne Konto öffnet.
Was Sie brauchen: ein Tischler-Konto auf app.getfittle.com mit Abrechnung pro Anfrage (Portale und Ketten zahlen kein Paket, sondern pro empfangene Anfrage) – nach der Umstellung erscheint im Konto die Karte Integration mit Webhook und API-Schlüssel. Schreiben Sie uns, wir stellen um.
403 integration_off. Das reine Einbetten des Konfigurators (Schritt 1) funktioniert auch im normalen Paket.Schritt 1 – Konfigurator einbetten (5 Minuten)
Der einfachste Weg: ein Skript auf der Seite, auf der der Konfigurator erscheinen soll. Den Schlüssel FITT-… finden Sie im Konto; er ist an Ihre Domains gebunden (im Konto einstellen) und kann daher nicht von einer fremden Seite missbraucht werden.
<div id="configurator-de"></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. Die Sprache des Konfigurators ist die Standardsprache Ihres Kontos; der Kunde kann sie in der Kopfzeile umschalten.
Portal mit angemeldeten Nutzern: statt data-target rufen Sie HNL.mount() auf – Sie füllen Name, E-Mail und Telefon im Anfrageformular vor und erfahren, wann eine Anfrage gesendet wurde:
<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>
Ereignisse HNL.on: ready {product}, quote {id, product, design}, design (Antwort auf HNL.getDesign(cb)), height {height}. HNL.prefill({…}) kann auch später aufgerufen werden.
quote kommt im Browser des Kunden – gut für eine Dankesseite oder Weiterleitung. Für die serverseitige Verarbeitung nutzen Sie den Webhook (Schritt 2), der ist signiert.Schritt 2 – Webhook (20 Minuten)
Im Konto → Integration tragen Sie die Webhook-URL (https) und ein Secret ein (beliebige lange Zeichenkette, z. B. 32 Zufallszeichen). Bei jeder Anfrage senden wir:
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": { … } }
}
}
}
Zustellregeln:
- Antworten Sie mit 2xx innerhalb von 8 Sekunden. Verarbeiten Sie erst nach der Antwort (Queue, Worker) – sonst droht ein Timeout.
- Bei Fehler oder Timeout wiederholen wir nach 10 s, 1 min und 5 min, dann geben wir auf – die Anfrage können Sie jederzeit über die API holen. Weiterleitungen (3xx) folgen wir nicht.
- Gleiche
X-Furniconf-Delivery= gleiche Zustellung. Speichern Sie sie und ignorieren Sie Duplikate (Idempotenz). - Kundenfotos sind nicht im Webhook (nur
photoCount) – laden Sie sie über die API (Schritt 3).
Signatur prüfen
Die Signatur ist HMAC-SHA256 mit Ihrem Secret über die Zeichenkette timestamp + "." + Roh-Body. Vergleichen Sie in konstanter Zeit:
// 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);
Dasselbe in 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: Die Schaltfläche Test-Webhook senden im Konto schickt ein Ereignis webhook.test mit denselben Headern; das Ergebnis (HTTP-Status, Versuche) sehen Sie im Konto und wir im Log.
express.raw() vor express.json(), in PHP php://input, in Django request.body, in Laravel $request->getContent().Schritt 3 – API (15 Minuten)
Im Konto → Integration → API-Schlüssel erzeugen. Der Schlüssel fak_… wird nur einmal angezeigt – legen Sie ihn in den Server-Secrets ab, nie im Frontend. Jeder Aufruf trägt Authorization: Bearer fak_…, Bodies sind JSON, Limit 600 Aufrufe pro 10 Minuten.
Neue Anfragen (neueste zuerst; Fotos nur als Anzahl):
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": { … } }
] }
Eine Anfrage mit Fotoliste und Download eines Fotos:
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
Statuswechsel – damit Sie und wir sehen, wo die Anfrage steht:
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-Vorschau-Link für den Handwerker (ohne Konto, signiert, gültig für die angegebene Anzahl Tage):
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" }
Ohne Webhook: regelmäßiges Abholen (z. B. alle 5 Minuten):
# 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_…"
Anfragestatus: new → seen → contacted → quoted → won / lost. Weitere Pfade: GET /api/portal/me (Ihr Profil und Einstellungen), POST /api/portal/webhook/test.
Fehlerantworten haben die Form { "error": "…", "reason": "…", "reqId": "…" } – nennen Sie bei einer Problemmeldung die reqId.
Schritt 4 – 3D-Vorschau für den Handwerker und im Portal
Jede Anfrage hat einen Link aus view-link: Der Handwerker öffnet ihn im Browser (auch mobil) und sieht genau, was der Kunde entworfen hat – drehen, Maße, Türen öffnen, Rundgang, ohne Anmeldung. Senden Sie ihn per E-Mail oder SMS oder zeigen Sie ihn im Anfragedetail Ihres Portals:
<!-- 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>
Der Link ist signiert und zeitlich begrenzt (days, Standard 30). Nach Ablauf erzeugen Sie einen neuen – die Anfrage bleibt gespeichert.
Schritt 5 – Was im Design-JSON steckt
design hat zwei Ebenen: eine lesbare Beschreibung für Menschen und state für Maschinen:
{
"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
}
- Lesbare Werte (Form, Maße, Module mit Inhalt, Dekore mit Codes, Geräte, Raum mit Öffnungen) sind immer in der Standardsprache Ihres Kontos – egal, in welcher Sprache der Kunde geklickt hat.
stateist der rohe Konfiguratorzustand (Codes, keine Texte). Damit lässt sich der Entwurf jederzeit wieder in 3D laden; nicht verändern.room.cart(nur im Preismodus): Warenkorb mit Produktcodes, Mengen und Summen, wenn der Kunde Ihre 3D-Objekte oder Fliesen im Raum platziert hat.- Das Format ist stabil – neue Felder kommen nur hinzu, bestehende werden nie umbenannt.
Abrechnung und Limits
- Sie zahlen pro empfangene Anfrage den vereinbarten Preis ohne MwSt.; die Monatsrechnung kommt per E-Mail (PDF), die Anfrageübersicht steht im Konto.
- API: 600 Aufrufe / 10 Minuten pro Schlüssel; Webhook: 8 s für die Antwort, 4 Versuche.
- Die Kundenanfrage ist größenbegrenzt (Entwurf bis 350 kB, Notiz 4 000 Zeichen, Fotos bis 2 MB) – Größeres lehnt der Browser schon vor dem Senden ab.
Checkliste vor dem Start
- Die Domains Ihrer Website stehen im Konto (Schlüsselsperre) und der Konfigurator lädt auf der Live-Seite.
- Webhook: Signatur über den Roh-Body geprüft, 2xx innerhalb 8 s, Duplikate nach
X-Furniconf-Deliveryignoriert, Test bestanden. - Der API-Schlüssel liegt in den Server-Secrets; bei einem Leck erzeugen Sie im Konto einen neuen (der alte wird sofort ungültig).
- Sie melden Anfragestatus zurück (
status) – sie sind auch im Konto sichtbar. - Der Handwerker bekommt den
view-link, kein JSON. - Die Abrechnung pro Anfrage ist eingerichtet (sonst fehlt die Integrationskarte).
Fehlercodes (reason)
| Code | Bedeutung und Lösung |
|---|---|
api_key (401) | Der Schlüssel fak_… ist ungültig oder es wurde ein neuer erzeugt. Prüfen Sie den Header Authorization. |
integration_off (403) | Das Konto hat keine Abrechnung pro Anfrage – Webhook/API sind inaktiv. Schreiben Sie uns. |
suspended (403) | Das Konto ist gesperrt. |
rate_limit (429) | Zu viele Aufrufe (600 / 10 min). Langsamer, nutzen Sie since. |
license, quota, bad_email… (/api/quote) | Der Konfigurator hat die Anfrage abgelehnt: ungültige Lizenz/Domain (license), Monatslimit überschritten (quota), ungültige E-Mail/Telefon/Länge (bad_email, bad_phone, too_long), Entwurf über 350 kB (too_big). |
Support
Chat direkt im Konto (Karte Support) oder E-Mail; bei technischen Problemen die reqId aus der Antwort und X-Furniconf-Delivery aus dem Webhook beilegen. Vollständige Referenz: PORTAL.md.