顧客データエンドポイント:注文、配送、請求書
顧客データ連携の技術リファレンス:署名付きリクエスト、応答スキーマ、上限、失敗理由、PHP・Nodeの完全な参照エンドポイント、APIインポート。
顧客データでは、確認済みの訪問者に、本人の注文についてだけ、「荷物はどこですか?」「請求書は支払い済みですか?」などに回答します。このページは、エンドポイントを作る人向けです。表示内容とフィールド選択はチャット内の顧客データにあります。
試用期間を含む有料プランで利用できます。
要件
- exp付きの本人確認。 有効な
userHashとexpが必要です(訪問者の本人確認)。expなしは確認済みでも有効期限内ではないため、取得しません。 - プロジェクト → 顧客データの設定。 モード、エンドポイント、フィールド、未ログイン者への案内を設定します。
モード:
- 取得 – 該当質問ごとに照会し、応答を保存しません。推奨。
- インポート – APIで顧客の注文を送り、暗号化して最大90日保存。
- 両方 – インポートを優先し、なければ取得。
- 無効。
リクエスト
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"}
ヘッダー:
- X-RC-Zeit – リクエスト時のUnix秒。
- X-RC-Signatur –
hex(HMAC_SHA256(endpoint_secret, X-RC-Zeit + "." + raw_body))。小文字64文字で、プレフィックスはなし。キーはアカウントのエンドポイント用シークレット(rckd_で始まる)を文字列で使います。
本文:
- user_id(文字列) –
identifyの確認済みID。チャット入力は使いません。 - anliegen(文字列) –
bestellung(注文)、lieferung(配送)、rechnung(請求書)、retoure(返品)、テストボタンのtest。 - referenz(文字列または
null) – 顧客がチャットで伝えた番号。このuser_idの注文内だけの絞り込みで、他人の番号では何も見つけてはいけません。 - sprache(文字列) –
deなどの2文字。 - nonce(文字列) – 毎回新しい16進数32文字。
エンドポイントの要件:
- 再エンコードしたJSONではなく、生の本文の署名を定時間で検証(
hash_equalsなど)。 X-RC-Zeitが自分の時計と300秒超ずれたものを拒否。- 各
nonceは1回だけ受け付けることを推奨。 - 指定した
user_idのデータだけを返す。 - 公開アドレスのポート443の
httpsで到達でき、リダイレクトやURLのユーザー名・パスワードなし。 - 3秒以内に完全に応答し、接続確立は最大2秒。
応答
厳密にHTTP 200のJSONオブジェクト。該当なしも200で{"bestellungen":[]}です。
{
"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" }
}
]
}
注文のフィールド:
- nummer(文字列、必須) – 番号なしは破棄。
- datum(文字列、
YYYY-MM-DD)。 - status(文字列) – 注文状況の通常テキスト。
- summe(文字列または数値)、waehrung(ISO 4217、
EURなど)。 - positionen(リスト、最大20) –
nameとmenge。 - versand(オブジェクト) –
dienst、sendungsnummer、status、任意のort、plz(アカウントで明示的に有効な場合だけ)。 - rechnung(オブジェクト) –
nummer、status、faellig_am。
上限: 最大10注文、注文ごと20商品、テキスト200文字、全体16,384バイト。不明なフィールドや無効なものは、エラーなしで破棄します。
絶対に取り込まないもの: 通りの住所と追加住所、名前、メール、電話、IBAN/BIC/口座/カード情報、生年月日、パスワード/トークン/キー、税番号、身分証番号。そもそも送らないことをお勧めします。
応答の扱い: その1回答だけに使い、会話にもナレッジベースにも保存しません。ログは件数と時間だけです。応答が遅い、またはエラーなら、データがないものとして回答します。
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]);
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) });
});
接続をテストする
アカウントの接続をテストは、user_idをrc-test、anliegenをtestとして呼び出します。空一覧かサンプル注文を返してください。失敗時は理由を表示します。
- kein_endpunkt、kein_geheimnis – アカウントの設定不足。
- url_ungueltig、nur_https、port_verboten、zugangsdaten_in_url – アドレスが不許可(
https、443、認証情報なしのみ)。 - host_unaufloesbar、adresse_privat、adresse_gewechselt – 名前を解決できないか、プライベートアドレス。
- tls、verbindung、zeitueberschreitung – 証明書、接続、3秒期限。
- umleitung – 3xx応答。
- http_<code> – 200以外(
http_401などは署名検証による拒否)。 - antwort_zu_gross、antwort_kein_json、antwort_ohne_bestellungen – スキーマ不一致。
- tarif – 有料プランではない。
APIでインポートする
エンドポイントを提供できなければ、データを送信します。顧客データのインポートのみ(kundendaten:schreiben)のAPIキーと、インポートか両方のモードが必要です。
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} – その顧客の全注文を置き換えます(最大50、空一覧で削除)。応答は
{"data":{"stored":1,"dropped":0,"max_orders":50,"expires_in_days":90}}。 - DELETE /v1/kundendaten/{user_id} – 削除。プランなしでも常に可能。応答は
{"data":{"deleted":1}}。 - project_id – 複数プロジェクトの場合は必須。
- user_id –
identifyで署名するのと同じID。URLエンコード、最大200文字。エンコードしたスラッシュ(%2F)は不可。 GETはありません。
暗号化して保存し、顧客番号はハッシュだけです。最終インポートから90日後に削除します。状態変更ごとなど、継続的に更新してください。
エラー: 402 plan_required(有料でない)、409 import_disabled(モードで未許可、kd_modus付き)、400 bad_requestとparam(user_id、project_id、bestellungen)、404(他人のプロジェクト)。一般形式はサーバーAPI参照。
ログインしていない訪問者
本人確認なしで注文を尋ねても何も取得せず、設定した言語別の案内と任意のログインリンクを表示するか、チームへ引き継ぎます。
最終更新: