Skip to main content

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():

  1. URL rewriting — https://api.lifx.com/v1/lights/all becomes https://keychains.dev/api.lifx.com/v1/lights/all
  2. Token injection — your proxy token is sent via X-Proxy-Authorization so the proxy knows who you are
  3. Scope check — the proxy verifies the user has approved the required credentials for this API
  4. Credential resolution — the proxy replaces {{LIFX_PERSONAL_ACCESS_TOKEN}} with the real API key stored in the user's vault
  5. Request forwarding — the proxy forwards the request to the upstream API with real credentials injected
  6. 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...")

Multi-account

If a user has connected multiple accounts for the same provider, pass account to select which one to use:

keychains.get(url, account="work@example.com")
keychains.Session(account="work@example.com")
keychains.AsyncClient(account="work@example.com")

The value is sent as the X-Proxy-Account header. When omitted, the proxy uses the default account.

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!

Metadata

Release files for keychains 0.1.13

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for keychains 0.1.13
File Size Uploaded Provenance Actions
keychains-0.1.13.tar.gz 10.1 kB Without provenance Details for keychains-0.1.13.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for keychains 0.1.13
File Interpreter ABI Platform Actions
keychains-0.1.13-py3-none-any.whl Python 3 none any Details for keychains-0.1.13-py3-none-any.whl

Total release size: 19.9 kB

Release files / keychains-0.1.13.tar.gz

Download URL keychains-0.1.13.tar.gz
Size 10.1 kB
Tags Source
SHA-256 checksum
How to use checksums
3a3160ef16398051475372424f0ee0ac06262dc98d898e8307712fab9917c1b3
BLAKE2b-256 checksum
How to use checksums
e9b08fc44a77b084dc8a8346a23fabaeeecf2497566670e7a62e71e64aa68d34
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.13.12

Release files / keychains-0.1.13-py3-none-any.whl

Download URL keychains-0.1.13-py3-none-any.whl
Size 9.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
17b011dcb9707e4b53ee4b2669a0aa42788836e624ff0f1b5a55b1bc7607d471
BLAKE2b-256 checksum
How to use checksums
03653c879bcfc1ffc12cd86ebc5a0520e8db243dd85d52a99c84a1cbd41a216a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.13.12

Security for keychains 0.1.13

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page