ads.txt Not Found or Not Verifying in AdSense

AdSense shows "Earnings at risk — you need to fix some ads.txt file issues." How to deploy ads.txt to the right place, verify it with curl, and clear the warning.

The AdSense dashboard banner says “Earnings at risk — you need to fix some ads.txt file issues to avoid severe impact to your revenue.” You added the file, but the warning won’t clear. Or https://yourdomain.com/ads.txt returns 404 even though you swear you pushed it.

Fastest fix: copy the exact line from AdSense (homepage alert → Fix now, or Sites → your site → Ads.txt snippetCopy), drop it in your framework’s static folder so it serves at https://yourdomain.com/ads.txt with HTTP 200 and Content-Type: text/plain, deploy, and confirm with one curl. Then wait — AdSense re-crawls on its own schedule, a few days in most cases (up to a month on low-traffic sites, per Google’s docs), so a verified-correct file with a stuck banner is usually just the crawl lag, not a real problem.

The ads.txt spec itself is trivial — a plain text file at the root of the domain listing authorized sellers — but a handful of deploy traps catch most indie sites.

Which bucket are you in?

Run this first; the result tells you which section to read.

curl -sIL "https://yourdomain.com/ads.txt"
What you seeMost likely causeGo to
404 / 403Wrong path, or framework didn’t ship the fileCauses 1, 2
200 but Content-Type: text/htmlPlatform/CDN rewriting .txt to HTMLCause 3
200, text/plain, but content looks wrongWrong publisher ID, BOM, smart quotesCause 4
200, content correct, but only on ?nocache=...Stale CDN cacheCause 5
301/302 chain to a different hostRedirect leaves the root domainCause 6
Everything correct, banner still upAdSense hasn’t re-crawled yetCause 7

The -L flag follows redirects so you see the final response, not the first hop.

Common causes

Ordered by hit rate, highest first.

1. File is at the wrong path

ads.txt must resolve at https://yourdomain.com/ads.txt — not /public/ads.txt, not /assets/ads.txt, not /static/ads.txt. Google’s crawl “will start at the root domain, and the root domain needs to return from, or redirect to, the ads.txt file” (Google docs). It hits exactly one URL.

How to spot it:

curl -sI "https://yourdomain.com/ads.txt"

Look for HTTP/2 200. Anything else (404, 301 to the wrong place, 403) means it’s not resolving at the root.

2. Framework didn’t ship the file with the deploy

The file source lives in your repo’s static folder, which the build copies to the web root. Put it in src/ and it never ships.

  • Astro: public/ads.txt
  • Next.js (pages or app router): public/ads.txt
  • SvelteKit: static/ads.txt
  • Nuxt: public/ads.txt (Nuxt 3) — older Nuxt 2 used static/
  • Hugo / Jekyll: static/ads.txt / repo root
  • Vercel static deploy: project root or public/

How to spot it: after deploy, curl https://yourdomain.com/ads.txt. A 404 means the build didn’t ship it — check that the file is committed and inside the static folder, not gitignored.

3. Content-Type wrong, or you get an HTML “soft 404”

Some setups (misconfigured nginx, a catch-all CDN rewrite rule, or a SPA that returns index.html for unknown paths) serve .txt with Content-Type: text/html, or return a 200-status HTML 404 page. AdSense parses HTML, finds no seller line, and treats the file as missing.

How to spot it:

curl -sI "https://yourdomain.com/ads.txt" | grep -i content-type

It should report Content-Type: text/plain (often text/plain; charset=utf-8). If it says text/html, your platform is rewriting or your SPA fallback is swallowing the request.

4. Wrong publisher ID or invisible characters

The line must be exactly:

google.com, pub-XXXXXXXXXXXXXXXX, DIRECT, f08c47fec0942fa0

Four comma-separated fields: the ad system domain (google.com), your publisher ID, the relationship (DIRECT), and Google’s certification-authority ID (f08c47fec0942fa0, the same for every AdSense publisher). Common breakers: trailing whitespace, smart quotes from a copy-paste, a BOM at the start of the file, the wrong pub- number, or a missing comma.

How to spot it:

curl -s "https://yourdomain.com/ads.txt" | od -c | head -3

Watch for unexpected bytes — a UTF-8 BOM shows up as 357 273 277 at the very start of the file. If it’s there, re-save the file as UTF-8 without BOM.

5. Cloudflare or CDN cache is serving the old version

You fixed the file, but a cached copy still wins. AdSense crawls the cached version and sees the old (missing) state.

How to spot it:

curl -s "https://yourdomain.com/ads.txt?nocache=$(date +%s)"

The query string forces a cache miss. If the content differs from the cached URL, your CDN cache is stale — purge it (Cause 5 fix in the next section).

6. Redirect leaves the root domain, or only one protocol serves

Google explicitly allows redirects, but with rules. A single redirect can point off the root domain (e.g. example1.com/ads.txtexample2.com/ads.txt). Multiple redirects are fine only if every hop stays within the original root domain (including www or other subdomains). Also, the crawler tries both http:// and https://; if only one protocol serves the file, set up a redirect so the other one reaches it too.

How to spot it:

curl -sIL "http://yourdomain.com/ads.txt"   # note: plain http
curl -sIL "https://yourdomain.com/ads.txt"

Both should end at a 200 returning your ads.txt. A redirect that bounces twice to another domain, or an http:// that dead-ends, will fail verification.

7. AdSense hasn’t re-crawled yet (most common false alarm)

