In market data troubleshooting, 403 and 429 get lumped together constantly — yet they are fundamentally different. A 429 means you exceeded usage and waiting may fix it. A 403 means you lack entitlement, and waiting will never fix it.
Treating a 403 like a 429 has a predictable outcome: you write automatic retry logic, then watch the service fail continuously for a whole day. This article takes apart all six causes of a 403.
Six distinct causes of a 403
A 403 from a market data API maps to at least six different entitlement problems, each with its own remedy:
| Error code | Meaning | Remedy |
|---|---|---|
NO_SUBSCRIPTION | No active subscription | Subscribe to a plan |
SUBSCRIPTION_EXPIRED | Subscription has expired | Renew or check billing status |
MARKET_NOT_ENABLED | Market not enabled on your plan | Upgrade, or request an enabled market |
API_KEY_SCOPE_DENIED | Key scope excludes this endpoint | Use a key with the correct scope |
IP_DENIED | Source IP rejected by policy | Update the IP allowlist or egress path |
GEO_DENIED | Source region rejected by policy | Change deployment region or contact support |
As with 429, the deciding signal is the code field in the response body, not the HTTP status. All six are 403s, but they lead down completely different paths.
NO_SUBSCRIPTION and SUBSCRIPTION_EXPIRED: account state
These two indicate no usable subscription at the account level. The distinction: the first means "never subscribed or cancelled", the second means "was valid, now expired".
Worth noting: the free tier is itself a subscription. If you request a market outside the free tier's coverage, the expected error is MARKET_NOT_ENABLED, not NO_SUBSCRIPTION. So when you see NO_SUBSCRIPTION, check account subscription state and billing before you start scrutinising request parameters.
MARKET_NOT_ENABLED: the most common 403
This is the one you will hit most often in practice. The meaning is unambiguous: your plan does not cover the market you requested.
Market entitlements vary substantially across tiers, and note that "live" and "delayed" are licensed separately. A typical case: a market is available to you as delayed data, but you call the live endpoint and receive MARKET_NOT_ENABLED. Upgrading does fix this — but first confirm whether you genuinely need live or delayed data, which often saves real money.
The diagnostic is straightforward: establish the entitlement level for the target market on your current plan, then compare it against the endpoint type you called. If delayed data is sufficient, use the matching endpoint.
API_KEY_SCOPE_DENIED: key scope
Keys can be scoped by purpose. You hit this error when a read-only market data key is used against a different endpoint, or when a key scoped to one service is used for another.
Two common scenarios:
- Cross-environment key reuse. A development key gets deployed to production without the scopes production needs.
- Rotation out of sync. A new key was created with the wrong scope while the old one was already revoked.
Create separate keys per environment (dev, staging, production) and follow least privilege. If a key leaks, revoke and rotate it in the console immediately — editing a string in your codebase is not enough.
IP_DENIED and GEO_DENIED: network policy
These come from access policy rather than plan entitlement. Typical causes:
- The egress IP changed. The service moved from a fixed-IP server to elastic IPs or a container platform, so the egress address is no longer allowlisted.
- Local development collateral. Debugging from an office or home connection with a production key, where the local IP is not allowlisted.
- A region change. The service migrated to a new cloud region, tripping a geographic policy restriction.
Start by confirming your actual egress IP — containers, NAT gateways, and proxies all change it — then compare against the allowlist and region policy.
Why retrying a 403 never works
Entitlement problems do not resolve with the passage of time. Worse, blind retries cause three side effects:
- They mask the failure. Retry logic turns an unambiguous entitlement error into "intermittent failures" and delays diagnosis.
- They waste quota. If rejected requests count toward rate counters, they squeeze legitimate traffic.
- They pollute monitoring. Error-rate metrics get diluted and alert thresholds lose meaning.
The right rule: never retry a 403. Alert immediately, and log the code alongside the X-Request-Id.
Diagnostic workflow
- Read
codeand classify it into one of the six. - Record
X-Request-Idto quote to support. - Subscription class (NO_SUBSCRIPTION / SUBSCRIPTION_EXPIRED): check account subscription and billing.
- Market class (MARKET_NOT_ENABLED): verify the market's entitlement level on your plan, and whether you called a live or delayed endpoint.
- Key class (API_KEY_SCOPE_DENIED): review key scopes and check for cross-environment reuse.
- Policy class (IP_DENIED / GEO_DENIED): confirm the actual egress IP and deployment region.
- Separate 403 from 429 in your client. Fail fast on 403; retry only the rate-limiting flavour of 429.
Python: error handling that separates 403 from 429
import httpx
API_KEY = "your_api_key"
BASE = "https://api.tiqex.io/api/v1/market"
# No 403 is ever retried; only rate limiting is
FATAL_403 = {
"NO_SUBSCRIPTION", "SUBSCRIPTION_EXPIRED", "MARKET_NOT_ENABLED",
"API_KEY_SCOPE_DENIED", "IP_DENIED", "GEO_DENIED",
}
RETRYABLE_429 = {"RATE_LIMIT_EXCEEDED"}
class FatalAccessError(Exception):
"""Entitlement error: retrying is pointless and needs a human."""
def get_quote(codes: str):
resp = httpx.get(
f"{BASE}/quote",
params={"codes": codes},
headers={"X-API-Key": API_KEY},
timeout=10,
)
if resp.status_code == 200:
return resp.json()
body = resp.json() if resp.content else {}
code = body.get("code")
request_id = resp.headers.get("X-Request-Id")
if resp.status_code == 403:
hint = {
"NO_SUBSCRIPTION": "no active subscription; check plan and billing",
"SUBSCRIPTION_EXPIRED": "subscription expired; renew it",
"MARKET_NOT_ENABLED": "market not on your plan, or wrong live/delayed endpoint",
"API_KEY_SCOPE_DENIED": "key scope excludes this endpoint",
"IP_DENIED": "source IP not allowlisted; verify the egress IP",
"GEO_DENIED": "source region blocked by policy",
}.get(code, "unknown entitlement error")
raise FatalAccessError(f"{code}: {hint} (requestId={request_id})")
raise RuntimeError(f"request failed code={code} requestId={request_id}")
try:
print(get_quote("BTCUSDT,AAPL"))
except FatalAccessError as e:
print("needs human attention:", e)
# Alert here — do not enqueue for retry
The core idea is giving 403 its own exception path. It never enters the retry queue; it raises an alert. That stops entitlement outages from being masked as intermittent problems.
FAQ
How do I quickly tell a 403 from a 429?
Look at the code field. If the error clears by itself after a pause, it is almost certainly a 429. If it persists and is independent of request volume, it is a 403.
Can the free tier return a 403?
Yes. The free tier covers delayed data for a subset of markets, so requesting a market outside that coverage returns MARKET_NOT_ENABLED.
Why does the same key work locally but fail on my server?
Most likely IP_DENIED. The key is valid, but the server's egress IP is not allowlisted. Remember that container platforms, NAT gateways, and proxies all change the effective egress IP.
Will upgrading resolve every 403?
No. Only MARKET_NOT_ENABLED and the subscription-class errors can be resolved by upgrading. API_KEY_SCOPE_DENIED, IP_DENIED, and GEO_DENIED are configuration and policy issues where an upgrade changes nothing.
Do rejected requests consume quota?
Business quota is generally unaffected, but requests still increment rate counters. More importantly, a flood of 403s distorts error-rate metrics, so group 403s separately in monitoring.
Next steps
Removing 403 from your retry logic is one of the highest-return changes you can make in a market data integration. The complete error code reference is in the errors and rate limits docs. Key creation, per-environment management, and rotation are covered in the authentication docs and the REST authentication guide. If the problem is plan entitlement, compare live versus delayed coverage across tiers on the pricing page, or check a specific market page such as crypto.