# Terraform Webhooks

HCP Terraform (formerly Terraform Cloud) and Terraform Enterprise send workspace
notifications to any HTTP endpoint using the **generic** destination type. This
skill covers that destination only; Slack, Microsoft Teams, and email use their own
formats.

## When to Use This Skill

* How do I receive HCP Terraform / Terraform Cloud run notification webhooks?
* How do I verify the `X-TFE-Notification-Signature` header?
* How do I handle `run:completed`, `run:errored`, or `run:needs_attention` notifications?
* How do I react to drift detection (`assessment:drifted`) or failed health assessments?
* Why is my Terraform notification configuration failing verification or not enabling?

## Verification (core)

The signature is the **hex HMAC-SHA512 of the raw request body**, keyed with the
notification configuration's **token**, sent in `X-TFE-Notification-Signature`.
The token is optional in HCP Terraform: **if you don't set one, no signature is
sent**, so set one and reject unsigned requests.

```javascript
const crypto = require('crypto');

function verifyTerraformSignature(rawBody, signatureHeader, token) {
  if (!signatureHeader || !token) return false;
  const expected = crypto.createHmac('sha512', token).update(rawBody).digest('hex');
  try {
    const received = Buffer.from(signatureHeader.toLowerCase());
    const expectedBuf = Buffer.from(expected);
    return received.length === expectedBuf.length && crypto.timingSafeEqual(received, expectedBuf);
  } catch {
    return false;
  }
}

```

```python
import hmac, hashlib

def verify_terraform_signature(raw_body: bytes, signature_header: str, token: str) -> bool:
    if not signature_header or not token:
        return False
    expected = hmac.new(token.encode(), raw_body, hashlib.sha512).hexdigest()
    return hmac.compare_digest(signature_header.lower().encode(), expected.encode())

```

In Express, read the body with `express.raw({ type: '*/*' })` so the signature
verifies whatever `Content-Type` the request carries.

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/terraform-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/terraform-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/terraform-webhooks/examples/fastapi/).

## Verification Request

When you save a configuration with **Enabled** on, or click **Send a test**, HCP
Terraform POSTs a test payload whose `notifications[0].trigger` is `"verification"`
(run fields are `null`). Return any **2xx**; the configuration can't be enabled
otherwise. The request is signed like any other when a token is set, so the token
must already be configured in your handler.

## Triggers

Run payloads (`payload_version: 1`) carry the trigger in `notifications[].trigger`.
Workspace payloads (`payload_version: "2"`) carry it in a top-level `trigger`.

| Trigger | Fires When |
| --- | --- |
| `run:created` | A run is created and enters the Pending stage |
| `run:planning` | A run acquires the lock and starts executing |
| `run:needs_attention` | A plan has changes and needs approval or a policy override |
| `run:applying` | A run enters the Apply stage |
| `run:completed` | A run completes successfully |
| `run:errored` | A run ends early from an error or cancellation |
| `assessment:drifted` | Drift detected (health assessments enabled) |
| `assessment:check_failure` | Continuous validation checks failed |
| `assessment:failed` | A health assessment itself failed |
| `workspace:auto_destroy_reminder` | An automatic destroy run is imminent |
| `workspace:auto_destroy_run_results` | An automatic destroy run was attempted |
| `workspace:deleted` | A workspace was deleted |
| `verification` | Test request sent on enable / "Send a test" |

Run payload fields: `payload_version`, `notification_configuration_id`, `run_url`,
`run_id`, `run_message`, `run_created_at`, `run_created_by`, `workspace_id`,
`workspace_name`, `organization_name`, and `notifications[]` (`message`, `trigger`,
`run_status`, `run_updated_at`, `run_updated_by`).

> **For the full payload reference**, see [Notification configurations API](https://developer.hashicorp.com/terraform/cloud-docs/api-docs/notification-configurations).

## Important Headers

| Header | Description |
| --- | --- |
| `X-TFE-Notification-Signature` | Hex HMAC-SHA512 of the raw body (only when a token is set) |

## Environment Variables

```bash
# The token entered on the notification configuration (cannot be viewed after saving)
TERRAFORM_NOTIFICATION_TOKEN=your_notification_token

```

## Local Development

```bash
# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 terraform --path /webhooks/terraform

```

Use the tunnel URL as the **Webhook URL** of a generic notification configuration.
Enabling the configuration sends the verification request through it.

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/terraform-webhooks/references/overview.md) - Notification types, triggers, payloads
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/terraform-webhooks/references/setup.md) - Creating a notification configuration with a token
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/terraform-webhooks/references/verification.md) - Signature verification details and gotchas