The X-API-Key Header: How to Send an API Key in a Request

By TwitterAPI.io Team•14 min read
Diagram of three places an API key can travel in an HTTP request. Panel 1, custom header: GET /twitter/user/info?userName=X to api.twitterapi.io with X-API-Key: your key, a de-facto convention used by AWS API Gateway, Anthropic and twitterapi.io, header names case-insensitive. Panel 2, Authorization: Bearer token to api.x.com, the RFC 6750 scheme used by OAuth 2.0, 401 with WWW-Authenticate on a bad token. Panel 3, query string api_key=, which ends up in access logs, proxies, Referer and history; RFC 6750 and OWASP say avoid it. Footer: a wrong value in X-API-Key returned HTTP 401 in all 3 variants we probed on api.twitterapi.io; a missing header is documented as 403.
Same key, three placements. The custom header and the Bearer scheme keep the secret out of URLs; the query string does not. Diagram; the footer numbers come from our probe below.

This page is about the header itself: what X-API-Key is, how it differs from Authorization: Bearer, the exact syntax in curl, Python, Node and Postman, and why a request that looks right still comes back 401 or 403. If you landed here looking for a key for X's own developer platform, that is a different job, covered step by step in how to get an X API key; and if you need a key for xAI's models, see how to get a Grok API key. The overlap is not just yours: in our sample of public posts below, nearly a third of the posts that contain the phrase use it to mean a key for X, not the header.

Our worked example is twitterapi.io, which authenticates every read with exactly this header and no OAuth, so every snippet below runs against a real API. The conventions apply to any service that asks for X-API-Key: AWS API Gateway usage plans, many SaaS APIs, and the Claude API, which now prefers Authorization: Bearer but still accepts x-api-key.

Then two pieces of first-hand data: a live probe of what our API returns for the three most common wrong values, and a read of every recent public post that mentions x-api-key, to see what developers are actually wiring it into and how often a real-looking key gets pasted in public.

01 — Section

What the X-API-Key header is (and what it is not)

X-API-Key is an HTTP request header whose value is a static secret the provider issued to you. The server reads it, looks the key up, and decides who you are, which plan you are on and how fast you may call. There is no scheme word and no signature: the header line is the whole mechanism, X-API-Key: <key>.

It is a convention, not a standard. No RFC defines X-API-Key. The X- prefix itself was the old way to mark experimental headers, and RFC 6648 (June 2012, BCP 178) says creators of new parameters SHOULD NOT prefix them with X-. The name survived because it was already everywhere: changing it would break every client, so providers kept it.

Who uses it. AWS API Gateway documents two API key sources for usage plans: HEADER, where clients pass the key as the X-API-Key header, and AUTHORIZER, where a Lambda authorizer returns it; a request without a matching key gets 403 Forbidden. Anthropic's Claude API lists x-api-key as a legacy fallback for Authorization: Bearer, still supported. Postman has a dedicated API Key auth type with a Key, a Value and an Add to choice of Header or Query Params. And twitterapi.io's docs state it plainly: a single x-api-key header, no OAuth.

Case does not matter for the name. RFC 9110 section 5.1 says field names are case-insensitive, so x-api-key, X-API-Key and X-Api-Key are the same header to any compliant server, proxy or framework. The value is a different story: keys are case-sensitive strings and must arrive byte for byte.

What it is not. It is not how the official X API authenticates. X's docs list OAuth 1.0a user context, an app-only Bearer token (OAuth 2.0), Basic auth for some enterprise APIs, and OAuth 2.0 with PKCE; none of them uses an X-API-Key header. If a client library asked you for an X API key, that is the X developer portal flow, covered in our separate step-by-step guide.

Two-panel chart. Left: of 380 public posts containing x-api-key, 51.6% mean the header name, 30.5% mean a key for X's own API, 17.9% neither. Right: the 196 header posts spell it x-api-key 52.0%, X-API-Key 35.2%, X-Api-Key 8.2%, X-API-KEY 3.6%, other casing 1.5%.
The phrase is two searches in one, and the header is spelled at least five ways, all identical on the wire. Source: twitterapi.io /twitter/tweet/advanced_search, 380 posts, collected in 2026.
02 — Section

