Pientegra API
Webhooks

Webhook Security

HMAC-SHA256 signature verification, raw body handling ve replay protection.

Pientegra webhook'ları per-site webhook secret ile HMAC-SHA256 kullanarak imzalar. Signature doğrulanmadan event işlenmemelidir.

Signature header

Pientegra-Signature: t=1730131200000,v1=4f3c2a8d9b1e0c7a6f5d4b3a2e1c0d9b8a7f6e5d4c3b2a190e1d2c3b4a5f6e7d
ParçaAçıklama
tUnix timestamp, millisecond.
v1HMAC-SHA256 hex digest.

Pientegra signature input'u şu formatta hesaplar:

HMAC-SHA256(webhook_secret, "<timestamp>.<raw-body>")

raw-body tam olarak HTTP request body byte'larıdır. JSON parse edip tekrar serialize etmek whitespace veya key order değiştirebileceği için signature'ı bozar.

Verification steps

  1. Pientegra-Signature header'ını parse edin.
  2. t timestamp'ini alın ve makul tolerance içinde olduğunu kontrol edin.
  3. "<timestamp>.<raw-body>" string'i üzerinden HMAC-SHA256 hesaplayın.
  4. Provided digest ile expected digest'i constant-time compare edin.
  5. Doğrulama geçerse JSON parse edip event'i işleyin.

Node.js example

verify-pientegra-signature.ts
import { createHmac, timingSafeEqual } from 'node:crypto';

const TOLERANCE_MS = 5 * 60 * 1000;

export function verifyPientegraSignature(
  secret: string,
  rawBody: Buffer | string,
  signatureHeader: string,
): boolean {
  const parts = Object.fromEntries(
    signatureHeader.split(',').map((part) => {
      const [key, value] = part.split('=', 2);
      return [key?.trim(), value?.trim()];
    }),
  );

  const timestamp = Number(parts.t);
  const provided = parts.v1;
  if (!timestamp || !provided) return false;

  if (Math.abs(Date.now() - timestamp) > TOLERANCE_MS) {
    return false;
  }

  const body = Buffer.isBuffer(rawBody) ? rawBody : Buffer.from(rawBody, 'utf8');
  const expected = createHmac('sha256', secret)
    .update(Buffer.concat([Buffer.from(`${timestamp}.`, 'utf8'), body]))
    .digest('hex');

  const expectedBuffer = Buffer.from(expected, 'hex');
  const providedBuffer = Buffer.from(provided, 'hex');

  if (expectedBuffer.length !== providedBuffer.length) return false;
  return timingSafeEqual(expectedBuffer, providedBuffer);
}

Express raw body example

Express route
app.post('/pientegra/webhooks', express.raw({ type: 'application/json' }), async (req, res) => {
  const signature = req.header('Pientegra-Signature');
  if (!signature) return res.status(401).send('missing signature');

  if (!verifyPientegraSignature(process.env.PIENTEGRA_WEBHOOK_SECRET!, req.body, signature)) {
    return res.status(401).send('invalid signature');
  }

  const event = JSON.parse(req.body.toString('utf8'));
  await handlePientegraEvent(event);
  return res.status(200).send('ok');
});

Express'te global express.json() middleware'i raw body'yi tüketebilir. Webhook route'u için express.raw({ type: 'application/json' }) kullandığınızdan emin olun.

PHP example

verify-pientegra-signature.php
<?php
function verify_pientegra_signature(
    string $secret,
    string $rawBody,
    string $signatureHeader,
    int $toleranceMs = 300000
): bool {
    $parts = [];
    foreach (explode(',', $signatureHeader) as $part) {
        [$key, $value] = explode('=', $part, 2);
        $parts[trim($key)] = trim($value);
    }

    $timestamp = intval($parts['t'] ?? 0);
    $provided = $parts['v1'] ?? '';
    if ($timestamp === 0 || $provided === '') return false;

    $nowMs = intval(microtime(true) * 1000);
    if (abs($nowMs - $timestamp) > $toleranceMs) return false;

    $expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
    return hash_equals($expected, $provided);
}

Replay protection

Timestamp check tek başına yeterli değildir. Saldırgan kısa tolerance penceresi içinde aynı signed request'i tekrar gönderebilir. Bu yüzden:

  • eventId değerini saklayın. eventId retry'lar arasında değişmez, bir business event tek bir eventId taşır — yani aynı eventId'i ikinci kez gördüğünüzde duplicate'in kendisidir.
  • Balance mutation'ları ayrıca business key ile idempotent yapın.
  • Duplicate event gördüğünüzde side effect çalıştırmadan 2xx dönün.

Önerilen unique key örnekleri:

FlowIdempotency key
Deposit creditintent.approved:<intent.id>
Withdrawal debitwithdrawal.sent:<withdrawal.id>
Withdrawal refundwithdrawal.rejected:<withdrawal.id>

Secret rotation

Webhook secret leak şüphesinde yeni secret üretin ve deployment sırasında kısa bir overlap penceresi kullanın. Handler, rotation süresince önce yeni secret ile, başarısız olursa eski secret ile verify edebilir.

Rotation window
function verifyWithRotation(rawBody: Buffer, signature: string): boolean {
  return (
    verifyPientegraSignature(process.env.PIENTEGRA_WEBHOOK_SECRET_NEW!, rawBody, signature) ||
    verifyPientegraSignature(process.env.PIENTEGRA_WEBHOOK_SECRET_OLD!, rawBody, signature)
  );
}

On this page