Ръководство

Интегриране на Fittle в портал – стъпка по стъпка.

За разработчици на портали, вериги и собствени системи: вграждане на конфигуратора, получаване на запитвания чрез webhook, работа с API и показване на 3D прегледа на майстора. С примери на Node.js, PHP и curl. Базовата интеграция отнема около час.

1. Как работи

Не хоствате и не инсталирате нищо. Целият процес има четири стъпки:

  1. Конфигураторът работи на вашия сайт в iframe (един <script>). Клиентът проектира в 3D кухня, гардероб, секция за хол, мебели или баня.
  2. Изпраща запитване (име, имейл, телефон, изисквания, снимки). Fittle го записва под вашия ключ.
  3. Получавате го по три начина: по имейл (с 3D връзка), чрез webhook (подписан POST към вашия сървър – веднага) и през API (по всяко време по-късно, включително снимките).
  4. Разпределяте го на майсторите – всяко запитване има връзка към 3D преглед, която се отваря без акаунт.

Какво ви трябва: акаунт на мебелист в app.getfittle.com с таксуване на запитване (порталите и веригите не плащат пакет, а всяко получено запитване) – след превключването в акаунта се появява картата Интеграция с webhook и API ключ. Пишете ни и ще ви превключим.

Без таксуване на запитване няма карта Интеграция и API повикванията връщат 403 integration_off. Самото вграждане на конфигуратора (стъпка 1) работи и с обикновен пакет.

Стъпка 1 – Вграждане на конфигуратора (5 минути)

Най-простият начин: един скрипт на страницата, където трябва да е конфигураторът. Ключът FITT-… е в акаунта ви; той е обвързан с вашите домейни (задават се в акаунта), така че не може да се използва от друг сайт.

<div id="configurator-bg"></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() – попълвате предварително име, имейл и телефон във формата за запитване и разбирате кога е изпратено запитване:

<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 идва в браузъра на клиента – подходящо за страница с благодарност или пренасочване. За обработка на сървъра използвайте 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 = същата доставка. Записвайте го и игнорирайте дубликатите (идемпотентност).
  • Снимките на клиента не са в 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 статус, опити) се вижда в акаунта ви и в нашия лог.

Най-честа грешка: подписът не съвпада, защото фреймуъркът първо е парснал и отново сериализирал тялото (други интервали, ред на ключовете). Подписва се суровото тяло – в Express express.raw() преди express.json(), в PHP php://input, в Django request.body, в Laravel $request->getContent().

Стъпка 3 – API (15 минути)

В акаунта → Интеграция → Генерирай API ключ. Ключът fak_… се показва само веднъж – запазете го в тайните на сървъра, никога във фронтенда. Всяко повикване носи 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_…"

Статуси на запитването: newseencontactedquotedwon / lost. Други пътища: GET /api/portal/me (вашият профил и настройки), POST /api/portal/webhook/test.

Отговорите при грешка имат вида { "error": "…", "reason": "…", "reqId": "…" } – при съобщаване на проблем посочете reqId.

Стъпка 4 – 3D преглед за майстора и в портала

Всяко запитване има връзка от view-link: майсторът я отваря в браузъра (и на мобилен) и вижда точно това, което клиентът е проектирал – въртене, размери, отваряне на вратички, разходка, без вход. Изпратете я по имейл или 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 обекти или плочки.
  • Форматът е стабилен – новите полета само се добавят, съществуващите никога не се преименуват.

Таксуване и лимити

  • Плащате за всяко получено запитване договорената цена без ДДС; месечната фактура идва по имейл (PDF), справката за запитванията е в акаунта.
  • API: 600 повиквания / 10 минути на ключ; webhook: 8 с за отговор, 4 опита.
  • Запитването на клиента е ограничено по размер (проект до 350 kB, бележка 4 000 знака, снимки до 2 MB) – по-големите браузърът отхвърля още преди изпращане.

Контролен списък преди пускане

  • Домейните на вашия сайт са в акаунта (заключване на ключа) и конфигураторът се зарежда на реалната страница.
  • 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)Конфигураторът отхвърли запитването: невалиден лиценз/домейн (license), надвишен месечен лимит (quota), невалиден имейл/телефон/дължина (bad_email, bad_phone, too_long), проект над 350 kB (too_big).

Поддръжка

Чат директно в акаунта (карта Поддръжка) или имейл; при технически проблем приложете reqId от отговора и X-Furniconf-Delivery от webhook-а. Пълна справка: PORTAL.md.