Caller-Hosted Tools
Most external tools live at a fixed address — a public API, a service you run at a known hostname. You put the URL on the tool and PromptShuttle calls it.
Caller-hosted tools are the other case: the tool is served by your own application, the same one making the request. That means its address is different on every machine your app runs on — your laptop, a preview environment, production — and none of those addresses belong on a shared tool definition.
So don't store one. Give the tool a relative path and let each request say where its tools live.
{
"name": "search_categories",
"toolType": "external",
"webUrl": "/api/v1/tools/categorization/search?schema_id=[[schema_id]]"
}curl -X POST https://app.promptshuttle.com/api/v1/flows/categorize/runs \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "X-Shuttle-Callback-Url: https://app.example.com" \
-H "Content-Type: application/json" \
-d '{ "parameters": { "schema_id": "abc123" } }'One tool definition. Your developer machine answers its own tool calls, production answers its own, and neither is written down anywhere.
Absolute or relative
The shape of webUrl is the whole switch. There is no separate flag.
webUrl
Meaning
https://api.example.com/search
Fixed host. Resolves as it always has; the callback header is not needed.
/api/v1/search
Caller-hosted. Origin comes from X-Shuttle-Callback-Url on each request.
Environment overrides compose with this: a tool can be caller-hosted by default and pinned to a real host in one environment.
[[parameter]] placeholders work in relative paths exactly as in absolute URLs.
A missing origin fails the call
If a caller-hosted tool is invoked on a request that carries no X-Shuttle-Callback-Url, the tool call fails. The model receives an explicit error naming the cause, and the run records it. No stored URL is substituted.
This is deliberate, and it is worth understanding why, because the alternative looks harmless:
A flow ran on a developer machine with no callback origin configured. Its tools fell back to the URL on the definition — the deployed app. Production had never heard of that developer's data, so it answered honestly:
{"results": [], "total": 0}. Three times. The model concluded no categories existed, invented a plausible taxonomy, and returned an answer with 0.85 confidence. Downstream, a batch import ran against nothing at roughly 100× the normal cost and reported success.Every tool call was a 200. The run is recorded as
Succeeded.
A wrong answer that looks exactly like a right one is worse than an error. So: send the header from every environment, production included. Treat it as part of your PromptShuttle configuration, not as a debugging aid you switch on when something breaks.
Registering allowed origins
The origin arrives in a request header, so it needs a boundary. Otherwise an API key would buy the ability to point PromptShuttle's outbound calls at any host on the internet and read back whatever it returns.
Register the origins your apps serve tools from under Settings → Callback origins, one per line:
Scheme and port are optional. When present, they must match.
A leading
*.matches sub-domains —*.ngrok-free.devmatcheslexi-wanning.ngrok-free.devbut notngrok-free.devitself, and not a look-alike likeevil-ngrok-free.dev.With no origins registered, caller-hosted tools do not run. Deny-by-default: an unregistered origin fails loudly rather than resolving to something plausible.
Private and reserved IP ranges are blocked regardless of the allowlist, so a registered origin still cannot reach internal infrastructure. That check is relaxed only in local development builds — which is why localhost in the example above works against a local PromptShuttle but not against the hosted one. To reach a laptop from hosted PromptShuttle, use a tunnel.
Per-run tokens
Your tool endpoints should authenticate their callers. A single secret stored on the tool definition can't do that once the host varies per request — a developer's tunnel would present production's token.
Send the secret with the request instead:
PromptShuttle holds it for the duration of the run, never persists it, and presents it back on caller-origin calls under X-Tool-Token — or whatever header name the tool's Callback token header field specifies. It takes precedence over any same-named header stored on the tool.
Each instance can then hold its own secret, and a leaked tunnel token is worth nothing anywhere else.
Configuring the client SDK
The .NET client sends both headers on every request when configured:
Set CallbackUrl per deployment — that is the point of it. Leave it unset only if none of your tools are caller-hosted.
Seeing which host answered
Every tool call records the endpoint it was actually placed against, plus where that origin came from. A run served by a tunnel is distinguishable from one served by the deployed app at a glance:
Invocation log — each tool-call step shows the endpoint and a badge reading tool definition, environment binding, or caller-supplied origin.
get_run(MCP) — atoolCallsarray withtargetUrlandoriginSourceper call, andcallbackOriginon the run itself.Streaming events —
tool.started,tool.completedandtool.failedcarrytargetUrlandoriginSource.
Header reference
X-Shuttle-Callback-Url
For caller-hosted tools
Origin serving this caller's tools, e.g. https://app.example.com. Any path is ignored.
X-Shuttle-Callback-Token
No
Secret presented back on caller-origin calls. Never stored.
X-Shuttle-Debug-Url
No — deprecated
Legacy alias of X-Shuttle-Callback-Url. Also attaches a DebugUrl: tag. Prefer the callback header.
Migrating an existing tool
Register your origins under Settings → Callback origins — including production.
Set
CallbackUrl(or sendX-Shuttle-Callback-Url) everywhere the app runs. Deploy that first.Change the tool's
webUrlfromhttps://app.example.com/api/v1/searchto/api/v1/search.
Steps 1 and 2 change nothing on their own: while the tool's URL is still absolute, a callback origin only redirects the host, which is the behaviour the old X-Shuttle-Debug-Url header already had. Step 3 is the switch, and it is reversible.
Last updated