# How to Integrate Replit with Smooch (now Zendesk Sunshine Conversations)

- Tool: Replit
- Difficulty: Advanced
- Time required: 45 minutes
- Last updated: March 2026

## TL;DR

To integrate Replit with Zendesk Sunshine Conversations (formerly Smooch), create a Sunshine Conversations app in the Zendesk developer console, store your App ID, Key ID, and Key Secret in Replit Secrets (lock icon 🔒), and use the Sunshine Conversations REST API from an Express or Flask server to send and receive messages across WhatsApp, Facebook Messenger, SMS, and web chat through a unified omnichannel API.

## Why Integrate Replit with Zendesk Sunshine Conversations?

Zendesk Sunshine Conversations is the messaging layer that powers multi-channel communication for the Zendesk platform. When you integrate it with Replit, you gain access to a unified API that lets you reach users on whatever channel they prefer — WhatsApp, Facebook Messenger, SMS, Apple Messages for Business, Line, Telegram, and custom web/mobile SDK channels — all through a single consistent API surface. Instead of maintaining separate integrations for each messaging platform, Sunshine Conversations abstracts the differences and handles the channel-specific protocol details.

This is an Advanced integration for three reasons. First, authentication uses JWT (JSON Web Token) signed with your Key Secret rather than a simple Bearer token — you must generate a correctly signed JWT for every API call. Second, the Sunshine Conversations data model introduces Apps, Users, Conversations, and Messages as distinct entities with relationships that must be understood to build correct API calls. Third, a production-grade webhook handler requires signature verification, idempotency handling (Sunshine Conversations retries webhooks on failure), and channel-aware message routing since different channels support different message types.

For teams already using Zendesk for customer support, Sunshine Conversations is the bridge that connects incoming messages from any channel into the Zendesk agent workspace. Your Replit backend can act as a bot layer that handles common queries automatically before escalating to a human agent, creating a hybrid automation and human support workflow.

## Before you start

- A Zendesk account with Sunshine Conversations enabled (Suite Professional or above, or a standalone Sunshine Conversations subscription)
- A Sunshine Conversations App created in the Zendesk developer console with an API Key generated
- A Replit account with a Node.js or Python Repl created
- Understanding of JWT (JSON Web Token) authentication and how tokens are signed
- At least one messaging channel configured in your Sunshine Conversations App (WhatsApp Business, Facebook Messenger, Twilio SMS, or web SDK)
- Familiarity with Express (Node.js) or Flask (Python) and webhook concepts

## Step-by-step guide

### 1. Create a Sunshine Conversations App and generate API keys

Log in to your Zendesk account and navigate to the Sunshine Conversations section. In the Zendesk Admin Center, go to Platform > Sunshine Conversations (or access it at app.smooch.io if you have a standalone Smooch account). Create a new App or use an existing one. An App in Sunshine Conversations is the container for your messaging configuration — it holds the connected channel integrations, webhook configurations, and API keys. Once inside your App, navigate to Settings > API Keys and create a new key pair. The system will generate a Key ID (starting with 'app_') and a Key Secret (a long random string). These two values, combined with your App ID (visible in the URL when viewing your App), are the three credentials you need for authentication. Copy all three immediately — the Key Secret is only shown once. Store them in Replit Secrets as SMOOCH_APP_ID, SMOOCH_KEY_ID, and SMOOCH_KEY_SECRET. Also configure at least one messaging channel in your App (e.g., Twilio SMS or a Web Messenger) before testing — you cannot send messages without a channel configured. Note: the API base URL changed from api.smooch.io to api.smooch.io or the newer https://api.zendesk.com/sc depending on your Zendesk plan — check your dashboard for the correct API host.

```
# Python — install required packages
# Run in Replit Shell:
# pip install requests pyjwt flask
# Or add to requirements.txt:
# requests>=2.28.0
# PyJWT>=2.8.0
# flask>=2.3.0

# For Node.js:
# npm install express axios jsonwebtoken

# Verify PyJWT is installed
try:
    import jwt
    print(f'PyJWT version: {jwt.__version__}')
except ImportError:
    print('Run: pip install PyJWT')
```

