Learn how to use webhooks to integrate Doppler into your continuous delivery workflows.

Webhooks enable you to perform actions in third party systems when your secrets change. You can use them do things like automatically restart or redeploy your applications by triggering a continuous delivery pipeline.

When a webhook is triggered, Doppler will send a POST request to the specified URL. Webhooks will generally be delivered once, though exactly once delivery is not guaranteed.

Supported Platforms

Doppler's webhooks support triggering workflows on many different platforms. Some of the supported platforms include:

  • Azure DevOps Pipelines (docs)
  • Bitbucket Pipelines (docs)
  • CircleCI Pipeline (docs)
  • GitHub Actions (docs)
  • GitLab Pipeline (docs)
  • Google Cloud Build (docs)
  • Heroku (docs)
  • Netlify (docs)
  • Vercel (docs)

Creating a Webhook

  1. Navigate to a project, then click Webhooks from the project menu
  1. Click the + Add button which will open a drawer where you can configure the webhook.
  2. Add the webhook URL and choose which configs it will trigger for. Root configs are bolded in the selection list for easy identification
  3. Provide an optional JSON payload. The provided payload must be valid JSON. It does not yet support variables, replacement values, or any other dynamic data. Leave the payload field empty to use our default payload (documented below).
  4. Provide an optional signing secret. When a Secret value is provided, Doppler uses this to cryptographically sign each webhook request, enabling you to verify that the request originated from Doppler. See the Request Signing section to learn more. Configuring this field is highly encouraged when Authentication is set to None.
  1. Click the Save button. You'll now see it listed in the Active Webhooks list.
  1. You can edit the webhook by clicking the three-dot menu on the right and choosing Edit.

Default Payload

By default, your endpoint will receive a JSON payload with the following structure. Note that this payload will be different when a custom payload is configured.

typeCurrently alwaysconfig.secrets.update
configConfig object
projectProject object
workplaceWorkplace object
diffList of secret names that have been modified

Here is an example payload:

  "type": "config.secrets.update",
  "config": {
    "name": "dev",
    "root": true,
    "locked": true,
    "initial_fetch_at": "2023-01-01T00:00:00.000Z",
    "last_fetch_at": "2023-01-01T00:00:00.000Z",
    "created_at": "2023-01-01T00:00:00.000Z",
    "environment": "dev",
    "project": "backend",
    "slug": "c9748dae-de1c-4971-b00c-f539970fee24"
  "project": {
    "id": "backend",
    "slug": "backend",
    "name": "backend",
    "description": "",
    "created_at": "2023-01-01T00:00:00.000Z"
  "workplace": {
    "id": "9f0c0c0d5d4e240ce26e",
    "name": "My Workplace"
  "diff": {
    "added": ["FOO"],
    "removed": ["BAR"],
    "updated": ["BAZ"]

Verify Webhook With Request Signing

Doppler can optionally sign the webhook events by including a signature in each event’s X-Doppler-Signature header. This allows you to verify that the events were sent by Doppler, not by a third party.

To enable webhook verification, a value for Secret must be supplied when creating the webhook. The X-Doppler-Signature header will then contain a SHA256 hash of the request body prepended with sha256= (e.g. X-Doppler-Signature: sha256=724cd2c1110335a8ea6207f74cd74fb198a8e59ca1b7b39d67678e0f97df832e).

To verify the signature, create your own SHA256 hash of the request body using the Secret value. Then compare the header value against your own computed value using a timing-safe equality function. Here is an example of how you might accomplish this using Node.js.

const bodyParser = require("body-parser");
const crypto = require("crypto");
const express = require("express");

const app = express();
  limit: '200kb',

app.post('/webhooks/doppler', (req, res) => {
  const requestHMAC = req.header("X-Doppler-Signature");
  const secret = process.env.WEBHOOK_SECRET;
  const computedHMAC = `sha256=${crypto.createHmac('sha256', secret).update(JSON.stringify(req.body)).digest('hex')}`;
  const signatureMatches = crypto.timingSafeEqual(Buffer.from(requestHMAC), Buffer.from(computedHMAC));
  if (signatureMatches) {
    // do stuff

  res.sendStatus(signatureMatches ? 204 : 401);
require "sinatra"
require "json"
require "openssl"

set :port, ENV["PORT"] || 8081

post "/webhooks/doppler" do
  if valid_webhook_signature?(request)
    # do something
    status 200
    status 401

def valid_webhook_signature?(request)
  data = request.body.read
  digest = OpenSSL::Digest.new("sha256")

  computedHMAC = OpenSSL::HMAC.hexdigest(digest, key, data)
  requestHMAC = request.env["HTTP_X_DOPPLER_SIGNATURE"].delete_prefix("sha256=")

  return OpenSSL.secure_compare(requestHMAC, computedHMAC)