Endpoint danych klientów: zamówienia, wysyłka i faktury
Dokumentacja techniczna integracji danych klientów – podpisane zapytanie reactive.chat do twojego sklepu, schemat odpowiedzi, limity, przyczyny błędów, kompletny endpoint referencyjny w PHP i Node oraz import przez API.
Dzięki danym klientów asystent AI odpowiada na pytania takie jak „Gdzie jest moja paczka?” albo „Czy moja faktura jest opłacona?” – tylko potwierdzonym odwiedzającym i tylko na temat ich własnych zamówień. Ta strona jest dokumentacją techniczną dla osoby, która buduje endpoint. Co pokazuje asystent i jakie pola udostępniasz w koncie, opisuje Dane klientów w czacie.
Dostępne w płatnych planach (także w okresie próbnym).
Wymagania
- Weryfikacja tożsamości z exp. Odwiedzający musi być zalogowany z prawidłowym
userHashiexp(Identyfikacja odwiedzających). Bezexpuchodzi za potwierdzonego, ale nie za świeżego – wtedy nic nie jest pobierane. - Ustawienie w koncie w sekcji Projekt → Dane klientów: wybierz tryb, wpisz endpoint, udostępnij pola, ustal komunikat dla niezalogowanych odwiedzających.
Tryby:
- Pobieranie ze sklepu – reactive.chat przy każdym pasującym pytaniu odpytuje twój endpoint. Odpowiedź nie jest zapisywana. Zalecane.
- Import przez API – twój sklep przesyła zamówienia klienta do nas przez API; są przechowywane u nas w postaci zaszyfrowanej, najwyżej 90 dni.
- Oba – najpierw dane zaimportowane, a jeśli tam nic nie ma, pobieranie.
- Wyłączone.
Pobieranie
reactive.chat wywołuje twój endpoint metodą POST:
POST /rc-kundendaten HTTP/1.1
Host: shop.deine-firma.de
Content-Type: application/json
Accept: application/json
User-Agent: reactive.chat-kundendaten/1
X-RC-Zeit: 1792503600
X-RC-Signatur: 5d41402abc4b2a76b9719d911017c592...
{"user_id":"4711","anliegen":"lieferung","referenz":"2026-0815","sprache":"de","nonce":"9f86d081884c7d659a2feaa0c55ad015"}
Nagłówki:
- X-RC-Zeit – sekundy Unix w chwili żądania.
- X-RC-Signatur –
hex(HMAC_SHA256(endpunkt_geheimnis, X-RC-Zeit + "." + roher_body)), 64 małe litery, bez prefiksu. Kluczem jest sekret endpointu z konta jako ciąg znaków (zaczyna się odrckd_).
Treść:
- user_id (String) – potwierdzony identyfikator z
identify. Nigdy dane podane w czacie. - anliegen (String) –
bestellung,lieferung,rechnung,retoure; przy przycisku testowymtest. - referenz (String albo
null) – numer, który klient podał w czacie. Tylko filtr w obrębie zamówień tegouser_id– cudzy numer nie może niczego znaleźć. - sprache (String) – dwuliterowy, np.
de. - nonce (String) – 32 znaki hex, nowy przy każdym żądaniu.
Twój endpoint musi:
- sprawdzać podpis na surowej treści (w stałym czasie, np.
hash_equals) – a nie na ponownie zakodowanym JSON; - odrzucać żądania, których
X-RC-Zeitróżni się od twojego zegara o ponad 300 sekund; - przyjmować każdy
noncetylko raz (zalecane); - zwracać wyłącznie dane przekazanego
user_id; - być dostępny przez
httpsna porcie 443 pod publicznym adresem – bez przekierowania, bez nazwy użytkownika/hasła w adresie; - odpowiedzieć w całości w ciągu 3 sekund (nawiązanie połączenia najwyżej 2 sekundy).
Odpowiedź
Dokładnie HTTP 200 z obiektem JSON. Brak trafienia to {"bestellungen":[]}, również z 200.
{
"bestellungen": [
{
"nummer": "2026-0815",
"datum": "2026-09-28",
"status": "wysłane",
"summe": "49.90",
"waehrung": "EUR",
"positionen": [ { "name": "Rowerek biegowy niebieski", "menge": 1 } ],
"versand": { "dienst": "DHL", "sendungsnummer": "00340434161234567890", "status": "w doręczeniu" },
"rechnung": { "nummer": "R-2026-1201", "status": "opłacona", "faellig_am": "2026-10-12" }
}
]
}
Pola zamówienia:
- nummer (String, obowiązkowe) – bez numeru zamówienie jest odrzucane.
- datum (String,
RRRR-MM-DD). - status (String) – status zamówienia jako zwykły tekst.
- summe (String albo liczba), waehrung (ISO 4217, np.
EUR). - positionen (lista, najwyżej 20) – każda z
nameimenge. - versand (obiekt) –
dienst,sendungsnummer,status, opcjonalnieortiplz(tylko jeśli wyraźnie udostępnione w koncie). - rechnung (obiekt) –
nummer,status,faellig_am.
Limity: najwyżej 10 zamówień, 20 pozycji na zamówienie, teksty najwyżej 200 znaków, cała odpowiedź najwyżej 16 384 bajty. Nieznane i nieudostępnione w koncie pola są odrzucane bez komunikatu o błędzie.
Nigdy nie przejmowane, nawet jeśli je wyślesz: ulica i kolejne linie adresu, imiona i nazwiska, e-mail, telefon, IBAN/BIC/dane konta i karty, data urodzenia, hasła/tokeny/klucze, numery podatkowe i numery dokumentów tożsamości. Najlepiej w ogóle ich nie wysyłaj.
Co dzieje się z odpowiedzią: trafia tylko do tej jednej odpowiedzi asystenta i nie jest zapisywana – ani w rozmowie, ani w bazie wiedzy; w logu widnieją tylko liczba i czas trwania. Jeśli twój endpoint nie odpowie na czas albo odpowie błędnie, asystent odpowiada tak, jakby nie było danych.
Endpoint referencyjny w PHP
<?php
declare(strict_types=1);
$GEHEIMNIS = (string) getenv('RC_KD_GEHEIMNIS'); // rckd_... z konta
function antworte(int $status, array $daten): never {
http_response_code($status);
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');
echo json_encode($daten, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
exit;
}
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') { antworte(405, ['fehler' => 'tylko POST']); }
$body = (string) file_get_contents('php://input', false, null, 0, 4097);
if (strlen($body) > 4096) { antworte(413, ['fehler' => 'za duże']); }
$zeit = (string) ($_SERVER['HTTP_X_RC_ZEIT'] ?? '');
$sig = strtolower((string) ($_SERVER['HTTP_X_RC_SIGNATUR'] ?? ''));
if (!preg_match('/^\d{9,11}$/', $zeit) || abs(time() - (int) $zeit) > 300) {
antworte(401, ['fehler' => 'zeit']);
}
if (!hash_equals(hash_hmac('sha256', $zeit . '.' . $body, $GEHEIMNIS), $sig)) {
antworte(401, ['fehler' => 'signatur']);
}
$j = json_decode($body, true);
$userId = is_string($j['user_id'] ?? null) ? $j['user_id'] : '';
$referenz = is_string($j['referenz'] ?? null) ? trim($j['referenz']) : '';
$nonce = is_string($j['nonce'] ?? null) ? $j['nonce'] : '';
if ($userId === '' || !preg_match('/^[0-9a-f]{32}$/', $nonce)) {
antworte(400, ['fehler' => 'anfrage']);
}
$pdo = new PDO(getenv('SHOP_DB_DSN'), getenv('SHOP_DB_USER'), getenv('SHOP_DB_PASS'),
[PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
// nonce tylko raz: CREATE TABLE rc_nonce (nonce CHAR(32) PRIMARY KEY, zeit INT NOT NULL);
$pdo->prepare('DELETE FROM rc_nonce WHERE zeit < ?')->execute([time() - 600]);
try {
$pdo->prepare('INSERT INTO rc_nonce (nonce, zeit) VALUES (?, ?)')->execute([$nonce, time()]);
} catch (PDOException $e) {
antworte(409, ['fehler' => 'nonce już użyty']);
}
if ($userId === 'rc-test') { antworte(200, ['bestellungen' => []]); } // przycisk testowy
// TYLKO ten klient; referencja jest tylko filtrem
$sql = 'SELECT id, nummer, datum, status, summe, waehrung, versand_dienst, sendungsnummer,
versand_status, rechnung_nummer, rechnung_status, faellig_am
FROM bestellungen WHERE kunde_id = :u';
$w = [':u' => $userId];
if ($referenz !== '') {
$sql .= ' AND (nummer = :r1 OR rechnung_nummer = :r2 OR sendungsnummer = :r3)';
$w += [':r1' => $referenz, ':r2' => $referenz, ':r3' => $referenz];
}
$st = $pdo->prepare($sql . ' ORDER BY datum DESC LIMIT 10');
$st->execute($w);
$pos = $pdo->prepare('SELECT name, menge FROM bestell_positionen WHERE bestellung_id = ? LIMIT 20');
$aus = [];
foreach ($st->fetchAll(PDO::FETCH_ASSOC) as $b) {
$pos->execute([$b['id']]);
$aus[] = [
'nummer' => (string) $b['nummer'],
'datum' => $b['datum'],
'status' => $b['status'],
'summe' => $b['summe'],
'waehrung' => $b['waehrung'],
'positionen' => $pos->fetchAll(PDO::FETCH_ASSOC),
'versand' => ['dienst' => $b['versand_dienst'], 'sendungsnummer' => $b['sendungsnummer'],
'status' => $b['versand_status']],
'rechnung' => ['nummer' => $b['rechnung_nummer'], 'status' => $b['rechnung_status'],
'faellig_am' => $b['faellig_am']],
];
}
antworte(200, ['bestellungen' => $aus]);
Endpoint referencyjny w Node.js (Express)
const crypto = require('crypto');
const express = require('express');
const app = express();
const GEHEIM = process.env.RC_KD_GEHEIMNIS;
const gesehen = new Map(); // nonce -> czas; na produkcji baza danych
app.post('/rc-kundendaten', express.raw({ type: 'application/json', limit: '4kb' }), async (req, res) => {
const body = req.body.toString('utf8'); // SUROWA treść
const zeit = String(req.get('X-RC-Zeit') || '');
const sig = String(req.get('X-RC-Signatur') || '').toLowerCase();
if (!/^\d{9,11}$/.test(zeit) || Math.abs(Date.now() / 1000 - Number(zeit)) > 300) {
return res.status(401).json({ fehler: 'zeit' });
}
const soll = crypto.createHmac('sha256', GEHEIM).update(zeit + '.' + body).digest('hex');
if (sig.length !== soll.length || !crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(soll))) {
return res.status(401).json({ fehler: 'signatur' });
}
const j = JSON.parse(body);
if (!/^[0-9a-f]{32}$/.test(j.nonce || '') || gesehen.has(j.nonce)) {
return res.status(409).json({ fehler: 'nonce' });
}
gesehen.set(j.nonce, Date.now());
if (j.user_id === 'rc-test') { return res.json({ bestellungen: [] }); }
const bestellungen = await ladeBestellungen(j.user_id, j.referenz); // TYLKO ten klient
res.json({ bestellungen: bestellungen.slice(0, 10) });
});
Testowanie połączenia
Testuj połączenie w koncie wywołuje twój endpoint z user_id rc-test i anliegen test. Zwróć wtedy pustą listę albo przykładowe zamówienie. Jeśli wywołanie się nie powiedzie, konto podaje jedną z tych przyczyn:
- kein_endpunkt, kein_geheimnis – czegoś brakuje w koncie.
- url_ungueltig, nur_https, port_verboten, zugangsdaten_in_url – adres jest niedozwolony (tylko
https, port 443, bez danych logowania w adresie). - host_unaufloesbar, adresse_privat, adresse_gewechselt – nazwa się nie rozwiązuje albo rozwiązuje się na adres prywatny.
- tls, verbindung, zeitueberschreitung – certyfikat, połączenie albo limit 3 sekund.
- umleitung – twój endpoint odpowiada kodem 3xx.
- http_<code> – status inny niż 200 (np.
http_401: twoja kontrola podpisu odrzuca żądanie). - antwort_zu_gross, antwort_kein_json, antwort_ohne_bestellungen – odpowiedź nie pasuje do schematu.
- tarif – projekt nie ma płatnego planu.
Import przez API
Jeśli twój sklep nie może udostępnić endpointu, przesyłasz dane do nas. Potrzebujesz klucza API z zakresem tylko import danych klientów (kundendaten:schreiben) i trybu Import przez API albo Oba.
curl -X PUT "https://reactive.chat/v1/kundendaten/4711?project_id=123" \
-H "Authorization: Bearer rc_live_..." \
-H "Content-Type: application/json" \
-d '{"bestellungen":[{"nummer":"2026-0815","datum":"2026-09-28","status":"wysłane"}]}'
- PUT /v1/kundendaten/{user_id} – zastępuje wszystkie zamówienia tego klienta (najwyżej 50; pusta lista usuwa). Odpowiedź
{"data":{"stored":1,"dropped":0,"max_orders":50,"expires_in_days":90}}. - DELETE /v1/kundendaten/{user_id} – usuwa je; działa zawsze, także bez planu. Odpowiedź
{"data":{"deleted":1}}. - project_id – obowiązkowe, gdy tylko twoja przestrzeń robocza ma więcej niż jeden projekt.
- user_id – ten sam identyfikator, który podpisujesz w
identify; zakodowany dla URL, najwyżej 200 znaków. Zakodowany ukośnik (%2F) nie jest możliwy. GETnie istnieje.
Zaimportowane dane są przechowywane w postaci zaszyfrowanej (numer klienta tylko jako hash) i usuwane 90 dni po ostatnim imporcie. Wysyłaj więc zmiany na bieżąco, np. przy każdej zmianie statusu.
Błędy: 402 plan_required (brak płatnego planu), 409 import_disabled (tryb nie pozwala na import, z kd_modus), 400 bad_request z param (user_id, project_id, bestellungen), 404 (cudzy projekt). Ogólny format błędów opisuje API serwera.
Niezalogowani odwiedzający
Jeśli ktoś bez potwierdzonej tożsamości zapyta o zamówienie, asystent niczego nie pobiera, tylko pokazuje twój komunikat z konta (tekst w każdym języku, opcjonalnie link do logowania) albo przekazuje rozmowę twojemu zespołowi.
Dalej: Webhooki · API serwera
Kemas kini terakhir: