고객 데이터 엔드포인트: 주문, 배송과 청구서
고객 데이터 연결 기술 참조: reactive.chat의 서명된 쇼핑몰 요청, 응답 스키마, 제한, 실패 이유, PHP·Node 참조 엔드포인트와 API 가져오기.
고객 데이터로 AI 어시스턴트가 “내 택배는 어디에 있나요?”, “청구서가 결제되었나요?”에 답합니다. 확인된 방문자의 자신의 주문만 사용합니다. 이 페이지는 엔드포인트 개발자용 기술 참조입니다. 표시 내용과 계정의 필드 활성화는 채팅의 고객 데이터에 있습니다.
체험 기간을 포함한 유료 요금제에서 제공합니다.
요구 사항
- 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같은 두 글자입니다. - nonce(문자열) – 요청마다 새로운 32자리 16진수입니다.
엔드포인트 요구 사항:
- 다시 인코딩한 JSON이 아닌 원시 본문 서명을 검증합니다(예:
hash_equals로 상수 시간 비교). X-RC-Zeit가 서버 시계와 300초 넘게 다른 요청을 거부합니다.- 각
nonce는 한 번만 수락합니다(권장). - 지정한
user_id의 데이터만 반환합니다. - 공인 주소의 443 포트에서
https로 접근 가능하며 리디렉션과 주소의 사용자 이름·비밀번호가 없어야 합니다. - 3초 안에 응답을 완료합니다(연결 설정 최대 2초).
응답
정확히 HTTP 200과 JSON 객체입니다. 일치 없음은 {"bestellungen":[]}이며 역시 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" }
}
]
}
주문별 필드:
- 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·계좌·카드 정보, 생년월일, 비밀번호·토큰·키, 세금·신분증 번호입니다. 처음부터 보내지 않는 것이 좋습니다.
응답 처리: 어시스턴트의 답변 한 번에만 사용하며 대화나 지식 베이스에 저장하지 않습니다. 로그에는 개수와 소요 시간만 기록합니다. 제시간에 응답하지 않거나 오류이면 데이터가 없는 것처럼 답합니다.
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에 있습니다.
로그인하지 않은 방문자
확인된 신원 없이 주문을 질문하면 조회하지 않습니다. 대신 계정의 안내(언어별 텍스트, 선택적 로그인 링크)를 표시하거나 팀에 인계합니다.
최종 업데이트: