Handling webhooks

This page explains how webhook events from Hypertune should be handled by your server.

Request structure

When an event occurs in your project, we'll notify all the subscribed active webhooks.

We do this by sending them each a POST request to their payload URLs. This request has a body that contains information about the event, for example:

{
  "id": "QAq7JB7nE5tY8mS99qsmM",
  "createdAt": "2023-04-17T12:02:26.095Z",
  "type": "NEW_COMMIT",
  "project": {
    "id": "2489",
    "name": "demo-webhook project"
  },
  "commit": {
    "id": "2792",
    "message": "v5"
  },
  "user": {
    "id": "2431",
    "displayName": "Adam Jones",
    "email": "adam@hypertune.com"
  }
}

Additionally, it will include the X-Hypertune-Signature header which will be the HMAC-SHA-256 digest in hexadecimal format of the raw body, using the webhook secret.

We will retry webhook sends with exponential backoff, until a limit. As such, your implementation should be idempotent and should handle missing hooks.

Processing webhooks

To process a request you should:

  1. Verify it is from Hypertune. To do this, validate that the X-Hypertune-Signature matches the definition above, using the secret you configured when setting up your webhook.

  2. Within 10 seconds, respond with a successful status code: one in the range 200-299 inclusive.

JavaScript example

import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";

// Do not hardcode in a production application
const WEBHOOK_SECRET = "1de87274cc2ddba774cbcec5a9ad8727";

const app = express();

// If you want to use body parser JSON, do something like this:
// https://github.com/stripe/stripe-node/issues/341#issuecomment-304733080
app.use(express.raw({ type: "*/*" }));

app.post("/", (req, res) => {
  const actualSignature = [req.headers["x-hypertune-signature"]].flat()[0];
  if (!actualSignature) {
    console.log("Rejected webhook request with missing signature");
    res.status(401).send("Missing X-Hypertune-Signature header");
    return;
  }

  const expectedSignature = createHmac("sha256", WEBHOOK_SECRET)
    .update(req.body.toString())
    .digest("hex");
  if (
    actualSignature.length !== expectedSignature.length ||
    !timingSafeEqual(
      Buffer.from(actualSignature),
      Buffer.from(expectedSignature)
    )
  ) {
    console.log("Rejected webhook request with bad signature");
    res.status(403).send("Bad signature");
    return;
  }

  console.log("Received valid webhook event");
  const data = JSON.parse(req.body.toString());
  // TODO: do something interesting with the data here

  res.sendStatus(204);
});

const port = 8080;

app.listen(port);
console.log(`Server listening at http://localhost:${port}`);

Last updated