Skip to Content

Agent exposure

By the end of this page you will have exposed an agent on an installed appliance and called it with an API key.

An appliance has no Studio, so exposure is set in one file on the install host: config/api-keys.yaml. The gateway re-reads it within about 5 seconds of a save, so exposing an agent, adding a key or revoking one never needs a restart.

Prerequisites

  • An installed, running appliance. See Installing it.
  • Shell access to the install host, in the archive folder.
  • Model credentials in .env. The appliance never carries qRaptor’s model credentials, so an AI agent or multi-agent system can’t answer until you add your own provider’s key. See Connecting models.
  • The roles your agents’ data needs. A key acts with the roles you give it, and only sees the tables those roles can read. See Give the key a role.

How it works

A call goes through only when both of these allow it:

ControlsOn a fresh install
ExposureWhether an agent or multi-agent system can be called with a key at allEvery agent and system is off
API keyWho is calling, what they may do (scopes), which roles it acts with, and optionally which agents they may reachTwo sample keys that don’t work until you replace their values

Keys only reach agent, multi-agent system and conversation endpoints — never your app’s own /api routes.

Find your agents’ endpoints

Each export lists the endpoints of every agent and system it carries, with their IDs:

  • Studio — the export’s API access tab on the project’s Appliance page.
  • The archive — the API access section of README.md.
Agent typeEndpointPurposeScope
AI agentPOST /agents/{id}/runAsk once, get a JSON answerinvoke
POST /agents/{id}/streamSame, streamedinvoke
POST /agents/{id}/conversationMulti-turn chatinvoke
GET /agents/{id}/infoAgent detailsinfo:read
GET /agents/{id}/runsPast runsruns:read
Task AgentPOST /agents/{id}/runRun with input variablesinvoke
GET /agents/{id}/runsPast runsruns:read
GET /agents/{id}/runs/{runId}One run’s resultruns:read
GET /agents/{id}/runs/{runId}/streamOne run’s step-by-step traceruns:read
Multi-agent systemPOST /mas/{id}/conversation/streamSend a message, streamedinvoke
GET /mas/{id}/agents/skillsMember agents and their skillsinfo:read

Expose an agent

Generate a key

KEY="qr_$(openssl rand -hex 24)" echo "$KEY"

Keys must start with qr_. Save the value in your secrets manager.

Turn on exposure

In config/api-keys.yaml, set enabled: true on each agent or system you want callable:

exposed: agents: - id: 3f6c2a10-7d4e-4b8a-9c21-5e0f8a7b6d34 name: Order Details Agent type: task_agent enabled: true

Keep the IDs already in your file — they’re your export’s agents.

Add the key

Under keys, replace a sample’s key: value with your key, or add an entry:

keys: - name: order-portal key: qr_your_generated_key scopes: [invoke, "runs:read", "info:read"] roles: [admin] restrict_to: - 3f6c2a10-7d4e-4b8a-9c21-5e0f8a7b6d34 enabled: true

name shows up in logs and run history. roles decides which tables the agent can read on this key’s behalf — see Give the key a role. restrict_to limits the key to the listed IDs; leave it out to allow every exposed agent.

Save and confirm

docker compose logs gateway --since 1m | grep appliance_api

Look for appliance_api_keys_loaded … keys=1 exposed_agents=1.

Call it

curl -sS -X POST "https://apps.example.com/agents/3f6c2a10-7d4e-4b8a-9c21-5e0f8a7b6d34/run" \ -H "Authorization: Bearer $KEY" \ -H "Content-Type: application/json" \ -d '{ "agent_id": "3f6c2a10-7d4e-4b8a-9c21-5e0f8a7b6d34", "variables": { "order_id": "0b9e7d21-4c3a-4f5e-8a6b-7c1d2e3f4a5b" }, "execution_mode": "production" }'

Use your QR_PUBLIC_URL as the address.

Give the key a role

An agent reads your app’s data as the key that called it. The key’s roles decide which tables it can see, with the same data governance rules your app’s users get — the permissions you set in Studio under your app’s security travel with the export.

A key with no roles acts as guest. If guest can’t read a table, the agent can’t either: it reports the table doesn’t exist, finds no rows, or gives a vague answer — the call itself still returns 200.

To fix it, give the key a role that can read what the agent needs:

- name: order-portal key: qr_your_generated_key scopes: [invoke, "runs:read", "info:read"] roles: [admin]
ℹ️

