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 ​