Abone olduğun event'ler, imzalı POST olarak webhook endpoint'ine teslim edilir (asenkron, fire-and-forget). İmzayı doğrula, 2xx dön ve işle. 2xx dönmezsen Restomenum yeniden dener (retry).
Bir event oluşur (örn. packet.created)
│
▼
Restomenum ──imzalı POST──► https://<senin-origin>/webhook
│ header: X-Restomenum-Signature: t=..,v1=..
│ body: { id, type, version, environment, tenantId, occurredAt, data }
▼
Senin /webhook:
1) imzayı doğrula (raw body) → 401 değilse
2) 200 dön (hızlıca)
3) işle (idempotent — id ile tekrarı yut)Her teslimde şu header'lar gelir:
| Alan | Tip | Zorunlu | Açıklama |
|---|---|---|---|
| X-Restomenum-Signature | string | ✓ | t=<unixSec>,v1=<hex> — HMAC imzası. Her zaman doğrula (ham gövde). |
| X-Restomenum-Event | string | ✓ | Event tipi (örn. packet.created) — gövdedeki type ile aynı. |
| X-Restomenum-Delivery | string | ✓ | Teslim başına bir debug id (log/destek için). Idempotency anahtarı DEĞİLDİR — dedup için gövdedeki id'yi kullan. |
| X-Restomenum-Environment | "sandbox" | "production" | ✓ | Gövdedeki environment'ın kolaylık kopyası — değeri gövdeden okunur, ikisi ayrışamaz. Header imzaya dahil değildir ve gövdeyi kuyruğa atıp sonra işleyen tasarımlarda kaybolur → gövdedeki alanı kullan. |
Gövde tüm event'lerde aynı zarftır; event'e özel veri yalnız data içinde:
| Alan | Tip | Zorunlu | Açıklama |
|---|---|---|---|
| id | string | ✓ | Idempotency anahtarı. Çağıran verdiyse o, yoksa evt_<uuid>. Retry'da aynı id tekrar gelebilir — bir kez işle. |
| type | string | ✓ | Event tipi — X-Restomenum-Event ile aynı. |
| version | string | ✓ | Zarf şema sürümü — string "1" (sayı değil). Kurulum başına sabitlenir; versiyonlama. |
| environment | "sandbox" | "production" | ✓ | Teslimin ortamı — imzalı gövdede (test/dev mağazası = "sandbox", gerçek mağaza = "production"). Tüm imzalı gövdelerde bulunur: event webhook'ları, lifecycle, hook/action/capability. |
| tenantId | string | ✓ | Tenant kimliği; kurulumla ve event'leri bununla eşle. |
| occurredAt | number | ✓ | Event anı — epoch milisaniye. (Dikkat: imzadaki t= unix saniye — farklı birim.) |
| data | object | ✓ | Event'e özel gövde — event kataloğu. |
// /webhook — Restomenum event alıcısı (Node + Express)
// Ham gövde ŞART: imza ham byte'lar üzerinden doğrulanır (JSON.parse'tan ÖNCE).
import express from 'express';
import crypto from 'node:crypto';
const app = express();
const WEBHOOK_SECRET = process.env.RESTOMENUM_WEBHOOK_SECRET; // /connect exchange'inden geldi
// Bu route'ta JSON parser DEĞİL, raw body kullan:
app.post('/webhook', express.raw({ type: '*/*' }), (req, res) => {
const sig = req.header('X-Restomenum-Signature') || ''; // "t=1730..,v1=<hex>"
const parts = Object.fromEntries(sig.split(',').map((p) => p.split('=')));
const t = parts.t, v1 = parts.v1;
// 1) replay koruması: timestamp 5 dk toleransı
if (!t || Math.abs(Date.now() / 1000 - Number(t)) > 300) return res.sendStatus(401);
// 2) imza: HMAC_SHA256(secret, "<t>.<rawBody>")
const expected = crypto.createHmac('sha256', WEBHOOK_SECRET)
.update(`${t}.${req.body.toString('utf8')}`)
.digest('hex');
const ok = v1 && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
if (!ok) return res.sendStatus(401);
// 3) artık güvenli: parse et ve işle
const event = JSON.parse(req.body.toString('utf8'));
// event.type, event.data, event.tenantId ...
res.sendStatus(200); // 2xx dönmezsen Restomenum retry eder
});id retry'da tekrar gelebilir — bir kez işle.data için event kataloğu.app.installed / subscription.* / app.uninstalled da alır — type ile dallan (Lifecycle Webhook’ları).events:subscribe scope'u ve events[] listesi gerekir. Geliştirme sırasında portaldaki “Test Event” aracıyla webhook'una sandbox payload gönderebilirsin.