A URL can expose more than the resource it requests.
Copy one into a browser, paste it into a chat message, or enable verbose logging, and the URL may become visible in browser history, proxy logs, analytics systems, screenshots, and support tickets. For an ordinary page, that is an inconvenience. For an API key, it is a credential disclosure.
API clients must send credentials across networks, proxies, gateways, and application logs. The transport mechanism determines how many systems inspect the credential before it reaches the business logic.
This article compares URL parameters with HTTP headers, maps their leakage paths, and explains why TickDB uses the X-API-Key header for REST requests. It also covers the WebSocket authentication exception, deployment controls, and production-ready Python patterns.
Why API Keys Belong in HTTP Headers
TickDB uses an HTTP header for REST authentication:
GET /v1/market/kline/latest?symbol=AAPL.US HTTP/1.1
Host: api.tickdb.ai
X-API-Key: ${TICKDB_API_KEY}
Accept: application/json
This design does not make a leaked key harmless. TLS still encrypts the connection, and any credential can still be exposed through application bugs, verbose logs, compromised hosts, or incorrect secret handling. The header mainly reduces accidental disclosure by keeping the key out of the request target and the surfaces that conventionally record URLs.
The central distinction is simple:
- A URL identifies a resource and often propagates through infrastructure as metadata.
- A header carries request metadata and usually receives less routine logging.
- An API key in a URL therefore has more opportunities to be copied or recorded accidentally than the same key in a header.
REST Authentication and the WebSocket Exception
TickDB follows transport-specific authentication rules:
| Interface | Authentication location | Reason |
|---|---|---|
| REST API | X-API-Key request header |
Keeps the credential out of the request URL and common URL-logging surfaces |
| WebSocket | api_key URL query parameter |
TickDB's WebSocket handshake accepts the credential through the query parameter |
The WebSocket rule is an important exception. A WebSocket handshake is still an HTTP request, so the credential can enter access logs even when the application never exposes the full URL to end users. Deployments should therefore redact api_key query parameters at reverse proxies, gateways, and observability systems.
How a URL Becomes a Leaked Credential
A request URL passes through more components than application developers usually see.
Client
│
│ GET /endpoint?api_key=secret
▼
TLS termination / CDN / WAF
│
▼
Reverse proxy
│
▼
API gateway or load balancer
│
▼
Authentication middleware
│
▼
TickDB REST service
Each layer may retain the request target for routing, troubleshooting, caching, rate limiting, or observability. In a typical deployment, the URL may appear in:
- Access logs and reverse-proxy logs
- API gateway request histories
- APM traces and distributed tracing attributes
- Load-balancer diagnostic tools
- Error messages that include the requested URL
- Browser history and autocomplete indexes
- Shared links, chat messages, and screenshots
- Shell history when the key appears in a command-line argument
- Client-side telemetry or crash reports that collect full URLs
- HTTP
Refererheaders when a URL leads to another page
The exact exposure path depends on the architecture. Nevertheless, the number of systems that handle a request target is generally greater than the number that inspect arbitrary headers.
Leakage Risk by Transmission Method
| Exposure path | API key in URL parameter | API key in HTTP header |
|---|---|---|
| Reverse-proxy access log | High | Low by default |
| Browser history | High | Very low |
| Shared URL or screenshot | High | Low |
| APM URL attributes | High | Low by default |
HTTP Referer leakage |
Possible | Usually not applicable |
| Verbose request logging | Possible | Possible if headers are not redacted |
| Application exception containing the full URL | High | Low |
| Shell history or process arguments | Possible | Possible with inline CLI arguments |
| Compromise of the calling host | High | High |
| TLS interception by an authorized endpoint | High | High |
The last two rows matter. Headers reduce accidental exposure; they do not protect a key after the host, process, or endpoint has been compromised.
Header Versus URL Parameter: The Mechanical Differences
A header and a URL parameter both become bytes in an HTTP request. Their security properties differ because downstream software treats them differently.
URL Parameters Are Part of Resource Identity
A URL may define the resource to retrieve:
GET /v1/market/kline?symbol=AAPL.US&interval=1m&limit=100
The server must parse the query string before it can determine which resource the client requested. Consequently, URL-related components frequently retain the complete request target.
A custom header does not contribute to resource identity:
X-API-Key: ${TICKDB_API_KEY}
The API route remains the same whether the header is present, absent, or rotated. This separation makes it easier for infrastructure to log the resource path without recording the credential.
Headers Are Usually Excluded by Default
Most access logs begin with fields such as:
remote_addr - request_method request_path status response_size
They rarely include every request header. Production systems usually treat headers as sensitive because they may contain cookies, authorization credentials, session identifiers, or internal routing metadata.
This behavior is a default, not a guarantee. A verbose proxy configuration can still record the X-API-Key header:
log_format verbose '$remote_addr "$request" $status '
'key="$http_x_api_key"';
The second configuration violates the same confidentiality objective as logging a URL query parameter. Security depends on both the transport design and the logging policy.
Headers Provide Cleaner Credential Rotation
A header-based client can replace a credential without modifying the resource identifier:
headers = {
"X-API-Key": os.environ["TICKDB_API_KEY"],
"Accept": "application/json",
}
A URL-based client must rewrite the request target. That makes accidental propagation more likely, particularly when code copies complete URLs between components.
Headers Are Not Automatically Secure
The following practices still expose a header credential:
print("Request headers:", headers)
logger.debug("Outgoing request: %s", headers)
Avoid logging whole headers. Log only non-sensitive metadata:
logger.info(
"TickDB request started",
extra={"method": "GET", "path": "/v1/market/kline/latest"},
)
Apply the same rule to HTTP clients, reverse proxies, APM agents, error trackers, and developer tools.
TickDB REST Authentication Pattern
The safe client pattern has four properties:
- Load the API key from an environment variable.
- Place the key in the
X-API-Keyheader. - Set an explicit timeout on every HTTP request.
- Redact the credential from logs and exception messages.
import logging
import os
import random
import time
from typing import Any
import requests
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("tickdb-rest")
API_BASE_URL = "https://api.tickdb.ai"
API_KEY = os.environ.get("TICKDB_API_KEY")
if not API_KEY:
raise RuntimeError(
"TICKDB_API_KEY is not configured. "
"Set it in the process environment or secret manager."
)
session = requests.Session()
session.headers.update(
{
"X-API-Key": API_KEY,
"Accept": "application/json",
"User-Agent": "tickdb-secure-client/1.0",
}
)
class TickDBError(RuntimeError):
"""Base error for TickDB REST failures."""
class RateLimitError(TickDBError):
"""Raised when TickDB reports error code 3001."""
def __init__(self, retry_after: float):
self.retry_after = retry_after
super().__init__(f"TickDB rate limit exceeded; retry after {retry_after}s")
class AuthenticationError(TickDBError):
"""Raised when the API key is invalid or missing."""
def parse_retry_after(response: requests.Response, payload: dict[str, Any]) -> float:
"""Read Retry-After from HTTP headers, then from the API payload."""
header_value = response.headers.get("Retry-After")
if header_value:
try:
return max(0.0, float(header_value))
except ValueError:
pass
payload_value = payload.get("retry_after")
try:
return max(0.0, float(payload_value))
except (TypeError, ValueError):
return 5.0
def fetch_latest_kline(
symbol: str,
interval: str = "1m",
max_attempts: int = 3,
) -> Any:
"""Fetch the current candle with bounded retries and safe logging."""
url = f"{API_BASE_URL}/v1/market/kline/latest"
for attempt in range(max_attempts):
try:
response = session.get(
url,
params={"symbol": symbol, "interval": interval},
timeout=(3.05, 10),
)
try:
payload = response.json()
except ValueError:
payload = {}
code = payload.get("code", 0)
if response.status_code == 429 or code == 3001:
retry_after = parse_retry_after(response, payload)
if attempt == max_attempts - 1:
raise RateLimitError(retry_after)
delay = retry_after + random.uniform(0, 0.25)
logger.warning(
"TickDB rate limit reached; retrying",
extra={
"symbol": symbol,
"attempt": attempt + 1,
"delay_seconds": round(delay, 3),
},
)
time.sleep(delay)
continue
if response.status_code in (401, 403):
raise AuthenticationError("TickDB rejected the API key")
response.raise_for_status()
if code in (1001, 1002):
raise AuthenticationError(
"Invalid or missing TickDB API key; check TICKDB_API_KEY"
)
if code == 2002:
raise KeyError(
f"Symbol {symbol!r} was not found; "
"verify it through /v1/symbols/available"
)
if code != 0:
raise TickDBError(
f"TickDB returned error {code}: "
f"{payload.get('message', 'unknown error')}"
)
return payload.get("data")
except (requests.Timeout, requests.ConnectionError) as exc:
if attempt == max_attempts - 1:
# requests exceptions can include the URL, but not the API header.
raise TickDBError(f"TickDB request failed: {exc.__class__.__name__}") from exc
delay = min(2 ** attempt, 30) + random.uniform(0, 0.25)
logger.warning(
"TickDB request failed; retrying",
extra={
"symbol": symbol,
"attempt": attempt + 1,
"delay_seconds": round(delay, 3),
},
)
time.sleep(delay)
raise TickDBError("TickDB request exhausted all retry attempts")
The code deliberately avoids logging session.headers. It also logs the symbol and retry delay rather than the full request, preventing both credentials and unnecessary query data from entering telemetry.
Why the Timeout Matters
A missing timeout can leave a worker waiting indefinitely for a stalled connection. The tuple in the example defines separate connect and read limits:
timeout=(3.05, 10)
This bounds the two different phases of the request. It does not guarantee a total runtime of 13.05 seconds because retries and server processing can extend the overall operation, but it prevents either phase from hanging without a bound.
Secure WebSocket Handling with Heartbeat and Reconnection
TickDB's WebSocket interface uses api_key as a query parameter. Although this differs from REST authentication, the same defensive principles apply: use TLS, redact the query string from logs, rotate credentials, and never print the complete WebSocket URL.
The following client demonstrates heartbeat, exponential backoff with jitter, timeouts, and rate-limit handling:
import asyncio
import json
import logging
import os
import random
from typing import Any
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
import websockets
from websockets.exceptions import ConnectionClosed
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("tickdb-websocket")
API_KEY = os.environ.get("TICKDB_API_KEY")
WS_BASE_URL = os.environ.get("TICKDB_WS_URL")
if not API_KEY:
raise RuntimeError("TICKDB_API_KEY is not configured")
if not WS_BASE_URL:
raise RuntimeError(
"TICKDB_WS_URL is not configured. "
"Set it to the TickDB WebSocket endpoint without an API key."
)
def add_api_key(base_url: str, api_key: str) -> str:
"""Add TickDB's WebSocket authentication parameter without logging it."""
parts = urlsplit(base_url)
query = dict(parse_qsl(parts.query, keep_blank_values=True))
query["api_key"] = api_key
return urlunsplit(
(parts.scheme, parts.netloc, parts.path, urlencode(query), parts.fragment)
)
def sanitize_error(exc: BaseException) -> str:
"""Remove query strings from connection exceptions."""
# Never log the authenticated URL itself.
return exc.__class__.__name__
async def heartbeat(websocket: Any, interval: float = 15.0) -> None:
"""Send TickDB's application-level ping command."""
while True:
await asyncio.sleep(interval)
await websocket.send(json.dumps({"cmd": "ping"}))
async def consume_messages() -> None:
"""Connect, heartbeat, process messages, and reconnect with jitter."""
retry = 0
while True:
authenticated_url = add_api_key(WS_BASE_URL, API_KEY)
try:
async with websockets.connect(
authenticated_url,
open_timeout=10,
close_timeout=5,
ping_interval=20,
ping_timeout=10,
max_queue=1000,
) as websocket:
retry = 0
heartbeat_task = asyncio.create_task(heartbeat(websocket))
logger.info("TickDB WebSocket connected")
try:
async for raw_message in websocket:
try:
message = json.loads(raw_message)
except (TypeError, ValueError):
logger.warning("Received a non-JSON WebSocket message")
continue
code = message.get("code", 0)
if code == 3001:
retry_after = float(message.get("retry_after", 5))
logger.warning(
"TickDB WebSocket rate limit reached",
extra={"retry_after": retry_after},
)
await asyncio.sleep(retry_after)
break
if code in (1001, 1002):
raise PermissionError("TickDB rejected the WebSocket API key")
if code == 2002:
raise KeyError("TickDB could not resolve the requested symbol")
# Route validated market data to the application.
handle_market_message(message)
finally:
heartbeat_task.cancel()
await asyncio.gather(heartbeat_task, return_exceptions=True)
except PermissionError:
raise
except (ConnectionClosed, OSError, TimeoutError) as exc:
delay = min(2 ** retry, 30)
jitter = random.uniform(0, delay * 0.1)
retry += 1
logger.warning(
"TickDB WebSocket disconnected; reconnecting",
extra={
"error_type": sanitize_error(exc),
"delay_seconds": round(delay + jitter, 3),
},
)
await asyncio.sleep(delay + jitter)
def handle_market_message(message: dict[str, Any]) -> None:
"""Application callback for validated TickDB messages."""
logger.debug("TickDB market message received", extra={"keys": sorted(message)})
Two details in this implementation deserve attention.
First, the code does not log authenticated_url. A generic exception handler that prints the connection target would expose the query parameter and undo the transport design.
Second, the heartbeat and WebSocket protocol ping are separate mechanisms. The protocol ping checks transport liveness, while {"cmd": "ping"} checks TickDB's application-level connection state. A production client can use both, although the exact heartbeat interval should be validated against the service's limits.
Engineering warning: Do not use this polling pattern for high-frequency market-data workloads. Evaluate the appropriate asynchronous transport, benchmark callback latency, and size queues before production deployment.
The Security Boundary: Headers Are One Control, Not the Whole System
A header-based API key improves credential isolation, but it remains a reusable bearer secret. A robust deployment applies several controls around it.
1. Require TLS End to End
Use HTTPS or WSS for every connection, including connections between internal services. TLS prevents network observers from reading the header or WebSocket query parameter in transit.
TLS does not protect the key after decryption at the server, proxy, or application host.
2. Store Keys Outside Source Control
Load keys from environment variables, a container secret, or a dedicated secret manager. Do not place keys in:
- Source repositories
- Docker images
- Terraform state without encryption controls
- Shared notebooks
- CI logs
- Shell scripts containing literal values
An environment variable is appropriate for local development and simpler deployments. Larger environments should use a secret manager with rotation and access auditing.
3. Redact Sensitive Headers
Configure application logging, reverse proxies, and APM agents to omit:
X-API-KeyAuthorizationCookieSet-Cookieapi_keyquery parameters
For Nginx, log the path separately and avoid $request_uri when the query string can contain secrets. A safer pattern is:
log_format tickdb_safe '$remote_addr $request_method $uri $status '
'$body_bytes_sent $request_time';
The exact proxy configuration depends on the deployment, but the review question remains simple: can this log line contain a credential?
4. Rotate and Revoke Keys
Header placement improves leakage prevention, but key rotation limits the damage from an undisclosed exposure. Establish:
- A maximum credential lifetime
- Automated rotation where practical
- Immediate revocation for suspected disclosure
- Separate keys for development, staging, and production
- No shared keys across unrelated systems
5. Minimize Privileges
An API key should grant only the access required by the client. Although TickDB's plans may differ from fine-grained credential policies, the deployment rule remains universal: do not reuse a production credential in an untrusted script or public repository.
6. Avoid Command-Line Secrets
Passing a key in a command can expose it through shell history or process inspection. Prefer an environment variable:
export TICKDB_API_KEY='your-key-from-your-secret-manager'
For production automation, inject the variable from the orchestrator's secret mechanism rather than writing it into a launch script.
Security Controls by Deployment Environment
| Environment | Recommended controls | Logging policy |
|---|---|---|
| Local development | Environment variable; .env file excluded from Git |
Do not log headers |
| CI pipeline | Masked secret; short-lived credential where available | Mask secret values in all steps |
| Single VPS | TLS; secret file with restrictive permissions; automated restart after rotation | Redact headers and query parameters |
| Container platform | Runtime secret injection; separate credentials per environment | Disable request-header dumps |
| Kubernetes | External secret integration; network policy; Pod identity | Scrub proxy and APM attributes |
| Institutional deployment | Central secret manager; rotation; audit trails; dedicated egress rules | Restrict and retain access logs |
Operational Checklist
Before deploying a TickDB client, verify the following:
- REST requests send
X-API-Keyas a header. - WebSocket requests add
api_keyonly to the handshake URL. - Every HTTP request has an explicit timeout.
- Error code
3001triggers aRetry-After-aware delay. - Error codes
1001and1002produce an authentication failure. - Logs omit the API key and authenticated WebSocket URL.
- Reverse-proxy and APM configurations redact credential query parameters.
- Reconnection uses exponential backoff with jitter.
- Development and production use separate keys.
- Rotation and revocation procedures have an assigned owner.
Why Header Authentication Is the Better REST Default
Header authentication is not an absolute security guarantee. It is a deliberate reduction in accidental exposure.
| Design choice | Security benefit | Remaining risk |
|---|---|---|
X-API-Key header |
Keeps the key out of the request target and standard URL logs | Application or proxy may still log headers |
| API key in a URL query | Simple to construct and easy to append | Broad exposure across logs, traces, history, and shared URLs |
| Short-lived bearer token | Limits the useful lifetime of a leaked credential | Requires token issuance and renewal infrastructure |
| mTLS | Authenticates the calling machine cryptographically | Certificate and key management add operational complexity |
| Signed request | Can bind method, path, body, and timestamp | More complex client and server implementation |
For TickDB's REST interface, the header provides the right balance of simplicity and exposure reduction. It uses the standard request-header path for credentials, keeps the key separate from resource identity, and fits existing REST authentication middleware.
The WebSocket query parameter remains a necessary protocol constraint. That exception should be handled with stronger log redaction rather than treated as evidence that URL authentication is generally superior.
Conclusion: A URL Should Identify a Resource, Not Carry Its Secret
A URL can expose more than the resource it requests.
TickDB's REST header design makes accidental credential leakage less likely by separating the API key from the request target. The design does not replace TLS, secret management, log redaction, rotation, or scoped permissions. It narrows the number of systems that can expose the key by accident.
The deployment principle is straightforward:
- Use the
X-API-Keyheader for TickDB REST requests. - Use the documented query parameter for TickDB WebSocket handshakes, then redact it everywhere.
- Never log authenticated URLs or complete request headers.
- Rotate credentials and revoke them promptly after suspected exposure.
Next Steps
If you are an individual quant developer, sign up at tickdb.ai, generate an API key, and set it as TICKDB_API_KEY in your development environment. Keep it out of Git and run the examples against a non-production endpoint first.
If you operate a production platform, add secret rotation, proxy redaction, APM scrubbing, and credential-leak response procedures before scaling the deployment.
If you need institutional data access or governance controls, contact [email protected] to discuss Professional and Enterprise plans.
If you use AI coding assistants, search for and install the tickdb-market-data SKILL in your assistant's marketplace.
This article does not constitute investment advice. Markets involve risk; past performance does not guarantee future results.