Webhook’lar
Bu sayfa Open Play’in oyun olaylarını kendi sunucunuza HTTP POST ile nasıl ilettiğini anlatır: uç nokta kaydı, olay türleri, imza doğrulaması, yeniden deneme ve tekrar eden teslimatla başa çıkma. Karakter, mağaza ya da ödül olayında kendi arka uç sisteminizi tetiklemek istiyorsanız okuyun.
Ne işe yarar
Open Play bir olay oluştuğunda (ör. level_up, store_purchase_completed, reward_granted) kayıtlı uç noktalarınıza imzalı bir POST gönderir. Teslimat en az bir kez (at-least-once) yapılır: aynı olay ağ hataları yüzünden birden çok gelebilir, bu yüzden alıcınız tekrarı X-Crx-Event-Id ile elemelidir.
Modül kapalıysa uçlar 404 ve {"code":"module_disabled","module":"webhooks"} döner. Modülü Oyun ayarları › Modüller sayfasından açın.
Panelde nerede
| Ne yapacaksınız | Panel sayfası |
|---|---|
| Kayıtlı uç noktaları, teslimat denemelerini, başarısız (DLQ) teslimatları ve olay kuyruğunu görmek | Webhook ve olay kuyruğu |
| Modülü açmak | Oyun ayarları › Modüller |
Panel bu sayfada salt okunurdur: uç nokta kaydı REST ile yapılır; panelde yeniden gönderme ya da başarısız teslimatı kurtarma düğmesi yoktur. Panel sırları, ham gövdeyi ve ham hata metnini göstermez.
Veri modeli (düz dille)
- Uç nokta (endpoint):
url, isteğe bağlıevent_typessüzgeci,enabledvecreated_at. - Sır (secret): Sunucu üretir (
whsec_+ 64 küçük harf hex), yalnız kayıt yanıtında bir kez döner ve şifreli saklanır. Sonra hiçbir uç onu vermez. Sır değiştirme ucu yoktur: yenilemek için uç noktayı silip yeniden kaydedin. - Olay zarfı: Gönderilen JSON gövdesi. Her zarfta
event_id,type,environment_idvedatabulunur. - Teslimat (delivery): Bir olayın bir uç noktaya gönderimi. Durumları bekleyen, teslim edilen ve başarısız (DLQ).
Seçilebilir olay türleri
event_types boşsa uç nokta bütün olayları alır (aşağıdaki not). Doluysa yalnız şu türlerden seçebilirsiniz:
| Alan | Türler |
|---|---|
| Karakter | flag_set, attribute_set, resource_changed, modifier_added, modifiers_removed, xp_added, level_up, ability_unlocked, event_emitted, character_rolled_back |
| Mağaza | store_page_created, store_page_updated, store_page_deleted, store_pages_reordered, store_listing_created, store_listing_updated, store_listing_deleted, store_listings_reordered, store_purchase_initiated, store_payment_started, store_purchase_completed, store_reservation_cancelled |
| Ödül | reward_granted |
Bilinmeyen bir tür 400 döner. Tam liste ve veri alanları: Webhook olayları.
Uyarı
Süzgeci boş bırakırsanız uç nokta yukarıdaki listeyle sınırlı kalmaz: outbox’a yazılan bütün olayları (eşya, cüzdan, klan, parti, skor, bildirim kuyruğu gibi) alır. Bu ek olayların zarf biçimi belgelenmiş bir sözleşme değildir ve haber verilmeden değişebilir. Üretimde
event_typessüzgecini her zaman açıkça verin.
Zarf biçimi
Karakter olayları:
{
"event_id": "0192f500-1111-7222-8333-944455556666",
"type": "level_up",
"character_id": "0192f0f2-5b1a-7c3e-9d11-4a5b6c7d8e9f",
"environment_id": "0192f000-0000-7000-8000-00000000e001",
"seq": 12,
"data": { "...": "olaya özgü alanlar" }
}
Mağaza olaylarında character_id yoktur; seq ile birlikte now_ms (olayın işlendiği an) ve actor_api_key_id bulunur. reward_granted olayında player_id ve data içinde source_kind, table_key, table_version ile verilenler yer alır; çekiliş tohumu hiçbir olayda yoktur.
Oyunun yaptığı çağrılar
Bütün yönetim uçları sunucu anahtarıyla çağrılır; istemci anahtarı 403 alır:
Authorization: Bearer crx_live_<önek>_<gizli>
| Yöntem ve yol | Ne yapar |
|---|---|
POST /v1/webhooks | Uç nokta kaydeder (201; sır yalnız burada) |
GET /v1/webhooks | Uç noktaları listeler (cursor, limit 1–200, varsayılan 50) |
GET /v1/webhooks/{id} | Tek uç noktayı okur (sırsız) |
DELETE /v1/webhooks/{id} | Uç noktayı siler (yumuşak silme, 204) |
TypeScript SDK’sı bu dört ucu tipli sunar: createWebhook, listWebhooks, getWebhook, deleteWebhook (bkz. TypeScript SDK).
Uç nokta kaydetmek
POST /v1/webhooks
Authorization: Bearer crx_live_<önek>_<gizli>
Content-Type: application/json
{
"url": "https://hooks.ornek-oyun.com/open-play",
"event_types": ["level_up", "store_purchase_completed"]
}
{
"id": "0192f600-…",
"url": "https://hooks.ornek-oyun.com/open-play",
"event_types": ["level_up", "store_purchase_completed"],
"enabled": true,
"created_at": "2026-11-03T09:00:00Z",
"secret": "whsec_<64 hex karakter>"
}
secret değerini hemen güvenli bir yere (gizli yönetim servisine) yazın; bir daha okunamaz.
Kayıt kuralları:
- URL
httpsolmalı; kullanıcı bilgisi (user:parola@) taşımamalı; harf dışı kontrol karakteri ve boşluk içermemeli. Loopback, özel ve link-local IP adresleri kayıtta reddedilir (400). - Ayrıca her gönderimde adın çözümlendiği IP yeniden denetlenir; özel ya da loopback adrese çözümlenen ad gönderilemez. Yönlendirme (redirect) takip edilmez.
- Aynı URL aktifken ikinci kayıt
409.
Teslimat
Her olay için uç noktanıza şu istek gelir:
POST /open-play
Content-Type: application/json
X-Crx-Signature: t=1790000000,v1=9c1f…e2
X-Crx-Event-Id: 0192f500-1111-7222-8333-944455556666
X-Crx-Event-Type: level_up
{ …zarf… }
| Başlık | Anlamı |
|---|---|
X-Crx-Signature | t=<unix saniye>,v1=<hex HMAC-SHA256> |
X-Crx-Event-Id | Olay kimliği; tekrar eleme anahtarınız |
X-Crx-Event-Type | Zarftaki type değeri |
İmzayı doğrulamak
İmza, "{t}.{ham gövde}" metninin, kayıtta dönen sırrın tamamı (whsec_… önekiyle) anahtar alınarak hesaplanan HMAC-SHA256 özetidir (küçük harf hex). Doğrulama adımları:
X-Crx-Signaturebaşlığındantvev1değerlerini ayırın.- Ham gövde baytlarını alın. JSON’u ayrıştırıp yeniden serileştirmeyin; imza tel üzerindeki birebir baytlar içindir.
HMAC-SHA256(secret, t + "." + ham_gövde)hesaplayın vev1ile sabit zamanlı karşılaştırın.tdeğerini kendi saatinizle karşılaştırın; çok eski (örneğin 5 dakikadan eski) istekleri reddedin. Her yeniden denemedetyenilenir, bu yüzden gerçek bir yeniden deneme bu pencerede kalır.X-Crx-Event-Iddaha önce işlendiyse işlemi tekrarlamadan2xxdönün.
Yanıt ve yeniden deneme kuralları
- Yalnız
2xxbaşarıdır. Başka her yanıt, zaman aşımı ve bağlantı hatası yeniden denemeyi tetikler. - Bağlantı için 5 saniye, DNS dahil toplam 10 saniye beklenir.
- Gövde 64 KB’ı aşarsa gönderilmez ve hata sayılır.
- Gecikme
min(1 sn × 2^deneme, 5 dk)’dır ve ±%20 rastgelelik içerir. En çok 10 deneme yapılır; 10. başarısız denemeden sonra teslimat başarısız (DLQ) durumuna alınır ve otomatik tekrar edilmez. - Uç noktayı sildiğinizde bekleyen teslimatlar da başarısız duruma alınır.
- Sıra garantisi yoktur: olaylar karakter için
seqile sıralanabilir, ama teslimat sırası değişebilir. - Uç nokta yokken oluşan olaylar sonradan gönderilmez; geriye dönük teslimat yoktur.
Sürümleme ve tekrar güvenliği
- Alıcı tarafı:
X-Crx-Event-Idile tekrar eleme zorunludur. Aynı olay yeniden denemede aynıevent_idile gelir; imza zamanı (t) yenilenir. - Kayıt ucunda
Idempotency-Keyyoktur; aynı URL’yi ikinci kez kaydetmek409döner. - Zarfta bir olay sürümü alanı yoktur; tanımadığınız alanları yok sayın ve gövdeyi sıkı bir şemayla reddetmeyin.
Sınırlar
- Gövde 64 KB; toplam bekleme 10 saniye; 10 deneme.
- Liste ucunda
limit1–200. - Redirect takip edilmez; yalnız
https.
Sık görülen hatalar
| Belirti | Neden | Çözüm |
|---|---|---|
Kayıtta 400 | URL https değil, IP adresi, kullanıcı bilgisi ya da bilinmeyen olay türü | URL ve event_types değerlerini düzeltin |
Kayıtta 403 | İstemci anahtarı | Sunucu anahtarı kullanın |
Kayıtta 409 | Aynı URL zaten aktif | Var olanı silin ya da kullanın |
| İmza doğrulanmıyor | Gövde yeniden serileştirildi; sırrın whsec_ öneki atıldı; saat farkı | Ham baytlarla, sırrın tamamıyla hesaplayın |
| Aynı olay iki kez geliyor | At-least-once teslimat | X-Crx-Event-Id ile eleyin |
| Teslimatlar durdu | 10 deneme doldu (DLQ) ya da uç nokta silindi | Alıcıyı düzeltin; kaçan olaylar için kendi kaynağınızdan okuyun |
| Teslimat hiç gelmiyor | Uç adı özel/loopback IP’ye çözümleniyor ya da yanıt 10 saniyeyi aşıyor | Genel bir adres kullanın; yanıtı hızlı verin |
Çalışan örnek: Node ile alıcı
Aşağıdaki sunucu imzayı doğrular, tekrar eden olayı eler ve 2xx döner. Sır ortam değişkeninden gelir; üretimde kalıcı bir depoyla tekrar elemesi yapın.
/** Webhook alıcısı (Node 22): imzayı doğrular, tekrar eden teslimatı `X-Crx-Event-Id` ile eler. */
import { createHmac, timingSafeEqual } from "node:crypto";
import { createServer } from "node:http";
const SECRET = process.env["OPEN_PLAY_WEBHOOK_SECRET"] ?? ""; // uç noktayı kaydederken bir kez dönen sır
const TOLERANCE_SECONDS = 300; // eski teslimatı (replay) reddetme penceresi: alıcının kendi politikası
const seen = new Set<string>(); // örnek amaçlı bellek; üretimde kalıcı depo kullanın
/** `X-Crx-Signature: t=<unix-sn>,v1=<hex>` değerini ham gövdeye karşı doğrular. */
export function verifySignature(secret: string, header: string, rawBody: Buffer, nowSeconds: number): boolean {
const parts = new Map(header.split(",").map((part) => part.split("=", 2) as [string, string]));
const t = parts.get("t");
const v1 = parts.get("v1");
if (t === undefined || v1 === undefined) return false;
if (Math.abs(nowSeconds - Number(t)) > TOLERANCE_SECONDS) return false;
const expected = createHmac("sha256", secret).update(`${t}.`).update(rawBody).digest("hex");
const a = Buffer.from(expected, "utf8");
const b = Buffer.from(v1, "utf8");
return a.length === b.length && timingSafeEqual(a, b);
}
createServer((req, res) => {
const chunks: Buffer[] = [];
req.on("data", (chunk: Buffer) => chunks.push(chunk));
req.on("end", () => {
const rawBody = Buffer.concat(chunks); // imza ham bayt üzerindendir: JSON'u yeniden serileştirmeyin
const signature = req.headers["x-crx-signature"];
if (typeof signature !== "string" || !verifySignature(SECRET, signature, rawBody, Math.floor(Date.now() / 1000))) {
res.writeHead(401).end();
return;
}
const eventId = String(req.headers["x-crx-event-id"] ?? "");
if (!seen.has(eventId)) {
seen.add(eventId);
const event = JSON.parse(rawBody.toString("utf8")) as { type: string; data: unknown };
console.log("olay", event.type, event.data);
}
res.writeHead(200).end(); // yalnız 2xx başarıdır; başka kod yeniden denemeyi tetikler
});
}).listen(8080);