Repository: honeycombio/terraform-provider-honeycombio
Pull request #594 (2024-12-20 13:54:05)

feat(webhook recipient): notification variables
This adds the notification variable functionality to custom webhooks.



Patch:
diff --git a/client/recipient.go b/client/recipient.go
index e5e90e4..33b01e0 100644
--- a/client/recipient.go
+++ b/client/recipient.go
@@ -71,8 +71,14 @@ type RecipientDetails struct {
 	WebhookPayloads *WebhookPayloads `json:"webhook_payloads,omitempty"`
 }
 
+type NotificationVariable struct {
+	Name  string `json:"name"`
+	Value string `json:"value"`
+}
+
 type NotificationRecipientDetails struct {
-	PDSeverity PagerDutySeverity `json:"pagerduty_severity,omitempty"`
+	Variables  []NotificationVariable `json:"variables,omitempty"`
+	PDSeverity PagerDutySeverity      `json:"pagerduty_severity,omitempty"`
 }
 
 type WebhookPayloads struct {
diff --git a/docs/data-sources/recipient.md b/docs/data-sources/recipient.md
index cc6f876..1629607 100644
--- a/docs/data-sources/recipient.md
+++ b/docs/data-sources/recipient.md
@@ -88,4 +88,4 @@ In addition to all arguments above, the following attributes are exported:
 * `secret` - (Sensitive) The webhook recipient's secret -- if of type `webhook`.
 * `url` - The webhook recipient's URL - if of type `webhook`, `msteams` or `msteams_workflow`.
 * `integration_key` - (Sensitive) The PagerDuty recipient's integration key -- if of type `pagerduty`.
-* `integration_name` - The PagerDuty recipient's inregration name -- if of type `pagerduty`.
+* `integration_name` - The PagerDuty recipient's integration name -- if of type `pagerduty`.
diff --git a/docs/resources/burn_alert.md b/docs/resources/burn_alert.md
index 0321338..bb947f6 100644
--- a/docs/resources/burn_alert.md
+++ b/docs/resources/burn_alert.md
@@ -100,6 +100,47 @@ resource "honeycombio_burn_alert" "example_alert" {
 }
 ```
 
+### Example - Exhaustion Time Burn Alert with Webhook Recipient and Notification Variable
+```hcl
+variable "dataset" {
+  type = string
+}
+
+variable "slo_id" {
+  type = string
+}
+
+data "honeycombio_recipient" "custom_webhook" { 
+    type = "webhook"
+
+    detail_filter {
+        name  = "name"
+        value = "My Custom Webhook"
+    }
+}
+
+resource "honeycombio_burn_alert" "example_alert" { 
+    exhaustion_minutes = 60
+    description        = "Burn alert description"
+    dataset            = var.dataset
+    slo_id             = var.slo_id
+
+    dataset = var.dataset
+    slo_id  = var.slo_id
+
+    recipient {
+      id = data.honeycombio_recipient.custom_webhook.id
+
+      notification_details {
+          variable {
+              name = "severity"
+              value = "info"
+          }
+      }
+    }
+}
+```
+
 
 ## Argument Reference
 
@@ -130,7 +171,9 @@ Each burn alert configuration may have one or more `recipient` blocks, which eac
 * `type` - (Optional) The type of the recipient, allowed types are `email`, `pagerduty`, `msteams`, `slack` and `webhook`. Should not be used in combination with `id`.
 * `target` - (Optional) Target of the recipient, this has another meaning depending on the type of recipient (see the table below). Should not be used in combination with `id`.
 * `id` - (Optional) The ID of an already existing recipient. Should not be used in combination with `type` and `target`.
-* `notification_details` - (Optional) a block of additional details to send along with the notification. The only supported option currently is `pagerduty_severity` which has a default value of `critical` but can be set to one of `info`, `warning`, `error`, or `critical` and must be used in combination with a PagerDuty recipient.
+* `notification_details` - (Optional) a block of additional details to send along with the notification. Supported details are described below.
+  * `pagerduty_severity` - (Optional) Indicates the severity of an alert and has a default value of `critical` but can be set to one of `info`, `warning`, `error`, or `critical` and must be used in combination with a PagerDuty recipient.
+  * `variable` - (Optional) Up to 10 configuration blocks with a `name` and a `value` to override the default variable value. Must be used in combination with a Webhook recipient that already has a variable with the same name configured.
 
 | Type      | Target              |
 |-----------|---------------------|
diff --git a/docs/resources/trigger.md b/docs/resources/trigger.md
index 6a9c0a0..cd6530c 100644
--- a/docs/resources/trigger.md
+++ b/docs/resources/trigger.md
@@ -60,7 +60,7 @@ variable "dataset" {
   type = string
 }
 
-data "honeycombio_recipient" "pd-prod" {
+data "honeycombio_recipient" "pd_prod" {
   type = "pagerduty"
 
   detail_filter {
@@ -97,7 +97,7 @@ resource "honeycombio_trigger" "example" {
   }
 
   recipient {
-    id = data.honeycombio_recipient.pd-prod.id
+    id = data.honeycombio_recipient.pd_prod.id
 
     notification_details {
       pagerduty_severity = "warning"
@@ -116,6 +116,71 @@ resource "honeycombio_trigger" "example" {
   }
 }
 ```
+### Example - Example with Webhook Recipient and Notification Variable
+```hcl
+variable "dataset" {
+    type = string
+}
+
+data "honeycombio_recipient" "custom_webhook" {
+    type = "webhook"
+
+    detail_filter {
+        name  = "name"
+        value = "My Custom Webhook"
+    }
+}
+
+data "honeycombio_query_specification" "example" {
+    calculation {
+        op     = "AVG"
+        column = "duration_ms"
+    }
+
+    filter {
+        column = "trace.parent_id"
+        op     = "does-not-exist"
+    }
+}
+
+resource "honeycombio_trigger" "example" {
+    name        = "Requests are slower than usual"
+    description = "Average duration of all requests for the last 10 minutes."
+
+    query_json = data.honeycombio_query_specification.example.json
+    dataset    = var.dataset
+
+    frequency = 600 // in seconds, 10 minutes
+
+    threshold {
+        op             = ">"
+        value          = 1000
+        exceeded_limit = 3
+    }
+
+    recipient {
+        id = data.honeycombio_recipient.custom_webhook.id
+
+        notification_details {
+            variable {
+                name = "severity"
+                value = "info"
+            }
+        }
+    }
+
+    evaluation_schedule {
+        start_time = "13:00"
+        end_time   = "21:00"
+
+        days_of_week = [
+            "monday",
+            "wednesday",
+            "friday"
+        ]
+    }
+}
+```
 
 ## Argument Reference
 
@@ -166,7 +231,9 @@ Cannot not be used in combination with `id`.
 * `target` - (Optional) Target of the trigger recipient, this has another meaning depending on the type of recipient (see the table below).
 Cannot not be used in combination with `id`.
 * `id` - (Optional) The ID of an already existing recipient. Cannot not be used in combination with `type` and `target`.
-* `notification_details` - (Optional) a block of additional details to send along with the notification. The only supported option currently is `pagerduty_severity` which has a default value of `critical` but can be set to one of `info`, `warning`, `error`, or `critical` and must be used in combination with a PagerDuty recipient.
+* `notification_details` - (Optional) a block of additional details to send along with the notification.
+  * `pagerduty_severity` - (Optional) Indicates the severity of an alert and has a default value of `critical` but can be set to one of `info`, `warning`, `error`, or `critical` and must be used in combination with a PagerDuty recipient.
+  * `variable` - (Optional) Up to 10 configuration blocks with a `name` and a `value` to override the default variable value. Must be used in combination with a Webhook recipient that already has a variable with the same name configured.
 
 Type      | Target
 ----------|-------------------------
diff --git a/internal/helper/validation/valid_notification_variables.go b/internal/helper/validation/valid_notification_variables.go
new file mode 100644
index 0000000..58b2ac9
--- /dev/null
+++ b/internal/helper/validation/valid_notification_variables.go
@@ -0,0 +1,51 @@
+package validation
+
+import (
+	"context"
+
+	"github.com/hashicorp/terraform-plugin-framework/path"
+	"github.com/hashicorp/terraform-plugin-framework/schema/validator"
+
+	"github.com/honeycombio/terraform-provider-honeycombio/internal/models"
+)
+
+var _ validator.Set = notificationVariablesValidator{}
+
+type notificationVariablesValidator struct{}
+
+func (v notificationVariablesValidator) Description(_ context.Context) string {
+	return "value must be a valid set of webhook variables"
+}
+
+func (v notificationVariablesValidator) MarkdownDescription(ctx context.Context) string {
+	return v.Description(ctx)
+}
+
+func (v notificationVariablesValidator) ValidateSet(ctx context.Context, request validator.SetRequest, response *validator.SetResponse) {
+	if request.ConfigValue.IsNull() || request.ConfigValue.IsUnknown() {
+		return
+	}
+
+	// variable names cannot be duplicated
+	var variables []models.NotificationVariableModel
+	response.Diagnostics.Append(request.ConfigValue.ElementsAs(ctx, &variables, false)...)
+
+	duplicateMap := make(map[string]bool)
+	for i, v := range variables {
+		name := v.Name.ValueString()
+		if duplicateMap[name] {
+			response.Diagnostics.AddAttributeError(
+				path.Root("variable").AtListIndex(i).AtName("name"),
+				"Conflicting configuration arguments",
+				"cannot have more than one \"variable\" with the same \"name\"",
+			)
+		}
+		duplicateMap[name] = true
+	}
+}
+
+// ValidNotificationVariables determines if the provided recipient notification variables are valid.
+// Today this means no variable names are duplicated.
+func ValidNotificationVariables() validator.Set {
+	return notificationVariablesValidator{}
+}
diff --git a/internal/models/notification_recipients.go b/internal/models/notification_recipients.go
index e6ef1ff..bcc1d6d 100644
--- a/internal/models/notification_recipients.go
+++ b/internal/models/notification_recipients.go
@@ -23,8 +23,20 @@ var NotificationRecipientAttrType = map[string]attr.Type{
 
 type NotificationRecipientDetailsModel struct {
 	PDSeverity types.String `tfsdk:"pagerduty_severity"`
+	Variables  types.Set    `tfsdk:"variable"`
 }
 
 var NotificationRecipientDetailsAttrType = map[string]attr.Type{
 	"pagerduty_severity": types.StringType,
+	"variable":           types.SetType{ElemType: types.ObjectType{AttrTypes: NotificationVariableAttrType}},
+}
+
+type NotificationVariableModel struct {
+	Name  types.String `tfsdk:"name"`
+	Value types.String `tfsdk:"value"`
+}
+
+var NotificationVariableAttrType = map[string]attr.Type{
+	"name":  types.StringType,
+	"value": types.StringType,
 }
diff --git a/internal/provider/notification_recipients.go b/internal/provider/notification_recipients.go
index e48f69a..ae57b3f 100644
--- a/internal/provider/notification_recipients.go
+++ b/internal/provider/notification_recipients.go
@@ -6,6 +6,7 @@ import (
 
 	"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
 	"github.com/hashicorp/terraform-plugin-framework-validators/objectvalidator"
+	"github.com/hashicorp/terraform-plugin-framework-validators/setvalidator"
 	"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
 	"github.com/hashicorp/terraform-plugin-framework/attr"
 	"github.com/hashicorp/terraform-plugin-framework/diag"
@@ -13,11 +14,14 @@ import (
 	"github.com/hashicorp/terraform-plugin-framework/resource/schema"
 	"github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier"
 	"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringdefault"
 	"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
 	"github.com/hashicorp/terraform-plugin-framework/schema/validator"
 	"github.com/hashicorp/terraform-plugin-framework/types"
 	"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
 
+	"github.com/honeycombio/terraform-provider-honeycombio/internal/helper/validation"
+
 	"github.com/honeycombio/terraform-provider-honeycombio/client"
 	"github.com/honeycombio/terraform-provider-honeycombio/internal/helper"
 	"github.com/honeycombio/terraform-provider-honeycombio/internal/helper/modifiers"
@@ -83,6 +87,36 @@ func notificationRecipientSchema(allowedTypes []client.RecipientType) schema.Set
 								},
 							},
 						},
+						Blocks: map[string]schema.Block{
+							"variable": schema.SetNestedBlock{
+								Description: "The variables to set with the webhook notification.",
+								Validators: []validator.Set{
+									setvalidator.SizeAtMost(10),
+									validation.ValidNotificationVariables(),
+								},
+								NestedObject: schema.NestedBlockObject{
+									Attributes: map[string]schema.Attribute{
+										"name": schema.StringAttribute{
+											Required:    true,
+											Description: "The name of the variable",
+											Validators: []validator.String{
+												stringvalidator.LengthBetween(1, 64),
+												stringvalidator.RegexMatches(webhookTemplateNameRegex, "must be an alphanumeric string beginning with a lowercase letter"),
+											},
+										},
+										"value": schema.StringAttribute{
+											Description: "The value of the variable",
+											Optional:    true,
+											Computed:    true,
+											Default:     stringdefault.StaticString(""),
+											Validators: []validator.String{
+												stringvalidator.LengthAtMost(256),
+											},
+										},
+									},
+								},
+							},
+						},
 					},
 				},
 			},
