Skip to content

Credentials and identity

How the agent authenticates in your system, how it knows who the end user is, and what to do when only login and password exist — with what the model sees and what it never sees.

Updated on Sep 02, 2026

Every agent that does something useful needs to answer three different questions — and most security problems come from treating them as if they were one:

  1. Who is the system? Your company's credential on your API.
  2. Who is the person? The identity of who is talking.
  3. Who can act? What that person is allowed to do.

Each one has its right place in the agent. None of them is solved by asking for a password in the chat.

What the model sees and what it never sees

InformationReaches the model?Where it lives
auth from the Webhook card (token, key, basic password)**No**In the node config. The runtime injects it directly into the HTTP call.
defaultHeaders and route headers**No**Same — built at request time.
baseUrl and the route pathNo (only the tool name)Node config.
Route parameters**Yes** — the model fills them inCome from the conversation, appear in the tool_use event of the stream.
Route response**Yes**Returns as the tool result and enters the context.
Run variables (variables)Only if you put them in the promptResolved in {{name}} inside the system_prompt.

Verified in practice

In a test with an echo server, the authenticated route reached the destination with Authorization: Bearer … and the custom header — and the corresponding tool_use event came out with empty input: no piece of the credential passed through the model or the stream. It's explicit runtime behavior, not coincidence.

The practical consequence

Everything that needs to enter a route passes through the model. That's why an opaque client identifier (acct_1042) is acceptable there, and a password, a personal token, or a card number are not.

Where {{variables}} are resolved — and where they are not

This table prevents most errors when building your first flow with identity. The variables from the run do not reach everywhere on their own:

PlaceResolves {{var}}?Note
Agent system_prompt**Yes**It's the normal path: the value appears to the model, which copies it to the route.
If/Else and While conditions**Yes**Workflow expression engine.
URL and headers of the **MCP** card**Yes**Resolved at connection, **without passing through the model** — the only path for per-user secrets.
auth, baseUrl and headers from the **Webhook** card**No**They are static per node. A {{token}} there travels literally and the call fails.
path, queryParams and bodyTemplate of a routeOnly with **route parameters**Not with run variables. The value must come from the model.

The error that appears in almost every first flow

Declare identifier as a run variable, use {{identifier}} in the route — and forget to write the value in system_prompt. In a real test, the agent sent identifier: "unknown": it didn't have the value and filled the field with a guess. The fix is one line in the prompt: identifier: {{identifier}}.

no system_prompt do Agent
DADOS DESTE CANAL — use exatamente estes valores nas ferramentas,
sem perguntar e sem inventar:
- channel: {{channel}}
- identifier: {{identifier}}

The four patterns that solve 99% of cases

1. Service credential — the agent is a system

The entire agent talks to your API using a company credential, configured in the Webhook card. It's the standard in the vast majority of cases: support, order lookup, ticket opening, scheduling.

auth do card Webhookjson
"auth": { "type": "bearer",  "token": "sk_live_..." }
"auth": { "type": "api_key", "apiKey": "...", "apiKeyName": "X-API-Key", "apiKeyLocation": "header" }
"auth": { "type": "basic",   "username": "agente", "password": "..." }
CautionWhy
Key with **minimal scope**The agent can call any route enabled in the card. Give it a credential that only does what the routes do.
Key **exclusive to the agent**If you need to revoke, you revoke the agent — not your entire app. And your API log shows who it was.
Write routes separatedSee the [approval](/en/api/agents/flows/orders-desk) pattern: read in the conversation agent, write only after the lock.

2. Identity from your application

The conversation starts in a place where the person is already authenticated — the app, the logged area, the portal. Your application already knows who they are: send the identifier in the variables of the run.

POST /v1/agents/{id}/runjson
{
  "message": "cadê meu pedido?",
  "variables": { "customer_id": "acct_1042" },
  "stream": true
}

The customer_id enters the system_prompt as {{customer_id}} and the model passes it to the routes. Two rules that make this pattern secure:

  • The identifier is opaque — an internal id, not CPF, email or phone.
  • Your API checks the scope — the route validates that that id really belongs to the caller, because the value passed through the model and, in theory, can be induced.

This is the pattern of the Support with tickets and Post-sale flows.

3. Per-user secret without passing through the model (MCP)

