By the end of this you will have an API key that an agent can use, without the agent ever holding the key.
We will use OpenAI as the upstream. Any HTTP API works the same way.
1. Store the credential as a Secret
In the dashboard, go to Secrets and create one:
| Field | Value |
|---|---|
| Name | OpenAI production |
| Credential | your real sk-… key |
| Inject as | Header |
| Header name | Authorization |
| Prefix | Bearer |
Latch encrypts the credential at rest and never returns it, not to you, not to the dashboard, not to a caller. From here on you refer to it by name.
2. Mint a Latch
Go to Latches and create one against that secret:
| Field | Value |
|---|---|
| Name | Support bot |
| Secret | OpenAI production |
| Upstream | https://api.openai.com |
You get an access token, lat_…. Copy it now, it is shown once.
This token is what the agent holds. It is not the OpenAI key, it cannot be turned back into the OpenAI key, and revoking it does not require rotating the OpenAI key.
3. Give it a policy
Right now the latch forwards anything. Add two filters in the pipeline editor:
[
{ "type": "method", "name": "No deletes", "allowed": ["GET", "POST"] },
{ "type": "endpoint", "name": "Completions only", "mode": "allowlist",
"patterns": ["/v1/chat/completions"] }
]
The pipeline runs in order and the first filter to deny wins. See the filter reference for all 13 types, including rate limits, body assertions, time windows, and daily spend caps.
4. Call it
Point your client at Latch instead of the upstream, and use the latch token instead of the key. Everything else stays the same.
curl https://onlatch.com/proxy/v1/chat/completions \
-H "Authorization: Bearer lat_9f3c…" \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-4o-mini",
"messages": [{ "role": "user", "content": "hello" }]
}'
Latch evaluates the pipeline, injects the real OpenAI key, forwards the request, and returns OpenAI's response to you verbatim.
The path after /proxy/ is passed straight through, so /proxy/v1/chat/completions
reaches https://api.openai.com/v1/chat/completions.
5. Watch it deny
Now try something the policy forbids:
curl https://onlatch.com/proxy/v1/embeddings \
-H "Authorization: Bearer lat_9f3c…" \
-H "Content-Type: application/json" \
-d '{ "model": "text-embedding-3-small", "input": "hello" }'
{
"error": "Request denied by policy",
"requestId": "req_4a1f2c",
"latchId": "lnk_9f3c1a2b",
"deniedBy": "Completions only",
"reason": "/v1/embeddings not in allowlist"
}
403. The request never reached OpenAI, and the key was never used. deniedBy names
the filter that stopped it, so the agent (or you) can tell exactly what it did wrong.
Every call, allowed or denied, lands in Activity with the full filter trace.
Using it from an SDK
Most clients let you override the base URL, which is all this takes:
from openai import OpenAI
client = OpenAI(
api_key="lat_9f3c…", # the latch, not your OpenAI key
base_url="https://onlatch.com/proxy",
)
client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user", "content": "hello"}],
)
The agent has no idea Latch is there. It just has less power than it thinks.
Next
- Filter reference: all 13 filter types.
- Multi-mount latches: several upstreams behind one token.
- Proxy API: the full contract, and the OpenAPI document.