How to Integrate Twitter (X) Ads with Your Headless CMS
Connect Twitter (X) Ads to your structured content so campaign copy, landing page URLs, and creative metadata can move from publish to paid distribution without copy-paste work.
What is Twitter (X) Ads?
Twitter (X) Ads is the paid advertising platform for X, where teams run promoted posts, video ads, follower campaigns, website traffic campaigns, app campaigns, and audience targeting. It gives marketers API access to accounts, campaigns, line items, promoted posts, creatives, audiences, and reporting. Growth, performance, and brand teams use it when they want paid placement inside X timelines, search results, profiles, and conversation surfaces.
Why integrate Twitter (X) Ads with a headless CMS?
Paid social teams often build the same campaign assets twice. A product launch page goes live with a headline, short description, image, offer URL, UTM parameters, and compliance copy. Then someone re-enters that same information into Twitter (X) Ads, or pastes it into a spreadsheet for a media buyer. That works for 3 ads. It breaks down at 300 localized creatives across 12 markets.
Architecture overview
A typical integration starts when an editor publishes an ad creative document in Sanity Studio. A GROQ-powered webhook matches documents where _type == "xAdCreative" and status == "approved", then sends the document ID to a Sanity Function or webhook listener. The function uses @sanity/client to fetch the latest fields from the Content Lake with GROQ, including referenced campaign, locale, image, landing page, and UTM data. It then signs a request to the Twitter (X) Ads API with OAuth 1.0a and calls the account endpoint, for example POST /12/accounts/:account_id/tweet to create a promoted-only post, followed by POST /12/accounts/:account_id/promoted_tweets to attach that post to a line item. After Twitter (X) Ads accepts the creative, the ad can enter the platform’s normal review and delivery flow, and the end user sees the paid post in X placements based on the campaign’s targeting and budget settings.
Common use cases
Launch paid social with product releases
Publish a launch page in Sanity, then create matching promoted-only posts in Twitter (X) Ads with the same headline, URL, and campaign image.
Sync localized ad creatives
Send approved English, Spanish, French, and Japanese copy to the right X line items without rebuilding each ad by hand.
Keep offer and pricing copy consistent
When an editor updates a sale end date, disclaimer, or product price, trigger a new creative sync instead of leaving stale ad copy live.
Connect content metadata to campaign reporting
Attach Sanity document IDs, campaign slugs, and UTM values to ad URLs so performance reports can be tied back to specific content variants.
Step-by-step integration
- 1
Set up Twitter (X) Ads API access
Create or use an X Ads account, confirm it has an active funding source, and apply for developer access with Ads API permissions. In the X Developer Portal, create an app, generate the API key, API secret, access token, and access token secret, then note the ads account ID, campaign ID, and line item ID you’ll sync creatives into.
- 2
Install the integration dependencies
In your Sanity Function, Next.js route, or Node webhook listener, install @sanity/client, oauth-1.0a, and a supported fetch runtime. Twitter (X) Ads API requests use OAuth 1.0a signing, so keep tokens in environment variables, not in Sanity documents.
- 3
Model Twitter (X) Ads creatives in Sanity Studio
Create a schema such as xAdCreative with fields for headline, body, destinationUrl, image, locale, status, campaignSlug, xAccountId, xLineItemId, and utmCampaign. Keep budget, bid strategy, and audience targeting in Twitter (X) Ads unless your media team explicitly wants those fields controlled from code.
- 4
Create the publish trigger
Add a GROQ-powered webhook that fires only for approved ad creative documents, for example _type == "xAdCreative" && status == "approved". Send a small payload such as { "id": _id } to a Sanity Function or webhook route, then fetch the full document inside the handler.
- 5
Call the Twitter (X) Ads API
Use GROQ to fetch the exact fields Twitter (X) Ads needs, sign the Ads API request, create a promoted-only post with POST /12/accounts/:account_id/tweet, and attach it to a line item with POST /12/accounts/:account_id/promoted_tweets. Store returned X IDs back on the Sanity document if your team needs audit history.
- 6
Test the full publishing path
Start with a test line item and a small daily budget. Publish one creative from Sanity Studio, confirm the promoted post appears in Twitter (X) Ads, check review status, validate UTM parameters on the landing page, and confirm updates don’t create duplicate live ads unless that’s your intended behavior.
Code example
import { createClient } from '@sanity/client'
import OAuth from 'oauth-1.0a'
import crypto from 'node:crypto'
const sanity = createClient({
projectId: process.env.SANITY_PROJECT_ID!,
dataset: process.env.SANITY_DATASET!,
apiVersion: '2025-02-19',
token: process.env.SANITY_READ_TOKEN!,
useCdn: false
})
const oauth = new OAuth({
consumer: { key: process.env.X_API_KEY!, secret: process.env.X_API_SECRET! },
signature_method: 'HMAC-SHA1',
hash_function(base, key) {
return crypto.createHmac('sha1', key).update(base).digest('base64')
}
})
async function xAdsPost(accountId: string, path: string, data: Record<string, string>) {
const url = `https://ads-api.x.com/12/accounts/${accountId}${path}`
const auth = oauth.toHeader(oauth.authorize({ url, method: 'POST', data }, {
key: process.env.X_ACCESS_TOKEN!,
secret: process.env.X_ACCESS_TOKEN_SECRET!
}))
const res = await fetch(url, {
method: 'POST',
headers: { ...auth, 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams(data)
})
if (!res.ok) throw new Error(`X Ads API error ${res.status}: ${await res.text()}`)
return res.json()
}
export default async function handler(req: Request) {
const { id } = await req.json()
const ad = await sanity.fetch(`*[_id == $id][0]{
headline,
destinationUrl,
xAccountId,
xLineItemId,
"utm": utmCampaign
}`, { id })
const url = `${ad.destinationUrl}?utm_source=x&utm_medium=paid_social&utm_campaign=${ad.utm}`
const tweet = await xAdsPost(ad.xAccountId, '/tweet', {
text: `${ad.headline} ${url}`,
nullcast: 'true'
})
await xAdsPost(ad.xAccountId, '/promoted_tweets', {
line_item_id: ad.xLineItemId,
tweet_ids: tweet.data.id
})
return new Response(JSON.stringify({ ok: true, tweetId: tweet.data.id }))
}How Sanity + Twitter (X) Ads works
Build your Twitter (X) Ads integration on Sanity
Sanity gives you the AI Content Operating System, structured content foundation, real-time event system, and flexible APIs to connect approved campaign content with Twitter (X) Ads.
Start building free →CMS approaches to Twitter (X) Ads
| Capability | Traditional CMS | Sanity |
|---|---|---|
| Creative data structure | Ad copy often lives inside pages, rich text fields, or plugin-specific records, so extraction usually needs custom parsing. | Campaign creative, locale, product, image, and legal copy can be modeled as schema-defined JSON in the Content Lake. |
| Sync timing | Syncs often run on schedules or depend on manual exports, which can leave Twitter (X) Ads with old copy. | GROQ-powered webhooks and Functions can trigger on specific publish events and run the Twitter (X) Ads sync server-side. |
| Field-level control | The integration may receive entire pages or rendered content, then strip out the pieces needed for ads. | GROQ can fetch the exact headline, URL, locale, image, product, and line item metadata in one query. |
| Editorial and media team boundaries | Editors and media buyers may work in separate tools with spreadsheets as the handoff layer. | Sanity Studio can include approval status, comments, tasks, and campaign fields while Twitter (X) Ads remains the place for budget and targeting. |
| Multi-channel reuse | Website content and ad content often diverge because each channel has its own fields and workflows. | One structured back end can feed web, mobile, Twitter (X) Ads, reporting pipelines, and AI agents through Agent Context. |
Keep building
Explore related integrations to complete your content stack.
Sanity + Google Ads
Sync approved landing page copy, product metadata, and campaign URLs from Sanity into Google Ads workflows.
Sanity + Meta Ads
Use structured campaign content from Sanity to keep Meta ad creatives aligned with product launches and localized offers.
Sanity + Mailchimp
Reuse the same approved campaign copy for paid social and email so launch messaging stays consistent across channels.