Market holidayJapan will be closed on 2026-11-03 (Culture Day), 5 days from now.All announcements
Article

Decoding 403 Errors on Market Data APIs: Six Reasons Access Gets Denied

A 403 is not a 429 — waiting will never fix it. We break down all six causes on market data APIs: subscription state, market entitlement, key scope, and IP/region policy, plus why retrying is useless and how to code the split in Python.

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 codeMeaningRemedy
NO_SUBSCRIPTIONNo active subscriptionSubscribe to a plan
SUBSCRIPTION_EXPIREDSubscription has expiredRenew or check billing status
MARKET_NOT_ENABLEDMarket not enabled on your planUpgrade, or request an enabled market
API_KEY_SCOPE_DENIEDKey scope excludes this endpointUse a key with the correct scope
IP_DENIEDSource IP rejected by policyUpdate the IP allowlist or egress path
GEO_DENIEDSource region rejected by policyChange 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:

  1. They mask the failure. Retry logic turns an unambiguous entitlement error into "intermittent failures" and delays diagnosis.
  2. They waste quota. If rejected requests count toward rate counters, they squeeze legitimate traffic.
  3. 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

  1. Read code and classify it into one of the six.
  2. Record X-Request-Id to quote to support.
  3. Subscription class (NO_SUBSCRIPTION / SUBSCRIPTION_EXPIRED): check account subscription and billing.
  4. Market class (MARKET_NOT_ENABLED): verify the market's entitlement level on your plan, and whether you called a live or delayed endpoint.
  5. Key class (API_KEY_SCOPE_DENIED): review key scopes and check for cross-environment reuse.
  6. Policy class (IP_DENIED / GEO_DENIED): confirm the actual egress IP and deployment region.
  7. 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

Python
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.