[{"data":1,"prerenderedAt":763},["ShallowReactive",2],{"content-query-Z9Y3Dm95TB":3},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"layout":10,"crumb":11,"summary":12,"group":13,"order":14,"code":14,"updated":15,"cta":16,"related":24,"faq":43,"body":59,"_type":756,"_id":757,"_source":758,"_file":759,"_stem":760,"_extension":761,"sitemap":762},"/http-errors/401-vs-403","http-errors",false,"","401 vs 403: Unauthorized vs Forbidden explained","401 vs 403: 401 Unauthorized means not authenticated, 403 Forbidden means authenticated but not allowed. When to use each and how to fix them.","reference","401 vs 403","401: who are you? 403: I know who you are, and no.","4xx client errors",401,"2026-10-28",{"eyebrow":17,"title":18,"text":19,"to":20,"label":21,"secondaryTo":22,"secondaryLabel":23},"Webhook Relay","Check a webhook signature before you blame the code","Most webhook 401s are a signature mismatch: the wrong secret, a parsed body, or a stale timestamp. Paste the payload, secret and header into the free verifier to see whether the signature is valid and what it should have been.","/verify-webhook-signature/","Verify a signature","/docs/webhooks/auth/hmac/","Verify HMAC in a function",[25,28,31,34,37,40],{"title":26,"to":27},"Webhook signature","/glossary/webhook-signature/",{"title":29,"to":30},"Webhook secret","/glossary/webhook-secret/",{"title":32,"to":33},"HMAC","/glossary/hmac/",{"title":35,"to":36},"HMAC verification tool","/hmac-verification/",{"title":38,"to":39},"400 vs 422","/http-errors/400-vs-422/",{"title":41,"to":42},"How to verify a webhook signature","/blog/verify-webhook-signature/",[44,47,50,53,56],{"q":45,"a":46},"What is the difference between 401 and 403?","401 Unauthorized means the request has no valid credentials: the server doesn't know who you are, so log in or send a valid token and try again. 403 Forbidden means the server knows who you are, or doesn't need to, and refuses anyway: your account or key isn't allowed to do this, and sending the same credentials again won't help.",{"q":48,"a":49},"Should I return 401 or 403 for an expired token?","401. An expired, malformed or revoked token means the client isn't authenticated. Return 401 with a WWW-Authenticate header so the client knows to refresh the token or log in again. Return 403 only when a valid token lacks permission.",{"q":51,"a":52},"Why do I get 403 Forbidden with a valid API key?","The key is recognised but not allowed to do this. Common reasons are a missing scope or permission, a key restricted to certain IPs or domains, a resource owned by another account, or a WAF or CDN rule blocking the request before it reaches the app.",{"q":54,"a":55},"Should a webhook endpoint return 401 or 403 for a bad signature?","Either is defensible, and 401 is the most common choice; some providers' examples use 400. What matters is returning a 4xx, not a 2xx, and logging why. A bad signature usually means the wrong secret or a body that was parsed and re-serialised before verification, not an attack.",{"q":57,"a":58},"Does 401 Unauthorized mean not authenticated?","Yes. Despite the name, 401 is about authentication, not authorization. The HTTP spec says a 401 response must include a WWW-Authenticate header describing how to authenticate. 403 is the code for an authorization failure.",{"type":60,"children":61,"toc":749},"root",[62,91,96,209,267,304,311,387,393,454,460,470,478,572,578,598,603,660,693,698,704,743],{"type":63,"tag":64,"props":65,"children":66},"element","p",{},[67,89],{"type":63,"tag":68,"props":69,"children":70},"strong",{},[71,79,81,87],{"type":63,"tag":72,"props":73,"children":75},"code",{"className":74},[],[76],{"type":77,"value":78},"text","401 Unauthorized",{"type":77,"value":80}," means the server doesn't know who you are: credentials are missing, invalid or expired. ",{"type":63,"tag":72,"props":82,"children":84},{"className":83},[],[85],{"type":77,"value":86},"403 Forbidden",{"type":77,"value":88}," means the server knows who you are (or doesn't care) and refuses anyway.",{"type":77,"value":90}," Fix a 401 by authenticating. A 403 won't go away by sending the same credentials again; you need different permissions.",{"type":63,"tag":64,"props":92,"children":93},{},[94],{"type":77,"value":95},"The name is misleading. 401 is really \"unauthenticated\", and 403 is \"unauthorized\".",{"type":63,"tag":97,"props":98,"children":99},"table",{},[100,128],{"type":63,"tag":101,"props":102,"children":103},"thead",{},[104],{"type":63,"tag":105,"props":106,"children":107},"tr",{},[108,112,120],{"type":63,"tag":109,"props":110,"children":111},"th",{},[],{"type":63,"tag":109,"props":113,"children":114},{},[115],{"type":63,"tag":72,"props":116,"children":118},{"className":117},[],[119],{"type":77,"value":78},{"type":63,"tag":109,"props":121,"children":122},{},[123],{"type":63,"tag":72,"props":124,"children":126},{"className":125},[],[127],{"type":77,"value":86},{"type":63,"tag":129,"props":130,"children":131},"tbody",{},[132,151,169,187],{"type":63,"tag":105,"props":133,"children":134},{},[135,141,146],{"type":63,"tag":136,"props":137,"children":138},"td",{},[139],{"type":77,"value":140},"Question",{"type":63,"tag":136,"props":142,"children":143},{},[144],{"type":77,"value":145},"Who are you?",{"type":63,"tag":136,"props":147,"children":148},{},[149],{"type":77,"value":150},"I know who you are. Not allowed.",{"type":63,"tag":105,"props":152,"children":153},{},[154,159,164],{"type":63,"tag":136,"props":155,"children":156},{},[157],{"type":77,"value":158},"Cause",{"type":63,"tag":136,"props":160,"children":161},{},[162],{"type":77,"value":163},"No token, bad password, expired or revoked token, invalid signature",{"type":63,"tag":136,"props":165,"children":166},{},[167],{"type":77,"value":168},"Missing scope or role, IP or WAF block, resource belongs to someone else",{"type":63,"tag":105,"props":170,"children":171},{},[172,177,182],{"type":63,"tag":136,"props":173,"children":174},{},[175],{"type":77,"value":176},"Will retrying with new credentials help?",{"type":63,"tag":136,"props":178,"children":179},{},[180],{"type":77,"value":181},"Yes",{"type":63,"tag":136,"props":183,"children":184},{},[185],{"type":77,"value":186},"Usually no",{"type":63,"tag":105,"props":188,"children":189},{},[190,195,204],{"type":63,"tag":136,"props":191,"children":192},{},[193],{"type":77,"value":194},"Required header",{"type":63,"tag":136,"props":196,"children":197},{},[198],{"type":63,"tag":72,"props":199,"children":201},{"className":200},[],[202],{"type":77,"value":203},"WWW-Authenticate",{"type":63,"tag":136,"props":205,"children":206},{},[207],{"type":77,"value":208},"None",{"type":63,"tag":210,"props":211,"children":215},"pre",{"className":212,"code":213,"language":214,"meta":7,"style":7},"language-http shiki shiki-themes github-dark","HTTP/1.1 401 Unauthorized\nWWW-Authenticate: Bearer realm=\"api\", error=\"invalid_token\", error_description=\"The access token expired\"\nContent-Type: application/json\n\n{\"error\": \"invalid_token\"}\n","http",[216],{"type":63,"tag":72,"props":217,"children":218},{"__ignoreMap":7},[219,230,239,248,258],{"type":63,"tag":220,"props":221,"children":224},"span",{"class":222,"line":223},"line",1,[225],{"type":63,"tag":220,"props":226,"children":227},{},[228],{"type":77,"value":229},"HTTP/1.1 401 Unauthorized\n",{"type":63,"tag":220,"props":231,"children":233},{"class":222,"line":232},2,[234],{"type":63,"tag":220,"props":235,"children":236},{},[237],{"type":77,"value":238},"WWW-Authenticate: Bearer realm=\"api\", error=\"invalid_token\", error_description=\"The access token expired\"\n",{"type":63,"tag":220,"props":240,"children":242},{"class":222,"line":241},3,[243],{"type":63,"tag":220,"props":244,"children":245},{},[246],{"type":77,"value":247},"Content-Type: application/json\n",{"type":63,"tag":220,"props":249,"children":251},{"class":222,"line":250},4,[252],{"type":63,"tag":220,"props":253,"children":255},{"emptyLinePlaceholder":254},true,[256],{"type":77,"value":257},"\n",{"type":63,"tag":220,"props":259,"children":261},{"class":222,"line":260},5,[262],{"type":63,"tag":220,"props":263,"children":264},{},[265],{"type":77,"value":266},"{\"error\": \"invalid_token\"}\n",{"type":63,"tag":210,"props":268,"children":270},{"className":212,"code":269,"language":214,"meta":7,"style":7},"HTTP/1.1 403 Forbidden\nContent-Type: application/json\n\n{\"error\": \"insufficient_scope\", \"message\": \"This key cannot write to repositories\"}\n",[271],{"type":63,"tag":72,"props":272,"children":273},{"__ignoreMap":7},[274,282,289,296],{"type":63,"tag":220,"props":275,"children":276},{"class":222,"line":223},[277],{"type":63,"tag":220,"props":278,"children":279},{},[280],{"type":77,"value":281},"HTTP/1.1 403 Forbidden\n",{"type":63,"tag":220,"props":283,"children":284},{"class":222,"line":232},[285],{"type":63,"tag":220,"props":286,"children":287},{},[288],{"type":77,"value":247},{"type":63,"tag":220,"props":290,"children":291},{"class":222,"line":241},[292],{"type":63,"tag":220,"props":293,"children":294},{"emptyLinePlaceholder":254},[295],{"type":77,"value":257},{"type":63,"tag":220,"props":297,"children":298},{"class":222,"line":250},[299],{"type":63,"tag":220,"props":300,"children":301},{},[302],{"type":77,"value":303},"{\"error\": \"insufficient_scope\", \"message\": \"This key cannot write to repositories\"}\n",{"type":63,"tag":305,"props":306,"children":308},"h2",{"id":307},"common-causes-of-401",[309],{"type":77,"value":310},"Common causes of 401",{"type":63,"tag":312,"props":313,"children":314},"ul",{},[315,334,367,377],{"type":63,"tag":316,"props":317,"children":318},"li",{},[319,324,326,332],{"type":63,"tag":68,"props":320,"children":321},{},[322],{"type":77,"value":323},"No credentials sent.",{"type":77,"value":325}," A missing ",{"type":63,"tag":72,"props":327,"children":329},{"className":328},[],[330],{"type":77,"value":331},"Authorization",{"type":77,"value":333}," header, often stripped by a proxy or lost on a redirect.",{"type":63,"tag":316,"props":335,"children":336},{},[337,342,344,350,352,358,359,365],{"type":63,"tag":68,"props":338,"children":339},{},[340],{"type":77,"value":341},"Wrong format.",{"type":77,"value":343}," ",{"type":63,"tag":72,"props":345,"children":347},{"className":346},[],[348],{"type":77,"value":349},"Bearer",{"type":77,"value":351}," vs ",{"type":63,"tag":72,"props":353,"children":355},{"className":354},[],[356],{"type":77,"value":357},"Basic",{"type":77,"value":351},{"type":63,"tag":72,"props":360,"children":362},{"className":361},[],[363],{"type":77,"value":364},"Token",{"type":77,"value":366},", or a missing space.",{"type":63,"tag":316,"props":368,"children":369},{},[370,375],{"type":63,"tag":68,"props":371,"children":372},{},[373],{"type":77,"value":374},"Expired or revoked token.",{"type":77,"value":376}," OAuth access tokens expire, often within an hour.",{"type":63,"tag":316,"props":378,"children":379},{},[380,385],{"type":63,"tag":68,"props":381,"children":382},{},[383],{"type":77,"value":384},"Wrong environment.",{"type":77,"value":386}," A test key against the live API, or a staging token against production.",{"type":63,"tag":305,"props":388,"children":390},{"id":389},"common-causes-of-403",[391],{"type":77,"value":392},"Common causes of 403",{"type":63,"tag":312,"props":394,"children":395},{},[396,406,424,434,444],{"type":63,"tag":316,"props":397,"children":398},{},[399,404],{"type":63,"tag":68,"props":400,"children":401},{},[402],{"type":77,"value":403},"Missing scope or role.",{"type":77,"value":405}," The token works but wasn't granted this permission.",{"type":63,"tag":316,"props":407,"children":408},{},[409,414,416,422],{"type":63,"tag":68,"props":410,"children":411},{},[412],{"type":77,"value":413},"IP, country or WAF rules.",{"type":77,"value":415}," Cloudflare, AWS WAF or an nginx ",{"type":63,"tag":72,"props":417,"children":419},{"className":418},[],[420],{"type":77,"value":421},"deny",{"type":77,"value":423}," rule blocks the request before your code sees it. Cloudflare's block page says \"Sorry, you have been blocked\".",{"type":63,"tag":316,"props":425,"children":426},{},[427,432],{"type":63,"tag":68,"props":428,"children":429},{},[430],{"type":77,"value":431},"File permissions.",{"type":77,"value":433}," nginx and Apache return 403 when they can't read a file, or when a directory has no index file and listing is off.",{"type":63,"tag":316,"props":435,"children":436},{},[437,442],{"type":63,"tag":68,"props":438,"children":439},{},[440],{"type":77,"value":441},"Ownership.",{"type":77,"value":443}," The resource exists but belongs to another account. Some APIs return 404 here instead, to avoid revealing that it exists.",{"type":63,"tag":316,"props":445,"children":446},{},[447,452],{"type":63,"tag":68,"props":448,"children":449},{},[450],{"type":77,"value":451},"CSRF failures",{"type":77,"value":453}," in frameworks like Django and Laravel.",{"type":63,"tag":305,"props":455,"children":457},{"id":456},"how-to-fix-them",[458],{"type":77,"value":459},"How to fix them",{"type":63,"tag":64,"props":461,"children":462},{},[463,468],{"type":63,"tag":68,"props":464,"children":465},{},[466],{"type":77,"value":467},"As a user:",{"type":77,"value":469}," for 401, log in again. For 403, check you're signed into the right account and ask an admin for access.",{"type":63,"tag":64,"props":471,"children":472},{},[473],{"type":63,"tag":68,"props":474,"children":475},{},[476],{"type":77,"value":477},"As a developer:",{"type":63,"tag":479,"props":480,"children":481},"ol",{},[482,502,531,544],{"type":63,"tag":316,"props":483,"children":484},{},[485,487,493,495,500],{"type":77,"value":486},"Print the exact request with ",{"type":63,"tag":72,"props":488,"children":490},{"className":489},[],[491],{"type":77,"value":492},"curl -v",{"type":77,"value":494}," and confirm the ",{"type":63,"tag":72,"props":496,"children":498},{"className":497},[],[499],{"type":77,"value":331},{"type":77,"value":501}," header arrives. Proxies and redirects drop it more often than you'd think.",{"type":63,"tag":316,"props":503,"children":504},{},[505,507,513,515,521,523,529],{"type":77,"value":506},"For 401, decode the token (JWTs are readable at the payload level) and check ",{"type":63,"tag":72,"props":508,"children":510},{"className":509},[],[511],{"type":77,"value":512},"exp",{"type":77,"value":514},", ",{"type":63,"tag":72,"props":516,"children":518},{"className":517},[],[519],{"type":77,"value":520},"aud",{"type":77,"value":522}," and ",{"type":63,"tag":72,"props":524,"children":526},{"className":525},[],[527],{"type":77,"value":528},"iss",{"type":77,"value":530},".",{"type":63,"tag":316,"props":532,"children":533},{},[534,536,542],{"type":77,"value":535},"For 403, check whether the response came from your app or from a CDN or WAF in front of it. The ",{"type":63,"tag":72,"props":537,"children":539},{"className":538},[],[540],{"type":77,"value":541},"Server",{"type":77,"value":543}," header and the body usually tell you.",{"type":63,"tag":316,"props":545,"children":546},{},[547,549,555,557,563,565,571],{"type":77,"value":548},"On nginx, a 403 for static files is usually file permissions or a missing ",{"type":63,"tag":72,"props":550,"children":552},{"className":551},[],[553],{"type":77,"value":554},"index",{"type":77,"value":556},". Check the error log for ",{"type":63,"tag":72,"props":558,"children":560},{"className":559},[],[561],{"type":77,"value":562},"permission denied",{"type":77,"value":564}," or ",{"type":63,"tag":72,"props":566,"children":568},{"className":567},[],[569],{"type":77,"value":570},"directory index ... is forbidden",{"type":77,"value":530},{"type":63,"tag":305,"props":573,"children":575},{"id":574},"_401-and-403-on-a-webhook-endpoint",[576],{"type":77,"value":577},"401 and 403 on a webhook endpoint",{"type":63,"tag":64,"props":579,"children":580},{},[581,583,589,591,596],{"type":77,"value":582},"Webhooks rarely carry login credentials. Instead, the provider signs the payload with a shared ",{"type":63,"tag":584,"props":585,"children":586},"a",{"href":30},[587],{"type":77,"value":588},"webhook secret",{"type":77,"value":590},", and your endpoint checks the ",{"type":63,"tag":584,"props":592,"children":593},{"href":27},[594],{"type":77,"value":595},"webhook signature",{"type":77,"value":597},". When that check fails, most handlers return 401. Some providers' examples use 400. Either way, a 4xx tells the provider the request was rejected.",{"type":63,"tag":64,"props":599,"children":600},{},[601],{"type":77,"value":602},"When signatures fail, the cause is rarely an attacker. It's usually:",{"type":63,"tag":312,"props":604,"children":605},{},[606,616,632,642],{"type":63,"tag":316,"props":607,"children":608},{},[609,614],{"type":63,"tag":68,"props":610,"children":611},{},[612],{"type":77,"value":613},"The wrong secret.",{"type":77,"value":615}," Test vs live, or a secret from a different endpoint. Each endpoint usually has its own.",{"type":63,"tag":316,"props":617,"children":618},{},[619,624,626,630],{"type":63,"tag":68,"props":620,"children":621},{},[622],{"type":77,"value":623},"A modified body.",{"type":77,"value":625}," The signature is computed over the raw bytes. If your framework parses the JSON and you re-serialise it, the bytes change and the ",{"type":63,"tag":584,"props":627,"children":628},{"href":33},[629],{"type":77,"value":32},{"type":77,"value":631}," no longer matches.",{"type":63,"tag":316,"props":633,"children":634},{},[635,640],{"type":63,"tag":68,"props":636,"children":637},{},[638],{"type":77,"value":639},"A stale timestamp.",{"type":77,"value":641}," Providers like Stripe include a timestamp, and your code rejects it if it's outside a tolerance window. Check the server clock.",{"type":63,"tag":316,"props":643,"children":644},{},[645,650,652,658],{"type":63,"tag":68,"props":646,"children":647},{},[648],{"type":77,"value":649},"The wrong header or encoding.",{"type":77,"value":651}," Hex vs base64, or a ",{"type":63,"tag":72,"props":653,"children":655},{"className":654},[],[656],{"type":77,"value":657},"sha256=",{"type":77,"value":659}," prefix that wasn't stripped.",{"type":63,"tag":64,"props":661,"children":662},{},[663,665,670,672,677,679,684,686,692],{"type":77,"value":664},"Paste the exact payload, secret and header into the ",{"type":63,"tag":584,"props":666,"children":667},{"href":20},[668],{"type":77,"value":669},"webhook signature verifier",{"type":77,"value":671}," or the ",{"type":63,"tag":584,"props":673,"children":674},{"href":36},[675],{"type":77,"value":676},"HMAC tool",{"type":77,"value":678}," to see what the signature should be. To reject unsigned requests before they reach your app, a Webhook Relay function can ",{"type":63,"tag":584,"props":680,"children":681},{"href":22},[682],{"type":77,"value":683},"verify the HMAC",{"type":77,"value":685}," at the edge, and buckets also support ",{"type":63,"tag":584,"props":687,"children":689},{"href":688},"/docs/webhooks/auth/username-password/",[690],{"type":77,"value":691},"username/password and token auth",{"type":77,"value":530},{"type":63,"tag":64,"props":694,"children":695},{},[696],{"type":77,"value":697},"Return 403 for webhooks only for a policy decision, for example a request from an IP outside the provider's published range.",{"type":63,"tag":305,"props":699,"children":701},{"id":700},"when-to-return-401-vs-403-from-your-own-api",[702],{"type":77,"value":703},"When to return 401 vs 403 from your own API",{"type":63,"tag":312,"props":705,"children":706},{},[707,723,733],{"type":63,"tag":316,"props":708,"children":709},{},[710,715,717,722],{"type":63,"tag":68,"props":711,"children":712},{},[713],{"type":77,"value":714},"401",{"type":77,"value":716}," when credentials are missing, malformed, expired or don't verify. Always include ",{"type":63,"tag":72,"props":718,"children":720},{"className":719},[],[721],{"type":77,"value":203},{"type":77,"value":530},{"type":63,"tag":316,"props":724,"children":725},{},[726,731],{"type":63,"tag":68,"props":727,"children":728},{},[729],{"type":77,"value":730},"403",{"type":77,"value":732}," when the caller is authenticated but not permitted, or when a policy (IP allowlist, plan, ownership) blocks the request.",{"type":63,"tag":316,"props":734,"children":735},{},[736,741],{"type":63,"tag":68,"props":737,"children":738},{},[739],{"type":77,"value":740},"404",{"type":77,"value":742}," instead of 403 when even confirming the resource exists would leak information.",{"type":63,"tag":744,"props":745,"children":746},"style",{},[747],{"type":77,"value":748},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":7,"searchDepth":241,"depth":241,"links":750},[751,752,753,754,755],{"id":307,"depth":232,"text":310},{"id":389,"depth":232,"text":392},{"id":456,"depth":232,"text":459},{"id":574,"depth":232,"text":577},{"id":700,"depth":232,"text":703},"markdown","content:http-errors:401-vs-403.md","content","http-errors/401-vs-403.md","http-errors/401-vs-403","md",{"loc":4},1793199905089]