Easy2257
Webhooks

Webhook Signatures

How to verify Easy2257 webhook signatures. Raw body is required — not re-parsed JSON.

Every webhook request from Easy2257 includes an X-EZ2257-Signature header.

Always verify signatures before processing events. An unverified webhook endpoint is an open door for spoofed events.

Signature format

X-EZ2257-Signature: t=1714000000,v1=abc123...
  • t — Unix timestamp (seconds) when Easy2257 sent the request
  • v1 — HMAC-SHA256 of {t}.{rawBody} using your webhook secret

Verification algorithm

  1. Extract t and v1 from the header
  2. Construct the signed payload: "{t}.{rawBody}"
  3. Compute HMAC-SHA256 of the payload using your webhook secret
  4. Compare your computed signature to v1 using a constant-time comparison
  5. Optionally: reject if t is more than 5 minutes ago (replay protection)

Use the raw request body for the HMAC — the literal bytes received over the wire. Never JSON.parse the body and re-serialize it before hashing. Re-serialization changes whitespace and key order, producing a different hash. This is the single most common integration failure.

Verify function + webhook handler

import { createHmac, timingSafeEqual } from 'crypto';

function verifyWebhookSignature(rawBody, header, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(header.split(',').map(p => p.split('=')));
  const { t, v1 } = parts;
  if (!t || !v1) return false;
  if (toleranceSeconds > 0 && Math.floor(Date.now() / 1000) - parseInt(t) > toleranceSeconds)
    return false;
  const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
  return timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
}

// Next.js App Router — app/api/webhooks/ez2257/route.ts
export async function POST(req) {
  const rawBody = await req.text(); // NEVER req.json() before this
  const sig = req.headers.get('x-ez2257-signature') ?? '';
  if (!verifyWebhookSignature(rawBody, sig, process.env.EZ2257_WEBHOOK_SECRET))
    return new Response('Unauthorized', { status: 401 });
  const event = JSON.parse(rawBody);
  // handle event...
  return new Response('ok');
}

// Express — use express.raw(), not express.json(), on this route
app.post('/webhooks/ez2257', express.raw({ type: 'application/json' }), (req, res) => {
  const rawBody = req.body.toString('utf8');
  const sig = req.headers['x-ez2257-signature'] ?? '';
  if (!verifyWebhookSignature(rawBody, sig, process.env.EZ2257_WEBHOOK_SECRET))
    return res.status(401).send('Unauthorized');
  const event = JSON.parse(rawBody);
  // handle event...
  res.send('ok');
});
import hmac, hashlib, time

def verify_webhook_signature(raw_body, header, secret, tolerance_seconds=300):
    parts = dict(p.split('=', 1) for p in header.split(',') if '=' in p)
    t, v1 = parts.get('t'), parts.get('v1')
    if not t or not v1:
        return False
    if tolerance_seconds > 0 and int(time.time()) - int(t) > tolerance_seconds:
        return False
    body_bytes = raw_body if isinstance(raw_body, bytes) else raw_body.encode('utf-8')
    payload = f'{t}.'.encode() + body_bytes
    expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
    return hmac.compare_digest(v1, expected)

# Flask — use request.get_data(), not request.get_json()
from flask import Flask, request, abort
import os, json

app = Flask(__name__)

@app.post('/webhooks/ez2257')
def ez2257_webhook():
    raw_body = request.get_data()  # bytes
    sig = request.headers.get('X-EZ2257-Signature', '')
    if not verify_webhook_signature(raw_body, sig, os.environ['EZ2257_WEBHOOK_SECRET']):
        abort(401)
    event = json.loads(raw_body)
    # handle event...
    return 'ok'

# FastAPI — use await request.body(), not request.json()
from fastapi import FastAPI, Request, HTTPException

fastapi_app = FastAPI()