@@ -155,8 +189,10 @@ func expandNotificationRecipients(ctx context.Context, set types.Set, diags *dia
 			if diags.HasError() {
 				return nil
 			}
+
 			rcpt.Details = &client.NotificationRecipientDetails{
 				PDSeverity: client.PagerDutySeverity(details[0].PDSeverity.ValueString()),
+				Variables:  expandNotificationVariables(ctx, details[0].Variables, diags),
 			}
 		}
 		clientRecips[i] = rcpt
@@ -207,7 +243,8 @@ func notificationRecipientModelToObjectValue(ctx context.Context, r models.Notif
 		if diags.HasError() {
 			return basetypes.ObjectValue{}
 		}
-		detailsObjVal, d := types.ObjectValue(models.NotificationRecipientDetailsAttrType, map[string]attr.Value{"pagerduty_severity": details[0].PDSeverity})
+
+		detailsObjVal, d := types.ObjectValue(models.NotificationRecipientDetailsAttrType, map[string]attr.Value{"pagerduty_severity": details[0].PDSeverity, "variable": details[0].Variables})
 		diags.Append(d...)
 		result, d = types.ListValueFrom(ctx, types.ObjectType{AttrTypes: models.NotificationRecipientDetailsAttrType}, []attr.Value{detailsObjVal})
 		diags.Append(d...)
@@ -236,7 +273,7 @@ func notificationRecipientToModel(ctx context.Context, r client.NotificationReci
 func notificationRecipientDetailsToList(ctx context.Context, details *client.NotificationRecipientDetails, diags *diag.Diagnostics) basetypes.ListValue {
 	var result basetypes.ListValue
 	if details != nil {
-		detailsObj := map[string]attr.Value{"pagerduty_severity": types.StringValue(string(details.PDSeverity))}
+		detailsObj := map[string]attr.Value{"pagerduty_severity": types.StringValue(string(details.PDSeverity)), "variable": flattenNotificationVariables(ctx, details.Variables, diags)}
 		objVal, d := types.ObjectValue(models.NotificationRecipientDetailsAttrType, detailsObj)
 		diags.Append(d...)
 		result, d = types.ListValueFrom(ctx, types.ObjectType{AttrTypes: models.NotificationRecipientDetailsAttrType}, []attr.Value{objVal})
@@ -247,3 +284,49 @@ func notificationRecipientDetailsToList(ctx context.Context, details *client.Not
 
 	return result
 }
+
+func notificationVariableToObjectValue(v client.NotificationVariable, diags *diag.Diagnostics) basetypes.ObjectValue {
+	variableObj := map[string]attr.Value{
+		"name":  types.StringValue(v.Name),
+		"value": types.StringValue(v.Value),
+	}
+	varObjVal, d := types.ObjectValue(models.NotificationVariableAttrType, variableObj)
+	diags.Append(d...)
+
+	return varObjVal
+}
+
+func flattenNotificationVariables(ctx context.Context, vars []client.NotificationVariable, diags *diag.Diagnostics) types.Set {
+	if len(vars) == 0 || vars == nil {
+		return types.SetNull(types.ObjectType{AttrTypes: models.NotificationVariableAttrType})
+	}
+
+	var notifVarValues []attr.Value
+	for _, v := range vars {
+		notifVarValues = append(notifVarValues, notificationVariableToObjectValue(v, diags))
+	}
+	notifVarResult, d := types.SetValueFrom(ctx, types.ObjectType{AttrTypes: models.WebhookHeaderAttrType}, notifVarValues)
+	diags.Append(d...)
+
+	return notifVarResult
+}
+
+func expandNotificationVariables(ctx context.Context, set types.Set, diags *diag.Diagnostics) []client.NotificationVariable {
+	var notifVars []models.NotificationVariableModel
+	diags.Append(set.ElementsAs(ctx, &notifVars, false)...)
+	if diags.HasError() {
+		return nil
+	}
+
+	clientNotifVars := make([]client.NotificationVariable, len(notifVars))
+	for i, v := range notifVars {
+		notifVar := client.NotificationVariable{
+			Name:  v.Name.ValueString(),
+			Value: v.Value.ValueString(),
+		}
+
+		clientNotifVars[i] = notifVar
+	}
+
+	return clientNotifVars
+}


Test patch:
diff --git a/internal/provider/burn_alert_resource_test.go b/internal/provider/burn_alert_resource_test.go
index 56c8280..adf68c5 100644
--- a/internal/provider/burn_alert_resource_test.go
+++ b/internal/provider/burn_alert_resource_test.go
@@ -119,6 +119,43 @@ func TestAcc_BurnAlertResource_exhaustionTimeBasic(t *testing.T) {
 	})
 }
 
+func TestAcc_BurnAlertResource_exhaustionTimeBasicWebhookRecipient(t *testing.T) {
+	dataset, sloID := burnAlertAccTestSetup(t)
+	burnAlert := &client.BurnAlert{}
+	exhaustionMinutes := 240
+
+	resource.Test(t, resource.TestCase{
+		PreCheck:                 testAccPreCheck(t),
+		ProtoV5ProviderFactories: testAccProtoV5MuxServerFactory,
+		CheckDestroy:             testAccEnsureBurnAlertDestroyed(t),
+		Steps: []resource.TestStep{
+			// Create - basic
+			{
+				Config: testAccConfigBurnAlertExhaustionTime_basicWebhookRecipient(exhaustionMinutes, dataset, sloID, "warning"),
+				Check:  testAccEnsureSuccessExhaustionTimeAlertWithWebhookRecip(t, burnAlert, exhaustionMinutes, sloID, "warning"),
+			},
+			// Update - change variable value
+			{
+				Config: testAccConfigBurnAlertExhaustionTime_basicWebhookRecipient(exhaustionMinutes, dataset, sloID, "info"),
+				Check:  testAccEnsureSuccessExhaustionTimeAlertWithWebhookRecip(t, burnAlert, exhaustionMinutes, sloID, "info"),
+			},
+			// Update - remove variables
+			{
+				Config: testAccConfigBurnAlertExhaustionTime_basicWebhookRecipient(exhaustionMinutes, dataset, sloID, ""),
+				Check:  testAccEnsureSuccessExhaustionTimeAlertWithWebhookRecip(t, burnAlert, exhaustionMinutes, sloID, ""),
+			},
+			// Import
+			{
+				ResourceName:            "honeycombio_burn_alert.test",
+				ImportStateIdPrefix:     fmt.Sprintf("%v/", dataset),
+				ImportState:             true,
+				ImportStateVerify:       true,
+				ImportStateVerifyIgnore: []string{"recipient"},
+			},
+		},
+	})
+}
+
 func TestAcc_BurnAlertResource_budgetRateBasic(t *testing.T) {
 	dataset, sloID := burnAlertAccTestSetup(t)
 	burnAlert := &client.BurnAlert{}
@@ -169,6 +206,44 @@ func TestAcc_BurnAlertResource_budgetRateBasic(t *testing.T) {
 	})
 }
 
+func TestAcc_BurnAlertResource_budgetRateBasicWebhookRecipient(t *testing.T) {
+	dataset, sloID := burnAlertAccTestSetup(t)
+	burnAlert := &client.BurnAlert{}
+	budgetRateWindowMinutes := 60
+	budgetRateDecreasePercent := float64(5)
+
+	resource.Test(t, resource.TestCase{
+		PreCheck:                 testAccPreCheck(t),
+		ProtoV5ProviderFactories: testAccProtoV5MuxServerFactory,
+		CheckDestroy:             testAccEnsureBurnAlertDestroyed(t),
+		Steps: []resource.TestStep{
+			// Create - basic
+			{
+				Config: testAccConfigBurnAlertBudgetRate_basicWebhookRecipient(budgetRateWindowMinutes, budgetRateDecreasePercent, dataset, sloID, "warning"),
+				Check:  testAccEnsureSuccessBudgetRateAlertWithWebhookRecip(t, burnAlert, budgetRateWindowMinutes, budgetRateDecreasePercent, sloID, "warning"),
+			},
+			// Update - change variable value
+			{
+				Config: testAccConfigBurnAlertBudgetRate_basicWebhookRecipient(budgetRateWindowMinutes, budgetRateDecreasePercent, dataset, sloID, "info"),
+				Check:  testAccEnsureSuccessBudgetRateAlertWithWebhookRecip(t, burnAlert, budgetRateWindowMinutes, budgetRateDecreasePercent, sloID, "info"),
+			},
+			// Update - remove variables
+			{
+				Config: testAccConfigBurnAlertBudgetRate_basicWebhookRecipient(budgetRateWindowMinutes, budgetRateDecreasePercent, dataset, sloID, ""),
+				Check:  testAccEnsureSuccessBudgetRateAlertWithWebhookRecip(t, burnAlert, budgetRateWindowMinutes, budgetRateDecreasePercent, sloID, ""),
+			},
+			// Import
+			{
+				ResourceName:            "honeycombio_burn_alert.test",
+				ImportStateIdPrefix:     fmt.Sprintf("%v/", dataset),
+				ImportState:             true,
+				ImportStateVerify:       true,
+				ImportStateVerifyIgnore: []string{"recipient"},
+			},
+		},
+	})
+}
+
 // Check that creating a budget rate alert with a
 // budget_rate_decrease_percent with trailing zeros works,
 // doesn't produce spurious plans after apply, and imports successfully
@@ -328,6 +403,10 @@ func TestAcc_BurnAlertResource_validateExhaustionTime(t *testing.T) {
 				Config:      testAccConfigBurnAlertExhaustionTime_validateAttributesWhenAlertTypeIsExhaustionTime(dataset, sloID),
 				ExpectError: regexp.MustCompile(`"budget_rate_decrease_percent": must not be configured when`),
 			},
+			{
+				Config:      testAccConfigBurnAlertExhaustionTime_basicWebhookRecipientDuplicateVar(240, dataset, sloID),
+				ExpectError: regexp.MustCompile(`cannot have more than one "variable" with the same "name"`),
+			},
 		},
 	})
 }
