HookGet Open dashboard

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.

If you write your own verifier, check it against the same file. The signed content is {id}.{timestamp}.{body}, HMAC-SHA256 with the secret's decoded bytes as the key, base64-encoded, presented as v1,<signature>.

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]