AdSense says ads.txt is not found, but the file is right there
AdSense shows ads.txt "Not found" for two opposite reasons. If you added the site in the last day or two, AdSense simply has not crawled the file yet, so wait. If your host returns your homepage HTML at /ads.txt with a 200 status, the file is effectively missing. One curl command tells you which.
A file that opened fine and still read Not found
AdSense reported Ads.txt status: Not found on a site where the file opened in a browser and looked entirely fine. The browser was rendering the wrong thing. A request for a file that did not exist was being answered with the homepage and a 200 OK status, so curl returned <!DOCTYPE html> where the publisher line should have been, and the crawler read that as not being an ads.txt file.
Waiting is the instinct here and it is the wrong one, because no amount of patience creates a file. The harder version of the same symptom is a file that does exist and is served correctly, with a proxy, a Worker or a catch-all middleware transforming the response on the way out, so the body carries the publisher line plus something else and the content type comes back as text/html rather than text/plain.
AdSense reports Ads.txt status: Not found. You open the URL in a browser, and the file is right there. So which is lying?
Neither. There are two distinct causes and they need opposite responses, so identify which one you have before changing anything.
Cause one: AdSense has not crawled yet
If you added the site in the last day or two, this is almost certainly it. AdSense checks ads.txt on its own schedule, not when you add a site. The status catches up on its own. Do nothing.
Cause two: your server is serving HTML and calling it ads.txt
This is the one that matters. On many static hosts, a request for a file that does not exist does not return a 404. It returns your homepage, with a 200 OK status. Your browser renders it as a page, so opening the URL looks vaguely fine, and AdSense reads it as "this is not an ads.txt file" and reports Not found.
Telling them apart
Look at the actual bytes, not the rendered page:
curl -s https://yourdomain.com/ads.txt | head -3
You want to see this:
google.com, pub-0000000000000000, DIRECT, f08c47fec0942fa0
If instead you see <!DOCTYPE html>, the file does not exist and your host is masking that fact. Waiting will not help. No amount of patience creates a file.
Why the browser fools you: a browser asked for ads.txt, got HTML, and helpfully rendered it. It does not warn you that a plain text file came back as a web page. Only the raw response shows the truth.
The fix, and the bigger problem behind it
Add a real ads.txt file at the site root containing exactly one line, your publisher line, with no leading spaces and no HTML around it. Redeploy and re-run the curl check.
Then fix the underlying behaviour, because ads.txt is not the only thing it breaks. A host that answers every unknown path with a 200 and your homepage is also telling search engines that every typo is a real page, and it will silently defeat any code that checks whether a fetch succeeded. That failure mode gets its own guide: when a missing file returns 200 instead of 404.
Cause three: something on your edge is rewriting the file
There is a third cause, less common than the two above but harder to spot, because the file exists, the server has it, and the bytes still come back wrong.
If your site sits behind a proxy, a Worker, or a middleware function that runs on every request, that code can intercept a plain text file and hand back something else. A catch-all middleware written to add security headers or gate access will happily run against /ads.txt unless you exempted it. Some CDN features do the same thing: Cloudflare's Email Address Obfuscation rewrites anything that looks like an email address in a response body, and script-injection features can append markup to responses that were never meant to be HTML.
The tell is that the response contains your publisher line plus something else. Check the whole body and the content type together:
curl -sI https://yourdomain.com/ads.txt | grep -i content-type
curl -s https://yourdomain.com/ads.txt | cat -A | head -5
You want text/plain. If you get text/html, something is transforming the file. cat -A shows invisible characters, which catches the other version of this problem: a byte order mark at the start of the file, or Windows line endings, either of which can make a parser reject an otherwise correct line.
The format, precisely
Ads.txt is unforgiving in ways that are easy to get wrong because the file looks casual.
Each record is four comma-separated fields: the ad system domain, your publisher ID, the relationship (DIRECT or RESELLER), and an optional certification authority ID. For AdSense that is one line, and the certification ID is not optional in practice because every published example includes it.
Comments start with # and run to the end of the line. Blank lines are fine. Leading whitespace is not reliably tolerated, so do not indent. The file must be UTF-8 without a BOM, and it must be at the root of the domain, not in a subdirectory.
If you sell inventory through more than one network, every network gets its own line in the same file. There is exactly one ads.txt per domain, and adding a second network means appending a line, never replacing the file. A surprising number of sites lose their AdSense line the day they add a second ad partner.
Subdomains, and why yours probably does not need its own
Ads.txt is looked up on the domain that serves the page. If you run ads on blog.yourdomain.com, crawlers look for blog.yourdomain.com/ads.txt, not the apex.
For most small sites this never comes up, because everything is served from one hostname. It matters the moment you serve the same site on both the apex and www, since those are different hostnames. If both resolve and both serve ads, both need the file. The cleaner fix is to make one of them redirect to the other so only one hostname ever serves a page.
There is also app-ads.txt, which is the same idea for mobile apps and is looked up on the developer website listed in the app store entry. If you do not publish an app, you do not need it, and adding one does nothing.
What the statuses actually mean
Unauthorized. This one means something different from Not found: a file was found and parsed, but it does not contain a line authorizing your publisher ID. Almost always this is a typo in the publisher ID, or a file that was overwritten when a second ad network was added.
Authorized. Done. Leave the file alone.
The important thing is that ads.txt does not gate approval. A missing file does not stop a site being reviewed and does not by itself cause a rejection. What it does is prevent some buyers from bidding on your inventory once you are live, which shows up as lower revenue rather than as an error. That is why it is worth fixing properly and not worth panicking about.
Re-check on a schedule, not once
The failure that costs real money is the file disappearing months after you verified it, because a deploy replaced the whole site and the file was never in the source folder. That is a different guide, but it is the same file.
Put the check in whatever you run after a deploy:
curl -s https://yourdomain.com/ads.txt | grep -q "^google.com, pub-" \
&& echo "ads.txt OK" || echo "ads.txt BROKEN"
Two seconds, and it catches the version of this problem that nobody notices for six weeks.
Where a valid ads.txt stops helping
Adding a correct ads.txt file fixes the status and nothing else. If the cause was a host answering every unknown path with a 200 and your homepage, that behaviour survives the fix, still telling search engines that every typo is a real page and still defeating any code that checks whether a fetch succeeded, which is why it gets its own treatment in when a missing file returns 200 instead of 404. The file can also vanish again on the next deploy that replaces the whole site, so the check belongs in whatever you run afterwards. And fixing it does not move you closer to approval, because ads.txt does not gate review; what it recovers is buyers being able to bid on your inventory.
Questions about the ads.txt warning
My ads.txt opens fine in a browser, so why does AdSense say Not found?
Because the browser is rendering HTML that came back in place of the file. A browser asked for a text file, got a web page, and displayed it without warning you. Only the raw response shows the truth, so check it with curl -s https://yourdomain.com/ads.txt | head -3.
How do I tell whether the file is missing or just not crawled yet?
If the curl output is your publisher line, the file is there and the status will catch up on its own, which is normal in the first day or two after adding a site. If the output starts with <!DOCTYPE html>, the file does not exist and your host is masking that.
Does a missing ads.txt stop AdSense approving my site?
No. A missing file does not stop a site being reviewed and does not by itself cause a rejection. What it does is prevent some buyers bidding on your inventory once you are live, which shows up as lower revenue rather than as an error.
What does Unauthorized mean, as opposed to Not found?
It means a file was found and parsed but contains no line authorising your publisher ID. Almost always that is a typo in the publisher ID, or a file that was overwritten when a second ad network was added rather than having a line appended.
Do I need a separate ads.txt for www and for the apex domain?
Ads.txt is looked up on the hostname that serves the page, and the apex and www are different hostnames, so if both resolve and both serve ads then both need the file. The cleaner fix is to redirect one to the other so only one hostname ever serves a page.
The file exists and the bytes still come back wrong. What else could be doing it?
Anything that runs on every request: a proxy, a Worker, or middleware added for security headers or access gating, and CDN features such as email address obfuscation or script injection. Check curl -sI for a text/plain content type, and cat -A for a byte order mark or Windows line endings that can make a parser reject a correct line.