@@ -371,6 +450,10 @@ func TestAcc_BurnAlertResource_validateBudgetRate(t *testing.T) {
 				Config:      testAccConfigBurnAlertBudgetRate_validateAttributesWhenAlertTypeIsBudgetRate(dataset, sloID),
 				ExpectError: regexp.MustCompile(`"exhaustion_minutes": must not be configured when "alert_type"`),
 			},
+			{
+				Config:      testAccConfigBurnAlertBudgetRate_basicWebhookRecipientDuplicateVar(budgetRateWindowMinutes, budgetRateDecreasePercent, dataset, sloID),
+				ExpectError: regexp.MustCompile(`cannot have more than one "variable" with the same "name"`),
+			},
 		},
 	})
 }
@@ -551,6 +634,54 @@ func testAccEnsureSuccessExhaustionTimeAlert(t *testing.T, burnAlert *client.Bur
 	)
 }
 
+// Checks that the exhaustion time burn alert exists, has the correct attributes, and has the correct state
+func testAccEnsureSuccessExhaustionTimeAlertWithWebhookRecip(t *testing.T, burnAlert *client.BurnAlert, exhaustionMinutes int, sloID, varValue string) resource.TestCheckFunc {
+	if varValue == "" {
+		return resource.ComposeAggregateTestCheckFunc(
+			// Check that the burn alert exists
+			testAccEnsureBurnAlertExists(t, "honeycombio_burn_alert.test", burnAlert),
+
+			// Check that the burn alert has the correct attributes
+			testAccEnsureAttributesCorrectExhaustionTime(burnAlert, exhaustionMinutes, sloID),
+
+			// Check that the burn alert has the correct values in state
+			resource.TestCheckResourceAttr("honeycombio_burn_alert.test", "slo_id", sloID),
+			resource.TestCheckResourceAttr("honeycombio_burn_alert.test", "description", testBADescription),
+			resource.TestCheckResourceAttr("honeycombio_burn_alert.test", "alert_type", "exhaustion_time"),
+			resource.TestCheckResourceAttr("honeycombio_burn_alert.test", "exhaustion_minutes", fmt.Sprintf("%d", exhaustionMinutes)),
+			resource.TestCheckResourceAttr("honeycombio_burn_alert.test", "recipient.#", "1"),
+			resource.TestCheckResourceAttr("honeycombio_burn_alert.test", "recipient.0.notification_details.#", "0"),
+
+			// Budget rate attributes should not be set
+			resource.TestCheckNoResourceAttr("honeycombio_burn_alert.test", "budget_rate_window_minutes"),
+			resource.TestCheckNoResourceAttr("honeycombio_burn_alert.test", "budget_rate_decrease_percent"),
+		)
+	}
+
+	return resource.ComposeAggregateTestCheckFunc(
+		// Check that the burn alert exists
+		testAccEnsureBurnAlertExists(t, "honeycombio_burn_alert.test", burnAlert),
+
+		// Check that the burn alert has the correct attributes
+		testAccEnsureAttributesCorrectExhaustionTime(burnAlert, exhaustionMinutes, sloID),
+
+		// Check that the burn alert has the correct values in state
+		resource.TestCheckResourceAttr("honeycombio_burn_alert.test", "slo_id", sloID),
+		resource.TestCheckResourceAttr("honeycombio_burn_alert.test", "description", testBADescription),
+		resource.TestCheckResourceAttr("honeycombio_burn_alert.test", "alert_type", "exhaustion_time"),
+		resource.TestCheckResourceAttr("honeycombio_burn_alert.test", "exhaustion_minutes", fmt.Sprintf("%d", exhaustionMinutes)),
+		resource.TestCheckResourceAttr("honeycombio_burn_alert.test", "recipient.#", "1"),
+		resource.TestCheckResourceAttr("honeycombio_burn_alert.test", "recipient.0.notification_details.#", "1"),
+		resource.TestCheckResourceAttr("honeycombio_burn_alert.test", "recipient.0.notification_details.0.variable.#", "1"),
+		resource.TestCheckResourceAttr("honeycombio_burn_alert.test", "recipient.0.notification_details.0.variable.0.name", "severity"),
+		resource.TestCheckResourceAttr("honeycombio_burn_alert.test", "recipient.0.notification_details.0.variable.0.value", varValue),
+
+		// Budget rate attributes should not be set
+		resource.TestCheckNoResourceAttr("honeycombio_burn_alert.test", "budget_rate_window_minutes"),
+		resource.TestCheckNoResourceAttr("honeycombio_burn_alert.test", "budget_rate_decrease_percent"),
+	)
+}
+
 // Checks that the budget rate burn alert exists, has the correct attributes, and has the correct state
 func testAccEnsureSuccessBudgetRateAlert(t *testing.T, burnAlert *client.BurnAlert, budgetRateWindowMinutes int, budgetRateDecreasePercent float64, pagerdutySeverity, sloID string) resource.TestCheckFunc {
 	return resource.ComposeAggregateTestCheckFunc(
@@ -574,6 +705,54 @@ func testAccEnsureSuccessBudgetRateAlert(t *testing.T, burnAlert *client.BurnAle
 	)
 }
 
+// Checks that the budget rate burn alert exists, has the correct attributes, and has the correct state
+func testAccEnsureSuccessBudgetRateAlertWithWebhookRecip(t *testing.T, burnAlert *client.BurnAlert, budgetRateWindowMinutes int, budgetRateDecreasePercent float64, sloID, varValue string) resource.TestCheckFunc {
+	if varValue == "" {
+		return resource.ComposeAggregateTestCheckFunc(
+			// Check that the burn alert exists
+			testAccEnsureBurnAlertExists(t, "honeycombio_burn_alert.test", burnAlert),
+
+			// Check that the burn alert has the correct attributes
+			testAccEnsureAttributesCorrectBudgetRate(burnAlert, budgetRateWindowMinutes, budgetRateDecreasePercent, sloID),
+
+			// Check that the burn alert has the correct values in state
+			resource.TestCheckResourceAttr("honeycombio_burn_alert.test", "slo_id", sloID),
+			resource.TestCheckResourceAttr("honeycombio_burn_alert.test", "description", testBADescription),
+			resource.TestCheckResourceAttr("honeycombio_burn_alert.test", "alert_type", "budget_rate"),
+			resource.TestCheckResourceAttr("honeycombio_burn_alert.test", "budget_rate_window_minutes", fmt.Sprintf("%d", budgetRateWindowMinutes)),
+			resource.TestCheckResourceAttr("honeycombio_burn_alert.test", "budget_rate_decrease_percent", helper.FloatToPercentString(budgetRateDecreasePercent)),
+			resource.TestCheckResourceAttr("honeycombio_burn_alert.test", "recipient.#", "1"),
+			resource.TestCheckResourceAttr("honeycombio_burn_alert.test", "recipient.0.notification_details.#", "0"),
+
+			// Exhaustion time attributes should not be set
+			resource.TestCheckNoResourceAttr("honeycombio_burn_alert.test", "exhaustion_minutes"),
+		)
+	}
+
+	return resource.ComposeAggregateTestCheckFunc(
+		// Check that the burn alert exists
+		testAccEnsureBurnAlertExists(t, "honeycombio_burn_alert.test", burnAlert),
+
+		// Check that the burn alert has the correct attributes
+		testAccEnsureAttributesCorrectBudgetRate(burnAlert, budgetRateWindowMinutes, budgetRateDecreasePercent, sloID),
+
+		// Check that the burn alert has the correct values in state
+		resource.TestCheckResourceAttr("honeycombio_burn_alert.test", "slo_id", sloID),
+		resource.TestCheckResourceAttr("honeycombio_burn_alert.test", "description", testBADescription),
+		resource.TestCheckResourceAttr("honeycombio_burn_alert.test", "alert_type", "budget_rate"),
+		resource.TestCheckResourceAttr("honeycombio_burn_alert.test", "budget_rate_window_minutes", fmt.Sprintf("%d", budgetRateWindowMinutes)),
+		resource.TestCheckResourceAttr("honeycombio_burn_alert.test", "budget_rate_decrease_percent", helper.FloatToPercentString(budgetRateDecreasePercent)),
+		resource.TestCheckResourceAttr("honeycombio_burn_alert.test", "recipient.#", "1"),
+		resource.TestCheckResourceAttr("honeycombio_burn_alert.test", "recipient.0.notification_details.#", "1"),
+		resource.TestCheckResourceAttr("honeycombio_burn_alert.test", "recipient.0.notification_details.0.variable.#", "1"),
+		resource.TestCheckResourceAttr("honeycombio_burn_alert.test", "recipient.0.notification_details.0.variable.0.name", "severity"),
+		resource.TestCheckResourceAttr("honeycombio_burn_alert.test", "recipient.0.notification_details.0.variable.0.value", varValue),
+
+		// Exhaustion time attributes should not be set
+		resource.TestCheckNoResourceAttr("honeycombio_burn_alert.test", "exhaustion_minutes"),
+	)
+}
+
 func testAccEnsureBurnAlertExists(t *testing.T, name string, burnAlert *client.BurnAlert) resource.TestCheckFunc {
 	return func(s *terraform.State) error {
 		resourceState, ok := s.RootModule().Resources[name]
@@ -722,6 +901,13 @@ resource "honeycombio_burn_alert" "test" {
 }
 
 func testAccConfigBurnAlertDefault_basic(exhaustionMinutes int, dataset, sloID, pdseverity string) string {
+	tmplBody := `<<EOT
+		{
+			"name": " {{ .Name }}",
+			"id": " {{ .ID }}",
+			"description": " {{ .Description }}",
+		}
+		EOT`
 	return fmt.Sprintf(`
 resource "honeycombio_pagerduty_recipient" "test" {
   integration_key  = "08b9d4cacd68933151a1ef1028b67da2"
@@ -742,7 +928,147 @@ resource "honeycombio_burn_alert" "test" {
       pagerduty_severity = "%[4]s"
     }
   }
-}`, exhaustionMinutes, dataset, sloID, pdseverity, testBADescription)
+}`, exhaustionMinutes, dataset, sloID, pdseverity, testBADescription, tmplBody)
+}
+
+func testAccConfigBurnAlertExhaustionTime_basicWebhookRecipient(exhaustionMinutes int, dataset, sloID, variableValue string) string {
+	tmplBody := `<<EOT
+		{
+			"name": " {{ .Name }}",
+			"id": " {{ .ID }}",
+			"description": " {{ .Description }}",
+		}
+		EOT`
+
+	if variableValue == "" {
+		return fmt.Sprintf(`
+resource "honeycombio_webhook_recipient" "test" {
+  name = "test"
+	url  = "http://example.com"
+
+	header {
+	  name = "Authorization"
+	  value = "Bearer abc123"
+	}
+
+	variable {
+	  name = "severity"
+      default_value = "critical"
+	}
+
+	template {
+	  type   = "exhaustion_time"
+      body = %[5]s
+    }
+}
+
+resource "honeycombio_burn_alert" "test" {
+  exhaustion_minutes = %[1]d
+
+  dataset            = "%[2]s"
+  slo_id             = "%[3]s"
+  description        = "%[4]s"
+
+  recipient {
+	id = honeycombio_webhook_recipient.test.id
+  }
+}`, exhaustionMinutes, dataset, sloID, testBADescription, tmplBody)
+	}
+
+	return fmt.Sprintf(`
+resource "honeycombio_webhook_recipient" "test" {
+  name = "test"
+	url  = "http://example.com"
+
+	header {
+	  name = "Authorization"
+	  value = "Bearer abc123"
+	}
+
+	variable {
+	  name = "severity"
+      default_value = "critical"
+	}
+
+	template {
+	  type   = "exhaustion_time"
+      body = %[5]s
+    }
+}
+
+resource "honeycombio_burn_alert" "test" {
+  exhaustion_minutes = %[1]d
+
+  dataset            = "%[2]s"
+  slo_id             = "%[3]s"
+  description        = "%[4]s"
+
+  recipient {
+	id = honeycombio_webhook_recipient.test.id
+
+	notification_details {	
+ 		variable {
+ 			name = "severity"
+ 			value = "%[6]s"
+ 		}
+ 	}
+  }
+}`, exhaustionMinutes, dataset, sloID, testBADescription, tmplBody, variableValue)
+}
+
+func testAccConfigBurnAlertExhaustionTime_basicWebhookRecipientDuplicateVar(exhaustionMinutes int, dataset, sloID string) string {
+	tmplBody := `<<EOT
+		{
+			"name": " {{ .Name }}",
+			"id": " {{ .ID }}",
+			"description": " {{ .Description }}",
+		}
+		EOT`
+
+	return fmt.Sprintf(`
+resource "honeycombio_webhook_recipient" "test" {
+  name = "test"
+	url  = "http://example.com"
+
+	header {
+	  name = "Authorization"
+	  value = "Bearer abc123"
+	}
+
+	variable {
+	  name = "severity"
+      default_value = "critical"
+	}
+
+	template {
+	  type   = "exhaustion_time"
+      body = %[5]s
+    }
+}
+
+resource "honeycombio_burn_alert" "test" {
+  exhaustion_minutes = %[1]d
+
+  dataset            = "%[2]s"
+  slo_id             = "%[3]s"
+  description        = "%[4]s"
+
+  recipient {
+	id = honeycombio_webhook_recipient.test.id
+
+	notification_details {	
+ 		variable {
+ 			name = "severity"
+ 			value = "info"
+ 		}
+
+		variable {
+ 			name = "severity"
+ 			value = "critical"
+ 		}
+ 	}
+  }
+}`, exhaustionMinutes, dataset, sloID, testBADescription, tmplBody)
 }
 
 func testAccConfigBurnAlertDefault_validateAttributesWhenAlertTypeIsExhaustionTime(dataset, sloID string) string {
@@ -829,6 +1155,152 @@ resource "honeycombio_burn_alert" "test" {
 }`, budgetRateWindowMinutes, helper.FloatToPercentString(budgetRateDecreasePercent), dataset, sloID, pdseverity, testBADescription)
 }
 
+func testAccConfigBurnAlertBudgetRate_basicWebhookRecipient(budgetRateWindowMinutes int, budgetRateDecreasePercent float64, dataset, sloID, variableValue string) string {
+	tmplBody := `<<EOT
+		{
+			"name": " {{ .Name }}",
+			"id": " {{ .ID }}",
+			"description": " {{ .Description }}",
+		}
+		EOT`
+
+	if variableValue == "" {
+		return fmt.Sprintf(`
+resource "honeycombio_webhook_recipient" "test" {
+  name = "test"
+	url  = "http://example.com"
+
+	header {
+	  name = "Authorization"
+	  value = "Bearer abc123"
+	}
+
+	variable {
+	  name = "severity"
+      default_value = "critical"
+	}
+
+	template {
+	  type   = "budget_rate"
+      body = %[6]s
+    }
+}
+
+resource "honeycombio_burn_alert" "test" {
+  alert_type                   = "budget_rate"
+  description                  = "%[5]s"
+  budget_rate_window_minutes   = %[1]d
+  budget_rate_decrease_percent = %[2]s
+
+  dataset = "%[3]s"
+  slo_id  = "%[4]s"
+
+  recipient {
+	id = honeycombio_webhook_recipient.test.id
+  }
+}`, budgetRateWindowMinutes, helper.FloatToPercentString(budgetRateDecreasePercent), dataset, sloID, testBADescription, tmplBody)
+	}
+
+	return fmt.Sprintf(`
+resource "honeycombio_webhook_recipient" "test" {
+  name = "test"
+	url  = "http://example.com"
+
+	header {
+	  name = "Authorization"
+	  value = "Bearer abc123"
+	}
+
+	variable {
+	  name = "severity"
+      default_value = "critical"
+	}
+
+	template {
+	  type   = "budget_rate"
+      body = %[6]s
+    }
+}
+
+resource "honeycombio_burn_alert" "test" {
+  alert_type                   = "budget_rate"
+  description                  = "%[5]s"
+  budget_rate_window_minutes   = %[1]d
+  budget_rate_decrease_percent = %[2]s
+
+  dataset = "%[3]s"
+  slo_id  = "%[4]s"
+
+  recipient {
+	id = honeycombio_webhook_recipient.test.id
+	
+	notification_details {	
+		variable {
+			name = "severity"
+			value = "%[7]s"
+		}
+	}
+  }
+}`, budgetRateWindowMinutes, helper.FloatToPercentString(budgetRateDecreasePercent), dataset, sloID, testBADescription, tmplBody, variableValue)
+}
+
+func testAccConfigBurnAlertBudgetRate_basicWebhookRecipientDuplicateVar(budgetRateWindowMinutes int, budgetRateDecreasePercent float64, dataset, sloID string) string {
+	tmplBody := `<<EOT
+		{
+			"name": " {{ .Name }}",
+			"id": " {{ .ID }}",
+			"description": " {{ .Description }}",
+		}
+		EOT`
+
+	return fmt.Sprintf(`
+resource "honeycombio_webhook_recipient" "test" {
+  name = "test"
+	url  = "http://example.com"
+
+	header {
+	  name = "Authorization"
+	  value = "Bearer abc123"
+	}
+
+	variable {
+	  name = "severity"
+      default_value = "critical"
+	}
+
+	template {
+	  type   = "budget_rate"
+      body = %[6]s
+    }
+}
+
+resource "honeycombio_burn_alert" "test" {
+  alert_type                   = "budget_rate"
+  description                  = "%[5]s"
+  budget_rate_window_minutes   = %[1]d
+  budget_rate_decrease_percent = %[2]s
+
+  dataset = "%[3]s"
+  slo_id  = "%[4]s"
+
+  recipient {
+	id = honeycombio_webhook_recipient.test.id
+	
+	notification_details {	
+		variable {
+			name = "severity"
+			value = "info"
+		}
+
+		variable {
+			name = "severity"
+			value = "critical"
+		}
+	}
+  }
+}`, budgetRateWindowMinutes, helper.FloatToPercentString(budgetRateDecreasePercent), dataset, sloID, testBADescription, tmplBody)
+}
+
 func testAccConfigBurnAlertBudgetRate_trailingZeros(dataset, sloID string) string {
 	return fmt.Sprintf(`
 resource "honeycombio_pagerduty_recipient" "test" {
diff --git a/internal/provider/notification_recipients_test.go b/internal/provider/notification_recipients_test.go
index d8b90fa..d3544c4 100644
--- a/internal/provider/notification_recipients_test.go
+++ b/internal/provider/notification_recipients_test.go
@@ -96,7 +96,7 @@ func Test_reconcileReadNotificationRecipientState(t *testing.T) {
 					ID:      types.StringValue("ijkl13579"),
 					Type:    types.StringValue("pagerduty"),
 					Target:  types.StringValue("test-pagerduty"),
-					Details: types.ListValueMust(types.ObjectType{AttrTypes: models.NotificationRecipientDetailsAttrType}, severityStringToValue("warning")),
+					Details: types.ListValueMust(types.ObjectType{AttrTypes: models.NotificationRecipientDetailsAttrType}, notificationRecipientDetailsToValue("warning")),
 				},
 			}),
 		},
@@ -109,7 +109,7 @@ func Test_reconcileReadNotificationRecipientState(t *testing.T) {
 				state: notificationRecipientModelsToSet([]models.NotificationRecipientModel{
 					{ID: types.StringValue("abcd12345")},
 					{Type: types.StringValue("slack"), Target: types.StringValue("#test-foo")},
-					{ID: types.StringValue("ijkl13579"), Details: types.ListValueMust(types.ObjectType{AttrTypes: models.NotificationRecipientDetailsAttrType}, severityStringToValue("warning"))},
+					{ID: types.StringValue("ijkl13579"), Details: types.ListValueMust(types.ObjectType{AttrTypes: models.NotificationRecipientDetailsAttrType}, notificationRecipientDetailsToValue("warning"))},
 				}),
 			},
 			want: notificationRecipientModelsToSet([]models.NotificationRecipientModel{
@@ -125,7 +125,7 @@ func Test_reconcileReadNotificationRecipientState(t *testing.T) {
 				state: notificationRecipientModelsToSet([]models.NotificationRecipientModel{
 					{ID: types.StringValue("abcd12345")},
 					{Type: types.StringValue("slack"), Target: types.StringValue("#test-channel")},
-					{ID: types.StringValue("ijkl13579"), Details: types.ListValueMust(types.ObjectType{AttrTypes: models.NotificationRecipientDetailsAttrType}, severityStringToValue("warning"))},
+					{ID: types.StringValue("ijkl13579"), Details: types.ListValueMust(types.ObjectType{AttrTypes: models.NotificationRecipientDetailsAttrType}, notificationRecipientDetailsToValue("warning"))},
 				}),
 			},
 			want: notificationRecipientModelsToSet([]models.NotificationRecipientModel{
@@ -149,6 +149,6 @@ func notificationRecipientModelsToSet(n []models.NotificationRecipientModel) typ
 	return types.SetValueMust(types.ObjectType{AttrTypes: models.NotificationRecipientAttrType}, values)
 }
 
-func severityStringToValue(s string) []attr.Value {
-	return []attr.Value{types.ObjectValueMust(models.NotificationRecipientDetailsAttrType, map[string]attr.Value{"pagerduty_severity": types.StringValue(s)})}
+func notificationRecipientDetailsToValue(s string) []attr.Value {
+	return []attr.Value{types.ObjectValueMust(models.NotificationRecipientDetailsAttrType, map[string]attr.Value{"pagerduty_severity": types.StringValue(s), "variable": types.SetNull(types.ObjectType{AttrTypes: models.NotificationVariableAttrType})})}
 }
diff --git a/internal/provider/trigger_resource_test.go b/internal/provider/trigger_resource_test.go
index d7b9ba0..314d1ee 100644
--- a/internal/provider/trigger_resource_test.go
+++ b/internal/provider/trigger_resource_test.go
@@ -78,6 +78,80 @@ func TestAcc_TriggerResource(t *testing.T) {
 			},
 		})
 	})
+
+	t.Run("trigger resource with custom webhook recipient", func(t *testing.T) {
+		resource.Test(t, resource.TestCase{
+			PreCheck:                 testAccPreCheck(t),
+			ProtoV5ProviderFactories: testAccProtoV5MuxServerFactory,
+			Steps: []resource.TestStep{
+				{
+					Config: testAccConfigBasicTriggerTestWithWebhookRecip(dataset, name, "info"),
+					Check: resource.ComposeAggregateTestCheckFunc(
+						testAccEnsureTriggerExists(t, "honeycombio_trigger.test"),
+						resource.TestCheckResourceAttr("honeycombio_trigger.test", "name", name),
+						resource.TestCheckResourceAttr("honeycombio_trigger.test", "frequency", "600"),
+						resource.TestCheckResourceAttr("honeycombio_trigger.test", "recipient.#", "1"),
+						resource.TestCheckResourceAttr("honeycombio_trigger.test", "recipient.0.notification_details.#", "1"),
+						resource.TestCheckResourceAttr("honeycombio_trigger.test", "recipient.0.notification_details.0.variable.#", "1"),
+						resource.TestCheckResourceAttr("honeycombio_trigger.test", "recipient.0.notification_details.0.variable.0.name", "severity"),
+						resource.TestCheckResourceAttr("honeycombio_trigger.test", "recipient.0.notification_details.0.variable.0.value", "info"),
+						resource.TestCheckResourceAttr("honeycombio_trigger.test", "threshold.0.exceeded_limit", "1"),
+						resource.TestCheckResourceAttrPair("honeycombio_trigger.test", "query_id", "honeycombio_query.test", "id"),
+						resource.TestCheckNoResourceAttr("honeycombio_trigger.test", "query_json"),
+					),
+				},
+				// then update the variable value from info -> critical
+				{
+					Config: testAccConfigBasicTriggerTestWithWebhookRecip(dataset, name, "critical"),
+					Check: resource.ComposeAggregateTestCheckFunc(
+						testAccEnsureTriggerExists(t, "honeycombio_trigger.test"),
+						resource.TestCheckResourceAttr("honeycombio_trigger.test", "name", name),
+						resource.TestCheckResourceAttr("honeycombio_trigger.test", "frequency", "600"),
+						resource.TestCheckResourceAttr("honeycombio_trigger.test", "recipient.#", "1"),
+						resource.TestCheckResourceAttr("honeycombio_trigger.test", "recipient.0.notification_details.#", "1"),
+						resource.TestCheckResourceAttr("honeycombio_trigger.test", "recipient.0.notification_details.0.variable.#", "1"),
+						resource.TestCheckResourceAttr("honeycombio_trigger.test", "recipient.0.notification_details.0.variable.0.name", "severity"),
+						resource.TestCheckResourceAttr("honeycombio_trigger.test", "recipient.0.notification_details.0.variable.0.value", "critical"),
+						resource.TestCheckResourceAttr("honeycombio_trigger.test", "threshold.0.exceeded_limit", "1"),
+						resource.TestCheckResourceAttrPair("honeycombio_trigger.test", "query_id", "honeycombio_query.test", "id"),
+						resource.TestCheckNoResourceAttr("honeycombio_trigger.test", "query_json"),
+					),
+				},
+				// remove variables
+				{
+					Config: testAccConfigBasicTriggerTestWithWebhookRecip(dataset, name, ""),
+					Check: resource.ComposeAggregateTestCheckFunc(
+						testAccEnsureTriggerExists(t, "honeycombio_trigger.test"),
+						resource.TestCheckResourceAttr("honeycombio_trigger.test", "name", name),
+						resource.TestCheckResourceAttr("honeycombio_trigger.test", "frequency", "600"),
+						resource.TestCheckResourceAttr("honeycombio_trigger.test", "recipient.#", "1"),
+						resource.TestCheckResourceAttr("honeycombio_trigger.test", "recipient.0.notification_details.#", "0"),
+						resource.TestCheckResourceAttr("honeycombio_trigger.test", "threshold.0.exceeded_limit", "1"),
+						resource.TestCheckResourceAttrPair("honeycombio_trigger.test", "query_id", "honeycombio_query.test", "id"),
+						resource.TestCheckNoResourceAttr("honeycombio_trigger.test", "query_json"),
+					),
+				},
+				{
+					ResourceName:        "honeycombio_trigger.test",
+					ImportStateIdPrefix: fmt.Sprintf("%v/", dataset),
+					ImportState:         true,
+				},
+			},
+		})
+	})
+
+	t.Run("duplicate variable on custom webhook recipient", func(t *testing.T) {
+		resource.Test(t, resource.TestCase{
+			PreCheck:                 testAccPreCheck(t),
+			ProtoV5ProviderFactories: testAccProtoV5MuxServerFactory,
+			Steps: []resource.TestStep{
+				{
+					Config:      testAccConfigBasicTriggerTestWithWebhookRecipAndDuplicateVar(dataset, name),
+					ExpectError: regexp.MustCompile(`cannot have more than one "variable" with the same "name"`),
+				},
+			},
+		})
+	})
 }
 
 // TestAcc_TriggerResourceUpgradeFromVersion014 is intended to test the migration
@@ -876,6 +950,209 @@ resource "honeycombio_trigger" "test" {
 }`, dataset, name, pdseverity, email, pdKey, pdName)
 }
 
+func testAccConfigBasicTriggerTestWithWebhookRecip(dataset, name, varValue string) string {
+	tmplBody := `<<EOT
+		{
+			"name": " {{ .Name }}",
+			"id": " {{ .ID }}",
+			"description": " {{ .Description }}",
+		}
+		EOT`
+
+	if varValue == "" {
+		return fmt.Sprintf(`
+data "honeycombio_query_specification" "test" {
+  calculation {
+    op     = "AVG"
+    column = "duration_ms"
+  }
+  time_range = 1200
+}
+
+resource "honeycombio_query" "test" {
+  dataset    = "%[1]s"
+  query_json = data.honeycombio_query_specification.test.json
+}
+
+resource "honeycombio_webhook_recipient" "test" {
+  name = "test"
+	url  = "http://example.com"
+
+	header {
+	  name = "Authorization"
+	  value = "Bearer abc123"
+	}
+
+	variable {
+	  name = "severity"
+      default_value = "critical"
+	}
+
+	template {
+	  type   = "trigger"
+      body 	 = %[3]s
+    }
+}
+
+resource "honeycombio_trigger" "test" {
+  name    = "%[2]s"
+  dataset = "%[1]s"
+
+  description = "My nice description"
+
+  query_id = honeycombio_query.test.id
+
+  threshold {
+    op    = ">"
+    value = 100
+  }
+
+  frequency = data.honeycombio_query_specification.test.time_range / 2
+
+  recipient {
+	id = honeycombio_webhook_recipient.test.id
+  }
+}`, dataset, name, tmplBody)
+	}
+
+	return fmt.Sprintf(`
+data "honeycombio_query_specification" "test" {
+  calculation {
+    op     = "AVG"
+    column = "duration_ms"
+  }
+  time_range = 1200
+}
+
+resource "honeycombio_query" "test" {
+  dataset    = "%[1]s"
+  query_json = data.honeycombio_query_specification.test.json
+}
+
+resource "honeycombio_webhook_recipient" "test" {
+  name = "test"
+	url  = "http://example.com"
+
+	header {
+	  name = "Authorization"
+	  value = "Bearer abc123"
+	}
+
+	variable {
+	  name = "severity"
+      default_value = "critical"
+	}
+
+	template {
+	  type   = "trigger"
+      body 	 = %[4]s
+    }
+}
+
+resource "honeycombio_trigger" "test" {
+  name    = "%[2]s"
+  dataset = "%[1]s"
+
+  description = "My nice description"
+
+  query_id = honeycombio_query.test.id
+
+  threshold {
+    op    = ">"
+    value = 100
+  }
+
+  frequency = data.honeycombio_query_specification.test.time_range / 2
+
+  recipient {
+	id = honeycombio_webhook_recipient.test.id
+	
+	notification_details {	
+		variable {
+			name = "severity"
+			value = "%[3]s"
+		}
+	}
+  }
+}`, dataset, name, varValue, tmplBody)
+}
+
+func testAccConfigBasicTriggerTestWithWebhookRecipAndDuplicateVar(dataset, name string) string {
+	tmplBody := `<<EOT
+		{
+			"name": " {{ .Name }}",
+			"id": " {{ .ID }}",
+			"description": " {{ .Description }}",
+		}
+		EOT`
+
+	return fmt.Sprintf(`
+data "honeycombio_query_specification" "test" {
+  calculation {
+    op     = "AVG"
+    column = "duration_ms"
+  }
+  time_range = 1200
+}
+
+resource "honeycombio_query" "test" {
+  dataset    = "%[1]s"
+  query_json = data.honeycombio_query_specification.test.json
+}
+
+resource "honeycombio_webhook_recipient" "test" {
+  name = "test"
+	url  = "http://example.com"
+
+	header {
+	  name = "Authorization"
+	  value = "Bearer abc123"
+	}
+
+	variable {
+	  name = "severity"
+      default_value = "critical"
+	}
+
+	template {
+	  type   = "trigger"
+      body 	 = %[3]s
+    }
+}
+
+resource "honeycombio_trigger" "test" {
+  name    = "%[2]s"
+  dataset = "%[1]s"
+
+  description = "My nice description"
+
+  query_id = honeycombio_query.test.id
+
+  threshold {
+    op    = ">"
+    value = 100
+  }
+
+  frequency = data.honeycombio_query_specification.test.time_range / 2
+
+  recipient {
+	id = honeycombio_webhook_recipient.test.id
+	
+	notification_details {	
+		variable {
+			name = "severity"
+			value = "info"
+		}
+
+		variable {
+			name = "severity"
+			value = "critical"
+		}
+	}
+  }
+}`, dataset, name, tmplBody)
+}
+
 func testAccConfigBasicTriggerTest_QuerySpec(dataset, name, pdseverity string) string {
 	email := test.RandomEmail()
 	pdKey := test.RandomString(32)