Skip to content

Webhooks

  • Webhooks are the "glue" that connect your agent to your users. Note that to use webhooks (rather than websockets), you will need a publically-accessible hostname to associate with your agent.

  • If you use a secret with your webhook (which you should), see Securing Webhooks for information about how to validate the secret passed on payloads.

  • In the SpeedyBot Gargage, you can create, inspect, and delete webhooks associated with your bot's access token.

Securing Webhooks

Webhook secrets

To help secure your agent, you can (and SHOULD!!) add a "secret" when creating your webhooks.

If you add a secret when creating a webhook on each incoming request will receive a hashed version of the request body under the header X-Spark-Signature

Bottom line: DO THIS. With your webhook secret you can take the SHA-1 representation of the request body and if it matches the signature on the header proceed otherwise simply discard the request.

Note: All of the samples in the examples directory of the SpeedyBot repo have reference implementations of validating webhooks in the manner appropriate for each platform.

Reference implementations

NodeJS (Lambda, standard server, etc)

ts
import crypto from "crypto";

export const validateWebhook = <T = any>(
  signature: string,
  secret: string,
  requestBody: T
): boolean => {
  const hmac = crypto.createHmac("sha1", secret);
  if (typeof requestBody === "string") {
    hmac.update(requestBody);
  } else {
    hmac.update(JSON.stringify(requestBody));
  }
  const isValid = hmac.digest("hex") === signature;
  return isValid;
};
js
const crypto = require("crypto");

// validate signature
export const validateWebhook = (secret, signature, requestData) => {
  const hmac = crypto.createHmac("sha1", secret);
  if (typeof requestData === "string") {
    hmac.update(requestData);
  } else {
    hmac.update(JSON.stringify(requestData));
  }

  const isValid = hmac.digest("hex") === signature;
  return isValid;
};

Sample data

js
const requestBody = {
  data: {
    a: 1,
    b: 2,
    c: {
      d: 3,
    },
  },
  signature: "01e0cb6a53731b9615b483335d77d97023410c72",
};
const secret = "myBongoSecret";

const res = validateWebhook(secret, requestBody.signature, requestBody.data);

console.log("is valid?", res);

Web Crypto (for "Workers", V8 Isolates)

js
const validateWebhook = async (secret, signature, requestData) => {
  const stringyBody =
    typeof requestData !== "string" ? JSON.stringify(requestData) : requestData;
  const algo = {
    name: "HMAC",
    hash: "SHA-1",
  };
  const enc = {
    name: "UTF-8",
  };
  const hmacKey = await crypto.subtle.importKey(
    "raw",
    new TextEncoder().encode(secret),
    algo,
    false,
    ["sign"]
  );
  const hmacData = await crypto.subtle.sign(
    algo,
    hmacKey,
    new TextEncoder().encode(stringyBody)
  );

  const bufferToHex = (buffer) => {
    return Array.prototype.map
      .call(new Uint8Array(buffer), (x) => ("00" + x.toString(16)).slice(-2))
      .join("");
  };
  const hmacDataHex = bufferToHex(hmacData);
  return hmacDataHex === signature;
};

const requestBody = {
  data: {
    a: 1,
    b: 2,
    c: {
      d: 3,
    },
  },
  signature: "01e0cb6a53731b9615b483335d77d97023410c72",
};
const secret = "myBongoSecret";

const res = validateWebhook(
  secret,
  requestBody.signature,
  requestBody.data
).then((val) => console.log("is valid?", val));

Resources