Skip to main content

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.
FieldTypeRequiredDefaultWhat it is
urlstringyesFull endpoint URL. Must start with http:// or https://. Supports template placeholders.
methodstringnoPOSTOne of GET, POST, PUT, PATCH, DELETE.
headersstring (JSON)no"{}"JSON-stringified object of header name → value. Values support placeholders.
body_templatestringno""Request body. Either a JSON-template string or empty (no body). Sent as-is — set the right content-type header to match.
secretsstring (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_fieldsstring (JSON)no"[]"JSON-stringified array of input keys whose model-supplied values get overridden with trusted session values before the template renders.
response_max_bytesstring (number)no"8192"Cap on response bytes returned to the model. Truncated bodies append a clear […truncated…] notice. Hard ceiling 1 MiB.
timeout_msstring (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:
PlaceholderSourceTrustworthy?
{{config.X}}The developer-supplied config (url, method, etc.)Yes — set at registration
{{secrets.X}}The developer-supplied secrets objectYes — 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_idYes — set at thread creation
{{end_user.metadata.X}}Any field from the thread’s metadata objectYes — 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:
  1. Drops whatever the model put in args.<key>.
  2. Replaces it with end_user.metadata.<same_key> if present.
  3. Or, for the canonical id keys (user_id, end_user_id, customer_id), falls back to end_user.id.
  4. 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:
  1. url is present and starts with http:// or https://.
  2. method is one of GET POST PUT PATCH DELETE (or absent).
  3. headers, secrets, lock_input_fields parse as JSON if non-empty.
  4. headers parses to an object (not array, not primitive).
  5. secrets parses to an object.
  6. lock_input_fields parses to an array of strings.
  7. 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.).

4. Lock down a “send refund” tool

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"buttheactualcallalwaystargetstheloggedincustomer.Theamountcentsschemaadditionallycapstherefundat10" but the actual call always targets the logged-in customer. The `amount_cents` schema additionally caps the refund at 500.

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\"}"
  }
}

6. Form-encoded API (e.g. a webhook hook your app exposes)

{
  "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.

How to test a tool before going live

  1. Register the tool with POST /v1/tools (a 201 confirms config-time validation passed).
  2. Create a test thread with an end_user_id and the metadata your template needs.
  3. Send a message that should trigger the tool, with tools: [<tool_id>].
  4. Inspect the assistant’s response — if the tool fired, the tool_use and tool_result blocks are visible in GET /v1/threads/<id>/messages.
  5. 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 sideqlaud-builtin/http-call (this page)
Send email / SMS / Slack / file ticket via a popular SaaSThe named scaffold for that SaaS — see /api-reference/builtins
Let each end-user OAuth into their own Notion/GitHub/StripeA per-user MCP server — see /api-reference/mcp-catalog
Run arbitrary logic that needs to live on your infrastructureA webhook tool — see /api-reference/tools

Errors

StatusMeaning
400Config missing url, bad URL scheme, headers/secrets/lock_input_fields not valid JSON of the expected shape, or numeric field is non-positive.
401Missing / revoked qlk key.
403Caller used a per-user (standard-scope) key. /v1/tools requires a master-scope key.
409A non-revoked tool with the same name already exists.
503Gateway is missing the TOOL_CONFIG_ENC_KEY operator secret. Built-ins (including http-call) can’t be registered until it’s set.