**Expected result:** A Sunshine Conversations App is created with an App ID, Key ID, and Key Secret. All three are stored in Replit Secrets. At least one messaging channel is configured in the App.

### 2. Generate JWT tokens for API authentication

Sunshine Conversations uses JWT (JSON Web Token) authentication for all API calls. Unlike Bearer token auth where you include a pre-generated token, with JWT you generate a new signed token for each session or request batch. The JWT must be signed with the HS256 algorithm using your Key Secret as the signing key. The JWT payload must include the 'scope' claim set to 'app' (for app-level access to all conversations) or 'appUser' (for user-scoped access). The header must set the 'kid' claim to your Key ID so the API knows which key was used to sign the token. Tokens should include standard JWT claims: 'iat' (issued at, Unix timestamp) and optionally 'exp' (expiration, recommended to be iat + 1 hour to limit token lifetime). Build a utility function that generates a fresh JWT — call this function at the start of each API request rather than caching tokens long-term. In Python, use the PyJWT library; in Node.js, use the jsonwebtoken package. Both handle the HS256 signing algorithm. Once you have a valid JWT, include it in every API request as the Authorization header: 'Bearer YOUR_JWT'.

```
// Node.js — JWT generation utility (jwt-utils.js)
const jwt = require('jsonwebtoken');

const KEY_ID = process.env.SMOOCH_KEY_ID;
const KEY_SECRET = process.env.SMOOCH_KEY_SECRET;

function generateAppToken() {
  if (!KEY_ID || !KEY_SECRET) {
    throw new Error('SMOOCH_KEY_ID and SMOOCH_KEY_SECRET must be set in Replit Secrets (lock icon 🔒)');
  }
  const payload = { scope: 'app' };
  const options = {
    header: { alg: 'HS256', typ: 'JWT', kid: KEY_ID },
    expiresIn: '1h'
  };
  return jwt.sign(payload, KEY_SECRET, options);
}

function getAuthHeaders() {
  return { Authorization: `Bearer ${generateAppToken()}` };
}

module.exports = { generateAppToken, getAuthHeaders };

// Test JWT generation
try {
  const token = generateAppToken();
  console.log(`JWT generated successfully (${token.length} chars)`);
  const decoded = jwt.decode(token, { complete: true });
  console.log('Header:', JSON.stringify(decoded.header));
  console.log('Payload scope:', decoded.payload.scope);
} catch (err) {
  console.error('JWT generation failed:', err.message);
}
```

**Expected result:** The JWT generation script outputs a valid token with the correct header (kid = your Key ID, alg = HS256) and payload (scope = app). No errors are thrown.

### 3. Send messages via the Sunshine Conversations API

With JWT generation working, you can now send messages to users. The API base URL is https://api.smooch.io/v2. To send a message, you need the App ID, the Conversation ID (or User ID to start a new conversation), and the message content formatted for the target channel. The main send endpoint is POST /apps/{appId}/conversations/{conversationId}/messages. The message body varies by type: text messages use type 'text' with a text field; image messages use type 'image' with a mediaUrl; location messages use type 'location' with lat/long. Not all message types are supported on all channels — WhatsApp only accepts template messages for business-initiated conversations unless the user has messaged in the last 24 hours. To create a new conversation or find an existing user's conversation, first get or create the user via POST /apps/{appId}/users (passing an externalId that maps to your own user identifier), then list their conversations via GET /apps/{appId}/conversations?filter[userId}={userId}. Build a send function that abstracts the conversation lookup and message creation into a single call: given an externalUserId and message text, it finds or creates the conversation and posts the message.

