Skip to main content
Ask your AI

Google Data Manager API

Server Source code Package

The Google Data Manager API destination sends conversion events from your server to Google Ads, Display & Video 360 and Google Analytics 4 through one API endpoint.

Where this fits

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/collector @walkeros/server-destination-datamanager
import { startFlow } from '@walkeros/collector';
import destinationDatamanager from '@walkeros/server-destination-datamanager';

await startFlow({
  destinations: {
    datamanager: {
      code: destinationDatamanager,
      config: {
        settings: {
          destinations: [
            {
              operatingAccount: {
                accountId: '123-456-7890',
                accountType: 'GOOGLE_ADS',
              },
              productDestinationId: 'AW-CONVERSION-123',
            },
          ],
        },
      },
    },
  },
});

The destination authenticates with Application Default Credentials: the GOOGLE_APPLICATION_CREDENTIALS environment variable, or the metadata server on Google Cloud. To use a service account instead, set config.credentials to its client_email and private_key, or settings.keyFilename to the path of its key file. If no credentials are found, the destination does not start: every push answers failed with the code config, and the collector logs destination init failed.

The destination sends an event only when its mapped data carries a transactionId; any other event fails with the code missing-input and one ERROR line (transactionId is required). The quickstart maps none, so add one, for example data: { map: { transactionId: 'id' } } in config to use the walkerOS event ID. For an order that the Google tag also reports, map the order ID the tag sends instead, like data.id in the order's rule: Google deduplicates the two by transactionId within one conversion action, so the values must match (additional data sources). With a Google Analytics destination (accountType: 'GOOGLE_ANALYTICS_PROPERTY'), an event without an eventName fails the same way.

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​

PropertyTypeDescriptionMore
credentialscredentialsService account credentials (client_email + private_key). Prefer config.credentials; set here, they are used and the destination logs a WARN line.
client_email*stringService account email
private_key*stringService account private key (PEM format)
keyFilenamestringPath to service account JSON file. For local development or environments with filesystem access.
scopesArray<string>OAuth scopes for Data Manager API. Defaults to datamanager scope.
destinations*Array<object>Array of destination accounts and conversion actions (max 10)
eventSourceEvent source for all events. Defaults to WEB. Values: WEB, APP, IN_STORE, PHONE, OTHER
batchSizeintegerHas no effect: every event is sent in its own request (accepts 1 to 2000)
batchIntervalintegerHas no effect: every event is sent in its own request (accepts milliseconds, like 5000)
validateOnlybooleanIf true, validate request without ingestion (testing mode)
urlstringOverride API endpoint for testing
consentconsentRequest-level consent, sent with every request and applied to each event that carries no consent of its own
adUserDataConsent for data collection and use
adPersonalizationConsent for ad personalization
testEventCodestringTest event code for debugging
userDataRecord<string, any>Guided helper: User data mapping for all events
userIdanyGuided helper: First-party user ID for all events
clientIdanyGuided helper: GA4 client ID for all events
appInstanceIdanyGuided helper: GA4 app instance ID (Firebase) for all events
sessionAttributesanyGuided helper: Privacy-safe attribution for all events
consentAdUserDatastring | booleanConsent mapping: Field name from event.consent (like 'marketing') or static boolean value
consentAdPersonalizationstring | booleanConsent mapping: Field name from event.consent (like 'targeting') or static boolean value
ipany | booleanClient IP, sent as eventDeviceInfo.ipAddress. Resolves against { ingest, event }. Default: ["ingest.ip", "event.user.ip"]. false switches it off. A mapped ipAddress wins.
userAgentany | booleanClient user agent, sent as eventDeviceInfo.userAgent. Resolves against { ingest, event }. Default: ["ingest.userAgent", "event.user.userAgent"]. false switches it off. A mapped userAgent wins.
* Required fields

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.

Event
{
  "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"
  }
}
Mapping
{
  "name": "page_view",
  "data": {
    "map": {
      "transactionId": "id",
      "eventName": {
        "value": "page_view"
      },
      "userId": "user.id"
    }
  }
}
Out
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.