@fastapi_app.post('/webhooks/ez2257')
async def ez2257_webhook_fastapi(request: Request):
    raw_body = await request.body()
    sig = request.headers.get('x-ez2257-signature', '')
    if not verify_webhook_signature(raw_body, sig, os.environ['EZ2257_WEBHOOK_SECRET']):
        raise HTTPException(status_code=401)
    event = json.loads(raw_body)
    # handle event...
    return {'ok': True}
function verifyWebhookSignature(
    string $rawBody,
    string $header,
    string $secret,
    int $toleranceSeconds = 300
): bool {
    $parts = [];
    foreach (explode(',', $header) as $pair) {
        $kv = explode('=', $pair, 2);
        if (count($kv) === 2) $parts[$kv[0]] = $kv[1];
    }
    $t = $parts['t'] ?? null;
    $v1 = $parts['v1'] ?? null;
    if (!$t || !$v1) return false;
    if ($toleranceSeconds > 0 && time() - (int) $t > $toleranceSeconds) return false;
    $expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret);
    return hash_equals($expected, $v1);
}

// Plain PHP — use php://input
$rawBody = file_get_contents('php://input');
$sig = $_SERVER['HTTP_X_EZ2257_SIGNATURE'] ?? '';
if (!verifyWebhookSignature($rawBody, $sig, getenv('EZ2257_WEBHOOK_SECRET'))) {
    http_response_code(401); exit('Unauthorized');
}
$event = json_decode($rawBody, true);
// handle event...
echo 'ok';

// Laravel — use $request->getContent(), exempt route from VerifyCsrfToken
Route::post('/webhooks/ez2257', function (Illuminate\Http\Request $request) {
    $rawBody = $request->getContent();
    $sig = $request->header('X-EZ2257-Signature', '');
    if (!verifyWebhookSignature($rawBody, $sig, env('EZ2257_WEBHOOK_SECRET')))
        abort(401);
    $event = json_decode($rawBody, true);
    // handle event...
    return response('ok');
});
using System.Security.Cryptography;
using System.Text;

public static bool VerifyWebhookSignature(
    string rawBody, string header, string secret, int toleranceSeconds = 300)
{
    var parts = header.Split(',')
        .Select(p => p.Split('=', 2)).Where(p => p.Length == 2)
        .ToDictionary(p => p[0], p => p[1]);
    if (!parts.TryGetValue("t", out var t) || !parts.TryGetValue("v1", out var v1))
        return false;
    if (toleranceSeconds > 0 &&
        DateTimeOffset.UtcNow.ToUnixTimeSeconds() - long.Parse(t) > toleranceSeconds)
        return false;
    var payload = Encoding.UTF8.GetBytes($"{t}.{rawBody}");
    using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
    var expected = Convert.ToHexString(hmac.ComputeHash(payload)).ToLower();
    return CryptographicOperations.FixedTimeEquals(
        Encoding.UTF8.GetBytes(v1), Encoding.UTF8.GetBytes(expected));
}

// ASP.NET Core controller — call EnableBuffering() before reading body
[HttpPost("/webhooks/ez2257")]
public async Task<IActionResult> Ez2257Webhook()
{
    Request.EnableBuffering();
    using var reader = new StreamReader(Request.Body, leaveOpen: true);
    var rawBody = await reader.ReadToEndAsync();
    Request.Body.Position = 0;
    var sig = Request.Headers["X-EZ2257-Signature"].FirstOrDefault() ?? "";
    if (!WebhookVerifier.VerifyWebhookSignature(rawBody, sig, _webhookSecret))
        return Unauthorized();
    var evt = JsonSerializer.Deserialize<Ez2257Event>(rawBody);
    // handle event...
    return Ok();
}

Replay protection

The t timestamp lets you reject old events. The default tolerance in the snippets above is 5 minutes — tighten to 60 seconds in production if your clock is reliable.

Deduplicate on event.id — Easy2257 retries failed deliveries and the same event may arrive more than once.

On this page