X Ads API returns 403? The Ads Starter plan is MCP-only — connecting X Ads in 2026
The X Ads API returning an empty 403 on a valid token is usually not a permissions bug: on X's Ads Starter plan the developer console grants the Ads product as MCP access only. Our first live connect got a 403 from the REST Ads API and 74 tools from X's Ads MCP server with the same token.
What we measured
- OAuth 2.0 authorization code with PKCE worked. The token exchange at
api.x.com/2/oauth2/tokensucceeded with the confidential client's Basic auth. GET ads-api.x.com/12/accountsreturned HTTP 403 with an empty body. No error code, no message — the hardest kind of 403 to debug.- The developer console explained it. The Ads product sits on the "Ads Starter" plan, which X describes as MCP access only.
ads-api.x.com/mcpanswered the same token and listed 74 tools.- The test login had no ads account, so everything after discovery — creates, budgets, stats — is described below from the server's own tool definitions, not from a live run.
Getting the token right
- Ask for
offline.access. Without it there is no refresh token and the grant dies in about two hours. The full set isads.read,ads.write,offline.access. - PKCE S256 even for a confidential client. We derive the verifier from a nonce and a server key instead of storing it, so nothing sits in a cookie or a database between the redirect and the callback.
- Refresh tokens rotate. Each renewal spends the previous refresh token; if two places hold a copy of the same grant, the first to refresh breaks the other. Write the new pair to the account first, then to anything sharing the grant.
Talking to X's Ads MCP server
- Streamable HTTP, stateless. A
tools/callneeds no initialize and no session id; responses come back framed as server-sent events. - A 403 there means the developer app is not enrolled in the Ads project (console → Projects → Ads → Manage) — worth saying in the error, because the empty REST 403 says nothing.
- Discovery still needs a call: list the ads accounts the login can reach, and keep every later call bound to the one the user picked — an id from another account answers "not found".
The ad model, which is not Google's or Meta's
- A campaign has no budget. It is a name and a funding instrument. Daily and total budget, bid, schedule, pacing and placements live on the line item, and creating a line item requires a daily budget and a start time.
- Pausing and activating are different operations. An update can pause a campaign but cannot activate it; activation is a separate call, and a line item serves only when its campaign is active too.
- There is no campaign or line-item delete. Pause is the stop. The deletes the server does offer are for audiences, cards, tags and media.
- Removing the last targeting criterion widens delivery. A line item with no criteria serves broadly, so a removal is effectively a spend decision.
- Stats: at most seven days and twenty ids per request, placements ALL_ON_TWITTER or PUBLISHER_NETWORK, days in the ads account's time zone. A month-long report is five requests merged.
- Money in micro-units (
*_amount_local_micro). Convert once.
How we wired it
In Agent Planners, X Ads has 9 reads and 8 approval-gated writes over that server: typed tools for accounts, funding, campaigns, line items, promoted posts, targeting and stats, plus get for any other read by name and mutate for any other write by name from an allow-list. Creates start paused, activation goes through the same money check as a budget raise, and the server's permanent deletes and removing a targeting criterion always wait for a person.
Frequently asked questions
- Why does the X Ads API return 403 with an empty body?
- On X's Ads Starter plan the developer console grants the Ads product as MCP access only. In our test a valid OAuth 2.0 token got an empty 403 from ads-api.x.com/12/accounts and a full tool list from X's Ads MCP server at ads-api.x.com/mcp.
- Which OAuth scopes does the X Ads API need?
- ads.read and ads.write, plus offline.access so the grant includes a refresh token — without it the access expires in about two hours.
- Where do you set the budget on an X Ads campaign through the API?
- On the line item. A campaign carries a name and a funding instrument only; daily and total budget, bid, schedule and placements are line-item fields.
- Can you delete an X Ads campaign through the Ads MCP?
- No. X's Ads MCP server has no campaign or line-item delete; pausing is the stop.
- How much X Ads stats data can one request return?
- Seven days and twenty entity ids per request. Longer ranges or more entities have to be split and merged, in the ads account's time zone.