Event
{
  "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"
  }
}
Mapping
{
  "name": "generate_lead",
  "data": {
    "map": {
      "transactionId": "id",
      "eventName": {
        "value": "generate_lead"
      },
      "conversionValue": {
        "value": 10
      },
      "currency": {
        "value": "USD"
      },
      "email": "user.email"
    }
  }
}
Out
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.

Event
{
  "name": "order complete",
  "data": {
    "id": "ORD-600",
    "value": 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"
  }
}
Mapping
{
  "name": "purchase",
  "data": {
    "map": {
      "transactionId": "data.id",
      "conversionValue": "data.value",
      "currency": {
        "key": "data.currency",
        "value": "USD"
      },
      "eventName": {
        "value": "purchase"
      },
      "userId": "user.id",
      "email": "user.email"
    }
  }
}
Out
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\"}"
})

When it fails

  • order without transaction id: An order complete event without a transaction id cannot be sent to the Data Manager API: the push fails with missing-input instead of counting as delivered. Reported as failed with code missing-input.

Failures​

A response outside the 2xx range, a 2xx answer that reports validation errors or cannot be read, a network error, a failed token request, or no answer within the collector's 10-second timeout fails the event with the code transport or timeout: the event goes to the dead-letter queue and the collector logs one ERROR line without the response body. A rule with silent: true sends nothing.

Guided mapping helpers​

Define common fields once in Settings instead of repeating them in every event mapping. Event mappings always override Settings helpers.

await startFlow({
  destinations: {
    datamanager: {
      code: destinationDatamanager,
      config: {
        credentials: { /* ... */ },
        settings: {
          destinations: [...],

          // Guided helpers (apply to all events)
          userData: {
            email: 'user.email',
            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: a string names a key of the event's consent, a boolean is a static value. The collector hands a destination only the consent states an event grants, so a string sends CONSENT_GRANTED when the event grants that key and nothing for it when the event denies it: Google never receives CONSENT_DENIED from an event's consent. A static false sends CONSENT_DENIED. The request-level settings.consent fills in for every event that carries no consent of its own, so with settings.consent set to CONSENT_GRANTED, an event whose consent denies the mapped key is sent as granted.

User data​

The destination reads these keys from the mapped data (Settings helpers, then config.data, then the rule's data):

  • email, phone, firstName and lastName: normalized and SHA-256 hashed.
  • regionCode and postalCode: sent unhashed, as Google requires.
  • userId, clientId, appInstanceId, gclid, gbraid, wbraid and sessionAttributes: sent as mapped.
  • transactionId, conversionValue, currency, eventName, cartData, storeId, eventSource, adUserData, adPersonalization, ipAddress and userAgent: the event's conversion, consent and device fields, not hashed.

Any other key, a list, and a user_data or userData object in a rule's data are dropped without a log line. There is no doNotHash: a value that is already a hash is hashed again.

Normalization follows Google's formatting rules with these gaps today: for gmail.com and googlemail.com addresses dots are removed but a +alias is kept; spaces inside an email are kept; a family name ending in v, iv, ii or iii without a space before it loses those letters (Ivanov is hashed as ivano); and a phone number without a leading + gets +1 when it has 10 digits or fewer, or only a + otherwise. Map phone numbers in E.164 format (+491701234567).

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
}

The quickstart sets no config.consent, so Google receives every event with its user data, whatever consent the event carries, and a denial is not passed on (see Consent mapping above). To send only events with consent, add consent: { marketing: true } to config, next to settings (with several keys listed, one granted key is enough). An event without that consent is skipped or queued, depending on the collector's consentDenied. Server flows built by the CLI skip it and count it in skipped. With startFlow it waits in the destination's queue by default: a later event that carries consent does not release it, and only a walker consent command does, which then sends the waiting events of every user at once. Pass consentDenied: 'skip' to startFlow to skip such events instead (see Gate a destination with consent and consent-denied events on a server).

Simulate​

walkeros push --simulate destination.<id> uses the package's mock env for 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.value',
          currency: 'data.currency',
          storeId: 'data.storeId',
          email: 'user.email', // hashed PII still applies
        },
      },
    },
  },
}
💡 Need implementation support?
elbwalker offers hands-on support: setup review, measurement planning, destination mapping, and live troubleshooting. Book a 2-hour session (€399)