HookGet Start free

In short

Get the file

Copy it into your project. Nothing to install, nothing to keep up to date.

LanguageFileRequires
Node.jshookget.mjs · typesNode 18+
Pythonhookget.pyPython 3.8+, standard library only
PHPhookget.phpPHP 7.4+
Gohookget.goGo 1.21+

Apache-2.0. The shared test vectors are at /sdk/vectors.json if you would rather write your own.

Node

import express from 'express';
import { verify, HookgetVerificationError } from './hookget.mjs';

const app = express();

// express.raw, not express.json: the signature covers the bytes that arrived.
app.post('/hooks', express.raw({ type: '*/*' }), (req, res) => {
  try {
    verify(process.env.HOOKGET_SECRET, req.headers, req.body);
  } catch (err) {
    if (err instanceof HookgetVerificationError) return res.status(400).send(err.reason);
    throw err;
  }

  res.sendStatus(200);          // acknowledge first
  queueForProcessing(JSON.parse(req.body));   // work afterwards
});

Python

from flask import Flask, request
from hookget import verify, VerificationError

app = Flask(__name__)

@app.post("/hooks")
def hooks():
    try:
        # get_data(), not get_json(): the signature covers the bytes that arrived.
        verify(os.environ["HOOKGET_SECRET"], request.headers, request.get_data())
    except VerificationError as err:
        return err.reason, 400

    enqueue(request.get_json())   # acknowledge first, work afterwards
    return "", 200

PHP

<?php
require __DIR__ . '/hookget.php';

// php://input, not $_POST: the signature covers the bytes that arrived.
$raw = file_get_contents('php://input');

try {
    \HookGet\verify(getenv('HOOKGET_SECRET'), getallheaders(), $raw);
} catch (\HookGet\VerificationError $e) {
    http_response_code(400);
    exit($e->reason);
}

http_response_code(200);          // acknowledge first
enqueue(json_decode($raw, true)); // work afterwards

Go

func hooks(w http.ResponseWriter, r *http.Request) {
    // Read the body before decoding it: the signature covers these bytes.
    body, err := io.ReadAll(r.Body)
    if err != nil {
        http.Error(w, "unreadable body", http.StatusBadRequest)
        return
    }

    if _, err := hookget.Verify(os.Getenv("HOOKGET_SECRET"), r.Header, body, hookget.Options{}); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    w.WriteHeader(http.StatusOK)  // acknowledge first
    go process(body)              // work afterwards
}

Why they cannot drift

Four implementations of the same HMAC in four languages agree on the easy cases and disagree on the ones that matter.

So none of them is trusted on its own. A vector file is generated from the server's own signer and every library is checked against it in CI — including a body of Hebrew, an emoji and an accented Latin word, which is precisely where a byte-versus-character mistake stops being theoretical.

POST /hooks HTTP/1.1
webhook-id: msg_01J8ZQ3F9VBAQ4E1S0TZY6P8YV Stable across every retry — use it as your idempotency key
webhook-timestamp: 1755264000 Outside a 5-minute window, refuse it — that is the replay guard
webhook-signature: v1,ZeHt1v… v1a,mK4p… HMAC and ed25519 side by side during a scheme change, space separated
{"type":"order.created","timestamp":"…","data":{…}} Signed content is id.timestamp.body — the exact bytes, never a re-serialisation
One delivery on the wire. Three headers, and the reason each one exists.

If you write your own verifier, check it against the same file. The signed content is {id}.{timestamp}.{body} for both schemes. With a whsec_ secret it is HMAC-SHA256 over the secret's decoded bytes, base64-encoded, presented as v1,<signature>. With a whpk_ key it is ed25519 over the same bytes, presented as v1a,<signature> — that key verifies and cannot sign, which is why it is safe to hand to a partner or an auditor. An endpoint switching schemes sends both at once, so nobody has to change in the same second you do.

Never log a signing secret, and never put one in a URL. Rotate from the dashboard or the API — the overlap window means a rotation never drops a delivery.

Questions

[object Object]

Start delivering webhooks today

Point your webhooks at HookGet and watch the first delivery arrive, signed, in under a minute.

Create a free account Try the free webhook tester

10,000 deliveries a month free, no credit card. The free tier blocks rather than bills, so trying it cannot produce an invoice.