In this guide
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)
headers: {
'X-API-Key': process.env.MY_API_KEY, // never hardcode
'Accept': 'application/json'
}
});
const data = await response.json();
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
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
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
const response = await fetch('https://api.example.com/data', {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
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:
client_id, requested scope, and a redirect_uri where they'll return after approving.redirect_uri with a short-lived authorisation code in the URL query string.access_token (and usually a refresh_token) by making a POST to the provider's token endpoint. This happens server-side.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.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.
const response = await fetch('https://api.example.com/data', {
headers: {
'Authorization': `Basic ${credentials}`
}
});
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.
// 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
});
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.
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.
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.
- Generate an idempotency key before the first attempt and reuse it on all retries
- On a timeout or 5xx error, look up the payment status before retrying — it may have succeeded
- Implement exponential backoff: wait 1s, then 2s, then 4s between retries
- Cap retries at 3–5 attempts, then surface an error to the user rather than retrying indefinitely
- Never retry a 4xx error — those indicate a problem with your request that won't resolve itself
- Never generate a new idempotency key for a retry — that defeats the purpose
Handling auth errors gracefully
Authentication errors are among the most common API failures. Here's how to handle each one correctly:
Retry-After header if present. Implement request queuing or exponential backoff. Consider caching responses to reduce call volume.expires_in value when you receive a token. Proactively refresh 60 seconds before expiry rather than waiting for a 401 response.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();
}