I’ve stopped trusting the green “Connected” badge.
If Zapier, Make, n8n, or your own worker says Google Ads or Meta connected successfully, that only proves the interactive setup worked.
It does not prove your unattended job will still be authenticated at 3 AM.
That distinction explains a huge percentage of the "OAuth worked, but I still get 401" tickets I see around ad automation.
The short version
If OAuth works but your scheduled job still gets 401s, the problem is usually one of these:
- Google Ads: you authenticated correctly, but your API call is still missing a developer token
- Google Ads: you need the correct login-customer-id because you’re operating through a manager account
- Meta Ads: you used the wrong token type for headless automation
- Meta Ads: your long-lived user token quietly expired around day 60
OAuth is often fine.
Your production auth model is not.
Why this keeps happening
During setup, a human is present.
You click through Google or Facebook Login. The SDK handles redirects. The connector smooths over rough edges. You run one test call. It works.
Then the human disappears.
Now the real caller is:
- a cron job
- a background worker
- an n8n workflow
- a Make scenario
- a Zapier automation
- a Python script in Airflow
- a Node.js process inside an agent pipeline
That environment is much less forgiving.
No browser. No session rescue. No interactive refresh. No UI hiding missing headers.
Ads APIs are especially good at exposing that gap.
Google Ads gives people fake confidence
Google Ads is the most common version of this bug.
A developer completes OAuth, stores a refresh token, maybe even runs a successful test request, and assumes auth is done.
It isn’t.
For Google Ads API calls, OAuth alone is not enough.
You usually need:
- OAuth 2.0 access token
- Developer token
- login-customer-id when acting through a manager account
If any of those are missing or wrong, your automation can fail even though the browser login was perfectly valid.
The Google Ads request shape people forget
Here’s the practical shape of a real request:
curl -X POST \
'https://googleads.googleapis.com/v18/customers/1234567890/googleAds:searchStream' \
-H 'Authorization: Bearer ACCESS_TOKEN' \
-H 'developer-token: YOUR_DEVELOPER_TOKEN' \
-H 'login-customer-id: 9988776655' \
-H 'Content-Type: application/json' \
-d '{
"query": "SELECT campaign.id, campaign.name FROM campaign LIMIT 10"
}'
A valid bearer token is only one part of this request.
That’s why “but OAuth succeeded” is not a useful debugging statement for Google Ads.
Common Google Ads failure modes
1. Missing developer token
This is the classic one.
Your OAuth token is valid. The user consented. The request still fails because Google Ads requires a developer token on API calls.
2. Wrong or missing login-customer-id
If you’re calling through a manager account, this header matters.
The token can be valid and the request can still fail because the manager context is wrong.
3. The OAuth identity doesn’t actually have account access
This one is annoying because it feels like auth worked.
It did. But the authenticated identity still might not have access to the target ad account.
4. Refresh token died later
Google refresh tokens are not immortal.
A workflow can work for days or weeks and then start failing with no code changes because the refresh token was revoked or invalidated.
That’s not random. That’s delayed auth failure.
My Google Ads triage checklist
When a scheduled job starts throwing 401s, I check these in order:
- Is the developer token present on every request?
- Is the OAuth identity allowed to access the target Google Ads account?
- If using a manager account, is login-customer-id correct?
- Does the stored refresh token still work?
- Did we pick the right auth pattern for this workflow?
If you’re using n8n, Make, Zapier, or a custom worker, this is much more useful than repeatedly reconnecting the app.
A quick Python example for Google Ads headers
If you’re using plain HTTP instead of a client library, be explicit:
import requests
url = "https://googleads.googleapis.com/v18/customers/1234567890/googleAds:searchStream"
headers = {
"Authorization": f"Bearer {access_token}",
"developer-token": developer_token,
"login-customer-id": manager_customer_id,
"Content-Type": "application/json",
}
payload = {
"query": "SELECT campaign.id, campaign.name FROM campaign LIMIT 10"
}
resp = requests.post(url, headers=headers, json=payload, timeout=30)
print(resp.status_code)
print(resp.text)
If those headers aren’t right, OAuth success won’t save you.
Meta works great until it doesn’t
Meta has a similar trap, but the failure mode is different.
A lot of teams connect Meta Marketing API with a normal user token, run tests, launch a workflow, and assume they’re done.
Then the automation starts failing later because the token expired.
That’s the gotcha: long-lived user tokens are still temporary.
They usually last about 60 days.
That’s long enough to feel stable. It isn’t stable.
It’s just delayed breakage.
Why browser tests lie for Meta
This part catches a lot of people.
Meta SDKs in interactive environments can refresh tokens automatically under the right conditions.
So your browser-based dashboard can look healthy while your server-side automation is quietly heading toward expiration.
That means:
- your app UI may keep working
- your local test may keep working
- your unattended worker may still die later
If your job is truly headless, a normal user token is often the wrong credential.
The Meta fix: use the token type that matches the job
For unattended Marketing API workflows, a Meta system user token is usually the right fit.
That’s what system users are for: servers and software acting on business assets.
That maps much better to:
- nightly reporting jobs
- campaign sync workers
- budget automation
- AI agents making ad-management calls
- webhook-driven backend processes
Meta token options, bluntly
| Option | What it’s actually good for |
|---|---|
| Meta long-lived user token | Fine for user-authorized app access, but brittle for unattended jobs because it usually expires in about 60 days |
| Meta system user token | Best fit for server-side automation tied to business assets |
| Interactive browser auth | Good for setup and testing, bad proof of headless reliability |
| Headless automation auth | Requires explicit token lifecycle planning because no user is around to rescue it |
Meta setup examples
Exchanging for a long-lived user token looks like this:
curl -i -X GET "https://graph.facebook.com/v23.0/oauth/access_token?grant_type=fb_exchange_token&client_id=$APP_ID&client_secret=$APP_SECRET&fb_exchange_token=$SHORT_LIVED_TOKEN"
Attaching an app to a system user looks like this:
curl -F "business_app=$APP_ID" \
-F "access_token=$ACCESS_TOKEN" \
"https://graph.facebook.com/v23.0/$SYSTEM_USER_ID/applications"
That flow is more annoying than clicking “Continue with Facebook.”
It is also much closer to what a real backend automation actually needs.
One Meta detail that causes pain later
Once a user token has expired, you can’t magically recover it into a fresh long-lived token after the fact.
Usually the user has to log in again.
That’s a terrible property for unattended systems.
If your automation needs to survive without a human nearby, design around that from day one.
The auth model I wish connector UIs explained better
Here’s the version most setup wizards should show before asking you to authorize anything:
| Option | What it’s actually good for |
|---|---|
| Google Ads OAuth user flow | Acting on behalf of users, but still requires a developer token and often a manager header |
| Google Ads service account workflow | Better for accounts you control directly, but still not immune to access and header mistakes |
| Meta long-lived user token | OK for user-driven integrations, weak for unattended server jobs |
| Meta system user token | Better match for backend ad automation |
| Browser success | Proves setup worked |
| Headless success | Proves production auth is actually reliable |
That last distinction matters more than people think.
My default triage order when OAuth works but 401 keeps happening
When I’m debugging one of these, this is the sequence:
1. Is the token type correct for unattended execution?
Examples:
- Meta user token in a server job: suspicious
- Google user OAuth in a managed-account workflow: maybe fine, but verify everything around it
2. Is the request missing non-OAuth auth requirements?
Examples:
- missing Google Ads developer token
- wrong login-customer-id
3. Does the authenticated identity actually have account access?
A valid token with the wrong account permissions is a very normal failure mode.
4. Can the token still refresh?
Examples:
- Google refresh token was revoked or invalidated
- Meta long-lived user token aged out
5. Are you accidentally trusting browser behavior?
This is the big one.
A settings page proving that a human can fetch account data is not the same thing as proving your backend worker can keep doing it for 60 days.
Those are different claims.
What this breaks in real automation stacks
This is bigger than one failed API call.
In real systems, auth brittleness cascades.
A 401 in an ad connector can mean:
- your n8n workflow stops halfway through enrichment
- your Make scenario retries until rate limits become a second problem
- your Zapier automation silently skips campaign updates
- your Airflow DAG backs up downstream jobs
- your agent pipeline starts making decisions on stale data
That’s why I care about this distinction so much.
The cost isn’t the 401 itself.
The cost is the human time spent babysitting a workflow that looked production-ready and wasn’t.
The practical takeaway
If your integration says OAuth works but 401, assume the browser test was easy mode.
For Google Ads:
- treat OAuth as only one part of auth
- include the developer token
- set login-customer-id correctly when using manager accounts
- verify account access and refresh-token health
For Meta Ads:
- stop treating a normal user token as a durable server credential
- use a system user token when the job is truly unattended
- plan for token lifecycle instead of assuming setup success means production reliability
The recurring lesson is simple:
Browser auth proves setup. It does not prove reliability.
If your agents, scheduled workflows, or backend automations need to run all day without supervision, your auth has to be designed for unattended execution.
Otherwise you didn’t build a production system.
You built a demo with a timer on it.
One last thing: if you’re running lots of scheduled LLM workflows, ad automations, or agent pipelines, auth failures are bad enough without also worrying about per-token AI costs every time a job retries or a queue backs up.
That’s one reason I like what Standard Compute is doing: flat-rate, unlimited AI compute through an OpenAI-compatible API, which makes it much easier to run high-volume automations without staring at token bills while you debug the actual workflow. If your stack lives in n8n, Make, Zapier, or custom workers, predictable AI spend is a pretty nice property.
Top comments (0)