An API key 401 Unauthorized error does not automatically mean you need a new key. Start with the header, then the value, then the response body. A 403 needs a different check: the request may be authenticated but lack permission for the requested data. New to SiftingIO? Create an account and get an API key to try the examples; if you already have a key, keep it while you work through the checks below.
This guide is for API-key requests to the SiftingIO REST API. It is not an OAuth troubleshooting guide. The three 401 responses in the table were checked against the live API on October 9, 2026, using a missing credential and deliberately invalid test strings. They are diagnostic examples, not an exhaustive list of every possible authentication failure.
| Response | What to check first | Should you replace the key? |
|---|---|---|
401, missing api key (X-API-Key header or api_key query param) | Is the key actually reaching the server? | Not until you check the environment and header |
401, invalid_credentials after sending a key as Bearer | Was a dashboard API key put in Authorization instead of X-API-Key? | Correct the header first; this error can have other causes in OAuth flows |
401, invalid api key | Is the stored value complete, correct and still active? | Only if it is lost, revoked or must be rotated |
| 403 | Read the error and check endpoint permissions, market access or scope | Rotation does not add permissions |
| 429, 5xx or a network failure | Rate limits, service availability or connectivity | These are not, by themselves, proof of a bad key |
1. Send a dashboard API key in X-API-Key#
The REST API accepts a dashboard key in X-API-Key. It also accepts api_key as a query parameter, but the header keeps the credential out of the URL. The API quickstart documents both forms.
Use a key stored in your local environment or secret manager, not a literal pasted into a shared script:
: "${SIFTING_KEY:?Set SIFTING_KEY in this process before running the request}"
curl --compressed --max-time 15 -i \
-H "X-API-Key: $SIFTING_KEY" \
"https://api.sifting.io/v1/fnd/stocks/AAPL/profile"
-i includes the response status and headers. You do not need verbose request logging to see whether the server returned 401. Avoid sharing curl -v output or a dump of response.request.headers: either can expose the key. Also keep shell tracing (set -x) off while running commands that expand secrets.
For an API-key client, this is the relevant change:
# Wrong place for a dashboard API key:
headers = {"Authorization": f"Bearer {key}"}
# API-key authentication:
headers = {"X-API-Key": key}
This does not mean SiftingIO has no OAuth support. Account-linked integrations use a separate OAuth flow; its tokens and dashboard API keys are not interchangeable. Do not copy a dashboard session token or an integration's access token into this example. When testing API-key authentication, remove an unrelated Authorization header so the request has one clear credential source.
2. Check the process that makes the request#
The missing-key response was:
{"error":"missing api key (X-API-Key header or api_key query param)"}
An environment variable can exist in your terminal and be absent from the container, scheduled job or deployed process that runs the application. Check whether it is set there, without printing its value. Restart or redeploy the relevant process if changing its environment requires that; editing a local .env file does not update an already-running remote service.
Next, check the header name and the client instance. Setting a header on one session does not affect a request made through another. A proxy may also remove headers it is not configured to forward.
The parameter names differ between protocols: REST uses api_key for its query-string fallback, while the WebSocket connection uses key. For REST, prefer the header rather than trying variations of query parameters.
3. Check the saved secret, not the truncated dashboard label#
An intentionally invalid string sent in X-API-Key returned:
{"error":"invalid api key"}
Check for a truncated copy, literal quote characters, accidental whitespace, or a different secret loaded under the same environment-variable name. A client may reject a newline locally before any HTTP response arrives. The fact that a value starts with sft_ is not proof that it is valid.
Use your own securely stored copy for comparison. SiftingIO displays the full key only when it is created. The dashboard's label and truncated prefix identify a key; they cannot reveal its full value or let you recover its ending later. Check whether the key is still active in the dashboard, but do not expect to copy the secret from that list.
If you lost the full key, it must be replaced. Copy and save the new value when it is shown. Check your active-key allowance first: if there is only one available slot and it is occupied, revoking the old key will interrupt applications still using it. Plan that cutover rather than repeatedly generating keys while debugging. The separate API key rotation guide covers replacement and leak response.
A valid key from another account is not necessarily rejected. It can authenticate as that account, with that account's access and usage limits. Confirm the intended account and secret source instead of treating account mix-ups as a guaranteed 401.
4. Treat 403 as an access question#
For SiftingIO market-data requests, a 403 can mean the authenticated account is not entitled to the requested product or market. Inspect the message and your actual access before changing the request or plan. Free-plan coverage and paid-market permissions are not the same thing; do not assume every 403 requires a paid upgrade.
OAuth permissions can also produce a scope-related 403. A response from a proxy or firewall may not be an API entitlement response at all. The status alone does not prove that a particular API key was accepted.
If one endpoint succeeds and another returns 403, compare the resources and permissions they require. A successful company-profile request does not prove access to every US stock dataset, and it does not validate a separate forex data workflow. The error reference is the starting point for interpreting the response.
A Python diagnostic that does not mistake a timeout for success#
The following standalone check makes two GET requests, one for a stock profile and one for a forex quote. Remove any probe your application does not need. It does not print the key, raw request headers, response bodies or exception details.
Install requests with python -m pip install requests, save the code as check_api_key.py, and run python check_api_key.py with SIFTING_KEY supplied through your environment.
import os
import sys
import requests
BASE = "https://api.sifting.io"
PROBES = {
"stock profile": "/v1/fnd/stocks/AAPL/profile",
"forex quote": "/v1/last/quote/forex/EURUSD",
}
def preflight(key):
if not key:
return "SIFTING_KEY is missing or empty"
if key != key.strip():
return "The key contains leading or trailing whitespace"
if not key.isascii() or any(ord(c) < 32 or ord(c) == 127 for c in key):
return "The key contains non-ASCII or control characters"
if key[0] in ("'", '"') or key[-1] in ("'", '"'):
return "Remove literal quote characters from the stored value"
if key.lower().startswith("bearer "):
return "Store the bare API key, without a Bearer prefix"
return None
def error_text(response):
try:
body = response.json()
except ValueError:
return ""
value = body.get("error", "") if isinstance(body, dict) else ""
return value.lower() if isinstance(value, str) else ""
def classify(status, error):
if status == 200:
return 0, "request succeeded for this endpoint"
if status == 401:
if "missing api key" in error:
return 1, "key missing: check environment, header and proxy"
if error == "invalid_credentials":
return 1, "credential rejected: check type and Authorization header"
if "invalid api key" in error:
return 1, "key rejected: check the saved value and active status"
return 1, "401: credentials were not accepted; inspect a redacted response"
if status == 403:
return 1, "403: check resource permissions, market access and scope"
return 2, f"HTTP {status}: inconclusive, not a successful auth check"
def check(session):
results = []
for name, path in PROBES.items():
try:
response = session.get(
BASE + path, timeout=(5, 15), allow_redirects=False
)
except requests.RequestException:
code, message = 2, "network/client failure; no auth verdict"
else:
code, message = classify(response.status_code, error_text(response))
results.append(code)
print(f"[{name}] {message}")
if 1 in results:
return 1
return 2 if not results or 2 in results else 0
def main():
key = os.environ.get("SIFTING_KEY")
problem = preflight(key)
if problem:
print(problem)
return 1
with requests.Session() as session:
# Controlled direct test: do not inherit .netrc auth or proxy settings.
session.trust_env = False
session.headers.update({"X-API-Key": key, "Accept-Encoding": "gzip"})
return check(session)
if __name__ == "__main__":
sys.exit(main())
The exit code is 0 only when every configured probe returns 200, 1 for a local credential problem or a 401/403, and 2 when the outcome is inconclusive. A 429, 5xx, redirect or timeout never produces a successful check. HTTP 200 establishes success for that request; it does not certify data quality or permissions for other endpoints.
This test deliberately ignores environment-provided proxies, .netrc authentication and custom CA-bundle settings. If your network requires those settings, configure the required proxy or CA bundle explicitly rather than disabling TLS verification. A successful direct test is only a baseline: repeat the relevant request through your application's real client and network path to find a configuration difference.
The response strings are observations, not a permanent schema. Unknown 401 bodies still fail the check without being assigned an invented cause. The default logs stay free of raw secrets; if support needs more detail, inspect a redacted response separately.
REST and WebSocket authentication are separate checks#
For a stream, follow the WebSocket authentication instructions, not the REST header example above. The documented options are the connection's ?key= parameter or an initial auth message. An auth_required, auth_timeout or auth_failed frame should be investigated in that protocol's context.
A working REST request is useful evidence about a key, but it does not prove that a WebSocket subscription has the necessary product access or available connection capacity. Do not regenerate a key merely because a stream's subscription failed.
Once authentication works, keep the credential on your backend. Using a market data API in a web app without exposing your key covers that next step. If the response is 429 instead of 401, use the rate-limit and retry guide.
What to send when you need help#
Share the endpoint path without credential-bearing query parameters, the approximate time and timezone, HTTP status, and a redacted error message. Say whether the request ran locally, in a container or in a deployed service, and name the header you used without including its value. Never send the full key, OAuth tokens, cookies or an unredacted debug trace.
For a new integration, start with the quickstart and an endpoint your account can access. Confirm one request before adding retries, live subscriptions or a large historical download. That gives you a known working baseline to compare against when the application behaves differently.



