How to Solve Axios 403 Forbidden Error

Running into a 403 Forbidden error while working with Axios? You’re not alone. It’s one of those frustrating issues where your request looks fine—but the server slams the door shut anyway.

I’ve been on that side of the screen, staring at the console and wondering what’s going wrong. After helping countless developers troubleshoot this exact issue, I’ve learned that 403 errors almost always come down to a few specific causes.

This guide walks you through a practical, step-by-step process to figure out what’s triggering that 403 and, more importantly, how to fix it—so you can get back to building, not debugging.

By the time you reach the end, you’ll know how to pinpoint the problem and apply the exact solution your situation needs.

Why You Can Trust This Guide

Let’s be real: “403 Forbidden” is one of those errors that tends to send you down a rabbit hole of vague Stack Overflow threads. And often, you walk away more confused than when you started.

This guide is different. It’s built from real-world debugging experience across all kinds of APIs—from straightforward REST endpoints to complex OAuth2 workflows. It’s not theory. It’s the process I’ve used time and again to get things working.

You’re not just getting a list of possible fixes—you’re getting a clear path to a solution.

Step 1: Understand What a 403 Error Actually Means

Before you start changing code, it helps to understand what this error is actually saying.

A 403 doesn’t mean the server can’t identify you (that’s a 401). It means the server knows who you are but doesn’t think you’re allowed to access the resource. That could be due to missing permissions, bad headers, or other security rules.

axios.get('https://api.example.com/data')
  .then(response => console.log(response))
  .catch(error => {
    if (error.response && error.response.status === 403) {
      console.error('403 Forbidden:', error.response.data);
      // Error details will be here
    }
  });

Common culprits include:

  • Missing or invalid authentication tokens
  • Permissions issues on the resource
  • CORS policies blocking your request
  • Rate limiting or IP bans
  • Required headers that are missing
  • Unexpected request formats

Pro tip: Always check error.response.data. API error responses often give you helpful context about why the request was blocked.

Step 2: Check Your Authentication Headers

This is the first place to look—because 403s often boil down to something simple: your credentials weren’t sent the right way.

Here’s what to keep in mind for common auth methods:

Bearer Token Authentication

Make sure you’re including the Authorization header exactly how the API expects it. Even a missing space or incorrect casing can cause issues.

// Correct way
const config = {
  headers: {
    'Authorization': `Bearer ${yourToken}` // Note the space after "Bearer"
  }
};

axios.get('https://api.example.com/data', config)
  .then(response => console.log(response.data));

// Common mistakes:
// 'Authorization': 'Bearer' + token  // Missing space
// 'Authorization': token              // Missing "Bearer" prefix
// 'authorization': `Bearer ${token}`  // Wrong case (some APIs are case-sensitive)

API Key Authentication

Depending on the API, the key might go in a header, as a query parameter, or under a custom header. Always refer to the API’s docs and double-check you’re following their pattern.

// In headers
const config = {
  headers: {
    'X-API-Key': yourApiKey,
    // or 'Api-Key': yourApiKey
    // or 'Authorization': `ApiKey ${yourApiKey}`
  }
};

// In query parameters
axios.get(`https://api.example.com/data?api_key=${yourApiKey}`);

// As a custom header (check API documentation)
const config = {
  headers: {
    'X-Custom-Auth': yourApiKey
  }
};

Basic Authentication

For APIs using username/password combos, Axios can handle that natively, or you can manually set the Authorization header by base64-encoding your credentials.

// Using auth property
const config = {
  auth: {
    username: 'yourUsername',
    password: 'yourPassword'
  }
};

// Or manually encoding
const token = Buffer.from(`${username}:${password}`).toString('base64');
const config = {
  headers: {
    'Authorization': `Basic ${token}`
  }
};

Step 3: Verify CORS Configuration

If you're working in the browser, CORS can be the hidden culprit behind a 403. It’s a security measure that restricts how different origins can communicate.

Start by checking the browser console. If you see errors mentioning CORS or missing headers like Access-Control-Allow-Origin, that’s your signal.

