Google Data Manager API
The Google Data Manager API destination package provides server-side integration for sending conversion events and audience data to Google's advertising ecosystem through a single unified API endpoint.
Google Data Manager API is a server destination in the walkerOS flow:
Sends conversion events server-side to Google Ads, Display & Video 360, and GA4 through a single unified API.
Installation
npm install @walkeros/server-destination-datamanagerimport { startFlow } from '@walkeros/collector';
import { destinationDataManager } from '@walkeros/server-destination-datamanager';
await startFlow({
destinations: {
datamanager: {
code: destinationDataManager,
config: {
// Service account credentials
credentials: {
client_email: process.env.GOOGLE_CLIENT_EMAIL,
private_key: process.env.GOOGLE_PRIVATE_KEY,
},
settings: {
destinations: [
{
operatingAccount: {
accountId: '123-456-7890',
accountType: 'GOOGLE_ADS',
},
productDestinationId: 'AW-CONVERSION-123',
},
],
},
},
},
},
});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 |
|---|---|---|---|
credentials | credentials | Service account credentials (client_email + private_key). Recommended for serverless environments. (deprecated: use config.credentials) | |
client_email | string | Service account email | |
private_key | string | Service account private key (PEM format) | |
keyFilename | string | Path to service account JSON file. For local development or environments with filesystem access. | |
scopes | Array<string> | OAuth scopes for Data Manager API. Defaults to datamanager scope. | |
destinations | Array<object> | Array of destination accounts and conversion actions/user lists (max 10) | |
eventSource | Event source for all events. Defaults to WEB. Values: WEB, APP, IN_STORE, PHONE, OTHER | ||
batchSize | integer | Maximum number of events to batch before sending (max 2000, like 100) | |
batchInterval | integer | Time in milliseconds to wait before auto-flushing batch | |
validateOnly | boolean | If true, validate request without ingestion (testing mode) | |
url | string | Override API endpoint for testing | |
consent | consent | Request-level consent for all events | |
adUserData | Consent for data collection and use | ||
adPersonalization | Consent for ad personalization | ||
testEventCode | string | Test event code for debugging | |
logLevel | Log level for debugging (debug shows all API calls) | ||
userData | Record<string, any> | Guided helper: User data mapping for all events | |
userId | any | Guided helper: First-party user ID for all events | |
clientId | any | Guided helper: GA4 client ID for all events | |
appInstanceId | any | Guided helper: GA4 app instance ID (Firebase) for all events | |
sessionAttributes | any | Guided helper: Privacy-safe attribution for all events | |
consentAdUserData | string | boolean | Consent mapping: Field name from event.consent (like 'marketing') or static boolean value | |
consentAdPersonalization | string | boolean | Consent mapping: Field name from event.consent (like 'targeting') or static boolean value | |
ip | any | boolean | Client IP, sent as eventDeviceInfo.ipAddress. Resolves against { ingest, event }. Default: ["ingest.ip", "event.user.ip"]. false switches it off. A mapped ipAddress wins. | |
userAgent | any | boolean | Client user agent, sent as eventDeviceInfo.userAgent. Resolves against { ingest, event }. Default: ["ingest.userAgent", "event.user.userAgent"]. false switches it off. A mapped userAgent wins. |
Mapping
This package does not define custom rule-level settings. For the standard rule fields (consent, condition, data, batch, name, policy) see mapping.
Examples
Page view
A page view is sent to Data Manager as a page_view event tied to the walker user id.
{
"name": "page view",
"data": {
"title": "Pricing",
"url": "https://example.com/pricing"
},
"context": {
"dev": [
"test",
1
]
},
"globals": {
"pagegroup": "docs"
},
"custom": {
"completely": "random"
},
"user": {
"id": "visitor-55"
},
"nested": [
{
"entity": "child",
"data": {
"is": "subordinated"
}
}
],
"consent": {
"functional": true
},
"id": "ev-1700000902000",
"trigger": "load",
"entity": "page",
"action": "view",
"timestamp": 1700000902000,
"timing": 3.14,
"source": {
"count": 1,
"trace": "0a1b2c3d4e5f60718293a4b5c6d7e8f9",
"type": "express",
"platform": "server"
}
}{
"name": "page_view",
"data": {
"map": {
"transactionId": "id",
"eventName": {
"value": "page_view"
},
"userId": "user.id"
}
}
}fetch("https://datamanager.googleapis.com/v1/events:ingest", {
"method": "POST",
"headers": {
"Authorization": "Bearer ya29.c.test_token",
"Content-Type": "application/json"
},
"body": "{\"events\":[{\"eventTimestamp\":\"2023-11-14T22:28:22.000Z\",\"transactionId\":\"ev-1700000902000\",\"userId\":\"visitor-55\",\"eventName\":\"page_view\",\"eventSource\":\"WEB\"}],\"destinations\":[{\"operatingAccount\":{\"accountId\":\"123-456-7890\",\"accountType\":\"GOOGLE_ADS\"},\"productDestinationId\":\"AW-CONVERSION-123\"}],\"encoding\":\"HEX\"}"
})Lead
A demo request form submission is sent to Data Manager as a generate_lead conversion with a hashed email.
{
"name": "form submit",
"data": {
"type": "demo-request"
},
"context": {
"dev": [
"test",
1
]
},
"globals": {
"lang": "elb"
},
"custom": {
"completely": "random"
},
"user": {
"email": "prospect@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"
}
}{
"name": "generate_lead",
"data": {
"map": {
"transactionId": "id",
"eventName": {
"value": "generate_lead"
},
"conversionValue": {
"value": 10
},
"currency": {
"value": "USD"
},
"email": "user.email"
}
}
}fetch("https://datamanager.googleapis.com/v1/events:ingest", {
"method": "POST",
"headers": {
"Authorization": "Bearer ya29.c.test_token",
"Content-Type": "application/json"
},
"body": "{\"events\":[{\"eventTimestamp\":\"2023-11-14T22:28:21.000Z\",\"transactionId\":\"ev-1700000901000\",\"userData\":{\"userIdentifiers\":[{\"emailAddress\":\"395ec5f334be0ab5b28568a1e7f6ed5ea80e443fb1ce3d803340586a3df46642\"}]},\"conversionValue\":10,\"currency\":\"USD\",\"eventName\":\"generate_lead\",\"eventSource\":\"WEB\"}],\"destinations\":[{\"operatingAccount\":{\"accountId\":\"123-456-7890\",\"accountType\":\"GOOGLE_ADS\"},\"productDestinationId\":\"AW-CONVERSION-123\"}],\"encoding\":\"HEX\"}"
})Purchase
A completed order is posted to Google Data Manager as a purchase conversion with hashed user identifiers.
{
"name": "order complete",
"data": {
"id": "ORD-600",
"total": 149.99,
"currency": "EUR"
},
"context": {
"shopping": [
"complete",
0
]
},
"globals": {
"pagegroup": "shop"
},
"custom": {
"completely": "random"
},
"user": {
"id": "user-abc",
"email": "buyer@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"
}
}{
"name": "purchase",
"data": {
"map": {
"transactionId": "data.id",
"conversionValue": "data.total",
"currency": {
"key": "data.currency",
"value": "USD"
},
"eventName": {
"value": "purchase"
},
"userId": "user.id",
"email": "user.email"
}
}
}fetch("https://datamanager.googleapis.com/v1/events:ingest", {
"method": "POST",
"headers": {
"Authorization": "Bearer ya29.c.test_token",
"Content-Type": "application/json"
},
"body": "{\"events\":[{\"eventTimestamp\":\"2023-11-14T22:28:20.000Z\",\"transactionId\":\"ORD-600\",\"userId\":\"user-abc\",\"userData\":{\"userIdentifiers\":[{\"emailAddress\":\"6a6c26195c3682faa816966af789717c3bfa834eee6c599d667d2b3429c27cfd\"}]},\"conversionValue\":149.99,\"currency\":\"EUR\",\"eventName\":\"purchase\",\"eventSource\":\"WEB\"}],\"destinations\":[{\"operatingAccount\":{\"accountId\":\"123-456-7890\",\"accountType\":\"GOOGLE_ADS\"},\"productDestinationId\":\"AW-CONVERSION-123\"}],\"encoding\":\"HEX\"}"
})Guided mapping helpers
Define common fields once in Settings instead of repeating them in every event mapping:
await startFlow({
destinations: {
datamanager: {
code: destinationDataManager,
config: {
credentials: { /* ... */ },
settings: {
destinations: [...],
// Guided helpers (apply to all events)
userData: {
email: 'user.mail',
phone: 'data.phone',
},
userId: 'user.id',
clientId: 'user.device', // GA4 web stream
appInstanceId: 'user.appInstanceId', // GA4 app stream (Firebase)
sessionAttributes: 'context.sessionAttributes',
// Consent mapping
consentAdUserData: 'marketing',
consentAdPersonalization: 'personalization',
},
},
},
},
});Consent mapping: Map your consent field names (string) or use static values (boolean). Event mappings always override Settings helpers.
Client IP and user agent
The destination sends the client IP and user agent as eventDeviceInfo.ipAddress
and eventDeviceInfo.userAgent 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 event mapping sets
for ipAddress or userAgent wins; a value that resolves to nothing is not
sent. Set either to false to turn it off:
settings: {
destinations: [...],
ip: false, // never send the client IP
userAgent: 'ingest.userAgent', // request only, ignore event.user
}Simulate
walkeros push --simulate destination.<id> runs offline: the package's mock env
supplies the auth client and fetch, so no token is requested and nothing
reaches Google. The output shows the fetch call with the request body the
destination would send.
Store sales (IN_STORE)
For physical store conversions, set eventSource: 'IN_STORE' on the destination
and map a storeId in the event mapping. The destination wraps it into the
API's required eventLocation.storeId shape.
mapping: {
order: {
complete: {
name: 'purchase',
data: {
map: {
transactionId: 'data.id',
conversionValue: 'data.total',
currency: 'data.currency',
storeId: 'data.storeId',
email: 'user.email', // hashed PII still applies
},
},
},
},
}Hash encoding
Every request is sent with encoding: 'HEX'. Google requires the field
whenever the payload contains hashed userData. The value is pinned because
all hashing in this destination produces lowercase hex SHA-256 digests;
exposing encoding as a setting would risk silently mismatched identifiers.