AdSense re-checks ads.txt on its own schedule, not on demand. Per Google, changes take a few days to reflect, and up to a month on sites that make few ad requests. If a previously crawled file briefly returns a 5xx error, Google keeps the last-good entries for up to 5 days, so transient outages don’t immediately re-trigger the warning.

How to spot it: if curl confirms the file is correct and you fixed it recently, you’re just waiting. Don’t keep redeploying.

Shortest path to fix

Step 1: Get the exact text from AdSense

If there’s an alert on your AdSense homepage, click Fix now. Otherwise go to Sites → click your site → Ads.txt snippetCopy. Copy the line; don’t retype it.

It should look like:

google.com, pub-XXXXXXXXXXXXXXXX, DIRECT, f08c47fec0942fa0

Step 2: Put it at the right path

FrameworkFile goes inURL it serves at
Astropublic/ads.txt/ads.txt
Next.js (pages or app)public/ads.txt/ads.txt
SvelteKitstatic/ads.txt/ads.txt
Nuxt 3public/ads.txt/ads.txt
Hugostatic/ads.txt/ads.txt
Plain HTMLads.txt in webroot/ads.txt

Deploy, then verify.

Step 3: Verify with curl

curl -sIL "https://yourdomain.com/ads.txt"
# Expect: final HTTP/2 200, Content-Type: text/plain

curl -sL "https://yourdomain.com/ads.txt"
# Expect: google.com, pub-XXXXX, DIRECT, f08c47fec0942fa0

If Content-Type is text/html or anything else, your platform or CDN is rewriting. Add an explicit rule:

Vercelvercel.json:

{
  "headers": [
    { "source": "/ads.txt", "headers": [{ "key": "Content-Type", "value": "text/plain" }] }
  ]
}

Netlify_headers:

/ads.txt
  Content-Type: text/plain

Cloudflare Pages_headers file at the project root:

/ads.txt
  Content-Type: text/plain

Step 4: Make sure robots.txt isn’t blocking the crawler

The Google, Mediapartners-Google, and Google-Display-Ads-Bot crawlers honor robots.txt. If a rule disallows the /ads.txt path or one of those user agents, the file is silently ignored. Check it:

curl -s "https://yourdomain.com/robots.txt"

If a Disallow rule could catch /ads.txt, add an explicit allow:

User-agent: *
Allow: /ads.txt

Step 5: Flush CDN cache

Cloudflare → CachingConfigurationPurge CacheCustom Purge → enter https://yourdomain.com/ads.txt. Vercel and Netlify usually self-purge on each deploy.

Step 6: Wait for the AdSense recheck

The banner doesn’t disappear instantly. As long as curl returns the correct content over plain text, AdSense will pick it up on its next crawl — a few days typically, up to a month on low-traffic sites. Resist the urge to redeploy repeatedly; it doesn’t speed up the crawl.

Step 7: Add a CI check so it never silently regresses

# .github/workflows/verify-ads-txt.yml or a postdeploy script
curl -sfL "https://yourdomain.com/ads.txt" | grep -q "pub-$ADSENSE_PUB_ID" || \
  { echo "ads.txt missing or wrong"; exit 1; }

Run it after every deploy. It catches a vanished or rewritten file the moment it ships.

How to confirm it’s fixed

You’ve fixed it when all of these are true:

  1. curl -sIL https://yourdomain.com/ads.txt ends at HTTP/2 200.
  2. The Content-Type is text/plain.
  3. curl -sL https://yourdomain.com/ads.txt prints your exact pub- line with no BOM or stray characters.
  4. Both http:// and https:// reach the file.

Once those pass, the AdSense banner is purely a crawl-lag artifact. It will clear on the next crawl.

Easy to misdiagnose as

Placing ads.txt in the wrong directory (or letting the framework 404 it) is the single most common cause on static sites. A close second is a robots.txt rule quietly blocking the path. Verify with curl, not by eyeballing your codebase — the deployed reality is what AdSense crawls.

Prevention

  • Always keep ads.txt in the static / public folder so it ships with every deploy automatically.
  • Validate in CI: curl -sfL yourdomain.com/ads.txt | grep -q pub-$PUB_ID.
  • Force Content-Type: text/plain via platform config.
  • Keep /ads.txt allowed in robots.txt.
  • After any CDN or DNS change, re-verify ads.txt still serves correctly on both protocols.
  • If you add a second ad network (Mediavine, Ezoic, Raptive), append their lines — one authorized seller per line.

FAQ

  • How long until the “Earnings at risk” banner clears after I fix it? Per Google, a few days in most cases, and up to a month for sites that make few ad requests. There’s no manual “re-crawl now” button.
  • Do I need ads.txt if I only use AdSense? Yes. Without your publisher ID in ads.txt, AdSense restricts demand for your inventory, which is what the warning is about.
  • Can ads.txt have multiple lines? Yes — one line per authorized seller. Each ad network you work with gets its own line.
  • My file is on the root domain but a subdomain still warns. Why? Crawl ads.txt on a subdomain only if its authorized seller or publisher ID differs from the root. Reference the subdomain file from the root ads.txt; otherwise the root file covers it.
  • Does a 301 redirect to my ads.txt work? A single redirect off your root domain is allowed, or multiple redirects as long as every hop stays within the same root domain. A multi-hop chain to a foreign host will fail.
  • AdSense says “file missing” but curl shows it. What now? Check robots.txt isn’t blocking the path, confirm both http:// and https:// serve it, then wait for the next crawl. A correct file plus a stuck banner is the crawl lag.

Tags: #AdSense #Monetization #Troubleshooting #ads.txt