Ürün ve API rehberi

Webhook'lar

Neler gönderiliyor

PDFgine size iki ayrı sebeple dönüyor: kuyruk, ürettiği belgenin bittiğini söylüyor; görüntüleyici de belgeyi paylaştığınız kişinin onu açtığını ya da indirdiğini.

  • document.render.succeeded — kuyruktaki belge üretildi ve indirilebilir
  • document.render.failed — kuyruktaki belge üretilemedi; gövdede failureCode var
  • document.viewed — paylaşılan belge görüntüleyicide açıldı
  • document.downloaded — paylaşılan belge indirildi

Üretim olayları, render isteğiyle verdiğiniz webhookUrl adresine gider. Görüntüleyici olayları ise POST /api/v1/engagement-webhooks ile bir kez kaydettiğiniz uçlara gider; her uç hangi olayları istediğini kendisi söyler.

Her çağrıda gelen başlıklar

İki webhook türü de aynı başlıkları taşıyor, yani tek bir doğrulama yordamı hepsini karşılıyor.

Başlık Ne taşıyor
X-PDFgine-EventOlay türü, örneğin document.render.succeeded.
X-PDFgine-DeliveryTeslim kimliği. Bir çağrı yeniden denendiğinde aynı kimlik tekrar gelir — işleyicinizi tekrara dayanıklı yapmak için bunu kullanın.
X-PDFgine-TimestampÇağrının imzalandığı an; UTC, Unix saniyesi.
X-PDFgine-Signaturev1= ve ardından küçük harfli onaltılık imza.
User-AgentPDFgine-Webhooks/1.0

İmza nasıl kuruluyor

İmza, zaman damgasını ve gövdeyi birlikte kapsıyor; böylece yakalanan bir çağrı sonradan yeni bir zaman damgasıyla tekrar oynatılamıyor.

Şema
X-PDFgine-Signature: v1=<hex>

hex = HMAC-SHA256(
        key     = <the secret you registered>,
        message = <X-PDFgine-Timestamp> + "." + <raw request body>
      )            // lower-case hexadecimal

Aldığınız ham baytları imzalayın, yeniden serileştirilmiş bir nesneyi değil. Bir JSON çözümleyicisi anahtarların sırasını ya da boşlukları değiştirebilir ve imza bir daha asla tutmaz — doğrulamanın başarısız olmasının en yaygın sebebi budur.

Zaman toleransı

Toleransa PDFgine değil siz karar veriyorsunuz. Zaman damgası kendi saatinizden beş dakikadan fazla uzaktaysa çağrıyı reddedin — normal saat kayması ve yeniden denemeler için yeterince geniş, yakalanan bir çağrının hızla işe yaramaz hale gelmesi için yeterince dar.

İki imzayı sabit sürede karşılaştırın. Düz bir metin karşılaştırması, ilk harfi yanlış olan bir imza için son harfi yanlış olandan daha erken döner ve bu fark ölçülebilir.

Doğrulama, çalıştırmaya hazır

İki örnek de aynı dört şeyi yapıyor: ham gövdeyi oku, zaman damgasının yaşını kontrol et, imzayı yeniden hesapla ve sabit sürede karşılaştır.

Node.js
import crypto from "node:crypto";
import express from "express";

const app = express();
const SECRET = process.env.PDFGINE_WEBHOOK_SECRET;
const TOLERANCE_SECONDS = 300;

// HAM gövde şart: express.json() imzayı bozar, çünkü JSON'u yeniden yazar.
app.post("/hooks/pdfgine", express.raw({ type: "application/json" }), (req, res) => {
  const timestamp = req.get("X-PDFgine-Timestamp");
  const header = req.get("X-PDFgine-Signature") ?? "";

  const age = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));
  if (!timestamp || Number.isNaN(age) || age > TOLERANCE_SECONDS) {
    return res.sendStatus(400);
  }

  const expected = crypto
    .createHmac("sha256", SECRET)
    .update(`${timestamp}.${req.body.toString("utf8")}`)
    .digest("hex");
  const supplied = header.startsWith("v1=") ? header.slice(3) : "";

  const a = Buffer.from(expected, "utf8");
  const b = Buffer.from(supplied, "utf8");
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    return res.sendStatus(401);
  }

  const event = JSON.parse(req.body.toString("utf8"));
  console.log(req.get("X-PDFgine-Event"), req.get("X-PDFgine-Delivery"), event);
  res.sendStatus(204);
});
C#
app.MapPost("/hooks/pdfgine", async (HttpRequest request) =>
{
    const int toleranceSeconds = 300;
    var secret = Environment.GetEnvironmentVariable("PDFGINE_WEBHOOK_SECRET")!;

    using var reader = new StreamReader(request.Body);
    var body = await reader.ReadToEndAsync();

    var timestampHeader = request.Headers["X-PDFgine-Timestamp"].ToString();
    if (!long.TryParse(timestampHeader, out var timestamp)
        || Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - timestamp) > toleranceSeconds)
    {
        return Results.BadRequest();
    }

    var expected = Convert.ToHexString(HMACSHA256.HashData(
            Encoding.UTF8.GetBytes(secret),
            Encoding.UTF8.GetBytes($"{timestamp}.{body}")))
        .ToLowerInvariant();

    var header = request.Headers["X-PDFgine-Signature"].ToString();
    var supplied = header.StartsWith("v1=", StringComparison.Ordinal) ? header[3..] : "";
    if (!CryptographicOperations.FixedTimeEquals(
            Encoding.UTF8.GetBytes(expected),
            Encoding.UTF8.GetBytes(supplied)))
    {
        return Results.Unauthorized();
    }

    return Results.NoContent();
});

