cd /news/ai-agents/i-stopped-blaming-gpt-5-for-my-ads-a… · home › topics › ai-agents › article
[ARTICLE · art-153295] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=· neutral

I stopped blaming GPT-5 for my Ads API 401 OAuth mess and fixed the 4 auth states that were actually broken

A developer traced recurring 2 a.m. failures in AI-driven ad automation — 401 errors, invalid_grant responses, and broken Make and Meta syncs — to OAuth misconfiguration rather than the model layer, identifying four broken auth states. The writeup notes that Google Ads API requests often require a login-customer-id header for manager-account access, that Google refresh tokens can expire in 7 days when an External OAuth consent screen is left in Testing mode, and that unattended Meta Marketing API jobs should use system user tokens instead of short-lived user tokens. The developer recommends treating auth failures as state transitions rather than transient errors, since blind retries only burn time when credentials are invalid.

by read7 min views1 publishedOct 19, 2026

I lost a stupid amount of time blaming GPT-5 for an automation failure that had nothing to do with GPT-5.

The setup looked normal enough:

Instead, the job kept dying around 2 a.m. with a 401.

Then a Make scenario started failing on a Meta sync.

Then a Python worker started returning invalid_grant.

Then I did what a lot of us do when an "AI workflow" breaks:

I blamed the model layer.

Maybe GPT-5 was acting weird.

Maybe Claude had generated a bad payload.

Maybe the prompt had drifted.

Maybe my routing logic was flaky.

Nope.

The real problem was much more boring: auth.

And honestly, that's useful news, because auth failures are easier to fix than model behavior.

If you're running AI agents or automations against ad platforms, here's the thing I wish I'd internalized earlier:

Most Ads API 401s happen before your model, agent, or prompt ever gets a turn.

For Google Ads API, a valid access token is only part of the story.

You usually need all of these lined up at the same time:

login-customer-id when going through a manager account Miss one, and your "AI failure" is really just a request getting rejected at the gate.

That login-customer-id detail is especially good at wasting afternoons.

If the authenticated identity is accessing a client account through a Google Ads manager account, you often need to send the manager account's 10-digit customer ID as login-customer-id.

Without it, the token can be valid and the request can still fail.

When I suspect a dead credential, I test refresh outside the app first:

curl -s -X POST https://oauth2.googleapis.com/token \
  -d client_id="$GOOGLE_CLIENT_ID" \
  -d client_secret="$GOOGLE_CLIENT_SECRET" \
  -d refresh_token="$GOOGLE_REFRESH_TOKEN" \
  -d grant_type=refresh_token

If this fails with invalid_grant, stop debugging your prompt.

{
  "developer_token": "YOUR_DEVELOPER_TOKEN",
  "login_customer_id": "1234567890"
}
Authorization: Bearer ACCESS_TOKEN
developer-token: YOUR_DEVELOPER_TOKEN
login-customer-id: 1234567890

Not glamorous. But this is the layer that decides whether your agent runs at all.

This was the part that bit me hardest.

A lot of teams treat Google refresh tokens like long-lived infrastructure. They are not.

Google refresh tokens can stop working for a bunch of reasons, including:

That last one is brutal.

If your OAuth consent screen is set to Testing and your app is External, refresh tokens can expire in 7 days unless you're only using basic identity scopes.

Seven days.

So you can build a workflow on Friday, watch it pass on Monday, and then spend the next week blaming GPT-5 for an auth lifecycle problem.

I've seen postmortems call this:

But the root cause was a 7-day refresh token timeout.

That's not an LLM issue. That's plumbing.

Because several different failures collapse into the same symptom.

A 401-looking Google Ads failure can mean:

This is why blind retries are mostly useless.

If the auth state is wrong, retries just burn time and make logs noisier.

The mental model that helped me was this:

Treat auth failures as state transitions, not transient errors.

Once I started doing that, the failures stopped feeling random.

Different API, same class of problem.

Meta Marketing API gives you multiple token types, and a lot of overnight failures come from choosing the wrong one.

API or pattern What actually matters
Google Ads API OAuth 2.0 is required, a developer token is also required, and login-customer-id is often required for manager-account access
Google user refresh token vs service account User refresh tokens can expire or be revoked; service accounts avoid human re-consent but still need Google Ads access granted in the UI
Meta Marketing API Short-lived user tokens are bad for unattended jobs; long-lived user tokens are better; system user tokens are usually the right fit for business automation

If your Make or Zapier scenario is using a short-lived user token for an unattended sync job, that is not AI unreliability.

That's just the wrong credential strategy.

For some Google Ads automations: yes, absolutely.

If the automation is server-side and operating on accounts your team already controls, service accounts are often the cleaner setup.

