SDK Webhooks
GrowthBook has SDK-based webhooks that trigger a script on your server whenever something changes within GrowthBook which will affect that SDK.
Adding a Webhook
When logged into GrowthBook as an admin, navigate to SDK Connections .
Under the SDK Webhooks section you can add a webhook endpoint and select with method you would like to be notified with POST, PUT, GET, DELETE, PURGE
.
Once a SDK webhook is created you will be able to view the secret and when the last time it was fired as well as if there was an error.
VPCs and Firewalls
If your webhook endpoint is behind a firewall and you are using GrowthBook Cloud, make sure to whitelist the ip address 52.70.79.40
.
Verify Signatures
SDK Webhook payloads are signed with a shared secret so you can verify they actually came from GrowthBook.
Standard Webhooks
We follow the Standard Webhooks specification, so you can use any of their SDKs to verify our webhook signatures.
import { Webhook } from "standardwebhooks"
const wh = new Webhook(base64_secret);
wh.verify(webhook_body, webhook_headers);
Custom Verification
Webhook requests sent to your endpoint include 3 headers:
webhook-id
- The unique id for this eventwebhook-timestamp
- The unix integer timestamp of the eventwebhook-signature
- The signature (format described below)
To create the signature, we concatenate the webhook-id
, the webhook-timestamp
, and the body contents, all separated by dots (.
). Then, we create an HMAC SHA-256 hash of this using the shared secret.
What we set in the webhook-signature
header is the hashing algorithm identifier for HMAC SHA-256 (v1
), followed by a comma (,
), followed by the base64-encoded hash from above. For example:
v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
You can find the shared secret via SDK Configuration → SDK Connections, choosing the connection, and viewing your webhook's details.
Here is example code in NodeJS for verifying the signature. Other languages should be similar:
const crypto = require("crypto");
const express = require("express");
const bodyParser = require("body-parser");
// Retrieve from GrowthBook SDK connection settings
const GROWTHBOOK_WEBHOOK_SECRET = "wk_123A5341464B3A13";
const port = 1337;
const app = express();
app.post(
"/webhook",
bodyParser.raw({ type: "application/json" }),
(req, res) => {
// If there is no body sent, use an empty string to compute the signature
const body = req.body || "";
// Get the request headers
const id = req.get("webhook-id");
const timestamp = req.get("webhook-timestamp");
const rawSignature = req.get("webhook-signature") || "";
// Remove the "v1," prefix from the signature for comparison
const signature = rawSignature.split(",")[1];
if (id && timestamp && signature) {
// Base64 encode the secret
const base64_secret = btoa(GROWTHBOOK_WEBHOOK_SECRET);
// Compute the signature
const computed = crypto
.createHmac("sha256", base64_secret)
.update(`${id}.${timestamp}.${body}`)
.digest("base64");
if (!crypto.timingSafeEqual(Buffer.from(computed), Buffer.from(signature))) {
throw new Error("Invalid signature");
}
} else {
throw new Error("Missing signature headers");
}
const parsedBody = JSON.parse(body);
const payload = parsedBody.data.payload;
// TODO: Do something with the webhook data
// Make sure to respond with a 200 status code
res.status(200).send("");
}
);
app.listen(port, () => {
console.log(`Webhook endpoint listening on port ${port}`);
});
Errors and Retries
If your endpoint returns any HTTP status besides 200
, the webhook will be considered failed.
Webhooks are retried up to 2 additional times with an exponential back-off between attempts.
You can view the status of your webhooks in the GrowthBook app under SDK Connections.
Supported HTTP Types
- POST
- PUT
- GET
- DELETE
- PURGE
Sending Payload
If you opt to include a payload in your webhook request, the body will be in the following format:
{
"type": "payload.changed",
"timestamp": "2024-04-03T01:54:20.449Z",
"data":{
"payload":"{\"features\":{\"my-feature\":{\"defaultValue\":true}}}"
}
}
}
The data.payload
object contains the exact JSON format that our SDKs are expecting. For example, you can pass this directly into the JavaScript SDK:
const payload = JSON.parse(parsedBody.data.payload);
const gb = new GrowthBook();
await gb.init({
payload: payload
});