Skip to content
guide

API key vs JWT vs OAuth2: pick the right auth for your API

| 8 min read
Lock and key on a circuit board representing API security
Photo by FLY:D on Unsplash

You're building an API. The endpoints work. The data flows. Now you need to answer one question before shipping: how do callers prove who they are?

Three approaches dominate API authentication: API keys, JWTs, and OAuth2. Each solves a different problem. Pick the wrong one and you'll either over-engineer a simple integration or leave a security gap in a complex one.

This guide compares all three across seven criteria, with working code examples, a decision table, and clear recommendations based on your API's use case.

API key authentication: the direct approach

An API key is a long random string that identifies and authorizes the caller. The client sends it with every request, the server looks it up, and if it matches a valid key, the request goes through.

Here's what an API key call looks like with the botoi API:

# API key in a custom header
curl -s -X POST https://api.botoi.com/v1/dns/lookup \
  -H "Content-Type: application/json" \
  -H "x-api-key: your_api_key_here" \
  -d '{"domain": "example.com", "type": "A"}'

Response:

{
  "success": true,
  "data": {
    "domain": "example.com",
    "type": "A",
    "records": [
      { "type": "A", "value": "93.184.216.34", "ttl": 86400 }
    ]
  }
}

The x-api-key header carries the credential. No tokens to negotiate, no redirects, no authorization servers. One header, one lookup, one response.

When API keys win

  • Server-to-server calls. Your backend calls another backend. No user is involved. A cron job queries an IP geolocation API. A CI pipeline runs DNS checks. The caller is always a trusted server.
  • Utility APIs. APIs that perform stateless operations (hashing, encoding, validation, lookups) where every request is independent. botoi uses API keys for this reason: 150+ endpoints, all stateless, all server-to-server.
  • Fast integration. A developer copies the key, adds one header, and starts calling. No OAuth dance, no token refresh logic, no JWKS endpoint to configure.

Here's the same call in Node.js:

const response = await fetch("https://api.botoi.com/v1/hash/sha256", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "x-api-key": process.env.BOTOI_API_KEY,
  },
  body: JSON.stringify({ input: "hello world" }),
});

const result = await response.json();
// result.data.hash = "b94d27b9934d3e08..."

Where API keys fall short

  • No user identity. An API key identifies the account, not the user. If three developers share one key, you can't tell who made which request.
  • Revocation requires a round-trip. Revoking a key means updating the server's key store. Until the cache refreshes, the old key still works.
  • No delegated access. You can't give a third-party app limited, temporary access to a user's resources with an API key alone.

JWT authentication: stateless user sessions

A JSON Web Token (JWT) is a signed JSON object that carries claims about the caller. The auth server creates it at login; the client sends it with every request; the resource server verifies the signature without calling the auth server again.

// Header
{
  "alg": "RS256",
  "typ": "JWT"
}

// Payload
{
  "sub": "user_12345",
  "email": "dev@example.com",
  "role": "admin",
  "iat": 1775000000,
  "exp": 1775000900   // 15 minutes
}

// Signature
RSASHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  privateKey
)

The server verifies the signature with the public key. If the signature checks out and exp hasn't passed, the request is authorized. No database lookup needed.

When JWTs win

  • User-facing APIs with high traffic. A mobile app sends a JWT on every request. The API gateway verifies the signature locally instead of querying a session store on every call. At 10,000 requests per second, that database call you skipped matters.
  • Microservice architectures. Service A calls Service B with a JWT. Service B validates it locally and extracts the user's ID and roles from the claims. No shared session database between services.
  • Short-lived authorization. A 15-minute token for a file upload. A 5-minute token for a payment confirmation. The expiry is baked into the token itself.

Here's Express middleware that verifies a JWT:

import jwt from "jsonwebtoken";

function authMiddleware(req, res, next) {
  const token = req.headers.authorization?.replace("Bearer ", "");
  if (!token) return res.status(401).json({ error: "Missing token" });

  try {
    const decoded = jwt.verify(token, process.env.JWT_PUBLIC_KEY, {
      algorithms: ["RS256"],
    });
    req.user = decoded;
    next();
  } catch (err) {
    return res.status(401).json({ error: "Invalid or expired token" });
  }
}

