How to Authenticate with APIs

Real code examples for every auth type, plus safe practices for payment and order APIs.

← Learn

In this guide

  1. Using API Key authentication
  2. Using Bearer token authentication
  3. Using OAuth 2.0
  4. Using Basic authentication
  5. Safe order and payment processing
  6. Handling auth errors gracefully

Using API Key authentication

API key auth is the simplest to implement. You include your key in every request, either as a header or a query parameter. Most APIs prefer headers — they don't appear in server logs or browser history.

In a request header (recommended)

JavaScript (fetch)
const response = await fetch('https://api.example.com/data', {
  headers: {
    'X-API-Key': process.env.MY_API_KEY, // never hardcode
    'Accept': 'application/json'
  }
});
const data = await response.json();
Python (requests)
import requests, os

response = requests.get(
  'https://api.example.com/data',
  headers={'X-API-Key': os.environ['MY_API_KEY']}
)
data = response.json()

Always read the API's documentation to find the exact header name. Common names include X-API-Key, Authorization, api-key, and x-rapidapi-key.

As a query parameter

JavaScript (fetch)
// Only use query params if the API requires it
const url = new URL('https://api.example.com/data');
url.searchParams.set('api_key', process.env.MY_API_KEY);
const response = await fetch(url.toString());

Using Bearer token authentication

Bearer token auth involves two steps: first exchange your credentials for a token, then use that token in subsequent requests. Tokens expire, so your code needs to handle renewal.

Step 1 — Get a token

JavaScript
async function getToken() {
  const response = await fetch('https://auth.example.com/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'client_credentials',
      client_id: process.env.CLIENT_ID,
      client_secret: process.env.CLIENT_SECRET
    })
  });
  const { access_token, expires_in } = await response.json();
  return access_token;
}

Step 2 — Use the token

JavaScript
const token = await getToken();

const response = await fetch('https://api.example.com/data', {
  headers: {
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json'
  }
});
Cache your token: Fetching a new token for every API call is wasteful and will get you rate-limited on auth endpoints. Cache the token in memory and only refresh it when it's within 60 seconds of expiry. Store the expiry time (Date.now() + expires_in * 1000) alongside the token.

Using OAuth 2.0

OAuth is used when your app needs to access data owned by a user on another service — for example, reading someone's Google Calendar or posting on their behalf to Twitter. The user grants permission explicitly; your app never sees their password.

The authorisation code flow (the most common for web apps) has four steps:

1. Redirect the user
Send the user to the provider's authorisation URL with your client_id, requested scope, and a redirect_uri where they'll return after approving.
2. Receive the code
After the user approves, the provider redirects back to your redirect_uri with a short-lived authorisation code in the URL query string.
3. Exchange for tokens
Your server exchanges the code for an access_token (and usually a refresh_token) by making a POST to the provider's token endpoint. This happens server-side.
4. Make API calls
Use the access_token as a Bearer token in your requests. When it expires, use the refresh_token to get a new one without re-prompting the user.
Validate the state parameter: When you build the authorisation URL, include a random state value. When the user returns to your redirect URI, confirm the state matches what you sent. This prevents CSRF attacks where a malicious site tricks a user into linking your app to the attacker's account.

In practice, most developers use an OAuth library rather than implementing the flow manually. Libraries like Passport.js (Node.js), Authlib (Python), or The League OAuth2 client (PHP) handle the state, token storage, and refresh logic for you.

Using Basic authentication

Basic auth sends a Base64-encoded combination of your username and password in the Authorization header. It's simple but must only be used over HTTPS — the encoding is not encryption.

JavaScript
const credentials = btoa(`${process.env.API_USER}:${process.env.API_PASS}`);

const response = await fetch('https://api.example.com/data', {
  headers: {
    'Authorization': `Basic ${credentials}`
  }
});
Python
import requests, os

