DEVekspertiz.app

09 · Test hattı cihaz entegrasyonu

Bu rehber, test hattı cihazı üreticilerinin cihaz ölçümlerini ekspertiz.app'e göndermesi için uçtan uca entegrasyon adımlarını anlatır. Markadan bağımsızdır — her üretici aynı endpoint'i kullanır (örneklerdeki Mates, entegre olan ilk üreticidir). Endpoint referansı: test-line-measurements.md

Genel akış

┌─────────────┐  ölçüm    ┌──────────────────┐  HTTPS POST   ┌─────────────────┐
│ Test hattı  │ ────────► │ Üretici yazılımı │ ────────────► │  ekspertiz.app  │
│ cihazı      │           │ (istasyon PC     │  HMAC imzalı  │  Partner API    │
│ (fren/süsp.)│           │  yazılımı)       │               └────────┬────────┘
└─────────────┘           └──────────────────┘                        │ bayi kodu + plaka
                                                                      ▼
                                                       ┌──────────────────────────┐
                                                       │ Ekspertiz raporu          │
                                                       │ "Test Hattı Kontrolleri"  │
                                                       │ formu — değerler otomatik │
                                                       └──────────────────────────┘
  1. Araç test hattından geçer, operatör plakayı cihaz yazılımına girer (veya plaka tanıma sisteminden gelir).
  2. Üretici yazılımı ölçüm sonuçlarını tek bir JSON paketi olarak POST /api/v1/partner/test-line/measurements'a gönderir. Paketdeki branch_code (bayi kodu) ölçümün hangi ekspertiz bayisine ait olduğunu söyler — kurulumda istasyon yazılımına bir kez yazılır.
  3. ekspertiz.app bayi kodundan firmayı + bayiyi bulur, plakaya açık rapor varsa değerleri anında rapora işler; yoksa havuzda bekletir ve eksper rapor açtığında tek tıkla uygular.

Sıralama serbesttir. Ölçüm rapordan önce de sonra da gönderilebilir. Rapor açıkken gelirse otomatik işlenir; önce gelirse eksper ekranında "Cihazdan ölçüm geldi" bandı olarak görünür.

Devreye alma (onboarding)

AdımKimNe
1ekspertiz.appPartner hesabı + API anahtarı üretir (önce tmk_test_…, canlıya geçişte tmk_live_…). Secret yalnız bir kez gösterilir
2ekspertiz.appPartner'ın hangi ekspertiz firmalarına veri gönderebileceğini tanımlar (erişim kapsamı). Kapsam dışı firmalara gönderim 404 döner
3ekspertiz.appCihaz kurulacak bayilerin bayi kodlarını paylaşır (örn. YVZ-SB-01). Kodlar GET /organizations/{orgId}/branches ile API'den de çekilebilir
4ÜreticiHer istasyon kurulumunda yazılıma o bayinin kodunu yazar; anahtarları güvenli saklar (üretici başına tek anahtar yeterlidir, cihaz başına değil)
5ÜreticiTest ortamında örnek gönderimleri çalıştırır (aşağıdaki senaryolar)
6BirlikteCanlı anahtara geçiş + ilk sahada doğrulama

Yanlış/erişim dışı bayi koduyla gönderim 404 not_found döner — yeni bayi eklenirken kodu bizden teyit edin.

Kimlik doğrulama — özet

Tüm istekler HMAC-SHA256 ile imzalanır. Ayrıntı: 01 · Kimlik doğrulama · örnek kodlar: 02 · İmza örnekleri · hazır script'ler: sign.node.mjs · sign.php · sign.py

Dört header:

X-Trameks-Key-Id:    tmk_test_XXXXXXXXXXXXXXXXXXXXXX
X-Trameks-Timestamp: 1781090246              # unix saniye (UTC), ±5 dk tolerans
X-Trameks-Nonce:     9f8a3b...               # istek başına benzersiz, ≥16 karakter
X-Trameks-Signature: sha256=ab12cd34...      # kanonik string'in HMAC-SHA256'sı

Kanonik string (satırlar \n ile ayrılır):

POST
/api/v1/partner/test-line/measurements
                                  ← query yok: boş satır
