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.
Anatomy of a valid config
Everyhttp-call config has the same six knobs. Only url is required;
everything else has a sensible default.
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:
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:
args.items = ["a", "b"] renders as {"items": ["a","b"]}.
Lock semantics — the security primitive
The single most important field islock_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 toend_user.id. - If neither source has a value, drops the key from
argsentirely (the request still fires, but with the field absent).
to(email) → locked toend_user.metadata.emailphone(SMS) → locked toend_user.metadata.phonecustomer_id(account lookup) → locked toend_user.idaccount_id,team_id(anything tenant-scoped to the caller)
A complete, valid config (annotated)
This is the single most important reference on the page. Every http-call registration looks like this: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:urlis present and starts withhttp://orhttps://.methodis one ofGET POST PUT PATCH DELETE(or absent).headers,secrets,lock_input_fieldsparse as JSON if non-empty.headersparses to an object (not array, not primitive).secretsparses to an object.lock_input_fieldsparses to an array of strings.timeout_msandresponse_max_bytesare positive integers if set.
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: trueand a clear message. - Timeouts → tool result with
is_error: trueand 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, validPOST /v1/tools body. Replace the
secret values + URLs with your own.
When to skip http-call entirely. Several common “send to logged-in
user” cases have native lock support on a named scaffold and don’t need
http-call at all:
- Email to user → use
qlaud-builtin/send-emailwithlock_to_session: "true". Forcestotoend_user.metadata.email. - SMS to user → use
qlaud-builtin/twilio-send-smswithlock_to_session: "true". Forcestotoend_user.metadata.phone. - Linear / Zendesk / GitHub / Notion / Slack actions → the recipient is your account, not an end-user. Tenant credentials at registration already bind the destination — no lock needed. Just use the named scaffold.
http-call when wrapping non-catalog endpoints (your
internal API, a third-party without an MCP server, GraphQL, custom
auth) — that’s the case the recipes below cover.1. Look up a customer in your internal database
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
3. File a Linear ticket — but use the named scaffold instead
For Linear specifically, use the named scaffold — it’s friendlier: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:customer_id is locked to end_user.id — the model can suggest
“refund Bob 500.
5. GraphQL query
6. Form-encoded API (e.g. a webhook hook your app exposes)
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 forheaders:
{"items":"["a","b"]"} (string-of-array). Drop the quotes:
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 blob is visible in dashboard read-paths to the developer.
Move secrets into secrets and reference them:
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:
How to test a tool before going live
- Register the tool with
POST /v1/tools(a 201 confirms config-time validation passed). - Create a test thread with an
end_user_idand 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_useandtool_resultblocks are visible inGET /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).