```
# Python — send messages via Sunshine Conversations (send_message.py)
import requests
import jwt
import time
import os

APP_ID = os.environ['SMOOCH_APP_ID']
KEY_ID = os.environ['SMOOCH_KEY_ID']
KEY_SECRET = os.environ['SMOOCH_KEY_SECRET']
BASE_URL = 'https://api.smooch.io/v2'

def generate_jwt():
    payload = {'scope': 'app', 'iat': int(time.time())}
    headers_extra = {'kid': KEY_ID}
    return jwt.encode(payload, KEY_SECRET, algorithm='HS256', headers=headers_extra)

def get_headers():
    return {'Authorization': f'Bearer {generate_jwt()}', 'Content-Type': 'application/json'}

def get_or_create_user(external_id):
    """Get or create a Sunshine Conversations user by external ID."""
    resp = requests.post(
        f'{BASE_URL}/apps/{APP_ID}/users',
        json={'externalId': external_id},
        headers=get_headers()
    )
    return resp.json().get('user', {})

def get_user_conversation(user_id):
    """Get the default conversation for a user."""
    resp = requests.get(
        f'{BASE_URL}/apps/{APP_ID}/conversations',
        params={'filter[userId]': user_id},
        headers=get_headers()
    )
    convos = resp.json().get('conversations', [])
    if convos:
        return convos[0]['id']
    # Create a conversation if none exists
    create_resp = requests.post(
        f'{BASE_URL}/apps/{APP_ID}/conversations',
        json={'type': 'personal', 'participants': [{'userId': user_id}]},
        headers=get_headers()
    )
    return create_resp.json().get('conversation', {}).get('id')

def send_text_message(external_user_id, text):
    """Send a text message to a user by their external ID."""
    user = get_or_create_user(external_user_id)
    user_id = user.get('id')
    convo_id = get_user_conversation(user_id)
    payload = {
        'author': {'type': 'business'},
        'content': {'type': 'text', 'text': text}
    }
    resp = requests.post(
        f'{BASE_URL}/apps/{APP_ID}/conversations/{convo_id}/messages',
        json=payload,
        headers=get_headers()
    )
    result = resp.json()
    print(f'Message sent: {result.get("messages", [{}])[0].get("id", "unknown")}')
    return result

# Send a test message
send_text_message('test-user-001', 'Hello from Replit! This is a test message.')
```

**Expected result:** The send function creates or retrieves the user, finds or creates a conversation, and sends a text message. The message ID is returned and the message appears in the conversation on the configured channel.

### 4. Configure and handle webhooks

Webhooks are how Sunshine Conversations notifies your server of incoming messages and conversation events. To configure a webhook, make a POST request to /apps/{appId}/integrations (or use the Sunshine Conversations dashboard UI to add a webhook integration). Specify your webhook URL, the events to subscribe to (message:appUser for incoming messages, conversation:read for read receipts, etc.), and optionally a shared secret for payload verification. Sunshine Conversations will POST a JSON payload to your URL whenever a subscribed event occurs. Your webhook handler must return HTTP 200 within 5 seconds or Sunshine Conversations will retry. Implement webhook signature verification using the shared secret: Sunshine Conversations sends an X-Api-Key header with the webhook secret on each request — verify this matches your SMOOCH_WEBHOOK_SECRET before processing the payload. Your webhook handler must be idempotent since retries can deliver the same event multiple times. Deploy as Autoscale to ensure the webhook endpoint is always reachable — Sunshine Conversations will stop retrying after a configurable number of failed attempts, causing message loss.

```
// Node.js — Sunshine Conversations webhook handler (webhook.js)
const express = require('express');
const crypto = require('crypto');

const app = express();

// Use raw body for signature verification
app.use(express.json());

const WEBHOOK_SECRET = process.env.SMOOCH_WEBHOOK_SECRET;
const APP_ID = process.env.SMOOCH_APP_ID;

function verifyWebhookSignature(req) {
  if (!WEBHOOK_SECRET) return true; // Skip verification if no secret configured
  const receivedKey = req.headers['x-api-key'];
  return receivedKey === WEBHOOK_SECRET;
}

app.post('/webhook', (req, res) => {
  // Verify webhook authenticity
  if (!verifyWebhookSignature(req)) {
    console.error('Invalid webhook signature');
    return res.status(401).json({ error: 'Invalid signature' });
  }

  const payload = req.body;
  const appId = payload.app?.id;

  if (appId !== APP_ID) {
    return res.status(400).json({ error: 'App ID mismatch' });
  }

  // Always respond 200 first, then process
  res.json({ received: true });

  const events = payload.events || [];
  for (const event of events) {
    const type = event.type;
    console.log(`Event: ${type}`);

    if (type === 'conversation:message') {
      const msg = event.payload?.message;
      const author = event.payload?.conversation;
      if (msg?.author?.type === 'user') {
        const text = msg.content?.text || '[non-text message]';
        const channel = msg.source?.type || 'unknown';
        console.log(`Incoming [${channel}]: ${text}`);
        // Add routing logic here
      }
    }
  }
});

app.listen(3000, '0.0.0.0', () => console.log('Sunshine Conversations webhook server running'));
```