X-API-Key vs Authorization: Bearer vs a query-string key

There are three places a static key can travel. The first two are fine for server-to-server calls; the third is the one standards bodies warn about.

X-API-Key: <key>Authorization: Bearer <token>?api_key=<key>
Defined byconvention (AWS, SaaS APIs)RFC 6750 (OAuth 2.0)nothing; provider choice
Typical credentiallong-lived static keyoften short-lived tokenstatic key
Error contractprovider-specific (401 or 403)401 + WWW-Authenticateprovider-specific
Leaks into URL logsnonoyes
Stripped on cross-host redirect by Python requestsnoyesstays in the new URL

Bearer is the standard, not necessarily the safer header. RFC 6750 says clients SHOULD send bearer tokens in the Authorization request header field, and that a resource server MUST include a WWW-Authenticate challenge when the token is missing or invalid, with invalid_token mapped to 401 and insufficient_scope to 403. That shared contract is why generic tooling (HTTP clients, proxies, API gateways, log scrubbers) treats Authorization as sensitive by default. A custom X-API-Key header gets no such special treatment unless someone configured it.

The query string is the weak option. RFC 6750 section 2.3 says the URI query parameter method SHOULD NOT be used unless the Authorization header is impossible, and that such requests should carry Cache-Control: no-store. The OWASP REST Security Cheat Sheet makes the same point for API keys: sensitive information should not appear in the URL, because URLs are captured in web server logs. A URL also lands in browser history, proxy logs and, from a browser, the Referer header.

Which one to use is not your choice as a client. Send what the provider documents. Some services accept several forms (several posts in our sample announced APIs that take the same key as X-API-Key, as a Bearer token, or as a query parameter), but twitterapi.io documents x-api-key, so that is what every example on this page sends.

03 — Section

How to send the X-API-Key header: curl and Postman

curl. Use -H (or --header) with the header name, a colon, a space and the bare key. Keep the key in an environment variable so it never lands in your shell history or a pasted command, and use double quotes so the shell expands the variable. The snippet below shows the correct call, a status-only call for debugging, and a verbose call that prints the request headers curl actually sent.

Postman. Open the request, go to the Authorization tab, choose API Key, set Key to X-API-Key, Value to a variable such as {{twitterapi_key}}, and Add to: Header. Postman then sends the header on every request in that scope. Choosing Query Params instead would append the key to the URL, which is the option to avoid. Postman's Bearer Token type is different: it builds Authorization: Bearer <token>, which twitterapi.io does not read.

Two quoting mistakes we tested. Wrapping the value in literal quotes (X-API-Key: "abc") and adding a scheme word (X-API-Key: Bearer abc) both reach the server as a different string than your key. Both returned 401 in our probe. The header value is the key and nothing else.

bash
# 1) Correct: the bare key in the X-API-Key header
curl -s "https://api.twitterapi.io/twitter/user/info?userName=XDevelopers" \
  -H "X-API-Key: $TWITTERAPI_IO_API_KEY"

# 2) Print only the status code while you debug (200, 401 or 403)
curl -s -o /dev/null -w "%{http_code}\n" \
  "https://api.twitterapi.io/twitter/user/info?userName=XDevelopers" \
  -H "X-API-Key: $TWITTERAPI_IO_API_KEY"

# 3) Show exactly which headers curl sends (the key is printed, so never paste this output)
curl -sv -o /dev/null "https://api.twitterapi.io/twitter/user/info?userName=XDevelopers" \
  -H "X-API-Key: $TWITTERAPI_IO_API_KEY" 2>&1 | grep -i "^> "

# Wrong: these all reached the server as an invalid key (HTTP 401 in our probe)
#   -H "X-API-Key: \"$TWITTERAPI_IO_API_KEY\""      quotes inside the value
#   -H "X-API-Key: Bearer $TWITTERAPI_IO_API_KEY"   Bearer prefix inside the value
04 — Section

