1. Comment ça marche
Vous n’hébergez rien et n’installez rien. Le flux complet tient en quatre étapes :
- Le configurateur tourne sur votre site dans un iframe (un seul
<script>). Le client conçoit une cuisine, une armoire, un meuble TV, du mobilier ou une salle de bain en 3D. - Il envoie une demande (nom, e-mail, téléphone, exigences, photos). Fittle l’enregistre sous votre clé.
- Vous la recevez de trois façons : par e-mail (avec lien 3D), par webhook (POST signé vers votre serveur – immédiatement) et via l’API (à tout moment, photos comprises).
- Vous la transmettez à vos artisans – chaque demande a un lien d’aperçu 3D qui s’ouvre sans compte.
Ce qu’il vous faut : un compte menuisier sur app.getfittle.com avec facturation à la demande (les portails et enseignes ne paient pas un forfait mais chaque demande reçue) – après le passage, la carte Intégration avec le webhook et la clé API apparaît dans votre compte. Écrivez-nous, nous faisons le changement.
403 integration_off. L’intégration du configurateur seule (étape 1) fonctionne aussi avec un forfait classique.Étape 1 – Intégrer le configurateur (5 minutes)
Le plus simple : un script sur la page où le configurateur doit apparaître. La clé FITT-… est dans votre compte ; elle est liée à vos domaines (à définir dans le compte), donc inutilisable depuis un autre site.
<div id="configurator-fr"></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. La langue du configurateur est la langue par défaut de votre compte ; le client peut la changer dans l’en-tête.
Portail avec utilisateurs connectés : au lieu de data-target, appelez HNL.mount() – vous préremplissez nom, e-mail et téléphone dans le formulaire de demande et êtes informé quand une demande est envoyée :
<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>
Événements HNL.on : ready {product}, quote {id, product, design}, design (réponse à HNL.getDesign(cb)), height {height}. HNL.prefill({…}) peut aussi être appelé plus tard.
quote arrive dans le navigateur du client – utile pour une page de remerciement ou une redirection. Pour le traitement côté serveur, utilisez le webhook (étape 2), qui est signé.Étape 2 – Webhook (20 minutes)
Dans votre compte → Intégration, définissez l’URL du webhook (https) et un secret (n’importe quelle chaîne longue, p. ex. 32 caractères aléatoires). À chaque demande, nous envoyons :
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": { … } }
}
}
}
Règles de livraison :
- Répondez 2xx en moins de 8 secondes. Traitez après avoir répondu (file, worker) – sinon risque de timeout.
- En cas d’erreur ou de timeout, nous réessayons après 10 s, 1 min et 5 min, puis abandonnons – vous pouvez toujours récupérer la demande via l’API. Les redirections (3xx) ne sont pas suivies.
- Même
X-Furniconf-Delivery= même livraison. Stockez-le et ignorez les doublons (idempotence). - Les photos du client ne sont pas dans le webhook (seulement
photoCount) – téléchargez-les via l’API (étape 3).
Vérification de la signature
La signature est un HMAC-SHA256 avec votre secret sur la chaîne timestamp + "." + corps brut. Comparez en temps constant :
// 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);
La même chose en 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 : le bouton Envoyer un webhook de test dans votre compte envoie un événement webhook.test avec les mêmes en-têtes ; le résultat (statut HTTP, tentatives) est visible dans votre compte et dans notre journal.
express.raw() avant express.json(), en PHP php://input, dans Django request.body, dans Laravel $request->getContent().Étape 3 – API (15 minutes)
Dans votre compte → Intégration → Générer une clé API. La clé fak_… n’est affichée qu’une fois – stockez-la dans les secrets du serveur, jamais dans le frontend. Chaque appel porte Authorization: Bearer fak_…, les corps sont en JSON, limite 600 appels par 10 minutes.
Nouvelles demandes (les plus récentes d’abord ; photos en nombre seulement) :
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": { … } }
] }
Une demande avec sa liste de photos, et téléchargement d’une photo :
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
Changement de statut – pour que vous et nous voyions où en est la demande :
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", … } }
Lien d’aperçu 3D pour l’artisan (sans compte, signé, valable le nombre de jours indiqué) :
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" }
Sans webhook : interrogation périodique (p. ex. toutes les 5 minutes) :
# 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_…"
Statuts d’une demande : new → seen → contacted → quoted → won / lost. Autres chemins : GET /api/portal/me (votre profil et paramètres), POST /api/portal/webhook/test.
Les réponses d’erreur ont la forme { "error": "…", "reason": "…", "reqId": "…" } – indiquez le reqId quand vous signalez un problème.
Étape 4 – Aperçu 3D pour l’artisan et dans votre portail
Chaque demande a un lien issu de view-link : l’artisan l’ouvre dans un navigateur (mobile aussi) et voit exactement ce que le client a conçu – rotation, cotes, ouverture des portes, visite, sans connexion. Envoyez-le par e-mail ou SMS, ou affichez-le dans le détail de la demande de votre portail :
<!-- 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>
Le lien est signé et limité dans le temps (days, 30 par défaut). Une fois expiré, générez-en un nouveau – la demande reste enregistrée.
Étape 5 – Contenu du JSON du projet
design a deux couches : une description lisible pour les humains et state pour la machine :
{
"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
}
- Les valeurs lisibles (forme, dimensions, modules et leur contenu, décors avec codes, appareils, pièce avec ouvertures) sont toujours dans la langue par défaut de votre compte – quelle que soit la langue utilisée par le client.
stateest l’état brut du configurateur (codes, pas de textes). Il permet de recharger le projet en 3D à tout moment ; ne le modifiez pas.room.cart(mode prix seulement) : panier avec codes produits, quantités et montants si le client a placé vos objets 3D ou carrelages dans la pièce.- Le format est stable – de nouveaux champs sont seulement ajoutés, les existants ne sont jamais renommés.
Facturation et limites
- Vous payez par demande reçue le prix convenu HT ; la facture mensuelle arrive par e-mail (PDF), le relevé des demandes est dans votre compte.
- API : 600 appels / 10 minutes par clé ; webhook : 8 s pour répondre, 4 tentatives.
- La demande du client est limitée en taille (projet jusqu’à 350 Ko, note 4 000 caractères, photos jusqu’à 2 Mo) – au-delà, le navigateur refuse avant l’envoi.
Liste de contrôle avant la mise en service
- Les domaines de votre site sont dans le compte (verrou de clé) et le configurateur se charge sur la page en production.
- Webhook : signature vérifiée sur le corps brut, 2xx en moins de 8 s, doublons selon
X-Furniconf-Deliveryignorés, test réussi. - La clé API est dans les secrets du serveur ; en cas de fuite, générez-en une nouvelle dans le compte (l’ancienne cesse immédiatement).
- Vous renvoyez les statuts des demandes (
status) – ils sont aussi visibles dans le compte. - L’artisan reçoit le
view-link, pas le JSON. - La facturation à la demande est configurée (sinon la carte Intégration est absente).
Codes d’erreur (reason)
| Code | Signification et solution |
|---|---|
api_key (401) | La clé fak_… est invalide ou une nouvelle a été générée. Vérifiez l’en-tête Authorization. |
integration_off (403) | Le compte n’a pas de facturation à la demande – webhook/API inactifs. Écrivez-nous. |
suspended (403) | Le compte est suspendu. |
rate_limit (429) | Trop d’appels (600 / 10 min). Ralentissez, utilisez since. |
license, quota, bad_email… (/api/quote) | Le configurateur a refusé la demande : licence/domaine invalide (license), plafond mensuel dépassé (quota), e-mail/téléphone/longueur invalide (bad_email, bad_phone, too_long), projet de plus de 350 Ko (too_big). |
Support
Chat directement dans votre compte (carte Support) ou e-mail ; pour un problème technique, joignez le reqId de la réponse et le X-Furniconf-Delivery du webhook. Référence complète : PORTAL.md.