Getting Started

Choose your path:

Use NotifyHub with Claude, Cursor, or any AI assistant

No programming required. Just download, setup, and start talking to your AI.

Step 1: Download

Download the NotifyHub MCP executable for your platform:

Download
# Windows
https://github.com/GabrielBBaldez/notify-hub/releases/latest

# Or if you have Java installed:
java -jar notifyhub-mcp.jar --setup

Step 2: Run the Setup Wizard

The wizard guides you through everything — no config files to edit manually.

Terminal
notifyhub-mcp --setup

# The wizard will:
# 1. Ask for your license key (optional)
# 2. Let you choose channels (Email, Slack, Telegram, etc.)
# 3. Ask for credentials (API keys, tokens)
# 4. Detect your AI editor (Claude, Cursor, VS Code...)
# 5. Generate the config automatically

Step 3: Use with your AI

Open Claude Desktop, Cursor, or any supported editor and just talk:

Claude / Cursor / Any AI
"Send an email to john@example.com saying the project is complete"

"Notify the team on Slack #general that deployment succeeded"

"Send a Telegram message to the ops group about the outage"

"Post a tweet announcing our v2.0 release"

"Send an SMS to +1234567890 with the login code 482901"
Supported editors: Claude Desktop, Claude Code, Cursor, Windsurf, VS Code + Copilot, Cline, Roo Code, Continue.dev, Amazon Q Developer — the setup wizard detects all of them.

Updating

Check for updates and install with one command:

Terminal
notifyhub-mcp --update

Updates only replace the executable — your channel configuration stays intact.

Integrate NotifyHub into your Java / Spring Boot application

Add the dependency, configure your channels, and send notifications with a fluent API.

Step 1: Add the dependency

pom.xml
<dependency>
    <groupId>io.github.gabrielbbaldez</groupId>
    <artifactId>notify-spring-boot-starter</artifactId>
    <version>1.0.0</version>
</dependency>
<dependency>
    <groupId>io.github.gabrielbbaldez</groupId>
    <artifactId>notify-email</artifactId>
</dependency>

Step 2: Configure

application.yml
notify:
  channels:
    email:
      host: smtp.gmail.com
      port: 587
      username: ${GMAIL_USER}
      password: ${GMAIL_PASS}
      from: noreply@myapp.com
      tls: true

Step 3: Send

Java
@Service
public class OrderService {
    private final NotifyHub notify;

    public OrderService(NotifyHub notify) {
        this.notify = notify;
    }

    public void confirmOrder(User user) {
        notify.to(user)
            .via(Channel.EMAIL)
            .fallback(Channel.SMS)
            .subject("Order Confirmed!")
            .content("Your order has shipped!")
            .send();
    }
}
Tip: Only channels with their required properties configured are auto-activated. If you add notify-slack but don't set notify.channels.slack.webhook-url, the Slack channel won't be registered.

Quick Start

Three steps to send your first notification:

1. Add the dependency

Add notify-spring-boot-starter and at least one channel module (e.g., notify-email) to your project.

2. Configure the channel

application.yml
notify:
  channels:
    email:
      host: smtp.gmail.com
      port: 587
      username: ${GMAIL_USER}
      password: ${GMAIL_PASS}
      from: noreply@myapp.com
      from-name: MyApp
      tls: true

3. Inject and send

Java
@Service
public class OrderService {
    private final NotifyHub notify;

    public OrderService(NotifyHub notify) {
        this.notify = notify;
    }

    public void confirmOrder(User user) {
        notify.to(user)
            .via(Channel.EMAIL)
            .subject("Order Confirmed!")
            .template("order-confirmed")
            .param("orderId", "ORD-123")
            .send();
    }
}
Tip: Only channels with their required properties configured are auto-activated. If you add notify-slack but don't set notify.channels.slack.webhook-url, the Slack channel won't be registered.

Core Concepts

NotifyHub

The main entry point. In Spring Boot, it's auto-configured as a singleton bean. Standalone, use the builder:

Java
NotifyHub hub = NotifyHub.builder()
    .channel(emailChannel)
    .channel(slackChannel)
    .channels(List.of(smsChannel, pushChannel))
    .templateEngine(new MustacheTemplateEngine())
    .defaultRetryPolicy(RetryPolicy.exponential(3))
    .listener(myListener)
    .executor(Executors.newFixedThreadPool(4))
    .scheduler(Executors.newScheduledThreadPool(2))
    .tracker(new InMemoryNotificationTracker())
    .rateLimiter(new TokenBucketRateLimiter(config))
    .deadLetterQueue(new InMemoryDeadLetterQueue())
    .router(myRouter)
    .deduplicationStore(new InMemoryDeduplicationStore())
    .auditLog(new InMemoryAuditLog())
    .audienceManager(new AudienceManager(contactRepo))
    .build();

Channel Enum

All built-in channels are represented by the Channel enum:

Java
Channel.EMAIL, Channel.SMS, Channel.WHATSAPP, Channel.PUSH
Channel.SLACK, Channel.TELEGRAM, Channel.DISCORD, Channel.TEAMS
Channel.WEBSOCKET, Channel.GOOGLE_CHAT
Channel.TWITTER, Channel.LINKEDIN, Channel.NOTION
Channel.TWITCH, Channel.YOUTUBE, Channel.INSTAGRAM
Channel.TIKTOK_SHOP, Channel.FACEBOOK
Channel.custom("sendgrid")   // SendGrid email with tracking
Channel.custom("pagerduty")  // Custom channels

Priority

PriorityDescription
Priority.URGENTBypasses rate limiting. Use for critical alerts.
Priority.HIGHHigh priority notification.
Priority.NORMALDefault priority.
Priority.LOWLow priority, best-effort delivery.

DeliveryReceipt

Returned by sendTracked() and sendAllTracked():

Java
DeliveryReceipt receipt = notify.to(user)
    .via(Channel.EMAIL)
    .template("welcome")
    .sendTracked();

receipt.getId();          // UUID
receipt.getChannelName(); // "email"
receipt.getRecipient();   // "user@example.com"
receipt.getStatus();      // PENDING, SCHEDULED, SENT, FAILED, CANCELLED
receipt.getTimestamp();    // Instant
receipt.getErrorMessage(); // null or error details

Fluent API

Entry Points

Java
// Single recipient
notify.to(user)                           // Notifiable object
notify.to("user@example.com")             // Email string
notify.toPhone("+5548999999999")           // Phone number

// Batch recipients
notify.toAll(List.of("a@x.com", "b@x.com"))
notify.toAllNotifiable(List.of(user1, user2))
notify.toAudience("premium-users")

// Auto-route via preferred channels
notify.notify(user)

Builder Chain

The full fluent builder with all available methods:

Java
notify.to(user)
    .via(Channel.EMAIL)                   // Primary channel
    .fallback(Channel.SMS)                // Fallback if primary fails
    .fallback(Channel.WHATSAPP)           // Second fallback
    .priority(Priority.HIGH)              // URGENT, HIGH, NORMAL, LOW
    .subject("Order confirmed")           // Subject (email/push/teams)
    .content("Your order #123 is ready")  // Plain text body
    .template("order-confirmed")          // OR use Mustache template
    .templateVersion("v2")                // Template version (A/B testing)
    .param("orderId", order.getId())      // Template parameter
    .params(Map.of("k", "v"))             // Bulk template params
    .locale(Locale.PT_BR)                 // i18n locale
    .attach(file)                         // File attachment (email)
    .retry(3)                             // Override retry count
    .deduplicationKey("order-123")        // Prevent duplicates
    .metadata("traceId", "abc-123")       // Custom metadata
    .send();                              // Execute synchronously

Execution Methods

MethodDescription
.send()Synchronous. Uses fallback chain on failure.
.sendAll()Synchronous. Sends to ALL .via() channels.
.sendAsync()Asynchronous. Returns CompletableFuture<Void>.
.sendAllAsync()Async to all channels.
.sendTracked()Sync, returns DeliveryReceipt.
.sendAllTracked()Returns List<DeliveryReceipt>.
.schedule(Duration)Schedule for later (e.g., Duration.ofMinutes(30)).
.scheduleAt(Instant)Schedule at specific time.

Examples

Fallback chain

Java
notify.to(user)
    .via(Channel.WHATSAPP).fallback(Channel.SMS).fallback(Channel.EMAIL)
    .template("payment-reminder")
    .param("amount", "R$ 150,00")
    .send();

Multi-channel broadcast

Java
notify.to(user)
    .via(Channel.EMAIL).via(Channel.SLACK).via(Channel.TEAMS)
    .subject("Security Alert")
    .content("Login from new device detected")
    .sendAll();

Tracked async

Java
CompletableFuture<Void> future = notify.to(user)
    .via(Channel.EMAIL)
    .template("invoice")
    .param("invoiceId", "INV-456")
    .sendAsync();

