Geliştiriciler

Müşteri verisi API'si (e-Temsilci JSON biçimi)

Asistanın arayanın sipariş ve müşteri bilgisini sorduğu uç noktayı yazmak; istek, imza, cevap biçimi, durumlar ve PHP / Node.js örnekleri.

Bu sayfada
  1. Kurallar
  2. İstek
  3. İmza
  4. Süre, tekrar ve önbellek
  5. Cevap
  6. Doğrulama
  7. Durumlar
  8. Bulunamadı ve hatalar
  9. Örnek cevap
  10. PHP örneği (kendi veritabanından)
  11. Node.js örneği (bağımlılıksız, kendi API'nizden)

Altyapınız için hazır bir bağlantı yoksa (kendi yazılımınız, ERP, CRM, randevu sistemi, pazaryeri paneli…) bizim biçimimizde cevap veren küçük bir uç nokta yazarsınız. Her sorguda bu adrese imzalı bir POST gelir; 2 saniye içinde aşağıdaki biçimde cevap verirsiniz. Kurulum: Ayarlar → Entegrasyonlar → Müşteri verisi → Kendi sisteminiz (e-Temsilci JSON) (bkz. Müşteri verisi).

İstek zarfı ve imza, webhook olaylarımızla aynıdır: webhook için yazdığınız imza doğrulama kodu olduğu gibi çalışır.

Kurallar

  1. https:// ile herkese açık bir adres, geçerli sertifika, yönlendirme yok (3xx izlenmez, http:// kabul edilmez).
  2. İmzayı doğrulayın, 300 saniyeden eski istekleri reddedin.
  3. Telefonu son 10 haneyle (data.phone.nsn) karşılaştırın; o sütunda indeks olsun. 2 saniye içinde cevap verin (hedef 800 ms'nin altı).
  4. Yalnızca asistanın ihtiyacı olanı gönderin. Kendi durum kodlarınızı aşağıdaki değerlere eşleyin, kendi durum adınızı status_text'e yazın.
  5. Kayıt yoksa 200 {"version":"1","found":false}; sizin tarafınızda hata varsa 5xx.
  6. Telefonu ve gövdeyi loglamayın; gerekiyorsa maskeleyin (+90 5** *** ** 33).
  7. Güvenlik duvarınızda yalnızca sunucu IP adresimize izin verebilirsiniz: 188.245.28.244

İstek

POST {adresiniz}
Content-Type: application/json
User-Agent: arar-customer-data/1
X-Arar-Event: customer.lookup
X-Arar-Event-Id: 01k6r8m2v3d4e5f6g7h8j9k0aa
X-Arar-Timestamp: 1791025501
X-Arar-Signature: v1=<hex(HMAC-SHA256(anahtar, timestamp + "." + ham_gövde))>
{
  "version": "1",
  "id": "01k6r8m2v3d4e5f6g7h8j9k0aa",
  "event": "customer.lookup",
  "created_at": "2026-10-03T14:05:01+03:00",
  "data": {
    "phone": { "e164": "+905321112233", "national": "05321112233", "nsn": "5321112233", "digits": "905321112233" },
    "order_number": null,
    "max_orders": 5,
    "call": { "id": "01k6r8kz0c1b2a3d4e5f6g7h8j", "direction": "inbound" }
  }
}
  • phone: aynı numaranın dört yazımı. national ve nsn (başında 0 olmadan 10 hane) yalnızca Türkiye numaralarında doludur, diğerlerinde null.
  • order_number: arayan bir sipariş numarası söylediyse dolu, yoksa null. Doluysa o siparişi döndürün; sipariş arayanın numarasına ait değilse belongs_to_caller: false yazın (asistan o siparişin yalnızca durumunu ve kargo bilgisini görür).
  • max_orders: en fazla kaç sipariş istendiği. Fazlası kırpılır.
  • call: id yalnızca kendi loglarınızda eşleştirme içindir. Panelden yapılan Test et sorgusunda null gelir.
  • Gövdede başka kişisel veri yoktur. Telefon adreste değil gövdededir, böylece erişim loglarınıza düşmez.

İmza

X-Arar-Signature = v1= + hex(HMAC-SHA256(imza anahtarınız, X-Arar-Timestamp + "." + ham gövde)). Sabit zamanlı karşılaştırın. İmza anahtarı (whsec_…) kaynağı ilk kaydettiğinizde bir kez gösterilir; kaybettiyseniz kartta Anahtarı yenile'ye basın, eski anahtar hemen geçersiz olur.

API ağ geçidiniz sabit bir anahtar istiyorsa kartta Ek başlık adı ve Ek başlık değeri girin (ör. Authorization: Bearer … veya X-Api-Key: …); her istekte gönderilir ama imzanın yerine geçmez.

Uygulamanızı şu değerlerle sınayın:

anahtar   = whsec_test_0123456789abcdef
timestamp = 1791025501
gövde     = {"version":"1","id":"01k6r8m2v3d4e5f6g7h8j9k0aa","event":"customer.lookup"}
imza      = v1=0ee1d1d615df4e52fdb98c13bf443b883f0fa6ed866257c8313e1513fbfab945

Gövde tam olarak yukarıdaki baytlardır: boşluk ve satır sonu yok.

Süre, tekrar ve önbellek

  • Bağlantı 1 saniyede kurulmalı, cevap 2 saniyede gelmelidir. Çağrı cevaplanmadan önce en fazla 1,5 saniye beklenir; daha geç gelen cevap arka planda tamamlanır ve asistan onu arayan sorduğunda kullanır.
  • Çağrı öncesi sorgu tekrar edilmez. Arka plan tamamlaması bağlantı hatası, 408, 429 ve 5xx'te bir kez daha dener (Retry-After 2 saniyeyi geçmiyorsa ona uyar). Diğer 4xx yanıtları tekrar denenmez.
  • Aynı numara 120 saniye içinde tekrar sorulmaz. Cevaba cache_ttl_seconds (0–300) ekleyerek değiştirebilirsiniz; 0 = önbelleğe alma.

Cevap

200 OK, Content-Type: application/json, UTF-8, en fazla 64 KB. Tanımadığımız alanlar yok sayılır; eksik isteğe bağlı alanlar sorun değildir.

Alan Tür Açıklama
version "1" Zorunlu.
found bool Zorunlu. false → kayıt yok.
customer nesne veya null id (≤64, okunmaz), name (≤120, hitap edilecek ad soyad), first_name / last_name (≤60), email (≤254; asistana maskeli gider), segment (≤60: "VIP", "Kurumsal"), internal_note (≤500; asistanın bilmesi gereken iç not, müşteriye okunmaz)
orders[] liste (≤20; en yeni 5'i kullanılır) Aşağıda
facts[] liste (≤30; ilk 20'si kullanılır) {group?, label, value}: sipariş dışı her şey (randevu, bakiye, puan, servis kaydı, abonelik). group ≤40, label ≤60, value ≤300. Müşteriyle paylaşılabilir bilgilerdir.
cache_ttl_seconds tam sayı 0–300 İsteğe bağlı.

orders[] öğesi:

Alan Tür Açıklama
number metin ≤40 Zorunlu. Müşterinin bildiği sipariş numarası ("#" olmadan).
created_at RFC 3339 Zorunlu, saat dilimli (+03:00 veya Z).
status durum Zorunlu. Aşağıdaki sipariş durumlarından biri; bilinmeyen değer unknown sayılır.
status_text metin ≤80 Kendi panelinizdeki durum adı ("Tedarik ediliyor"). unknown'da asistan bunu kullanır.
total {amount, currency} amount noktalı ondalık metin, binlik ayırıcı yok ("1250.00"); currency ISO 4217 (TRY).
items[] ≤50 (5'i kullanılır) {name ≤120, quantity ≥1, variant? ≤60}
items_summary metin ≤200 items yerine tek cümle ("2 tişört, 1 şort").
payment nesne status (ödeme durumu), method (ödeme yöntemi)
shipments[] ≤10 (3'ü kullanılır) carrier (≤60, "Yurtiçi Kargo"), carrier_code? (≤30), tracking_number? (≤64), tracking_url? (https, ≤500; asistana gitmez), status (kargo durumu), status_text?, shipped_at?, estimated_delivery? (YYYY-MM-DD veya RFC 3339), delivered_at?, last_event? {at, description ≤160, location? ≤80}
return nesne veya null status (iade durumu), reason? ≤160, refund_amount? {amount, currency}, updated_at?
shipping_area nesne veya null Yalnızca {city, district}. Açık adres göndermeyin.
public_note metin ≤300 Müşteriye söylenebilecek açıklama ("Ürün tedarikçiden 7 Ekim'de gelecek").
belongs_to_caller bool Yalnızca order_number sorgusunda anlamlı; varsayılan true.

Göndermeyin: T.C. kimlik no, kart numarası, IBAN, açık adres, doğum tarihi, sağlık bilgisi (teşhis, tedavi), şifre veya anahtar. Biçimde bu alanlar yoktur; gelirse atılır.

Doğrulama

  • Katı: gövde geçerli bir JSON nesnesi, version "1" ve found true/false olmalıdır. Bunlar bozuksa sorgu başarısız sayılır.
  • Esnek: bozuk tek bir sipariş atılır (diğerleri kullanılır), bilinmeyen durum unknown olur, uzun metinler sınırda kesilir, kontrol karakterleri ve satır sonları temizlenir. Atılan alanlar Test et sonucunda listelenir.
  • Asistana giden bilgi toplamda 4000 karakteri geçmez: en yeni siparişler önce gelir, sığmayanlar yalnızca sayı olarak bildirilir.

Durumlar

Sipariş (status):

Değer Anlamı
awaiting_payment Ödeme bekleniyor (havale/EFT bekleniyor, ödeme tamamlanmadı)
received Sipariş alındı, henüz işleme alınmadı
processing Hazırlanıyor (onaylandı, paketleniyor, tedarik ediliyor)
ready_to_ship Kargoya verilmek üzere
partially_shipped Bir kısmı kargoya verildi
shipped Kargoya verildi
delivered Teslim edildi
cancelled İptal edildi
refunded İade edildi (ayrıntı return'de)
on_hold Beklemede (stok, adres, ödeme sorunu)
failed Tamamlanamadı
unknown Eşlenemeyen durum; status_text söylenir

Kargo (shipments[].status): label_created (kargo kaydı oluşturuldu), in_transit (yolda), at_branch (teslimat şubesinde), out_for_delivery (dağıtıma çıktı), delivered (teslim edildi), delivery_failed (teslim edilemedi), returning_to_sender (göndericiye geri dönüyor), unknown.

Ödeme (payment.status): pending, paid, partially_paid, refunded, partially_refunded, failed, unknown. Yöntem (payment.method): credit_card, bank_transfer, cash_on_delivery, wallet, other.

İade (return.status): requested, approved, in_transit, received, refunded, rejected, cancelled.

Bulunamadı ve hatalar

  • Kayıt yok: 200 {"version":"1","found":false}. 404 kullanmayın: 404 "adres yanlış" sayılır ve kartta hata olarak görünür.
  • Sizin tarafınızda hata: herhangi bir 5xx. Gövde isteğe bağlıdır: {"version":"1","error":{"code":"…","message":"…"}}; message en fazla 200 karakter olsun ve kişisel veri içermesin (rakam dizileri maskelenir). Asistan "şu an ulaşamıyorum" der, sorgu arka planda bir kez daha denenir.
  • İmza geçersizse 401 dönün; tekrar denenmez, kartta "istek reddedildi" görünür.
  • Hız sınırı: 429 ve Retry-After.

Örnek cevap

{
  "version": "1",
  "found": true,
  "customer": { "id": "C-88412", "name": "Ayşe Yılmaz", "email": "[email protected]", "segment": "VIP" },
  "orders": [
    {
      "number": "100245",
      "created_at": "2026-09-30T14:12:00+03:00",
      "status": "shipped",
      "status_text": "Kargoda",
      "total": { "amount": "1249.90", "currency": "TRY" },
      "items": [ { "name": "Kablosuz kulaklık", "quantity": 1, "variant": "Siyah" } ],
      "payment": { "status": "paid", "method": "credit_card" },
      "shipments": [
        {
          "carrier": "Yurtiçi Kargo",
          "tracking_number": "301234567890",
          "status": "out_for_delivery",
          "estimated_delivery": "2026-10-03",
          "last_event": { "at": "2026-10-03T09:41:00+03:00", "description": "Kurye dağıtıma çıkardı", "location": "Kadıköy" }
        }
      ],
      "shipping_area": { "city": "İstanbul", "district": "Kadıköy" }
    }
  ],
  "facts": [ { "group": "Hesap", "label": "Puan bakiyesi", "value": "340 puan" } ]
}

Sipariş yerine randevu, bakiye veya servis kaydı tutan firmalar orders göndermeden yalnızca facts kullanabilir:

{
  "version": "1",
  "found": true,
  "customer": { "name": "Mehmet Demir" },
  "facts": [
    { "group": "Randevular", "label": "Sonraki randevu", "value": "8 Ekim 2026 Perşembe 14:30, Kadıköy şubesi" },
    { "group": "Hesap", "label": "Ödenmemiş bakiye", "value": "750,00 TL" }
  ],
  "cache_ttl_seconds": 60
}

PHP örneği (kendi veritabanından)

<?php
// musteri-verisi.php — e-Temsilci müşteri verisi uç noktası (biçim v1)
declare(strict_types=1);

header('Content-Type: application/json; charset=utf-8');

$secret = getenv('ETEMSILCI_SECRET');            // panelde bir kez gösterilen whsec_… anahtarı
$body = file_get_contents('php://input') ?: '';
$timestamp = (int) ($_SERVER['HTTP_X_ARAR_TIMESTAMP'] ?? 0);
$expected = 'v1=' . hash_hmac('sha256', $timestamp . '.' . $body, $secret);

if (! hash_equals($expected, $_SERVER['HTTP_X_ARAR_SIGNATURE'] ?? '') || abs(time() - $timestamp) > 300) {
    http_response_code(401);
    exit;
}

$request = json_decode($body, true, 16, JSON_THROW_ON_ERROR);
$nsn = (string) ($request['data']['phone']['nsn'] ?? '');          // 5321112233
$orderNumber = $request['data']['order_number'] ?? null;

// Kendi durum kodlarınız → e-Temsilci durumları
$statusMap = ['beklemede' => 'received', 'hazirlaniyor' => 'processing', 'kargoda' => 'shipped',
              'teslim' => 'delivered', 'iptal' => 'cancelled', 'iade' => 'refunded'];

try {
    $pdo = new PDO(getenv('DB_DSN'), getenv('DB_USER'), getenv('DB_PASS'), [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);

    // phone_nsn: numaranın son 10 hanesini tuttuğunuz, indeksli bir sütun
    $sql = 'SELECT o.order_no, o.created_at, o.status, o.total, o.cargo_company, o.cargo_tracking_no,
                   o.phone_nsn, c.full_name
              FROM orders o JOIN customers c ON c.id = o.customer_id
             WHERE ' . ($orderNumber !== null ? 'o.order_no = :order_no' : 'o.phone_nsn = :nsn') . '
             ORDER BY o.created_at DESC LIMIT 5';
    $stmt = $pdo->prepare($sql);
    $stmt->execute($orderNumber !== null ? ['order_no' => $orderNumber] : ['nsn' => $nsn]);
    $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
} catch (Throwable) {
    http_response_code(503);                            // asistan "şu an ulaşamıyorum" der
    echo json_encode(['version' => '1', 'error' => ['code' => 'db_unavailable', 'message' => 'Veritabanına ulaşılamadı']]);
    exit;
}

if ($rows === []) {
    echo json_encode(['version' => '1', 'found' => false]);
    exit;
}

$orders = array_map(fn (array $r): array => array_filter([
    'number' => $r['order_no'],
    'created_at' => (new DateTimeImmutable($r['created_at'], new DateTimeZone('Europe/Istanbul')))->format(DATE_RFC3339),
    'status' => $statusMap[$r['status']] ?? 'unknown',
    'status_text' => $r['status'],
    'total' => ['amount' => number_format((float) $r['total'], 2, '.', ''), 'currency' => 'TRY'],
    'shipments' => $r['cargo_company'] ? [[
        'carrier' => $r['cargo_company'],
        'tracking_number' => $r['cargo_tracking_no'],
        'status' => $r['status'] === 'teslim' ? 'delivered' : 'in_transit',
    ]] : null,
    'belongs_to_caller' => $r['phone_nsn'] === $nsn,
], fn ($v) => $v !== null), $rows);

$isCaller = $rows[0]['phone_nsn'] === $nsn;

echo json_encode([
    'version' => '1',
    'found' => true,
    'customer' => $isCaller ? ['name' => $rows[0]['full_name']] : null,
    'orders' => $orders,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

Node.js örneği (bağımlılıksız, kendi API'nizden)

// musteri-verisi.mjs — Node 20+, e-Temsilci müşteri verisi uç noktası (biçim v1)
import { createServer } from 'node:http';
import { createHmac, timingSafeEqual } from 'node:crypto';

const SECRET = process.env.ETEMSILCI_SECRET; // whsec_…
const STATUS = { NEW: 'received', PICKING: 'processing', SHIPPED: 'shipped', DELIVERED: 'delivered', CANCELED: 'cancelled' };

function validSignature(req, raw) {
  const ts = Number(req.headers['x-arar-timestamp'] ?? 0);
  const given = Buffer.from(String(req.headers['x-arar-signature'] ?? ''));
  const expected = Buffer.from('v1=' + createHmac('sha256', SECRET).update(`${ts}.${raw}`).digest('hex'));
  return Math.abs(Date.now() / 1000 - ts) <= 300 && given.length === expected.length && timingSafeEqual(given, expected);
}

// Kendi sisteminizden okuyun (örnek: iç API'niz). 1,5 sn'de kesin.
async function findOrders(nsn, orderNumber) {
  const url = new URL('https://ic-api.ornek.com.tr/orders');
  url.searchParams.set(orderNumber ? 'number' : 'phone', orderNumber ?? nsn);
  const res = await fetch(url, { headers: { authorization: `Bearer ${process.env.INTERNAL_TOKEN}` }, signal: AbortSignal.timeout(1500) });
  if (!res.ok) throw new Error(`internal api ${res.status}`);
  return res.json(); // [{ no, createdAt, state, total, phone, customerName, cargo: { company, trackingNo, eta } }]
}

createServer(async (req, res) => {
  const chunks = [];
  for await (const chunk of req) chunks.push(chunk);
  const raw = Buffer.concat(chunks).toString('utf8');
  const send = (status, body) => { res.writeHead(status, { 'content-type': 'application/json; charset=utf-8' }); res.end(JSON.stringify(body)); };

  if (req.method !== 'POST' || !validSignature(req, raw)) return send(401, { version: '1', error: { code: 'unauthorized' } });

  try {
    const { data } = JSON.parse(raw);
    if (!data.phone.nsn && !data.order_number) return send(200, { version: '1', found: false }); // yurt dışı numara
    const rows = (await findOrders(data.phone.nsn, data.order_number)).slice(0, data.max_orders ?? 5);
    if (rows.length === 0) return send(200, { version: '1', found: false });

    const mine = (row) => String(row.phone).replace(/\D/g, '').slice(-10) === data.phone.nsn;
    send(200, {
      version: '1',
      found: true,
      customer: mine(rows[0]) ? { name: rows[0].customerName } : null,
      orders: rows.map((o) => ({
        number: String(o.no),
        created_at: new Date(o.createdAt).toISOString(),
        status: STATUS[o.state] ?? 'unknown',
        status_text: o.state,
        total: { amount: Number(o.total).toFixed(2), currency: 'TRY' },
        shipments: o.cargo ? [{ carrier: o.cargo.company, tracking_number: o.cargo.trackingNo,
                                status: o.state === 'DELIVERED' ? 'delivered' : 'in_transit',
                                estimated_delivery: o.cargo.eta ?? undefined }] : [],
        belongs_to_caller: mine(o),
      })),
    });
  } catch {
    send(503, { version: '1', error: { code: 'upstream_unavailable' } });
  }
}).listen(process.env.PORT ?? 8080);

Node örneği TLS'yi önündeki ters vekile (nginx, Caddy, bulut yük dengeleyici) bırakır; doğrudan internete açılacaksa node:https ve geçerli bir sertifika gerekir. Kod içinde telefon loglanmaz.