PactDOCS

Integrate / Identity

Cross-device consent

Apply one authenticated person's compatible choices across approved web, iOS, and Android properties without making a hostname the consent boundary.
01

Create one compatible consent scope

A scope contains one policy version, its purposes, and approved deployment bindings. Use another scope for an unrelated brand or materially different notice.

scope.json
json
{
  "id": "customer-account",
  "tenantId": "acme",
  "name": "Customer account properties",
  "policyVersion": "policy-2026-08",
  "purposes": ["essential", "analytics", "marketing"],
  "bindings": [
    { "platform": "web", "identifier": "app.example.com" },
    { "platform": "ios", "identifier": "com.example.ios" },
    { "platform": "android", "identifier": "com.example.android" }
  ]
}
02

Mint the link credential on your backend

After customer authentication, derive a tenant-scoped opaque subject and call Pact with the tenant's server-only link key. Never place that key or a raw identity in browser or app code.

pact-link.ts
typescript
const opaqueSubject = createHmac('sha256', customerIdentitySecret)
  .update(authenticatedUser.internalId)
  .digest('base64url')

const response = await fetch('https://consent.example.com/v1/consent/link-credentials', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    Authorization: `Bearer ${process.env.PACT_LINK_KEY}`,
  },
  body: JSON.stringify({ consentScopeId: 'customer-account', opaqueSubject }),
})

return response.json() // credential expires after five minutes
03

Exchange from an approved deployment

The device exchanges the short-lived credential for a 24-hour consent session. On the first merge, denial wins and GPC remains restrictive.

linked-consent.ts
typescript
import { createPactLinkedConsentClient } from '@pact/web/linked'

const linked = createPactLinkedConsentClient({
  tenantId: 'acme',
  consentScopeId: 'customer-account',
  policyVersion: 'policy-2026-08',
  purposes: ['essential', 'analytics', 'marketing'],
})

await linked.link(linkCredentialFromYourAuthenticatedBackend)
await linked.refresh()
04

Know the boundary

  • Anonymous decisions never leave the device before linking
  • Only deployment bindings approved on the scope can exchange or use a session
  • A policy-version mismatch fails closed instead of silently applying incompatible choices
  • Native and web changes append to the same SHA-256 evidence chain
UnderstandInspect the canonical evidence model