1. Docs
  2. Pulumi Cloud
  3. Webhooks

Pulumi Cloud webhooks

Pulumi Webhooks is a feature available on the Pulumi Team, Enterprise and Business Critical editions. To try it out, start a trial now.

Pulumi Webhooks allow you to notify external services of events happening within your Pulumi organization. For example, you can trigger a notification whenever a stack is updated. Whenever an event occurs, Pulumi will send an HTTP POST request to all registered webhooks. The webhook can then be used to emit a notification, start running integration tests, or even update additional stacks.

Webhooks can be used for pretty much anything you want, and are the foundation of most ChatOps workflows.

Management

Webhooks can be attached to either a stack or an organization. Stack webhooks will be notified of events specific to the stack. Organization webhooks will be notified for events happening within each of the organization’s stacks.

The Webhooks management page is on the Stack or Organization Settings tab.

Organization webhooks

Create a Webhook

Pulumi Webhooks may be created through the UI using the steps outlined below, or by using the Webhook resource from the Pulumi provider, or by using the API directly.

Create an Organization Webhook

  1. Navigate to Settings > Webhooks.
  2. Select Create webhook.
  3. Select between a slack-formatted or generic JSON webhooks.
  4. If you selected Slack, you will be prompted to provide a Slack webhook URL and a display name.
  5. If you selected Webhook, provide a display name, payload URL, and optionally a secret.
  6. Choose between receiving all events or only receiving specific events using the filters menu.

Create a Stack Webhook

  1. Navigate to the stack.
  2. Then navigate to Settings > Webhooks
  3. Select Create webhook.
  4. Select between a slack-formatted webhook or generic JSON webhooks.
  5. If you selected Slack, you will be prompted to provide a Slack webhook URL and a display name.
  6. If you selected Webhook, provide a display name, payload URL, and optionally a secret.
  7. Choose between receiving all events or only receiving specific events using the filters menu.

Event Filtering

Event filtering allows you to choose which events should be delivered to each webhook. You may choose to receive all events, or filter to specific events (only failures, only deployment events, etc.). The following table describes the various event filters available and the context in which they are relevant.

FilterEvent KindWebhook TypeTriggered
stack_createdstackOrganization webhooks onlyWhen a stack is created.
stack_deletedstackOrganization webhooks onlyWhen a stack is deleted.
preview_succeededstack_previewBothWhen a stack preview succeeds.
preview_failedstack_previewBothWhen a stack preview fails.
update_succeededstack_updateBothWhen a stack update succeeds.
update_failedstack_updateBothWhen a stack update fails.
destroy_succeededstack_updateBothWhen a stack destroy succeeds.
destroy_failedstack_updateBothWhen a stack destroy fails.
refresh_succeededstack_updateBothWhen a stack refresh succeeds.
refresh failedstack_updateBothWhen a stack refresh fails.
deployment_queueddeploymentBothWhen a deployment is queued.
deployment_starteddeploymentBothWhen a deployment starts running.
deployment_succeededdeploymentBothWhen a deployment succeeds.
deployment_faileddeploymentBothWhen a deployment fails.

Webhook Formats

When creating a webhook, you can choose between the generic JSON webhook payload or slack formatted events.

Slack-formatted Webhooks

Slack-formatted webhooks allow you to seamlessly integrate notifications about your Pulumi stacks and organizations into your Slack workspace by simply providing a Slack incoming webhook URL and optionally choosing which events you want delivered using event filters.

You can either create your own Slack app (or use an existing one you may already have installed in your workspace), or follow the link below to quickly get started with a pre-defined Slack app manifest.

Generic JSON Webhooks

When using generic JSON webhooks, Pulumi will send an HTTP POST request to all registered webhooks. The webhook can then be used to emit a notification, start running integration tests, or even update additional stacks.

If a secret is provided, webhook deliveries will contain a signature in the HTTP request header that can be used to authenticate messages as coming from the Pulumi Cloud.

Payload Examples

Each webhook payload has a format specific to the payload being emitted. Every payload will contain a sender, organization, and stack reference as appropriate. For examples of specific payloads, see Payload Reference below.

Each webhook will contain a user field, which is the user who requested the action, an organization which is the organization name, and a URL for the event. It will also contain the stackName for the stack which was modified when applicable.