Roles and their table permissions come from your project and are sealed in the export — you can’t widen them on the appliance. If no role can read a table an agent needs, grant it in Studio under your app’s security, then export again. Previewing an agent in Studio runs as you, the builder, and skips these permissions, so test with the role your integration will use.

Choose the narrowest role that works. A key with admin reads everything admin can — for a public-facing integration, prefer a role limited to the tables it needs.

Request examples

Every request sends Authorization: Bearer qr_…. Set BASE to your QR_PUBLIC_URL.

AI agent — ask once:

curl -sS -X POST "$BASE/agents/$AGENT_ID/run" \ -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \ -d '{"message": "fetch me orders placed by jane.doe@example.com", "input_variables": {}}'

AI agent — streamed (-N shows events as they arrive):

curl -sS -N -X POST "$BASE/agents/$AGENT_ID/stream" \ -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \ -d '{"message": "fetch me orders placed by jane.doe@example.com", "input_variables": {}}'

AI agent — conversation. Start with session_id: null, then send back the sessionId from the response as session_id on each next message:

curl -sS -X POST "$BASE/agents/$AGENT_ID/conversation" \ -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \ -d '{"message": "Hello", "session_id": null, "input_variables": {}}'

Task Agent — a run’s result and trace (run ID from the run response or GET /agents/{id}/runs):

curl -sS "$BASE/agents/$AGENT_ID/runs/$RUN_ID" -H "Authorization: Bearer $KEY" curl -sS -N "$BASE/agents/$AGENT_ID/runs/$RUN_ID/stream" -H "Authorization: Bearer $KEY"

Multi-agent system:

curl -sS -N -X POST "$BASE/mas/$SYSTEM_ID/conversation/stream" \ -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \ -d '{"message": "A customer is unhappy that their order is late", "input_variables": {}}'

Key reference

FieldRequiredMeaning
nameYesLabel shown in logs and as the caller of every run
keykey or key_sha256The key, starting qr_
key_sha256key or key_sha256The key’s SHA-256 instead, so the secret isn’t stored on disk: printf %s 'qr_…' | sha256sum
scopesYesWhat the key may do (below)
restrict_toNoAgent and system IDs the key may call. Omit for all exposed ones
rolesNoRoles the key acts with. Agents that require a role check them, and data governance rules for those roles apply
expires_atNoWhen the key stops working, e.g. "2027-01-31T00:00:00Z"
enabledNofalse turns the key off. Defaults to true
ScopeAllows
invokeRunning agents, streaming, conversations, multi-agent system messages
runs:readRun lists, results and traces
runs:writeCancelling runs
info:readAgent details and a system’s member skills
hil:resumeResuming a run paused at a HIL node
conversations:read, conversations:writeReading and managing conversation history
"*"All of the above (quote it in YAML)

Each key acts as its own caller — runs one key starts aren’t visible to another.

Rotate and revoke

Changes apply within about 5 seconds of saving.

  • Revoke — delete the key’s entry, or set enabled: false.
  • Rotate — add the new key, move your integration to it, then delete the old one.
  • Close an agent — set its enabled: false under exposed. Every key loses access at once.
⚠️

If a save leaves the file with a YAML error, the gateway keeps the last working version and logs appliance_api_keys_unparseable. A revocation in a broken file hasn’t taken effect — after revoking, confirm appliance_api_keys_loaded appears.

config/api-keys.yaml isn’t part of the signed bundle, so editing it never fails verification. Keep a copy when you upgrade: unpacking a new archive writes a fresh, all-off file.

Troubleshooting

ResponseCauseFix
401 Invalid API keyNo entry matches the keyCheck the value. A new shell loses $KEY
401 API key disabled / API key expiredenabled: false, or expires_at has passedRe-enable it or issue a new key
403 … is not exposed for API accessThe agent’s enabled is false under exposedSet it to true
403 This API key is not permitted to call this agentThe ID isn’t in the key’s restrict_toAdd it
403 Insufficient API key scope: this endpoint requires '…'The key lacks that scopeAdd the scope
403 API keys can only call agent, MAS and conversation endpointsThe key was used on an app routeCall the agent endpoints instead
200, but the agent says a table doesn’t exist or finds no dataThe key has no roles, so it acts as guest, which can’t read that tableGive the key a role that can
200, but the answer says the model or provider failedNo model credentials in .envAdd them — see Connecting models — then docker compose up -d llm-proxy
200, but the answer is an errorThe agent itself faileddocker compose logs agent-runtime --since 5m
Nothing changes after savingThe file has a YAML errordocker compose logs gateway | grep appliance_api names it