1781090246
<nonce>
<hex SHA-256(gövde bayt'ları)>

Önemli: İmza, gövdenin gönderilen bayt'ları üzerinden hesaplanır. İmzayı hesapladıktan sonra JSON'u yeniden serialize etmeyin — aynı string'i gönderin.

Node.js (bağımlılıksız, test edilmiş örnek)

import { createHash, createHmac, randomBytes } from "node:crypto";

const KEY_ID = process.env.TMK_KEY_ID;
const SECRET = process.env.TMK_SECRET;
const BASE = "https://api.ekspertiz.app";
const PATH = "/api/v1/partner/test-line/measurements";

const body = JSON.stringify({
  branch_code: "YVZ-SB-01",
  plate: "06 ABC 123",
  measured_at: new Date().toISOString(),
  external_id: "MATES-OLCUM-90002",
  measurements: { BRAKE_FRONT_LEFT: 2.4, BRAKE_FRONT_RIGHT: 2.6 },
});

const ts = String(Math.floor(Date.now() / 1000));
const nonce = randomBytes(16).toString("hex");
const bodyHash = createHash("sha256").update(body, "utf8").digest("hex");
const canonical = ["POST", PATH, "", ts, nonce, bodyHash].join("\n");
const signature = "sha256=" + createHmac("sha256", SECRET).update(canonical).digest("hex");

const res = await fetch(BASE + PATH, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Trameks-Key-Id": KEY_ID,
    "X-Trameks-Timestamp": ts,
    "X-Trameks-Nonce": nonce,
    "X-Trameks-Signature": signature,
  },
  body,
});
console.log(res.status, await res.json());

C# (.NET 6+)

using System.Security.Cryptography;
using System.Text;

var keyId  = Environment.GetEnvironmentVariable("TMK_KEY_ID");
var secret = Environment.GetEnvironmentVariable("TMK_SECRET");
var path   = "/api/v1/partner/test-line/measurements";

var body = """
{"branch_code":"YVZ-SB-01","plate":"06 ABC 123","external_id":"MATES-OLCUM-90002","measurements":{"BRAKE_FRONT_LEFT":2.4,"BRAKE_FRONT_RIGHT":2.6}}
""";

var ts    = DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
var nonce = Convert.ToHexString(RandomNumberGenerator.GetBytes(16)).ToLower();

var bodyHash  = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(body))).ToLower();
var canonical = string.Join("\n", "POST", path, "", ts, nonce, bodyHash);
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret!));
var signature = "sha256=" + Convert.ToHexString(hmac.ComputeHash(Encoding.UTF8.GetBytes(canonical))).ToLower();

using var http = new HttpClient { BaseAddress = new Uri("https://api.ekspertiz.app") };
var req = new HttpRequestMessage(HttpMethod.Post, path)
{
    Content = new StringContent(body, Encoding.UTF8, "application/json"),
};
req.Headers.Add("X-Trameks-Key-Id", keyId);
req.Headers.Add("X-Trameks-Timestamp", ts);
req.Headers.Add("X-Trameks-Nonce", nonce);
req.Headers.Add("X-Trameks-Signature", signature);

var res = await http.SendAsync(req);
Console.WriteLine($"{(int)res.StatusCode} {await res.Content.ReadAsStringAsync()}");

PHP ve Python örnekleri için: 02 · İmza örnekleri.

İstemci tarafı öneriler

  • external_id her zaman gönderin. Ağ kopması / timeout durumunda aynı external_id ile güvenle tekrar deneyin — sunucu çift kayıt açmaz, mevcut sonucu döner (idempotent_replay: true).
  • Bayi kodunu konfigürasyonda tutun. İstasyon yazılımına kurulumda yazılır, ölçüm başına değişmez. Koda elle her seferinde girilmesi gereken bir alan muamelesi yapmayın — yazım hatası yanlış bayiye veri akıtmaz (erişim dışıysa 404 alırsınız) ama akışı durdurur.
  • Yerel kuyruk tutun. İnternet kesintisinde ölçümleri diske kuyruğa yazıp bağlantı dönünce sırayla gönderin. measured_at ölçümün gerçek zamanı olduğundan geç gönderim sorun yaratmaz.
  • Tekrar denemede yeni nonce + yeni timestamp + yeni imza üretin (nonce tek kullanımlıktır; aynısını göndermek replay_detected üretir). external_id ise sabit kalır.
  • Saat senkronu: istasyon PC'sinin saati NTP ile senkron olsun — ±5 dakikadan fazla sapma expired_timestamp üretir.
  • 429 yanıtında Retry-After süresine uyun (varsayılan limit dk'da 60 istek — normal saha kullanımında erişilmesi pek mümkün değil).

Test senaryoları (kabul kriterleri)

#SenaryoBeklenen
1Açık rapora sahip plakaya ölçüm201 + status: "applied" + applied_points dolu
2Aynı external_id ile tekrar gönderim200 + idempotent_replay: true, çift kayıt yok
3Raporu olmayan plakaya ölçüm201 + status: "pending" — eksper rapor açınca bant görünür
4Bilinmeyen / erişim dışı bayi kodu404 not_found
5Hatalı imza401 invalid_signature
6Geçersiz plaka ("ISTANBUL")400 invalid_request

Tüm senaryolar ekspertiz.app tarafında entegrasyon testlerinden geçirilmiştir. Canlıya geçiş öncesi 1, 2 ve 3'ü kendi yazılımınızdan koşmanızı rica ediyoruz.