For AI Agents & Integrations
Constructing proxy URLs when acting on behalf of a customer — for AI agents and automation. Covers URL construction, path allowlists, header handling, and the dashboard API.
Last updated Jul 23, 2026
name: guardproxy description: "Route third-party API calls through GuardProxy to make a full-access API key read-only. Use when an agent is about to call a vendor API (ITGlue, Hudu, ConnectWise, Autotask, httpbin, any REST/GraphQL API) on behalf of a user, and the user wants destructive writes (POST/PUT/PATCH/DELETE) safely blocked. Covers constructing the proxy URL, path allowlists for POST-as-read endpoints (GraphQL, search), auth-header passthrough, and managing upstreams via the dashboard API."
GuardProxy — read-only API access for AI agents
What it is
GuardProxy is a reverse proxy that sits between you (the agent) and a third-party upstream API. It inspects the HTTP method of every request:
- GET, HEAD, OPTIONS → forwarded to the upstream unchanged.
- POST, PUT, PATCH, DELETE → blocked with a
403before they ever reach the upstream. The block is logged to the customer's account.
The customer's Authorization header (their real API key) passes through verbatim
and is never stored or logged — it exists in memory for the request lifetime only.
You (agent) → https://cust_abc.guardproxy.dev/itglue/api/v2/accounts
│
┌──────┴─────── GET/HEAD/OPTIONS? → forward, return upstream response
│
└────── POST/PUT/PATCH/DELETE? → 403 + logged
The whole point: you get to use the customer's admin-level API key (the only kind some vendors issue) without the ability to create, modify, or delete anything.
When to use this skill
Use it whenever you are about to make HTTP calls to a third-party API on behalf of a user, and any of these are true:
- The API key you've been handed is a full-access / admin key.
- The user said "read-only", "don't let me write", "safely", "guard", or "proxy".
- You're integrating an MSP-style tool (ITGlue, Hudu, ConnectWise, Autotask) that doesn't offer a scoped read-only key.
- You're setting up an automation/agent that will hit a vendor API and the user wants a safety net against accidental writes.
Do NOT use it when:
- The task genuinely requires writing (creating tickets, updating records). GuardProxy will 403 you, by design. Surface this to the user and ask.
- You're calling a first-party service you fully control.
How to construct the proxy URL
The URL is the single thing that changes. Everything else — the API key, the auth header, query params, request body — stays exactly as you'd send it to the real API.
URL shape
https://{customer_slug}.{base_domain}/{upstream_slug}/{original_path_and_query}
└────────────┘ └──────────┘ └──────┘ └────────────────────────────┘
subdomain zone apex upstream the path the upstream expects
Concretely, to call GET https://api.itglue.com/v2/accounts through the proxy:
Real API: https://api.itglue.com/v2/accounts
Proxy: https://cust_abc.guardproxy.dev/itglue/v2/accounts
│ │
│ └── same as the real path
└── the upstream slug the customer configured
Rules:
- The customer slug comes from the subdomain (
cust_abcfromcust_abc.guardproxy.dev). Reserved names —app,www,api,dashboard,status,docs— are rejected. - The upstream slug is the first path segment. Everything after it is forwarded verbatim, including the query string.
- Slugs are lowercase,
[a-z0-9-], 1–32 chars.
Find the exact base URL from the customer's account rather than guessing — see "Discovering or creating an upstream" below. The
proxy_endpointfield is canonical.
What you send
Nothing changes about your request except the host + the leading upstream segment:
// Before (direct, full access):
await fetch("https://api.itglue.com/v2/accounts", {
headers: { Authorization: "Bearer " + apiKey },
});
// After (read-only via GuardProxy):
await fetch("https://cust_abc.guardproxy.dev/itglue/v2/accounts", {
headers: { Authorization: "Bearer " + apiKey }, // same key, same header
});
Method semantics — what will and won't work
| Method | Result | Notes |
|---|---|---|
GET |
✅ forwarded | reads work |
HEAD |
✅ forwarded | |
OPTIONS |
✅ forwarded | always allowed (CORS preflight needs this) |
POST |
❌ 403 |
unless the path is on the allowlist (below) |
PUT |
❌ 403 |
|
PATCH |
❌ 403 |
|
DELETE |
❌ 403 |
A 403 response body looks like:
{ "error": { "code": "method_blocked", "message": "..." } }
Treat error.code === "method_blocked" as "the proxy did its job" — not an
upstream error. Don't retry; don't escalate it as a bug. It means the safety net
caught a write the user didn't want.
POST-as-read endpoints (GraphQL, search) → use the path allowlist
Some APIs use POST for reads. GraphQL is the canonical case — every query and
mutation is a POST. GuardProxy blocks these by default. The fix is the path
allowlist: matching path prefixes are exempt from the method block.
To send a GraphQL query through the proxy, the customer must have added the GraphQL path to that upstream's allowlist. Then:
POST https://cust_abc.guardproxy.dev/myapi/graphql ← allowlisted → 200
POST https://cust_abc.guardproxy.dev/myapi/mutate ← not allowlisted → 403
⚠️ The allowlist is a prefix match and bypasses ALL methods on matching paths. A
POSTto an allowlisted path works — but so would aDELETE. Keep allowlist entries as specific as possible (prefer/graphqlover/). If you need to add or narrow an allowlist entry, see the dashboard API below.
Header handling (important gotchas)
Authorizationand all other headers are forwarded verbatim. Your API key is untouched and reaches the upstream exactly as sent. It is stripped from logs.- Method-override headers are stripped before forwarding:
X-HTTP-Method-Override,X-Method-Override, and similar. These would otherwise let aGETsecretly execute aDELETEat the upstream — GuardProxy removes them deliberately. Do not rely on method-override headers through the proxy. - Custom headers (e.g.
X-Custom-Header: foo) pass through normally. - The upstream sees the same body, same content-type, same everything.
Discovering or creating an upstream
You usually won't hardcode proxy URLs. Query the customer's account instead. All dashboard API routes are session-authenticated and owner-scoped (you can only ever see/modify the authenticated customer's own upstreams).
List upstreams → get the canonical proxy_endpoint
GET /api/upstreams
Cookie: <session>
{
"upstreams": [
{
"id": "...",
"slug": "itglue",
"display_name": "ITGlue Production",
"upstream_url": "https://api.itglue.com",
"blocked_methods": ["POST", "PUT", "PATCH", "DELETE"],
"path_allowlist": ["/graphql"],
"proxy_endpoint": "https://cust_abc.guardproxy.dev/itglue",
"created_at": "...",
"updated_at": "..."
}
]
}
Use the returned proxy_endpoint as your base URL — it's the source of truth.
Create a new upstream (if the customer needs one for an API you're integrating)
POST /api/upstreams
Content-Type: application/json
Cookie: <session>
{
"slug": "hudu", // required, [a-z0-9-], 1–32 chars, lowercase
"displayName": "Hudu", // required
"upstreamUrl": "https://hudu.example.com", // required, http(s); private IPs/localhost rejected (SSRF guard)
"blockedMethods": ["POST","PUT","PATCH","DELETE"], // optional; defaults are sensible
"pathAllowlist": ["/graphql"] // optional, each must start with "/"
}
201returns the new upstream with itsproxy_endpoint.409 duplicate_slug→ that slug already exists for this customer; pick another.400 invalid_*→ validation failed (slug/url/methods/paths).
PATCH /api/upstreams/:id takes any subset of the same fields (e.g. just
{ "pathAllowlist": ["/graphql", "/search"] }). DELETE /api/upstreams/:id → 204.
Auth & billing state
GET /api/me→ your user + customer record (includingsubscription_statusand trial window). The proxy itself enforces this: an expired/past_dueaccount returns402, and hard abuse overage returns429.
Verifying it works
Run a read, confirm a write is blocked:
# Read passes through (upstream response)
curl https://cust_abc.guardproxy.dev/httpbin/get?foo=bar
# Write is blocked (403 method_blocked)
curl -X POST https://cust_abc.guardproxy.dev/httpbin/post -d '{"x":1}'
Decision checklist before you call a vendor API
- Do I need to write? If yes, GuardProxy can't help — ask the user.
- Is the key full-access / admin? If yes, prefer routing through the proxy.
- Do I have a
proxy_endpointfor this upstream? If not,GET /api/upstreams; if still not,POST /api/upstreamsto create one. - Am I POSTing for a read (GraphQL/search)? Ensure the path is in the upstream's
path_allowlist; otherwise expect a403. - Am I using method-override headers? Drop them — they're stripped.
- Got a
403 method_blocked? That's the proxy working as intended, not a bug. Don't retry; inform the user the write was blocked.
Quick reference
| Need | Do |
|---|---|
| Read through the proxy | GET https://{cust}.{domain}/{upstream}/{path}?{query} |
| Confirm a write is blocked | send POST/PUT/PATCH/DELETE; expect 403 {error.code:"method_blocked"} |
| Exempt a POST-as-read path | add the path prefix to the upstream's path_allowlist (PATCH /api/upstreams/:id) |
| Find the proxy base URL | GET /api/upstreams → use the proxy_endpoint field |
| Create a new guarded upstream | POST /api/upstreams with slug, displayName, upstreamUrl |
| Check account/trial status | GET /api/me |