Outbound Communications
Enterprise-grade notifications without the infrastructure burden.
The problem you're not solving anymore
Building a production notification system typically means:
- Weeks of provider integration — SMTP configuration, SMS gateway contracts, telephony stack setup
- Template management sprawl — Hardcoded strings, no version control, developer bottlenecks for copy changes
- Reliability engineering — Retry logic, queue management, rate limiting, failure handling
- Multi-channel fragmentation — Different APIs, different patterns, different monitoring for each channel
- Compliance overhead — Audit trails, unsubscribe handling, delivery confirmation, GDPR considerations
The WIIL Outbound Communications system eliminates this entire layer. Your application sends a single API call. The platform handles everything else.
SDK parity matrix
This reflects what the SDK example guides demonstrate for each language.
| Area | TypeScript | Python |
|---|---|---|
| Unified send (email / SMS / call) | ✓ | ✓ |
| Templates — create + render | ✓ | ✓ |
| Templates — get, list, update, activate, deactivate, delete | ✓ | — |
| Email — send with template | ✓ | ✓ |
| Email — direct content, multiple recipients, operations | ✓ | — |
| SMS — send with template | ✓ | ✓ |
| SMS — direct content, operations | ✓ | — |
| Calls — immediate, scheduled | ✓ | ✓ |
| Calls — operations | ✓ | — |
| Status — query by status | ✓ | ✓ |
| Use cases — transactional, reminders, scheduled calls | ✓ | ✓ |
| Use cases — verification, reservation confirmations | ✓ | — |
What you get
Unified multi-channel delivery
One SDK, one pattern, three channels.
- TypeScript
- Python
// Email
await client.outboundEmails.create({ to: [...], templateId, templateVariables });
// SMS
await client.outboundSms.create({ to, templateId, templateVariables });
// Voice Call
await client.outboundCalls.create({ to, agentConfigurationId });
import os
from wiil import WiilClient
from wiil.models.conversation import (
CreateCallRequest,
CreateEmailRequest,
CreateSmsRequest,
EmailRecipient,
)
from wiil.types import ScheduleType
client = WiilClient(api_key=os.environ["WIIL_API_KEY"])
email = client.outbound_emails.create(
CreateEmailRequest(
to=[EmailRecipient(email="customer@example.com")],
template_id="order_confirmation",
subject="Order confirmed",
body_html="<p>Your order {{orderNumber}} is confirmed.</p>",
body_text="Your order {{orderNumber}} is confirmed.",
variables={"orderNumber": "ORD-5521"},
)
)
sms = client.outbound_sms.create(
CreateSmsRequest(
to="+12125551234",
from_number="+12125559999",
template_id="appointment_reminder",
body="Reminder: {{serviceName}} at {{time}}.",
variables={"serviceName": "Hair Styling", "time": "3:00 PM"},
)
)
call = client.outbound_calls.create(
CreateCallRequest(
to="+12125551234",
from_number="+12125559999",
agent_configuration_id="appointment_confirmation_agent",
schedule_type=ScheduleType.IMMEDIATE,
)
)
Centralized template management
Templates live in the platform, not your codebase. Create once; reuse everywhere.
- TypeScript
- Python
// Create once
await client.outboundTemplates.createEmailTemplate({
name: 'Order Shipped',
code: 'order_shipped',
subjectTemplate: 'Your order #{{orderNumber}} is on the way',
bodyHtmlTemplate: '<h1>{{customerName}}, your order shipped!</h1>...',
variables: [
{ key: 'customerName', required: true },
{ key: 'orderNumber', required: true },
],
});
// Use everywhere
await client.outboundEmails.create({
to: [{ email: customer.email }],
templateId: 'order_shipped',
templateVariables: { customerName: 'Jane', orderNumber: 'ORD-5521' },
});
from wiil.models.conversation import CreateEmailTemplate, TemplateVariable
template = client.outbound_templates.create_email_template(
CreateEmailTemplate(
name="Order Shipped",
code="order_shipped",
subject_template="Your order {{orderNumber}} is on the way",
body_html_template="<p>{{customerName}}, your order shipped.</p>",
variables=[
TemplateVariable(key="customerName", required=True),
TemplateVariable(key="orderNumber", required=True),
],
)
)
rendered = client.outbound_templates.render(
template.id,
{"customerName": "Jane", "orderNumber": "ORD-5521"},
)
Production infrastructure — already built
| Concern | Platform responsibility |
|---|---|
| Delivery | Provider failover, retry with exponential backoff |
| Queuing | High-volume batching, rate limiting, priority handling |
| Scheduling | Future sends, timezone-aware delivery windows |
| Tracking | Delivery status, bounce handling, engagement metrics |
| Compliance | Audit logs, unsubscribe management, data retention |
| Monitoring | Delivery dashboards, failure alerts, channel health |
You call .create(). The platform ensures it arrives.
Use cases
Transactional notifications
- TypeScript
- Python
// Order confirmation — immediate
await client.outboundEmails.create({
to: [{ email: order.customerEmail }],
templateId: 'order_confirmation',
templateVariables: {
orderNumber: order.id,
total: order.total,
items: order.items.map(i => i.name).join(', '),
},
});
// Shipping update — with tracking
await client.outboundSms.create({
to: order.customerPhone,
from: businessPhone,
templateId: 'shipping_update',
templateVariables: {
trackingUrl: shipment.trackingUrl,
},
});
client.outbound_emails.create(
CreateEmailRequest(
to=[EmailRecipient(email=order.customer_email, name=order.customer_name)],
template_id="order_confirmation",
subject="Order {{orderNumber}} confirmed",
body_html="<p>Total: {{total}}</p>",
body_text="Total: {{total}}",
variables={
"orderNumber": order.id,
"total": order.total,
},
)
)
Appointment reminders
- TypeScript
- Python
// 24-hour reminder via SMS
await client.outboundSms.create({
to: appointment.customerPhone,
from: businessPhone,
templateId: 'appointment_reminder_24h',
templateVariables: {
serviceName: appointment.serviceName,
dateTime: formatDateTime(appointment.startsAt),
location: appointment.location,
},
});
// Same-day confirmation call
await client.outboundCalls.create({
to: appointment.customerPhone,
from: businessPhone,
agentConfigurationId: 'appointment_confirmation_agent',
scheduleType: 'scheduled',
scheduledAt: appointment.startsAt - (2 * 60 * 60 * 1000), // 2 hours before
});
client.outbound_sms.create(
CreateSmsRequest(
to=appointment.customer_phone,
from_number=business_phone,
template_id="appointment_reminder_24h",
body="{{serviceName}} is scheduled for {{dateTime}}.",
variables={
"serviceName": appointment.service_name,
"dateTime": appointment.display_time,
},
)
)
Scheduled / proactive calls
AI-powered outbound calls for follow-ups, surveys, and re-engagement.
- TypeScript
- Python
// Post-service follow-up call
await client.outboundCalls.create({
to: customer.phone,
from: businessPhone,
agentConfigurationId: 'satisfaction_survey_agent',
scheduleType: 'scheduled',
scheduledAt: serviceCompletedAt + (24 * 60 * 60), // Next day (seconds)
maxDuration: 300, // 5 minutes
maxRetries: 2,
});
import time
client.outbound_calls.create(
CreateCallRequest(
to=customer.phone,
from_number=business_phone,
agent_configuration_id="satisfaction_survey_agent",
schedule_type=ScheduleType.SCHEDULED,
scheduled_at=int(time.time() * 1000) + 24 * 60 * 60 * 1000,
max_duration=300,
max_retries=2,
)
)
Verification & security (TypeScript)
Two-factor authentication without third-party dependencies:
const code = generateVerificationCode();
await client.outboundSms.create({
to: user.phone,
from: businessPhone,
templateId: 'verification_code',
templateVariables: { code },
});
// Template: "Your verification code is {{code}}. Valid for 10 minutes."
Reservation confirmations (TypeScript)
Multi-channel confirmation for bookings:
// Email with full details
await client.outboundEmails.create({
to: [{ email: guest.email, name: guest.name }],
templateId: 'reservation_confirmed',
templateVariables: {
guestName: guest.name,
checkIn: reservation.checkIn,
checkOut: reservation.checkOut,
roomType: reservation.roomType,
confirmationCode: reservation.code,
},
});
// SMS with essential info
await client.outboundSms.create({
to: guest.phone,
from: businessPhone,
templateId: 'reservation_confirmed_sms',
templateVariables: {
confirmationCode: reservation.code,
checkIn: reservation.checkIn,
},
});
Architecture
┌─────────────────────────────────────────────────────────────────────┐
│ YOUR APPLICATION │
│ │
│ client.outboundEmails.create() ← Single API call │
│ client.outboundSms.create() │
│ client.outboundCalls.create() │
└───────────────────────────────┬─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ WIIL PLATFORM LAYER │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌────────────┐ │
│ │ Template │ │ Queue │ │ Retry │ │ Status │ │
│ │ Rendering │ │ Management │ │ Logic │ │ Tracking │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └────────────┘ │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌────────────┐ │
│ │ Rate │ │ Provider │ │ Delivery │ │ Audit │ │
│ │ Limiting │ │ Failover │ │ Optimization│ │ Logs │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └────────────┘ │
└───────────────────────────────┬─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ PROVIDER INTEGRATIONS │
│ │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │ Email │ │ SMS │ │ Telephony │ │
│ │ Providers │ │ Gateways │ │ Stack │ │
│ └───────────┘ └───────────┘ └───────────┘ │
│ │
│ SendGrid, SES, Twilio, Vonage, SIP, PSTN, │
│ Mailgun, etc. MessageBird WebRTC │
└─────────────────────────────────────────────────────────────────────┘
You own the top layer. The platform owns everything else.
Implementation reference
Setup
- TypeScript
- Python
import { WiilClient } from 'wiil-js';
import {
OutboundTemplateChannel,
CallRequestStatus,
EmailStatus,
SmsStatus
} from 'wiil-core-js';
const client = new WiilClient({ apiKey: process.env.WIIL_API_KEY! });
import os
from wiil import WiilClient
from wiil.models.conversation import (
CreateCallRequest,
CreateEmailRequest,
CreateSmsRequest,
EmailRecipient,
)
from wiil.types import ScheduleType
client = WiilClient(api_key=os.environ["WIIL_API_KEY"])
Templates — creation
- TypeScript
- Python
const template = await client.outboundTemplates.createEmailTemplate({
name: 'Welcome Email',
code: 'welcome',
channel: OutboundTemplateChannel.EMAIL,
subjectTemplate: 'Welcome to {{companyName}}, {{firstName}}!',
bodyHtmlTemplate: `
<h1>Welcome, {{firstName}}!</h1>
<p>Thank you for joining {{companyName}}.</p>
`,
bodyTextTemplate: 'Welcome, {{firstName}}! Thank you for joining {{companyName}}.',
variables: [
{ key: 'firstName', required: true },
{ key: 'companyName', required: true },
],
});
const smsTemplate = await client.outboundTemplates.createSmsTemplate({
name: 'Verification Code',
code: 'verify',
channel: 'SMS',
bodyTemplate: 'Your {{companyName}} code is {{code}}. Valid for 10 minutes.',
variables: [
{ key: 'code', required: true },
{ key: 'companyName', required: true },
],
});
from wiil.models.conversation import CreateEmailTemplate, TemplateVariable
template = client.outbound_templates.create_email_template(
CreateEmailTemplate(
name="Order Shipped",
code="order_shipped",
subject_template="Your order {{orderNumber}} is on the way",
body_html_template="<p>{{customerName}}, your order shipped.</p>",
variables=[
TemplateVariable(key="customerName", required=True),
TemplateVariable(key="orderNumber", required=True),
],
)
)
rendered = client.outbound_templates.render(
template.id,
{"customerName": "Jane", "orderNumber": "ORD-5521"},
)
Templates — operations (TypeScript)
// Get by ID
const template = await client.outboundTemplates.get('template_123');
// Get by code (useful for deployment-independent references)
const template = await client.outboundTemplates.getByCode('welcome');
// Get all templates for a channel
const emailTemplates = await client.outboundTemplates.getByChannel(
OutboundTemplateChannel.EMAIL
);
// List all templates
const all = await client.outboundTemplates.list();
// Update email template
await client.outboundTemplates.updateEmailTemplate({
id: template.id,
subjectTemplate: 'Updated: Welcome to {{companyName}}!',
});
// Update SMS template
await client.outboundTemplates.updateSmsTemplate({
id: template.id,
bodyTemplate: 'Your code: {{code}}. Expires in 5 min.',
});
// Activate/deactivate (inactive templates cannot be used)
await client.outboundTemplates.activate(template.id);
await client.outboundTemplates.deactivate(template.id);
// Preview with test data
const preview = await client.outboundTemplates.render(template.id, {
firstName: 'Test',
companyName: 'Acme Corp',
});
// Delete
await client.outboundTemplates.delete(template.id);
Emails — send with template
- TypeScript
- Python
const result = await client.outboundEmails.create({
to: [{ email: 'customer@example.com', name: 'Jane Doe' }],
templateId: 'order_confirmation',
templateVariables: {
customerName: 'Jane',
orderNumber: 'ORD-12345',
total: '149.99',
},
});
email = client.outbound_emails.create(
CreateEmailRequest(
to=[EmailRecipient(email="customer@example.com", name="John Smith")],
template_id="order_shipped",
subject="Your order {{orderNumber}} has shipped",
body_html="<p>Track your order here: {{trackingUrl}}</p>",
body_text="Track your order: {{trackingUrl}}",
variables={
"orderNumber": "ORD-12345",
"trackingUrl": "https://track.example.com/12345",
},
)
)
Emails — direct content & operations (TypeScript)
// Direct content
const result = await client.outboundEmails.create({
to: [{ email: 'customer@example.com', name: 'Jane Doe' }],
subject: 'Your receipt',
bodyHtml: '<h1>Thank you for your purchase!</h1>',
bodyText: 'Thank you for your purchase!',
replyTo: 'support@example.com',
});
// Multiple recipients
const result2 = await client.outboundEmails.create({
to: [
{ email: 'alice@example.com', name: 'Alice' },
{ email: 'bob@example.com', name: 'Bob' },
],
cc: [{ email: 'manager@example.com' }],
bcc: [{ email: 'archive@example.com' }],
templateId: 'team_update',
templateVariables: { ... },
});
// Operations
const email = await client.outboundEmails.get('email_123');
const queued = await client.outboundEmails.getByStatus(EmailStatus.QUEUED);
const sent = await client.outboundEmails.getByStatus(EmailStatus.SENT);
const fromTemplate = await client.outboundEmails.getByTemplate('template_123');
const nowSec = Math.floor(Date.now() / 1000);
const recent = await client.outboundEmails.getByDateRange(nowSec - (7 * 24 * 60 * 60), nowSec);
const all = await client.outboundEmails.list();
await client.outboundEmails.update({ id: email.id, subject: 'Updated subject' });
await client.outboundEmails.cancel(email.id, 'Content needs revision');
await client.outboundEmails.delete(email.id);
SMS — send with template
- TypeScript
- Python
const result = await client.outboundSms.create({
to: '+14155551234',
from: '+14155555678',
templateId: 'verification_code',
templateVariables: { code: '847291' },
});
sms = client.outbound_sms.create(
CreateSmsRequest(
to="+12125551234",
from_number="+12125559999",
template_id="appointment_reminder",
body="Reminder: {{serviceName}} at {{time}}.",
variables={"serviceName": "Hair Styling", "time": "3:00 PM"},
)
)
SMS — direct content & operations (TypeScript)
// Direct content
const result = await client.outboundSms.create({
to: '+14155551234',
from: '+14155555678',
body: 'Your appointment is confirmed for tomorrow at 2 PM.',
maxRetries: 2,
});
// Operations
const sms = await client.outboundSms.get('sms_123');
const queued = await client.outboundSms.getByStatus(SmsStatus.QUEUED);
const toNumber = await client.outboundSms.getByRecipient('+14155551234');
const fromTemplate = await client.outboundSms.getByTemplate('template_123');
const recent = await client.outboundSms.getByDateRange(startDate, endDate);
const all = await client.outboundSms.list();
await client.outboundSms.update({ id: sms.id, body: 'Updated message' });
await client.outboundSms.cancel(sms.id, 'Wrong recipient');
await client.outboundSms.delete(sms.id);
Calls — immediate
- TypeScript
- Python
const result = await client.outboundCalls.create({
to: '+14155551234',
from: '+14155555678',
agentConfigurationId: 'support_agent',
scheduleType: 'immediate',
maxDuration: 300, // 5 minutes
maxRetries: 2,
});
call = client.outbound_calls.create(
CreateCallRequest(
to="+12125551234",
from_number="+12125559999",
agent_configuration_id="agent_456",
schedule_type=ScheduleType.IMMEDIATE,
)
)
Calls — scheduled
- TypeScript
- Python
const result = await client.outboundCalls.create({
to: '+14155551234',
from: '+14155555678',
agentConfigurationId: 'survey_agent',
scheduleType: 'scheduled',
scheduledAt: Math.floor(Date.now() / 1000) + (2 * 60 * 60), // 2 hours from now
maxDuration: 600,
maxRetries: 3,
});
import time
call = client.outbound_calls.create(
CreateCallRequest(
to=customer.phone,
from_number=business_phone,
agent_configuration_id="satisfaction_survey_agent",
schedule_type=ScheduleType.SCHEDULED,
scheduled_at=int(time.time() * 1000) + 24 * 60 * 60 * 1000,
max_duration=300,
max_retries=2,
)
)
Calls — operations (TypeScript)
const call = await client.outboundCalls.get('call_123');
const agentCalls = await client.outboundCalls.getByAgent('agent_config_123');
const pending = await client.outboundCalls.getByStatus(CallRequestStatus.PENDING);
const completed = await client.outboundCalls.getByStatus(CallRequestStatus.COMPLETED);
const recent = await client.outboundCalls.getByDateRange(startDate, endDate);
const all = await client.outboundCalls.list();
await client.outboundCalls.update({ id: call.id, scheduledAt: newTime });
await client.outboundCalls.cancel(call.id, 'Customer requested cancellation');
await client.outboundCalls.delete(call.id);
Status tracking
- TypeScript
- Python
import { CallRequestStatus, EmailStatus, SmsStatus } from 'wiil-core-js';
const pendingCalls = await client.outboundCalls.getByStatus(CallRequestStatus.PENDING);
const queuedEmails = await client.outboundEmails.getByStatus(EmailStatus.QUEUED);
const queuedSms = await client.outboundSms.getByStatus(SmsStatus.QUEUED);
from wiil.types import CallRequestStatus, EmailStatus, PaginationRequest, SmsStatus
pending_calls = client.outbound_calls.get_by_status(
CallRequestStatus.PENDING,
PaginationRequest(page=1, page_size=20),
)
queued_emails = client.outbound_emails.get_by_status(
EmailStatus.QUEUED,
PaginationRequest(page=1, page_size=20),
)
queued_sms = client.outbound_sms.get_by_status(
SmsStatus.QUEUED,
PaginationRequest(page=1, page_size=20),
)
Status enums (TypeScript)
import {
OutboundTemplateChannel,
CallRequestStatus,
EmailStatus,
SmsStatus
} from 'wiil-core-js';
// Template channels
OutboundTemplateChannel.EMAIL
OutboundTemplateChannel.SMS
// Call statuses
CallRequestStatus.PENDING
CallRequestStatus.SCHEDULED
CallRequestStatus.IN_PROGRESS
CallRequestStatus.COMPLETED
CallRequestStatus.FAILED
CallRequestStatus.CANCELLED
// Email statuses
EmailStatus.QUEUED
EmailStatus.SENDING
EmailStatus.SENT
EmailStatus.DELIVERED
EmailStatus.FAILED
EmailStatus.BOUNCED
EmailStatus.CANCELLED
// SMS statuses
SmsStatus.QUEUED
SmsStatus.SENDING
SmsStatus.SENT
SmsStatus.DELIVERED
SmsStatus.FAILED
SmsStatus.CANCELLED
Complete example: multi-channel notification system (TypeScript)
import { WiilClient } from 'wiil-js';
import { OutboundTemplateChannel } from 'wiil-core-js';
async function initializeNotificationSystem() {
const client = new WiilClient({ apiKey: process.env.WIIL_API_KEY! });
// 1. Create your template library
const templates = {
orderConfirmation: await client.outboundTemplates.createEmailTemplate({
name: 'Order Confirmation',
code: 'order_confirmation',
channel: OutboundTemplateChannel.EMAIL,
subjectTemplate: 'Order #{{orderNumber}} Confirmed',
bodyHtmlTemplate: `
<h1>Thank you, {{customerName}}!</h1>
<p>Your order #{{orderNumber}} is confirmed.</p>
<p><strong>Total:</strong> ${{total}}</p>
`,
bodyTextTemplate: 'Order #{{orderNumber}} confirmed. Total: ${{total}}',
variables: [
{ key: 'customerName', required: true },
{ key: 'orderNumber', required: true },
{ key: 'total', required: true },
],
}),
shippingUpdate: await client.outboundTemplates.createSmsTemplate({
name: 'Shipping Update',
code: 'shipping_update',
channel: 'SMS',
bodyTemplate: 'Your order #{{orderNumber}} shipped! Track: {{trackingUrl}}',
variables: [
{ key: 'orderNumber', required: true },
{ key: 'trackingUrl', required: true },
],
}),
verificationCode: await client.outboundTemplates.createSmsTemplate({
name: 'Verification Code',
code: 'verification',
channel: 'SMS',
bodyTemplate: 'Your code is {{code}}. Valid for 10 minutes.',
variables: [{ key: 'code', required: true }],
}),
};
// 2. Activate all templates
await Promise.all(
Object.values(templates).map(t => client.outboundTemplates.activate(t.id))
);
console.log('Notification system initialized');
return templates;
}
// Usage in your application
async function onOrderPlaced(order: Order) {
// Email confirmation
await client.outboundEmails.create({
to: [{ email: order.customer.email, name: order.customer.name }],
templateId: 'order_confirmation',
templateVariables: {
customerName: order.customer.name,
orderNumber: order.id,
total: order.total.toFixed(2),
},
});
}
async function onOrderShipped(order: Order, shipment: Shipment) {
// SMS notification
await client.outboundSms.create({
to: order.customer.phone,
from: BUSINESS_PHONE,
templateId: 'shipping_update',
templateVariables: {
orderNumber: order.id,
trackingUrl: shipment.trackingUrl,
},
});
}
async function sendVerificationCode(phone: string, code: string) {
await client.outboundSms.create({
to: phone,
from: BUSINESS_PHONE,
templateId: 'verification',
templateVariables: { code },
});
}
Best practices
Use template codes, not IDs
Template IDs change between environments. Codes are stable:
// Fragile — ID differs per environment
templateId: 'tmpl_abc123'
// Robust — code is consistent
const template = await client.outboundTemplates.getByCode('order_confirmation');
await client.outboundEmails.create({ templateId: template.id, ... });
Validate templates before go-live
const preview = await client.outboundTemplates.render('order_confirmation', {
customerName: 'Test User',
orderNumber: 'TEST-001',
total: '99.99',
});
Handle status appropriately
const result = await client.outboundEmails.create({ ... });
// The message is QUEUED, not SENT
// Use webhooks or polling for delivery confirmation
if (result.request?.status === 'QUEUED') {
console.log(`Email queued: ${result.request.id}`);
}
Phone number format
Always use E.164 format: +[country][number] (for example +14155551234).
Troubleshooting
| Issue | Cause | Solution |
|---|---|---|
| "Required variable not provided" | Missing template variable | Include all required variables in templateVariables / variables |
| "Template not found" | Template doesn't exist or inactive | Verify template exists and call activate() |
| "Invalid phone number" | Wrong format | Use E.164 format: +[country][number] |
| Email not delivered | Various | Check status via getByStatus() / get_by_status(), verify recipient |
Production checklist (Python guide)
- Store all phone numbers in E.164 format.
- Keep message templates in the platform when copy changes frequently.
- Persist outbound request IDs so you can reconcile delivery status.
- Configure retry counts deliberately per channel.
- Respect local consent, opt-out, and calling-hour rules.
- Use metadata to attach your own order, appointment, or campaign IDs.
Related
- Messaging Quick Start — the fast path to your first send