Stack Creation
{
	"user": {
		"name": "Morty Smith",
		"githubLogin": "morty",
		"avatarUrl": "https://crazy-adventures.net/morty.png"
	},
	"organization": {
		"name": "Crazy Adventures",
		"githubLogin": "crazy-adventures",
		"avatarUrl": "https://crazy-adventures.net/logo.png"
	},
	"action": "created",
	"projectName": "website",
	"stackName": "website-prod"
}
Stack Update
{
	"user": {
		"name": "Morty Smith",
		"githubLogin": "morty",
		"avatarUrl": "https://crazy-adventures.net/morty.png"
	},
	"organization": {
		"name": "Crazy Adventures",
		"githubLogin": "crazy-adventures",
		"avatarUrl": "https://crazy-adventures.net/logo.png"
	},
	"projectName": "website",
	"stackName": "website-prod",
	"updateUrl": "https://app.pulumi.com/crazy-adventures/website/website-prod/updates/42",
	"kind": "refresh",
	"result": "succeeded",
	"resourceChanges": {
		"update": 3,
		"delete": 1,
		"update-replace": 2
	},
    "isPreview": false
}
Stack Preview
{
	"user": {
		"name": "Morty Smith",
		"githubLogin": "morty",
		"avatarUrl": "https://crazy-adventures.net/morty.png"
	},
	"organization": {
		"name": "Crazy Adventures",
		"githubLogin": "crazy-adventures",
		"avatarUrl": "https://crazy-adventures.net/logo.png"
	},
	"projectName": "website",
	"stackName": "website-prod",
	"updateUrl": "https://app.pulumi.com/crazy-adventures/website/website-prod/previews/11bf162b-d9d5-4715-8f88-20dcd0e0b167",
	"kind": "update",
	"result": "failed",
	"resourceChanges": {
		"update": 3,
		"delete": 1,
		"update-replace": 2
	},
    "isPreview": true
}
Deployment
{
	"user": {
		"name": "Morty Smith",
		"githubLogin": "morty",
		"avatarUrl": "https://crazy-adventures.net/morty.png"
	},
	"organization": {
		"name": "Crazy Adventures",
		"githubLogin": "crazy-adventures",
		"avatarUrl": "https://crazy-adventures.net/logo.png"
	},
	"projectName": "website",
	"stackName": "website-prod",
	"deploymentUrl": "https://app.pulumi.com/crazy-adventures/website/website-prod/deployments/127",
    "version": 127,
	"operation": "update",
	"status": "running"
}

Headers

Payloads contain several headers.

HeaderDescription
Pulumi-Webhook-IDUnique ID for each webhook sent which you can reference when looking at delivery logs in the Pulumi Cloud.
Pulumi-Webhook-KindThe kind of webhook event, e.g. stack_update.
Pulumi-Webhook-SignatureOnly set if the webhook has a shared secret. HMAC hex digest of the request payload, using the sha256 hash function and the webhook secret as the HMAC key.

The following snippets show how to compute and verify the webhook signature. For examples in other languages, see danharper/hmac-examples.

var crypto = require('crypto');

const sharedSecret = ...
const payload = req.body.toString();

var hmacAlg = crypto.createHmac('sha256', sharedSecret);
var expectedSignature = hmac.update(payloadBody).digest('hex');
import * as crypto from "crypto";

const sharedSecret = ...
const payload = req.body.toString();

const hmacAlg = crypto.createHmac("sha256", stackConfig.sharedSecret);
const hmac = hmacAlg.update(payload).digest("hex");

const result = crypto.timingSafeEqual(Buffer.from(webhookSig), Buffer.from(hmac));
import hashlib
import hmac
import base64

message = bytes('...', 'utf-8')
secret = bytes('...', 'utf-8')

hash = hmac.new(secret, message, hashlib.sha256)
hash.hexdigest()

expected_signature = base64.b64encode(hash.digest())
func computeSignature(payload []byte, secret string) string {
	mac := hmac.New(sha256.New, []byte(secret))
	_, err := mac.Write(payload)
	contract.AssertNoErrorf(err, "computing HMAC digest")
	return fmt.Sprintf("%x", mac.Sum(nil))
}

Additional Resources