Herald LogoHerald Docs
Sdk

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/sdk

The 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 write

4. 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' | null

Bulk 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 SMS

Delivery 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 requested

The receiptStatus lifecycle:

ValueMeaning
pendingNotification delivered — receipt not yet written on-chain
confirmedZK receipt landed on Solana (receiptTx is populated)
failedOn-chain write failed — check receiptFailureReason
disabledReceipt 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 lands

List 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:

  1. Join link — Share https://notify.useherald.xyz/join/{protocolId}. Users connect their wallet and opt in via one click.
  2. SDK — Call herald.subscribe() after the user signs a transaction in your app.
  3. 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 complete

Broadcast 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 fire

Recurring 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 allowed

API 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 with HeraldEventListener.

On this page