future.thenRun(() -> log.info("Invoice email sent"));

Notifiable Interface

Implement this interface on your domain objects (User, Customer, etc.) to enable direct usage with notify.to(user):

Java
public class User implements Notifiable {
    public String getNotifyEmail()     { return email; }
    public String getNotifyPhone()     { return phone; }
    public String getNotifyPushToken() { return pushToken; }
    public String getNotifyName()      { return name; }
    public List<Channel> getPreferredChannels() {
        return List.of(Channel.EMAIL, Channel.PUSH);
    }
}

All methods have default implementations returning null or empty lists. Override only the ones you need.

Auto-routing: When you call notify.notify(user), NotifyHub automatically sends via the user's preferred channels in order, using the remaining as fallbacks.

Channels

NotifyHub supports 20 built-in channels. Each channel requires its own module dependency and configuration. Below is the complete reference for each channel.

Multi-channel: You can send to multiple channels at once with a single call:
Java
notify.to(user)
    .via(Channel.EMAIL, Channel.SLACK, Channel.PUSH)
    .subject("Order Confirmed!")
    .body("Your order #123 has been confirmed.")
    .send();
Use .fallback(Channel.SMS) to automatically try another channel if the primary fails.
Email

Email (SMTP)

Send emails via any SMTP server (Gmail, AWS SES, Outlook, custom). Supports HTML templates, attachments, and TLS/SSL.

Module: notify-email

application.yml
notify:
  channels:
    email:
      host: smtp.gmail.com
      port: 587
      username: ${GMAIL_USER}
      password: ${GMAIL_PASS}
      from: noreply@myapp.com
      from-name: MyApp
      tls: true
Java (Standalone)
SmtpConfig config = SmtpConfig.builder()
    .host("smtp.gmail.com").port(587)
    .username("user").password("pass")
    .from("noreply@app.com").fromName("MyApp")
    .tls(true)
    .build();

Getting Credentials

  1. Gmail: Enable 2FA on your Google account, then go to App Passwords and generate one for "Mail"
  2. AWS SES: Go to SES Console > SMTP Settings > Create SMTP credentials
  3. Other SMTP: Use the host, port, username, and password provided by your email provider

Usage

Java
notify.to("user@example.com")
    .via(Channel.EMAIL)
    .subject("Order Confirmed!")
    .body("Your order #123 has been confirmed.")
    .send();
Tip: For Gmail, use an App Password (not your regular password). Enable 2FA in your Google account, then generate an app password at myaccount.google.com.

SMS (Twilio)

Send SMS messages via the Twilio API.

Module: notify-sms

application.yml
notify:
  channels:
    sms:
      account-sid: ${TWILIO_ACCOUNT_SID}
      auth-token: ${TWILIO_AUTH_TOKEN}
      from-number: "+15551234567"
Java (Standalone)
TwilioConfig config = TwilioConfig.builder()
    .accountSid("AC...").authToken("token")
    .fromNumber("+15551234567")
    .build();

Getting Credentials

  1. Create an account at Twilio
  2. Go to Console > Account Info to find your Account SID and Auth Token
  3. Buy or use a trial phone number with SMS capability
Tip: Free trial accounts can only send to verified numbers. Upgrade to remove this restriction.

Usage

Java
notify.to("+5511999999999")
    .via(Channel.SMS)
    .body("Your verification code is 1234")
    .send();
WhatsApp

WhatsApp

Send WhatsApp messages via Twilio or Meta WhatsApp Cloud API. Supports text messages and media attachments.

Module: notify-sms (same module as SMS)

Option A: Twilio Backend

application.yml — Twilio
notify:
  channels:
    whatsapp:
      account-sid: ${TWILIO_ACCOUNT_SID}
      auth-token: ${TWILIO_AUTH_TOKEN}
      from-number: "+14155238886"
Java (Standalone — Twilio)
TwilioConfig config = TwilioConfig.builder()
        .accountSid("AC...")
        .authToken("token")
        .fromNumber("+14155238886")
        .build();
hub.registerChannel(new WhatsAppChannel(config));

Option B: Meta WhatsApp Cloud API

application.yml — Cloud API
notify:
  channels:
    whatsapp:
      access-token: ${WHATSAPP_ACCESS_TOKEN}
      phone-number-id: ${WHATSAPP_PHONE_NUMBER_ID}

Getting Credentials

Option A — Twilio:

  1. Create an account at Twilio
  2. Go to Console > Account Info to find your Account SID and Auth Token
  3. In Messaging > Try it out > Send a WhatsApp message, activate the WhatsApp sandbox
  4. The From Number is the Twilio sandbox number (e.g., +14155238886)

Option B — Meta WhatsApp Cloud API:

  1. Go to Meta for Developers and create an app with "Conectar-se com clientes pelo WhatsApp"
  2. Create a Business Portfolio if prompted
  3. Go to WhatsApp > API Setup and click Generate Access Token
  4. Copy the Phone Number ID shown on the same page
  5. Add your phone number as a test recipient in step 3 and verify with the code
Tip: The Meta test token expires in 60 minutes. For production, generate a permanent System User token in the Business Manager.

Usage

Java
notify.to("+5511999999999")
    .via(Channel.WHATSAPP)
    .body("Your order has been shipped!")
    .send();

Media Attachments

Java
// Attach media (image, video, PDF) via public URL
notify.to("+5511999999999")
    .via(Channel.WHATSAPP)
    .body("Here is your invoice")
    .param("mediaUrl", "https://example.com/invoice.pdf")
    .send();
Dual backend: NotifyHub auto-detects which backend to use based on your configuration. If account-sid is set, Twilio is used. If access-token and phone-number-id are set, Meta Cloud API is used.
Slack

Slack

Send messages to Slack channels or users via Incoming Webhooks.

Module: notify-slack

application.yml
notify:
  channels:
    slack:
      webhook-url: ${SLACK_WEBHOOK_URL}
      recipients:
        engineering: "https://hooks.slack.com/services/..."
        alerts: "https://hooks.slack.com/services/..."
Java (Standalone)
SlackConfig config = SlackConfig.builder()
    .webhookUrl("https://hooks.slack.com/services/...")
    .recipients(Map.of(
        "engineering", "https://hooks.slack.com/services/...",
        "alerts", "https://hooks.slack.com/services/..."
    ))
    .build();

