Python SDK for Keychains.dev — credential proxy for API calls
Project description
keychains
Minimal Python SDK for making authenticated API calls through the Keychains.dev proxy. Your code never touches real credentials — the proxy injects them at runtime.
Quickstart
1. Install
pip install keychains
2. Run with a fresh token
The keychains token command registers your machine (if needed), creates a wildcard permission, and mints a short-lived proxy token — all in one step:
KEYCHAINS_TOKEN=$(npx -y keychains token) \
python your_script.py
3. Write your script
Use keychains.get() as a drop-in replacement for requests.get(). The only difference? You can replace any credential with a template variable:
import keychains
# Gmail — get last 10 emails from my inbox
response = keychains.get(
"https://gmail.googleapis.com/gmail/v1/users/me/messages?maxResults=10",
headers={
"Authorization": "Bearer {{OAUTH2_ACCESS_TOKEN}}",
},
)
emails = response.json()
print(emails)
That's it. The proxy resolves {{OAUTH2_ACCESS_TOKEN}} with the user's real Google OAuth token — your code never sees it.
Running multiple scripts
Tokens expire after 15 minutes. To reuse the same token across multiple commands in a shell session, use eval:
eval $(npx -y keychains token --env)
# KEYCHAINS_TOKEN is now set for the next 15 minutes
python script_a.py
python script_b.py
Template Variables
How to write them
Template variables use the {{VARIABLE_NAME}} syntax. The variable name tells the proxy which type of credential to inject:
| Prefix | Type | Supported Variables |
|---|---|---|
OAUTH2_ |
OAuth 2.0 token | {{OAUTH2_ACCESS_TOKEN}}, {{OAUTH2_REFRESH_TOKEN}} |
OAUTH1_ |
OAuth 1.0 token | {{OAUTH1_ACCESS_TOKEN}}, {{OAUTH1_REQUEST_TOKEN}} |
| Anything else | API key | {{LIFX_PERSONAL_ACCESS_TOKEN}}, {{OPENAI_API_KEY}}, etc. |
Where to put them
Place them exactly where you'd normally put the real credential — headers, body, or query parameters:
import keychains
# In a header (most common)
response = keychains.get(
"https://api.lifx.com/v1/lights/all",
headers={"Authorization": "Bearer {{LIFX_PERSONAL_ACCESS_TOKEN}}"},
)
# In the request body
response = keychains.post(
"https://slack.com/api/chat.postMessage",
headers={
"Authorization": "Bearer {{OAUTH2_ACCESS_TOKEN}}",
"Content-Type": "application/json",
},
json={"channel": "#general", "text": "Hello!"},
)
# In query parameters
response = keychains.get(
"https://api.example.com/data?api_key={{MY_API_KEY}}&format=json",
)
What Happens Next
When you call keychains.get():
- URL rewriting —
https://api.lifx.com/v1/lights/allbecomeshttps://keychains.dev/api.lifx.com/v1/lights/all - Token injection — your proxy token is sent via
X-Proxy-Authorizationso the proxy knows who you are - Scope check — the proxy verifies the user has approved the required credentials for this API
- Credential resolution — the proxy replaces
{{LIFX_PERSONAL_ACCESS_TOKEN}}with the real API key stored in the user's vault - Request forwarding — the proxy forwards the request to the upstream API with real credentials injected
- Response passthrough — the upstream response is returned to you as-is
Handling missing approvals
With wildcard permissions, users approve scopes on demand. The first time your code hits a new API, the user may not have approved it yet. When that happens, the SDK raises an ApprovalRequired exception containing an approval_url — share it with the user so they can grant access:
import keychains
from keychains.exceptions import ApprovalRequired
try:
response = keychains.get(
"https://api.github.com/user",
headers={"Authorization": "Bearer {{OAUTH2_ACCESS_TOKEN}}"},
)
print(response.json())
except ApprovalRequired as err:
# The user hasn't approved GitHub yet — show them the link
print("Please approve access:", err.approval_url)
# Once approved, retry the same call and it will succeed
The exception includes useful details:
| Property | Type | Description |
|---|---|---|
approval_url |
str | None |
URL the user should visit to approve the missing scopes |
missing_scopes |
list[str] | None |
Scopes that need approval |
refused_scopes |
list[str] | None |
Scopes explicitly refused by the user |
code |
str |
Error code (insufficient_scope, scope_refused, permission_denied, etc.) |
Session (Connection Pooling)
For multiple requests, use Session to reuse connections — just like requests.Session:
import keychains
with keychains.Session() as s:
s.headers.update({"Authorization": "Bearer {{OAUTH2_ACCESS_TOKEN}}"})
repos = s.get("https://api.github.com/user/repos")
for repo in repos.json():
issues = s.get(f"https://api.github.com/repos/{repo['full_name']}/issues")
print(f"{repo['name']}: {len(issues.json())} issues")
Async
For asyncio codebases, use AsyncClient:
import keychains
async with keychains.AsyncClient() as client:
response = await client.get("https://api.github.com/user/repos")
repos = response.json()
Configuration
The SDK automatically loads variables from a .env file in your working directory (via python-dotenv).
| Environment variable | Description |
|---|---|
KEYCHAINS_TOKEN |
Proxy token — a JWT minted by npx -y keychains token |
Tokens can also be passed explicitly:
keychains.get(url, token="ey...")
keychains.Session(token="ey...")
keychains.AsyncClient(token="ey...")
Security benefits
- Secrets never leave the Keychains.dev servers — your code, logs, and environment stay clean
- Users approve exactly which scopes and APIs an agent can access
- Credentials can only be sent to the APIs of the providers they belong to
- Every proxied request is audited with full traceability
- Permissions can be revoked instantly from the dashboard
Bug Reports & Feedback
Found a bug or have a suggestion? Submit it straight from your terminal:
# Report a bug
npx -y keychains feedback "The proxy returns 502 on large POST bodies"
# Send feedback
npx -y keychains feedback --type feedback "Love the wildcard permissions!"
# With more detail
npx -y keychains feedback --type bug \
--title "502 on large POST" \
--description "When sending >1MB body to Slack API..." \
--contact you@example.com
The keychains feedback command (alias: keychains bug) sends your report directly to the engineering team.
More Info
Let's meet on keychains.dev!
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file keychains-0.1.7.tar.gz.
File metadata
- Download URL: keychains-0.1.7.tar.gz
- Upload date:
- Size: 9.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
5c0c3994bbb74a762ee39037d01d83ebafaa7f5a3fec94ac0129d453da236e5d
|
|
| MD5 |
497ea535e782ebb9897d9400c0653fa6
|
|
| BLAKE2b-256 |
caf4c3d43e2cb6dab744e49abebe4db955aae70864dfee52edee23c15ec020dd
|
File details
Details for the file keychains-0.1.7-py3-none-any.whl.
File metadata
- Download URL: keychains-0.1.7-py3-none-any.whl
- Upload date:
- Size: 9.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
96c89f69399c28c9a1cbd1481c038002f00d1e11a69c6858be0041e4124458a6
|
|
| MD5 |
1b4ab9da1b6a1db07d72447d6524838a
|
|
| BLAKE2b-256 |
bd4120eb58583ed0e3ed792d84b1db51fd6ec004558e6d5f1319ec58a7ee6d30
|