Communications & Broadcasts
Transactional sends, multi-channel broadcasts, consent management, push registration, and analytics.
Communications (Comms & Broadcasts)
This guide covers the full communications surface of the SDK: transactional sends, multi-channel broadcasts, consent management, push registration, and analytics.
What You Can Do
- Transactional Send — Fire a single targeted message to one contact without creating a broadcast record.
- Broadcasts — Define and enqueue multi-channel campaigns (email, push, SMS, wallet); preview and test before sending.
- Comms Settings — Configure topics and unsubscribe behaviour per collection.
- Consent & Preferences — Record opt-ins/outs and per-subject preferences.
- Method Registration — Register email, SMS, and Web Push contact methods.
- Analytics — Query delivery events, recipient lists, and log custom events.
Transactional Send
Sends a single message to one contact using a template. No broadcast record is created. The send is logged to the contact's communication history with sourceType: 'transactional'.
Endpoint: POST /admin/collection/:collectionId/comm.send
import { comms } from '@proveanything/smartlinks'
import type { TransactionalSendRequest } from '@proveanything/smartlinks'
const result = await comms.sendTransactional('collectionId', {
contactId: 'e4f2a1b0-...',
templateId: 'warranty-update',
channel: 'preferred', // 'email' | 'sms' | 'push' | 'wallet' | 'preferred' (default)
props: { claimRef: 'CLM-0042', decision: 'approved' },
include: {
productId: 'prod-abc123', // hydrate product context into template
appCase: 'c9d1e2f3-...', // hydrate an app case record
},
ref: 'warranty-decision-notification', // arbitrary string for your own tracking
appId: 'warrantyApp',
})
if (result.ok) {
console.log(`Sent via ${result.channel}`, result.messageId)
} else {
console.error('Send failed:', result.error)
}
Request Body
| Field | Type | Description |
|---|---|---|
contactId | string | Required. Contact to send to. |
templateId | string | Required. Template to render. |
channel | 'email' | 'sms' | 'push' | 'wallet' | 'preferred' | Channel to send on. 'preferred' (default) picks the contact's best available channel. |
props | Record<string, unknown> | Extra variables merged into template rendering. |
include | object | Hydration flags — see table below. |
ref | string | Arbitrary reference string stored with the event. |
appId | string | App identifier, used for context/logging. |
include fields:
| Field | Type | Description |
|---|---|---|
collection | boolean | Include collection data in template context. |
productId | string | Hydrate a specific product. |
proofId | string | Hydrate a specific proof. |
user | boolean | Include the requesting user's data. |
appCase | string | Hydrate an app case record by ID. |
appThread | string | Hydrate an app thread record by ID. |
appRecord | string | Hydrate an app record by ID. |
Response
// Success
{ ok: true; channel: 'email' | 'sms' | 'push' | 'wallet'; messageId?: string }
// Failure
{ ok: false; error: string }
Broadcasts
Broadcasts are multi-channel campaigns defined and managed under a collection. Create a broadcast record in the platform first, then use the SDK to preview, test, and enqueue.
Import:
import { broadcasts } from '@proveanything/smartlinks'
import type { BroadcastSendRequest } from '@proveanything/smartlinks'
Preview
Render a broadcast template for a given channel without sending.
const preview = await broadcasts.preview('collectionId', 'broadcastId', {
email: 'user@example.com', // optional: override recipient email for preview
channelOverride: 'push', // force a specific channel
props: { productId: 'prod_123' },
hydrate: true,
include: { product: true, proof: true }
})
// preview.channel — which channel was rendered
// preview.payload / preview.subject / preview.body — channel-specific render output
Test Send
Send a single test message to a contact.
await broadcasts.sendTest('collectionId', 'broadcastId', {
contactId: 'contact_123',
props: { promo: 'JAN' }
})
Enqueue Production Send
Enqueue a background send to all recipients. When channel is omitted the broadcast's own mode setting drives channel selection:
preferred— picks the best single channel per recipient.channels/all— sends on every enabled channel.
const body: BroadcastSendRequest = {
pageSize: 200,
hydrate: true,
include: { product: true, proof: true, user: true }
}
await broadcasts.send('collectionId', 'broadcastId', body)
Manual Send
await broadcasts.sendManual('collectionId', 'broadcastId', { /* overrides */ })
Topic Targeting and Consent
A broadcast must declare a topic under broadcast.data.topic (e.g. newsletter, critical). The topic key must match one defined in your collection's comms settings, and consent enforcement uses it at send time.
Consent resolution order (per recipient, per channel):
preferences._default.topicsByChannel[channel][topic]preferences._default.channels[channel]preferences._default.topics[topic]- Subject-specific preferences (when hydrating a subject context)
Minimal broadcast data shape:
{
"data": {
"topic": "newsletter",
"channelSettings": {
"mode": "preferred",
"channels": [
{ "channel": "email", "enabled": true, "priority": 1, "templateId": "tmpl_newsletter_email" },
{ "channel": "push", "enabled": true, "priority": 2 }
]
}
}
}
Append Recipients and Events
// Append a single delivery event
await broadcasts.append('collectionId', {
broadcastId: 'broadcastId',
contactId: 'contact_123',
channel: 'email',
eventType: 'delivered',
outcome: 'success'
})
// Append bulk events
await broadcasts.appendBulk('collectionId', {
params: { broadcastId: 'broadcastId', channel: 'push' },
ids: ['contact_1', 'contact_2'],
idField: 'contactId'
})
// List recipients
const recips = await broadcasts.recipients('collectionId', 'broadcastId', { limit: 50 })
Admin Comms Settings
Configure unsubscribe behaviour and topics per collection.
Base path: admin/collection/:collectionId/comm.settings
Get Settings
import { comms } from '@proveanything/smartlinks'
const { settings } = await comms.getSettings('collectionId', { includeSecret: true })
// settings.unsub — { requireToken, hasSecret }
// settings.topics — map of topic key → config
Patch Settings
await comms.patchSettings('collectionId', {
unsub: { requireToken: true, secret: '<your-secret>' },
topics: { /* ... */ }
})
To clear the unsubscribe secret, send secret: ''.
Topics Config
Each topic key maps to a config object:
{
"newsletter": {
"label": "Newsletter",
"description": "Occasional updates and stories",
"classification": "marketing",
"defaults": {
"policy": "opt-in",
"byChannel": { "email": "opt-in", "push": "opt-in" },
"channels": { "email": true, "push": true, "sms": false }
},
"rules": {
"allowChannels": ["email", "sms", "push"],
"allowUnsubscribe": true
},
"required": false
},
"critical": {
"label": "Critical Notices",
"classification": "transactional",
"defaults": {
"policy": "opt-out",
"channels": { "email": true, "push": true }
},
"rules": { "allowChannels": ["email", "push"], "allowUnsubscribe": false },
"required": true
}
}
classification:'transactional'or'marketing'— used by the UI when no explicit policy is set.defaults.policy:'opt-in'or'opt-out'— fallback policy for channels not overridden bybyChannel.required: trueandrules.allowUnsubscribe: false— use for critical/mandatory messages.
Unsubscribe Tokens
If unsub.requireToken is true, public unsubscribe calls must include a token query param.
Token generation (topic opt-out):
basis = "${contactId}:${topic}"
token = sha256(basis + ":" + unsub.secret) // hex
Token generation (channel opt-out):
basis = "${contactId}:channel:${channel}"
token = sha256(basis + ":" + unsub.secret)
Browser helper:
async function sha256Hex(input: string): Promise<string> {
const data = new TextEncoder().encode(input)
const hash = await crypto.subtle.digest('SHA-256', data)
return Array.from(new Uint8Array(hash))
.map(b => b.toString(16).padStart(2, '0'))
.join('')
}
const token = await sha256Hex(`${contactId}:${topic}:${secret}`)
Public Comms API
All public endpoints live under public/collection/:collectionId/comm/* and do not require an API key.
Get Topics
const { topics } = await comms.getPublicTopics('collectionId')
Each topic carries UI metadata:
classification:'transactional'|'marketing'defaults.policy:'opt-in'|'opt-out'defaults.byChannel[channel]: per-channel override
Helper to resolve effective policy for a toggle UI:
type Policy = 'opt-in' | 'opt-out'
function effectivePolicy(topic: any, channel: 'email' | 'sms' | 'push' | 'wallet'): Policy {
return (
topic?.defaults?.byChannel?.[channel] ??
topic?.defaults?.policy ??
(topic?.classification === 'marketing' ? 'opt-in' : 'opt-out')
) as Policy
}
Consent (Default)
Record a contact's default channel and topic opt-ins.
await comms.upsertConsent('collectionId', {
contactId: 'contact_123',
channels: { email: true, push: true },
topics: { newsletter: true, critical: true },
topicsByChannel: { email: { newsletter: true }, push: { critical: true } }
})
Preferences (Subject-Specific)
Record consent specific to a product, proof, or other subject. Omit subject to update defaults.
await comms.upsertPreferences('collectionId', {
contactId: 'contact_123',
subject: { type: 'product', id: 'prod_1' },
channels: { email: true },
topics: { updates: true }
})
Subscribe / Unsubscribe (Subject)
Manage subscriptions to a specific subject (e.g. product updates, proof alerts).
// Subscribe
const { subscriptionId } = await comms.subscribe('collectionId', {
contactId: 'contact_123',
subject: { type: 'proof', id: 'prf_1', productId: 'prod_1' },
subscribe: true,
source: 'api'
})
// Unsubscribe (same call, subscribe: false)
await comms.subscribe('collectionId', {
contactId: 'contact_123',
subject: { type: 'proof', id: 'prf_1', productId: 'prod_1' },
subscribe: false,
source: 'api'
})
Check Subscription
const r = await comms.checkSubscription('collectionId', {
contactId: 'contact_123',
subjectType: 'proof',
subjectId: 'prf_1',
productId: 'prod_1'
})
// r.subscribed === true | false
Resolve Subscriptions
Find contacts that are subscribed to a subject using identity hints.
const res = await comms.resolveSubscriptions('collectionId', {
subject: { type: 'product', id: 'prod_1' },
hints: { userId: 'user_1', email: 'user@example.com' }
})
List and Register Methods
// List registered methods for a contact
const { methods } = await comms.listMethods('collectionId', {
contactId: 'contact_123',
type: 'email' // optional filter: 'email' | 'sms' | 'push'
})
// Register email
await comms.registerEmail('collectionId', {
contactId: 'contact_123',
email: 'user@example.com'
})
// Register SMS
await comms.registerSms('collectionId', {
contactId: 'contact_123',
phone: '+12065550100'
})
Unsubscribe (Public Link / One-Click)
await comms.unsubscribe('collectionId', {
contactId: 'contact_123',
topic: 'newsletter',
token: '<sha256-hex-when-requireToken-is-true>'
})
Omit topic to unsubscribe from all. Omit channel to apply across all channels.
Push Registration
Web Push requires a service worker and the browser's Push API.
1. Fetch the VAPID Public Key
import { comms } from '@proveanything/smartlinks'
const { publicKey } = await comms.getPushVapidPublicKey('collectionId')
2. Subscribe in the Service Worker
const registration = await navigator.serviceWorker.ready
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(publicKey),
})
const subJson = subscription.toJSON() as any
3. Register the Method
await comms.registerPush('collectionId', {
contactId: 'contact_123',
endpoint: subscription.endpoint,
keys: subJson?.keys,
meta: { userId: 'user_1' }
})
VAPID Key Utility
function urlBase64ToUint8Array(base64String: string): Uint8Array {
const padding = '='.repeat((4 - (base64String.length % 4)) % 4)
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/')
const rawData = atob(base64)
const output = new Uint8Array(rawData.length)
for (let i = 0; i < rawData.length; i++) output[i] = rawData.charCodeAt(i)
return output
}
Analytics
Query Events by User / Contact
const events = await comms.queryByUser('collectionId', {
userId: 'user_123',
limit: 100
})
Recipient IDs for a Source
const ids = await comms.queryRecipientIds('collectionId', {
broadcastId: 'br_456'
})
Recipients Without an Action
const withoutOpen = await comms.queryRecipientsWithoutAction('collectionId', {
broadcastId: 'br_456',
actionId: 'open'
})
Recipients With an Action
const withClick = await comms.queryRecipientsWithAction('collectionId', {
broadcastId: 'br_456',
actionId: 'click',
includeOutcome: true // returns RecipientWithOutcome[] instead of RecipientId[]
})
Log Events
// Single event
await comms.logCommunicationEvent('collectionId', {
eventType: 'opened',
channel: 'email',
contactId: 'contact_123'
})
// Bulk events (same params applied to all IDs)
await comms.logBulkCommunicationEvents('collectionId', {
params: { broadcastId: 'br_456', channel: 'email', eventType: 'delivered' },
ids: ['contact_1', 'contact_2', 'contact_3'],
idField: 'contactId'
})
End-to-End Admin Workflow
A typical sequence from setup to delivery:
import { comms, broadcasts } from '@proveanything/smartlinks'
// 1. Configure topics and unsubscribe rules
await comms.patchSettings('collectionId', {
unsub: { requireToken: true, secret: process.env.UNSUB_SECRET },
topics: {
newsletter: {
label: 'Newsletter', classification: 'marketing',
defaults: { policy: 'opt-in', channels: { email: true, push: true } },
rules: { allowChannels: ['email', 'push', 'sms'], allowUnsubscribe: true }
}
}
})
// 2. Register contact methods (done at contact creation / profile update)
await comms.registerEmail('collectionId', { contactId: 'c_123', email: 'user@example.com' })
// 3. Record consent (done when user confirms opt-in)
await comms.upsertConsent('collectionId', {
contactId: 'c_123',
channels: { email: true }, topics: { newsletter: true }
})
// 4a. Transactional: send a one-off triggered message
const result = await comms.sendTransactional('collectionId', {
contactId: 'c_123', templateId: 'order-confirmed',
channel: 'email', props: { orderId: 'ORD-001' }
})
// 4b. Broadcast: preview → test → enqueue
const preview = await broadcasts.preview('collectionId', 'br_spring', {
channelOverride: 'email', hydrate: true, include: { product: true }
})
await broadcasts.sendTest('collectionId', 'br_spring', { contactId: 'c_123' })
await broadcasts.send('collectionId', 'br_spring', { pageSize: 200, hydrate: true })
Prerequisites
| Feature | Requirement |
|---|---|
| SendGrid credentials (env or collection comms settings) | |
| Push | VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY env vars |
| SMS | Twilio credentials (env or collection comms settings) |
| Wallet | Google Wallet issuer + service account |
| Hydration | Set hydrate: true and populate include on broadcast/transactional sends |