Skip to main content
An MCP server or webhook tool normally authenticates with one static credential, stored as an integration secret and referenced from the assistant configuration. Every conversation uses the same one. That does not work when the credential belongs to the end user. If each caller has their own bearer token for an MCP server, a single static credential either over-shares — one token that can reach everyone’s data — or cannot be used at all. Encrypted dynamic variables solve this. The dynamic variables webhook returns a credential encrypted with a key only the account holds, and Telnyx decrypts it at the moment it authenticates that one conversation’s request.

How it works

  1. Store a key. Generate a 256-bit key and save it as an integration secret.
  2. Return a ciphertext. The dynamic variables webhook returns the caller’s credential in a new encrypted_dynamic_variables section, encrypted with that key.
  3. Reference it. The assistant configuration points at the variable and the key with {{variable | encryption_secret_ref}}.
  4. Telnyx decrypts it only where a credential is actually needed, for that conversation only.
Decrypted values exist in memory at the moment of use. They are never stored, never written to a log, and never visible to the model.

1. Store the encryption key

Generate 32 random bytes and store them base64url-encoded as an integration secret. The secret’s identifier is what the configuration references.
Store it with POST /integration_secrets. The identifier is what the configuration references:
Padding is optional — a value with or without trailing = is accepted. Keep the raw key: the webhook needs it to encrypt.

2. Return encrypted variables from the webhook

The dynamic variables webhook response gains an optional encrypted_dynamic_variables section, a sibling of dynamic_variables:
The two sections are separate namespaces. An encrypted variable is never substituted into instructions, greetings, messages, or tool descriptions — only into the credential positions in step 3. A plain dynamic variable is never usable as a credential. Using the same name in both sections is not an error, but it is almost always a mistake, and Telnyx flags it as one.

Encryption scheme

AES-256-GCM, nonce-prefixed, base64url-encoded:
  • Nonce: 12 random bytes, unique per encryption, prefixed to the ciphertext.
  • Tag: the standard 16-byte GCM tag, appended by the cipher.
  • Plaintext: an opaque UTF-8 string, at most 8 KB. No associated data.
Because GCM is authenticated, a wrong key or a modified ciphertext is detected and treated as a failed credential rather than partial plaintext.
There is no openssl enc equivalent: that command does not support GCM.

Key rotation

Update the integration secret’s value. Ciphertexts produced with the old key fail authentication — and the affected requests fail safely, per failure behavior — until the webhook encrypts with the new one. Rotate the key and the webhook together.

3. Reference the credential

Reference an encrypted variable with pipe syntax, naming the variable and the secret holding its decryption key. Whitespace around the parts and the pipe is optional.
It is accepted in exactly two places.

MCP server credential

The whole api_key_ref value is one reference:
Each conversation’s decrypted mcp_token becomes that conversation’s bearer token for the server. A plain identifier in api_key_ref keeps its existing meaning — one static integration secret for every conversation. The two forms are mutually exclusive per server.

Tool webhook header values

As a token inside a configured header value, alongside the existing {{#integration_secret}} and {{plain_variable}} syntax:

Everywhere else

Anywhere else — instructions, greetings, messages, tool descriptions, webhook URLs, preset body fields, preset query parameters — the reference is not resolved. Where Telnyx can tell at save time that a reference sits in a position that cannot resolve one, the configuration is rejected with a 422. Reading a configuration back always returns the reference verbatim. A decrypted value is never echoed on a configuration endpoint.

When credentials refresh

Each webhook response replaces the stored set for that conversation in full, so a token refreshed on one turn is the token used on the next. A response that omits the section leaves the previous set in place; a response with an empty section clears it.

Validation

Rejected at save time with a 422:
  • A value that looks like a reference but does not parse. A typo is never quietly downgraded to a plain variable, because that would send the literal {{…}} as a credential.
  • An encryption_secret_ref that names no integration secret on the account.
  • A reference in a position that cannot resolve one.

Failure behavior

A credential that cannot be resolved at conversation time — the variable is missing from that conversation’s set, the key secret is unavailable, or decryption fails — is never worked around: There is no fallback to another credential, and the literal {{…}} is never sent. The failure is recorded with the variable name, the secret reference, and what went wrong — never with key material, ciphertext, or any part of the plaintext.
An assistant whose MCP server drops out of a conversation loses that server’s tools for the whole conversation. If a credential is optional for some callers, configure a second assistant or a workflow branch rather than relying on partial resolution.

Confidentiality

  • Values are stored and logged only as ciphertext.
  • A decrypted value exists in memory only, at the moment it authenticates a request. It is never written to a log, a conversation record, or a webhook log.
  • Encrypted variables cannot reach model-visible text: the templating that renders instructions, greetings, and tool descriptions does not resolve the pipe form at all.
  • Decrypted values are never returned by a configuration endpoint.

End-to-end example

  1. Store base64url(32 random bytes) as integration secret mcp_enc_key.
  2. Configure the MCP server with "api_key_ref": "{{mcp_token | mcp_enc_key}}".
  3. Point the assistant’s dynamic_variables_webhook_url at the endpoint.
  4. On each webhook call, identify the caller from the payload, fetch that user’s token, encrypt it, and return it:
Every MCP request in that conversation now carries Authorization: Bearer <that user's token>. Two concurrent conversations for two different callers use two different tokens, and neither can reach the other’s data.