response = requests.get(
  'https://api.example.com/data',
  auth=(os.environ['API_USER'], os.environ['API_PASS'])
)

Safe order and payment processing

Payment APIs handle real money. A bug that causes a double-charge or a missed payment confirmation can have serious consequences. These patterns protect you and your users.

Use idempotency keys

An idempotency key is a unique value you generate and send with a write request (POST/PUT). If the request fails and you retry, sending the same key tells the server "I already tried this — don't create a duplicate." Stripe, PayPal, and most serious payment APIs support this.

JavaScript — Stripe example
const { v4: uuidv4 } = require('uuid');

// Generate once and store with the order — reuse on retry
const idempotencyKey = uuidv4(); // e.g. "550e8400-e29b-41d4-a716-446655440000"

const charge = await stripe.paymentIntents.create({
  amount: 2000, // $20.00 in cents
  currency: 'usd',
  payment_method: paymentMethodId,
  confirm: true,
}, {
  idempotencyKey // same key = same result, no duplicate charge
});
Rule: Generate the idempotency key when the user initiates the action. Store it alongside the pending order in your database. On every retry, retrieve and reuse the same key — never generate a new one for the same order.

Never trust the client for order amounts

Always calculate the price on your server, not in your frontend. If your checkout page sends the price to your server, a malicious user can intercept and modify that value before it arrives. Your server should calculate the amount from the order items it fetches from the database.

Never do this: accept a price or amount field from the browser in a payment request. Fetch the product price from your database on the server and compute the total there.

Verify webhooks before acting on them

Payment APIs send webhooks (server-to-server notifications) to tell you when a payment succeeds or fails. Anyone can send a fake POST to your webhook endpoint. Always verify the signature the provider includes before processing the event.

JavaScript — Stripe webhook verification
// Express.js — use raw body, not parsed JSON
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['stripe-signature'];
  let event;
  try {
    event = stripe.webhooks.constructEvent(req.body, sig, process.env.WEBHOOK_SECRET);
  } catch (err) {
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }
  // Safe to process — signature verified
  if (event.type === 'payment_intent.succeeded') {
    fulfillOrder(event.data.object);
  }
  res.json({ received: true });
});

Handle network failures with safe retries

Network requests can fail silently — a timeout doesn't mean the payment didn't process. Safe retry logic checks the order status before retrying a charge, and always uses idempotency keys.

Handling auth errors gracefully

Authentication errors are among the most common API failures. Here's how to handle each one correctly:

401 Unauthorized
Your credentials are missing or invalid. For Bearer tokens, the token may have expired — try refreshing it. For API keys, check for typos or confirm the key is active in the provider's dashboard.
403 Forbidden
You're authenticated but lack permission. Check that your API key or OAuth scope includes access to this endpoint. You may need to request additional permissions from the provider.
429 Rate Limited
You've sent too many requests. Respect the Retry-After header if present. Implement request queuing or exponential backoff. Consider caching responses to reduce call volume.
Token expired
For OAuth and Bearer tokens, check the expires_in value when you receive a token. Proactively refresh 60 seconds before expiry rather than waiting for a 401 response.
JavaScript — robust auth error handling
async function apiRequest(url, options = {}, retries = 2) {
  const response = await fetch(url, {
    ...options,
    headers: { 'Authorization': `Bearer ${await getToken()}`, ...options.headers }
  });

  if (response.status === 401 && retries > 0) {
    clearTokenCache(); // force a fresh token
    return apiRequest(url, options, retries - 1);
  }
  if (response.status === 429) {
    const wait = parseInt(response.headers.get('Retry-After') || '5') * 1000;
    await new Promise(r => setTimeout(r, wait));
    return apiRequest(url, options, retries - 1);
  }
  if (!response.ok) throw new Error(`API error: ${response.status}`);
  return response.json();
}

Continue learning

What is an API key?
Auth types explained
What is REST?
HTTP methods and endpoints
What is JSON?
Reading API responses