**Expected result:** Incoming messages from any connected channel trigger the webhook handler. The server logs the channel type and message text, responds with 200, and is ready for routing logic.

### 5. Deploy as Autoscale and implement multi-channel routing

For a production Sunshine Conversations integration, your Replit server must be reliably available since webhook delivery failures directly mean missed customer messages. Deploy as Autoscale: click Deploy in the Replit toolbar, select Autoscale, and set the start command. After deploying, copy the production URL from the Deployments panel and update your Sunshine Conversations webhook URL (via the API or the dashboard). Add a .replit config file with the correct port binding and deployment target. Implement channel-aware routing in your webhook handler: extract the message source type (whatsapp, messenger, sms, web, etc.) from the event payload and apply different response logic per channel. WhatsApp requires template messages for proactive notifications; SMS has 160-character limits; web channels support rich message types. Build a message router that abstracts these differences so your business logic does not need to be channel-aware. Additionally, add a simple deduplication store (a JavaScript Set or Python set in memory, or a Redis-backed store for production) to track processed message IDs and skip retried duplicates. This prevents double-processing when Sunshine Conversations retries webhook delivery.

```
# Python — multi-channel message router with deduplication (router.py)
from flask import Flask, request, jsonify
import os

app = Flask(__name__)

WEBHOOK_SECRET = os.environ.get('SMOOCH_WEBHOOK_SECRET', '')
processed_ids = set()  # In production, use Redis for persistence

CHANNEL_HANDLERS = {
    'whatsapp': lambda text: f'[WhatsApp template response for: {text[:50]}]',
    'sms': lambda text: text[:160],  # SMS length limit
    'messenger': lambda text: text,
    'web': lambda text: text,
}

@app.route('/webhook', methods=['POST'])
def webhook():
    # Verify webhook secret
    if WEBHOOK_SECRET and request.headers.get('x-api-key') != WEBHOOK_SECRET:
        return jsonify({'error': 'Unauthorized'}), 401

    payload = request.get_json()
    res = jsonify({'received': True}), 200  # Respond immediately

    events = payload.get('events', [])
    for event in events:
        event_id = event.get('id', '')
        if event_id in processed_ids:
            print(f'Skipping duplicate event: {event_id}')
            continue
        processed_ids.add(event_id)

        if event.get('type') == 'conversation:message':
            msg = event.get('payload', {}).get('message', {})
            if msg.get('author', {}).get('type') == 'user':
                channel = msg.get('source', {}).get('type', 'unknown')
                text = msg.get('content', {}).get('text', '')
                handler = CHANNEL_HANDLERS.get(channel, lambda t: t)
                formatted = handler(text)
                print(f'[{channel}] Received: {text}')
                print(f'[{channel}] Would send: {formatted}')
                # Call send_text_message(user_id, formatted) here

    return res

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=3000)
```

**Expected result:** The deployed Autoscale server receives webhooks from all connected channels, deduplicates events, routes messages to channel-specific handlers, and responds reliably within the 5-second webhook timeout.

## Best practices