Getting Credentials

  1. Go to Slack API and create a new app (or select existing)
  2. Go to Incoming Webhooks and toggle it On
  3. Click Add New Webhook to Workspace and select the target channel
  4. Copy the Webhook URL (starts with https://hooks.slack.com/services/...)

Usage

Java
notify.to("engineering")
    .via(Channel.SLACK)
    .body("Deploy to production complete!")
    .send();
Named recipients: Use recipients to map friendly names to webhook URLs, then send with notify.to("engineering").via(SLACK).send().
Telegram

Telegram

Send messages via the Telegram Bot API to any chat ID or channel.

Module: notify-telegram

application.yml
notify:
  channels:
    telegram:
      bot-token: ${TELEGRAM_BOT_TOKEN}
      chat-id: ${TELEGRAM_CHAT_ID}
      recipients:
        admin: "123456789"
Java (Standalone)
TelegramConfig config = TelegramConfig.builder()
    .botToken("123456:ABC-DEF").chatId("987654321")
    .recipients(Map.of("admin", "123456789"))
    .build();

Getting Credentials

  1. Open Telegram and search for @BotFather
  2. Send /newbot, choose a name and username to get the Bot Token
  3. To get a Chat ID: add the bot to a group or send it a message, then call https://api.telegram.org/bot{TOKEN}/getUpdates and look for the chat.id field
Tip: For group chats, the Chat ID is negative (e.g., -1001234567890). For direct messages, it's a positive number.

Usage

Java
notify.to("admin")
    .via(Channel.TELEGRAM)
    .body("Alert: CPU usage above 90%")
    .send();
Discord

Discord

Send messages via Discord webhooks with custom username and avatar.

Module: notify-discord

application.yml
notify:
  channels:
    discord:
      webhook-url: ${DISCORD_WEBHOOK_URL}
      username: NotifyBot
      avatar-url: "https://example.com/avatar.png"
      recipients:
        alerts: "https://discord.com/api/webhooks/..."
Java (Standalone)
DiscordConfig config = DiscordConfig.builder()
    .webhookUrl("https://discord.com/api/webhooks/...")
    .username("NotifyBot").avatarUrl("https://...")
    .recipients(Map.of("alerts", "https://discord.com/api/webhooks/..."))
    .build();

Getting Credentials

  1. In Discord, go to the channel > Edit Channel > Integrations > Webhooks
  2. Click New Webhook, give it a name and avatar
  3. Click Copy Webhook URL

Usage

Java
notify.to("alerts")
    .via(Channel.DISCORD)
    .body("New user signup: john@example.com")
    .send();

Microsoft Teams

Send messages via Microsoft Teams Incoming Webhooks with MessageCard format.

Module: notify-teams

application.yml
notify:
  channels:
    teams:
      webhook-url: ${TEAMS_WEBHOOK_URL}
      recipients:
        devops: "https://outlook.office.com/webhook/..."
Java (Standalone)
TeamsConfig config = TeamsConfig.builder()
    .webhookUrl("https://outlook.office.com/webhook/...")
    .recipients(Map.of("devops", "https://outlook.office.com/webhook/..."))
    .build();

Getting Credentials

  1. In Microsoft Teams, go to the channel where you want notifications
  2. Click ... > Connectors > Incoming Webhook
  3. Give the webhook a name and optional image, then click Create
  4. Copy the Webhook URL
Tip: Microsoft is migrating Incoming Webhooks to the "Workflows" app. If the Connectors option is not available, use Power Automate to create a webhook workflow.

Usage

Java
notify.to("devops")
    .via(Channel.TEAMS)
    .subject("Incident Alert")
    .body("Database connection pool exhausted")
    .send();
Google Chat

Google Chat

Send messages to Google Chat spaces via webhooks.

Module: notify-google-chat

application.yml
notify:
  channels:
    google-chat:
      webhook-url: ${GOOGLE_CHAT_WEBHOOK_URL}
      recipients:
        team: "https://chat.googleapis.com/..."
Java (Standalone)
GoogleChatConfig config = GoogleChatConfig.builder()
    .webhookUrl("https://chat.googleapis.com/v1/spaces/...")
    .recipients(Map.of("team", "https://chat.googleapis.com/..."))
    .build();

Getting Credentials

  1. Open the Google Chat space where you want to send messages
  2. Click the space name > Apps & integrations > Manage webhooks
  3. Click Add webhook, give it a name and (optional) avatar URL
  4. Copy the Webhook URL

Usage

Java
notify.to("team")
    .via(Channel.GOOGLE_CHAT)
    .body("Sprint planning starts in 10 minutes")
    .send();
Firebase Push

Firebase Push

Send push notifications via Firebase Cloud Messaging (FCM).

Module: notify-push-firebase

application.yml
notify:
  channels:
    push:
      server-key: ${FIREBASE_SERVER_KEY}
Java (Standalone)
FirebasePushConfig config = FirebasePushConfig.builder()
    .serverKey("AAAA...")
    .build();

Getting Credentials

  1. Go to Firebase Console and create or select a project
  2. Go to Project Settings > Cloud Messaging tab
  3. Copy the Server Key (Legacy API) or use the Firebase Admin SDK with a service account JSON
  4. Device tokens are generated by the client app (Android/iOS/Web) when registering for push notifications
Tip: The legacy Server Key is being deprecated. For new projects, use the Firebase Admin SDK with a service account key file.

Usage

Java
notify.to("fcm-device-token-here")
    .via(Channel.PUSH)
    .subject("New Message")
    .body("You have a new message from John")
    .send();

WebSocket

Send real-time notifications via JDK WebSocket (java.net.http) with auto-reconnect.

Module: notify-websocket

application.yml
notify:
  channels:
    websocket:
      uri: wss://ws.example.com/notifications
      reconnect-enabled: true
      reconnect-delay-ms: 3000
      max-reconnect-attempts: 10
      headers:
        Authorization: "Bearer ${WS_TOKEN}"
Java (Standalone)
WebSocketConfig config = WebSocketConfig.builder()
    .uri("wss://ws.example.com/notifications")
    .reconnectEnabled(true).reconnectDelayMs(3000).maxReconnectAttempts(10)
    .headers(Map.of("Authorization", "Bearer token"))
    .messageFormat("json")
    .build();

Usage

Java
notify.to("session-id-123")
    .via(Channel.WEBSOCKET)
    .body("{\"type\":\"update\",\"data\":\"new item\"}")
    .send();

Generic Webhook

Send to any HTTP endpoint with custom payload templates. Ideal for PagerDuty, Datadog, or custom APIs.

Module: notify-webhook

application.yml
notify:
  channels:
    webhooks:
      - name: pagerduty
        url: "https://events.pagerduty.com/v2/enqueue"
        method: POST
        headers:
          Authorization: "Token token=${PAGERDUTY_TOKEN}"
        payload-template: '{"routing_key":"{{routingKey}}","event_action":"trigger","payload":{"summary":"{{content}}"}}'
Java (Standalone)
WebhookConfig config = WebhookConfig.builder()
    .name("pagerduty")
    .url("https://events.pagerduty.com/v2/enqueue")
    .method("POST")
    .headers(Map.of("Authorization", "Token token=..."))
    .payloadTemplate("{\"routing_key\":\"R0...\",\"event_action\":\"trigger\",\"payload\":{\"summary\":\"{{content}}\"}}")
    .build();

Usage

Java
notify.to("pagerduty")
    .via(Channel.custom("pagerduty"))
    .param("routingKey", "R0...")
    .body("Server web-01 is unreachable")
    .send();
Custom channel name: Use Channel.custom("pagerduty") to reference the webhook by its name field.

Twitter / X

Post tweets via the Twitter API v2 using OAuth 1.0a authentication.

Module: notify-twitter

application.yml
notify:
  channels:
    twitter:
      api-key: ${TWITTER_API_KEY}
      api-secret: ${TWITTER_API_SECRET}
      access-token: ${TWITTER_ACCESS_TOKEN}
      access-token-secret: ${TWITTER_ACCESS_TOKEN_SECRET}
Java (Standalone)
TwitterConfig config = TwitterConfig.builder()
    .apiKey("key").apiSecret("secret")
    .accessToken("token").accessTokenSecret("tokenSecret")
    .build();

Getting Credentials

  1. Go to X Developer Portal and create a project + app
  2. In Keys and Tokens, generate API Key and API Secret
  3. Generate Access Token and Access Token Secret with Read and Write permissions
  4. Make sure your app has OAuth 1.0a enabled under User authentication settings
Tip: Free tier allows 1,500 tweets/month. The Access Token and Secret are tied to your account and don't expire.

Usage

Java
notify.via(Channel.TWITTER)
    .body("We just launched NotifyHub v2.0! Check it out.")
    .send();

LinkedIn

Publish posts on LinkedIn via the REST API with OAuth 2.0 Bearer token.

Module: notify-linkedin

application.yml
notify:
  channels:
    linkedin:
      access-token: ${LINKEDIN_ACCESS_TOKEN}
      author-id: "urn:li:person:abc123"
Java (Standalone)
LinkedInConfig config = LinkedInConfig.builder()
    .accessToken("token")
    .authorId("urn:li:person:abc123")
    .build();

Getting Credentials

  1. Go to LinkedIn Developer Portal and create an app
  2. In Products, request access to Share on LinkedIn and Sign In with LinkedIn using OpenID Connect
  3. Use the OAuth 2.0 flow to get an Access Token with scope w_member_social
  4. Your Author ID is your LinkedIn URN: urn:li:person:{id}. Get it by calling GET /v2/userinfo with the token
Tip: LinkedIn access tokens expire in 60 days. Use the refresh token flow to renew automatically.

Usage

Java
notify.via(Channel.LINKEDIN)
    .body("Excited to announce our new open-source project!")
    .send();

Notion

Create pages in Notion databases via the API with an Integration Token.

Module: notify-notion

application.yml
notify:
  channels:
    notion:
      api-key: ${NOTION_API_KEY}
      database-id: ${NOTION_DATABASE_ID}
Java (Standalone)
NotionConfig config = NotionConfig.builder()
    .apiKey("secret_...").databaseId("db-id")
    .recipients(Map.of("tasks", "another-db-id"))
    .build();

Getting Credentials

  1. Go to Notion Integrations and create a new integration
  2. Copy the Internal Integration Token (starts with secret_)
  3. Open the target Notion database, click ... > Connections > Connect to and select your integration
  4. The Database ID is in the database URL: notion.so/{workspace}/{database-id}?v=...
Tip: The database must have a "Title" property. NotifyHub uses subject for the page title and body for the page content.

Usage

Java
notify.to("tasks")
    .via(Channel.NOTION)
    .subject("Bug Report")
    .body("Login page returns 500 on invalid email")
    .send();
Twitch

Twitch

Send chat messages via the Twitch Helix API with OAuth 2.0. Supports automatic token refresh.

Module: notify-twitch

application.yml
notify:
  channels:
    twitch:
      client-id: ${TWITCH_CLIENT_ID}
      access-token: ${TWITCH_ACCESS_TOKEN}
      refresh-token: ${TWITCH_REFRESH_TOKEN}        # Optional: auto-refresh
      client-secret: ${TWITCH_CLIENT_SECRET}         # Required with refresh-token
      broadcaster-id: "836211004"
      sender-id: "836211004"
Java (Standalone)
TwitchConfig config = TwitchConfig.builder()
    .clientId("tyxy0tn8...")
    .accessToken("ck6uie0g...")       // Seed token
    .refreshToken("pmn0lc2x...")      // Optional: enables auto-refresh
    .clientSecret("wku9i8gu...")      // Required with refreshToken
    .broadcasterId("836211004")
    .senderId("836211004")
    .build();
// getAccessToken() auto-renews 5 minutes before expiry

Getting Credentials

  1. Go to Twitch Developer Console and register a new application
  2. Copy the Client ID from the app settings
  3. Generate an Access Token using the OAuth Authorization Code flow with scopes user:write:chat and user:bot
  4. Your Broadcaster ID is your numeric Twitch user ID (find it at Twitch ID Lookup)
  5. Sender ID is usually the same as Broadcaster ID (your bot account)

Usage

Java
notify.to("836211004")
    .via(Channel.TWITCH)
    .body("Hello chat! Stream starting soon.")
    .send();
OAuth auto-refresh: Set refresh-token and client-secret to enable automatic token renewal. Without them, the static access token is used (will expire after ~4 hours).
YouTube

YouTube

Send live chat messages via the YouTube Data API v3. Supports automatic OAuth token refresh.

Module: notify-youtube

application.yml
notify:
  channels:
    youtube:
      access-token: ${YOUTUBE_ACCESS_TOKEN}
      refresh-token: ${YOUTUBE_REFRESH_TOKEN}        # Optional: auto-refresh
      client-id: ${YOUTUBE_CLIENT_ID}                # Required with refresh-token
      client-secret: ${YOUTUBE_CLIENT_SECRET}         # Required with refresh-token
      channel-id: ${YOUTUBE_CHANNEL_ID}
      live-chat-id: ${YOUTUBE_LIVE_CHAT_ID}
Java (Standalone)
YouTubeConfig config = YouTubeConfig.builder()
    .accessToken("ya29.a0...")        // Seed token
    .refreshToken("1//0eXyz...")      // Optional: enables auto-refresh
    .clientId("123.apps.google...")    // Required with refreshToken
    .clientSecret("GOCSPX-...")       // Required with refreshToken
    .channelId("UC...")
    .liveChatId("Cg0KC...")
    .build();
// Token auto-refreshes via Google OAuth2 endpoint

Getting Credentials

  1. Go to Google Cloud Console and create a project
  2. Enable the YouTube Data API v3
  3. Create OAuth 2.0 credentials (Web application type)
  4. Use the OAuth Playground to generate an Access Token with scope youtube.force-ssl
  5. Get your Channel ID from YouTube Studio > Settings > Advanced Settings
  6. The Live Chat ID is obtained from an active livestream via the API: GET /liveBroadcasts?mine=true
Tip: Set refresh-token, client-id, and client-secret for automatic token renewal. Without them, access tokens expire in ~1 hour.

Usage

Java
notify.via(Channel.YOUTUBE)
    .body("Thanks for watching! Don't forget to subscribe.")
    .send();
Instagram

Instagram

Send DMs and publish feed posts via the Instagram Graph API (Meta).

Module: notify-instagram

application.yml
notify:
  channels:
    instagram:
      access-token: ${INSTAGRAM_ACCESS_TOKEN}
      ig-user-id: ${INSTAGRAM_USER_ID}
Java (Standalone)
InstagramConfig config = InstagramConfig.builder()
    .accessToken("IGQ...")
    .igUserId("17841...")
    .recipients(Map.of("support", "user-id"))
    .build();

Getting Credentials

  1. Go to Meta for Developers and select your app
  2. In Casos de uso, add "Gerenciar mensagens e conteudo no Instagram"
  3. Link your Instagram Professional account to a Facebook Page
  4. Generate an Access Token with instagram_basic and instagram_manage_messages permissions via the Graph API Explorer
  5. Get your IG User ID by calling GET /me?fields=id with the Instagram token
Tip: Instagram API requires a Professional (Business or Creator) account linked to a Facebook Page. Personal accounts are not supported.

Usage

Java
notify.to("support")
    .via(Channel.INSTAGRAM)
    .body("Thanks for reaching out! We'll get back to you soon.")
    .send();

SendGrid

Send emails via the SendGrid API with full delivery tracking support. Track delivery status including opened, clicked, bounced, spam complaints, dropped, and deferred events.

Module: notify-sendgrid

application.yml
notify:
  channels:
    sendgrid:
      api-key: ${SENDGRID_API_KEY}
      from: noreply@myapp.com
      from-name: MyApp
Java (Standalone)
SendGridConfig config = SendGridConfig.builder()
    .apiKey("SG.xxxxxx")
    .from("noreply@myapp.com")
    .fromName("MyApp")
    .build();

Getting Credentials

  1. Create a free account at SendGrid
  2. Go to Settings > API Keys and create a new API key with "Mail Send" permission
  3. Complete Sender Authentication (domain or single sender) to avoid emails going to spam
Tip: The free tier allows 100 emails/day. Use "Restricted Access" API keys with only the permissions you need.

Usage

Java
// Send and get provider message ID for tracking
SendResult result = notify.to("user@example.com")
    .via(Channel.custom("sendgrid"))
    .subject("Welcome to MyApp!")
    .body("Thanks for signing up.")
    .sendWithResult();

String providerMessageId = result.getProviderMessageId();

Delivery Tracking

SendGrid supports advanced email delivery tracking. Check the status of a sent email:

Java
// Check email delivery status
EmailStatusProvider sendgrid = (EmailStatusProvider) hub.getChannel("sendgrid");
DeliveryStatus status = sendgrid.checkEmailStatus(providerMessageId);
// Returns: DELIVERED, OPENED, CLICKED, BOUNCED, SPAM_COMPLAINT, DROPPED, DEFERRED
Tracking statuses: SendGrid provides granular delivery events: DELIVERED, OPENED, CLICKED, BOUNCED, SPAM_COMPLAINT, DROPPED, and DEFERRED. Use the MCP tool check_email_status to query these from an AI assistant.
TikTok Shop

TikTok Shop

Send notifications via the TikTok Shop API with HMAC-SHA256 signed requests for secure communication.

Module: notify-tiktok-shop

application.yml
notify:
  channels:
    tiktok-shop:
      app-key: ${TIKTOK_SHOP_APP_KEY}
      app-secret: ${TIKTOK_SHOP_APP_SECRET}
      access-token: ${TIKTOK_SHOP_ACCESS_TOKEN}
      shop-id: ${TIKTOK_SHOP_ID}
Java (Standalone)
TikTokShopConfig config = TikTokShopConfig.builder()
    .appKey("app-key")
    .appSecret("app-secret")
    .accessToken("access-token")
    .shopId("shop-id")
    .build();

Getting Credentials

  1. Go to TikTok Shop Partner Center and register as a developer
  2. Create an app to get your App Key and App Secret
  3. Authorize the app for a shop to get the Access Token
  4. Your Shop ID is found in the TikTok Shop Seller Center dashboard

Usage

Java
notify.to("shop-id")
    .via(Channel.TIKTOK_SHOP)
    .body("Your order has been shipped!")
    .send();
HMAC-SHA256: All requests to the TikTok Shop API are automatically signed with HMAC-SHA256 using your app-secret. No manual signing required.
Facebook

Facebook

Post to Facebook Page feeds and send Messenger DMs via the Facebook Graph API.

Module: notify-facebook

application.yml
notify:
  channels:
    facebook:
      page-access-token: ${FACEBOOK_PAGE_ACCESS_TOKEN}
      page-id: ${FACEBOOK_PAGE_ID}
Java (Standalone)
FacebookConfig config = FacebookConfig.builder()
    .pageAccessToken("EAAGx...")
    .pageId("123456789")
    .build();

Getting Credentials

  1. Go to Meta for Developers and create or select your app
  2. In Casos de uso, add "Interagir com clientes no Messenger"
  3. Go to your app's Settings > Basic to find the App ID and App Secret
  4. Link a Facebook Page to the app and generate a Page Access Token via the Graph API Explorer
  5. The Page ID is visible on your Page's "About" section or via the Graph API
Tip: For long-lived tokens, exchange the short-lived token via the /oauth/access_token endpoint. Short-lived tokens expire in ~1 hour.

Usage — Page Feed Post

Java
// Post to page feed — use "feed" as recipient
notify.to("feed")
    .via(Channel.FACEBOOK)
    .body("Exciting news! Our v2.0 release is live.")
    .send();

Usage — Messenger DM

Java
// Send Messenger DM — use PSID as recipient
notify.to("1234567890123456")
    .via(Channel.FACEBOOK)
    .body("Thanks for contacting us! We'll reply shortly.")
    .send();
Recipient types: Use "feed" as the recipient to publish a post on your Page's feed. Use a Page-Scoped ID (PSID) to send a Messenger DM to a specific user.

AWS SNS

Publish notifications to Amazon SNS topics, endpoints (push), or phone numbers (SMS) via the AWS SDK v2.

Module: notify-aws-sns

application.yml
notify:
  channels:
    aws-sns:
      region: us-east-1
      access-key-id: ${AWS_ACCESS_KEY_ID}
      secret-access-key: ${AWS_SECRET_ACCESS_KEY}
      topic-arn: arn:aws:sns:us-east-1:123456789:my-topic  # optional
Java (Standalone)
AwsSnsConfig config = AwsSnsConfig.builder()
    .region("us-east-1")
    .accessKeyId("AKIA...")
    .secretAccessKey("secret...")
    .topicArn("arn:aws:sns:us-east-1:123456789:my-topic")
    .build();

Getting Credentials

  1. Sign in to the AWS SNS Console
  2. Create a Topic (Standard or FIFO) and copy the Topic ARN
  3. Go to IAM Console and create a user with AmazonSNSFullAccess policy
  4. Generate an Access Key ID and Secret Access Key for the IAM user
  5. Set the Region to match your topic's region (e.g., us-east-1)
Tip: For production, use IAM roles instead of static credentials. The AWS SDK auto-discovers credentials from environment variables, instance profiles, or the default credential chain.

Usage

Java
// Publish to a topic (all subscribers receive it)
notify.to("all-users")
    .via(Channel.AWS_SNS)
    .subject("Deployment Complete")
    .body("Version 2.0 deployed successfully.")
    .send();

// Send SMS directly via SNS (recipient = phone number)
notify.to("+5511999887766")
    .via(Channel.AWS_SNS)
    .body("Your verification code is 123456")
    .send();

// Publish to a specific endpoint ARN
notify.to("arn:aws:sns:us-east-1:123:endpoint/GCM/myapp/abc123")
    .via(Channel.AWS_SNS)
    .body("Push notification via SNS")
    .send();
Recipient types: Use a Topic ARN or Endpoint ARN as recipient for targeted delivery, a phone number (e.g., +5511...) for direct SMS, or any string when a default topic-arn is configured.
Mailgun

Mailgun

Send transactional and marketing emails via the Mailgun REST API with US and EU region support.

Module: notify-mailgun

application.yml
notify:
  channels:
    mailgun:
      api-key: ${MAILGUN_API_KEY}
      domain: mg.myapp.com
      from: noreply@myapp.com
      region: US  # US (default) or EU
Java (Standalone)
MailgunConfig config = MailgunConfig.builder()
    .apiKey("key-abc123...")
    .domain("mg.myapp.com")
    .from("noreply@myapp.com")
    .region("EU")  // optional, defaults to "US"
    .build();

Getting Credentials

  1. Sign up at Mailgun and verify your account
  2. Go to Sending > Domains and add your domain (e.g., mg.myapp.com)
  3. Follow the DNS verification steps (add TXT, MX, and CNAME records)
  4. Go to API Security and copy your API Key (starts with key-)
  5. Set the From address to an email on your verified domain
Tip: If your data must stay in the EU, set region: EU to use Mailgun's EU endpoint (api.eu.mailgun.net). The sandbox domain (sandboxXXX.mailgun.org) works for testing without DNS setup.

Usage

Java
notify.to("user@example.com")
    .via(Channel.MAILGUN)
    .subject("Welcome to MyApp!")
    .body("Thanks for signing up. Get started at https://myapp.com/start")
    .send();
PagerDuty

PagerDuty

Trigger incidents on PagerDuty via the Events API v2. Supports severity levels and automatic summary truncation.

Module: notify-pagerduty

application.yml
notify:
  channels:
    pagerduty:
      routing-key: ${PAGERDUTY_ROUTING_KEY}
      severity: warning  # critical, error, warning (default), info
Java (Standalone)
PagerDutyConfig config = PagerDutyConfig.builder()
    .routingKey("R0KEY...")
    .severity("critical")  // optional, defaults to "warning"
    .build();

Getting Credentials

  1. Sign in to PagerDuty
  2. Go to Services > Service Directory and create a new service (or select an existing one)
  3. In the service's Integrations tab, click "Add Integration"
  4. Select "Events API v2" as the integration type
  5. Copy the Integration Key (this is the routing-key)
Tip: Each service has its own routing key. Use different keys for different environments (staging vs production) or different severity levels.

Usage

Java
// Trigger an incident
notify.to("ops-team")
    .via(Channel.PAGERDUTY)
    .subject("Database connection pool exhausted")
    .body("Pool usage at 100% for 5 minutes. Auto-scaling failed.")
    .send();

// With template
notify.to("sre-team")
    .via(Channel.PAGERDUTY)
    .template("infra-alert")
    .param("service", "api-gateway")
    .param("metric", "p99 latency > 2s")
    .send();
Note: The recipient field is informational for PagerDuty — routing is determined by the routing-key which maps to a specific PagerDuty service.
Kick

Kick

Send chat messages to Kick channels via the official Kick Public API. Supports bot and user message types with OAuth 2.1 authentication.

Module: notify-kick

application.yml
notify:
  channels:
    kick:
      client-id: ${KICK_CLIENT_ID}
      access-token: ${KICK_ACCESS_TOKEN}
      broadcaster-id: ${KICK_BROADCASTER_ID}
      message-type: bot  # bot (default) or user
Java (Standalone)
KickConfig config = KickConfig.builder()
    .clientId("your-client-id")
    .accessToken("your-oauth-access-token")
    .broadcasterId("12345678")
    .messageType("bot")  // optional, defaults to "bot"
    .build();

Getting Credentials

  1. Go to kick.com/settings/developer and register your application
  2. Enable Two-Factor Authentication (2FA) on your Kick account if not already enabled (required for API access)
  3. Copy your Client ID and Client Secret from the developer settings
  4. Generate an OAuth 2.1 access token with the chat:write scope
  5. Find your Broadcaster ID via the Kick API or your channel settings
Tip: NotifyHub supports automatic OAuth token refresh for Kick. Store your client-id, client-secret, and refresh-token and the channel will handle re-authentication transparently.

API Details

Usage

Java
// Send a chat message to your Kick channel
notify.to("my-kick-channel")
    .via(Channel.KICK)
    .body("Stream is going live in 5 minutes! 🎮")
    .send();

// With template
notify.to("my-kick-channel")
    .via(Channel.KICK)
    .template("stream-alert")
    .param("game", "Minecraft")
    .param("viewers", "1234")
    .send();
Note: The recipient field is used as a label for routing purposes — the actual target channel is determined by the broadcaster-id configured in your Kick settings.

Custom Channels

Implement the NotificationChannel interface to create your own channel:

Java
public class MyCustomChannel implements NotificationChannel {

    public String getName() { return "my-custom"; }

    public boolean isAvailable() { return apiKey != null; }

    public void send(Notification notification) throws NotificationSendException {
        // Your implementation here
    }
}

Register it:

Java
// Programmatically
hub.registerChannel(new MyCustomChannel(config));

// Or as a Spring Bean
@Bean
public MyCustomChannel myCustomChannel() {
    return new PagerDutyChannel(config);
}

Use it with Channel.custom("pagerduty").

Templates

NotifyHub uses Mustache as its built-in template engine. Templates support HTML and plain-text variants, i18n, and versioning for A/B testing.

Directory Structure

Template layout
src/main/resources/templates/notify/
  order-confirmed/
    html.mustache          # Email HTML variant
    txt.mustache           # SMS/WhatsApp text variant
  welcome/
    html.mustache
    txt.mustache
  alert/
    txt.mustache

Usage

Java
notify.to(user)
    .via(Channel.EMAIL)
    .template("order-confirmed")
    .param("customerName", "Gabriel")
    .param("orderId", "ORD-123")
    .locale(Locale.PT_BR)           // i18n support
    .templateVersion("v2")          // A/B testing
    .send();

TemplateEngine Interface

Java
public interface TemplateEngine {
    String render(String templateName, String variant, Map<String, Object> params);
    String render(String templateName, String variant, Map<String, Object> params, Locale locale);
    boolean exists(String templateName, String variant);
}

Built-in implementations: MustacheTemplateEngine and VersionedTemplateEngine.

Retry & Error Handling

Retry Policies

Java
RetryPolicy.none()                          // Fail immediately
RetryPolicy.fixed(3, Duration.ofSeconds(2)) // 3 attempts, 2s delay
RetryPolicy.exponential(3)                  // 3 attempts, 1s/2s/4s backoff
RetryPolicy.exponential(5, Duration.ofMillis(500)) // Custom initial delay

Methods: getMaxAttempts(), getInitialDelay(), getStrategy() (NONE/FIXED/EXPONENTIAL), getDelayForAttempt(int), shouldRetry().

Override per notification

Java
notify.to(user).via(Channel.EMAIL)
    .retry(5)                           // Override retry count for this send
    .content("Important!").send();

Exception Types

ExceptionWhen
NotificationSendExceptionChannel send failure (network, auth, payload error)
OAuthTokenRefreshExceptionOAuth token refresh failure (YouTube/Twitch)
RateLimitExceededExceptionRate limit exceeded for a channel
TemplateNotFoundExceptionTemplate name not found

Rate Limiting

Built-in TokenBucketRateLimiter with per-channel API-aware presets that match real API limits:

ChannelLimitWindow
YouTube180 requests1 day
Twitch20 requests30 seconds
Twitter/X300 requests3 hours
Slack1 request1 second
Telegram30 requests1 second
Discord30 requests1 minute
LinkedIn100 requests1 day
Instagram200 requests1 hour
Google Chat60 requests1 minute

Factory Methods

Java
RateLimitConfig.youtube()                   // Factory method per channel
RateLimitConfig.forChannel("twitch")        // Lookup by name
RateLimitConfig.allDefaults()               // Map of all presets
RateLimitConfig.perSecond(10)               // Custom
RateLimitConfig.perMinute(60)               // Custom
Important: Priority.URGENT bypasses rate limiting entirely. Use it only for critical alerts.

Delivery Tracking

DeliveryReceipt

Java
DeliveryReceipt receipt = notify.to(user)
    .via(Channel.EMAIL)
    .template("welcome")
    .sendTracked();

receipt.getId();          // UUID
receipt.getChannelName(); // "email"
receipt.getRecipient();   // "user@example.com"
receipt.getStatus();      // PENDING, SCHEDULED, SENT, FAILED, CANCELLED
receipt.getTimestamp();    // Instant
receipt.getErrorMessage(); // null or error details

Status Values

StatusDescription
PENDINGNotification created, not yet sent.
SCHEDULEDScheduled for future delivery.
SENTSuccessfully delivered.
FAILEDFailed after all retry attempts.
CANCELLEDCancelled before delivery.

Dead Letter Queue (DLQ)

Failed notifications (after exhausting all retries) are captured in the DLQ for inspection and replay:

Java
// Access DLQ
List<DeadLetter> failures = hub.getDeadLetterQueue().findAll();

// DeadLetter contains
deadLetter.getChannelName();   // "email"
deadLetter.getRecipient();     // "user@example.com"
deadLetter.getSubject();       // "Welcome"
deadLetter.getContent();       // "Hello!"
deadLetter.getErrorMessage();  // "Connection refused"
deadLetter.getAttemptCount();  // 3

Audience & Contacts

Create Contacts

Java
Contact vip = Contact.builder()
    .name("Gabriel")
    .email("gabriel@example.com")
    .phone("+5548999999999")
    .tag("vip").tag("plan:premium")
    .metadata("region", "south")
    .build();

Create Audiences

Audiences are defined by tag filters using AND logic — contacts must match ALL tags to be included:

Java
// Create audience (AND logic on tags)
hub.getAudienceManager().createAudience("premium-users", Set.of("vip", "plan:premium"));

// Send to audience
notify.toAudience("premium-users")
    .via(Channel.EMAIL)
    .template("exclusive-offer")
    .param("discount", "30%")
    .send();

Scheduling

Schedule notifications for future delivery with delay-based or absolute-time scheduling. List pending notifications and cancel them before delivery.

Schedule with Delay

Java
// Schedule with relative delay
notify.to(user)
    .via(Channel.PUSH)
    .content("Don't forget your appointment tomorrow!")
    .schedule(Duration.ofHours(24));

Schedule at Specific Time

Java
// Schedule at absolute time
notify.to(user)
    .via(Channel.EMAIL)
    .template("weekly-digest")
    .scheduleAt(Instant.parse("2024-12-25T09:00:00Z"));

List Scheduled Notifications

Java
// List all scheduled notifications
List<ScheduledNotification> pending = hub.getScheduledNotifications();

// Filter by status
List<ScheduledNotification> sent = hub.getScheduledNotifications("SENT");

Cancel Scheduled Notifications

Java
// Cancel before delivery
hub.cancelScheduledNotification(notificationId);

Configuration

application.yml
notify:
  scheduling:
    enabled: true
    pool-size: 2

MCP / AI Usage

Use the scheduling tools from any AI assistant:

Claude / Cursor / Any AI
"Schedule an email to user@test.com in 30 minutes reminding about the meeting"

"Schedule a Slack notification for tomorrow at 9am saying standup starts soon"

"List all scheduled notifications"

"Cancel the scheduled notification with ID abc-123"
MCP tools: Use schedule_notification with either delay (e.g., "5m", "1h30m") or scheduled_at (ISO 8601) for flexible scheduling. Use list_scheduled_notifications and cancel_scheduled_notification to manage pending deliveries.

Event Listeners

Implement the NotificationListener interface to react to notification lifecycle events:

Java
public class MyListener implements NotificationListener {
    public void onSuccess(String channel, String template) {
        log.info("Sent via {} using {}", channel, template);
    }
    public void onFailure(String channel, String template, Exception e) {
        log.error("Failed on {}: {}", channel, e.getMessage());
    }
    public void onScheduled(String channel, String recipient, Duration delay) {
        log.info("Scheduled {} to {} in {}", channel, recipient, delay);
    }
    public void onCancelled(String channel, String recipient) {
        log.info("Cancelled {} to {}", channel, recipient);
    }
}

Register via builder: .listener(new MyListener()) or as a Spring Bean.

application.yml
notify:
  events:
    enabled: true    # Enables Spring event publishing

Circuit Breaker

Per-channel circuit breaker prevents cascading failures. When a channel fails repeatedly, the circuit opens and short-circuits further attempts until a recovery period elapses.

Java — Without Spring Boot
NotifyHub notify = NotifyHub.builder()
    .channel(emailChannel)
    .circuitBreaker(CircuitBreakerConfig.defaults()) // 5 failures → open for 30s
    .build();

// Custom thresholds
NotifyHub notify = NotifyHub.builder()
    .channel(emailChannel)
    .circuitBreaker(CircuitBreakerConfig.custom()
        .failureThreshold(3)
        .openDuration(Duration.ofMinutes(1))
        .windowSize(Duration.ofSeconds(30))
        .build())
    .build();
application.yml — Spring Boot
notify:
  circuit-breaker:
    enabled: true
    failure-threshold: 5
    open-duration: 30s
    window-size: 60s

States: CLOSED (normal) → OPEN (rejecting all sends) → HALF_OPEN (testing recovery with one attempt). The Actuator health endpoint includes circuit breaker state per channel.

You can also add bulkhead isolation to limit concurrent sends per channel:

Java
NotifyHub notify = NotifyHub.builder()
    .channel(emailChannel)
    .bulkhead(BulkheadConfig.defaults())       // 10 concurrent per channel
    .bulkhead(BulkheadConfig.perChannel(5))    // or custom limit
    .build();

Multi-Channel Orchestration

Build escalation workflows that promote through channels if the user doesn't engage:

Java
notify.to(user)
    .orchestrate()
    .first(Channel.EMAIL)
        .template("order-update")
    .ifNoOpen(Duration.ofHours(24))
    .then(Channel.PUSH)
        .content("You have an unread order update")
    .ifNoOpen(Duration.ofHours(48))
    .then(Channel.SMS)
        .content("Order update waiting — check your email")
    .execute();

Each step waits for the specified duration before escalating to the next channel. Combine with delivery tracking to check open/click status.

A/B Testing

Built-in deterministic A/B testing for notifications. Variant assignment uses SHA-256 hashing — the same recipient always gets the same variant:

Java
notify.to(user)
    .via(Channel.EMAIL)
    .subject("Welcome!")
    .abTest("welcome-experiment")
        .variant("control", b -> b.template("welcome-v1"))
        .variant("new-design", b -> b.template("welcome-v2"))
        .split(50, 50);

Supports any number of variants with weighted splits. Deterministic hashing ensures consistent experiences across sends — no external A/B testing service needed.

Quiet Hours

Respect user preferences for notification timing. Implement on your Notifiable:

Java
public class User implements Notifiable {
    @Override
    public QuietHours getQuietHours() {
        return QuietHours.between(
            LocalTime.of(22, 0),  // 10 PM
            LocalTime.of(8, 0),   // 8 AM
            ZoneId.of("America/Sao_Paulo")
        );
    }

    @Override
    public Set<Channel> getOptedOutChannels() {
        return Set.of(Channel.SMS); // User opted out of SMS
    }
}

Notifications sent during quiet hours are delayed to the next allowed window. Opted-out channels are silently skipped.

Test Utilities

TestNotifyHub provides a test-friendly wrapper with capturing channels for all built-in channel types. No mocking needed:

Java — JUnit 5
@Test
void shouldSendWelcomeEmail() {
    TestNotifyHub test = TestNotifyHub.create();

    test.to("user@test.com")
        .via(Channel.EMAIL)
        .subject("Welcome")
        .content("Hello!")
        .send();

    assertThat(test.sent()).hasSize(1);
    assertThat(test.sent("email").get(0).subject()).isEqualTo("Welcome");
}

@AfterEach
void cleanup() {
    test.reset(); // Clear captured notifications between tests
}

TestNotifyHub registers capturing channels for every Channel enum value. Query with sent() (all) or sent("email") (by channel).

Spring Boot Integration

Injection

Java
@Service
public class MyService {
    private final NotifyHub notify;
    public MyService(NotifyHub notify) { this.notify = notify; }
}

Auto-configured channels appear as beans. Only channels with their required properties set are activated.

Full application.yml Reference

application.yml
notify:
  channels:
    email:
      host: smtp.gmail.com
      port: 587
      username: ${GMAIL_USER}
      password: ${GMAIL_PASS}
      from: noreply@myapp.com
      from-name: MyApp
      tls: true

    sms:
      account-sid: ${TWILIO_ACCOUNT_SID}
      auth-token: ${TWILIO_AUTH_TOKEN}
      from-number: "+15551234567"

    whatsapp:
      account-sid: ${TWILIO_ACCOUNT_SID}
      auth-token: ${TWILIO_AUTH_TOKEN}
      from-number: "+14155238886"

    slack:
      webhook-url: ${SLACK_WEBHOOK_URL}
      recipients:
        engineering: "https://hooks.slack.com/services/..."

    telegram:
      bot-token: ${TELEGRAM_BOT_TOKEN}
      chat-id: ${TELEGRAM_CHAT_ID}

    discord:
      webhook-url: ${DISCORD_WEBHOOK_URL}
      username: NotifyBot

    teams:
      webhook-url: ${TEAMS_WEBHOOK_URL}

    push:
      server-key: ${FIREBASE_SERVER_KEY}

    websocket:
      uri: wss://ws.example.com/notifications
      reconnect-enabled: true
      reconnect-delay-ms: 3000
      max-reconnect-attempts: 10

    google-chat:
      webhook-url: ${GOOGLE_CHAT_WEBHOOK_URL}

    twitter:
      api-key: ${TWITTER_API_KEY}
      api-secret: ${TWITTER_API_SECRET}
      access-token: ${TWITTER_ACCESS_TOKEN}
      access-token-secret: ${TWITTER_ACCESS_TOKEN_SECRET}

    linkedin:
      access-token: ${LINKEDIN_ACCESS_TOKEN}
      author-id: "urn:li:person:abc123"

    notion:
      api-key: ${NOTION_API_KEY}
      database-id: ${NOTION_DATABASE_ID}

    twitch:
      client-id: ${TWITCH_CLIENT_ID}
      access-token: ${TWITCH_ACCESS_TOKEN}
      refresh-token: ${TWITCH_REFRESH_TOKEN}
      client-secret: ${TWITCH_CLIENT_SECRET}
      broadcaster-id: "836211004"
      sender-id: "836211004"

    youtube:
      access-token: ${YOUTUBE_ACCESS_TOKEN}
      refresh-token: ${YOUTUBE_REFRESH_TOKEN}
      client-id: ${YOUTUBE_CLIENT_ID}
      client-secret: ${YOUTUBE_CLIENT_SECRET}
      channel-id: ${YOUTUBE_CHANNEL_ID}
      live-chat-id: ${YOUTUBE_LIVE_CHAT_ID}

    instagram:
      access-token: ${INSTAGRAM_ACCESS_TOKEN}
      ig-user-id: ${INSTAGRAM_USER_ID}

    sendgrid:
      api-key: ${SENDGRID_API_KEY}
      from: noreply@myapp.com
      from-name: MyApp

    tiktok-shop:
      app-key: ${TIKTOK_SHOP_APP_KEY}
      app-secret: ${TIKTOK_SHOP_APP_SECRET}
      access-token: ${TIKTOK_SHOP_ACCESS_TOKEN}
      shop-id: ${TIKTOK_SHOP_ID}

    facebook:
      page-access-token: ${FACEBOOK_PAGE_ACCESS_TOKEN}
      page-id: ${FACEBOOK_PAGE_ID}

    aws-sns:
      region: us-east-1
      access-key-id: ${AWS_ACCESS_KEY_ID}
      secret-access-key: ${AWS_SECRET_ACCESS_KEY}
      topic-arn: ${AWS_SNS_TOPIC_ARN}

    mailgun:
      api-key: ${MAILGUN_API_KEY}
      domain: mg.myapp.com
      from: noreply@myapp.com
      region: US

    pagerduty:
      routing-key: ${PAGERDUTY_ROUTING_KEY}
      severity: warning

  retry:
    max-attempts: 3
    strategy: exponential              # none, fixed, exponential

  scheduling:
    enabled: true
    pool-size: 2

  tracking:
    enabled: true
    type: memory                       # memory or jpa
    dlq-enabled: true

  events:
    enabled: true

  rate-limit:
    enabled: true
    use-defaults: true                 # Use API-aware presets per channel
    max-requests: 100                  # Global default
    window: 60s
    channels:                          # Per-channel overrides
      slack:
        max-requests: 2
        window: 1s

  deduplication:
    enabled: true
    ttl: 24h
    strategy: content-hash             # content-hash or explicit-key

  audit:
    enabled: true
    type: memory

  audience:
    enabled: true

  status-webhook:
    url: "https://myapp.com/notify/status"
    timeout-ms: 10000
    signing-secret: ${WEBHOOK_SIGNING_SECRET}

REST API / Swagger

NotifyHub includes a fully documented REST API with Swagger UI (OpenAPI 3.0). When running the demo or Docker image, access the interactive API at:

Bash
# Swagger UI (interactive)
http://localhost:8080/swagger-ui.html

# OpenAPI JSON spec
http://localhost:8080/v3/api-docs

Available Endpoints

MethodEndpointDescription
GET/api/infoAPI info and registered channels
POST/send/emailSend email via SMTP
POST/send/templateSend email with Mustache template
POST/send/notifiableSend to a Notifiable entity
POST/send/smsSend SMS via Twilio
POST/send/whatsappSend WhatsApp via Twilio
POST/send/telegramSend Telegram message
POST/send/discordSend Discord message
POST/send/slackSend Slack message
POST/send/teamsSend Microsoft Teams message
POST/send/google-chatSend Google Chat message
POST/send/pushSend Firebase push notification
POST/send/websocketSend WebSocket message
POST/send/twitterPost tweet on Twitter/X
POST/send/linkedinPublish LinkedIn post
POST/send/notionCreate Notion page
POST/send/multiSend to multiple channels at once
POST/send/fallbackTest fallback chain
POST/send/trackedSend with delivery tracking
POST/send/scheduledSchedule notification for future
POST/send/audienceSend to audience segment
GET/trackingDelivery tracking history
GET/scheduledList scheduled notifications
DELETE/scheduled/{id}Cancel scheduled notification
POST/contactsCreate contact with tags
GET/contactsList all contacts
POST/audiencesCreate audience by tags

Example

Bash
# Send email
curl -X POST "localhost:8080/send/email?to=user@example.com&subject=Hello&body=Hi+from+NotifyHub"

# Send to Slack
curl -X POST "localhost:8080/send/slack?channel=%23general&message=Deploy+complete"

# Send multi-channel (email + slack)
curl -X POST "localhost:8080/send/multi?email=admin@test.com&slackChannel=%23alerts"

# Schedule notification (30s delay)
curl -X POST "localhost:8080/send/scheduled?to=user@test.com&delaySeconds=30&subject=Reminder"
Swagger UI: Open http://localhost:8080/swagger-ui.html in your browser to explore and test all endpoints interactively — no code needed.

Admin Dashboard

NotifyHub ships with a built-in web dashboard at /notify-admin. It provides real-time monitoring, delivery tracking, analytics, and dead letter management — all in a dark/light theme UI with responsive design.

Enable

Add the notify-admin module to your project:

XML
<dependency>
    <groupId>io.notifyhub</groupId>
    <artifactId>notify-admin</artifactId>
</dependency>

That's it. Spring Boot auto-configuration registers the admin controller automatically. Access the dashboard at:

Bash
http://localhost:8080/notify-admin

Pages

The dashboard includes 8 pages, each accessible via the sidebar navigation:

PageRouteDescription
Dashboard/notify-adminOverview with metric cards (sent, failed, pending, dead letters, active channels), recent activity feed, system status indicators, and registered channels with health check.
Analytics/notify-admin/analyticsChart.js charts — volume over time, distribution by channel, success rate, and hourly heatmap.
Tracking/notify-admin/trackingList every delivery receipt with timestamp, channel, recipient, and status. Filter by channel.
Dead Letter Queue/notify-admin/dlqNotifications that failed after all retry attempts. Inspect error details and remove/reprocess entries.
Channels/notify-admin/channelsAll registered channels with availability status and implementation type.
Audit Log/notify-admin/audit-logComplete event history — SENT, FAILED, SCHEDULED, CANCELLED. Filter by event type.
Audiences/notify-admin/audiencesManage contacts and audience segments with tag-based filtering.
Status Webhook/notify-admin/status-webhookView webhook configuration, recent deliveries, and payload details.

Analytics API

The dashboard exposes JSON API endpoints used by the charts, which you can also consume programmatically:

MethodEndpointDescription
GET/notify-admin/api/stats/overviewCount by status (SENT, FAILED, PENDING, etc.)
GET/notify-admin/api/stats/by-channelMessages per channel
GET/notify-admin/api/stats/timeline?days=7Sent/failed per day for the last N days
GET/notify-admin/api/stats/errorsTop 10 error messages

Features

Tip: The dashboard auto-detects which features are enabled. If you haven't configured tracking or DLQ, those sections will show "Not configured" instead of empty data.

Docker

Don't use Java? Run NotifyHub as a REST API or MCP Server with Docker. Just set environment variables for the channels you need.

REST API

Bash
docker run -d -p 8080:8080 \
  -e NOTIFY_CHANNELS_EMAIL_HOST=smtp.gmail.com \
  -e NOTIFY_CHANNELS_EMAIL_PORT=587 \
  -e NOTIFY_CHANNELS_EMAIL_USERNAME=you@gmail.com \
  -e NOTIFY_CHANNELS_EMAIL_PASSWORD=app-password \
  -e NOTIFY_CHANNELS_EMAIL_FROM=you@gmail.com \
  gabrielbbal10/notifyhub-api
Bash — call the API
# Send email
curl -X POST "localhost:8080/send/email?to=user@test.com&subject=Hi&body=Hello!"

# Send Discord
curl -X POST "localhost:8080/send/discord?message=Deploy done!"

# Swagger UI
open http://localhost:8080/swagger-ui.html

MCP Server

claude_desktop_config.json
{
  "mcpServers": {
    "notifyhub": {
      "command": "docker",
      "args": [
        "run", "-i", "--rm",
        "-e", "NOTIFY_CHANNELS_EMAIL_HOST=smtp.gmail.com",
        "-e", "NOTIFY_CHANNELS_EMAIL_FROM=you@gmail.com",
        "gabrielbbal10/notifyhub-mcp"
      ]
    }
  }
}

Environment Variables

Both Docker images use NOTIFY_CHANNELS_* environment variables. Only set the channels you need:

VariableExample
NOTIFY_CHANNELS_EMAIL_HOSTsmtp.gmail.com
NOTIFY_CHANNELS_EMAIL_PORT587
NOTIFY_CHANNELS_EMAIL_USERNAMEyou@gmail.com
NOTIFY_CHANNELS_EMAIL_PASSWORDapp-password
NOTIFY_CHANNELS_EMAIL_FROMyou@gmail.com
NOTIFY_CHANNELS_SLACK_WEBHOOK_URLhttps://hooks.slack.com/...
NOTIFY_CHANNELS_TELEGRAM_BOT_TOKEN123456:ABC-DEF
NOTIFY_CHANNELS_TELEGRAM_CHAT_ID987654321
NOTIFY_CHANNELS_DISCORD_WEBHOOK_URLhttps://discord.com/api/webhooks/...
NOTIFY_CHANNELS_SMS_ACCOUNT_SIDAC...
NOTIFY_CHANNELS_SMS_AUTH_TOKENtoken
NOTIFY_CHANNELS_SMS_FROM_NUMBER+15551234567
NOTIFY_CHANNELS_SENDGRID_API_KEYSG.xxxxxx
NOTIFY_CHANNELS_SENDGRID_FROMnoreply@myapp.com
NOTIFY_CHANNELS_TIKTOK_SHOP_APP_KEYapp-key
NOTIFY_CHANNELS_TIKTOK_SHOP_APP_SECRETapp-secret
NOTIFY_CHANNELS_FACEBOOK_PAGE_ACCESS_TOKENEAAGx...
NOTIFY_CHANNELS_FACEBOOK_PAGE_ID123456789

MCP Server

NotifyHub includes an MCP (Model Context Protocol) server with 33 tools for AI agent integration. Connect it to Claude Desktop, Claude Code, Cursor, or any MCP-compatible client.

All 33 Tools

CategoryToolDescription
Channel Sendsend_emailSend via SMTP
send_smsSend via Twilio
send_whatsappSend via Twilio WhatsApp
send_slackSend via webhook
send_telegramSend via Bot API
send_discordSend via webhook
send_teamsSend via webhook
send_google_chatSend via webhook
send_pushFirebase Cloud Messaging
send_twitterTwitter/X API v2
send_linkedinLinkedIn REST API
send_notionNotion Database API
send_twitchTwitch Helix API
send_youtubeYouTube Live Chat
send_instagramInstagram Graph API
send_tiktok_shopTikTok Shop API (HMAC-SHA256)
send_facebookFacebook Graph API (Page + Messenger)
Unifiedsend_notificationSend via any channel
send_batchSame message to multiple recipients
send_multi_channelSame message across multiple channels
send_to_audienceSend to a named audience
Contactscreate_contactCreate contact with tags
list_contactsList/filter contacts
create_audienceCreate audience by tag filter
list_audiencesList audiences
Schedulingschedule_notificationSchedule for future delivery (delay or absolute time)
list_scheduled_notificationsList pending/sent/cancelled scheduled notifications
cancel_scheduled_notificationCancel a pending scheduled notification
Monitoringlist_channelsList configured channels
list_delivery_receiptsView delivery history
list_dead_lettersView failed notifications
get_analyticsDelivery analytics and stats
check_email_statusCheck email delivery status (SendGrid: delivered, opened, bounced, etc.)

Example Prompts

Once connected, you can use natural language:

OAuth Token Refresh

NotifyHub supports automatic OAuth 2.0 token refresh for YouTube and Twitch. The OAuthTokenManager is thread-safe using ReentrantReadWriteLock and proactively refreshes tokens 5 minutes before expiry.

Java
OAuthTokenManager manager = OAuthTokenManager.builder()
    .tokenEndpoint("https://oauth2.googleapis.com/token")
    .refreshToken("1//0eXyz...")
    .clientId("client-id")
    .clientSecret("client-secret")
    .initialAccessToken("ya29...")  // Optional seed token
    .refreshBufferSeconds(300)      // Default: 300 (5 min before expiry)
    .timeoutSeconds(10)             // HTTP timeout
    .build();

String token = manager.getAccessToken();  // Auto-refresh if needed
manager.forceRefresh();                   // Force refresh (e.g., after 401)
Automatic: Token refresh is enabled automatically when you provide refreshToken + clientSecret (and clientId for YouTube) in the channel config. If only a static accessToken is set, behavior is backward-compatible with no refresh.

How It Works

Message Queues

For high-throughput or distributed systems, enqueue notifications to RabbitMQ or Kafka. A consumer picks them up and sends via NotifyHub automatically.

RabbitMQ

Module: notify-queue-rabbitmq

application.yml
notify:
  queue:
    rabbitmq:
      enabled: true
      queue-name: notifyhub-notifications
      exchange-name: notifyhub-exchange
      routing-key: notification

Apache Kafka

Module: notify-queue-kafka

application.yml
notify:
  queue:
    kafka:
      enabled: true
      topic: notifyhub-notifications
      consumer:
        group-id: notifyhub-group

Testing

Running Tests

Bash
# All modules
mvn test

# Specific module
mvn test -pl notify-core
mvn test -pl notify-channels/notify-youtube
mvn test -pl notify-spring-boot-starter

Test Patterns

Project Modules

ModuleDescription
notify-coreCore API, interfaces, fluent builder, templates, retry, rate limiting, DLQ, audiences.
notify-emailSMTP channel (JavaMail).
notify-smsSMS + WhatsApp (Twilio).
notify-slackSlack webhooks.
notify-telegramTelegram Bot API.
notify-discordDiscord webhooks.
notify-teamsMicrosoft Teams webhooks.
notify-push-firebaseFirebase Cloud Messaging.
notify-webhookGeneric HTTP webhooks.
notify-websocketJava WebSocket (java.net.http).
notify-google-chatGoogle Chat webhooks.
notify-twitterTwitter/X API v2.
notify-linkedinLinkedIn REST API.
notify-notionNotion Database API.
notify-twitchTwitch Helix API.
notify-youtubeYouTube Data API v3.
notify-instagramInstagram Graph API.
notify-sendgridSendGrid Email API (with delivery tracking).
notify-tiktok-shopTikTok Shop API (HMAC-SHA256).
notify-facebookFacebook Graph API (Page + Messenger).
notify-spring-boot-starterAuto-configuration + properties.
notify-mcpMCP Server for AI assistants (33 tools).
notify-demoDemo application.