1. Πώς λειτουργεί
Δεν φιλοξενείτε και δεν εγκαθιστάτε τίποτα. Όλη η ροή έχει τέσσερα βήματα:
- Ο διαμορφωτής τρέχει στον ιστότοπό σας σε iframe (ένα
<script>). Ο πελάτης σχεδιάζει σε 3D κουζίνα, ντουλάπα, σύνθετο σαλονιού, έπιπλα ή μπάνιο. - Στέλνει αίτημα (όνομα, e-mail, τηλέφωνο, απαιτήσεις, φωτογραφίες). Το Fittle το αποθηκεύει κάτω από το κλειδί σας.
- Το λαμβάνετε με τρεις τρόπους: με e-mail (με σύνδεσμο 3D), με webhook (υπογεγραμμένο POST στον διακομιστή σας – αμέσως) και μέσω API (οποτεδήποτε αργότερα, μαζί με φωτογραφίες).
- Το μοιράζετε στους τεχνίτες σας – κάθε αίτημα έχει σύνδεσμο 3D προεπισκόπησης που ανοίγει χωρίς λογαριασμό.
Τι χρειάζεστε: λογαριασμό επιπλοποιού στο app.getfittle.com με χρέωση ανά αίτημα (οι πύλες και οι αλυσίδες δεν πληρώνουν πακέτο αλλά κάθε αίτημα που λαμβάνουν) – μετά την αλλαγή εμφανίζεται στον λογαριασμό η κάρτα Ενσωμάτωση με webhook και κλειδί API. Γράψτε μας και θα σας αλλάξουμε.
403 integration_off. Η απλή ενσωμάτωση του διαμορφωτή (βήμα 1) λειτουργεί και με κανονικό πακέτο.Βήμα 1 – Ενσωμάτωση του διαμορφωτή (5 λεπτά)
Ο απλούστερος τρόπος: ένα script στη σελίδα όπου θα εμφανίζεται ο διαμορφωτής. Το κλειδί FITT-… βρίσκεται στον λογαριασμό σας· είναι δεσμευμένο στα domain σας (ορίζονται στον λογαριασμό), οπότε δεν μπορεί να χρησιμοποιηθεί από άλλον ιστότοπο.
<div id="configurator-el"></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. Η γλώσσα του διαμορφωτή είναι η προεπιλεγμένη γλώσσα του λογαριασμού σας· ο πελάτης μπορεί να την αλλάξει από την κεφαλίδα.
Πύλη με συνδεδεμένους χρήστες: αντί για data-target καλέστε HNL.mount() – προσυμπληρώνετε όνομα, e-mail και τηλέφωνο στη φόρμα αιτήματος και ενημερώνεστε όταν σταλεί ένα αίτημα:
<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>
Συμβάντα HNL.on: ready {product}, quote {id, product, design}, design (απάντηση στο HNL.getDesign(cb)), height {height}. Το HNL.prefill({…}) μπορεί να κληθεί και αργότερα.
quote έρχεται στον browser του πελάτη – χρήσιμο για σελίδα ευχαριστιών ή ανακατεύθυνση. Για επεξεργασία στον διακομιστή χρησιμοποιήστε το webhook (βήμα 2), που είναι υπογεγραμμένο.Βήμα 2 – Webhook (20 λεπτά)
Στον λογαριασμό → Ενσωμάτωση ορίστε το Webhook URL (https) και ένα μυστικό (οποιαδήποτε μεγάλη συμβολοσειρά, π.χ. 32 τυχαίοι χαρακτήρες). Σε κάθε αίτημα στέλνουμε:
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": { … } }
}
}
}
Κανόνες παράδοσης:
- Απαντήστε 2xx μέσα σε 8 δευτερόλεπτα. Επεξεργαστείτε μετά την απάντηση (ουρά, worker) – αλλιώς κινδυνεύετε από timeout.
- Σε σφάλμα ή timeout ξαναπροσπαθούμε μετά από 10 δ., 1 λ. και 5 λ., μετά τα παρατάμε – το αίτημα μπορείτε πάντα να το κατεβάσετε μέσω API. Δεν ακολουθούμε ανακατευθύνσεις (3xx).
- Ίδιο
X-Furniconf-Delivery= ίδια παράδοση. Αποθηκεύστε το και αγνοήστε τα διπλότυπα (idempotency). - Οι φωτογραφίες του πελάτη δεν είναι στο webhook (μόνο
photoCount) – κατεβάστε τις μέσω API (βήμα 3).
Έλεγχος υπογραφής
Η υπογραφή είναι HMAC-SHA256 με το μυστικό σας πάνω στη συμβολοσειρά timestamp + "." + ακατέργαστο σώμα. Συγκρίνετε σε σταθερό χρόνο:
// 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);
Το ίδιο σε 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']
}
Δοκιμή: το κουμπί Αποστολή δοκιμαστικού webhook στον λογαριασμό στέλνει συμβάν webhook.test με τις ίδιες κεφαλίδες· το αποτέλεσμα (κατάσταση HTTP, προσπάθειες) φαίνεται στον λογαριασμό σας και στο log μας.
express.raw() πριν το express.json(), στην PHP php://input, στο Django request.body, στο Laravel $request->getContent().Βήμα 3 – API (15 λεπτά)
Στον λογαριασμό → Ενσωμάτωση → Δημιουργία κλειδιού API. Το κλειδί fak_… εμφανίζεται μόνο μία φορά – αποθηκεύστε το στα μυστικά του διακομιστή, ποτέ στο frontend. Κάθε κλήση φέρει Authorization: Bearer fak_…, τα σώματα είναι JSON, όριο 600 κλήσεις ανά 10 λεπτά.
Νέα αιτήματα (νεότερα πρώτα· φωτογραφίες μόνο ως αριθμός):
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": { … } }
] }
Ένα αίτημα με τη λίστα φωτογραφιών και λήψη φωτογραφίας:
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
Αλλαγή κατάστασης – ώστε εσείς κι εμείς να βλέπουμε πού βρίσκεται το αίτημα:
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 προεπισκόπησης για τον τεχνίτη (χωρίς λογαριασμό, υπογεγραμμένος, ισχύει τις ημέρες που ορίζετε):
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" }
Χωρίς webhook: περιοδική λήψη (π.χ. κάθε 5 λεπτά):
# 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_…"
Καταστάσεις αιτήματος: new → seen → contacted → quoted → won / lost. Άλλες διαδρομές: GET /api/portal/me (προφίλ και ρυθμίσεις σας), POST /api/portal/webhook/test.
Οι απαντήσεις σφάλματος έχουν τη μορφή { "error": "…", "reason": "…", "reqId": "…" } – αναφέρετε το reqId όταν δηλώνετε πρόβλημα.
Βήμα 4 – 3D προεπισκόπηση για τον τεχνίτη και στην πύλη
Κάθε αίτημα έχει σύνδεσμο από το view-link: ο τεχνίτης τον ανοίγει στον browser (και στο κινητό) και βλέπει ακριβώς ό,τι σχεδίασε ο πελάτης – περιστροφή, διαστάσεις, άνοιγμα πορτών, περιήγηση, χωρίς σύνδεση. Στείλτε τον με e-mail ή SMS, ή δείξτε τον στη λεπτομέρεια αιτήματος της πύλης σας:
<!-- 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>
Ο σύνδεσμος είναι υπογεγραμμένος και χρονικά περιορισμένος (days, προεπιλογή 30). Μετά τη λήξη δημιουργείτε νέο – το αίτημα παραμένει αποθηκευμένο.
Βήμα 5 – Τι περιέχει το JSON του σχεδίου
Το design έχει δύο επίπεδα: αναγνώσιμη περιγραφή για ανθρώπους και state για τη μηχανή:
{
"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
}
- Οι αναγνώσιμες τιμές (σχήμα, διαστάσεις, μονάδες και περιεχόμενο, διακοσμήσεις με κωδικούς, συσκευές, δωμάτιο με ανοίγματα) είναι πάντα στην προεπιλεγμένη γλώσσα του λογαριασμού σας – όποια γλώσσα κι αν χρησιμοποίησε ο πελάτης.
- Το
stateείναι η ακατέργαστη κατάσταση του διαμορφωτή (κωδικοί, όχι κείμενα). Επιτρέπει την επαναφόρτωση του σχεδίου σε 3D ανά πάσα στιγμή· μην το τροποποιείτε. - Το
room.cart(μόνο σε λειτουργία τιμών): καλάθι με κωδικούς προϊόντων, ποσότητες και ποσά, αν ο πελάτης τοποθέτησε στο δωμάτιο τα 3D αντικείμενα ή τα πλακάκια σας. - Η μορφή είναι σταθερή – νέα πεδία μόνο προστίθενται, τα υπάρχοντα δεν μετονομάζονται ποτέ.
Χρέωση και όρια
- Πληρώνετε για κάθε αίτημα που λαμβάνετε τη συμφωνημένη τιμή χωρίς ΦΠΑ· το μηνιαίο τιμολόγιο έρχεται με e-mail (PDF), η κατάσταση αιτημάτων βρίσκεται στον λογαριασμό.
- API: 600 κλήσεις / 10 λεπτά ανά κλειδί· webhook: 8 δ. για απάντηση, 4 προσπάθειες.
- Το αίτημα του πελάτη έχει όρια μεγέθους (σχέδιο έως 350 kB, σημείωση 4.000 χαρακτήρες, φωτογραφίες έως 2 MB) – τα μεγαλύτερα τα απορρίπτει ο browser πριν την αποστολή.
Λίστα ελέγχου πριν την έναρξη
- Τα domain του ιστότοπού σας είναι στον λογαριασμό (κλείδωμα κλειδιού) και ο διαμορφωτής φορτώνει στη ζωντανή σελίδα.
- Webhook: υπογραφή ελεγμένη στο ακατέργαστο σώμα, 2xx μέσα σε 8 δ., διπλότυπα κατά
X-Furniconf-Deliveryαγνοούνται, η δοκιμή πέρασε. - Το κλειδί API βρίσκεται στα μυστικά του διακομιστή· σε διαρροή δημιουργείτε νέο στον λογαριασμό (το παλιό παύει αμέσως).
- Στέλνετε πίσω τις καταστάσεις των αιτημάτων (
status) – φαίνονται και στον λογαριασμό. - Ο τεχνίτης λαμβάνει το
view-link, όχι JSON. - Έχει ρυθμιστεί χρέωση ανά αίτημα (αλλιώς λείπει η κάρτα Ενσωμάτωση).
Κωδικοί σφαλμάτων (reason)
| Κωδικός | Σημασία και λύση |
|---|---|
api_key (401) | Το κλειδί fak_… είναι άκυρο ή δημιουργήθηκε νέο. Ελέγξτε την κεφαλίδα Authorization. |
integration_off (403) | Ο λογαριασμός δεν έχει χρέωση ανά αίτημα – webhook/API ανενεργά. Γράψτε μας. |
suspended (403) | Ο λογαριασμός έχει ανασταλεί. |
rate_limit (429) | Πάρα πολλές κλήσεις (600 / 10 λ.). Επιβραδύνετε, χρησιμοποιήστε since. |
license, quota, bad_email… (/api/quote) | Ο διαμορφωτής απέρριψε το αίτημα: άκυρη άδεια/domain (license), υπέρβαση μηνιαίου ορίου (quota), άκυρο e-mail/τηλέφωνο/μήκος (bad_email, bad_phone, too_long), σχέδιο άνω των 350 kB (too_big). |
Υποστήριξη
Συνομιλία απευθείας στον λογαριασμό (κάρτα Υποστήριξη) ή e-mail· σε τεχνικό πρόβλημα επισυνάψτε το reqId από την απάντηση και το X-Furniconf-Delivery από το webhook. Πλήρης αναφορά: PORTAL.md.