Python requests and Node fetch examples

Python requests. Pass a headers dict, or set it once on a Session so every call carries it: session.headers.update({"X-API-Key": key}). Read the key with os.environ[...] and call .strip(): a key read from a file or cat key.txt usually ends with a newline, and requests (2.32.5 checked) refuses header values that contain return characters or leading whitespace, raising InvalidHeader before anything is sent. The full end-to-end script at the bottom of this page does exactly this, follows next_cursor for paginated results, and turns 401 and 403 into readable messages.

The redirect detail most guides miss. In the requests source (Session.rebuild_auth in 2.32.5), a redirect to a different host deletes the Authorization header, and nothing else. A custom X-API-Key header set on the session travels on to the new host. Other clients have their own rules, so do not assume. For an authenticated API you call from a server, pass allow_redirects=False (requests) or redirect: "manual" (fetch) and treat any 3xx as an error.

Node.js. Node 18 and later ship fetch, so no library is needed. Put the key in headers, set redirect: "manual", and check res.status before parsing. twitterapi.io returns HTTP 200 with status: "error" and a msg for some request-level problems (an unknown user, for example), so check the body too.

javascript
// Node 18+ (built-in fetch). Run: TWITTERAPI_IO_API_KEY=... node user.mjs
const BASE = "https://api.twitterapi.io";
const key = (process.env.TWITTERAPI_IO_API_KEY ?? "").trim();
if (!key) throw new Error("set TWITTERAPI_IO_API_KEY");

const res = await fetch(`${BASE}/twitter/user/info?userName=XDevelopers`, {
  headers: { "X-API-Key": key },
  redirect: "manual", // do not carry the key to another origin
});
if (res.status === 403) throw new Error("403: header missing or stripped on the way");
if (res.status === 401) throw new Error("401: key wrong or reset");
const body = await res.json();
if (body.status === "error") throw new Error(body.msg);
console.log(body.data.userName, body.data.followers);
05 — Section

401 vs 403: what each status means and the usual causes

The two status codes mean different things, and providers do not agree on which one a missing key gets. OWASP's REST Security Cheat Sheet describes 401 as wrong or no authentication and 403 as authenticated but not permitted. RFC 6750 maps an invalid bearer token to 401 and insufficient scope to 403. AWS API Gateway returns 403 when a required usage-plan key is missing or does not match. twitterapi.io's docs list 403 for a request with no x-api-key header and 401 for a key that is wrong or has been reset. Write your client to handle both.

We checked the 401 side first-hand. Using our rate-limited study client with deliberately invalid keys (the real key was never read), three requests to /twitter/user/info:

What the header carriedHTTP status
random string of the right shape401
value wrapped in double quotes401
value prefixed with 'Bearer '401
no header at all (documented, not probed)403

Wrong header name. Authorization: <key>, Api-Key, X-API-Token or apikey are all different headers. If the provider documents X-API-Key, a key in any other header is simply absent, which on twitterapi.io means 403. Spelling case is not the problem (names are case-insensitive), the words are.

Value decorated. Quotes, a Bearer prefix, a trailing newline or a stray space inside the value all change the string. In requests a trailing newline fails locally with InvalidHeader; in other clients it can reach the server and fail as 401.

Key reset or from another account. After you reset a key in a dashboard the old value returns 401 at once. Old values often survive in CI secrets, .env files on a second machine, or a teammate's shell.

Header dropped on the way. A corporate proxy, an API gateway in front of your service, or a CDN rule can strip unfamiliar headers. RFC 9110 says a proxy MUST forward unrecognized fields unless they are listed in Connection, but custom middleware and allow-lists do not always follow that. If your code sends the header and the server answers as if it were missing (403 on twitterapi.io), print the request headers at the last hop you control.

Calling the wrong host. A key for one provider sent to another is just an unknown string. The official X API at api.x.com ignores X-API-Key entirely and wants OAuth.

