Scope Access with Virtual Keys
What you'll build: a virtual key restricted to one model, capped at a request rate, and set to reuse cached responses for a minute — then proof that the restriction actually holds, by calling an allowed model and a disallowed one with the same key.
A virtual key (agh_sk_…) is what your app sends to the gateway — never
your provider's own key, which stays encrypted server-side. Every virtual
key can be unrestricted (any registered model, no caps) or scoped:
limited to specific models or providers, rate-limited, and given its own
cache window. Scoping matters because a virtual key is what ends up baked
into a mobile app, a CI pipeline, or a third-party service — anywhere a key
is more likely to leak than a browser session.
1. See what you already have
Open Gateway → Virtual keys. Each row shows whether a key is unrestricted or scoped, and to what.

2. Create a scoped key
Click New key. Everything under Scope is optional — leave it blank
for an unrestricted key. Here we're creating one for a mobile client:
restricted to gpt-4o-mini, capped at 120 requests/minute, with responses
cached for 60 seconds.

Click Create key. Like a personal API key, the plaintext token is shown exactly once.
Back on the list, the new key's scope is summarized right there — no need to open it again to remember what it's restricted to:

3. Prove the scope actually holds
Call the gateway with the new key using the allowed model — it works exactly like an unrestricted key would:
curl -X POST "$ACRUXCORE_BASE_URL/gateway/chat/completions" \
-H "Authorization: Bearer $MOBILE_APP_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "gpt-4o-mini", "messages": [{"role": "user", "content": "Say OK"}], "max_tokens": 5}'
{"id":"gen-1785555455-vODktjXf4x5AKdhpxbXt","model":"openai/gpt-4o-mini","object":"chat.completion","choices":[{"index":0,"message":{"role":"assistant","content":"OK!"},"finish_reason":"stop"}],"usage":{"prompt_tokens":9,"completion_tokens":3,"total_tokens":12}}
Now call it with a model outside the allow-list, same key:
curl -X POST "$ACRUXCORE_BASE_URL/gateway/chat/completions" \
-H "Authorization: Bearer $MOBILE_APP_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "claude-via-or", "messages": [{"role": "user", "content": "Say OK"}], "max_tokens": 5}'
{"error":{"code":"MODEL_NOT_ALLOWED","message":"Model 'claude-via-or' is not allowed for this key."}}
The rejection happens before any provider is called — a scoped key can't be
talked into spending outside its job, even by a client that's been
compromised or misconfigured. maxRpm / maxTpm enforce the same way, with
429 RATE_LIMITED and a Retry-After header once the cap is hit.
4. Revoke it when it's done
If a key leaks, or the service it belonged to is retired, click Revoke. It's immediate and permanent — the confirmation says so, because there's no undo.
- curl
curl -X DELETE "$ACRUXCORE_BASE_URL/gateway/keys/<key-id>" \
-H "Authorization: Bearer $ACRUXCORE_API_KEY"
Any request made with a revoked key gets 401 INVALID_KEY from that moment
on. Requests it already made keep their trace and usage history — revoking
doesn't rewrite what already happened, only what happens next.
What's next
- Cap total spend rather than just request rate — see Set Spend Limits with Gateway Budgets and Rate Limits.
- Route calls through the gateway in the first place — see Route your app's LLM calls through the gateway.
- Full field reference: Gateway Virtual Keys in the API Reference.