diff --git a/.env.example b/.env.example index e7f78e2..0694861 100644 --- a/.env.example +++ b/.env.example @@ -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 diff --git a/README.md b/README.md index 210ab02..738f94f 100644 --- a/README.md +++ b/README.md @@ -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( @@ -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. @@ -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, @@ -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` @@ -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); @@ -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); @@ -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); @@ -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:** @@ -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:'); @@ -646,6 +646,8 @@ async function sendMessages() { "+15559876543", "Hi ${firstName}!", "Test Campaign", + undefined, // customData (optional) + undefined, // senderPhone (optional) options ); } catch (error) { diff --git a/src/email/README.md b/src/email/README.md index cb6f33a..8bca1c1 100644 --- a/src/email/README.md +++ b/src/email/README.md @@ -32,6 +32,7 @@ const response = await ccai.email.sendSingle( 'john@example.com', // Email address 'Welcome to Our Service', // Subject '

Hello John,

Welcome!

', // HTML message content + undefined, // textContent (optional plain-text alternative) 'noreply@yourcompany.com', // Sender email 'support@yourcompany.com', // Reply-to email 'Your Company', // Sender name @@ -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 }; ``` diff --git a/src/email_send.ts b/src/email_send.ts index c05e92d..bb0841d 100644 --- a/src/email_send.ts +++ b/src/email_send.ts @@ -14,6 +14,7 @@ async function sendEmail() { 'andreas@allcode.com', 'Test Email Subject', '

Hello ${firstName},

This is a test email.

Thanks,
AllCode Team

', + undefined, // textContent (optional plain-text alternative) 'noreply@allcode.com', 'support@allcode.com', 'AllCode Team', diff --git a/src/examples/email-examples.ts b/src/examples/email-examples.ts index 9bb9ff8..ce15e9c 100644 --- a/src/examples/email-examples.ts +++ b/src/examples/email-examples.ts @@ -22,6 +22,7 @@ async function sendSingleEmail() { 'andreas@allcode.com', 'Welcome to Our Service', '

Hello Andreas,

Thank you for signing up for our service!

Best regards,
AllCode Team

', + undefined, // textContent (optional plain-text alternative) 'noreply@allcode.com', 'support@allcode.com', 'AllCode', @@ -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', diff --git a/src/webhook/README.md b/src/webhook/README.md index 9deb305..d4cf055 100644 --- a/src/webhook/README.md +++ b/src/webhook/README.md @@ -4,29 +4,46 @@ 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; // 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" + } } ``` @@ -34,43 +51,39 @@ CloudContactAI currently supports the following webhook events: ```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 } }); @@ -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 }); ``` @@ -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 }; + + 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 { diff --git a/test-webhook.js b/test-webhook.js index fbf69da..21f57d8 100644 --- a/test-webhook.js +++ b/test-webhook.js @@ -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); diff --git a/webhook-server.js b/webhook-server.js index 9ccae9d..fc2c4ca 100644 --- a/webhook-server.js +++ b/webhook-server.js @@ -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