06 — Section

What developers post about the x-api-key header: a sample of real posts

To see who actually types this phrase and what they are wiring it into, we pulled every recent public post on X that contains "x-api-key" through our own search endpoint: 380 posts covering 49.3 weeks, about 7.7 a week, after which the search ran out of results. In practice the search matched the words rather than the hyphenated name, so the sample mixes two meanings.

What the post means by the phrasePostsShare
the header name (x-api-key, X-API-Key, x_api_key)19651.6%
a key for X's own API ("X API key")11630.5%
neither (loose word match)6817.9%

The header is now mostly an AI-tooling topic. Of the 196 posts that spell the header name, 57.7% sit in an AI, LLM, agent or MCP context, 26.5% mention Anthropic or Claude (whose API historically used x-api-key), and 15.3% mention MCP servers. 29.1% also name Bearer or the Authorization header in the same post, usually because the service accepts both.

Few posts are about the error. Only 5.6% mention a 401 or 403 and 5.6% mention query-string keys; 16.3% show or mention curl, 9.2% JavaScript or Node, 7.1% Python. People announce what header an API takes far more often than they complain about it failing, which is why the error causes above come from the docs and our probe rather than from posts.

Spelling. 52.0% of header posts write x-api-key in lowercase and 35.2% write X-API-Key; X-Api-Key (8.2%) and X-API-KEY (3.6%) trail. All of them are the same header on the wire.

Keys in public. 3 of the 196 header posts (1.5%) appear to contain a literal key value right after the header name, in a pasted code sample, rather than a placeholder. We did not test them, store them or record which posts they were. It is a small share, and it is the reason the next section exists.

Horizontal bar chart of themes in 196 public posts that spell the x-api-key header name: AI, LLM or agent context 57.7%, also mentions Bearer or Authorization 29.1%, mentions Anthropic or Claude 26.5%, shows or mentions curl 16.3%, mentions MCP 15.3%, leaks, redirects, rotation or hashing 13.8%, JavaScript or Node 9.2%, Python 7.1%, query-string keys 5.6%, 401 or 403 5.6%.
What 196 public posts that spell the header name talk about. A post can match several themes. Source: twitterapi.io /twitter/tweet/advanced_search, 380 posts, collected in 2026.
07 — Section

Keeping an X-API-Key safe: storage, rotation and the server side

A static API key is a password that never expires on its own. The OWASP cheat sheet's advice fits in one line: require the key on every request, and do not rely on API keys alone to protect sensitive or high-value resources.

Keep it server-side. Never ship the key in browser JavaScript or a mobile app bundle. Anything the client can read, a user can read from the network tab,. If a front end needs the data, call your own backend, and let the backend add the header.

Keep it out of code and URLs. Load it from an environment variable or a secrets manager, add .env to .gitignore, and never build a URL that contains it. Mask it in logs: if you log request headers, redact X-API-Key the same way you redact Authorization, because generic log scrubbers often only know the standard one.

Rotate on any doubt. If a key appears in a commit, a screenshot, a pasted snippet or a support ticket, reset it in the provider's dashboard right away and redeploy the new value; the old one starts returning 401. On twitterapi.io the key lives in the dashboard, and a reset key fails with 401 as documented.

If you are the one issuing keys. Show each key once, store only a hash, look it up by a short non-secret prefix, compare with a constant-time function, and decide your 401 vs 403 contract up front and document it. The snippet below is a minimal Flask version.

python
# A minimal X-API-Key check for your own Flask API (pip install flask)
import hashlib
import hmac
import os

from flask import Flask, abort, request

app = Flask(__name__)
# Store only a hash of each issued key, never the key itself.
KEY_HASHES = {os.environ["CLIENT_KEY_SHA256"]}


def key_ok(raw):
    digest = hashlib.sha256(raw.encode()).hexdigest()
    return any(hmac.compare_digest(digest, h) for h in KEY_HASHES)