- Store SMOOCH_APP_ID, SMOOCH_KEY_ID, SMOOCH_KEY_SECRET, and SMOOCH_WEBHOOK_SECRET in Replit Secrets (lock icon 🔒) — never hardcode credentials or JWT signing keys
- Generate a fresh JWT for each API call or request batch — do not cache JWTs for more than 30 minutes to limit the window of exposure for a compromised token
- Return HTTP 200 from webhook handlers immediately and process events asynchronously to prevent Sunshine Conversations from triggering retries due to timeout
- Implement message ID deduplication in your webhook handler — Sunshine Conversations retries on failure, and duplicate processing can cause duplicate messages or double-counted events
- Deploy as Autoscale rather than keeping the Repl in development mode — missed webhooks due to sleeping Repls directly translate to missed customer messages
- Handle channel-specific message type restrictions in a routing layer: WhatsApp templates for proactive messages, SMS character limits, rich messages only for web/Messenger
- Verify the X-Api-Key header on all incoming webhooks against SMOOCH_WEBHOOK_SECRET to prevent unauthorized parties from injecting fake events into your webhook handler
- Use the externalId field when creating Sunshine Conversations users to map them to your own user IDs, making it easy to look up conversations for any user without storing Sunshine Conversations internal IDs

## Use cases

### Omnichannel Customer Support Bot

Your Replit server receives webhook events from Sunshine Conversations whenever a customer sends a message on any connected channel (WhatsApp, Messenger, SMS). The server matches the message against a keyword-based FAQ database and sends an automated reply through the same channel. If no match is found, it creates a Zendesk ticket and notifies the support team, providing a seamless escalation path from bot to human.

Prompt example:

```
Build an Express server with a POST /webhook endpoint that receives Sunshine Conversations webhook events, extracts the message text and channel type, matches against a FAQ map object keyed by keywords, and sends an automated reply via the Sunshine Conversations API. For unmatched messages, log the conversation ID for human follow-up. Store SMOOCH_APP_ID, SMOOCH_KEY_ID, SMOOCH_KEY_SECRET, and SMOOCH_WEBHOOK_SECRET in Replit Secrets.
```

### Proactive Notification System

When specific events occur in your application (order shipped, appointment reminder, subscription renewing), your Replit backend looks up the customer's preferred messaging channel from your database, creates a Sunshine Conversations message to that user on their preferred channel, and sends a formatted notification. WhatsApp users receive a template message (required for business-initiated WhatsApp), while SMS users receive plain text.

Prompt example:

```
Build a Flask endpoint at /notify that accepts a JSON payload with user ID, channel type (whatsapp or sms), and notification data (event type, order number, estimated delivery). Format the message appropriately for the channel (template for WhatsApp, plain text for SMS) and send via the Sunshine Conversations API. Store app credentials in Replit Secrets.
```

### Cross-Channel Conversation Aggregator

Your customer contacts you on WhatsApp on Monday and via your website chat on Thursday. Sunshine Conversations links these into a single user profile if configured correctly. Your Replit backend queries the Conversations API to retrieve the full cross-channel history for a given user, formats it into a unified timeline, and displays it in your custom CRM — giving agents a complete picture without switching between channel-specific inboxes.

Prompt example:

```
Build an Express endpoint at /customer-history/:userId that calls the Sunshine Conversations API to list all conversations for the given user, fetches messages from each conversation, merges and sorts them by timestamp, and returns a unified JSON timeline with channel source labels for each message.
```

## Troubleshooting

### API returns 401 Unauthorized with 'invalid token' even though the JWT looks correct

Cause: The JWT may have an incorrect 'kid' header claim (should equal the Key ID), wrong algorithm (must be HS256), or the Key Secret used for signing does not match the Key ID. Another common cause is clock skew — if the 'iat' claim is in the future (server clock is ahead), some servers reject the token.

Solution: Verify the JWT header includes kid matching your SMOOCH_KEY_ID and alg set to 'HS256'. Use jwt.decode() without verification to inspect the generated token. Ensure you are signing with the Key Secret (not the App ID or Key ID). Sync your server clock or subtract a few seconds from the iat timestamp.

```
// Debug: inspect generated JWT without verifying
const jwt = require('jsonwebtoken');
const token = generateAppToken();
const decoded = jwt.decode(token, { complete: true });
console.log('Header:', JSON.stringify(decoded.header));
console.log('Payload:', JSON.stringify(decoded.payload));
```

### Webhooks are received but the same event is processed multiple times

