Kundendaten-Endpunkt: Bestellungen, Versand und Rechnungen
Technische Referenz für die Kundendaten-Anbindung – der signierte Abruf von reactive.chat an deinen Shop, Antwortschema, Grenzen, Fehlergründe, ein vollständiger Referenz-Endpunkt in PHP und Node sowie der Import per API.
Mit Kundendaten beantwortet der KI-Assistent Fragen wie „Wo ist mein Paket?“ oder „Ist meine Rechnung bezahlt?“ – nur für bestätigte Besucher und nur zu ihren eigenen Bestellungen. Diese Seite ist die technische Referenz für die Person, die den Endpunkt baut. Was der Assistent zeigt und welche Felder du im Konto freigibst, beschreibt Kundendaten im Chat.
Verfügbar in den bezahlten Tarifen (auch in der Testphase).
Voraussetzungen
- Identitätsprüfung mit exp. Der Besucher muss mit gültigem
userHashundexpangemeldet sein (Besucher identifizieren). Ohneexpgilt er als bestätigt, aber nicht als frisch – dann wird nichts abgerufen. - Einstellung im Konto unter Projekt → Kundendaten: Betriebsart wählen, Endpunkt eintragen, Felder freigeben, Hinweis für nicht angemeldete Besucher festlegen.
Betriebsarten:
- Abruf – reactive.chat fragt je passender Frage deinen Endpunkt. Die Antwort wird nicht gespeichert. Empfohlen.
- Import – dein Shop schiebt die Bestellungen eines Kunden per API zu uns; sie liegen verschlüsselt bei uns, höchstens 90 Tage.
- Beide – zuerst die importierten Daten, findet sich dort nichts, der Abruf.
- Aus.
Der Abruf
reactive.chat ruft deinen Endpunkt mit POST auf:
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"}
Kopfzeilen:
- X-RC-Zeit – Unix-Sekunden zum Zeitpunkt der Anfrage.
- X-RC-Signatur –
hex(HMAC_SHA256(endpunkt_geheimnis, X-RC-Zeit + "." + roher_body)), 64 Kleinbuchstaben, ohne Präfix. Schlüssel ist das Endpunkt-Geheimnis aus dem Konto als Zeichenkette (beginnt mitrckd_).
Rumpf:
- user_id (String) – die bestätigte Kennung aus
identify. Nie eine Angabe aus dem Chat. - anliegen (String) –
bestellung,lieferung,rechnung,retoure; beim Testknopftest. - referenz (String oder
null) – eine Nummer, die der Kunde im Chat genannt hat. Nur ein Filter innerhalb der Bestellungen dieseruser_id– eine fremde Nummer darf nichts finden. - sprache (String) – zweibuchstabig, z. B.
de. - nonce (String) – 32 Hex-Zeichen, je Anfrage neu.
Dein Endpunkt muss:
- die Signatur über den rohen Body prüfen (zeitkonstant, z. B.
hash_equals) – nicht über ein neu kodiertes JSON; - Anfragen ablehnen, deren
X-RC-Zeitmehr als 300 Sekunden von deiner Uhr abweicht; - jede
noncenur einmal annehmen (empfohlen); - nur Daten der übergebenen
user_idliefern; - über
httpsauf Port 443 unter einer öffentlichen Adresse erreichbar sein – ohne Weiterleitung, ohne Benutzername/Passwort in der Adresse; - innerhalb von 3 Sekunden vollständig antworten (Verbindungsaufbau höchstens 2 Sekunden).
Die Antwort
Genau HTTP 200 mit einem JSON-Objekt. Kein Treffer ist {"bestellungen":[]}, ebenfalls mit 200.
{
"bestellungen": [
{
"nummer": "2026-0815",
"datum": "2026-09-28",
"status": "versandt",
"summe": "49.90",
"waehrung": "EUR",
"positionen": [ { "name": "Laufrad blau", "menge": 1 } ],
"versand": { "dienst": "DHL", "sendungsnummer": "00340434161234567890", "status": "in Zustellung" },
"rechnung": { "nummer": "R-2026-1201", "status": "bezahlt", "faellig_am": "2026-10-12" }
}
]
}
Felder je Bestellung:
- nummer (String, Pflicht) – ohne Nummer wird die Bestellung verworfen.
- datum (String,
JJJJ-MM-TT). - status (String) – Bestellstatus als Klartext.
- summe (String oder Zahl), waehrung (ISO 4217, z. B.
EUR). - positionen (Liste, höchstens 20) – je
nameundmenge. - versand (Objekt) –
dienst,sendungsnummer,status, optionalortundplz(nur, wenn im Konto ausdrücklich freigegeben). - rechnung (Objekt) –
nummer,status,faellig_am.
Grenzen: höchstens 10 Bestellungen, 20 Positionen je Bestellung, Texte höchstens 200 Zeichen, ganze Antwort höchstens 16 384 Bytes. Unbekannte und im Konto nicht freigegebene Felder werden ohne Fehlermeldung verworfen.
Nie übernommen, auch wenn du sie schickst: Straße und weitere Adresszeilen, Namen, E-Mail, Telefon, IBAN/BIC/Konto- und Kartendaten, Geburtsdatum, Passwörter/Tokens/Schlüssel, Steuer- und Ausweisnummern. Schick sie am besten gar nicht.
Was mit der Antwort passiert: Sie geht nur in die eine Antwort des Assistenten ein und wird nicht gespeichert – weder im Gespräch noch in der Wissensdatenbank; im Protokoll stehen nur Anzahl und Dauer. Antwortet dein Endpunkt nicht rechtzeitig oder fehlerhaft, antwortet der Assistent so, als lägen keine Daten vor.
Referenz-Endpunkt in PHP
<?php
declare(strict_types=1);
$GEHEIMNIS = (string) getenv('RC_KD_GEHEIMNIS'); // rckd_... aus dem Konto
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' => 'nur POST']); }
$body = (string) file_get_contents('php://input', false, null, 0, 4097);
if (strlen($body) > 4096) { antworte(413, ['fehler' => 'zu gross']); }
$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 nur einmal: 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 schon benutzt']);
}
if ($userId === 'rc-test') { antworte(200, ['bestellungen' => []]); } // Testknopf
// NUR dieser Kunde; die Referenz ist nur ein Filter
$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]);
Referenz-Endpunkt in 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 -> Zeit; in Produktion eine Datenbank
app.post('/rc-kundendaten', express.raw({ type: 'application/json', limit: '4kb' }), async (req, res) => {
const body = req.body.toString('utf8'); // ROHER Body
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); // NUR dieser Kunde
res.json({ bestellungen: bestellungen.slice(0, 10) });
});
Verbindung testen
Verbindung testen im Konto ruft deinen Endpunkt mit user_id rc-test und anliegen test auf. Liefere dafür eine leere Liste oder eine Beispielbestellung. Scheitert der Aufruf, nennt das Konto einen dieser Gründe:
- kein_endpunkt, kein_geheimnis – im Konto fehlt etwas.
- url_ungueltig, nur_https, port_verboten, zugangsdaten_in_url – die Adresse ist unzulässig (nur
https, Port 443, keine Zugangsdaten in der Adresse). - host_unaufloesbar, adresse_privat, adresse_gewechselt – der Name löst nicht oder auf eine private Adresse auf.
- tls, verbindung, zeitueberschreitung – Zertifikat, Verbindung oder 3-Sekunden-Frist.
- umleitung – dein Endpunkt antwortet mit 3xx.
- http_<code> – ein anderer Status als 200 (z. B.
http_401: deine Signaturprüfung lehnt ab). - antwort_zu_gross, antwort_kein_json, antwort_ohne_bestellungen – die Antwort passt nicht zum Schema.
- tarif – das Projekt hat keinen bezahlten Tarif.
Import per API
Kann dein Shop keinen Endpunkt anbieten, schiebst du die Daten zu uns. Du brauchst einen API-Schlüssel mit der Rechtestufe Kundendaten-Import (kundendaten:schreiben) und die Betriebsart Import oder Beide.
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":"versandt"}]}'
- PUT /v1/kundendaten/{user_id} – ersetzt alle Bestellungen dieses Kunden (höchstens 50; eine leere Liste löscht). Antwort
{"data":{"stored":1,"dropped":0,"max_orders":50,"expires_in_days":90}}. - DELETE /v1/kundendaten/{user_id} – entfernt sie; geht immer, auch ohne Tarif. Antwort
{"data":{"deleted":1}}. - project_id – Pflicht, sobald dein Workspace mehr als ein Projekt hat.
- user_id – dieselbe Kennung, die du in
identifysignierst; URL-kodiert, höchstens 200 Zeichen. Ein kodierter Schrägstrich (%2F) ist nicht möglich. - Ein
GETgibt es nicht.
Importierte Daten werden verschlüsselt abgelegt (die Kundennummer nur als Hash) und 90 Tage nach dem letzten Import gelöscht. Schicke Änderungen also laufend, etwa bei jedem Statuswechsel.
Fehler: 402 plan_required (kein bezahlter Tarif), 409 import_disabled (Betriebsart erlaubt keinen Import, mit kd_modus), 400 bad_request mit param (user_id, project_id, bestellungen), 404 (fremdes Projekt). Das allgemeine Fehlerformat steht unter Server-API.
Nicht angemeldete Besucher
Fragt jemand ohne bestätigte Identität nach einer Bestellung, ruft der Assistent nichts ab, sondern zeigt deinen Hinweis aus dem Konto (Text je Sprache, optional ein Link zur Anmeldung) oder übergibt an dein Team.
Weiter: Webhooks · Server-API
Zadnje ažuriranje: