무엇을 도와드릴까요?

문의 내용을 간단히 알려 주세요. 이메일로 답변드릴게요.

This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.
홈으로

고객 데이터 엔드포인트: 주문, 배송과 청구서

고객 데이터 연결 기술 참조: reactive.chat의 서명된 쇼핑몰 요청, 응답 스키마, 제한, 실패 이유, PHP·Node 참조 엔드포인트와 API 가져오기.

고객 데이터로 AI 어시스턴트가 “내 택배는 어디에 있나요?”, “청구서가 결제되었나요?”에 답합니다. 확인된 방문자의 자신의 주문만 사용합니다. 이 페이지는 엔드포인트 개발자용 기술 참조입니다. 표시 내용과 계정의 필드 활성화는 채팅의 고객 데이터에 있습니다.

체험 기간을 포함한 유료 요금제에서 제공합니다.

요구 사항

  1. exp 포함 본인 확인. 유효한 userHash와 exp가 모두 있는 로그인 방문자여야 합니다(방문자 식별). exp가 없으면 확인된 사용자이지만 만료되지 않은 신원으로는 간주하지 않으므로 조회하지 않습니다.
  2. 계정의 프로젝트 → 고객 데이터에서 모드, 엔드포인트, 허용 필드와 미로그인 방문자 안내를 설정합니다.

모드:

요청

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"}

헤더:

본문:

엔드포인트 요구 사항:

  1. 다시 인코딩한 JSON이 아닌 원시 본문 서명을 검증합니다(예: hash_equals로 상수 시간 비교).
  2. X-RC-Zeit가 서버 시계와 300초 넘게 다른 요청을 거부합니다.
  3. 각 nonce는 한 번만 수락합니다(권장).
  4. 지정한 user_id의 데이터만 반환합니다.
  5. 공인 주소의 443 포트에서 https로 접근 가능하며 리디렉션과 주소의 사용자 이름·비밀번호가 없어야 합니다.
  6. 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" }
    }
  ]
}

주문별 필드:

제한: 최대 주문 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로 호출합니다. 빈 목록이나 샘플 주문을 반환합니다. 실패하면 다음 이유를 표시합니다.

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"}]}'

가져온 데이터는 암호화해 저장하며 고객 번호는 해시만 저장합니다. 마지막 가져오기 90일 후 삭제하므로 상태 변경 때마다 보내는 등 지속적으로 갱신합니다.

오류: 402 plan_required(유료 아님), 409 import_disabled(가져오기 불가 모드, kd_modus 포함), 400 bad_request와 param(user_id, project_id, bestellungen), 404(다른 사람의 프로젝트)입니다. 일반 오류 형식은 서버 API에 있습니다.

로그인하지 않은 방문자

확인된 신원 없이 주문을 질문하면 조회하지 않습니다. 대신 계정의 안내(언어별 텍스트, 선택적 로그인 링크)를 표시하거나 팀에 인계합니다.

다음: 웹훅 · 서버 API

최종 업데이트: