IdentityPartner Enrichment (CDPs)

Server-Side Partner Enrichment

CDP and identity partners like Amperity, Acxiom, UID2, EUID, and Neustar Fabrick require server-to-server API calls to resolve visitors and fetch their profile data. Signal integrates them through a single abstraction — partnerenrich — so every partner plugs into the same three flows:

FlowTriggerWhat happens
A. Resolve IDEvery event during processing (Redis-cached)Signal maps the visitor’s _dfdid (or other linking key) to the partner’s internal ID. Result stored in Redis + attached to event.Context.VendorIDs.
B. Attribute enrichmentEvery event after a successful resolutionSignal fetches the partner’s profile attributes and attaches them to event.Context.PartnerAttrs.<provider>. Every downstream destination (GA4, Meta, TikTok, etc.) can read these via blueprint paths.
C. Browser personalizationDatafly.js /v1/enrich fetch on page loadIdentity-hub returns the allowlisted subset of the cached attributes. Zero partner credentials ever touch the browser.

Why this is different from pixel-style syncs

The Server-Proxied Enrichment page describes a browser-initiated pattern (browser asks the gateway to do a vendor lookup, result cached for future events). Partner enrichment is server-initiated — it happens on every event during processing, using the pipeline’s Redis cache to keep partner API traffic bounded. The browser just reads what’s already cached.

Use partner enrichment when the vendor:

  • Offers profile attributes (Persona, LTV, segments) in addition to or instead of just IDs
  • Is expected to participate in every event (not just a one-time resolution)
  • Needs to be queryable from the browser for client-side personalization

Use the older pixel-style pattern when the vendor:

  • Is a pure identity graph (return an envelope/token, no attributes)
  • Only needs resolving once per session

Architecture

                                  ┌─────────────────────────────┐
                                  │  Partner APIs               │
                                  │  • Amperity Profile API     │
                                  │  • Acxiom RealID            │
                                  │  • UID2 Operator            │
                                  │  • Neustar Fabrick          │
                                  └──────────────┬──────────────┘
                                                 │ (A) resolve + (B) fetch attrs

┌──────────┐  fetch profile (C) ┌────────────────┴─────────────────┐
│ Browser  │───────────────────▶│ identity-hub                     │
│          │                    │  └ GET /v1/enrich                │
│ datafly  │◀───────────────────│     reads partnerenrich:         │
│   .js    │   filtered attrs   │       browser_attrs:{org}:{dfid} │
│          │                    └──────────────▲───────────────────┘
│window.   │                                   │ reads
│  datafly │                    ┌──────────────┴───────────────────┐
│  .profile│                    │ event-processor                  │
└──────────┘                    │  └ org-layer partner enricher    │
                                │     1. ResolveID (cached 15m)    │
                                │     2. FetchAttributes (cached)  │
                                │     3. Filter + write Redis      │
                                │     4. Attach to event.Context   │
                                │        .PartnerAttrs.<provider>  │
                                └──────────────────────────────────┘

Configuration

Partner enrichment is configured per integration in the Signal UI. All settings flow from pipeline_versions.parameters → configwatcher → event-processor via the WatcherPartnerConfigSource.

Required fields (per partner)

Every partner has its own required credentials — see the individual integration pages:

  • Amperity — tenant_subdomain, tenant_id, profile_api_key, profile_collection_id
  • Acxiom — coming soon
  • UID2 — coming soon

Shared fields across all partners

FieldPurpose
linking_keysJSON array describing which Datafly identifiers Signal tries when resolving the visitor. Each entry has {source, key, hash}. Signal iterates in order until one matches.
browser_attribute_allowlistComma-separated list of attribute names safe to expose to datafly.js via /v1/enrich. Empty = nothing exposed (browsers cannot widen this).

Linking key sources

The source field in each linking_keys entry picks a value from Signal’s KnownIDs envelope:

SourceWhere it comes from
canonical_idThe _dfdid device cookie (stable for 400 days).
anonymous_idDatafly anonymous ID (usually same value as canonical_id).
user_idCustomer-assigned user identifier from identify events.
emailPlaintext email from event.properties.email (lowercased + trimmed before sending).
phonePlaintext phone from event.properties.phone.

