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 and select an existing SDK Connection.
Under the SDK Webhooks section you can add a webhook.
There are built-in webhook types for syncing to Cloudflare KV, Fastly KV, and Vercel Edge Config. For everything else, you can configure a generic HTTP Endpoint to be hit.
Once a SDK webhook is created you will be able to view the status and fire a test event.
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) {
// Compute the signature
const computed = crypto
.createHmac("sha256", GROWTHBOOK_WEBHOOK_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 Methods
- GET
- POST
- PUT
- DELETE
- PURGE
- PATCH
Payload Format
For all methods other than GET
, you may send a payload body. By default, webhooks will send in the "Standard" format.
Standard
Follows the Standard Webhooks specification. Inludes a JSON-encoded SDK Payload in the data.payload
field.
Example payload:
{
"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
});
Standard (no SDK Payload)
Same as above, but without the data.payload
field.
Example payload:
{
"type": "payload.changed",
"timestamp": "2024-04-03T01:54:20.449Z",
}
SDK Payload
Sends the raw SDK Payload using the same format as our SDK features endpoint. This is usually the correct format if you are using the webhook to set a cache value or assigning key/value storage.
Example payload:
{"features":{"my-feature":{"defaultValue":true}}}
Vercel Edge Config
Formats the body to work directly with Vercel's Edge Config API.
Example payload:
{
"items": [
{
"operation": "upsert",
"key": "gb_payload",
"value": "{\"features\":{\"my-feature\":{\"defaultValue\":true}}}"
}
]
}
In order for this to work properly, you must specify the following options as well, replacing the your_*
placeholders with actual values.
- Endpoint URL =
https://api.vercel.com/v1/edge-config/your_edge_config_id_here/items
- HTTP Method =
PATCH
- Headers =
{ "Authorization": "Bearer your_vercel_api_token_here" }
- Payload Key =
your_value_key_here
(defaults togb_payload
if left blank)
If you are updating an Edge Config item owned by a team, add ?teamId=your_team_id_here
to the end of your Endpoint URL.