Gövde neye benziyor

Üretim olayları; teslim kimliği, olay türü ve bir data nesnesi taşıyan bir zarfla gelir. Görüntüleyici olayları alanları doğrudan taşır.

document.render.succeeded
{
  "id": "evt_8f1c4a7d6b2e4f0a91c3d5e7f9a1b3c5",
  "type": "document.render.succeeded",
  "createdAt": "2026-09-26T14:39:45+00:00",
  "data": {
    "queueId": "queue_fe86ad38402a4909b3b5a7965847924a",
    "batchId": "batch_69c4ecb63f8642628b72bca89d4f280c",
    "status": "succeeded",
    "templateCode": "invoice",
    "format": "pdf",
    "renderJobId": "job_bfae00cc59354089bffd41fa298377b2",
    "documentId": "doc_a96063f5dcd5400286ce9b3be9b0595b",
    "failureCode": null
  }
}
document.downloaded
{
  "previewId": "prv_510ff800bbcd4f848b0b769cf37ad4c2",
  "templateCode": "invoice",
  "deviceClass": "desktop",
  "accessedAt": "2026-09-26T14:41:02+00:00",
  "format": "pdf"
}

İki şekil bugün bilerek farklı ve işleyiciyi yazmadan önce bilmekte fayda var: görüntüleyici olaylarında olay türü yalnızca X-PDFgine-Event başlığında, gövdede değil.

Teslim ve yeniden deneme

Webhook satır içinde gönderilmiyor, kuyruğa alınıyor; yani sizin yavaş bir ucunuz belge üretimini hiçbir zaman yavaşlatmıyor.

  • Herhangi bir 2xx teslim sayılır. Yönlendirme dahil, geri kalan her şey başarısız.
  • En çok 6 deneme; bekleme 15 saniyeden başlayıp katlanarak artıyor.
  • 15 saniye içinde yanıt vermeyen çağrı başarısız sayılır ve yeniden denenir.
  • Hedef https olmak ve açık bir adrese çözülmek zorunda. localhost, .local adlar ve özel ağ aralıkları hem uç kaydedilirken hem de her çağrıdan önce reddediliyor.

Yeniden deneme aynı X-PDFgine-Delivery kimliğini tekrarladığı için, işlediğiniz kimlikleri kaydedin ve tekrara işi iki kez yapmadan 2xx yanıtı verin.

Hatalar →

Sık takılınan yerler

  • Yeniden serileştirilmiş gövdeyle doğrulama hiçbir zaman tutmaz. JSON'u sizin için çözen çatılar anahtar sırasını ve boşlukları değiştirir; ham baytları hiçbir şey dokunmadan saklayın.
  • Teslim sayılan yalnız 2xx'tir. Gerçek işleyicinize giden bir 301 ya da 302 başarısızlıktır ve çağrı yeniden denenir — ucu son adrese yöneltin.
  • İmzayı doğrulayıp zaman damgasına bakmamak, yakalanmış bir çağrıyı sonsuza kadar yeniden oynatılabilir bırakır. Toleransınızdan eskisini reddedin.
  • Yeniden deneme aynı X-PDFgine-Delivery kimliğini taşır. İşi tekrar yapan bir işleyici aynı faturayı iki kez gönderir; işlediğiniz kimlikleri kaydedin.
  • Kayıtlı uç https ve herkese açık çözülebilir olmalı. localhost ve özel aralıklar kayıtta reddedilir; reddi zorlamak yerine bir tünel üzerinden deneyin.