MekanteMekante Developers
Getting started
Till API
Webhooks
llms.txt
  • Türkçe
  • English
Getting started
Till API
Webhooks
llms.txt
  • Türkçe
  • English
  • Getting started

    • Mekante Developers
    • Getting started
    • Authentication
  • Till API

    • Till flow
    • Endpoints
    • Errors
    • Limits
  • Webhooks

    • Webhooks
    • Signature verification

Signature verification

Because your webhook URL is publicly reachable on the internet, you should verify the cryptographic signature on every incoming request to ensure it genuinely came from Mekante.

How requests are signed

Every webhook request includes an X-Mekante-Signature header:

X-Mekante-Signature: t=1792749428,v1=794b3947df267c3354af75b5d8931538c765d768e5ca83983dcd52f7c6f93270
  • t: Unix timestamp in seconds when the request was signed.
  • v1: lowercase hexadecimal HMAC-SHA256 digest of:
HMAC_SHA256(signing_secret, "<t>.<raw_body>")

signing_secret is the whsec_… secret shown when you created the webhook endpoint. raw_body is the exact raw byte stream sent by the server: do not parse and re-serialize the JSON before verifying, or whitespace and key ordering differences will invalidate the signature.

Verification steps

  1. Split the X-Mekante-Signature header to extract t and v1.
  2. Check that t is within 5 minutes (300 seconds) of the current clock to prevent replay attacks.
  3. Compute HMAC_SHA256(signing_secret, "<t>.<raw_body>").
  4. Compare your computed hex digest with v1 using a constant-time comparison function (never plain === or ==).
  5. If valid, process the event and return 200; otherwise return 400.

Node.js (Express)

import crypto from "node:crypto";

import express from "express";

const SECRET = process.env.MEKANTE_WEBHOOK_SECRET; // whsec_…
const TOLERANCE_SECONDS = 300;

function verify(rawBody, header) {
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const t = Number(parts.t);
  const v1 = parts.v1;
  if (!t || !v1) return false;
  if (Math.abs(Date.now() / 1000 - t) > TOLERANCE_SECONDS) return false;

  const expected = crypto.createHmac("sha256", SECRET).update(`${t}.${rawBody}`).digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(v1);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

const app = express();
// Raw body is required: do NOT use express.json() on this route.
app.post("/hooks", express.raw({ type: "application/json" }), (req, res) => {
  const raw = req.body.toString("utf8");
  if (!verify(raw, req.get("X-Mekante-Signature") ?? "")) return res.sendStatus(400);

  const event = JSON.parse(raw);
  // Deduplicate by event.id, then handle event.event.
  res.sendStatus(200);
});

Python (Flask)

import hashlib, hmac, os, time
from flask import Flask, request, abort

SECRET = os.environ["MEKANTE_WEBHOOK_SECRET"].encode()  # whsec_…
TOLERANCE = 300
app = Flask(__name__)

def verify(raw: bytes, header: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    t, v1 = parts.get("t"), parts.get("v1")
    if not t or not v1 or abs(time.time() - int(t)) > TOLERANCE:
        return False
    expected = hmac.new(SECRET, f"{t}.".encode() + raw, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, v1)

@app.post("/hooks")
def hooks():
    raw = request.get_data()  # raw bytes
    if not verify(raw, request.headers.get("X-Mekante-Signature", "")):
        abort(400)
    event = request.get_json()
    # Deduplicate by event["id"] before processing.
    return "", 200

PHP

<?php
$secret = getenv('MEKANTE_WEBHOOK_SECRET'); // whsec_…
$raw = file_get_contents('php://input');     // raw request body
$header = $_SERVER['HTTP_X_MEKANTE_SIGNATURE'] ?? '';

parse_str(str_replace(',', '&', $header), $parts);
$t = $parts['t'] ?? '';
$v1 = $parts['v1'] ?? '';

$expected = hash_hmac('sha256', $t . '.' . $raw, $secret);
$fresh = ctype_digit($t) && abs(time() - (int) $t) <= 300;

if (!$fresh || !hash_equals($expected, $v1)) {
    http_response_code(400);
    exit;
}

$event = json_decode($raw, true);
// Deduplicate by $event['id'] before processing.
http_response_code(200);

Troubleshooting

  • If the signature never matches, check that your framework isn't parsing and re-serializing the JSON body or mutating line endings before verification.
  • Make sure there is no leading or trailing whitespace in your whsec_… environment variable.
  • Use Send test (Test gönder) in the dashboard to trigger a ping event and verify your handler end-to-end.
Prev
Webhooks