Webhook: tamamlanan yazı imzalı bir istekle sisteminize gelir

WordPress dışındaki siteler, kendi yayın akışınız ya da otomasyon araçları için. Betig, yazı tamamlandığında belirlediğiniz adrese JSON gövdeli bir POST isteği gönderir. İstek Standard Webhooks biçiminde imzalanır; sahte istekleri kolayca ayırırsınız.

Üç başlık, bir gövde

  • webhook-id
    Olayın benzersiz kimliği. Aynı olay yeniden gönderilirse aynı kalır; yinelenenleri ayıklamak için saklayın.
  • webhook-timestamp
    Gönderim zamanı, Unix saniyesi. Eski istekleri reddetmek için kullanın (ör. 5 dakika tolerans).
  • webhook-signature
    v1, ile başlayan, Base64 kodlu HMAC-SHA256 imzası. Birden fazla imza boşlukla ayrılmış gelebilir (anahtar yenilerken).
HTTP başlıkları
POST /webhooks/betig HTTP/1.1
Content-Type: application/json
webhook-id: msg_2b7Qz9yXfJ4kT1
webhook-timestamp: 1791633600
webhook-signature: v1,8axkxMcw4LOYGkdY7FftA629B+AGRGX2FibkqePZ8lE=

document.publish olayı

Gövde, Standard Webhooks önerisindeki type, timestamp ve data düzenini izler.1 data.status her zaman "draft" gelir: yazıyı taslak olarak kaydedin, yayından önce bir kişi okusun.

  • sources ve verification: kaynak listesi ve doğrulama notu.
  • parent_id ve language: çevirilerde kaynak yazının kimliği ve dil kodu (hreflang kurulumu için).
  • schema_jsonld: Article ve FAQPage yapılandırılmış verisi.
Örnek gövde (metinler kısaltıldı)
{
  "type": "document.publish",
  "timestamp": "2026-10-10T12:00:00Z",
  "data": {
    "document_id": "8f0c6a2e-2d0b-4c57-9a51-5b0f3c6a1e77",
    "project_id": "1b9e4d33-7c2a-4f0e-8d6b-2a4c9e0f5d12",
    "parent_id": null,
    "language": "tr",
    "status": "draft",
    "title": "Garanti süresi kaç yıl? Arızalı elektronik üründe haklarınız",
    "slug": "garanti-suresi-kac-yil",
    "target_query": "garanti süresi kaç yıl",
    "meta": {
      "title": "Garanti Süresi Kaç Yıl? Arızalı Üründe Haklarınız",
      "description": "Garanti süresi teslimden başlar ve en az iki yıldır. Arızalı üründe dört hakkınızı ve onarım süresini kaynağıyla öğrenin."
    },
    "article_html": "<p>Tüketiciye satılan mallarda garanti süresi...</p>",
    "article_md": "Tüketiciye satılan mallarda garanti süresi...",
    "faq": [
      { "q": "Garanti süresi ne zaman başlar?", "a": "Malın teslim tarihinden itibaren başlar." }
    ],
    "schema_jsonld": [
      { "@context": "https://schema.org", "@type": "Article", "headline": "Garanti süresi kaç yıl?" }
    ],
    "sources": [
      {
        "id": "1",
        "title": "6502 sayılı Tüketicinin Korunması Hakkında Kanun",
        "url": "https://www.mevzuat.gov.tr/mevzuat?MevzuatNo=6502&MevzuatTur=1&MevzuatTertip=5",
        "publisher": "Mevzuat Bilgi Sistemi",
        "date": "2013-11-28"
      }
    ],
    "verification": [
      { "claim": "Garanti süresi teslimden başlar, en az iki yıldır", "source_ids": ["1"], "status": "birebir", "note": "" }
    ],
    "word_count": 1342
  }
}

İmzayı doğrulayın

İmzalanan metin webhook-id, webhook-timestamp ve ham gövdenin noktayla birleştirilmesidir: id.timestamp.gövde. Anahtar, panelde gösterilen whsec_ önekli gizli anahtarın önek sonrası Base64 çözülmüş halidir.1 Gövdeyi JSON'a çevirip yeniden yazarsanız imza tutmaz; ham gövdeyle doğrulayın. Standard Webhooks'un Node.js, Python, PHP ve başka diller için hazır kütüphaneleri de kullanılabilir.

Node.js (Express)
import crypto from "node:crypto";
import express from "express";

const SECRET = process.env.BETIG_WEBHOOK_SECRET; // whsec_ ile başlar
const TOLERANCE = 5 * 60; // saniye

function verify(rawBody, headers) {
  const id = headers["webhook-id"];
  const ts = headers["webhook-timestamp"];
  const sigHeader = headers["webhook-signature"];
  if (!id || !ts || !sigHeader) return false;
  if (Math.abs(Date.now() / 1000 - Number(ts)) > TOLERANCE) return false;

  const key = Buffer.from(SECRET.replace(/^whsec_/, ""), "base64");
  const expected = crypto
    .createHmac("sha256", key)
    .update(`${id}.${ts}.${rawBody}`)
    .digest("base64");

  return sigHeader.split(" ").some((part) => {
    const [version, sig] = part.split(",");
    if (version !== "v1" || !sig) return false;
    const a = Buffer.from(sig);
    const b = Buffer.from(expected);
    return a.length === b.length && crypto.timingSafeEqual(a, b);
  });
}