// 1. Include credentials if required
const config = {
  withCredentials: true,
  headers: {
    'Authorization': `Bearer ${token}`
  }
};

// 2. Ensure proper headers for preflight requests
const config = {
  headers: {
    'Content-Type': 'application/json',
    'Accept': 'application/json',
    'Authorization': `Bearer ${token}`
  }
};

// 3. Use a proxy in development
// In package.json (for React apps):
// "proxy": "https://api.example.com"

// Or configure Axios baseURL
const api = axios.create({
  baseURL: process.env.NODE_ENV === 'development' 
    ? '/api'  // Proxied URL
    : 'https://api.example.com'
});

To address it:

  • Add withCredentials: true if cookies or auth headers are involved
  • Make sure your headers are properly configured for preflight
  • Use a proxy during development to bypass CORS entirely

Reminder: CORS issues won’t show up in tools like Postman—only in the browser.

Step 4: Inspect Request Headers and Cookies

Sometimes, it’s not what you’re sending—it’s what you’re not sending.

Use Axios interceptors to log your requests and responses. That way, you can verify what headers are being sent, what cookies are included, and whether something essential is missing.

Things to check:

  • Headers like User-Agent or Referer—some APIs block requests that don’t include them
  • Cookies—cross-origin requests need withCredentials: true to include them
  • Content-Type—some APIs reject requests if it’s not set properly

Debug your request configuration:

// Create an Axios instance with interceptors for debugging
const api = axios.create({
  baseURL: 'https://api.example.com'
});

// Log all requests
api.interceptors.request.use(
  config => {
    console.log('Request:', config);
    return config;
  },
  error => {
    console.error('Request error:', error);
    return Promise.reject(error);
  }
);

// Log all responses
api.interceptors.response.use(
  response => response,
  error => {
    if (error.response) {
      console.error('Response error:', {
        status: error.response.status,
        data: error.response.data,
        headers: error.response.headers
      });
    }
    return Promise.reject(error);
  }
);

Check required headers:

// Some APIs require specific headers
const config = {
  headers: {
    'Authorization': `Bearer ${token}`,
    'Content-Type': 'application/json',
    'Accept': 'application/json',
    'User-Agent': 'YourApp/1.0',  // Some APIs require this
    'Referer': 'https://yourapp.com',  // Some APIs check referer
  }
};

Handle cookies properly:

// Ensure cookies are sent with requests
const config = {
  withCredentials: true  // This sends cookies with cross-origin requests
};

// For same-origin requests, cookies are sent automatically

Step 5: Review API Permissions and Rate Limits

Got your authentication sorted and still seeing a 403? It could be about access rights or usage limits.

Start small: try calling a basic endpoint that your token should definitely have access to. If that works, the issue likely lies with scopes or endpoint-specific permissions.

// Test with a simple endpoint first
axios.get('https://api.example.com/user/profile', config)
  .then(() => console.log('Basic auth works'))
  .catch(error => console.error('Auth issue:', error));

// Then test the problematic endpoint
axios.get('https://api.example.com/admin/users', config)
  .then(() => console.log('Permission granted'))
  .catch(error => {
    if (error.response.status === 403) {
      console.error('Permission denied - check API key scopes');
    }
  });

Also, consider whether you’ve hit a rate limit. Many APIs return a 403 when you’ve made too many requests in a short period. Look at the response headers for clues like Retry-After.

// Implement retry logic with exponential backoff
async function makeRequestWithRetry(url, config, maxRetries = 3) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      const response = await axios.get(url, config);
      return response;
    } catch (error) {
      if (error.response && error.response.status === 403) {
        // Check if it's rate limiting
        const retryAfter = error.response.headers['retry-after'];
        if (retryAfter) {
          console.log(`Rate limited. Waiting ${retryAfter} seconds...`);
          await new Promise(resolve => setTimeout(resolve, retryAfter * 1000));
          continue;
        }
      }
      
      if (i === maxRetries - 1) throw error;
      
      // Exponential backoff
      const delay = Math.pow(2, i) * 1000;
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}

Not sure? Add retry logic with exponential backoff so you can test this without spamming the server.

Step 6: Test with Different Tools

