> ## Documentation Index
> Fetch the complete documentation index at: https://docs.knoxcall.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Provision with Terraform

> The IaC path through onboarding: a route, its environments and its secret in ~40 lines of HCL, with a secret value that never enters your Terraform state.

# Provision with Terraform

Everything in the [quick start](/getting-started/quick-start-guide) can be done in the dashboard. This page is the same onboarding in HCL, for teams whose answer to "where is that configured?" has to be "in the repo".

By the end you will have a route, three environments with their own upstream configuration, and one secret whose value is not in your state file.

<Warning>
  **The provider is not published yet.** It is not on the Terraform Registry and not on the OpenTofu registry, so `terraform init` cannot fetch it. Until it is released you build it and point the CLI at the binary with `dev_overrides`, which is what the prerequisites below set up. See the [Terraform provider guide](/sdks/terraform#install) for the full explanation.
</Warning>

## Prerequisites

1. **Terraform >= 1.11 or OpenTofu >= 1.11.** Write-only attributes — the mechanism that keeps secret values out of state — do not exist below that in either tool.
2. **A local build of the provider**, with a `dev_overrides` block pointing at it. [Install instructions](/sdks/terraform#install).
3. **An OAuth client for your tenant**, exported as `KNOXCALL_CLIENT_ID` and `KNOXCALL_CLIENT_SECRET`. Create one under **Settings → API**.
4. Your tenant slug, and the upstream credential you want KnoxCall to hold.

## The module

Around 40 lines. Copy it into `main.tf`, change the names and the upstream URL, and read the notes underneath before you apply.

```hcl theme={"dark"}
terraform {
  required_version = ">= 1.11"
  required_providers {
    knoxcall = { source = "knoxcall/knoxcall" }
  }
}

provider "knoxcall" {
  # Reads KNOXCALL_CLIENT_ID / KNOXCALL_CLIENT_SECRET from the environment.
  tenant_slug = "acme"
}

variable "stripe_key" {
  description = "Upstream credential. Supply via TF_VAR_stripe_key; do not commit it."
  type        = string
  sensitive   = true
}

# "production" already exists as the tenant default, so declare only the others.
resource "knoxcall_environment" "env" {
  for_each = toset(["development", "staging"])
  name     = each.key
}

# The value is write-only: Terraform sends it and forgets it.
resource "knoxcall_secret" "stripe" {
  name             = "stripe-key"
  value_wo         = var.stripe_key
  value_wo_version = 1 # bump to rotate
}

# The route owns its BASE environment's configuration (production).
resource "knoxcall_route" "payments" {
  name            = "stripe-payments"
  target_base_url = "https://api.stripe.com"

  inject_headers_json = jsonencode({
    Authorization = "Bearer {{secret_id:${knoxcall_secret.stripe.id}}}"
  })

  rate_limit_enabled    = true
  rate_limit_requests   = 600
  rate_limit_window_sec = 60
}

# Every other environment is its own row, with its own upstream and limits.
resource "knoxcall_route_environment" "payments" {
  for_each = knoxcall_environment.env

  route_id        = knoxcall_route.payments.id
  environment     = each.value.name
  target_base_url = "https://api.stripe.com"

  inject_headers_json = jsonencode({
    Authorization = "Bearer {{secret_id:${knoxcall_secret.stripe.id}}}"
  })

  rate_limit_enabled    = true
  rate_limit_requests   = 60
  rate_limit_window_sec = 60
}
```

Four things in there are worth understanding rather than copying:

* **`production` is not declared.** A route's `base_environment` defaults to `production`, and `knoxcall_route` owns that environment's configuration row itself. `knoxcall_route_environment` refuses the base environment for exactly that reason — two resources managing one row is how a module fights itself.
* **`{{secret_id:<uuid>}}` is resolved server-side**, at proxy time. The header your upstream receives carries the real credential; the header your route *configuration* carries is a reference to a UUID. Nothing in Terraform ever holds the value.
* **`value_wo` is write-only and `value_wo_version` is the rotation trigger.** Terraform never sees a write-only value, so it cannot notice you changed one. Changing `value_wo` alone does nothing at all; the integer is what says "this is different now".
* **The `for_each` on `knoxcall_route_environment` iterates the environment resources**, not a list of names. That is what makes Terraform create the environment before the override that references it.

<Warning>
  **Each environment needs its own value for the secret, and the provider cannot write those yet.** `knoxcall_secret.value_wo` sets the value for the secret's **base environment** — the tenant's default. A request carrying `x-knoxcall-environment: staging` looks up the staging value and, if nobody has set one, fails with

  ```
  Secret 'stripe-key' (9c8b7a65-…) has no value configured for environment 'staging'
  ```

  Add the other environments' values in the dashboard (**Secrets → your secret → Environments**) or through the API:

  ```sh theme={"dark"}
  curl -X PUT "https://api.knoxcall.com/v1/secrets/$SECRET_ID/value?environment=staging" \
    -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
    -d '{"value":"sk_test_..."}'
  ```

  A per-environment value is not a Terraform resource today. That is a real gap, not a recommendation — it is stated here rather than left for you to discover on the first staging request.
</Warning>

## Apply

```sh theme={"dark"}
export KNOXCALL_CLIENT_ID=tk_live_...
export KNOXCALL_CLIENT_SECRET=...
export TF_VAR_stripe_key=sk_live_...

# No `terraform init` while dev_overrides is in effect — see the install guide.
terraform plan
terraform apply
```

Terraform prints a warning that provider development overrides are in effect. That is expected, and it is also your reminder that this is a local build rather than a released provider.

## Call the route

The route is live as soon as the apply finishes:

```bash theme={"dark"}
TOKEN=$(curl -s -X POST https://api.knoxcall.com/oauth/token \
  -u "$KNOXCALL_CLIENT_ID:$KNOXCALL_CLIENT_SECRET" \
  -d "grant_type=client_credentials" | jq -r .access_token)

curl -X GET "https://a1b2c3d4.acme.knoxcall.com/v1/charges" \
  -H "x-knoxcall-route: stripe-payments" \
  -H "Authorization: Bearer $TOKEN"
```

KnoxCall matched the route by the `x-knoxcall-route` header, decrypted the secret referenced by the route's injected `Authorization` header, and forwarded the request upstream with it. Your caller never held the Stripe key, and neither does your state file.

Add `-H "x-knoxcall-environment: staging"` and the same route resolves through the `staging` row instead — its own upstream URL, its own rate limit, its own value for the secret. Omit the header and you get the tenant's default environment.

## Rotate the secret

Rotation is one integer:

```diff theme={"dark"}
 resource "knoxcall_secret" "stripe" {
   name             = "stripe-key"
   value_wo         = var.stripe_key
-  value_wo_version = 1
+  value_wo_version = 2
 }
```

`terraform apply` sends the new value and nothing else. The plan shows the version change, never the value. Server-side it is recorded as a `rotate` event in your tenant's audit log, which is where the history of a value Terraform cannot display actually lives.

<Warning>
  Changing `value_wo` **without** bumping `value_wo_version` is inert: the new value is not sent, no diff appears, and the apply reports success. Drive both from one variable where you can.
</Warning>

## What ends up in your state file

The point of the module above is what is missing from `terraform.tfstate`: the Stripe key. A value you write into `value_wo` is read from your configuration, sent to the API, and nullified by Terraform before it can reach a plan or a state file — and KnoxCall's API never returns a stored secret value, so there is nothing to read back either.

Two things this does *not* cover, both stated in full on the [provider page](/sdks/terraform#secrets-and-terraform-state):

* Credentials **KnoxCall mints and hands back once** — a webhook's generated `secret_key`, an API key's plaintext, an OAuth client's secret, an issued client-certificate private key — are captured into state, because a value the server generates has to be `Computed`, and the plugin framework forbids `Computed` together with write-only. Each has a documented alternative; the webhook one is to sign with `hmac_key_id` instead, so no signing secret exists anywhere in Terraform.
* **A rotation performed outside Terraform is invisible to `plan`** unless you pin `value_version`. Terraform has no copy of the value to compare.

## Where to go next

* [Terraform provider guide](/sdks/terraform) — authentication modes, the sandbox data space, the full resource and import-ID map, and the client's retry/idempotency behaviour.
* [Creating your first route](/getting-started/first-route) — what a route actually does at request time.
* [Environments](/getting-started/first-environment) — how per-environment overrides resolve.