Where JWTs fall short

  • Token revocation is hard. A JWT is valid until it expires. If a user logs out or you need to revoke access, you need a server-side blocklist, which brings back the database call you were trying to avoid.
  • Payload size. Every claim adds bytes. A JWT with user roles, permissions, and metadata can reach 1-2 KB. That's 1-2 KB on every single request, in every header.
  • Key rotation complexity. When you rotate signing keys, old tokens signed with the previous key need to remain valid until they expire. This means serving multiple public keys via a JWKS endpoint and handling the kid header claim.

OAuth2: delegated access for third parties

OAuth2 is an authorization framework, not an authentication protocol. It lets a user grant a third-party application limited access to their resources on another service, without sharing their password.

The classic example: a user authorizes a project management tool to read their GitHub repositories. The user logs in to GitHub, approves specific scopes, and the tool receives an access token scoped to those permissions.

# Step 1: Redirect user to authorization server
GET https://auth.example.com/authorize?
  response_type=code&
  client_id=your_app_id&
  redirect_uri=https://yourapp.com/callback&
  scope=read:repos+write:issues&
  state=random_csrf_token

# Step 2: Exchange authorization code for tokens
POST https://auth.example.com/token
  grant_type=authorization_code&
  code=AUTH_CODE_FROM_CALLBACK&
  client_id=your_app_id&
  client_secret=your_app_secret&
  redirect_uri=https://yourapp.com/callback

# Step 3: Call the API with the access token
curl -H "Authorization: Bearer eyJhbGciOiJSUzI1NiJ9..." \
  https://api.example.com/v1/repos

When OAuth2 wins

  • Third-party integrations. You run a platform and want external developers to build apps that access your users' data. OAuth2 gives users control over what each app can access.
  • Fine-grained scopes. read:repos but not delete:repos. write:issues but not admin:org. OAuth2 scopes let users approve specific permissions.
  • "Sign in with X" flows. When your app uses Google, GitHub, or Microsoft for login, you're using OAuth2 (often with OpenID Connect on top) to get an identity token.

Where OAuth2 falls short

  • Complexity. OAuth2 has four grant types, refresh tokens, authorization servers, redirect URIs, PKCE, and token introspection endpoints. For a utility API with no user context, this is overhead with no benefit.
  • Integration friction. A developer who wants to call your hash endpoint does not want to register an OAuth app, set up redirect URIs, and exchange authorization codes. They want a key and a curl command.
  • Token management burden. Access tokens expire. Refresh tokens rotate. Clients need retry logic for 401 responses. For simple server-to-server calls, this is unnecessary machinery.

Comparison table

Criteria API key JWT OAuth2
Integration time Minutes Hours Days
User identity Account-level User-level (claims) User-level (scopes)
Stateless verification No (server lookup) Yes (signature check) Depends on token format
Revocation speed Immediate (delete key) Delayed (until expiry) Immediate (revoke token)
Delegated access No No Yes
Third-party support Poor Good Excellent
Suitable for browsers No (key exposure) Yes (short-lived) Yes (authorization code + PKCE)

Decision framework: pick based on your use case

Stop asking "which is most secure?" All three are secure when used correctly. The right question: "who is calling my API and why?"

  • Server calls server, no user involved: API key. Your backend calls a utility API for DNS lookups, hashing, or data validation. One key, one header, done.
  • Your own app authenticates your own users: JWT. Your mobile app or SPA sends requests on behalf of a logged-in user. Sign a short-lived JWT at login, verify it on every request without a session store.
  • Third-party apps access user data: OAuth2. External developers build integrations with your platform. Users control what each app can access through scoped consent screens.

Many production systems combine these approaches. GitHub uses OAuth2 for third-party apps and API keys (personal access tokens) for server-side scripts. Stripe uses API keys for direct integration and OAuth2 (Stripe Connect) for marketplace platforms.

Managing API keys at scale with Unkey

API keys sound simple until you need to hash them, enforce rate limits, set expiration dates, track usage per key, and rotate them without downtime. Building all of this from scratch takes weeks.

Unkey is an open source API key management service that handles creation, verification, rate limiting, and analytics. botoi uses Unkey to manage all API keys across its 150+ endpoints.

Create a scoped key with built-in rate limiting:

import { Unkey } from "@unkey/api";

const unkey = new Unkey({ rootKey: process.env.UNKEY_ROOT_KEY });

// Create a scoped API key with built-in rate limiting
const key = await unkey.keys.create({
  apiId: "api_your_api_id",
  prefix: "botoi",
  meta: { userId: "user_12345", plan: "pro" },
  expires: Date.now() + 90 * 24 * 60 * 60 * 1000, // 90 days
  ratelimit: {
    type: "fast",
    limit: 100,
    refillRate: 10,
    refillInterval: 1000,
  },
});

