keenauth_webhook_endpoint

The keenauth_webhook_endpoint resource manages a webhook endpoint: an HTTPS address that receives the environment's events, signed with Standard Webhooks. The provider keeps the signing secret in state only encrypted to an RSA public key that you give it.

resource "keenauth_webhook_endpoint" "orders" {
  key         = "orders"
  url         = "https://hooks.example.com/keenauth"
  description = "Order service"
  event_types = ["user.created", "user.updated", "organization.created"]
  event_schema_versions = {
    "user.created" = 1
  }
  public_key = file("${path.module}/webhook-secret-key.pub.pem")

  rotation_trigger          = time_rotating.webhook_secret.id
  previous_secret_ttl_hours = 48
}

resource "time_rotating" "webhook_secret" {
  rotation_days = 90
}

output "orders_webhook_secret_encrypted" {
  value = keenauth_webhook_endpoint.orders.encrypted_signing_secret
}

Arguments

Every argument changes in place. Updates send only the changed fields as a JSON Merge Patch, with the ETag of the last read as If-Match. If the endpoint was changed elsewhere after the last read, the apply fails with 412 and nothing is changed; run plan again to review the drift.

The signing secret is never stored in plain text

The API returns the signing secret (idp_whsec_…) only when the endpoint is created and when the secret is rotated. With public_key, the provider encrypts the secret with RSA-OAEP (SHA-256, MGF1 SHA-256, empty label) and writes only the base64 ciphertext to encrypted_signing_secret. Without public_key the secret is discarded; rotate it later with keenauth webhook-endpoints rotate-secret or set public_key. The plain secret never reaches state, a plan file, an output or a log.

Create the key pair once, outside OpenTofu, and keep the private key out of your configuration:

openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:3072 -out webhook-secret-key.pem
openssl pkey -in webhook-secret-key.pem -pubout -out webhook-secret-key.pub.pem

After tofu apply, decrypt the secret and give it to the receiver:

tofu output -raw orders_webhook_secret_encrypted | base64 -d | \
  openssl pkeyutl -decrypt -inkey webhook-secret-key.pem \
    -pkeyopt rsa_padding_mode:oaep -pkeyopt rsa_oaep_md:sha256 -pkeyopt rsa_mgf1_md:sha256

public_key must be a PEM-encoded RSA public key (PUBLIC KEY or RSA PUBLIC KEY) of at least 2048 bits. A private key or a smaller key is rejected at plan time.

Rotation

The secret is rotated when you set or change public_key, and when rotation_trigger changes to a new value, for example the ID of a time_rotating resource. After a rotation the old secret keeps signing next to the new one for previous_secret_ttl_hours (0 to 168; the API default is 24; 0 drops it at once), so the receiver can switch without missing events. previous_secret_expires_at shows when the old secret stops. Removing public_key only removes encrypted_signing_secret; it does not rotate the secret.

Limits and status

An enabled endpoint counts against the plan's limit per environment: Free 3, Startup 20, Business 20, Enterprise 50. Creating or enabling one more fails with 403 quota_exceeded; disable or delete another endpoint, or upgrade the plan.

KeenAuth disables an endpoint that keeps failing (disabled_by_failure) or whose receiver answers 410 Gone (disabled_by_receiver). The next plan shows the change back to the configured status, and applying it enables the endpoint again.

Import

Import with the endpoint ID or its key:

tofu import keenauth_webhook_endpoint.orders orders

The signing secret cannot be read back, so encrypted_signing_secret is empty after import. If your configuration sets public_key, the next apply rotates the secret and delivers the new one encrypted.

Test sends, replays and delivery retries are one-off actions, not configuration. Use keenauth webhook-endpoints test, replay and deliveries for them.