@app.before_request
def require_api_key():
    raw = request.headers.get("X-API-Key")  # lookup is case-insensitive
    if raw is None:
        abort(401)  # many APIs answer 401 here; twitterapi.io answers 403
    if not key_ok(raw.strip()):
        abort(401)


@app.get("/ping")
def ping():
    return {"ok": True}
08 — Section

Worked example: authenticating to twitterapi.io with X-API-Key

twitterapi.io is an API for X (Twitter) data: tweets, profiles, followers, searches and lists. Reads need no X account and no OAuth: sign up, copy the key from the dashboard, and send it as X-API-Key to https://api.twitterapi.io. New accounts that pass the automatic bot check get up to $1 of free credits (at most 100,000 credits, no card, one per person or network, expiring after 31 days); purchased credits never expire.

What a call costs. You pay only for the data returned. Tweet endpoints such as /twitter/user/last_tweets and /twitter/tweet/advanced_search cost 15 credits per tweet returned, which is $0.15 per 1,000 tweets, with a minimum of 15 credits per request (an empty search costs $0.00015; a full 20-tweet page costs $0.003). Profiles from /twitter/user/info cost 18 credits per user, $0.18 per 1,000. $1 buys 100,000 credits.

Rate cardReading 1,000 postsReading 1,000 profiles
twitterapi.io$0.15$0.18
Official X API pay-per-use (docs.x.com pricing)$5.00 ($0.005 per post read)$10.00 ($0.010 per user read)

That is about 33x less per 1,000 posts read than the official X API pay-per-use rate. The official API also authenticates differently: OAuth 1.0a or an OAuth 2.0 Bearer token, never X-API-Key.

Rate limits follow the account, not the header. Before the first payment a key is limited to 0.2 requests per second; once paid it is 20, 50 on the Scale plan, 500 on Business, and up to 1,000 on request. A burst above your limit is a rate-limit response, not an authentication error; see the related guide on rate-limit errors for backoff. Plans and the full rate card are on the pricing page.

The study on this page used the same header: 20 search requests that returned 380 posts (about $0.06 at 15 credits per tweet), plus the 3 invalid-key probes.

09 — Section

How we collected this

Question. What do people mean and discuss when they post the phrase "x-api-key", and what status does api.twitterapi.io return for common wrong header values?

Query. /twitter/tweet/advanced_search with query="x-api-key" and queryType=Latest, newest first, following next_cursor until has_next_page was false: 20 requests, 380 posts, about 49.3 weeks of posts, collected in 2026. Separately, 3 requests to /twitter/user/info with deliberately invalid dummy keys through the same rate-limited client.