Two immediate benefits:

That second one matters more than people admit.

I've seen critical automations effectively authenticated as "someone from paid media who left three months ago." That's not architecture. That's a trap.

A Google service account does not automatically have Google Ads access.

You still have to add it inside the Google Ads UI.

Path:

Google Ads UI -> Admin -> Access and security -> Users -> + -> add service account email

Then assign an appropriate role.

If you skip that step, the service account exists, the code looks fine, and the automation still fails.

Service accounts are not a universal answer.

If you're building a multi-tenant SaaS product acting on behalf of outside advertisers, you still need a proper OAuth user-consent flow and a decent re-auth UX.

You can't service-account your way out of delegated access.

This is the boring part.

It's also the part that worked.

I now use the same auth hardening rules whether the workflow runs in n8n, Make, a Python worker, or a Node service.

Keep all of this:

If your database column is just token_blob, you're making future-you miserable.

Don't wait for a live request to discover that auth is dead.

Refresh proactively when expiry is near.

For example:

from datetime import datetime, timedelta, timezone

def should_refresh(expires_at: datetime, buffer_minutes: int = 10) -> bool:
    now = datetime.now(timezone.utc)
    return now >= (expires_at - timedelta(minutes=buffer_minutes))

Move auth volatility out of the critical path.

This one causes a lot of fake mystery bugs.

If multiple workers try to refresh the same expired credential at once, they can stomp on each other:

Now everyone thinks the API is flaky.

Use a lock per credential.

Example with Redis-style locking pseudocode:

def refresh_with_lock(credential_id):
    lock_key = f"refresh-lock:{credential_id}"

    if not acquire_lock(lock_key, ttl=30):
        raise RuntimeError("refresh already in progress")

    try:
        credential = load_credential(credential_id)
        if not should_refresh(credential.expires_at):
            return credential

        new_token = refresh_token(credential.refresh_token)
        save_credential(credential_id, new_token)
        return new_token
    finally:
        release_lock(lock_key)

One credential, one refresh path.

If you get:

401`` invalid_grant``OAuthException branch into auth recovery logic immediately.

That can mean:

What you should not do is retry the same broken state six times and call that resilience.

Example pattern:

def call_ads_api(request, credential):
    response = send_request(request, credential.access_token)

    if response.status_code != 401:
        return response

    refreshed = try_refresh(credential)
    if not refreshed:
        mark_reauth_required(credential.id)
        raise RuntimeError("reauth required")

    retry_response = send_request(request, refreshed.access_token)
    return retry_response

When an ad automation fails, I check these in order:

1. Can the refresh token still mint a new access token?
2. Is the developer token present and valid?
3. Is login-customer-id required here?
4. Does the principal actually have access to the target customer account?
5. Is this tied to a user token that should really be a service account?
6. Is the OAuth app still in Testing mode?
1. What token type is this?
2. Is it short-lived, long-lived, or system user?
3. Is this job unattended?
4. If yes, why am I still using a user token?

Only after auth checks pass do I start looking at prompts, model routing, or agent behavior.

That order matters.

This matters even more when you're running agents continuously.

Once you have n8n flows, Make scenarios, Python workers, and scheduled jobs all calling APIs around the clock, per-request billing is not your only source of pain.

Credential drift becomes its own tax:

And when you're also paying per token for the model layer, bad auth hurts twice:

That's one reason I like using Standard Compute for agent and automation workloads. If I'm iterating on routing, retries, classification prompts, or recovery logic across GPT-5.4, Claude Opus 4.6, and Grok 4.20, I don't want token accounting to become part of the debugging stress.

Flat-rate, OpenAI-compatible compute is a much better fit for this kind of always-on workflow work than watching every model call like a taxi meter.

Especially when the real bug is often outside the model layer anyway.

That was the whole lesson for me.

We spend a lot of time comparing GPT-5, Claude, Grok, Qwen, Llama, routers, prompts, and agent frameworks.

But the smartest component in your stack is still downstream of the dumbest failure mode.

If Google Ads rejects the request because:

then the model never had a chance to succeed or fail.

And weirdly, that's good news.

Model problems can be slippery.

Auth problems are usually concrete.

So if your ad automation keeps "mysteriously" dying, stop staring at GPT traces for a minute.

Check the credential type.

Check the refresh lifecycle.

Check whether the OAuth app is still in Testing.

Check whether the principal actually has account access.

Check login-customer-id.

Most of the pain is up there, before the agent ever runs.

Fix that layer first.

Then your AI stack suddenly looks a lot smarter.

── more in #ai-agents 4 stories · sorted by recency
── more on @google 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/i-stopped-blaming-gp…] indexed:0 read:7min 2026-10-19 · —