const app = express();
// Gövdeyi ham haliyle alın; JSON'a çevirip geri yazmak imzayı bozar.
app.post("/webhooks/betig", express.raw({ type: "application/json" }), (req, res) => {
  const raw = req.body.toString("utf8");
  if (!verify(raw, req.headers)) return res.status(401).end();
  const event = JSON.parse(raw);
  // event.data.status her zaman "draft": yazıyı taslak olarak kaydedin.
  res.status(204).end();
});
PHP
<?php
function betig_verify(string $body, array $h, string $secret): bool {
    $id  = $h['HTTP_WEBHOOK_ID'] ?? '';
    $ts  = $h['HTTP_WEBHOOK_TIMESTAMP'] ?? '';
    $sig = $h['HTTP_WEBHOOK_SIGNATURE'] ?? '';
    if ($id === '' || $ts === '' || $sig === '') return false;
    if (abs(time() - (int) $ts) > 300) return false;

    $key = base64_decode(preg_replace('/^whsec_/', '', $secret));
    $expected = base64_encode(hash_hmac('sha256', "$id.$ts.$body", $key, true));

    foreach (explode(' ', $sig) as $part) {
        [$version, $value] = array_pad(explode(',', $part, 2), 2, '');
        if ($version === 'v1' && hash_equals($expected, $value)) return true;
    }
    return false;
}

$body = file_get_contents('php://input');
if (!betig_verify($body, $_SERVER, getenv('BETIG_WEBHOOK_SECRET'))) {
    http_response_code(401);
    exit;
}
$event = json_decode($body, true);
http_response_code(204);
Python
import base64, hashlib, hmac, os, time

SECRET = os.environ["BETIG_WEBHOOK_SECRET"]  # whsec_ ile başlar

def verify(body: bytes, headers) -> bool:
    msg_id = headers.get("webhook-id", "")
    ts = headers.get("webhook-timestamp", "")
    sig_header = headers.get("webhook-signature", "")
    if not (msg_id and ts and sig_header):
        return False
    if abs(time.time() - int(ts)) > 300:
        return False
    key = base64.b64decode(SECRET.removeprefix("whsec_"))
    signed = f"{msg_id}.{ts}.".encode() + body
    expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
    for part in sig_header.split():
        version, _, sig = part.partition(",")
        if version == "v1" and hmac.compare_digest(sig, expected):
            return True
    return False

Test değeri

Kodunuzu bu sabit değerlerle sınayın; zaman toleransı kontrolünü sınama sırasında kapatın.

Bilinen girdi ve beklenen imza
Gizli anahtar : whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSw
webhook-id     : msg_2b7Qz9yXfJ4kT1
webhook-timestamp : 1791633600
Gövde         : {"type":"document.publish","timestamp":"2026-10-10T12:00:00Z","data":{"document_id":"8f0c6a2e-2d0b-4c57-9a51-5b0f3c6a1e77","status":"draft"}}
İmzalanan     : msg_2b7Qz9yXfJ4kT1.1791633600.<gövde>
Beklenen imza : v1,8axkxMcw4LOYGkdY7FftA629B+AGRGX2FibkqePZ8lE=

İsteğe nasıl yanıt verilir

  • İsteği aldığınızı 2xx kodlu bir yanıtla hemen bildirin; uzun işleri kuyruğa alın. 2xx dışındaki yanıtlar ve zaman aşımı başarısız teslim sayılır.1
  • Aynı olay birden fazla kez ulaşabilir. webhook-id değerini saklayın ve ikinci kez işlemeyin.
  • Gizli anahtarı yalnız sunucu ortamında tutun; tarayıcı koduna ya da depoya koymayın.

API ile sipariş verin

API Profesyonel ve Ajans planlarında açıktır. API anahtarını panelde oluşturursunuz; anahtar yalnız bir kez gösterilir. İstekler Authorization: Bearer başlığıyla gönderilir. Gövde, paneldeki yazı formuyla aynı alanları taşır: başlık, anahtar kelime ya da brief, uzunluk, SSS, brief onayı ve çeviri dilleri.

  • POST /api/v1/articles
    Yazı siparişi
  • GET /api/v1/jobs/{id}
    İşin durumu ve adımları
  • GET /api/v1/documents/{id}
    Yazı, meta, SSS, şema, kaynaklar ve doğrulama notu
Örnek istek
curl -X POST https://app.betig.app/api/v1/articles \
  -H "Authorization: Bearer $BETIG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "project_id": "1b9e4d33-7c2a-4f0e-8d6b-2a4c9e0f5d12",
    "kind": "article",
    "input": {
      "keyword": "garanti süresi kaç yıl",
      "length": "medium",
      "include_faq": true,
      "review_brief": false
    },
    "options": {}
  }'

İlk blog yazınızı kendi konunuzda görün

Hesap açmak ücretsizdir ve kart bilgisi istemez. Araçları ve site önizlemesini hemen kullanırsınız; ödemeli planlar çok yakında açılıyor. Her işin kaç yazı hakkı kullanacağı başlamadan gösterilir.