TypeScript SDK
The official @herald-protocol/sdk for interacting with the Herald Registry and Notification Gateway.
TypeScript SDK
The @herald-protocol/sdk is the easiest way to integrate privacy-preserving notifications into your app. It provides high-level abstractions for registration, identity lookups, channel management, E2E encryption, sending notifications, webhooks, analytics, and scheduled sends.
Installation
npm install @herald-protocol/sdkThe Five Clients
The SDK is organized into specialized clients to ensure security, minimize bundle size, and match different use cases.
1. UserClient
Use for: Frontend applications where the user signs with their own wallet.
- Register identities with encrypted email.
- Update notification preferences.
- Manage opt-in categories.
- Delete identity (GDPR erasure).
import { UserClient } from '@herald-protocol/sdk';
const client = new UserClient({ cluster: 'mainnet-beta' });
// Register a user (requires wallet signature)
await client.registerIdentity({
owner: userPublicKey,
encryptedEmail: ...,
emailHash: ...,
nonce: ...,
optIns: { optInAll: true }
});2. ReadClient
Use for: Quick, signing-free lookups. Safe for both browsers and backends.
- Check if a wallet is registered.
- Fetch user preferences.
- Validate protocol status.
- Batch fetch identity accounts.
const readClient = new ReadClient({ cluster: 'mainnet-beta' });
const isRegistered = await readClient.isRegistered(walletAddress);
// Check if a protocol can send (mirrors on-chain can_send logic)
const canSend = await readClient.checkProtocolCanSend(protocolPubkey);3. Herald (Gateway Client)
Use for: Protocols sending notifications via the Herald API.
- Send messages to individual wallets or in bulk.
- Broadcast to all your subscribers.
- Schedule one-time and recurring notifications.
- Manage webhooks, email templates, and analytics.
- Track ZK on-chain receipt status.
import { Herald } from '@herald-protocol/sdk';
const herald = new Herald({ apiKey: 'hrld_live_...' });
// Send a single notification
const result = await herald.notify({
wallet: '7xR4mKp2nQ...',
subject: 'Liquidation Warning',
body: 'Your health factor dropped to 1.03.',
category: 'defi',
receipt: true,
});
console.log(result.notificationId); // UUID
console.log(result.status); // 'queued'
console.log(result.receiptTx); // null — populated after on-chain write4. NotificationKeyClient
Use for: End-to-end encryption key management for users who want to decrypt notifications client-side.
- Register X25519 keys on-chain.
- Rotate encryption keys.
- Revoke keys.
- Decrypt notifications locally.
import { NotificationKeyClient } from '@herald-protocol/sdk';
const keyClient = new NotificationKeyClient({ cluster: 'mainnet-beta' });
// Derive X25519 keypair from wallet
const keypair = deriveX25519Keypair(walletKeypair);
// Register sealed pubkey on-chain
await keyClient.registerKey({
owner: userPublicKey,
sealedPubkey: sealedKey,
senderPubkey: senderKey,
});
// Decrypt a notification body
const plaintext = await keyClient.decrypt({
encryptedBody: encryptedPayload,
notificationKey: keypair,
});5. ChannelUserClient
Use for: Multi-channel notification delivery setup (Telegram, SMS).
- Register encrypted Telegram chat IDs.
- Register encrypted phone numbers for SMS.
- Enable/disable specific channels.
- Remove channel data (GDPR compliance).
import { ChannelUserClient } from '@herald-protocol/sdk';
const channelClient = new ChannelUserClient({ cluster: 'mainnet-beta' });
// Register Telegram channel
await channelClient.registerTelegram({
owner: userPublicKey,
encryptedChatId: encryptedId,
});
// Register SMS channel
await channelClient.registerSms({
owner: userPublicKey,
encryptedPhone: encryptedPhone,
});Sending Notifications
Single notification
const result = await herald.notify({
wallet: '7xR4mKp2nQ...',
subject: 'Position at risk',
body: 'Your health factor on MarginFi is 1.03.',
category: 'defi', // 'defi' | 'governance' | 'system' | 'marketing' | 'security'
priority: 'critical', // 'normal' | 'important' | 'critical' — critical adds SMS fallback
preferredChannel: 'telegram', // optional channel hint
receipt: true, // write ZK proof of delivery on-chain
idempotencyKey: `liq_${wallet}_${Date.now()}`,
});
console.log(result.notificationId); // UUID
console.log(result.status); // 'queued' | 'opted_out' | 'duplicate' | 'failed'
console.log(result.recipientRegistered); // boolean | null
console.log(result.estimatedDeliveryMs); // number
console.log(result.deliveryChannel); // 'email' | 'telegram' | 'sms' | nullBulk notification (up to 100 wallets)
const result = await herald.notifyBulk({
wallets: ['7xR4...', '9yG2...', 'Kp3n...'],
subject: 'Governance Proposal Live',
body: 'Vote on proposal #42 before it closes.',
category: 'governance',
receipt: true,
idempotencyPrefix: 'prop42', // each wallet gets 'prop42:{wallet}'
});
// result.results is a NotifyResult[] in the same order as wallets
result.results.forEach((r, i) => {
console.log(`Wallet ${i}: ${r.status}`);
});Preview before sending
const preview = await herald.preview({
wallet: '7xR4mKp2nQ...',
subject: 'Your weekly summary',
body: '## Summary\n\nHere are your stats...',
category: 'defi',
templateId: 'tmpl_xxx',
});
console.log(preview.renderedHtml); // full HTML email
console.log(preview.telegramText); // Telegram markdown
console.log(preview.smsText); // plain-text SMSDelivery Status & ZK Receipts
Check notification status
const status = await herald.getStatus(notificationId);
console.log(status.status); // 'delivered' | 'queued' | 'failed' ...
console.log(status.deliveredAt); // ISO datetime or null
console.log(status.receiptStatus); // 'pending' | 'confirmed' | 'failed' | 'disabled'
console.log(status.receiptTx); // Solana tx signature, once confirmed
console.log(status.receiptFailureReason); // human-readable error if receiptStatus is 'failed'
console.log(status.writeReceipt); // whether a receipt was requestedThe receiptStatus lifecycle:
| Value | Meaning |
|---|---|
pending | Notification delivered — receipt not yet written on-chain |
confirmed | ZK receipt landed on Solana (receiptTx is populated) |
failed | On-chain write failed — check receiptFailureReason |
disabled | Receipt was not requested for this notification |
Poll until delivered
// Waits up to 30s, polling every 2s
const final = await herald.waitForDelivery(notificationId, 30_000, 2_000);
console.log(final.status); // 'delivered' | 'failed' | 'opted_out' | 'blocked'
console.log(final.receiptStatus); // 'confirmed' once the on-chain write landsList notifications
const { data, total, page } = await herald.listNotifications({ page: 1, limit: 50 });
data.forEach((n) => {
console.log(n.notification_id, n.status, n.receipt_status);
});Audience & Broadcast
Building your audience
Users are added to your audience in three ways:
- Join link — Share
https://notify.useherald.xyz/join/{protocolId}. Users connect their wallet and opt in via one click. - SDK — Call
herald.subscribe()after the user signs a transaction in your app. - Automatic backfill — Any wallet that already received a notification from you is counted in your audience.
Managing subscriptions
// Subscribe a wallet
await herald.subscribe({
walletAddress: '7xR4mKp2nQ...',
channels: ['email', 'telegram'], // optional, defaults to ['email']
});
// Unsubscribe a wallet
await herald.unsubscribe('7xR4mKp2nQ...');
// Check subscription status
const status = await herald.checkSubscription('7xR4mKp2nQ...');
// { subscribed: true, channels: ['email'], subscribedAt: '2026-05-17T...' }Broadcasting to all subscribers
const result = await herald.broadcast({
subject: 'Governance Vote: Protocol Upgrade v2',
body: 'A governance proposal to upgrade the protocol is live. Vote before May 24.',
category: 'governance',
receipt: false, // receipts default to false for broadcasts
});
console.log(result.broadcast_id); // batch ID
console.log(result.queued_count); // notifications enqueued
console.log(result.total_subscribers); // active subscriber count
console.log(result.estimated_delivery_s); // estimated seconds to completeBroadcast requires Growth tier or above.
Scheduled Notifications
One-time scheduled send
const schedule = await herald.scheduleOnce({
wallet: '7xR4mKp2nQ...',
subject: 'Your staking rewards are ready',
body: 'You have 12.4 SOL in unclaimed staking rewards.',
category: 'defi',
scheduledFor: '2026-06-01T09:00:00Z', // ISO 8601
timezone: 'America/New_York',
});
console.log(schedule.id); // schedule ID
console.log(schedule.status); // 'PENDING'
console.log(schedule.nextRunAt); // when it will fireRecurring notifications via cron
const recurring = await herald.scheduleRecurring({
wallet: '7xR4mKp2nQ...',
subject: 'Weekly portfolio summary',
body: 'Here is your weekly DeFi activity report.',
category: 'defi',
cronExpr: '0 9 * * 1', // every Monday at 09:00 UTC
timezone: 'UTC',
});Managing schedules
// List all scheduled notifications
const { items, total } = await herald.listScheduled({ page: 1, limit: 50 });
// Cancel a schedule
await herald.cancelScheduled(schedule.id);
// { cancelled: true }Webhooks
Registering an endpoint
const webhook = await herald.createWebhook({
url: 'https://myprotocol.com/webhooks/herald',
events: ['notification.delivered', 'notification.failed', 'notification.bounced'],
});
// The secret is shown ONCE — store it securely
console.log(webhook.secret); // 'whsec_xxx...'
console.log(webhook.id); // 'wh_01HX...'Managing webhooks
// List all registered endpoints
const webhooks = await herald.listWebhooks();
// Update events or URL
await herald.updateWebhook(webhook.id, {
events: ['notification.delivered'],
isActive: true,
});
// Send a test event to verify connectivity
await herald.testWebhook(webhook.id);
// Remove an endpoint
await herald.deleteWebhook(webhook.id);Verifying signatures (static method)
import { Herald } from '@herald-protocol/sdk';
// In your webhook handler:
const isValid = await Herald.verifyWebhookSignature(
rawBody, // string | Buffer
request.headers['x-herald-signature'], // 't=...,v1=...'
process.env.HERALD_WEBHOOK_SECRET!,
);
if (!isValid) return new Response('Unauthorized', { status: 401 });Email Templates
Custom templates require Growth tier or above.
// Create a template
const { templateId } = await herald.createEmailTemplate({
name: 'Liquidation Alert',
category: 'defi',
subjectTemplate: 'Your {{protocol}} position is at risk',
htmlSource: '<h1>Alert</h1><p>Health factor: {{healthFactor}}</p>',
textSource: 'Health factor: {{healthFactor}}',
heraldFooter: 'minimal',
});
// Use it when sending
await herald.notify({
wallet: '...',
subject: 'Position at risk',
body: '...',
templateId,
templateVariables: { protocol: 'MarginFi', healthFactor: '1.03' },
});
// List templates
const { data } = await herald.listEmailTemplates();
// Update (creates a new version)
await herald.updateEmailTemplate(templateId, {
htmlSource: '<h1>Updated Alert</h1>...',
});
// Remove a template
await herald.deleteEmailTemplate(templateId);Analytics
Delivery analytics
const analytics = await herald.getAnalytics('30d'); // '7d' | '30d' | '90d'
console.log(analytics.total_sends); // 4521
console.log(analytics.delivery_rate); // 0.993 (99.3%)
console.log(analytics.bounce_rate); // 0.002
console.log(analytics.breakdown); // { delivered, failed, opted_out, bounced }Engagement metrics
const engagement = await herald.getEngagement({
startDate: '2026-05-01',
endDate: '2026-05-31',
});
console.log(engagement.openRate); // percentage as decimal
console.log(engagement.clickRate);
console.log(engagement.unsubscribeRate);Audience insights
const audience = await herald.getAudience();
console.log(audience.totalRegistered); // total Herald-registered wallets
console.log(audience.broadcastableSubscribers); // opted-in to your protocol
console.log(audience.channelCoverage); // { email: 92, telegram: 41, sms: 12 }
console.log(audience.registrationTrend); // [{ date, count }]Protocol info & quota
const protocol = await herald.getProtocol();
console.log(protocol.tier); // 0-3 (Developer → Enterprise)
console.log(protocol.tier_name); // 'Growth'
console.log(protocol.sends_this_period); // usage counter
console.log(protocol.subscription_expires_at);
// Current period usage
const usage = await herald.getUsage();
console.log(usage.remaining); // sends left this period
console.log(usage.overageEnabled); // whether overages are allowedAPI request log
const log = await herald.getRequestLog({
page: 1,
limit: 50,
statusCode: 400, // filter by HTTP status (optional)
endpoint: '/v1/notify', // filter by path substring (optional)
});
log.items.forEach((req) => {
console.log(req.method, req.endpoint, req.statusCode, req.latencyMs + 'ms');
});Encryption Utilities
import {
encryptEmail,
decryptEmail,
hashEmail,
deriveX25519FromEd25519,
deriveX25519Keypair,
sealX25519PubkeyForEnclave,
} from '@herald-protocol/sdk';
// Hash an email for identity verification
const hash = hashEmail('user@example.com', salt);
// Encrypt an email for on-chain storage
const { encrypted, nonce } = encryptEmail(email, userKeypair, authorityPubkey);
// Derive X25519 key from Ed25519 wallet
const x25519Pubkey = deriveX25519FromEd25519(ed25519Pubkey);PDA Utilities
import { findIdentityPda, findProtocolPda, deriveAllIdentityAddresses } from '@herald-protocol/sdk';
const [identityPda] = findIdentityPda(userPublicKey, programId);
const [protocolPda] = findProtocolPda(protocolPubkey, programId);Error Handling
import { HeraldError } from '@herald-protocol/sdk';
try {
await herald.notify({ ... });
} catch (e) {
if (e instanceof HeraldError) {
console.error(e.status); // HTTP status
console.error(e.code); // e.g. 'INSUFFICIENT_QUOTA'
console.error(e.message); // human-readable
}
}Sub-modules
@herald-protocol/sdk/billing— Manage subscriptions and Helio payments.@herald-protocol/sdk/channels— Advanced configuration for Telegram and SMS.@herald-protocol/sdk/encryption— Low-level encryption utilities.@herald-protocol/sdk/events— Real-time event streaming withHeraldEventListener.