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 Referer headers 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:

  1. Load the API key from an environment variable.
  2. Place the key in the X-API-Key header.
  3. Set an explicit timeout on every HTTP request.
  4. 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-Key
  • Authorization
  • Cookie
  • Set-Cookie
  • api_key query 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-Key as a header.
  • WebSocket requests add api_key only to the handshake URL.
  • Every HTTP request has an explicit timeout.
  • Error code 3001 triggers a Retry-After-aware delay.
  • Error codes 1001 and 1002 produce 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-Key header 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.