Cause: Sunshine Conversations retries webhook delivery if your server returns a non-200 response or times out. If your handler takes longer than 5 seconds to process, the retry triggers a duplicate delivery.

Solution: Return HTTP 200 immediately at the start of the webhook handler and process the event asynchronously. Implement event ID deduplication — store processed event IDs and skip any event whose ID is already in the processed set.

```
# Respond immediately, then process
@app.route('/webhook', methods=['POST'])
def webhook():
    payload = request.get_json()
    # Return 200 FIRST
    response = jsonify({'received': True})
    # Process asynchronously (simplified - use threading.Thread in production)
    process_events(payload.get('events', []))
    return response, 200
```

### WhatsApp messages fail with 'template required' or 'outside 24-hour window' error

Cause: WhatsApp Business API requires that business-initiated messages (sent outside a 24-hour window after the last user message) use pre-approved message templates. Sending freeform text in this scenario is not permitted by WhatsApp.

Solution: Create and get approval for WhatsApp message templates in your Meta Business Manager account. When sending proactive notifications to WhatsApp users, use the template message type instead of text. Check whether the user has messaged within the last 24 hours — if so, freeform text is allowed; if not, use a template.

```
// Send WhatsApp template message
const templatePayload = {
  author: { type: 'business' },
  content: {
    type: 'template',
    template: {
      name: 'order_shipped',
      language: { policy: 'deterministic', code: 'en' },
      components: [{ type: 'body', parameters: [{ type: 'text', text: orderNumber }] }]
    }
  }
};
```

### Webhook events stop arriving after the Replit Repl goes to sleep

Cause: The Repl is not deployed and went to sleep due to inactivity. Sunshine Conversations will retry failed webhook deliveries a limited number of times, after which events are dropped.

Solution: Deploy the Repl as an Autoscale deployment to get a stable always-on production URL. Update the webhook URL in Sunshine Conversations to the production deployment URL, not the development preview URL. Production Autoscale deployments remain available even when not actively processing requests.

## Frequently asked questions

### How do I connect Replit to Zendesk Sunshine Conversations?

Create a Sunshine Conversations App in the Zendesk developer console, generate an API Key (Key ID and Key Secret), store SMOOCH_APP_ID, SMOOCH_KEY_ID, and SMOOCH_KEY_SECRET in Replit Secrets (lock icon 🔒), and generate a JWT signed with HS256 for each API request. Use the JWT as a Bearer token in the Authorization header of all API calls.

### Does Replit work with Sunshine Conversations for free?

Sunshine Conversations is part of Zendesk's paid platform tiers (Suite Professional and above) or available as a standalone subscription. Replit's free tier is sufficient for development and testing, but a paid Replit Autoscale deployment is necessary for production webhook servers that must be always available.

### What is the difference between Smooch and Zendesk Sunshine Conversations?

Smooch was an independent company that built the omnichannel messaging API. Zendesk acquired Smooch in 2019 and rebranded it as Sunshine Conversations. The API and core architecture are the same product, just under a new brand. Older documentation and tutorials may still refer to it as Smooch, and the API domain (api.smooch.io) still works.

### Why do I need to generate a JWT instead of using a simple API key?

Sunshine Conversations uses JWT authentication to allow scoped access (app-level vs user-level) and token expiry enforcement, which is more secure than long-lived API keys. The JWT is signed with your Key Secret using HS256, allowing the API to verify the token's authenticity without storing it. Use PyJWT in Python or the jsonwebtoken package in Node.js to handle JWT generation.

### How do I send WhatsApp messages through Sunshine Conversations from Replit?

Connect a WhatsApp Business account to your Sunshine Conversations App, create approved message templates in Meta Business Manager for business-initiated messages (sent when the user has not contacted you in the last 24 hours), and use the template message type in your API call. Freeform WhatsApp messages are only permitted within a 24-hour window after the customer's last message.

---

Source: https://www.rapidevelopers.com/replit-integration/smooch-now-zendesk-sunshine-conversations
© RapidDev — https://www.rapidevelopers.com/replit-integration/smooch-now-zendesk-sunshine-conversations
