Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# CCAI API Configuration
CCAI_CLIENT_ID=your_client_id_here
CCAI_API_KEY=your_api_key_here

# Optional: used by the webhook example server (webhook-server.js / express-webhook.ts)
# CCAI_WEBHOOK_SECRET=
# PORT=3000
32 changes: 17 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ const uploaded = await ccai.mms.uploadImageToSignedUrl(

// Step 3 — (Optional) Confirm file is available
const stored = await ccai.mms.checkFileUploaded(fileKey);
console.log('File URL:', stored?.url);
console.log('File URL:', stored?.storedUrl);

// Step 4a — Send to multiple recipients using the uploaded fileKey
const bulkResponse = await ccai.mms.send(
Expand Down Expand Up @@ -164,11 +164,12 @@ const singleResponse = await ccai.mms.sendSingle(
// ── Progress tracking ────────────────────────────────────────────────────────
const options: SMSOptions = {
timeout: 60000,
retries: 3,
onProgress: (status: string) => console.log(`Progress: ${status}`)
};
```

`onProgress` is supported by SMS, MMS, and Email sends. `timeout` applies to MMS sends only.

### Brands

Register and manage brands for TCR (The Campaign Registry) business verification.
Expand Down Expand Up @@ -249,6 +250,8 @@ const campaign = await ccai.campaigns.create({
subUseCases: ['CUSTOMER_CARE', 'TWO_FACTOR_AUTHENTICATION', 'ACCOUNT_NOTIFICATION'],
description: 'Security codes and support messaging.',
messageFlow: 'Users opt-in via signup form at https://example.com/signup',
termsLink: 'https://example.com/terms',
privacyLink: 'https://example.com/privacy',
hasEmbeddedLinks: true,
hasEmbeddedPhone: false,
isAgeGated: false,
Expand Down Expand Up @@ -289,6 +292,8 @@ await ccai.campaigns.delete(campaign.id);

> Note: `MIXED` and `LOW_VOLUME_MIXED` campaigns require 2–3 `subUseCases`.

> `termsLink` and `privacyLink` are optional fields on `CampaignData`/`CampaignResponse`.

#### Sub-Use Cases

`TWO_FACTOR_AUTHENTICATION`, `ACCOUNT_NOTIFICATION`, `CUSTOMER_CARE`, `DELIVERY_NOTIFICATION`, `FRAUD_ALERT`, `MARKETING`, `POLLING_VOTING`
Expand Down Expand Up @@ -418,20 +423,15 @@ const webhookConfig: WebhookConfig = {
// secret is optional - if not provided, server generates one automatically
// method?: string (default 'POST')
// integrationType?: string (e.g. 'REST')
// events?: WebhookEventType[]
};
const webhook = await ccai.webhook.register(webhookConfig);
console.log('Webhook registered with ID:', webhook.id);
console.log('Secret Key:', webhook.secretKey); // Save this securely!

// Example 2: Register with custom secret and event types
// Example 2: Register with a custom secret
const webhookCustomConfig: WebhookConfig = {
url: 'https://your-app.com/api/custom-webhook',
secret: 'your-custom-secret', // optional - user-provided secret
events: [
WebhookEventType.MESSAGE_SENT,
WebhookEventType.MESSAGE_RECEIVED
]
secret: 'your-custom-secret' // optional - user-provided secret
};
const webhookCustom = await ccai.webhook.register(webhookCustomConfig);
console.log('Custom secret webhook registered:', webhookCustom.id);
Expand All @@ -445,8 +445,7 @@ webhooks.forEach(wh => {

// Update a webhook
const updateConfig: WebhookConfig = {
url: 'https://your-app.com/api/new-webhook',
events: [WebhookEventType.MESSAGE_SENT]
url: 'https://your-app.com/api/new-webhook'
};
const updated = await ccai.webhook.update(webhook.id, updateConfig);
console.log('Updated webhook URL:', updated.url);
Expand All @@ -456,7 +455,7 @@ await ccai.webhook.delete(webhook.id);

// Verify webhook signature (in your incoming request handler)
const signature = req.headers['x-ccai-signature'] as string;
const clientId = ccai.clientId;
const clientId = ccai.getClientId();
const eventHash = req.body.eventHash as string; // From the webhook payload
const secret = 'your-webhook-secret';
const isValid = ccai.webhook.verifySignature(signature, clientId, eventHash, secret);
Expand All @@ -483,6 +482,8 @@ CloudContactAI supports the following webhook event types (available via `Webhoo
| `MESSAGE_ERROR_CARRIER` | `message.error.carrier` | Carrier-side delivery error |
| `MESSAGE_ERROR_CLOUDCONTACT` | `message.error.cloudcontact` | Platform-side delivery error |

`createWebhookHandler` (below) provides typed callbacks for `MESSAGE_SENT` and `MESSAGE_RECEIVED`. For the other four event types, read `eventType` from the parsed payload — either in the "Simple Webhook Handler" pattern below, or by calling `ccai.webhook.parseEvent()` on the raw request body in your own route handler.

#### Event Payload Schema

**Message Sent Event:**
Expand Down Expand Up @@ -521,15 +522,14 @@ CloudContactAI supports the following webhook event types (available via `Webhoo

#### Using Webhooks with Next.js

`createWebhookHandler` routes `message.sent` and `message.received` events to `onMessageSent`/`onMessageReceived`. It does not verify the request signature — for signature verification, use the manual pattern in [Simple Webhook Handler](#simple-webhook-handler) below, or call `ccai.webhook.verifySignature(...)` yourself before your own routing logic.

```typescript
// pages/api/ccai-webhook.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { createWebhookHandler, WebhookEventType, type WebhookEvent } from 'ccai-node';

export default createWebhookHandler({
// Optional: Secret for verifying webhook signatures
secret: process.env.CCAI_WEBHOOK_SECRET,

// Handler for outbound messages (type-safe)
onMessageSent: async (event: WebhookEvent) => {
console.log('Message sent event received:');
Expand Down Expand Up @@ -646,6 +646,8 @@ async function sendMessages() {
"+15559876543",
"Hi ${firstName}!",
"Test Campaign",
undefined, // customData (optional)
undefined, // senderPhone (optional)
options
);
} catch (error) {
Expand Down
5 changes: 2 additions & 3 deletions src/email/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const response = await ccai.email.sendSingle(
'john@example.com', // Email address
'Welcome to Our Service', // Subject
'<p>Hello John,</p><p>Welcome!</p>', // HTML message content
undefined, // textContent (optional plain-text alternative)
'noreply@yourcompany.com', // Sender email
'support@yourcompany.com', // Reply-to email
'Your Company', // Sender name
Expand Down Expand Up @@ -161,9 +162,7 @@ The `EmailOptions` type represents optional settings for email operations:

```typescript
type EmailOptions = {
timeout?: number; // Optional timeout in milliseconds
retries?: number; // Optional retry count for failed requests
onProgress?: (status: string) => void; // Optional callback for tracking progress
onProgress?: (status: string) => void; // Callback for tracking send progress
};
```

Expand Down
1 change: 1 addition & 0 deletions src/email_send.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ async function sendEmail() {
'andreas@allcode.com',
'Test Email Subject',
'<p>Hello ${firstName},</p><p>This is a test email.</p><p>Thanks,<br>AllCode Team</p>',
undefined, // textContent (optional plain-text alternative)
'noreply@allcode.com',
'support@allcode.com',
'AllCode Team',
Expand Down
2 changes: 2 additions & 0 deletions src/examples/email-examples.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ async function sendSingleEmail() {
'andreas@allcode.com',
'Welcome to Our Service',
'<p>Hello Andreas,</p><p>Thank you for signing up for our service!</p><p>Best regards,<br>AllCode Team</p>',
undefined, // textContent (optional plain-text alternative)
'noreply@allcode.com',
'support@allcode.com',
'AllCode',
Expand Down Expand Up @@ -184,6 +185,7 @@ async function sendHtmlTemplateEmail() {
'john@example.com',
'Welcome to Our Platform',
htmlTemplate,
undefined, // textContent (optional plain-text alternative)
'welcome@yourcompany.com',
'support@yourcompany.com',
'Your Company',
Expand Down
119 changes: 70 additions & 49 deletions src/webhook/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,73 +4,86 @@ This module provides functionality for integrating with CloudContactAI's webhook

## Webhook Events

CloudContactAI currently supports the following webhook events:
CloudContactAI supports the following webhook event types, available via the `WebhookEventType` enum (`src/webhook/types.ts`):

1. **Message Sent (Outbound)** - Triggered when a message is sent from your CloudContactAI account
2. **Message Received (Inbound)** - Triggered when a message is received by your CloudContactAI account
| Event | Value | Description |
|---|---|---|
| `MESSAGE_SENT` | `message.sent` | Outbound message sent from your account |
| `MESSAGE_RECEIVED` | `message.received` | Inbound message received by your account |
| `MESSAGE_INCOMING` | `message.incoming` | Incoming message before processing |
| `MESSAGE_EXCLUDED` | `message.excluded` | Message excluded (e.g. opted-out contact) |
| `MESSAGE_ERROR_CARRIER` | `message.error.carrier` | Carrier-side delivery error |
| `MESSAGE_ERROR_CLOUDCONTACT` | `message.error.cloudcontact` | Platform-side delivery error |

`createWebhookHandler` (see below) provides typed callbacks for `MESSAGE_SENT` and `MESSAGE_RECEIVED`. To handle the other four event types, call `ccai.webhook.parseEvent()` on the raw request body and switch on `eventType` yourself.

## Event Payload Schema

Every event uses the same shape (`WebhookEvent` type in `src/webhook/types.ts`):

```typescript
type WebhookEvent = {
eventType: string; // e.g. "message.sent" | "message.received"
eventHash: string; // used for signature verification, see below
data: Record<string, unknown>; // event-specific payload
};
```

### Message Sent Event

```json
{
"type": "message.sent",
"campaign": {
"id": 123,
"title": "Default Campaign",
"message": "",
"senderPhone": "+11234567894",
"createdAt": "2025-07-14 22:18:28.273",
"runAt": ""
},
"from": "+11234567894",
"to": "+11453215437",
"message": "this is a test message for Jon Doe"
"eventType": "message.sent",
"eventHash": "abc123def456ghi789",
"data": {
"To": "+15551234567",
"From": "+15551234567",
"Message": "Hello John, this is a test message",
"TotalPrice": "0.01",
"Segments": 1,
"CampaignId": "123",
"CampaignTitle": "Test Campaign"
}
}
```

### Message Received Event

```json
{
"type": "message.received",
"campaign": {
"id": 123,
"title": "Default Campaign",
"message": "",
"senderPhone": "+11234567894",
"createdAt": "2025-07-14 22:18:28.273",
"runAt": ""
},
"from": "+11453215437",
"to": "+11234567894",
"message": "this is a reply message from Jon Doe"
"eventType": "message.received",
"eventHash": "xyz789abc123def456",
"data": {
"To": "+15551234567",
"From": "+15559876543",
"Message": "Reply from customer",
"TotalPrice": "0.01",
"Segments": 1,
"CampaignId": "123",
"CampaignTitle": "Test Campaign"
}
}
```

## Usage with Next.js

The ccai-node library provides a convenient utility for handling webhooks in Next.js applications:
The ccai-node library provides a convenient utility for handling webhooks in Next.js applications. `createWebhookHandler` does not verify the request signature itself — for that, use the manual handling pattern in [Manual Webhook Handling](#manual-webhook-handling) below.

```typescript
// pages/api/ccai-webhook.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { createWebhookHandler, WebhookEventType } from 'ccai-node';
import { createWebhookHandler, WebhookEventType, type WebhookEvent } from 'ccai-node';

export default createWebhookHandler({
// Optional: Secret for verifying webhook signatures
secret: process.env.CCAI_WEBHOOK_SECRET,

// Handler for outbound messages
onMessageSent: async (event) => {
console.log('Message sent:', event);
onMessageSent: async (event: WebhookEvent) => {
console.log('Message sent:', event.eventType, event.data);
// Process outbound message event
},

// Handler for inbound messages
onMessageReceived: async (event) => {
console.log('Message received:', event);
onMessageReceived: async (event: WebhookEvent) => {
console.log('Message received:', event.eventType, event.data);
// Process inbound message event
}
});
Expand All @@ -89,10 +102,6 @@ const ccai = new CCAI({
// Register a new webhook
const webhook = await ccai.webhook.register({
url: 'https://your-app.com/api/ccai-webhook',
events: [
WebhookEventType.MESSAGE_SENT,
WebhookEventType.MESSAGE_RECEIVED
],
secret: 'your-webhook-secret' // Optional but recommended for security
});
```
Expand All @@ -101,25 +110,37 @@ const webhook = await ccai.webhook.register({

For production use, it's recommended to use a webhook secret to verify that webhook requests are coming from CloudContactAI. The secret is used to create a signature that is sent with each webhook request in the `X-CCAI-Signature` header.

When you configure your webhook in the CloudContactAI interface (Settings -> Integrations), you can set a secret. This same secret should be used when setting up your webhook handler.
When you configure your webhook in the CloudContactAI interface (Settings -> Integrations), you can set a secret. This same secret should be used when setting up your webhook handler, passed to `ccai.webhook.verifySignature(signature, clientId, eventHash, secret)`.

## Manual Webhook Handling

If you prefer to handle webhooks manually without using the provided utilities:

```typescript
import { CCAI } from 'ccai-node';

const ccai = new CCAI({
clientId: process.env.CCAI_CLIENT_ID!,
apiKey: process.env.CCAI_API_KEY!
});

export default function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method === 'POST') {
const payload = req.body;
console.log('Webhook payload:', payload);

// Process the webhook based on its type
if (payload.type === 'message.sent') {
const payload = req.body as { eventType: string; eventHash: string; data: Record<string, unknown> };

const signature = req.headers['x-ccai-signature'] as string;
const secret = process.env.CCAI_WEBHOOK_SECRET!;
if (!ccai.webhook.verifySignature(signature, process.env.CCAI_CLIENT_ID!, payload.eventHash, secret)) {
return res.status(401).json({ error: 'Invalid signature' });
}

// Process the webhook based on its eventType
if (payload.eventType === 'message.sent') {
// Handle outbound message event
} else if (payload.type === 'message.received') {
} else if (payload.eventType === 'message.received') {
// Handle inbound message event
}

// Always respond with a 200 status code to acknowledge receipt
res.status(200).json({ received: true });
} else {
Expand Down
21 changes: 9 additions & 12 deletions test-webhook.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,15 @@ const http = require('http');

// Test webhook payload
const testPayload = {
type: 'message.sent',
campaign: {
id: 123,
title: 'Test Campaign',
message: '',
senderPhone: '+11234567894',
createdAt: '2025-01-14 22:18:28.273',
runAt: ''
},
from: '+11234567894',
to: '+15551234567',
message: 'Hello John Doe, this is a test message!'
eventType: 'message.sent',
eventHash: 'test-event-hash-1234567890',
data: {
From: '+11234567894',
To: '+15551234567',
Message: 'Hello John Doe, this is a test message!',
CampaignId: '123',
CampaignTitle: 'Test Campaign'
}
};

const data = JSON.stringify(testPayload);
Expand Down
10 changes: 5 additions & 5 deletions webhook-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ app.post('/webhook', (req, res) => {
const payload = req.body;

console.log('Webhook received:', payload);

// Handle different event types
if (payload.type === 'message.sent') {
console.log(`Message sent from ${payload.from} to ${payload.to}: ${payload.message}`);
} else if (payload.type === 'message.received') {
console.log(`Message received from ${payload.from} to ${payload.to}: ${payload.message}`);
if (payload.eventType === 'message.sent') {
console.log(`Message sent from ${payload.data?.From} to ${payload.data?.To}: ${payload.data?.Message}`);
} else if (payload.eventType === 'message.received') {
console.log(`Message received from ${payload.data?.From} to ${payload.data?.To}: ${payload.data?.Message}`);
}

// Always respond with 200
Expand Down
Loading