Documentation Index
Fetch the complete documentation index at: https://docs.qlaud.ai/llms.txt
Use this file to discover all available pages before exploring further.
The qlaud-builtin/http-call scaffold is qlaud’s universal Tier-3 tool:
any HTTP endpoint becomes a callable tool with one config blob, zero
code, and zero infrastructure on your side. The gateway makes the
outbound request directly from the edge worker and returns the response
to the model.
When to reach for it:
- You have an internal API the AI should be able to call.
- You want to expose a third-party SaaS endpoint that doesn’t have an
MCP server and isn’t in our named scaffold catalog.
- You need precise control over the request shape (specific headers,
custom auth, GraphQL queries, form-encoded bodies).
- You want session context (
end_user.id, metadata) auto-injected from
the trusted thread — never from model-supplied args.
If you instead want one of the named scaffolds we already maintain
(Resend email, Linear ticket, Slack message, Twilio SMS, GitHub issue,
…), see the builtins catalog — those exist
specifically so you don’t have to author the http-call config yourself.
Anatomy of a valid config
Every http-call config has the same six knobs. Only url is required;
everything else has a sensible default.
| Field | Type | Required | Default | What it is |
|---|
url | string | yes | — | Full endpoint URL. Must start with http:// or https://. Supports template placeholders. |
method | string | no | POST | One of GET, POST, PUT, PATCH, DELETE. |
headers | string (JSON) | no | "{}" | JSON-stringified object of header name → value. Values support placeholders. |
body_template | string | no | "" | Request body. Either a JSON-template string or empty (no body). Sent as-is — set the right content-type header to match. |
secrets | string (JSON) | no | "{}" | JSON-stringified object of secret name → value. Referenced via {{secrets.X}}. Stored AES-GCM encrypted; never returned by any read endpoint. |
lock_input_fields | string (JSON) | no | "[]" | JSON-stringified array of input keys whose model-supplied values get overridden with trusted session values before the template renders. |
response_max_bytes | string (number) | no | "8192" | Cap on response bytes returned to the model. Truncated bodies append a clear […truncated…] notice. Hard ceiling 1 MiB. |
timeout_ms | string (number) | no | "30000" | Per-call timeout. Hard ceiling 60000. |
Every config field is declared as type: string in the schema —
including JSON objects/arrays and numbers. They’re stored as strings
so the dashboard form renders consistently; the gateway parses them at
dispatch time. This means headers, secrets, and
lock_input_fields must be JSON-stringified, not raw objects.
Template syntax
Placeholders use {{...}} mustache syntax. Five namespaces are available:
| Placeholder | Source | Trustworthy? |
|---|
{{config.X}} | The developer-supplied config (url, method, etc.) | Yes — set at registration |
{{secrets.X}} | The developer-supplied secrets object | Yes — encrypted at rest |
{{args.X}} | Model-supplied tool input (matches your input_schema) | No — model controls these |
{{end_user.id}} | The thread’s end_user_id | Yes — set at thread creation |
{{end_user.metadata.X}} | Any field from the thread’s metadata object | Yes — set at thread creation |
Dot-paths drill into nested objects: {{end_user.metadata.profile.email}}
resolves to metadata.profile.email.
Missing paths render as the empty string. They do not error — this
keeps templates resilient when an optional metadata field is absent.
Object/array values are JSON-stringified when interpolated, so:
"body_template": "{\"items\": {{args.items}}}"
with args.items = ["a", "b"] renders as {"items": ["a","b"]}.
Lock semantics — the security primitive
The single most important field is lock_input_fields. It’s how you
prevent the classic prompt-injection abuse: the model being tricked into
filling a sensitive field with attacker-supplied content (e.g. setting
the to: of an email to a different person).
When you list a key in lock_input_fields, the gateway:
- Drops whatever the model put in
args.<key>.
- Replaces it with
end_user.metadata.<same_key> if present.
- Or, for the canonical id keys (
user_id, end_user_id,
customer_id), falls back to end_user.id.
- If neither source has a value, drops the key from
args entirely
(the request still fires, but with the field absent).
Use it on every input key whose value MUST belong to the logged-in
end-user. Examples:
to (email) → locked to end_user.metadata.email
phone (SMS) → locked to end_user.metadata.phone
customer_id (account lookup) → locked to end_user.id
account_id, team_id (anything tenant-scoped to the caller)
Skip it when the model legitimately should pick the value (e.g. an AI
support agent emailing a third-party supplier).
A complete, valid config (annotated)
This is the single most important reference on the page. Every
http-call registration looks like this:
{
"name": "lookup_subscription",
"description": "Look up the customer's current subscription tier from our internal API. Use this when the user asks 'what plan am I on?' or anything billing-related.",
"provider": "qlaud-builtin/http-call",
"input_schema": {
"type": "object",
"properties": {
"customer_id": {
"type": "string",
"description": "The customer's internal ID. Always equals the logged-in user."
}
},
"required": ["customer_id"]
},
"config": {
"method": "GET",
"url": "https://api.acme.internal/customers/{{args.customer_id}}/subscription",
"headers": "{\"authorization\": \"Bearer {{secrets.internal_token}}\", \"accept\": \"application/json\"}",
"secrets": "{\"internal_token\": \"sk_internal_REAL_TOKEN_HERE\"}",
"lock_input_fields": "[\"customer_id\"]"
}
}
POST that JSON to https://api.qlaud.ai/v1/tools with your master-scope
key in the Authorization header and you have a working tool.
Validation rules — what “valid” means
The gateway accepts a config if all of these hold:
url is present and starts with http:// or https://.
method is one of GET POST PUT PATCH DELETE (or absent).
headers, secrets, lock_input_fields parse as JSON if non-empty.
headers parses to an object (not array, not primitive).
secrets parses to an object.
lock_input_fields parses to an array of strings.
timeout_ms and response_max_bytes are positive integers if set.
Invalid configs return a 400 from POST /v1/tools with a message
identifying the bad field.
At dispatch time (when the model invokes the tool), additional checks:
- Network errors → tool result with
is_error: true and a clear message.
- Timeouts → tool result with
is_error: true and the timeout duration.
- 4xx/5xx upstream → tool result with
is_error: true, the response
body included (truncated if huge), and the upstream status code.
- Successful 2xx → response body parsed as JSON if possible, else
returned as plain text.
Recipes — copy-paste these and adapt
Each recipe is a complete, valid POST /v1/tools body. Replace the
secret values + URLs with your own.
1. Look up a customer in your internal database
{
"name": "get_customer_account",
"description": "Look up the logged-in customer's account details (plan, status, last_login) from our internal API.",
"provider": "qlaud-builtin/http-call",
"input_schema": {
"type": "object",
"properties": {},
"required": []
},
"config": {
"method": "GET",
"url": "https://api.your-app.com/internal/users/{{end_user.id}}",
"headers": "{\"authorization\": \"Bearer {{secrets.api_token}}\"}",
"secrets": "{\"api_token\": \"sk_replace_me\"}"
}
}
Notice no lock_input_fields is needed — the URL uses {{end_user.id}}
directly from the trusted session, so the model can’t influence which
user is fetched.
2. Send a Slack alert to the customer’s CSM channel
{
"name": "alert_csm",
"description": "Send a Slack alert to the customer's dedicated CSM channel. Use for escalations, churn risks, or urgent feedback.",
"provider": "qlaud-builtin/http-call",
"input_schema": {
"type": "object",
"properties": {
"message": { "type": "string", "description": "What to post." }
},
"required": ["message"]
},
"config": {
"method": "POST",
"url": "https://slack.com/api/chat.postMessage",
"headers": "{\"authorization\": \"Bearer {{secrets.slack_bot_token}}\", \"content-type\": \"application/json; charset=utf-8\"}",
"body_template": "{\"channel\": \"{{end_user.metadata.csm_channel}}\", \"text\": \"From {{end_user.metadata.email}}: {{args.message}}\"}",
"secrets": "{\"slack_bot_token\": \"xoxb-replace-me\"}"
}
}
When you create the thread, pass the CSM channel + email as metadata:
curl -X POST https://api.qlaud.ai/v1/threads \
-H "Authorization: Bearer $QLAUD_KEY" \
-d '{
"end_user_id": "u_alice",
"metadata": {
"email": "alice@acme.com",
"csm_channel": "C0123ABC456"
}
}'
3. File a Linear ticket — but use the named scaffold instead
For Linear specifically, use the named scaffold — it’s friendlier:
{
"name": "file_linear_bug",
"provider": "qlaud-builtin/linear-create-issue",
"config": {
"api_key": "lin_api_REPLACE_ME",
"team_id": "TEAM-ABC-UUID"
}
}
Reach for http-call only when the named scaffold doesn’t fit (custom
GraphQL fields, non-standard auth, etc.).
When the action is high-stakes (money, account changes), lock everything
the model could influence:
{
"name": "issue_refund",
"description": "Issue a partial refund to the logged-in customer. Use only when explicitly authorized by them.",
"provider": "qlaud-builtin/http-call",
"input_schema": {
"type": "object",
"properties": {
"customer_id": { "type": "string" },
"amount_cents": { "type": "integer", "minimum": 1, "maximum": 50000 },
"reason": { "type": "string" }
},
"required": ["customer_id", "amount_cents", "reason"]
},
"config": {
"method": "POST",
"url": "https://api.your-app.com/internal/refunds",
"headers": "{\"authorization\": \"Bearer {{secrets.api_token}}\", \"content-type\": \"application/json\"}",
"body_template": "{\"customer_id\":\"{{args.customer_id}}\",\"amount_cents\":{{args.amount_cents}},\"reason\":\"{{args.reason}}\"}",
"secrets": "{\"api_token\": \"sk_replace_me\"}",
"lock_input_fields": "[\"customer_id\"]"
}
}
The customer_id is locked to end_user.id — the model can suggest
“refund Bob 10"buttheactualcallalwaystargetsthelogged−incustomer.The‘amountcents‘schemaadditionallycapstherefundat500.
5. GraphQL query
{
"name": "search_internal_docs",
"description": "Search our internal documentation knowledge base.",
"provider": "qlaud-builtin/http-call",
"input_schema": {
"type": "object",
"properties": {
"query": { "type": "string" }
},
"required": ["query"]
},
"config": {
"method": "POST",
"url": "https://api.your-app.com/graphql",
"headers": "{\"authorization\": \"Bearer {{secrets.api_token}}\", \"content-type\": \"application/json\"}",
"body_template": "{\"query\":\"query Search($q: String!) { docs(query: $q) { id title snippet } }\",\"variables\":{\"q\":\"{{args.query}}\"}}",
"secrets": "{\"api_token\": \"sk_replace_me\"}"
}
}
{
"name": "trigger_workflow",
"description": "Kick off a long-running workflow on the customer's behalf.",
"provider": "qlaud-builtin/http-call",
"input_schema": {
"type": "object",
"properties": {
"workflow_name": { "type": "string" }
},
"required": ["workflow_name"]
},
"config": {
"method": "POST",
"url": "https://api.your-app.com/workflows/trigger",
"headers": "{\"authorization\": \"Bearer {{secrets.api_token}}\", \"content-type\": \"application/x-www-form-urlencoded\"}",
"body_template": "user_id={{end_user.id}}&workflow={{args.workflow_name}}",
"secrets": "{\"api_token\": \"sk_replace_me\"}"
}
}
Things that look right but aren’t
These are the patterns we see most often in mistaken configs. If you
generate a config and notice any of these, fix them before saving.
❌ Raw object instead of JSON-stringified for headers:
"headers": { "authorization": "Bearer xyz" } ← invalid
✅ Stringify it:
"headers": "{\"authorization\": \"Bearer xyz\"}"
❌ Quoting an object placeholder inside the body template:
"body_template": "{\"items\": \"{{args.items}}\"}"
This produces {"items":"["a","b"]"} (string-of-array). Drop the quotes:
"body_template": "{\"items\": {{args.items}}}"
❌ Forgetting content-type on a POST/PUT/PATCH body:
The body is sent as-is. If you intended JSON, include
"content-type": "application/json" in headers — most APIs reject
bodies without it.
❌ Putting secrets directly in headers:
"headers": "{\"authorization\": \"Bearer sk_REAL_KEY\"}" ← exposed
The headers blob is visible in dashboard read-paths to the developer.
Move secrets into secrets and reference them:
"headers": "{\"authorization\": \"Bearer {{secrets.api_key}}\"}",
"secrets": "{\"api_key\": \"sk_REAL_KEY\"}"
Only secrets values are AES-GCM encrypted at rest and never returned
by any read endpoint.
❌ Using lock_input_fields for a key the schema doesn’t declare:
The locked key has no effect (there’s nothing to override) and no
fallback either — fix the typo or remove the lock.
❌ Using {{end_user.email}} when there’s no email in metadata:
end_user.metadata.email only exists if you passed it when creating the
thread:
curl -X POST https://api.qlaud.ai/v1/threads \
-d '{"end_user_id":"u_alice","metadata":{"email":"alice@acme.com"}}'
Without it, the placeholder renders as empty string.
- Register the tool with
POST /v1/tools (a 201 confirms config-time
validation passed).
- Create a test thread with an
end_user_id and the metadata your
template needs.
- Send a message that should trigger the tool, with
tools: [<tool_id>].
- Inspect the assistant’s response — if the tool fired, the
tool_use and tool_result blocks are visible in
GET /v1/threads/<id>/messages.
- Iterate on the config (
PATCH /v1/tools/<id>/config, coming soon)
or just revoke + re-register (DELETE /v1/tools/<id> then re-POST).
When to use this vs. the alternatives
| You want to… | Use |
|---|
| Wrap an internal/3rd-party REST endpoint with no infra on your side | qlaud-builtin/http-call (this page) |
| Send email / SMS / Slack / file ticket via a popular SaaS | The named scaffold for that SaaS — see /api-reference/builtins |
| Let each end-user OAuth into their own Notion/GitHub/Stripe | A per-user MCP server — see /api-reference/mcp-catalog |
| Run arbitrary logic that needs to live on your infrastructure | A webhook tool — see /api-reference/tools |
Errors
| Status | Meaning |
|---|
| 400 | Config missing url, bad URL scheme, headers/secrets/lock_input_fields not valid JSON of the expected shape, or numeric field is non-positive. |
| 401 | Missing / revoked qlk key. |
| 403 | Caller used a per-user (standard-scope) key. /v1/tools requires a master-scope key. |
| 409 | A non-revoked tool with the same name already exists. |
| 503 | Gateway is missing the TOOL_CONFIG_ENC_KEY operator secret. Built-ins (including http-call) can’t be registered until it’s set. |