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
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
https://ile herkese açık bir adres, geçerli sertifika, yönlendirme yok (3xx izlenmez,http://kabul edilmez).- İmzayı doğrulayın, 300 saniyeden eski istekleri reddedin.
- 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ı). - 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. - Kayıt yoksa
200 {"version":"1","found":false}; sizin tarafınızda hata varsa 5xx. - Telefonu ve gövdeyi loglamayın; gerekiyorsa maskeleyin (
+90 5** *** ** 33). - 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ı.nationalvensn(başında 0 olmadan 10 hane) yalnızca Türkiye numaralarında doludur, diğerlerindenull.order_number: arayan bir sipariş numarası söylediyse dolu, yoksanull. Doluysa o siparişi döndürün; sipariş arayanın numarasına ait değilsebelongs_to_caller: falseyazı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:idyalnızca kendi loglarınızda eşleştirme içindir. Panelden yapılan Test et sorgusundanullgelir.- 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-After2 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"vefoundtrue/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
unknownolur, 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":"…"}};messageen 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
401dönün; tekrar denenmez, kartta "istek reddedildi" görünür. - Hız sınırı:
429veRetry-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.