API Key Management
Securely store, load, and rotate exchange API keys — environment variables, .env files, and encrypted key stores.
Notebook 97 — API Key Management
Overview
API keys are the credentials that grant programmatic access to exchange
accounts. A compromised key can result in unauthorized trades or complete
account drain. The correct approach is simple: keys are stored in a
.env file on disk, loaded into environment variables at runtime, and
accessed via os.getenv(). They never appear in source code, notebooks,
or configuration files.
The three fundamental rules:
| Rule | Implementation |
|---|
| Never store keys in source code | Store in .env file only |
| Apply minimum required permissions | Read-only for data; trade-only for execution; never enable withdraw |
| Rotate keys regularly | Replace every 30–90 days |
Key permission scoping — what to enable on the exchange dashboard:
| Permission | Required For | Enable? |
|---|---|---|
| Read (Market Data) | OHLCV fetch, price feeds | Always |
| Read (Account) | Balance and position queries | Yes |
| Trade | Order placement and cancellation | Only on execution keys |
| Withdraw | Moving funds off-exchange | Never on a trading key |
1. Dependency Installation
# python-dotenv — reads .env files and injects values into os.environ
!pip install python-dotenv pandasRequirement already satisfied: python-dotenv in /usr/local/lib/python3.12/dist-packages (1.2.2) Requirement already satisfied: pandas in /usr/local/lib/python3.12/dist-packages (2.2.2) Requirement already satisfied: numpy>=1.26.0 in /usr/local/lib/python3.12/dist-packages (from pandas) (2.0.2) Requirement already satisfied: python-dateutil>=2.8.2 in /usr/local/lib/python3.12/dist-packages (from pandas) (2.9.0.post0) Requirement already satisfied: pytz>=2020.1 in /usr/local/lib/python3.12/dist-packages (from pandas) (2025.2) Requirement already satisfied: tzdata>=2022.7 in /usr/local/lib/python3.12/dist-packages (from pandas) (2026.1) Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.12/dist-packages (from python-dateutil>=2.8.2->pandas) (1.17.0)
2. Library Imports
import warnings
warnings.filterwarnings("ignore")
import os # Read environment variables at runtime
from pathlib import Path # OS-agnostic file path handling
from dotenv import load_dotenv, set_key # Read and write .env files
import pandas as pd # Display summary tablesSummary
This notebook presents a refined approach to API key management, focusing on security, clarity, and modularity.
Key enhancements include:
- Centralized Imports: All library dependencies are declared within a single, dedicated cell.
- Atomic Function Definitions: Each API key management function (
save_api_keys,load_api_keys,get_api_key,get_api_secret,get_credentials,validate_credentials,update_api_key,list_configured_exchanges) is defined in an isolated code cell, followed by its technical explanation. - Structured Execution Examples: Practical demonstrations for each function are provided in distinct cells immediately after their respective definitions.
- Elimination of Redundancy: Duplicate sections, function definitions, and explanatory content have been removed to streamline the notebook's flow.
This structured methodology significantly improves the notebook's readability, maintainability, and overall utility as a robust API key management solution.
Standard Three-Step API Client Integration Pattern:
This pattern describes the recommended sequence for integrating API key management functions within any module requiring exchange API connectivity:
- Step 1: Environment Loading (
load_api_keys): Executed once at application initialization. This function injects all key-value pairs from the.envfile intoos.environ. Subsequent calls toos.getenv()across any module will retrieve values directly fromos.environ, negating further file I/O. - Step 2: Credential Validation (
validate_credentials): Performed prior to establishing any network connection. This step confirms the presence and correct formatting of API keys and secrets, preemptively identifying issues such as missing credentials or whitespace errors. This avoids obscure authentication failures from the exchange API. - Step 3: Credential Retrieval and Client Instantiation (
get_credentials): Validated API key and secret values are retrieved and subsequently passed to the exchange client constructor. The client implementation should not directly access the.envfile; it should only receive the pre-loaded string values.
3. Save API Keys to .env File
def save_api_keys(
exchange: str,
api_key: str,
api_secret: str,
env_file: str = ".env",
) -> None:
"""
Persists an API key and secret for a specified exchange to the .env file.
The function writes or updates two key-value pairs in the .env file:
`{EXCHANGE}_API_KEY=api_key`
`{EXCHANGE}_API_SECRET=api_secret`
The .env file is created if it does not exist. Existing variables
within the file are overwritten in-place if they match.
Parameters
----------
exchange : str
Exchange name (case-insensitive); stored as uppercase.
api_key : str
API key string obtained from the exchange dashboard.
api_secret : str
API secret string obtained from the exchange dashboard.
env_file : str, default='.env'
Path to the .env file.
"""
exchange_upper = exchange.upper()
key_var = f"{exchange_upper}_API_KEY"
secret_var = f"{exchange_upper}_API_SECRET"
set_key(env_file, key_var, api_key)
set_key(env_file, secret_var, api_secret)
print(f"API keys for {exchange.upper()} saved to {env_file}:")
print(f" {key_var:<30} = {api_key[:6]}...")
print(f" {secret_var:<30} = {api_secret[:6]}...")Explanation:
set_key(env_file, variable_name, value): Writes or updates a singleKEY=VALUEline in the.envfile. If the key already exists, it is updated; otherwise, it is appended as a new line. The file is created automatically if absent.- The exchange name is normalized to uppercase, ensuring that inputs such as
'bybit','Bybit', or'BYBIT'all result in consistent variable names (BYBIT_API_KEY,BYBIT_API_SECRET). - Only the first 6 characters of each value are displayed, confirming the correct key was saved without exposing the full credential.
# Save API keys for Bybit (using testnet demo keys)
save_api_keys(
exchange = "bybit",
api_key = "your_bybit_api_key_here", # Replace with actual key
api_secret = "your_bybit_api_secret_here", # Replace with actual secret
)
print()
# Save API keys for OKX
save_api_keys(
exchange = "okx",
api_key = "your_okx_api_key_here",
api_secret = "your_okx_api_secret_here",
)
print()
# Save API keys for Binance
save_api_keys(
exchange = "binance",
api_key = "your_binance_api_key_here",
api_secret = "your_binance_api_secret_here",
)
# Display contents of the .env file to confirm changes
print("\n--- Current .env file contents ---")
print(Path(".env").read_text())4. Load API Keys from .env File
def load_api_keys(env_file: str = ".env") -> None:
"""
Loads all key-value pairs from the .env file into os.environ.
Upon execution, all variables defined in the .env file become accessible
via `os.getenv()` for the duration of the current session.
This function must be invoked once at the application's startup,
prior to any calls to `os.getenv()` within the system.
Parameters
----------
env_file : str, default='.env'
Path to the .env file.
"""
loaded = load_dotenv(dotenv_path=env_file, override=False)
if loaded:
print(f"API keys successfully loaded from {env_file} into environment.")
else:
print(f"Warning: {env_file} not found or empty. No keys loaded.")Explanation:
load_dotenv(override=False): Reads the.envfile and injects each line as an environment variable. Withoverride=False, any environment variable already present from the shell takes priority. This design allows production environments to set credentials directly in the shell without interference from the.envfile.- The function returns
Trueif the file was found and processed, andFalseif the file was absent or empty. - This function is idempotent; multiple calls do not result in errors or duplicate injections.
# Load environment variables from the .env file at application startup
load_api_keys(".env")5. Read API Keys from Environment
def get_api_key(exchange: str) -> str:
"""
Retrieves the API key for a specified exchange from os.environ.
Parameters
----------
exchange : str
Exchange name (case-insensitive, e.g., 'bybit').
Returns
-------
str
The API key value. Returns an empty string if the variable is not set.
"""
var_name = f"{exchange.upper()}_API_KEY"
return os.getenv(var_name, "")
def get_api_secret(exchange: str) -> str:
"""
Retrieves the API secret for a specified exchange from os.environ.
Parameters
----------
exchange : str
Exchange name (case-insensitive, e.g., 'bybit').
Returns
-------
str
The API secret value. Returns an empty string if the variable is not set.
"""
var_name = f"{exchange.upper()}_API_SECRET"
return os.getenv(var_name, "")
def get_credentials(exchange: str) -> dict:
"""
Retrieves both the API key and secret for a specified exchange from os.environ.
Parameters
----------
exchange : str
Exchange name (case-insensitive, e.g., 'bybit').
Returns
-------
dict
A dictionary containing 'exchange', 'api_key', and 'api_secret' keys.
Values are empty strings if corresponding variables are not set.
"""
return {
"exchange": exchange.lower(),
"api_key": get_api_key(exchange),
"api_secret": get_api_secret(exchange),
}Explanation:
os.getenv(var_name, ""): Retrieves a single variable fromos.environby its name. If the variable is not present, an empty string is returned as the default. This practice preventsNonevalues from propagating through the codebase and causingTypeErrorin subsequent string operations.- The three functions (
get_api_key,get_api_secret,get_credentials) form a hierarchical structure.get_api_keyandget_api_secretretrieve individual values, whileget_credentialsreturns both as a dictionary. Callers should utilize the function that provides the necessary level of granularity.
# Retrieve credentials for each configured exchange
bybit_creds = get_credentials("bybit")
okx_creds = get_credentials("okx")
binance_creds = get_credentials("binance")
print("--- Loaded Credentials (masked) ---")
for creds in [bybit_creds, okx_creds, binance_creds]:
key = creds["api_key"]
secret = creds["api_secret"]
# Mask sensitive information: display only the first 6 characters, or '(not set)' if empty
key_display = key[:6] + "..." if key else "(not set)"
secret_display = secret[:6] + "..." if secret else "(not set)"
print(f" {creds['exchange'].upper():<10} | "
f"Key={key_display:<20} | Secret={secret_display}")6. Validate Credentials
def validate_credentials(exchange: str) -> dict:
"""
Verifies the presence and structural validity of API key and secret for a specified exchange in os.environ.
Validation checks include:
- Non-empty API key and secret values.
- Absence of leading or trailing whitespace in either value.
This function performs static checks and does not initiate live API calls.
To confirm active key status, an authenticated endpoint must be queried:
Bybit: GET /v5/account/info
Binance: GET /api/v3/account
OKX: GET /api/v5/account/balance
Parameters
----------
exchange : str
Exchange name (case-insensitive).
Returns
-------
dict
A dictionary containing validation results:
- 'exchange' : str — Normalized exchange name.
- 'valid' : bool — True if all checks pass.
- 'key_set' : bool — True if api_key is non-empty.
- 'secret_set' : bool — True if api_secret is non-empty.
- 'errors' : list — List of error messages; empty if valid.
"""
key = get_api_key(exchange)
secret = get_api_secret(exchange)
errors = []
if not key:
errors.append(
f"{exchange.upper()}_API_KEY is not set. "
f"Add it to .env and call load_api_keys()."
)
if not secret:
errors.append(
f"{exchange.upper()}_API_SECRET is not set. "
f"Add it to .env and call load_api_keys()."
)
if key and key != key.strip():
errors.append(
f"{exchange.upper()}_API_KEY contains leading or trailing whitespace. "
f"Remove it from the .env file."
)
if secret and secret != secret.strip():
errors.append(
f"{exchange.upper()}_API_SECRET contains leading or trailing whitespace. "
f"Remove it from the .env file."
)
return {
"exchange": exchange.lower(),
"valid": len(errors) == 0,
"key_set": bool(key),
"secret_set": bool(secret),
"errors": errors,
}Explanation:
- The whitespace validation specifically addresses a common copy-paste error: inadvertently including leading or trailing spaces when copying keys from exchange dashboards. This issue often results in obscure signature errors during authenticated API calls, which are difficult to diagnose without this explicit check.
- The function returns a dictionary of validation results rather than raising an exception. This design provides flexibility to the caller in handling invalid credentials, allowing for actions such as logging warnings, halting execution, or prompting the user for re-entry.
# Validate credentials for all configured exchanges
exchanges_to_validate = ["bybit", "okx", "binance"]
print("--- Credential Validation ---")
validation_summary_rows = []
for exchange in exchanges_to_validate:
result = validate_credentials(exchange)
validation_summary_rows.append({
"Exchange": result["exchange"],
"Key Set": result["key_set"],
"Secret Set": result["secret_set"],
"Valid": result["valid"],
})
status = "PASS" if result["valid"] else "FAIL"
print(f"\n {result['exchange'].upper():<10} : {status}")
if not result["valid"]:
for err in result["errors"]:
print(f" - {err}")
print("\n--- Validation Summary ---")
display(pd.DataFrame(validation_summary_rows))7. Update a Key
def update_api_key(
exchange: str,
api_key: str = None,
api_secret: str = None,
env_file: str = ".env",
) -> None:
"""
Updates one or both API credentials for a specified exchange in the .env file.
Only arguments provided (non-None) are updated. Unspecified arguments
remain unchanged in the .env file.
Subsequent to the update, `load_api_keys()` is automatically invoked
to reload the new values into `os.environ` for the current session.
Parameters
----------
exchange : str
Exchange name (case-insensitive).
api_key : str | None, default=None
New API key. Pass `None` to leave unchanged.
api_secret : str | None, default=None
New API secret. Pass `None` to leave unchanged.
env_file : str, default='.env'
Path to the .env file.
"""
exchange_upper = exchange.upper()
if api_key is not None:
set_key(env_file, f"{exchange_upper}_API_KEY", api_key)
print(f"Updated {exchange_upper}_API_KEY in {env_file}")
if api_secret is not None:
set_key(env_file, f"{exchange_upper}_API_SECRET", api_secret)
print(f"Updated {exchange_upper}_API_SECRET in {env_file}")
if api_key is None and api_secret is None:
print("No update values provided; no changes made.")
return
load_api_keys(env_file)
print("Environment reloaded with updated values.")Explanation:
- The use of
is not Nonefor checkingapi_keyandapi_secretallows for explicitly passing an empty string to clear a key.Noneexplicitly signifies "do not update this field", ensuring that only specified credentials are modified while others are preserved. load_api_keys()is automatically called after an update. This reloads the latest values from the.envfile intoos.environ, making them immediately accessible for subsequent operations without manual intervention.
# Rotate only the Bybit API secret, leaving the API key unchanged
update_api_key(
exchange = "bybit",
api_secret = "your_new_rotated_bybit_secret_here",
# api_key is omitted (None) and thus remains unchanged
)
print()
# Confirm the updated value has been reloaded into the environment
creds = get_credentials("bybit")
print(f"Bybit API Key : {creds['api_key'][:6]}...")
print(f"Bybit API Secret : {creds['api_secret'][:6]}...")8. List All Configured Exchanges
def list_configured_exchanges(env_file: str = ".env") -> pd.DataFrame:
"""
Scans the .env file and generates a summary of all exchanges for which
at least an API key variable is defined.
Parameters
----------
env_file : str, default='.env'
Path to the .env file.
Returns
-------
pd.DataFrame
A DataFrame where each row represents an exchange and includes columns:
'exchange', 'key_set', 'secret_set', 'key_preview', 'both_configured'.
Returns an empty DataFrame if the .env file does not exist.
"""
path = Path(env_file)
if not path.exists():
print(f"{env_file} not found.")
return pd.DataFrame()
lines = path.read_text().splitlines()
exchanges_found = set()
for line in lines:
line = line.strip()
if line and not line.startswith("#"): # Skip blank lines and comments
if "_API_KEY=" in line:
exchange_name = line.split("_API_KEY=")[0].lower()
exchanges_found.add(exchange_name)
rows = []
for exchange in sorted(exchanges_found):
key = get_api_key(exchange)
secret = get_api_secret(exchange)
rows.append({
"exchange": exchange,
"key_set": bool(key),
"secret_set": bool(secret),
"key_preview": key[:6] + "..." if key else "(not set)",
"both_configured": bool(key) and bool(secret),
})
return pd.DataFrame(rows)Explanation:
- The function directly reads the
.envfile rather than relying onos.environ. This ensures that the output accurately reflects the state of keys stored on disk, regardless of whetherload_api_keys()has been executed in the current session. - Lines prefixed with
#are treated as comments and are disregarded, adhering to standard.envfile conventions. - The
key_previewcolumn displays only the first 6 characters of each key. This allows for visual confirmation of key presence without exposing the full credential in notebook output, enhancing security.
# Display a summary table of all exchanges with configured API keys in the .env file
print("--- Configured Exchanges in .env ---")
df_exchanges_summary = list_configured_exchanges(".env")
display(df_exchanges_summary)9. Standard API Client Integration Pattern
# This section illustrates the recommended three-step integration pattern
# for any module requiring connectivity to an exchange API.
# Step 1: Load environment variables at application startup
# This function should be called once, typically at the top of the main application script.
load_api_keys(".env")
# Step 2: Validate credentials prior to establishing a connection
# Early validation helps identify missing or malformed keys, preventing obscure authentication errors.
validation_result = validate_credentials("bybit")
if not validation_result["valid"]:
print("Bybit credentials are either missing or invalid:")
for error_message in validation_result["errors"]:
print(f" - {error_message}")
else:
# Step 3: Retrieve validated credentials and instantiate the exchange client
# The client constructor receives pre-validated string values; it should not directly access the .env file.
credentials = get_credentials("bybit")
api_key = credentials["api_key"]
api_secret = credentials["api_secret"]
print(f"Successfully retrieved credentials for {credentials['exchange'].upper()}")
print(f" API Key : {api_key[:6]}...")
print(f" API Secret : {api_secret[:6]}...")
# Example client instantiation (uncomment and replace with actual client library as needed)
# --- Bybit Client --- (Requires 'pybit' library)
# from pybit.unified_trading import HTTP
# client = HTTP(api_key=api_key, api_secret=api_secret, testnet=True)
# --- Binance Client --- (Requires 'python-binance' library)
# from binance.client import Client
# client = Client(api_key=api_key, api_secret=api_secret)
# --- OKX Client --- (Requires 'okx-api' library)
# from okx.Trade import TradeAPI
# client = TradeAPI(api_key, api_secret, passphrase="your_passphrase", flag="1")3. What Is a .env File?
A .env file is a plain-text file that stores secret key-value pairs
on disk. Each line defines one variable in the format KEY=VALUE.
Example .env file:
BYBIT_API_KEY=your_bybit_key_here
BYBIT_API_SECRET=your_bybit_secret_here
OKX_API_KEY=your_okx_key_here
OKX_API_SECRET=your_okx_secret_here
BINANCE_API_KEY=your_binance_key_here
BINANCE_API_SECRET=your_binance_secret_here
Why .env and not source code or config files?
| Storage Method | Safe? | Reason |
|---|
| Hard-coded in .py file | No | Visible in git history permanently |
| In config.yaml | No | Easily committed by accident |
| In .env file | Yes | Listed in .gitignore — never committed |
| In shell environment variable | Yes | Never written to disk at all |
Required .gitignore entry — add this before the first commit:
.env