One of the fastest ways to isolate the issue is to try your request outside of Axios. Use tools like:

  • cURL: Run the exact request from the command line
  • Postman or Thunder Client: Great for copying/pasting headers and comparing results
  • Minimal Axios test case: Strip your code down to just the essentials to see if something in your setup is the problem
# Basic GET request
curl -X GET "https://api.example.com/data" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json"

# POST request with data
curl -X POST "https://api.example.com/data" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"key": "value"}'

If the request works in Postman but not Axios, start comparing every detail—headers, payload, encoding. You’d be surprised how often that reveals the issue.

Step 7: Implement the Right Solution

Once you’ve diagnosed the problem, it’s time to lock in your fix. Here are three solid implementation patterns to consider:

Solution 1: Use an Axios Instance with Defaults

Create an Axios client with sensible defaults and built-in interceptors. You’ll avoid duplicating headers and make sure every request is configured correctly.

// Create a configured Axios instance
const apiClient = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json',
    'Accept': 'application/json'
  }
});

// Add auth token dynamically
apiClient.interceptors.request.use(
  config => {
    const token = localStorage.getItem('authToken');
    if (token) {
      config.headers.Authorization = `Bearer ${token}`;
    }
    return config;
  },
  error => Promise.reject(error)
);

// Handle 403 errors globally
apiClient.interceptors.response.use(
  response => response,
  error => {
    if (error.response?.status === 403) {
      // Handle forbidden error
      console.error('Access forbidden:', error.response.data);
      // Redirect to login or show error message
      // window.location.href = '/login';
    }
    return Promise.reject(error);
  }
);

Add global error handling for 403s so users get a consistent experience when something goes wrong.

Solution 2: Adjust Based on Environment

Set up Axios to behave differently depending on whether you’re in development or production. In dev, maybe you proxy to avoid CORS. In prod, you use the real API URL. You can also conditionally include credentials or headers as needed.

// config/axios.js
const createApiClient = () => {
  const config = {
    baseURL: process.env.REACT_APP_API_URL || 'https://api.example.com',
    headers: {
      'Content-Type': 'application/json'
    }
  };

  // Add CORS credentials for browser environments
  if (typeof window !== 'undefined') {
    config.withCredentials = true;
  }

  const client = axios.create(config);

  // Add request/response interceptors
  client.interceptors.request.use(/* ... */);
  client.interceptors.response.use(/* ... */);

  return client;
};

export default createApiClient();

Solution 3: Centralize Error Handling

Build a shared error handler that interprets API responses. That way, you can show user-friendly messages when something goes wrong—and make your app easier to maintain.

// utils/apiErrorHandler.js
export const handleApiError = (error) => {
  if (!error.response) {
    return {
      message: 'Network error - please check your connection',
      type: 'network'
    };
  }

  const { status, data } = error.response;

  switch (status) {
    case 403:
      return {
        message: data.message || 'You don\'t have permission to access this resource',
        type: 'forbidden',
        details: data
      };
    case 401:
      return {
        message: 'Please log in to continue',
        type: 'unauthorized'
      };
    case 429:
      return {
        message: 'Too many requests - please try again later',
        type: 'rate_limit',
        retryAfter: error.response.headers['retry-after']
      };
    default:
      return {
        message: data.message || 'An error occurred',
        type: 'unknown',
        status
      };
  }
};

// Usage
try {
  const response = await apiClient.get('/protected-resource');
} catch (error) {
  const errorInfo = handleApiError(error);
  console.error(errorInfo);
  // Show user-friendly error message
}

Final Thoughts

403 Forbidden errors in Axios can feel mysterious—but they don’t have to be. With a clear step-by-step approach, you can narrow down the root cause and solve the problem with confidence.

Keep these principles in mind:

  • Always log and read the full error response
  • Test your requests in multiple environments
  • Don’t guess—verify everything, from headers to permissions
Marius Bernard

Marius Bernard

Marius Bernard is a Product Advisor, Technical SEO, & Brand Ambassador at Roundproxies. He was the lead author for the SEO chapter of the 2024 Web and a reviewer for the 2023 SEO chapter.