Sendable Docs
Node.js

Express Integration

Build a REST API with Express and Sendable WhatsApp API.

Project Setup

mkdir sendable-express-api
cd sendable-express-api
npm init -y
npm install express cors dotenv @sendable-dev/sdk
npm install -D @types/express @types/cors @types/node typescript ts-node nodemon

TypeScript Configuration

Create tsconfig.json:

{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "./dist"
  },
  "include": ["src/**/*"]
}

Project Structure

src/
├── config/
│   └── sendable.ts
├── routes/
│   ├── messages.ts
│   └── webhooks.ts
├── middleware/
│   └── auth.ts
└── index.ts

Configuration

Create src/config/sendable.ts:

import { Sendable } from '@sendable-dev/sdk'

if (!process.env.SENDABLE_API_KEY) {
  throw new Error('SENDABLE_API_KEY is required')
}

export const sendable = new Sendable({
  apiKey: process.env.SENDABLE_API_KEY,
  apiKeyScope: 'session',
  baseUrl: process.env.SENDABLE_API_URL,
  webhookSecret: process.env.SENDABLE_WEBHOOK_SECRET,
})

Authentication Middleware

Create src/middleware/auth.ts:

import { Request, Response, NextFunction } from 'express'

// Simple API key auth for your own API
export function authMiddleware(req: Request, res: Response, next: NextFunction) {
  const apiKey = req.headers['authorization']?.replace('Bearer ', '')
  
  if (apiKey !== process.env.API_KEY) {
    return res.status(401).json({ error: 'Unauthorized' })
  }
  
  next()
}

Messages Route

Create src/routes/messages.ts:

import { Router } from 'express'
import { sendable } from '../config/sendable'

const router = Router()

// Send a message
router.post('/send', async (req, res) => {
  try {
    const { chatId, text } = req.body

    if (!chatId || !text) {
      return res.status(400).json({
        error: 'chatId and message text are required'
      })
    }

    const response = await sendable.messages.send({
      chatId,
      text: { content: text },
    })

    res.json({
      success: true,
      messageId: response.messageId,
      status: response.status
    })
  } catch (error) {
    console.error('Error sending message:', error)
    res.status(500).json({ error: 'Failed to send message' })
  }
})

// Get message status
router.get('/:messageId/status', async (req, res) => {
  try {
    const { messageId } = req.params
    const message = await sendable.messages.get(messageId)
    res.json(message)
  } catch (error) {
    console.error('Error getting status:', error)
    res.status(500).json({ error: 'Failed to get message status' })
  }
})

export default router

Webhook Route

Create src/routes/webhooks.ts:

import express from 'express'
import { Router } from 'express'
import { sendable } from '../config/sendable'

const router = Router()

// Webhook handler
router.post('/', express.raw({ type: 'application/json' }), async (req, res) => {
  const payload = req.body.toString()

  let event
  try {
    event = await sendable.webhooks.verifyAndParse(payload, req.headers)
  } catch {
    return res.status(401).json({ error: 'Invalid signature' })
  }

  // Process event asynchronously
  handleWebhookEvent(event).catch(console.error)

  // Respond immediately
  res.json({ received: true })
})

async function handleWebhookEvent(event: any) {
  console.log(`[${new Date().toISOString()}] Event: ${event.type}`)

  switch (event.type) {
    case 'message.delivered':
      // Update database, notify user, etc.
      console.log(`Message ${event.data.id} delivered`)
      break

    case 'message.read':
      console.log(`Message ${event.data.id} read by ${event.data.to}`)
      break

    case 'message.failed':
      console.error(`Message ${event.data.id} failed with status ${event.data.status}`)
      // Handle failure - retry, notify, etc.
      break

    case 'message.received':
      console.log(`Received from ${event.data.from}: ${event.data.text?.content}`)
      // Handle incoming message - auto-reply, store, etc.
      break

    case 'session.socket-disconnected':
      console.error(`Session ${event.data.sessionId} disconnected`)
      // Alert admin, attempt reconnect, etc.
      break
  }
}

export default router

Main Server

Create src/index.ts:

import express from 'express'
import cors from 'cors'
import dotenv from 'dotenv'

dotenv.config()

import messagesRouter from './routes/messages'
import webhooksRouter from './routes/webhooks'
import { authMiddleware } from './middleware/auth'

const app = express()
const PORT = process.env.PORT || 3000

// Middleware
app.use(cors())

// Public webhook route (no auth - uses signature verification)
app.use('/webhook', webhooksRouter)

// JSON parsing for the rest of your API
app.use(express.json())

// Protected API routes
app.use('/api/messages', authMiddleware, messagesRouter)

// Health check
app.get('/health', (req, res) => {
  res.json({ status: 'ok', timestamp: new Date().toISOString() })
})

// Start server
app.listen(PORT, () => {
  console.log(`Server running on http://localhost:${PORT}`)
})

Environment Variables

Create .env:

## Sendable
SENDABLE_API_KEY=your_sendable_api_key
SENDABLE_WEBHOOK_SECRET=whsec_your_svix_webhook_secret

## Your API
API_KEY=your_internal_api_key

## Server
PORT=3000

Running the Server

## Development
npx nodemon src/index.ts

## Production
npm run build
npm start

API Usage Examples

Send a message

curl -X POST http://localhost:3000/api/messages/send \
  -H "Authorization: Bearer your_internal_api_key" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "6281234567890",
    "text": "Hello from Express!"
  }'

Get message status

curl http://localhost:3000/api/messages/msg_abc123/status \
  -H "Authorization: Bearer your_internal_api_key"

Production Deployment

  1. Use a process manager like PM2:
npm install -g pm2
pm2 start dist/index.js --name sendable-api
  1. Set up a reverse proxy with Nginx
  2. Use HTTPS for webhook endpoints
  3. Monitor logs and set up alerts

On this page