// key.result.key = "botoi_3xK9m2..."

Verify it in your middleware:

import { verifyKey } from "@unkey/api";

async function authMiddleware(req, res, next) {
  const apiKey = req.headers["x-api-key"];
  if (!apiKey) return res.status(401).json({ error: "Missing API key" });

  const result = await verifyKey(apiKey);

  if (!result.result.valid) {
    return res.status(result.result.code === "RATE_LIMITED" ? 429 : 403)
      .json({ error: result.result.code });
  }

  req.keyMeta = result.result.meta; // { userId, plan }
  next();
}

Unkey stores keys hashed (never in plain text), enforces rate limits at the edge, and gives you an analytics dashboard showing usage per key. When a key needs rotation, create a new one and set an expiry on the old one. No downtime, no code changes.

Security checklist for each approach

API keys

  • Send over HTTPS only. Never embed keys in URLs or query strings.
  • Store hashed on the server. Never log raw keys.
  • Scope keys to specific permissions (read-only, write, admin).
  • Set expiration dates. Force rotation every 90 days.
  • Use a custom header (x-api-key) instead of Authorization to avoid browser credential caching.

JWTs

  • Use asymmetric signing (RS256 or ES256). Never use HS256 with a shared secret in distributed systems.
  • Keep token lifetime short: 5-15 minutes for access tokens.
  • Validate iss, aud, and exp claims on every request.
  • Publish public keys via a JWKS endpoint. Rotate signing keys on a schedule.
  • Never store sensitive data in the payload. JWTs are encoded, not encrypted.

OAuth2

  • Use the Authorization Code flow with PKCE for all clients, including SPAs and mobile apps.
  • Never use the Implicit flow. It's deprecated in OAuth 2.1 for good reason.
  • Store client secrets on the server only. Never ship them in mobile apps or frontend code.
  • Validate state parameters to prevent CSRF attacks on the callback URL.
  • Use short-lived access tokens with refresh token rotation.

Key points

  • API keys are the right choice for server-to-server utility APIs. They're fast to integrate, easy to manage, and sufficient when no user identity is needed. botoi uses them across 150+ endpoints with Unkey for management.
  • JWTs are the right choice for stateless user sessions. They eliminate session store lookups at high scale, but token revocation needs extra infrastructure.
  • OAuth2 is the right choice when third-party apps need scoped access to user resources. The complexity is justified by the security model it provides.
  • Pick based on the caller, not the hype. A utility API with OAuth2 is over-engineered. A platform API with only API keys can't grant delegated access.
  • Combine approaches when your product demands it. API keys for direct integrations, OAuth2 for marketplace partners, JWTs for logged-in user sessions.

Frequently asked questions

When should I use an API key instead of OAuth2?
Use an API key when the caller is a server, not a user. API keys work well for server-to-server integrations, CI/CD pipelines, and utility APIs where every request comes from a trusted backend. OAuth2 adds unnecessary complexity when no end-user consent or delegated access is involved.
Can I use JWT and OAuth2 together?
Yes. OAuth2 defines the authorization flow; JWT defines the token format. Many OAuth2 providers issue JWTs as access tokens. The JWT carries claims (user ID, scopes, expiry) that resource servers verify without calling the auth server on every request.
Are API keys secure enough for production?
API keys are secure when you treat them like passwords. Send them over HTTPS only, store them hashed on the server, scope them to specific permissions, set expiration dates, and rotate them on a schedule. Services like Unkey handle hashing, rate limiting, and expiry for you.
How do I revoke a JWT before it expires?
JWTs are stateless, so revocation requires extra infrastructure. Common approaches include a server-side blocklist checked on each request, short-lived tokens (5-15 minutes) paired with refresh tokens, or a token version claim validated against a database. Each approach adds a server-side check that partly negates the stateless benefit.
What is the difference between authentication and authorization?
Authentication verifies identity: "who are you?" Authorization determines access: "what can you do?" API keys often combine both into one credential. OAuth2 separates them by design, letting a user (authentication) grant limited permissions (authorization) to a third-party app without sharing their password.

More guide posts

Start building with botoi

150+ API endpoints for lookup, text processing, image generation, and developer utilities. Free tier, no credit card.