Customer data endpoint: orders, shipping and invoices
Technical reference for the customer data connection – the signed request from reactive.chat to your shop, response schema, limits, failure reasons, a complete reference endpoint in PHP and Node, and the import via API.
With customer data, the AI assistant answers questions such as “Where is my parcel?” or “Has my invoice been paid?” – only for confirmed visitors and only about their own orders. This page is the technical reference for the person who builds the endpoint. What the assistant shows and which fields you enable in your account is described in Customer data in the chat.
Available on the paid plans (including during the trial).
Requirements
- Identity verification with exp. The visitor must be signed in with a valid
userHashandexp(Identifying visitors). Withoutexpthey count as confirmed but not as fresh – then nothing is fetched. - Setting in your account under Project → Customer data: choose the mode, enter the endpoint, enable fields, set the notice for visitors who are not signed in.
Modes:
- Fetch – reactive.chat queries your endpoint for each matching question. The response is not stored. Recommended.
- Import – your shop pushes a customer's orders to us via the API; they are stored encrypted on our side, for at most 90 days.
- Both – the imported data first; if nothing is found there, the fetch.
- Off.
The request
reactive.chat calls your endpoint with POST:
POST /rc-kundendaten HTTP/1.1
Host: shop.your-company.com
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":"en","nonce":"9f86d081884c7d659a2feaa0c55ad015"}
Headers:
- X-RC-Zeit – Unix seconds at the time of the request.
- X-RC-Signatur –
hex(HMAC_SHA256(endpoint_secret, X-RC-Zeit + "." + raw_body)), 64 lowercase characters, without a prefix. The key is the endpoint secret from your account as a string (it starts withrckd_).
Body:
- user_id (string) – the confirmed ID from
identify. Never anything taken from the chat. - anliegen (string) –
bestellung(order),lieferung(delivery),rechnung(invoice),retoure(return);testfor the test button. - referenz (string or
null) – a number the customer mentioned in the chat. Only a filter within the orders of thisuser_id– someone else's number must find nothing. - sprache (string) – two letters, e.g.
de. - nonce (string) – 32 hex characters, new for each request.
Your endpoint must:
- verify the signature over the raw body (constant-time, e.g.
hash_equals) – not over re-encoded JSON; - reject requests whose
X-RC-Zeitdiffers from your clock by more than 300 seconds; - accept each
nonceonly once (recommended); - return only data of the given
user_id; - be reachable via
httpson port 443 at a public address – without redirects, without a user name/password in the address; - respond completely within 3 seconds (connection setup at most 2 seconds).
The response
Exactly HTTP 200 with a JSON object. No match is {"bestellungen":[]}, also with 200.
{
"bestellungen": [
{
"nummer": "2026-0815",
"datum": "2026-09-28",
"status": "shipped",
"summe": "49.90",
"waehrung": "EUR",
"positionen": [ { "name": "Balance bike blue", "menge": 1 } ],
"versand": { "dienst": "DHL", "sendungsnummer": "00340434161234567890", "status": "out for delivery" },
"rechnung": { "nummer": "R-2026-1201", "status": "paid", "faellig_am": "2026-10-12" }
}
]
}
Fields per order:
- nummer (string, required) – an order without a number is discarded.
- datum (string,
YYYY-MM-DD). - status (string) – order status as plain text.
- summe (string or number), waehrung (ISO 4217, e.g.
EUR). - positionen (list, at most 20) – each with
nameandmenge. - versand (object) –
dienst,sendungsnummer,status, optionallyortandplz(only if explicitly enabled in your account). - rechnung (object) –
nummer,status,faellig_am.
Limits: at most 10 orders, 20 items per order, texts at most 200 characters, the whole response at most 16,384 bytes. Unknown fields and fields not enabled in your account are discarded without an error.
Never taken over, even if you send them: street and further address lines, names, email, phone, IBAN/BIC/account and card data, date of birth, passwords/tokens/keys, tax and ID numbers. Best not to send them at all.
What happens to the response: it is used only for the one answer of the assistant and is not stored – neither in the conversation nor in the knowledge base; the log records only count and duration. If your endpoint does not respond in time or responds with an error, the assistant answers as if there were no data.
Reference endpoint in PHP
<?php
declare(strict_types=1);
$GEHEIMNIS = (string) getenv('RC_KD_GEHEIMNIS'); // rckd_... from the account
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' => 'POST only']); }
$body = (string) file_get_contents('php://input', false, null, 0, 4097);
if (strlen($body) > 4096) { antworte(413, ['fehler' => 'too large']); }
$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 only once: 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 already used']);
}
if ($userId === 'rc-test') { antworte(200, ['bestellungen' => []]); } // test button
// ONLY this customer; the reference is only a 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]);
Reference endpoint 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 -> time; a database in production
app.post('/rc-kundendaten', express.raw({ type: 'application/json', limit: '4kb' }), async (req, res) => {
const body = req.body.toString('utf8'); // RAW 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); // ONLY this customer
res.json({ bestellungen: bestellungen.slice(0, 10) });
});
Testing the connection
Test connection in your account calls your endpoint with user_id rc-test and anliegen test. Return an empty list or a sample order for it. If the call fails, your account names one of these reasons:
- kein_endpunkt, kein_geheimnis – something is missing in your account.
- url_ungueltig, nur_https, port_verboten, zugangsdaten_in_url – the address is not permitted (only
https, port 443, no credentials in the address). - host_unaufloesbar, adresse_privat, adresse_gewechselt – the name does not resolve, or resolves to a private address.
- tls, verbindung, zeitueberschreitung – certificate, connection or the 3-second deadline.
- umleitung – your endpoint responds with 3xx.
- http_<code> – a status other than 200 (e.g.
http_401: your signature check rejects the request). - antwort_zu_gross, antwort_kein_json, antwort_ohne_bestellungen – the response does not match the schema.
- tarif – the project is not on a paid plan.
Import via API
If your shop cannot offer an endpoint, you push the data to us. You need an API key with the scope customer data import only (kundendaten:schreiben) and the mode Import or Both.
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":"shipped"}]}'
- PUT /v1/kundendaten/{user_id} – replaces all orders of this customer (at most 50; an empty list deletes them). Response
{"data":{"stored":1,"dropped":0,"max_orders":50,"expires_in_days":90}}. - DELETE /v1/kundendaten/{user_id} – removes them; always works, even without a plan. Response
{"data":{"deleted":1}}. - project_id – required as soon as your workspace has more than one project.
- user_id – the same ID you sign in
identify; URL-encoded, at most 200 characters. An encoded slash (%2F) is not possible. - There is no
GET.
Imported data is stored encrypted (the customer number only as a hash) and deleted 90 days after the last import. So send changes continuously, for example on every status change.
Errors: 402 plan_required (no paid plan), 409 import_disabled (the mode does not allow import, with kd_modus), 400 bad_request with param (user_id, project_id, bestellungen), 404 (someone else's project). The general error format is described under Server API.
Visitors who are not signed in
If someone without a confirmed identity asks about an order, the assistant fetches nothing; instead it shows your notice from your account (text per language, optionally a sign-in link) or hands over to your team.
Next: Webhooks · Server API
آخر تحديث: