X (Twitter) Conversions API
Server-side event delivery to X's (Twitter's) Conversions API for enhanced ad attribution and privacy-compliant conversion tracking.
X Conversions API is a server destination in the walkerOS flow:
Sends conversion events server-side to X's Conversions API, bypassing browser limitations for improved data quality. Deduplicates with the browser-side X Pixel via `conversion_id`.
Installation
npm install @walkeros/server-destination-twitter- Integrated
- Bundled
import { startFlow } from '@walkeros/collector';
import { destinationTwitter } from '@walkeros/server-destination-twitter';
await startFlow({
destinations: {
twitter: {
code: destinationTwitter,
config: {
settings: {
pixelId: 'YOUR_PIXEL_ID',
eventId: 'tw-xxxxx-xxxxx',
consumerKey: 'YOUR_CONSUMER_KEY',
consumerSecret: 'YOUR_CONSUMER_SECRET',
accessToken: 'YOUR_ACCESS_TOKEN',
accessTokenSecret: 'YOUR_ACCESS_TOKEN_SECRET',
},
},
},
},
});Add to your flow.json destinations:
"destinations": {
"twitter": {
"package": "@walkeros/server-destination-twitter",
"import": "destinationTwitter",
"config": {
"settings": {
"pixelId": "YOUR_PIXEL_ID",
"eventId": "tw-xxxxx-xxxxx",
"consumerKey": "YOUR_CONSUMER_KEY",
"consumerSecret": "YOUR_CONSUMER_SECRET",
"accessToken": "YOUR_ACCESS_TOKEN",
"accessTokenSecret": "YOUR_ACCESS_TOKEN_SECRET"
}
}
}
}Configuration
This destination uses the standard destination config wrapper (consent, data, env, id, ...). For the shared fields see destination configuration. Package-specific fields live under config.settings and are listed below.
Settings
| Property | Type | Description | More |
|---|---|---|---|
pixelId | string | X Pixel ID used in the Conversions API endpoint URL | |
eventId | string | Default pre-registered conversion event ID | |
consumerKey | string | OAuth 1.0a API Key (Consumer Key) for X Ads API | |
consumerSecret | string | OAuth 1.0a API Key Secret (Consumer Secret) | |
accessToken | string | OAuth 1.0a User Access Token | |
accessTokenSecret | string | OAuth 1.0a User Access Token Secret | |
apiVersion | string | X Ads API version number | |
doNotHash | Array<string> | Array of user data fields that should not be hashed | |
url | string | Custom base URL for the X Conversions API endpoint | |
user_data | Record<string, string> | Mapping configuration for user identifiers | |
ip | any | boolean | Client IP, sent as the ip_address identifier. Resolves against { ingest, event }. Default: ["ingest.ip", "event.user.ip"]. false switches it off. A mapped user_data.ip_address wins. | |
userAgent | any | boolean | Client user agent, sent as the user_agent identifier. Resolves against { ingest, event }. Default: ["ingest.userAgent", "event.user.userAgent"]. false switches it off. A mapped user_data.user_agent wins. |
Mapping
Per-event rules under config.mapping. For the standard rule fields (consent, condition, data, batch, name, policy) see mapping.
| Property | Type | Description | More |
|---|---|---|---|
eventId | string | Override the default conversion event ID for this event | |
value | string | number | Conversion monetary value (sent to X as a string) | |
currency | string | ISO 4217 currency code | |
number_items | integer | Number of items in the conversion | |
description | string | Free-text description of the conversion |
Examples
Lead
A form submission is sent to X as a conversion with the SHA-256 hashed email as the identifier.
{
"name": "form submit",
"data": {
"string": "foo",
"number": 1,
"boolean": true,
"array": [
0,
"text",
false
]
},
"context": {
"dev": [
"test",
1
]
},
"globals": {
"lang": "elb"
},
"custom": {
"completely": "random"
},
"user": {
"email": "user@example.com"
},
"nested": [
{
"entity": "child",
"data": {
"is": "subordinated"
}
}
],
"consent": {
"functional": true
},
"id": "ev-1700000901000",
"trigger": "test",
"entity": "form",
"action": "submit",
"timestamp": 1700000901000,
"timing": 3.14,
"source": {
"count": 1,
"trace": "0a1b2c3d4e5f60718293a4b5c6d7e8f9",
"type": "express",
"platform": "server"
}
}sendServer("https://ads-api.x.com/12/measurement/conversions/o8z6j", "{\"conversions\":[{\"conversion_time\":\"2023-11-14T22:28:21.000Z\",\"event_id\":\"tw-o8z6j-o8z21\",\"identifiers\":[{\"hashed_email\":\"b4c9a289323b21a01c3e940f150eb9b8c542587f1abfd8f0e1cc1ffc5e475514\"}],\"conversion_id\":\"ev-1700000901000\"}]}", {
"headers": {
"Authorization": "<OAUTH_SIGNATURE>",
"Content-Type": "application/json"
}
})Purchase
A completed order is sent to the X (Twitter) Conversions API with conversion value and the hashed email.
{
"name": "order complete",
"data": {
"total": 249.99,
"currency": "EUR"
},
"context": {
"shopping": [
"complete",
0
]
},
"globals": {
"pagegroup": "shop"
},
"custom": {
"completely": "random"
},
"user": {
"email": "jane@example.com"
},
"nested": [
{
"entity": "product",
"data": {
"id": "ers",
"name": "Everyday Ruck Snack",
"color": "black",
"size": "l",
"price": 420
},
"context": {
"shopping": [
"complete",
0
]
},
"nested": []
},
{
"entity": "product",
"data": {
"id": "cc",
"name": "Cool Cap",
"size": "one size",
"price": 42
},
"context": {
"shopping": [
"complete",
0
]
},
"nested": []
},
{
"entity": "gift",
"data": {
"name": "Surprise"
},
"context": {
"shopping": [
"complete",
0
]
},
"nested": []
}
],
"consent": {
"functional": true
},
"id": "ev-1700000900000",
"trigger": "load",
"entity": "order",
"action": "complete",
"timestamp": 1700000900000,
"timing": 3.14,
"source": {
"count": 1,
"trace": "0a1b2c3d4e5f60718293a4b5c6d7e8f9",
"type": "express",
"platform": "server"
}
}{
"settings": {
"value": "data.total",
"currency": {
"key": "data.currency",
"value": "EUR"
}
}
}sendServer("https://ads-api.x.com/12/measurement/conversions/o8z6j", "{\"conversions\":[{\"conversion_time\":\"2023-11-14T22:28:20.000Z\",\"event_id\":\"tw-o8z6j-o8z21\",\"identifiers\":[{\"hashed_email\":\"8c87b489ce35cf2e2f39f80e282cb2e804932a56a213983eeeb428407d43b52d\"}],\"conversion_id\":\"ev-1700000900000\",\"value\":\"249.99\"}]}", {
"headers": {
"Authorization": "<OAUTH_SIGNATURE>",
"Content-Type": "application/json"
}
})Purchase with twclid
A purchase is sent to X with both the hashed email and the twclid click id for ads attribution.
{
"name": "order complete",
"data": {
"total": 89.99,
"currency": "USD"
},
"context": {
"twclid": [
"23opevjt88psuo13lu8d020qkn",
0
]
},
"globals": {
"pagegroup": "shop"
},
"custom": {
"completely": "random"
},
"user": {
"email": "buyer@co.com"
},
"nested": [
{
"entity": "product",
"data": {
"id": "ers",
"name": "Everyday Ruck Snack",
"color": "black",
"size": "l",
"price": 420
},
"context": {
"shopping": [
"complete",
0
]
},
"nested": []
},
{
"entity": "product",
"data": {
"id": "cc",
"name": "Cool Cap",
"size": "one size",
"price": 42
},
"context": {
"shopping": [
"complete",
0
]
},
"nested": []
},
{
"entity": "gift",
"data": {
"name": "Surprise"
},
"context": {
"shopping": [
"complete",
0
]
},
"nested": []
}
],
"consent": {
"functional": true
},
"id": "ev-1700000902000",
"trigger": "load",
"entity": "order",
"action": "complete",
"timestamp": 1700000902000,
"timing": 3.14,
"source": {
"count": 1,
"trace": "0a1b2c3d4e5f60718293a4b5c6d7e8f9",
"type": "express",
"platform": "server"
}
}{
"settings": {
"value": "data.total",
"currency": {
"key": "data.currency",
"value": "USD"
}
},
"data": {
"map": {
"user_data": {
"map": {
"twclid": "context.twclid"
}
}
}
}
}sendServer("https://ads-api.x.com/12/measurement/conversions/o8z6j", "{\"conversions\":[{\"conversion_time\":\"2023-11-14T22:28:22.000Z\",\"event_id\":\"tw-o8z6j-o8z21\",\"identifiers\":[{\"hashed_email\":\"484c39bfb51212665d9673805c112b5ba04cbf0460b6d3f00bcdc18b92afed66\"},{\"twclid\":\"23opevjt88psuo13lu8d020qkn\"}],\"conversion_id\":\"ev-1700000902000\",\"value\":\"89.99\"}]}", {
"headers": {
"Authorization": "<OAUTH_SIGNATURE>",
"Content-Type": "application/json"
}
})Client IP and user agent
The destination sends the client IP as the ip_address identifier and the user agent as the user_agent identifier by default. The ip and userAgent settings resolve against { ingest, event } and default to ["ingest.ip", "event.user.ip"] and ["ingest.userAgent", "event.user.userAgent"], so they fill in whenever the source provides them (see the config.ingest recipe on the Express source). A value your mapping sets for user_data.ip_address or user_data.user_agent wins. A value that resolves to nothing is not sent. Set either setting to false to turn it off, or to another mapping value to read it elsewhere.
User identification
The destination builds an identifiers array. Each identifier is a separate single-key object in the payload. At least one primary identifier is required. Events without one are silently skipped.
Primary identifiers:
hashed_email: resolved fromevent.user.emailor auser_data.emailmapping. The email is normalized (trimmed, lowercased) and SHA-256 hashed before sending. SetdoNotHash: ['email']if you are pre-hashing upstream.hashed_phone_number: resolved fromevent.user.phoneor auser_data.phonemapping. SHA-256 hashed. UsedoNotHash: ['phone']to skip.twclid: X click ID (pass-through, not hashed). Typically forwarded from the browser session:
"settings": {
"pixelId": "$env.TWITTER_PIXEL_ID",
"eventId": "$env.TWITTER_EVENT_ID",
"user_data": {
"twclid": "context.twclid"
}
}Secondary identifiers (optional): ip_address, user_agent, pass-through.
Per-event overrides
The default eventId from settings applies to all events. Override it per event via mapping to route different events to different pre-registered conversion events:
"mapping": {
"order": {
"complete": {
"settings": {
"eventId": { "value": "tw-o8z6j-purchase01" },
"value": "data.total",
"currency": { "key": "data.currency", "value": "EUR" },
"number_items": "data.count"
}
}
}
}Browser deduplication
When running both the browser-side X Pixel and this server destination, the walkerOS event id is sent as conversion_id on both sides. X deduplicates conversions that share the same conversion_id and conversion event, so only one conversion is counted.