Method. Each post was classified by case-insensitive regular expressions (the exact patterns are in the study's results file): whether it spells the header name or means a key for X's API, its themes, and how it spells the header. A suspected literal key is 20 or more key-like characters directly after x-api-key: or x-api-key= that are not an obvious placeholder; those values were never stored or printed.

What the sample cannot show. It covers only posts that X search returns for the phrase, so it over-represents announcements and tutorials and under-represents private debugging. Regex themes miss paraphrases and can over-match. The probe covers three invalid values on one endpoint; we did not probe the missing-header case, which is the documented 403, and we did not record response bodies.

Privacy. The authors are private individuals, so we report aggregates only: no handles, texts, links or post IDs. Raw pages were kept out of the repository; the committed sample holds de-identified rows (theme flags, language, week index, counts).

python
import os
import sys
import requests

BASE = "https://api.twitterapi.io"
# Read the key from the environment; strip() removes the trailing newline an
# env file or `cat key.txt` often adds (requests rejects header values with one).
API_KEY = os.environ.get("TWITTERAPI_IO_API_KEY", "").strip()
if not API_KEY:
    sys.exit("set TWITTERAPI_IO_API_KEY first")

session = requests.Session()
# Header name is case-insensitive on the wire; the value is the bare key:
# no quotes, no "Bearer " prefix.
session.headers.update({"X-API-Key": API_KEY})


def call(url, params):
    r = session.get(url, params=params, timeout=30, allow_redirects=False)
    if r.status_code == 403:
        sys.exit("403: no X-API-Key header reached the server (check proxies and the header name)")
    if r.status_code == 401:
        sys.exit("401: the key was sent but is wrong or has been reset (re-copy it from the dashboard)")
    if 300 <= r.status_code < 400:
        # Never follow a redirect blindly with a custom auth header attached.
        sys.exit(f"unexpected redirect to {r.headers.get('Location')}")
    r.raise_for_status()
    return r.json()


# 1) One profile: 18 credits per user returned.
info = call(f"{BASE}/twitter/user/info", {"userName": "XDevelopers"})
if info.get("status") == "error":
    sys.exit(info.get("msg"))
user = info["data"]
print(user["userName"], user["followers"], "followers", user["statusesCount"], "posts")

# 2) Recent posts, following the cursor: 15 credits per tweet returned.
params = {"userName": "XDevelopers"}
seen = 0
for _ in range(3):  # at most 3 pages of 20
    page = call(f"{BASE}/twitter/user/last_tweets", params)
    tweets = page["data"]["tweets"]
    for tw in tweets:
        seen += 1
        print(tw["createdAt"], tw["likeCount"], tw["text"][:80].replace("\n", " "))
    if not page.get("has_next_page") or not page.get("next_cursor"):
        break
    params["cursor"] = page["next_cursor"]
print(seen, "posts read")
10 — Questions

Questions readers ask

What is the x-api-key header?

A custom HTTP request header that carries an API key as its bare value, X-API-Key: <key>, on every request. It is a widely used convention (AWS API Gateway usage plans, Anthropic's legacy fallback, twitterapi.io and many SaaS APIs), not a standard defined by an RFC.

Is x-api-key case-sensitive?

The header name is not: RFC 9110 says HTTP field names are case-insensitive, so x-api-key, X-API-Key and X-Api-Key are the same header. The key value is case-sensitive and must be sent exactly as issued.

What is the difference between X-API-Key and Authorization: Bearer?

Authorization: Bearer <token> is the standard scheme from RFC 6750, usually for OAuth 2.0 tokens, with a defined 401 plus WWW-Authenticate error contract and default handling as a secret by most tools. X-API-Key is a provider convention for a static key, with provider-specific errors. Send whichever the API documents; twitterapi.io reads X-API-Key.

How do I send an API key in a header with curl?

Use -H: curl "https://api.twitterapi.io/twitter/user/info?userName=XDevelopers" -H "X-API-Key: $TWITTERAPI_IO_API_KEY". Keep the key in an environment variable, and do not wrap the value in extra quotes or add Bearer.

How do I send x-api-key with Python requests?

Pass it in headers: requests.get(url, headers={"X-API-Key": key}, timeout=30), or set it once with session.headers.update(...). Call .strip() on a key read from a file; requests rejects header values with a trailing newline. Use allow_redirects=False, because requests strips Authorization, not X-API-Key, on a cross-host redirect.

Why do I get 401 or 403 with x-api-key?

On twitterapi.io, 403 means no x-api-key header arrived (wrong header name, or a proxy dropped it) and 401 means the key arrived but is wrong or has been reset. In our probe a quoted value and a Bearer -prefixed value also returned 401. Other providers map these differently; AWS API Gateway returns 403 for a missing or unmatched key.

Should I put the API key in the URL instead?

No, unless the API offers nothing else. RFC 6750 says the query-parameter method SHOULD NOT be used unless the header is impossible, and OWASP warns that URLs are captured in server logs. URLs also end up in browser history and proxy logs.

Does the official X (Twitter) API use an x-api-key header?

No. X authenticates with OAuth 1.0a, an OAuth 2.0 app-only Bearer token, or OAuth 2.0 with PKCE. twitterapi.io is a separate service that uses the X-API-Key header and needs no X developer account for reads.

11 — Further reading

Continue

Sources & further reading
More from this series
Build it

Stop reading. Start building.

Starter credits cover real testing on real data. Google sign-in, no card, no application queue.

Get an API key