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:
| Flow | Trigger | What happens |
|---|---|---|
| A. Resolve ID | Every 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 enrichment | Every event after a successful resolution | Signal 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 personalization | Datafly.js /v1/enrich fetch on page load | Identity-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
| Field | Purpose |
|---|---|
linking_keys | JSON 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_allowlist | Comma-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:
| Source | Where it comes from |
|---|---|
canonical_id | The _dfdid device cookie (stable for 400 days). |
anonymous_id | Datafly anonymous ID (usually same value as canonical_id). |
user_id | Customer-assigned user identifier from identify events. |
email | Plaintext email from event.properties.email (lowercased + trimmed before sending). |
phone | Plaintext 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:
| Cache | TTL | Rationale |
|---|---|---|
| Resolve success | 15 minutes | Amperity’s identity unification runs ~hourly, so 15m gives near-session freshness without hammering the Profile API. |
| Resolve empty (not found) | 5 minutes | First-touch visitors may become identified within a session — retry sooner. |
| Resolve failure (5xx / timeout) | 30 seconds | Short dampening prevents stampedes without hiding real outages. |
| Attributes | 15 minutes | Aligns with positive-resolve TTL; a fresh lookup per session of active browsing. |
| Personalize (context-hashed) | 2 minutes | Context-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_brandThis 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/enrichat 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.