Bewtee

Developer docs

Verify signatures

Make sure a webhook request really came from Bewtee.

On this page (3)
  1. How the signature is built
  2. Steps
  3. Rotating the secret

Anyone who learns your webhook address could send it fake requests. Every real request is signed with your webhook's signing secret, so check the signature before trusting the body.

How the signature is built

Bewtee-Signature: t=1794373200,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
  • t is the Unix time the request was sent.
  • v1 is the hex HMAC-SHA256 of the string <t>.<raw request body>, keyed with your signing secret.

Steps

  1. Read the raw request body, before any JSON parsing. Re-serialised JSON will not match.
  2. Compute HMAC-SHA256 of t + "." + rawBody with your signing secret.
  3. Compare it to v1 with a constant-time comparison.
  4. Reject requests whose t is more than 5 minutes old, to stop replays.

Node.js

const crypto = require('crypto');

function verifyBewteeSignature(rawBody, header, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(header.split(',').map((pair) => pair.split('=')));
  const timestamp = Number(parts.t);
  if (!timestamp || Math.abs(Date.now() / 1000 - timestamp) > toleranceSeconds) return false;

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

// Express: keep the raw body
app.post('/bewtee/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const ok = verifyBewteeSignature(req.body.toString('utf8'), req.get('Bewtee-Signature') || '', process.env.BEWTEE_WEBHOOK_SECRET);
  if (!ok) return res.sendStatus(400);

  const event = JSON.parse(req.body.toString('utf8'));
  res.sendStatus(200); // reply first, then do the work
  handle(event);
});

PHP

function verifyBewteeSignature(string $rawBody, string $header, string $secret, int $tolerance = 300): bool
{
    $parts = [];
    foreach (explode(',', $header) as $pair) {
        [$key, $value] = array_pad(explode('=', $pair, 2), 2, '');
        $parts[$key] = $value;
    }
    $timestamp = (int) ($parts['t'] ?? 0);
    if ($timestamp === 0 || abs(time() - $timestamp) > $tolerance) {
        return false;
    }

    return hash_equals(hash_hmac('sha256', $timestamp.'.'.$rawBody, $secret), $parts['v1'] ?? '');
}

$rawBody = file_get_contents('php://input');
if (! verifyBewteeSignature($rawBody, $_SERVER['HTTP_BEWTEE_SIGNATURE'] ?? '', getenv('BEWTEE_WEBHOOK_SECRET'))) {
    http_response_code(400);
    exit;
}
$event = json_decode($rawBody, true);
http_response_code(200);

Python

import hashlib, hmac, time

def verify_bewtee_signature(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    parts = dict(pair.split("=", 1) for pair in header.split(",") if "=" in pair)
    try:
        timestamp = int(parts.get("t", "0"))
    except ValueError:
        return False
    if not timestamp or abs(time.time() - timestamp) > tolerance:
        return False

    expected = hmac.new(secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))

Rotating the secret

In the dashboard choose New signing secret on the webhook. The old secret stops working immediately, so update your server at the same time.

Ready to connect Bewtee?

Sign in to your Bewtee business dashboard and choose Developers. Everything there is free for Bewtee businesses.

New to Bewtee? See what the salon software does

Found something wrong? Report an error

Login

Use your mobile number to view and manage your bookings.

+91

Enter the code we sent to +91 on WhatsApp.