The hash field optionally applies sha256 (lowercased-trimmed) to the value — use this for partners that require hashed PII linking keys.

Caching

Each flow has its own Redis TTL tuned for the cost/freshness trade-off:

CacheTTLRationale
Resolve success15 minutesAmperity’s identity unification runs ~hourly, so 15m gives near-session freshness without hammering the Profile API.
Resolve empty (not found)5 minutesFirst-touch visitors may become identified within a session — retry sooner.
Resolve failure (5xx / timeout)30 secondsShort dampening prevents stampedes without hiding real outages.
Attributes15 minutesAligns with positive-resolve TTL; a fresh lookup per session of active browsing.
Personalize (context-hashed)2 minutesContext-sensitive responses change more frequently; shorter window accepts a tiny latency cost for tighter correctness.

All cache keys are scoped by {provider}:{org}:{canonical_id} — multi-tenant and side-by-side providers don’t collide.

Using partner attributes in other destinations

Once a partner has attached attributes, any destination’s blueprint can reference them via the partner_attrs.<provider>.<attr> source path:

global:
  mappings:
    - source: partner_attrs.amperity.Persona
      target: user_persona
    - source: partner_attrs.amperity.LifetimeTier
      target: user_tier
    - source: partner_attrs.amperity.FavoriteBrand
      target: user_favorite_brand

This lets GA4 / Meta / TikTok receive enriched Amperity data on every event without each integration doing its own Amperity API calls.

Browser personalization

Datafly.js auto-hydrates window.datafly.profile.<provider> on page load. The page can gate render on the fetch completing:

await datafly.onProfileReady();
const persona = datafly.profile.amperity?.Persona;
if (persona === 'Value Hunter') {
  document.querySelector('#hero').dataset.variant = 'promo';
}
 
// Optional — refresh after an identify to pick up richer attributes.
datafly.identify('user-123', { email: '[email protected]' });
await datafly.refreshProfile();

See the Amperity integration guide for a complete example.

Credential security

  • Partner API tokens live server-side only. They’re stored encrypted in integration_instance.config (AES-256-GCM) and decrypted in-memory by the configwatcher. Never serialised to the browser, Kafka, or logs.
  • Browser allowlist is enforced server-side. Attribute filtering happens in the event-processor before caching for browser consumption. An untrusted page cannot escalate to full profile.
  • Per-visitor rate limiting. Identity-hub rate-limits /v1/enrich at 10 req/minute per canonical_id to prevent scraping via stolen cookies.
  • Consent-gated (optional). Future work: require a marketing-consent signal before returning attributes to the browser.

Extending to a new partner

Adding a partner to the partnerenrich system takes a single Provider implementation:

Implement the Provider interface

Create shared/partnerenrich/<partner>/ implementing:

type Provider interface {
    Key() string
    ResolveID(ctx, cfg, known KnownIDs) (string, error)
    FetchAttributes(ctx, cfg, partnerID string) (map[string]any, error)
    Personalize(ctx, cfg, partnerID string, contextValues map[string]any) (map[string]any, error)
}

Return ErrNotSupported from any operation the partner doesn’t expose.

Register in event-processor main.go

partnerRegistry.Register(partnerenrich.NewCache(
    mypartner.New(nil),
    redisAdapter{c: redisClient},
))

Add to the watcher’s vendor allowlist

In event-processor/internal/configwatcher/watcher.go:

var partnerEnrichVendorAllowlist = map[string]struct{}{
    "amperity":  {},
    "acxiom":    {},
    "my_partner": {},  // new
}

Add to identity-hub’s enrich handler allowlist

In identity-hub/internal/enrich/enrich.go:

func isKnownProvider(key string) bool {
    switch key {
    case "amperity", "acxiom", "my_partner":
        return true
    }
    return false
}

Ship an integration catalog entry

Add management-api/internal/catalog/data/cdp/my_partner.json with the config fields the customer needs (credentials + linking_keys + browser_attribute_allowlist).

Everything else — caching, config resolution, event-time enrichment, browser fetch, blueprint path support — is automatic.