Webhook tester
api API play_arrow Quickstart terminal Explorer verified Webhook tester history Changelog report Errors monitor_heart Status menu_book Glossary help Help

Webhook signature tester

Verify your ToneGrid webhook signature handling without hitting any of our servers. Paste a webhook secret, the raw request body, and either a received X-ToneGrid-Signature value (to check it matches) or click Generate to mint a fresh sample for testing your verification code.

lock Runs entirely in your browser. Nothing leaves this page.

Input

Result

pending
Waiting for input
Fill in secret + body, then click Verify or Generate.
Computed signature
Full X-ToneGrid-Signature header
Signed payload (ts.body)

How the signature is computed

  1. Every webhook POST carries the header X-ToneGrid-Signature: t=<unix_ts>,v1=<hex_sha256_hmac>.
  2. Parse the header into t (timestamp, integer) and v1 (signature, lowercase hex).
  3. Reject events where |now - t| > 300 seconds — prevents replay attacks.
  4. Compute the signed payload by concatenating: t + "." + raw_body. Use the raw body bytes, not a re-serialized JSON string — even whitespace differences will break the signature.
  5. Compute expected = HMAC_SHA256(secret, signed_payload) as lowercase hex.
  6. Constant-time compare expected against v1. If they match, the event is authentic.

The same shape Stripe uses. If you've integrated a Stripe webhook, the verification code translates almost line-for-line.

Verification snippets

function tg_verify(string $rawBody, string $sigHeader, string $secret, int $tolerance = 300): bool {
    if (!preg_match('/t=(\d+),v1=([0-9a-f]+)/', $sigHeader, $m)) {
        return false;
    }
    [, $ts, $v1] = $m;
    if (abs(time() - (int)$ts) > $tolerance) {
        return false; // replay protection
    }
    $expected = hash_hmac('sha256', $ts . '.' . $rawBody, $secret);
    return hash_equals($expected, $v1); // constant-time compare
}

// In your endpoint:
$raw = file_get_contents('php://input');
$sig = $_SERVER['HTTP_X_TONEGRID_SIGNATURE'] ?? '';
if (!tg_verify($raw, $sig, getenv('TG_WEBHOOK_SECRET'))) {
    http_response_code(401);
    return;
}
$event = json_decode($raw, true);
// handle $event…
import crypto from 'node:crypto';

export function tgVerify(rawBody, sigHeader, secret, toleranceSec = 300) {
  const m = (sigHeader || '').match(/^t=(\d+),v1=([0-9a-f]+)$/);
  if (!m) return false;
  const [, ts, v1] = m;
  if (Math.abs(Date.now() / 1000 - Number(ts)) > toleranceSec) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(ts + '.' + rawBody)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(expected, 'hex'),
    Buffer.from(v1,       'hex')
  );
}

// Express handler — use the raw body parser:
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const raw = req.body.toString('utf8');
  if (!tgVerify(raw, req.header('x-tonegrid-signature'), process.env.TG_WEBHOOK_SECRET)) {
    return res.sendStatus(401);
  }
  const event = JSON.parse(raw);
  res.sendStatus(200);
});
import hmac, hashlib, time, re

def tg_verify(raw_body: bytes, sig_header: str, secret: str, tolerance: int = 300) -> bool:
    m = re.fullmatch(r't=(\d+),v1=([0-9a-f]+)', sig_header or '')
    if not m:
        return False
    ts, v1 = m.groups()
    if abs(int(time.time()) - int(ts)) > tolerance:
        return False
    payload = (ts + '.').encode() + raw_body
    expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, v1)  # constant-time

# Flask handler:
@app.post('/webhook')
def webhook():
    if not tg_verify(request.get_data(),
                     request.headers.get('X-ToneGrid-Signature', ''),
                     os.environ['TG_WEBHOOK_SECRET']):
        abort(401)
    event = request.get_json()
    return ('', 200)
package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "fmt"
    "regexp"
    "strconv"
    "time"
)

var sigRe = regexp.MustCompile(`^t=(\d+),v1=([0-9a-f]+)$`)

func TGVerify(rawBody []byte, sigHeader, secret string, toleranceSec int64) bool {
    m := sigRe.FindStringSubmatch(sigHeader)
    if m == nil { return false }
    ts, _ := strconv.ParseInt(m[1], 10, 64)
    if abs(time.Now().Unix() - ts) > toleranceSec { return false }

    mac := hmac.New(sha256.New, []byte(secret))
    fmt.Fprintf(mac, "%d.", ts)
    mac.Write(rawBody)
    expected := hex.EncodeToString(mac.Sum(nil))
    return hmac.Equal([]byte(expected), []byte(m[2]))
}

func abs(x int64) int64 { if x < 0 { return -x }; return x }

warning Common gotchas