Skip to main content

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 event
  • webhook-timestamp - The unix integer timestamp of the event
  • webhook-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 to gb_payload if left blank)
tip

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.