When the call needs to carry that person's token — not the company's — there is a path where the secret never reaches the model: the MCP card resolves {{variables}} in the URL and connection headers, at connection time.

config do card MCPjson
{
  "servers": [{
    "name": "erp",
    "type": "http",
    "url": "https://mcp.suaempresa.com/erp",
    "headers": { "X-User-Token": "{{user_token}}" }
  }]
}

The user_token arrives in the variables of the run, is injected into the connection header and does not enter the prompt or the tool event. The price is that your system needs to expose an MCP server, not just a REST API.

Don't try this in the Webhook card

A {{token}} inside Webhook auth is not resolved: it goes literally to the header and the call fails with 401. Variable resolution exists today only in the MCP card.

4. Channel binding by single-use code

The conversation starts in an open channel — WhatsApp, Instagram, website chat — and nobody authenticated anything. This is the case where the question "do I ask for username and password?" usually comes up. The answer is bind the channel to the account once, with a single-use code sent to the registered email.

After binding, the agent never asks who the person is again: the routes receive channel + identifier (the channel address, which is not a secret) and your back-end resolves the account.

Identification without password

The complete flow, with the 3 binding routes, the PII gatekeeper and what your back-end needs to guarantee.

Why not ask for password in the conversation

It's not purism: it's the path the value takes. A password typed in the chat becomes a message, and a message:

Will end up inFor how long
Conversation history (thread)As long as the thread exists — and it exists for the customer to continue later.
Model contextIn all subsequent rounds of the turn.
Run stream (SSE)To whoever is monitoring the execution.
Logs and observabilityWherever the message is logged.

And the token that the login route would return travels the same path back. In one sentence: the conversation is not a vault and should not become one.

What about the legacy system that only has login and password?

SituationWhat to do
The user is already logged into your appPattern 2: send the id in variables. Your back-end uses the service credential to talk to the legacy system.
Open channel, no loginPattern 4: code linking. Better than password, even in security — there is no reusable secret in transit.
You really need to act *as* that user in the legacy systemYour back-end does the login (server-to-server), stores the session and exposes a route by channel/identifier. The agent never sees the credential.
Needs personal token in the call headerPattern 3, with MCP — the only way the secret doesn't pass through the model.

If it's still unavoidable

If a specific case forces you to collect a credential in the conversation, treat it as a controlled incident: private channel, single-use credential, expiration in minutes, immediate swap for a short token on your side, and never repeat the value in the response. Honestly, prefer to redesign the flow.

The safety net: the Gatekeeper card

The customer will send what they shouldn't — CPF, card photo, password — because that's what they've done for twenty years with human support. The Guardrails card inspects the message before the agent and routes it to its own exit.

config do card Guardrailsjson
{
  "target": "input",
  "checks": {
    "pii": true,
    "blocklist": ["senha", "cartão de crédito"],
    "jailbreak": false,
    "moderation": false,
    "custom": ""
  },
  "fail_message": "A mensagem parece conter um dado sensível."
}
CheckHow it worksCost
piiDeterministic regex for CPF, CNPJ, card (with Luhn validation), email and phone.Zero
blocklistForbidden terms or expressions.Zero
jailbreak · moderation · customClassification by model, all three in a single call.One inference per message

The pass and fail outputs work like If/Then: on the fail path, an agent with no tools politely asks the person not to send that data — without repeating the value. It's the flow design for Identification without password.

Checklist before publishing

The agent can call any enabled route. Its key must be able to do exactly that — nothing more. And it must be exclusive, so it can be revoked on its own.

The scope is per slot: each agent only sees the cards linked to it. Enable the write Webhook only on the executor agent — and when two agents share the same card, use tools_filter for each to expose only its subset.

The value went through the model. Treat it as user input: the route validates that the id belongs to the caller.

Route parameter is filled by the model and appears in the stream. If any field like this exists, the design is wrong — go back to standards 3 and 4.

Optional parameter that the model doesn't fill travels literally as {{name}} in the query string, in the path, or in the body. Declare required: true and say in the text which value to use when there isn't one.

The pii check costs zero and prevents documents and cards from entering the conversation history.

The error comes back to the model as text and it improvises a response. Write in the prompt what to do when the tool fails — usually: tell the truth and offer another path.

Ready-made flows

The four patterns above assembled, with JSON to import and test.