Skip to content

Secrets & rotation

Store credentials as secrets and non-sensitive configuration as variables. Both are resolved per environment, so the same endpoint or task can target production in one binding and staging in another.

  • A secret is encrypted at rest, write-only, and referenced by name. There is no API that returns its value.
  • A variable is plaintext by design and returned by the API. Names that look like credentials (TOKEN, KEY, PASSWORD, …) get a warning on create; move those values to secrets.
  • An environment is a named scope inside a workspace. Every workspace starts with the default production environment (max 10 environments). A secret or variable row with environmentId: null is the project-level default; a row bound to an environment overrides it.
{
"headers": { "x-api-key": "{{secret.API_TOKEN}}" },
"auth": { "mode": "bearer", "token": "{{secret.API_TOKEN}}" }
}
  • {{secret.NAME}} — resolves an encrypted secret. Names match ^[A-Z][A-Z0-9_]{0,63}$; lowercase in a secret reference is not a secret (it belongs to the payload template engine).
  • {{var.NAME}} — resolves a plaintext variable with the same name rules.

Where each is available:

Context {{secret.NAME}} {{var.NAME}}
Destination URL No (URLs are recorded; use a variable for host) Yes
Destination headers / query Yes Yes
Destination auth Yes Yes
Destination adapter config Yes Yes
Destination body template No — bodies never depend on stored credentials Yes
Task step with (any field) Yes Yes

Both resolve before the outbound request is built. A missing reference fails the delivery with OUTBOUND_REQUEST_BLOCKED (Secret resolution failed: Missing secret: NAME or Variable resolution failed: Missing variable: NAME); a literal {{secret.X}} is never sent.

Resolution happens in two steps:

  1. Choose the environment.
    • Deliveries: a one-off test event (environmentId on test-events / preview) wins over the endpoint’s environmentId binding; no binding means the default environment.
    • Tasks: a run-level environmentId override wins over the task’s binding; no binding means the default environment.
    • Replays and retries run through the same pipeline, so they use the endpoint’s current binding.
    • If a bound environment was deleted, delivery falls back to the default rather than failing on stale metadata.
  2. Resolve each name inside that environment. An environment-scoped row wins over the project-level row of the same name. If the chosen environment has no override, the project-level value applies.

Example: with API_TOKEN=prod-… unbound and API_TOKEN=staging-… bound to staging, an endpoint bound to staging resolves staging-…; the same endpoint moved back to the default resolves prod-….

Environment bindings live on the endpoint row (environmentId, null = default) and on the task row. Use PATCH /v1/endpoints/{id} or PATCH /v1/tasks/{name} with environmentId to change them. GET /v1/environments shows binding counts, and GET /v1/environments/{id}/resolved lists which secret names come from the environment versus the project default (secret values are never included).

Only workspace owners and admins may create, replace or delete secrets and variables; members get 403 AUTHORIZATION_DENIED. Reads use the endpoints:read scope, writes the endpoints:write scope. Audit entries record the action, actor and resource id — never a value.

Each endpoint also has its own signing secret. Treat it like a password and rotate it without downtime:

  1. Add the new secret to your verifier and accept both old and new signatures.
  2. Rotate in Hooks from the endpoint’s security settings, or call POST /v1/endpoints/{id}/rotate-secret.
  3. Wait for in-flight retries to finish, then remove the old secret from your verifier.

Secrets are encrypted at rest and never displayed in full after creation. If a secret leaks, rotate immediately and review delivery logs for unexpected destinations or replay attempts.